const { onRequest } = require("firebase-functions/v2/https"); const admin = require("firebase-admin"); const OpenAI = require("openai"); const Stripe = require("stripe"); const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); const db = admin.firestore(); // Auth middleware function to validate tokens and extract user context const validateAuthAndGetContext = async (req) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new Error('Missing or invalid authorization header'); } const token = authHeader.substring(7); // Validate token format (should start with 'session_') if (!token.startsWith('session_')) { throw new Error('Invalid token format'); } // Look up token in Firestore const tokenDoc = await db.collection("authTokens").doc(token).get(); if (!tokenDoc.exists) { throw new Error('Token not found'); } const tokenData = tokenDoc.data(); if (!tokenData.isActive) { throw new Error('Token is inactive'); } if (Date.now() > tokenData.expiresAt) { throw new Error('Token has expired'); } // Update last used timestamp await tokenDoc.ref.update({ lastUsedAt: Date.now() }); // Get user's organizations const userOrgsSnapshot = await db.collection("users").doc(tokenData.userId).collection("organizations").get(); const orgIds = userOrgsSnapshot.docs.map(doc => doc.id); return { userId: tokenData.userId, orgIds: orgIds, // For backward compatibility, use first org as default orgId: orgIds[0] || null, token: token }; }; // Helper function to verify user has access to specific organization const verifyOrgAccess = (authContext, targetOrgId) => { if (!targetOrgId) { return authContext.orgId; // Use default org } if (!authContext.orgIds.includes(targetOrgId)) { throw new Error('Unauthorized access to organization'); } return targetOrgId; }; // Initialize OpenAI if API key is available const openai = process.env.OPENAI_API_KEY ? new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }) : null; // Initialize Stripe if API key is available const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2024-11-20.acacia', }) : null; const RESPONSE_FORMAT = { type: "json_schema", json_schema: { name: "company_artifacts", strict: true, schema: { type: "object", additionalProperties: false, properties: { companyPerformance: { type: "object", additionalProperties: false, properties: { summary: { type: "string" }, metrics: { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, value: { anyOf: [{ type: "string" }, { type: "number" }] }, trend: { enum: ["up", "down", "flat"] } }, required: ["name", "value", "trend"] } } }, required: ["summary", "metrics"] }, immediateHiringNeeds: { type: "array", items: { type: "object", additionalProperties: false, properties: { role: { type: "string" }, urgency: { enum: ["low", "medium", "high"] }, reason: { type: "string" } }, required: ["role", "urgency", "reason"] } }, forwardOperatingPlan: { type: "object", additionalProperties: false, properties: { nextQuarterObjectives: { type: "array", items: { type: "string" } }, initiatives: { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, owner: { type: "string" }, kpis: { type: "array", items: { type: "string" } } }, required: ["name", "owner", "kpis"] } }, risks: { type: "array", items: { type: "object", additionalProperties: false, properties: { risk: { type: "string" }, mitigation: { type: "string" } }, required: ["risk", "mitigation"] } } }, required: ["nextQuarterObjectives", "initiatives", "risks"] }, organizationalInsights: { type: "object", additionalProperties: false, properties: { culture: { type: "string" }, teamDynamics: { type: "string" }, blockers: { type: "array", items: { type: "string" } } }, required: ["culture", "teamDynamics", "blockers"] }, strengths: { type: "array", items: { type: "string" } }, }, required: ["companyPerformance", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths"] } } }; // Helper function to generate OTP const generateOTP = () => { return Math.floor(100000 + Math.random() * 900000).toString(); }; // Send OTP Function exports.sendOTP = onRequest({ cors: true }, async (req, res) => { if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { email, inviteCode } = req.body; if (!email) { return res.status(400).json({ error: "Email is required" }); } try { // Generate OTP const otp = generateOTP(); const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry // Store OTP in Firestore await db.collection("otps").doc(email).set({ otp, expiresAt, attempts: 0, inviteCode: inviteCode || null, createdAt: Date.now(), }); // In production, send actual email console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`); res.json({ success: true, message: "Verification code sent to your email", // Always include OTP in emulator mode for testing otp, }); } catch (error) { console.error("Send OTP error:", error); res.status(500).json({ error: "Failed to send verification code" }); } }); // Verify OTP Function exports.verifyOTP = onRequest({ cors: true }, async (req, res) => { if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { email, otp } = req.body; if (!email || !otp) { return res.status(400).json({ error: "Email and OTP are required" }); } try { // Retrieve OTP document const otpDoc = await db.collection("otps").doc(email).get(); if (!otpDoc.exists) { return res.status(400).json({ error: "Invalid verification code" }); } const otpData = otpDoc.data(); // Check if OTP is expired if (Date.now() > otpData.expiresAt) { await otpDoc.ref.delete(); return res.status(400).json({ error: "Verification code has expired" }); } // Check if too many attempts if (otpData.attempts >= 5) { await otpDoc.ref.delete(); return res.status(400).json({ error: "Too many failed attempts" }); } // Verify OTP if (otpData.otp !== otp) { await otpDoc.ref.update({ attempts: (otpData.attempts || 0) + 1, }); return res.status(400).json({ error: "Invalid verification code" }); } // OTP is valid - clean up and create/find user await otpDoc.ref.delete(); // Generate a unique user ID for this email if it doesn't exist let userId; let userDoc; // Check if user already exists by email const existingUserQuery = await db.collection("users") .where("email", "==", email) .limit(1) .get(); if (!existingUserQuery.empty) { // User exists, get their ID userDoc = existingUserQuery.docs[0]; userId = userDoc.id; } else { // Create new user userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; userDoc = null; } // Prepare user object for response const user = { uid: userId, email: email, displayName: email.split("@")[0], emailVerified: true, }; // Create or update user document in Firestore const userRef = db.collection("users").doc(userId); const userData = { id: userId, email: email, displayName: email.split("@")[0], emailVerified: true, lastLoginAt: Date.now(), }; if (!userDoc) { // Create new user document userData.createdAt = Date.now(); await userRef.set(userData); } else { // Update existing user with latest login info await userRef.update({ lastLoginAt: Date.now(), }); } // Generate a simple session token (in production, use proper JWT) const customToken = `session_${userId}_${Date.now()}`; // Store auth token in Firestore for validation await db.collection("authTokens").doc(customToken).set({ userId: userId, createdAt: Date.now(), expiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000), // 30 days lastUsedAt: Date.now(), isActive: true }); // Handle invitation if present let inviteData = null; if (otpData.inviteCode) { try { const inviteDoc = await db .collectionGroup("invites") .where("code", "==", otpData.inviteCode) .where("status", "==", "pending") .limit(1) .get(); if (!inviteDoc.empty) { inviteData = inviteDoc.docs[0].data(); } } catch (error) { console.error("Error fetching invite:", error); } } res.json({ success: true, user, token: customToken, invite: inviteData, }); } catch (error) { console.error("Verify OTP error:", error); res.status(500).json({ error: "Failed to verify code" }); } }); // Create Invitation Function exports.createInvitation = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const { name, email, role = "employee", department } = req.body; if (!email || !name) { return res.status(400).json({ error: "Name and email are required" }); } // Use the user's default organization (first one) const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } // Generate invite code const code = Math.random().toString(36).substring(2, 15); // Generate employee ID const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Create employee object for the invite const employee = { id: employeeId, name: name.trim(), email: email.trim(), role: role?.trim() || "employee", department: department?.trim() || "General", status: "invited", inviteCode: code }; // Store invitation with employee data const inviteRef = await db .collection("orgs") .doc(orgId) .collection("invites") .doc(code); await inviteRef.set({ code, employee, email, orgId, status: "pending", createdAt: Date.now(), expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days }); // Generate invite links const baseUrl = process.env.CLIENT_URL || 'http://localhost:5173'; const inviteLink = `${baseUrl}/#/employee-form/${code}`; try { if (!!process.env.SENDGRID_API_KEY) { await fetch('https://api.sendgrid.com/v3/mail/send', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`, }, body: JSON.stringify({ personalizations: [ { to: [{ email }], dynamic_template_data: { name, inviteLink, }, }, ], from: { email: 'no-reply@auditly.com', name: 'Auditly' }, template_id: process.env.SENDGRID_TEMPLATE_ID, }), }); console.log(`📧 Invitation sent to ${email} (${name}) with code: ${code}`); } } catch (error) { console.error("SendGrid email error:", error); } // In production, send actual invitation email console.log(`📧 Invite link: ${inviteLink}`); res.json({ success: true, code, employee, inviteLink, message: "Invitation sent successfully", }); } catch (error) { console.error("Create invitation error:", error); res.status(500).json({ error: "Failed to create invitation" }); } }); // Get Invitation Status Function exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } const { code } = req.query; if (!code) { return res.status(400).json({ error: "Invitation code is required" }); } try { const inviteDoc = await db .collectionGroup("invites") .where("code", "==", code) .limit(1) .get(); if (inviteDoc.empty) { return res.status(404).json({ error: "Invitation not found" }); } const invite = inviteDoc.docs[0].data(); // Check if expired if (Date.now() > invite.expiresAt) { return res.status(400).json({ error: "Invitation has expired" }); } res.json({ success: true, used: invite.status !== 'pending', employee: invite.employee, invite, }); } catch (error) { console.error("Get invitation status error:", error); res.status(500).json({ error: "Failed to get invitation status" }); } }); // Consume Invitation Function exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { code, userId } = req.body; if (!code) { return res.status(400).json({ error: "Invitation code is required" }); } try { const inviteSnapshot = await db .collectionGroup("invites") .where("code", "==", code) .where("status", "==", "pending") .limit(1) .get(); if (inviteSnapshot.empty) { return res.status(404).json({ error: "Invitation not found or already used" }); } const inviteDoc = inviteSnapshot.docs[0]; const invite = inviteDoc.data(); // Check if expired if (Date.now() > invite.expiresAt) { return res.status(400).json({ error: "Invitation has expired" }); } // Get employee data from the invite const employee = invite.employee; if (!employee) { return res.status(400).json({ error: "Invalid invitation data - missing employee information" }); } // Mark invitation as consumed await inviteDoc.ref.update({ status: "consumed", consumedBy: employee.id, consumedAt: Date.now(), }); // Add employee to organization using data from invite await db .collection("orgs") .doc(invite.orgId) .collection("employees") .doc(employee.id) .set({ id: employee.id, name: employee.name || employee.email.split("@")[0], email: employee.email, role: employee.role || "employee", department: employee.department || "General", joinedAt: Date.now(), status: "active", inviteCode: code, }); res.json({ success: true, orgId: invite.orgId, message: "Invitation consumed successfully", }); } catch (error) { console.error("Consume invitation error:", error); res.status(500).json({ error: "Failed to consume invitation" }); } }); // Submit Employee Answers Function exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { employeeId, answers, inviteCode } = req.body; try { let finalOrgId, finalEmployeeId; if (inviteCode) { // Invite-based submission (no auth required) if (!inviteCode || !answers) { return res.status(400).json({ error: "Invite code and answers are required for invite submissions" }); } // Look up the invite to get employee and org data (should be pending, not consumed yet) const inviteSnapshot = await db .collectionGroup("invites") .where("code", "==", inviteCode) .where("status", "==", "pending") .limit(1) .get(); if (inviteSnapshot.empty) { return res.status(404).json({ error: "Invitation not found or already used" }); } const inviteDoc = inviteSnapshot.docs[0]; const invite = inviteDoc.data(); // Check if expired if (Date.now() > invite.expiresAt) { return res.status(400).json({ error: "Invitation has expired" }); } finalOrgId = invite.orgId; finalEmployeeId = invite.employee.id; // Consume the invitation now await inviteDoc.ref.update({ status: "consumed", consumedBy: finalEmployeeId, consumedAt: Date.now(), }); // Add employee to organization if not already added const employeeRef = db .collection("orgs") .doc(finalOrgId) .collection("employees") .doc(finalEmployeeId); const employeeDoc = await employeeRef.get(); if (!employeeDoc.exists) { await employeeRef.set({ id: invite.employee.id, name: invite.employee.name || invite.employee.email.split("@")[0], email: invite.employee.email, role: invite.employee.role || "employee", department: invite.employee.department || "General", joinedAt: Date.now(), status: "active", inviteCode: inviteCode, }); } } else { // Authenticated submission const authContext = await validateAuthAndGetContext(req); if (!employeeId || !answers) { return res.status(400).json({ error: "Employee ID and answers are required for authenticated submissions" }); } finalOrgId = authContext.orgId; finalEmployeeId = employeeId; if (!finalOrgId) { return res.status(400).json({ error: "User has no associated organizations" }); } } // Store submission const submissionRef = await db .collection("orgs") .doc(finalOrgId) .collection("submissions") .doc(finalEmployeeId); await submissionRef.set({ employeeId: finalEmployeeId, answers, submittedAt: Date.now(), status: "completed", submissionType: inviteCode ? "invite" : "regular", ...(inviteCode && { inviteCode }) }); // Generate employee report automatically with company context try { // Get employee data const employeeDoc = await db .collection("orgs") .doc(finalOrgId) .collection("employees") .doc(finalEmployeeId) .get(); const employeeData = employeeDoc.exists ? employeeDoc.data() : null; // Get company onboarding data for LLM context const orgDoc = await db.collection("orgs").doc(finalOrgId).get(); const orgData = orgDoc.exists ? orgDoc.data() : {}; // Prepare company context (onboarding data) const companyContext = { name: orgData.name, industry: orgData.industry, mission: orgData.mission, values: orgData.values, culture: orgData.cultureDescription, size: orgData.size, onboardingData: orgData // Include all org data for comprehensive context }; // Prepare submission data const submissionData = { employeeId: finalEmployeeId, answers, submittedAt: Date.now(), status: "completed" }; // Generate the report using the existing function logic let report; if (openai) { // Use OpenAI to generate the report with company context const prompt = ` You are an expert HR analyst. Generate a comprehensive employee performance report based on the following data: Employee Information: - Name: ${employeeData?.name || employeeData?.email || 'Unknown'} - Role: ${employeeData?.role || "Team Member"} - Department: ${employeeData?.department || "General"} - Email: ${employeeData?.email || 'Unknown'} Employee Questionnaire Responses: ${JSON.stringify(answers, null, 2)} Company Context & Alignment Criteria: ${JSON.stringify(companyContext, null, 2)} Generate a detailed report that: 1. Evaluates how well the employee aligns with company values and culture 2. Assesses their role performance and output 3. Identifies behavioral insights and work patterns 4. Highlights strengths and areas for improvement 5. Provides specific recommendations for growth 6. Suggests opportunities that align with company goals 7. Identifies any risks or concerns 8. Provides numerical grading across key performance areas Return ONLY valid JSON that matches this structure: { "roleAndOutput": { "currentRole": string, "keyResponsibilities": string[], "performanceRating": number }, "behavioralInsights": { "workStyle": string, "communicationSkills": string, "teamDynamics": string }, "strengths": string[], "weaknesses": string[], "opportunities": string[], "risks": string[], "recommendations": string[], "companyAlignment": { "valuesAlignment": number, "cultureAlignment": number, "missionAlignment": number }, "grading": { "overall": number, "technical": number, "communication": number, "teamwork": number, "leadership": number } } Be thorough, professional, and focus on actionable insights. `.trim(); const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: "You are an expert HR analyst. Generate comprehensive employee performance reports in JSON format that evaluate company alignment and performance." }, { role: "user", content: prompt } ], response_format: { type: "json_object" }, temperature: 0.7, }); const aiResponse = completion.choices[0].message.content; const parsedReport = JSON.parse(aiResponse); report = { employeeId: finalEmployeeId, generatedAt: Date.now(), summary: `AI-generated performance analysis for ${employeeData?.name || employeeData?.email || 'Employee'}`, submissionId: finalEmployeeId, companyContext: companyContext, ...parsedReport }; } else { // Fallback to mock report when OpenAI is not available report = { employeeId: finalEmployeeId, generatedAt: Date.now(), summary: `Performance analysis for ${employeeData?.name || employeeData?.email || 'Employee'}`, submissionId: finalEmployeeId, companyContext: companyContext, roleAndOutput: { currentRole: employeeData?.role || "Team Member", keyResponsibilities: ["Task completion", "Team collaboration", "Quality delivery"], performanceRating: 85, }, behavioralInsights: { workStyle: "Collaborative and detail-oriented", communicationSkills: "Strong verbal and written communication", teamDynamics: "Positive team player", }, strengths: [ "Excellent problem-solving abilities", "Strong attention to detail", "Reliable and consistent performance", ], weaknesses: [ "Could improve time management", "Needs to be more proactive in meetings", ], opportunities: [ "Leadership development opportunities", "Cross-functional project involvement", "Skill enhancement in emerging technologies", ], risks: [ "Potential burnout from heavy workload", "Limited growth opportunities in current role", ], recommendations: [ "Provide leadership training", "Assign mentorship role", "Consider promotion to senior position", ], companyAlignment: { valuesAlignment: 88, cultureAlignment: 82, missionAlignment: 85 }, grading: { overall: 85, technical: 88, communication: 82, teamwork: 90, leadership: 75, }, }; } // Store the report in Firestore const reportRef = db .collection("orgs") .doc(finalOrgId) .collection("reports") .doc(finalEmployeeId); await reportRef.set(report); console.log(`Employee report generated and stored for ${finalEmployeeId} in org ${finalOrgId}`); } catch (reportError) { console.error("Failed to generate employee report:", reportError); // Don't fail the submission if report generation fails } res.json({ success: true, message: "Employee answers submitted successfully", }); } catch (error) { console.error("Submit employee answers error:", error); res.status(500).json({ error: "Failed to submit answers" }); } }); // Generate Employee Report Function exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { employee, submission, companyWiki } = req.body; if (!employee || !submission) { return res.status(400).json({ error: "Employee and submission data are required" }); } try { let report; if (openai) { // Use OpenAI to generate the report const prompt = ` You are an expert HR analyst. Generate a comprehensive employee performance report based on the following data: Employee Information: - Name: ${employee.name || employee.email} - Role: ${employee.role || "Team Member"} - Department: ${employee.department || "General"} Employee Submission Data: ${JSON.stringify(submission, null, 2)} Company Context: ${companyWiki ? JSON.stringify(companyWiki, null, 2) : "No company context provided"} Generate a detailed report with the following structure: - roleAndOutput: Current role assessment and performance rating - behavioralInsights: Work style, communication, and team dynamics - strengths: List of employee strengths - weaknesses: Areas for improvement (mark critical issues) - opportunities: Growth and development opportunities - risks: Potential risks or concerns - recommendations: Specific action items - grading: Numerical scores for different performance areas Return ONLY valid JSON that matches this structure. Be thorough but professional. `.trim(); const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: "You are an expert HR analyst. Generate comprehensive employee performance reports in JSON format." }, { role: "user", content: prompt } ], response_format: { type: "json_object" }, temperature: 0.7, }); const aiResponse = completion.choices[0].message.content; const parsedReport = JSON.parse(aiResponse); report = { employeeId: employee.id, generatedAt: Date.now(), summary: `AI-generated performance analysis for ${employee.name || employee.email}`, ...parsedReport }; } else { // Fallback to mock report when OpenAI is not available report = { employeeId: employee.id, generatedAt: Date.now(), summary: `Performance analysis for ${employee.name || employee.email}`, roleAndOutput: { currentRole: employee.role || "Team Member", keyResponsibilities: ["Task completion", "Team collaboration", "Quality delivery"], performanceRating: 85, }, behavioralInsights: { workStyle: "Collaborative and detail-oriented", communicationSkills: "Strong verbal and written communication", teamDynamics: "Positive team player", }, strengths: [ "Excellent problem-solving abilities", "Strong attention to detail", "Reliable and consistent performance", ], weaknesses: [ "Could improve time management", "Needs to be more proactive in meetings", ], opportunities: [ "Leadership development opportunities", "Cross-functional project involvement", "Skill enhancement in emerging technologies", ], risks: [ "Potential burnout from heavy workload", "Limited growth opportunities in current role", ], recommendations: [ "Provide leadership training", "Assign mentorship role", "Consider promotion to senior position", ], grading: { overall: 85, technical: 88, communication: 82, teamwork: 90, leadership: 75, }, }; } res.json({ success: true, report, }); } catch (error) { console.error("Generate employee report error:", error); res.status(500).json({ error: "Failed to generate employee report" }); } }); // Generate Company Wiki Function exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { org, submissions = [] } = req.body; if (!org) { return res.status(400).json({ error: "Organization data is required" }); } try { let report, wiki; if (openai) { // Use OpenAI to generate the company report and wiki db.collection("orgs").doc(orgId) const system = "You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema."; const user = [ "Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.", "Be thorough and professional.", "", "Organization Information:", JSON.stringify(org, null, 2), "", "Employee Submissions:", JSON.stringify(submissions, null, 2) ].join("\n"); const completion = await openai.chat.completions.create({ model: "gpt-4o", temperature: 0, // consistency response_format: RESPONSE_FORMAT, messages: [ { role: "system", content: system }, { role: "user", content: user } ] }); // content is guaranteed to be schema-conformant JSON console.log(completion.choices[0].message); console.log(completion.choices[0].message.content); const parsed = JSON.parse(completion.choices[0].message.content); const report = { generatedAt: Date.now(), ...parsed }; const wiki = { companyName: org?.name ?? parsed.wiki.companyName, generatedAt: Date.now(), }; const companyReport = db.collection("orgs").doc(orgId).collection("companyReport"); await companyReport.set(report); const companyWiki = db.collection("orgs").doc(orgId).collection("companyWiki"); await companyWiki.set(wiki); console.log(report); console.log(wiki); } else { // Fallback to mock data when OpenAI is not available report = { generatedAt: Date.now(), companyPerformance: { overallScore: 82, trend: "improving", keyMetrics: { productivity: 85, satisfaction: 79, retention: 88, }, }, immediateHiringNeeds: [ { role: "Frontend Developer", priority: "high", timeline: "2-4 weeks", skills: ["React", "TypeScript", "CSS"], }, ], forwardOperatingPlan: { nextQuarter: "Focus on product development and team expansion", challenges: ["Scaling infrastructure", "Talent acquisition"], opportunities: ["New market segments", "Technology partnerships"], }, organizationalInsights: { teamDynamics: "Strong collaboration across departments", culturalHealth: "Positive and inclusive work environment", communicationEffectiveness: "Good but could improve cross-team coordination", }, strengths: [ "Strong technical expertise", "Collaborative team culture", "Innovative problem-solving approach", ] }; wiki = { companyName: org.name, industry: org.industry, description: org.description, mission: org.mission || "To deliver excellent products and services", values: org.values || ["Innovation", "Teamwork", "Excellence"], culture: "Collaborative and growth-oriented", generatedAt: Date.now(), }; } res.json({ success: true, ...report, ...wiki, }); } catch (error) { console.error("Generate company wiki error:", error); res.status(500).json({ error: "Failed to generate company wiki" }); } }); // Chat Function exports.chat = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { message, employeeId, context, mentions, attachments } = req.body; if (!message) { return res.status(400).json({ error: "Message is required" }); } try { let response; if (openai) { // Use OpenAI for chat responses const systemPrompt = ` You are an expert HR consultant and business analyst with access to employee performance data and company analytics. You provide thoughtful, professional advice based on the employee context and company data provided. ${context ? ` Current Context: ${JSON.stringify(context, null, 2)} ` : ''} ${mentions && mentions.length > 0 ? ` Mentioned Employees: ${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')} ` : ''} Provide helpful, actionable insights while maintaining professional confidentiality and focusing on constructive feedback. `.trim(); // Build the user message content let userContent = [ { type: "text", text: message } ]; // Add image attachments if present if (attachments && attachments.length > 0) { attachments.forEach(attachment => { if (attachment.type.startsWith('image/') && attachment.data) { userContent.push({ type: "image_url", image_url: { url: attachment.data, detail: "high" } }); } // For non-image files, add them as text context else if (attachment.data) { userContent.push({ type: "text", text: `[Attached file: ${attachment.name} (${attachment.type})]` }); } }); } const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userContent } ], temperature: 0.7, max_tokens: 1000, // Increased for more detailed responses when analyzing images }); response = completion.choices[0].message.content; } else { // Fallback responses when OpenAI is not available const attachmentText = attachments && attachments.length > 0 ? ` I can see you've attached ${attachments.length} file(s), but I'm currently unable to process attachments.` : ''; const responses = [ `That's an interesting point about performance metrics.${attachmentText} Based on the data, I'd recommend focusing on...`, `I can see from the employee report that there are opportunities for growth in...${attachmentText}`, `The company analysis suggests that this area needs attention.${attachmentText} Here's what I would suggest...`, `Based on the performance data, this employee shows strong potential in...${attachmentText}`, ]; response = responses[Math.floor(Math.random() * responses.length)]; } res.json({ success: true, response, timestamp: Date.now(), }); } catch (error) { console.error("Chat error:", error); res.status(500).json({ error: "Failed to process chat message" }); } }); // Create Organization Function exports.createOrganization = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const { name } = req.body; if (!name) { return res.status(400).json({ error: "Organization name is required" }); } // Generate unique organization ID const orgId = `org_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Create comprehensive organization document const orgData = { name, createdAt: Date.now(), updatedAt: Date.now(), onboardingCompleted: false, ownerId: authContext.userId, // Subscription fields (will be populated after Stripe setup) subscription: { status: 'trial', // trial, active, past_due, canceled stripeCustomerId: null, stripeSubscriptionId: null, currentPeriodStart: null, currentPeriodEnd: null, trialEnd: Date.now() + (14 * 24 * 60 * 60 * 1000), // 14 day trial }, // Usage tracking usage: { employeeCount: 0, reportsGenerated: 0, lastReportGeneration: null, }, // Organization settings settings: { allowedEmployeeCount: 50, // Default limit featuresEnabled: { aiReports: true, chat: true, analytics: true, } } }; const orgRef = db.collection("orgs").doc(orgId); await orgRef.set(orgData); // Get user information from Firestore (since we don't use Firebase Auth) const userRef = db.collection("users").doc(authContext.userId); const userDoc = await userRef.get(); if (!userDoc.exists) { console.error("User document not found:", authContext.userId); return res.status(400).json({ error: "User not found" }); } const userData = userDoc.data(); // Add user as owner to organization's employees collection const employeeRef = orgRef.collection("employees").doc(authContext.userId); await employeeRef.set({ id: authContext.userId, role: "owner", isOwner: true, joinedAt: Date.now(), status: "active", name: userData.displayName || userData.email.split("@")[0], email: userData.email, department: "Management", }); // Add organization to user's organizations (for multi-org support) const userOrgRef = db.collection("users").doc(authContext.userId).collection("organizations").doc(orgId); await userOrgRef.set({ orgId, name, role: "owner", onboardingCompleted: false, joinedAt: Date.now(), }); // Update user document with latest activity await userRef.update({ lastLoginAt: Date.now(), }); res.json({ success: true, orgId, name, role: "owner", onboardingCompleted: false, joinedAt: Date.now(), subscription: orgData.subscription, requiresSubscription: true, // Signal frontend to show subscription flow }); } catch (error) { console.error("Create organization error:", error); res.status(500).json({ error: "Failed to create organization" }); } }); // Get User Organizations Function exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); // Get user's organizations const userOrgsSnapshot = await db .collection("users") .doc(authContext.userId) .collection("organizations") .get(); const organizations = []; userOrgsSnapshot.forEach(doc => { organizations.push({ orgId: doc.id, ...doc.data(), }); }); res.json({ success: true, organizations, }); } catch (error) { console.error("Get user organizations error:", error); if (error.message.includes('Missing or invalid authorization') || error.message.includes('Token')) { return res.status(401).json({ error: error.message }); } res.status(500).json({ error: "Failed to get user organizations" }); } }); // Join Organization Function (via invite) exports.joinOrganization = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const { inviteCode } = req.body; if (!inviteCode) { return res.status(400).json({ error: "Invite code is required" }); } // Find the invitation const inviteSnapshot = await db .collectionGroup("invites") .where("code", "==", inviteCode) .where("status", "==", "pending") .limit(1) .get(); if (inviteSnapshot.empty) { return res.status(404).json({ error: "Invitation not found or already used" }); } const inviteDoc = inviteSnapshot.docs[0]; const invite = inviteDoc.data(); // Check if expired if (Date.now() > invite.expiresAt) { return res.status(400).json({ error: "Invitation has expired" }); } const orgId = invite.orgId; // Get organization details const orgDoc = await db.collection("orgs").doc(orgId).get(); if (!orgDoc.exists()) { return res.status(404).json({ error: "Organization not found" }); } const orgData = orgDoc.data(); // Get user information from Firestore (since we don't use Firebase Auth) const userRef = db.collection("users").doc(authContext.userId); const userDoc = await userRef.get(); if (!userDoc.exists) { console.error("User document not found:", authContext.userId); return res.status(400).json({ error: "User not found" }); } const userData = userDoc.data(); // Mark invitation as consumed await inviteDoc.ref.update({ status: "consumed", consumedBy: authContext.userId, consumedAt: Date.now(), }); // Add user to organization employees with full information await db .collection("orgs") .doc(orgId) .collection("employees") .doc(authContext.userId) .set({ id: authContext.userId, email: userData.email, name: userData.displayName || userData.email.split("@")[0], role: invite.role || "employee", joinedAt: Date.now(), status: "active", }); // Add organization to user's organizations await db .collection("users") .doc(authContext.userId) .collection("organizations") .doc(orgId) .set({ orgId, name: orgData.name, role: invite.role || "employee", onboardingCompleted: orgData.onboardingCompleted || false, joinedAt: Date.now(), }); // Update user document with latest login activity await userRef.update({ lastLoginAt: Date.now(), }); res.json({ success: true, orgId, name: orgData.name, role: invite.role || "employee", onboardingCompleted: orgData.onboardingCompleted || false, joinedAt: Date.now(), }); } catch (error) { console.error("Join organization error:", error); res.status(500).json({ error: "Failed to join organization" }); } }); // Create Stripe Checkout Session Function exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const { userEmail, priceId } = req.body; if (!userEmail) { return res.status(400).json({ error: "User email is required" }); } const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } if (!stripe) { return res.status(500).json({ error: "Stripe not configured" }); } // Get or create Stripe customer let customer; const existingCustomers = await stripe.customers.list({ email: userEmail, limit: 1, }); if (existingCustomers.data.length > 0) { customer = existingCustomers.data[0]; } else { customer = await stripe.customers.create({ email: userEmail, metadata: { userId: authContext.userId, orgId, }, }); } // Default to standard plan if no priceId provided const defaultPriceId = priceId || process.env.STRIPE_PRICE_ID || 'price_standard_monthly'; // Create checkout session const session = await stripe.checkout.sessions.create({ customer: customer.id, payment_method_types: ['card'], line_items: [ { price: defaultPriceId, quantity: 1, }, ], mode: 'subscription', success_url: `${process.env.CLIENT_URL || 'http://localhost:5173'}/#/dashboard?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.CLIENT_URL || 'http://localhost:5173'}/#/dashboard?canceled=true`, metadata: { orgId, userId: authContext.userId, }, subscription_data: { metadata: { orgId, userId: authContext.userId, }, trial_period_days: 14, // 14-day trial }, }); // Update organization with customer ID const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ 'subscription.stripeCustomerId': customer.id, 'subscription.checkoutSessionId': session.id, updatedAt: Date.now(), }); res.json({ success: true, sessionId: session.id, sessionUrl: session.url, customerId: customer.id, }); } catch (error) { console.error("Create checkout session error:", error); res.status(500).json({ error: "Failed to create checkout session" }); } }); // Handle Stripe Webhook Function exports.stripeWebhook = onRequest(async (req, res) => { if (!stripe) { return res.status(500).send('Stripe not configured'); } const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; let event; try { event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } // Handle the event try { switch (event.type) { case 'checkout.session.completed': await handleCheckoutCompleted(event.data.object); break; case 'customer.subscription.created': await handleSubscriptionCreated(event.data.object); break; case 'customer.subscription.updated': await handleSubscriptionUpdated(event.data.object); break; case 'customer.subscription.deleted': await handleSubscriptionDeleted(event.data.object); break; case 'invoice.payment_succeeded': await handlePaymentSucceeded(event.data.object); break; case 'invoice.payment_failed': await handlePaymentFailed(event.data.object); break; default: console.log(`Unhandled event type ${event.type}`); } res.json({ received: true }); } catch (error) { console.error('Webhook handler error:', error); res.status(500).json({ error: 'Webhook handler failed' }); } }); // Get Subscription Status Function exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } const orgDoc = await db.collection("orgs").doc(orgId).get(); if (!orgDoc.exists()) { return res.status(404).json({ error: "Organization not found" }); } const orgData = orgDoc.data(); const subscription = orgData.subscription || {}; // If we have a Stripe subscription ID, get the latest status if (stripe && subscription.stripeSubscriptionId) { try { const stripeSubscription = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId); // Update local subscription data await orgDoc.ref.update({ 'subscription.status': stripeSubscription.status, 'subscription.currentPeriodStart': stripeSubscription.current_period_start * 1000, 'subscription.currentPeriodEnd': stripeSubscription.current_period_end * 1000, updatedAt: Date.now(), }); subscription.status = stripeSubscription.status; subscription.currentPeriodStart = stripeSubscription.current_period_start * 1000; subscription.currentPeriodEnd = stripeSubscription.current_period_end * 1000; } catch (stripeError) { console.error('Failed to fetch Stripe subscription:', stripeError); } } res.json({ success: true, subscription, orgId, }); } catch (error) { console.error("Get subscription status error:", error); res.status(500).json({ error: "Failed to get subscription status" }); } }); // Webhook Helper Functions async function handleCheckoutCompleted(session) { const orgId = session.metadata?.orgId; if (!orgId) return; const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ 'subscription.status': 'trialing', 'subscription.stripeSubscriptionId': session.subscription, 'subscription.checkoutSessionId': session.id, updatedAt: Date.now(), }); } async function handleSubscriptionCreated(subscription) { const orgId = subscription.metadata?.orgId; if (!orgId) return; const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ 'subscription.status': subscription.status, 'subscription.stripeSubscriptionId': subscription.id, 'subscription.currentPeriodStart': subscription.current_period_start * 1000, 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, updatedAt: Date.now(), }); } async function handleSubscriptionUpdated(subscription) { const orgId = subscription.metadata?.orgId; if (!orgId) return; const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ 'subscription.status': subscription.status, 'subscription.currentPeriodStart': subscription.current_period_start * 1000, 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, updatedAt: Date.now(), }); } async function handleSubscriptionDeleted(subscription) { const orgId = subscription.metadata?.orgId; if (!orgId) return; const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ 'subscription.status': 'canceled', 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, updatedAt: Date.now(), }); } async function handlePaymentSucceeded(invoice) { const subscriptionId = invoice.subscription; if (!subscriptionId) return; // Update subscription status to active const subscription = await stripe.subscriptions.retrieve(subscriptionId); const orgId = subscription.metadata?.orgId; if (orgId) { const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ 'subscription.status': 'active', 'subscription.currentPeriodStart': subscription.current_period_start * 1000, 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, updatedAt: Date.now(), }); } } async function handlePaymentFailed(invoice) { const subscriptionId = invoice.subscription; if (!subscriptionId) return; const subscription = await stripe.subscriptions.retrieve(subscriptionId); const orgId = subscription.metadata?.orgId; if (orgId) { const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ 'subscription.status': 'past_due', updatedAt: Date.now(), }); } } // exports.helloWorld = onRequest((request, response) => { // response.send("Hello from Firebase!"); // }); // exports.sendOTP = onRequest(async (request, response) => { // // Set CORS headers // response.set('Access-Control-Allow-Origin', '*'); // response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); // response.set('Access-Control-Allow-Headers', 'Content-Type'); // if (request.method === 'OPTIONS') { // response.status(204).send(''); // return; // } // if (request.method !== 'POST') { // response.status(405).json({ error: 'Method not allowed' }); // return; // } // const { email } = request.body; // if (!email) { // response.status(400).json({ error: 'Email is required' }); // return; // } // // Generate a simple OTP // const otp = Math.floor(100000 + Math.random() * 900000).toString(); // response.json({ // success: true, // message: 'Verification code sent to your email', // otp: otp // Always return OTP in emulator mode // }); // }); // exports.verifyOTP = onRequest(async (request, response) => { // // Set CORS headers // response.set('Access-Control-Allow-Origin', '*'); // response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); // response.set('Access-Control-Allow-Headers', 'Content-Type'); // if (request.method === 'OPTIONS') { // response.status(204).send(''); // return; // } // if (request.method !== 'POST') { // response.status(405).json({ error: 'Method not allowed' }); // return; // } // const { email, otp } = request.body; // if (!email || !otp) { // response.status(400).json({ error: 'Email and OTP are required' }); // return; // } // // Mock verification - accept any 6-digit code // if (otp.length === 6) { // response.json({ // success: true, // user: { // uid: 'demo-user-123', // email: email, // displayName: email.split('@')[0], // emailVerified: true // }, // token: 'demo-token-123' // }); // } else { // response.status(400).json({ error: 'Invalid verification code' }); // } // }); // Save Company Report Function exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { orgId, report } = req.body; if (!orgId || !report) { return res.status(400).json({ error: "Organization ID and report are required" }); } try { // Add ID and timestamp if not present if (!report.id) { report.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } if (!report.createdAt) { report.createdAt = Date.now(); } // Save to Firestore const reportRef = db.collection("orgs").doc(orgId).collection("fullCompanyReports").doc(report.id); await reportRef.set(report); console.log(`Company report saved successfully for org ${orgId}`); res.json({ success: true, reportId: report.id, message: "Company report saved successfully" }); } catch (error) { console.error("Save company report error:", error); res.status(500).json({ error: "Failed to save company report" }); } }); // Helper function to verify user authorization const verifyUserAuthorization = async (userId, orgId) => { if (!userId || !orgId) { return false; } try { // Check if user exists in the organization's employees collection const employeeDoc = await db.collection("orgs").doc(orgId).collection("employees").doc(userId).get(); return employeeDoc.exists; } catch (error) { console.error("Authorization check error:", error); return false; } }; // Get Organization Data Function exports.getOrgData = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } // Get organization data const orgDoc = await db.collection("orgs").doc(orgId).get(); if (!orgDoc.exists) { return res.status(404).json({ error: "Organization not found" }); } const orgData = { id: orgId, ...orgDoc.data() }; res.json({ success: true, org: orgData }); } catch (error) { console.error("Get org data error:", error); if (error.message.includes('Missing or invalid authorization') || error.message.includes('Token')) { return res.status(401).json({ error: error.message }); } res.status(500).json({ error: "Failed to get organization data" }); } }); // Update Organization Data Function exports.updateOrgData = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "PUT") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const { data } = req.body; if (!data) { return res.status(400).json({ error: "Data is required" }); } const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } // Update organization data const orgRef = db.collection("orgs").doc(orgId); await orgRef.update({ ...data, updatedAt: Date.now() }); res.json({ success: true, message: "Organization data updated successfully" }); } catch (error) { console.error("Update org data error:", error); if (error.message.includes('Missing or invalid authorization') || error.message.includes('Token')) { return res.status(401).json({ error: error.message }); } res.status(500).json({ error: "Failed to update organization data" }); } }); // Get Employees Function exports.getEmployees = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } // Get all employees const employeesSnapshot = await db.collection("orgs").doc(orgId).collection("employees").get(); const employees = []; employeesSnapshot.forEach(doc => { employees.push({ id: doc.id, ...doc.data() }); }); res.json({ success: true, employees }); } catch (error) { console.error("Get employees error:", error); if (error.message.includes('Missing or invalid authorization') || error.message.includes('Token')) { return res.status(401).json({ error: error.message }); } res.status(500).json({ error: "Failed to get employees" }); } }); // Get Submissions Function exports.getSubmissions = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } // Get all submissions const submissionsSnapshot = await db.collection("orgs").doc(orgId).collection("submissions").get(); const submissions = {}; submissionsSnapshot.forEach(doc => { submissions[doc.id] = { id: doc.id, ...doc.data() }; }); res.json({ success: true, submissions }); } catch (error) { console.error("Get submissions error:", error); if (error.message.includes('Missing or invalid authorization') || error.message.includes('Token')) { return res.status(401).json({ error: error.message }); } res.status(500).json({ error: "Failed to get submissions" }); } }); // Get Reports Function exports.getReports = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } try { // Validate auth token and get user context const authContext = await validateAuthAndGetContext(req); const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } // Get all reports const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("reports").get(); const reports = {}; reportsSnapshot.forEach(doc => { reports[doc.id] = { id: doc.id, ...doc.data() }; }); res.json({ success: true, reports }); } catch (error) { console.error("Get reports error:", error); if (error.message.includes('Missing or invalid authorization') || error.message.includes('Token')) { return res.status(401).json({ error: error.message }); } res.status(500).json({ error: "Failed to get reports" }); } }); // Create/Update Employee Function exports.upsertEmployee = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { orgId, userId, employeeData } = req.body; if (!orgId || !userId || !employeeData) { return res.status(400).json({ error: "Organization ID, user ID, and employee data are required" }); } try { // Verify user authorization const isAuthorized = await verifyUserAuthorization(userId, orgId); if (!isAuthorized) { return res.status(403).json({ error: "Unauthorized access to organization" }); } // Generate employee ID if not provided if (!employeeData.id) { employeeData.id = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Add timestamps const currentTime = Date.now(); if (!employeeData.createdAt) { employeeData.createdAt = currentTime; } employeeData.updatedAt = currentTime; // Save employee const employeeRef = db.collection("orgs").doc(orgId).collection("employees").doc(employeeData.id); await employeeRef.set(employeeData); res.json({ success: true, employee: employeeData, message: "Employee saved successfully" }); } catch (error) { console.error("Upsert employee error:", error); res.status(500).json({ error: "Failed to save employee" }); } }); // Save Report Function exports.saveReport = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { orgId, userId, employeeId, reportData } = req.body; if (!orgId || !userId || !employeeId || !reportData) { return res.status(400).json({ error: "Organization ID, user ID, employee ID, and report data are required" }); } try { // Verify user authorization const isAuthorized = await verifyUserAuthorization(userId, orgId); if (!isAuthorized) { return res.status(403).json({ error: "Unauthorized access to organization" }); } // Add metadata const currentTime = Date.now(); if (!reportData.id) { reportData.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } if (!reportData.createdAt) { reportData.createdAt = currentTime; } reportData.updatedAt = currentTime; reportData.employeeId = employeeId; // Save report const reportRef = db.collection("orgs").doc(orgId).collection("reports").doc(employeeId); await reportRef.set(reportData); res.json({ success: true, report: reportData, message: "Report saved successfully" }); } catch (error) { console.error("Save report error:", error); res.status(500).json({ error: "Failed to save report" }); } }); // Get Company Reports Function exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } try { const authContext = await validateAuthAndGetContext(req); const orgId = authContext.orgId; if (!orgId) { return res.status(400).json({ error: "User has no associated organizations" }); } // Get all company reports const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("fullCompanyReports").get(); const reports = []; reportsSnapshot.forEach(doc => { reports.push({ id: doc.id, ...doc.data() }); }); // Sort by creation date (newest first) reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); res.json({ success: true, reports }); } catch (error) { console.error("Get company reports error:", error); res.status(500).json({ error: "Failed to get company reports" }); } }); // Upload Image Function exports.uploadImage = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const { orgId, userId, imageData } = req.body; if (!orgId || !userId || !imageData) { return res.status(400).json({ error: "Organization ID, user ID, and image data are required" }); } try { // Verify user authorization const isAuthorized = await verifyUserAuthorization(userId, orgId); if (!isAuthorized) { return res.status(403).json({ error: "Unauthorized access to organization" }); } // Validate image data const { collectionName, documentId, dataUrl, filename, originalSize, compressedSize, width, height } = imageData; if (!collectionName || !documentId || !dataUrl || !filename) { return res.status(400).json({ error: "Missing required image data fields" }); } // Create image document const imageDoc = { id: `${Date.now()}_${filename}`, dataUrl, filename, originalSize: originalSize || 0, compressedSize: compressedSize || 0, uploadedAt: Date.now(), width: width || 0, height: height || 0, orgId, uploadedBy: userId }; // Store image in organization's images collection const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); await imageRef.set({ ...imageDoc, collectionName, documentId }); res.json({ success: true, imageId: imageDoc.id, message: "Image uploaded successfully" }); } catch (error) { console.error("Upload image error:", error); res.status(500).json({ error: "Failed to upload image" }); } }); // Get Image Function exports.getImage = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } const { orgId, userId, collectionName, documentId } = req.query; if (!orgId || !userId || !collectionName || !documentId) { return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" }); } try { // Verify user authorization const isAuthorized = await verifyUserAuthorization(userId, orgId); if (!isAuthorized) { return res.status(403).json({ error: "Unauthorized access to organization" }); } // Get image document const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); const imageDoc = await imageRef.get(); if (!imageDoc.exists) { return res.status(404).json({ error: "Image not found" }); } const imageData = imageDoc.data(); // Return image data (excluding org-specific metadata) const responseData = { id: imageData.id, dataUrl: imageData.dataUrl, filename: imageData.filename, originalSize: imageData.originalSize, compressedSize: imageData.compressedSize, uploadedAt: imageData.uploadedAt, width: imageData.width, height: imageData.height }; res.json({ success: true, image: responseData }); } catch (error) { console.error("Get image error:", error); res.status(500).json({ error: "Failed to get image" }); } }); // Delete Image Function exports.deleteImage = onRequest({ cors: true }, async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; } if (req.method !== "DELETE") { return res.status(405).json({ error: "Method not allowed" }); } const { orgId, userId, collectionName, documentId } = req.body; if (!orgId || !userId || !collectionName || !documentId) { return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" }); } try { // Verify user authorization const isAuthorized = await verifyUserAuthorization(userId, orgId); if (!isAuthorized) { return res.status(403).json({ error: "Unauthorized access to organization" }); } // Delete image document const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`); const imageDoc = await imageRef.get(); if (!imageDoc.exists) { return res.status(404).json({ error: "Image not found" }); } await imageRef.delete(); res.json({ success: true, message: "Image deleted successfully" }); } catch (error) { console.error("Delete image error:", error); res.status(500).json({ error: "Failed to delete image" }); } });