diff --git a/employee_report_schema.json b/employee_report_schema.json new file mode 100644 index 0000000..ab793d7 --- /dev/null +++ b/employee_report_schema.json @@ -0,0 +1,161 @@ +{ + "type": "object", + "properties": { + "employeeId": { + "type": "string" + }, + "department": { + "type": "string" + }, + "role": { + "type": "string" + }, + "roleAndOutput": { + "type": "object", + "properties": { + "responsibilities": { + "type": "string", + "examples": [ + "Recruiting influencers, onboarding, campaign support, business development." + ] + }, + "clarityOnRole": { + "type": "string", + "examples": [ + "10/10 – Feels very clear on responsibilities." + ] + }, + "selfRatedOutput": { + "type": "string", + "examples": [ + "7/10 – Indicates decent performance but room to grow." + ] + }, + "recurringTasks": { + "type": "string", + "examples": [ + "Influencer outreach, onboarding, communications." + ] + } + } + }, + "insights": { + "type": "object", + "properties": { + "personalityInsights": { + "type": "string", + "examples": [ + "Loyal, well-liked by influencers, eager to grow, client-facing interest." + ] + }, + "psychologicalIndicators": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "Scores high on optimism and external motivation.", + "Shows ambition but lacks self-discipline in execution.", + "Displays a desire for recognition and community; seeks more appreciation." + ] + ] + }, + "selfAwareness": { + "type": "string", + "examples": [ + "High – acknowledges weaknesses like lateness and disorganization." + ] + }, + "emotionalResponses": { + "type": "string", + "examples": [ + "Frustrated by campaign disorganization; would prefer closer collaboration." + ] + }, + "growthDesire": { + "type": "string", + "examples": [ + "Interested in becoming more client-facing and shifting toward biz dev." + ] + } + } + }, + "strengths": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "Builds strong relationships with influencers.", + "Has sales and outreach potential.", + "Loyal, driven, and values-aligned with the company mission.", + "Open to feedback and self-improvement." + ] + ] + }, + "weaknessess": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + "Critical Issue: Disorganized and late with deliverables — confirmed by previous internal notes.", + "Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.", + "May unintentionally cause friction with campaigns team by stepping outside process boundaries." + ] + }, + "opportunities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "examples": [ + [ + { + "title": "Role Adjustment", + "description": "Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities." + }, + { + "title": "Accountability Support", + "description": "Pair with a high-output implementer (new hire) to balance Gentry’s strategic skills." + } + ] + ] + }, + "risks": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "Without strict structure, Gentry’s performance will stay flat or become a bottleneck.", + "If kept in a dual-role (recruiting + outreach), productivity will suffer.", + "He needs system constraints and direct oversight to stay focused." + ] + ] + }, + "recommendations": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.", + "Pair him with a new hire who is ultra-organized and can execute on Gentry’s deals." + ] + ] + } + } +} \ No newline at end of file diff --git a/functions/index.js b/functions/index.js index 992f618..5075738 100644 --- a/functions/index.js +++ b/functions/index.js @@ -80,99 +80,394 @@ const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SEC 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"] - } - } +const RESPONSE_FORMAT_EMPLOYEE = { + "type": "object", + "properties": { + "employeeId": { + "type": "string" + }, + "department": { + "type": "string" + }, + "role": { + "type": "string" + }, + "roleAndOutput": { + "type": "object", + "properties": { + "responsibilities": { + "type": "string", + "examples": [ + "Recruiting influencers, onboarding, campaign support, business development." + ] + }, + "clarityOnRole": { + "type": "string", + "examples": [ + "10/10 – Feels very clear on responsibilities." + ] + }, + "selfRatedOutput": { + "type": "string", + "examples": [ + "7/10 – Indicates decent performance but room to grow." + ] + }, + "recurringTasks": { + "type": "string", + "examples": [ + "Influencer outreach, onboarding, communications." + ] + } + } + }, + "insights": { + "type": "object", + "properties": { + "personalityInsights": { + "type": "string", + "examples": [ + "Loyal, well-liked by influencers, eager to grow, client-facing interest." + ] + }, + "psychologicalIndicators": { + "type": "array", + "items": { + "type": "string" }, - required: ["summary", "metrics"] + "examples": [ + [ + "Scores high on optimism and external motivation.", + "Shows ambition but lacks self-discipline in execution.", + "Displays a desire for recognition and community; seeks more appreciation." + ] + ] }, - immediateHiringNeeds: { - type: "array", - items: { - type: "object", - additionalProperties: false, - properties: { - role: { type: "string" }, - urgency: { enum: ["low", "medium", "high"] }, - reason: { type: "string" } - }, - required: ["role", "urgency", "reason"] - } + "selfAwareness": { + "type": "string", + "examples": [ + "High – acknowledges weaknesses like lateness and disorganization." + ] }, - 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"] + "emotionalResponses": { + "type": "string", + "examples": [ + "Frustrated by campaign disorganization; would prefer closer collaboration." + ] }, - 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" } }, + "growthDesire": { + "type": "string", + "examples": [ + "Interested in becoming more client-facing and shifting toward biz dev." + ] + } + } + }, + "strengths": { + "type": "array", + "items": { + "type": "string" }, - required: ["companyPerformance", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths"] - + "examples": [ + [ + "Builds strong relationships with influencers.", + "Has sales and outreach potential.", + "Loyal, driven, and values-aligned with the company mission.", + "Open to feedback and self-improvement." + ] + ] + }, + "weaknessess": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + "Critical Issue: Disorganized and late with deliverables — confirmed by previous internal notes.", + "Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.", + "May unintentionally cause friction with campaigns team by stepping outside process boundaries." + ] + }, + "opportunities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "examples": [ + [ + { + "title": "Role Adjustment", + "description": "Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities." + }, + { + "title": "Accountability Support", + "description": "Pair with a high-output implementer (new hire) to balance Gentry’s strategic skills." + } + ] + ] + }, + "risks": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "Without strict structure, Gentry’s performance will stay flat or become a bottleneck.", + "If kept in a dual-role (recruiting + outreach), productivity will suffer.", + "He needs system constraints and direct oversight to stay focused." + ] + ] + }, + "recommendations": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.", + "Pair him with a new hire who is ultra-organized and can execute on Gentry’s deals." + ] + ] + }, + "gradingOverview": { + "grade": { "type": "string" }, + "reliability": { "type": "number" }, + "roleFit": { "type": "number" }, + "scalability": { "type": "number" }, + "output": { "type": "number" }, + "initiative": { "type": "number" } } } -}; +} + +RESPONSE_FORMAT_COMPANY = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CompanyReport", + "type": "object", + "properties": { + "id": { "type": "string" }, + "createdAt": { "type": "number" }, + "overview": { + "type": "object", + "properties": { + "totalEmployees": { "type": "number" }, + "departmentBreakdown": { + "type": "array", + "items": { + "type": "object", + "properties": { + "department": { "type": "string" }, + "count": { "type": "number" } + }, + "required": ["department", "count"] + } + }, + "submissionRate": { "type": "number" }, + "lastUpdated": { "type": "number" }, + "averagePerformanceScore": { "type": "number" }, + "riskLevel": { + "type": "string", + "enum": ["Low", "Medium", "High"] + } + }, + "required": ["totalEmployees", "departmentBreakdown", "submissionRate", "lastUpdated"] + }, + "weaknesses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" } + }, + "required": ["title", "description"] + } + }, + "personnelChanges": { + "type": "object", + "properties": { + "newHires": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "department": { "type": "string" }, + "role": { "type": "string" }, + "impact": { "type": "string" } + }, + "required": ["name", "department", "role"] + } + }, + "promotions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "fromRole": { "type": "string" }, + "toRole": { "type": "string" }, + "impact": { "type": "string" } + }, + "required": ["name", "fromRole", "toRole"] + } + }, + "departures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "department": { "type": "string" }, + "reason": { "type": "string" }, + "impact": { "type": "string" } + }, + "required": ["name", "department", "reason"] + } + } + }, + "required": ["newHires", "promotions", "departures"] + }, + "immediateHiringNeeds": { + "type": "array", + "items": { + "type": "object", + "properties": { + "department": { "type": "string" }, + "role": { "type": "string" }, + "priority": { + "type": "string", + "enum": ["High", "Medium", "Low"] + }, + "reasoning": { "type": "string" }, + "urgency": { + "type": "string", + "enum": ["high", "medium", "low"] + } + }, + "required": ["department", "role", "priority", "reasoning"] + } + }, + "forwardOperatingPlan": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "details": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["title", "details"] + } + }, + "strengths": { + "type": "array", + "items": { "type": "string" } + }, + "organizationalImpactSummary": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": [ + "Mission Critical", + "Highly Valuable", + "Core Support", + "Low Criticality" + ] + }, + "employees": { + "type": "array", + "items": { + "type": "object", + "properties": { + "employeeName": { "type": "string" }, + "impact": { "type": "string" }, + "description": { "type": "string" }, + "suggestedPay": { "type": "string", "description": "Suggested yearly wage for the employee", "example": "$70,000" } + }, + "required": ["employeeName", "impact", "description", "suggestedPay"] + } + } + }, + "required": ["category", "employees"] + } + }, + "gradingBreakdown": { + "type": "array", + "items": { + "type": "object", + "properties": { + "departmentNameShort": { "type": "string" }, + "departmentName": { "type": "string" }, + "lead": { "type": "string" }, + "support": { "type": "string" }, + "departmentGrade": { "type": "string" }, + "executiveSummary": { "type": "string" }, + "teamScores": { + "type": "array", + "items": { + "type": "object", + "properties": { + "employeeName": { "type": "string" }, + "grade": { "type": "string" }, + "reliability": { "type": "number" }, + "roleFit": { "type": "number" }, + "scalability": { "type": "number" }, + "output": { "type": "number" }, + "initiative": { "type": "number" } + }, + "required": [ + "employeeName", + "grade", + "reliability", + "roleFit", + "scalability", + "output", + "initiative" + ] + } + } + }, + "required": [ + "departmentNameShort", + "departmentName", + "lead", + "support", + "departmentGrade", + "executiveSummary", + "teamScores" + ] + } + }, + "executiveSummary": { "type": "string" } + }, + "required": [ + "id", + "createdAt", + "overview", + "weaknesses", + "personnelChanges", + "immediateHiringNeeds", + "strengths", + "gradingBreakdown", + "executiveSummary" + ] +} // Helper function to generate OTP @@ -714,22 +1009,22 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { const orgData = orgDoc.exists ? orgDoc.data() : {}; // Prepare company context (onboarding data) - const companyContext = { + let 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 }; - + if (orgData.onboardingData) { + companyContext = { + ...companyContext, + ...orgData.onboardingData + }; + } // Prepare submission data const submissionData = { employeeId: finalEmployeeId, answers, submittedAt: Date.now(), - status: "completed" + status: "completed", + companyContext, }; // Generate the report using the existing function logic @@ -737,7 +1032,7 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { 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: +You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema: Employee Information: - Name: ${employeeData?.name || employeeData?.email || 'Unknown'} @@ -762,17 +1057,7 @@ Generate a detailed report that: 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 } -} +${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)} Be thorough, professional, and focus on actionable insights. `.trim(); @@ -796,8 +1081,13 @@ Be thorough, professional, and focus on actionable insights. const aiResponse = completion.choices[0].message.content; const parsedReport = JSON.parse(aiResponse); + console.log(parsedReport); + report = { employeeId: finalEmployeeId, + employeeName: employeeData?.name || employeeData?.email || 'Employee', + role: employeeData?.role || "Team Member", + email: employeeData?.email || 'Unknown', generatedAt: Date.now(), summary: `AI-generated performance analysis for ${employeeData?.name || employeeData?.email || 'Employee'}`, submissionId: finalEmployeeId, @@ -910,12 +1200,13 @@ exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => { 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: +You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema: Employee Information: -- Name: ${employee.name || employee.email} -- Role: ${employee.role || "Team Member"} -- Department: ${employee.department || "General"} +- Name: ${employee?.name || employee?.email || 'Unknown'} +- Role: ${employee?.role || "Team Member"} +- Department: ${employee?.department || "General"} +- Email: ${employee?.email || 'Unknown'} Employee Submission Data: ${JSON.stringify(submission, null, 2)} @@ -923,18 +1214,21 @@ ${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 +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. Be thorough but professional. - `.trim(); +Return ONLY valid JSON that matches this structure: +${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)} + +Be thorough, professional, and focus on actionable insights. +`.trim(); const completion = await openai.chat.completions.create({ model: "gpt-4o", @@ -1031,63 +1325,82 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => { return res.status(405).json({ error: "Method not allowed" }); } + const authContext = await validateAuthAndGetContext(req); + + const orgId = authContext.orgId; + if (!orgId) { + return res.status(400).json({ error: "User has no associated organizations" }); + } + const { org, submissions = [] } = req.body; if (!org) { return res.status(400).json({ error: "Organization data is required" }); } + const orgData = { + id: org.id, + name: org.name, + contextualData: org.onboardingData, + metrics: org.metrics + } + 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"); + // Use OpenAI to generate the company report + + const user = `You are a cut-and-dry expert business analyst who shys to no truths and with get a business in tip-top shape within swiftness. Return ONLY JSON that conforms to the provided schema: + +Employee Submissions: +${JSON.stringify(submissions, null, 2)} + +Company Context: +${JSON.stringify(orgData, null, 2)} + +Generate a detailed report that: +1. Evaluates the company based on all the key sections in the JSON schema, being thorough to touch on all categories and employees +2. Attempts to at your best effort further the companies success and growth potential +3. Provides clear, concise, and actionable recommendations for improvement +4. Doesn't cater to sugarcoating or vague generalities +5. Will beat the nail into the coffin of inefficiency with precise solutions, getting rid of all weak points. + +Return ONLY valid JSON that matches this JSON SCHEMA: +${JSON.stringify(RESPONSE_FORMAT_COMPANY, null, 0)} + +Be thorough, professional, and focus on actionable insights. +`; const completion = await openai.chat.completions.create({ model: "gpt-4o", - temperature: 0, // consistency - response_format: RESPONSE_FORMAT, + response_format: { type: "json_object" }, 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 = { + report = { generatedAt: Date.now(), ...parsed }; - const wiki = { - companyName: org?.name ?? parsed.wiki.companyName, - generatedAt: Date.now(), + const reportRef = db + .collection("orgs") + .doc(orgId) + .collection("companyReport") + .doc("main"); - }; - - 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); + await reportRef.set(report); console.log(report); - console.log(wiki); + return res.status(200).json({ + success: true, + report + }); } else { // Fallback to mock data when OpenAI is not available @@ -1136,13 +1449,11 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => { culture: "Collaborative and growth-oriented", generatedAt: Date.now(), }; + return res.status(200).json({ + success: true, + ...report + }); } - - 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" }); @@ -1173,7 +1484,7 @@ exports.chat = onRequest({ cors: true }, async (req, res) => { 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 are a cut-and-dry expert business analyst. You provide thoughtful, professional advice based on the employee context and company data provided. ${context ? ` @@ -1186,7 +1497,14 @@ 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. +You will discuss employees with the employer to help: +1. Evaluate the company based on all provided data, being thorough to touch on all information gathered from said employee doubled with information known about the company +2. Attempt to at your best effort further the companies success and growth potential +3. Provide clear, concise, and actionable recommendations for improvement +4. Don't cater to sugarcoating or vague generalities +5. Beat the nail into the coffin of inefficiency with precise solutions, getting rid of all weak points. + +Provide helpful, actionable insights while maintaining professional tone and focusing on critical must-know knowledge and actionable recommendations. `.trim(); // Build the user message content @@ -1332,17 +1650,18 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => { const userData = userDoc.data(); - // Add user as owner to organization's employees collection - const employeeRef = orgRef.collection("employees").doc(authContext.userId); - await employeeRef.set({ + // Add owner info to organization document (owners are NOT employees) + const ownerInfo = { id: authContext.userId, - role: "owner", - isOwner: true, - joinedAt: Date.now(), - status: "active", name: userData.displayName || userData.email.split("@")[0], email: userData.email, - department: "Management", + joinedAt: Date.now() + }; + + // Update org document with owner info + await orgRef.update({ + ownerInfo: ownerInfo, + updatedAt: Date.now() }); // Add organization to user's organizations (for multi-org support) @@ -2077,12 +2396,16 @@ exports.getEmployees = onRequest({ cors: true }, async (req, res) => { return res.status(400).json({ error: "User has no associated organizations" }); } - // Get all employees + // Get all employees (excluding owners - they should not be in employees collection) const employeesSnapshot = await db.collection("orgs").doc(orgId).collection("employees").get(); const employees = []; employeesSnapshot.forEach(doc => { - employees.push({ id: doc.id, ...doc.data() }); + const employeeData = doc.data(); + // Skip any owner records that might still exist (defensive programming) + if (employeeData.role !== "owner" && !employeeData.isOwner) { + employees.push({ id: doc.id, ...employeeData }); + } }); res.json({ @@ -2305,13 +2628,15 @@ exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => { } // Get all company reports - const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("fullCompanyReports").get(); - const reports = []; + const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("companyReport").doc("main").get(); - reportsSnapshot.forEach(doc => { - reports.push({ id: doc.id, ...doc.data() }); - }); + const reportsData = reportsSnapshot.data(); + const reports = reportsData ? [reportsData] : []; + // Convert the reports object to an array + // for (const [id, report] of Object.entries(reportsData || {})) { + // reports.push({ id, ...report }); + // } // Sort by creation date (newest first) reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); @@ -2487,4 +2812,82 @@ exports.deleteImage = onRequest({ cors: true }, async (req, res) => { console.error("Delete image error:", error); res.status(500).json({ error: "Failed to delete image" }); } +}); + +// Migration Function - Remove Owners from Employees Collection +exports.migrateOwnersFromEmployees = 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 orgId = authContext.orgId; + if (!orgId) { + return res.status(400).json({ error: "User has no associated organizations" }); + } + + // Get organization document + const orgRef = db.collection("orgs").doc(orgId); + const orgDoc = await orgRef.get(); + + if (!orgDoc.exists) { + return res.status(404).json({ error: "Organization not found" }); + } + + const orgData = orgDoc.data(); + let migratedCount = 0; + let ownerInfo = null; + + // Find and remove any owners from employees collection + const employeesSnapshot = await orgRef.collection("employees").where("role", "==", "owner").get(); + + if (!employeesSnapshot.empty) { + // Get owner info before deletion + const ownerEmployee = employeesSnapshot.docs[0].data(); + ownerInfo = { + id: ownerEmployee.id, + name: ownerEmployee.name, + email: ownerEmployee.email, + joinedAt: ownerEmployee.joinedAt || Date.now() + }; + + // Delete all owner records from employees collection + const batch = db.batch(); + employeesSnapshot.docs.forEach(doc => { + batch.delete(doc.ref); + migratedCount++; + }); + await batch.commit(); + } + + // Update org document with owner info if we found it + if (ownerInfo) { + await orgRef.update({ + ownerInfo: ownerInfo, + updatedAt: Date.now() + }); + } + + res.json({ + success: true, + message: `Migration completed. Removed ${migratedCount} owner record(s) from employees collection.`, + migratedCount, + ownerInfo + }); + } catch (error) { + console.error("Migration 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 complete migration" }); + } }); \ No newline at end of file diff --git a/index.css b/index.css index 6b5102e..89a22b4 100644 --- a/index.css +++ b/index.css @@ -16,67 +16,82 @@ @keyframes blinkLightGreen { - 0%, - 33% { - box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), - inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #a5ffc075; - + 0% { + box-shadow: inset 1px 0px 3px -1px rgba(255, 255, 255, 0.2), + inset -1px 0px 3px 1px rgba(0, 0, 0, 0.15), + 0px 0px 2px 1px #32ff6f67; } - 33%, - 66% { - box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), - inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15); + 50% { + box-shadow: inset -1px 0px 3px 1px rgba(255, 255, 255, 0.2), + inset 1px 0px 3px -1px rgba(0, 0, 0, 0.15); + } + + 100% { + box-shadow: inset 1px 0px 3px -1px rgba(255, 255, 255, 0.2), + inset -1px 0px 3px 1px rgba(0, 0, 0, 0.15), + 0px 0px 2px 1px #32ff6f67; } } @keyframes blinkLightYellow { - 0%, - 33% { + 0% { box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #f7f3c275; + 0px 0px 2px 1px #ffef3c6c; } - 33%, - 66% { + 50% { box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15); } + + 100% { + box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), + inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), + 0px 0px 2px 1px #ffef3c6c; + } } @keyframes blinkLightBlue { - 0%, - 33% { + 0% { box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #a5d8ff75; + 0px 0px 2px 1px #39a2f362; } - 33%, - 66% { + 50% { box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15); } + + 100% { + box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), + inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), + 0px 0px 2px 1px #39a2f362; + } } @keyframes blinkLightRed { - 0%, - 33% { + 0% { box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #f63d6875; + 0px 0px 2px 1px #ff2d5e63; } - 33%, - 66% { + 50% { box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15); } + + 100% { + box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), + inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), + 0px 0px 2px 1px #ff2d5e63; + } } .blinkLightBlue, @@ -93,15 +108,15 @@ animation-name: blinkLightBlue; box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #a5d8ff75; - border: solid 1px #54c2e456; + 0px 0px 2px 1px #39a2f32d; + border: solid 1px #39a2f362; } .blinkLightGreen { animation-name: blinkLightGreen; box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #a5ffc075; + 0px 0px 2px 1px #a5ffc075; border: solid 1px rgba(187, 248, 185, 0.31); } @@ -109,7 +124,7 @@ animation-name: blinkLightRed; box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #f63d6875; + 0px 0px 2px 1px #f63d6875; border: solid 1px #e4547656; } @@ -117,7 +132,7 @@ animation-name: blinkLightYellow; box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20), inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15), - 0px 0px 0px 2px #f7f3c275; + 0px 0px 2px 1px #f7f3c275; border: solid 1px #e4e25456; } diff --git a/src/App.tsx b/src/App.tsx index a0fe118..6ef0336 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,28 +1,42 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import { HashRouter, Routes, Route, Navigate, useParams } from 'react-router-dom'; import { ThemeProvider } from './contexts/ThemeContext'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import { UserOrganizationsProvider, useUserOrganizations } from './contexts/UserOrganizationsContext'; import { OrgProvider, useOrg } from './contexts/OrgContext'; -import { Layout } from './components/UiKit'; -import CompanyWiki from './pages/CompanyWiki'; -// import Report from '../deprecated/pages/EmployeeData'; -import Reports from './pages/Reports'; -import Submissions from './pages/Submissions'; -import Chat from './pages/Chat'; -import HelpNew from './pages/HelpNew'; -import SettingsNew from './pages/SettingsNew'; -import HelpAndSettings from './pages/HelpAndSettings'; -import ModernLogin from './pages/Login'; -import OrgSelection from './pages/OrgSelection'; -import Onboarding from './pages/Onboarding'; -import EmployeeQuestionnaire from './pages/EmployeeQuestionnaire'; -import EmployeeQuestionnaireNew from './pages/EmployeeQuestionnaireNew'; -import EmployeeQuestionnaireSteps from './pages/EmployeeQuestionnaireSteps'; -import QuestionTypesDemo from './pages/QuestionTypesDemo'; -import FormsDashboard from './pages/FormsDashboard'; -import QuestionnaireComplete from './pages/QuestionnaireComplete'; -import SubscriptionSetup from './pages/SubscriptionSetup'; + +// Lazy load all page components for better performance +const Layout = React.lazy(() => import('./components/UiKit').then(module => ({ default: module.Layout }))); +const CompanyWiki = React.lazy(() => import('./pages/CompanyWiki')); +const Reports = React.lazy(() => import('./pages/Reports')); +const Submissions = React.lazy(() => import('./pages/Submissions')); +const Chat = React.lazy(() => import('./pages/Chat')); +const HelpNew = React.lazy(() => import('./pages/HelpNew')); +const SettingsNew = React.lazy(() => import('./pages/SettingsNew')); +const ModernLogin = React.lazy(() => import('./pages/Login')); +const OrgSelection = React.lazy(() => import('./pages/OrgSelection')); +const Onboarding = React.lazy(() => import('./pages/Onboarding')); +const EmployeeQuestionnaire = React.lazy(() => import('./pages/EmployeeQuestionnaire')); +const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaireNew')); +const EmployeeQuestionnaireSteps = React.lazy(() => import('./pages/EmployeeQuestionnaireSteps')); +const QuestionTypesDemo = React.lazy(() => import('./pages/QuestionTypesDemo')); +const FormsDashboard = React.lazy(() => import('./pages/FormsDashboard')); +const QuestionnaireComplete = React.lazy(() => import('./pages/QuestionnaireComplete')); +const SubscriptionSetup = React.lazy(() => import('./pages/SubscriptionSetup')); + +// Loading component for Suspense fallback +const LoadingSpinner: React.FC = () => ( +
+
+
+); + +// Suspense wrapper for lazy components +const SuspenseWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + }> + {children} + +); const RequireAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { user, loading } = useAuth(); @@ -107,23 +121,23 @@ function App() { - } /> - } /> + } /> + } /> {/* } /> */} {/* Employee questionnaire - no auth needed, uses invite code */} - } /> - } /> + } /> + } /> {/* Legacy employee questionnaire route for backwards compatibility */} - } /> + } /> {/* Organization Selection - after auth, before entering app */} - + } /> @@ -133,7 +147,7 @@ function App() { path="/subscription-setup" element={ - + } /> @@ -145,7 +159,7 @@ function App() { - + @@ -159,7 +173,7 @@ function App() { - + @@ -172,7 +186,7 @@ function App() { - + @@ -185,14 +199,14 @@ function App() { - + } /> - } /> + } /> {/* New Figma Chat Implementation - Standalone route */} - + @@ -212,13 +226,13 @@ function App() { {/* New Figma Help Implementation - Standalone route */} - + @@ -228,13 +242,13 @@ function App() { {/* New Figma Settings Implementation - Standalone route */} - + @@ -249,7 +263,7 @@ function App() { - + @@ -257,11 +271,11 @@ function App() { } > } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + {/* } /> + } /> */} {/* Debug routes */} @@ -271,7 +285,7 @@ function App() { - + @@ -283,7 +297,7 @@ function App() { - + diff --git a/src/components/OwnerMigrationUtility.tsx b/src/components/OwnerMigrationUtility.tsx new file mode 100644 index 0000000..2a74ea4 --- /dev/null +++ b/src/components/OwnerMigrationUtility.tsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { secureApi } from '../services/secureApi'; + +/** + * Migration utility component to remove company owners from employees collection + * This fixes the architectural issue where owners were incorrectly stored as employees + */ +const OwnerMigrationUtility: React.FC = () => { + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const runMigration = async () => { + setIsRunning(true); + setError(null); + setResult(null); + + try { + const migrationResult = await secureApi.migrateOwnersFromEmployees(); + setResult(migrationResult); + console.log('Migration completed:', migrationResult); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Migration failed'; + setError(errorMessage); + console.error('Migration error:', err); + } finally { + setIsRunning(false); + } + }; + + return ( +
+
+
+ + + +
+
+

+ Data Migration Required +

+
+

+ Your organization data needs to be migrated to fix an architectural issue. + Previously, company owners were incorrectly stored in the employees collection. + This migration will move your owner data to the correct location. +

+
+
+
+ +
+
+
+
+ + {result && ( +
+
+
+ + + +
+
+

+ Migration Completed Successfully +

+
+

Migrated {result.migratedCount} owner record(s) from employees collection.

+ {result.ownerInfo && ( +

+ Owner: {result.ownerInfo.name} ({result.ownerInfo.email}) +

+ )} +
+
+
+
+ )} + + {error && ( +
+
+
+ + + +
+
+

+ Migration Failed +

+
+

{error}

+
+
+
+
+ )} +
+ ); +}; + +export default OwnerMigrationUtility; \ No newline at end of file diff --git a/src/components/UiKit.tsx b/src/components/UiKit.tsx index b9a63e5..6df8904 100644 --- a/src/components/UiKit.tsx +++ b/src/components/UiKit.tsx @@ -309,13 +309,13 @@ interface ButtonProps extends React.ButtonHTMLAttributes { } export const Button: React.FC = ({ children, variant = 'primary', size = 'md', className, ...props }) => { - const baseClasses = 'inline-flex items-center justify-center font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed'; + const baseClasses = 'inline-flex items-center justify-center font-semibold rounded-lg focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed'; const variantClasses = { - primary: 'bg-[--accent] text-[--accent-text] hover:bg-[--accent-hover] focus:ring-[--accent]', - secondary: 'bg-[--button-secondary-bg] text-[--text-primary] hover:bg-[--button-secondary-hover] focus:ring-[--accent] border border-[--border-color]', + primary: 'bg-[--Brand-Orange] text-[--accent-text] hover:bg-blue-400 focus:ring-[--accent-hover]', + secondary: 'bg-[--button-secondary-bg] text-[--text-primary] hover:bg-[--button-secondary-hover] focus:ring-[--accent]', danger: 'bg-[--status-red] text-white hover:bg-red-700 focus:ring-red-500', - ghost: 'bg-transparent text-[--text-primary] hover:bg-[--background-tertiary]' + ghost: 'bg-[--Neutrals-NeutralSlate100] text-[--text-primary] hover:bg-[--background-tertiary]' }; const sizeClasses = { diff --git a/src/components/figma/FigmaEmployeeForms.tsx b/src/components/figma/FigmaEmployeeForms.tsx index 8fa6db7..fb852b3 100644 --- a/src/components/figma/FigmaEmployeeForms.tsx +++ b/src/components/figma/FigmaEmployeeForms.tsx @@ -62,7 +62,7 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection sectionName }) => { return ( -
+
{Array.from({ length: 7 }, (_, index) => { const isActive = index === currentSection - 1; @@ -70,11 +70,11 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection
{isActive ? ( - + ) : ( - + )}
@@ -139,7 +139,7 @@ export const SectionIntro: React.FC<{ description: string; onStart: () => void; imageUrl?: string; -}> = ({ sectionNumber, title, description, onStart, imageUrl = "https://placehold.co/560x682" }) => { +}> = ({ sectionNumber, title, description, onStart, imageUrl = "/image/onboarding-robot.png" }) => { return (
@@ -167,7 +167,7 @@ export const SectionIntro: React.FC<{
-
+
{title}
@@ -286,8 +286,8 @@ export const TextAreaQuestion: React.FC<{ placeholder?: string; }> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, placeholder = "Type your answer...." }) => { return ( -
-
+
+
{question} @@ -341,7 +341,7 @@ export const TextAreaQuestion: React.FC<{ {onSkip && ( @@ -375,13 +375,13 @@ export const RatingScaleQuestion: React.FC<{ scale?: number; }> = ({ question, leftLabel, rightLabel, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, scale = 10 }) => { return ( -
-
+
+
{question}
-
+
{leftLabel}
@@ -392,10 +392,10 @@ export const RatingScaleQuestion: React.FC<{