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.
+
+
+
+
+
+ {isRunning ? 'Running Migration...' : 'Run Migration'}
+
+
+
+
+
+
+ {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
+
+
+
+
+
+ )}
+
+ );
+};
+
+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<{
Start
@@ -175,7 +175,7 @@ export const SectionIntro: React.FC<{
-
+
@@ -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 && (
Skip
@@ -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<{
onChange(ratingValue)}
- className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden transition-colors ${isSelected ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-neutral-200'
+ className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden transition-colors ${isSelected ? 'bg-[--Neutrals-NeutralSlate50]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate50]'
}`}
>
-
{ratingValue}
@@ -421,7 +421,7 @@ export const RatingScaleQuestion: React.FC<{
Next
@@ -474,8 +474,8 @@ export const YesNoChoice: React.FC<{
sectionName?: string;
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
return (
-
-
+
+
{question}
@@ -483,20 +483,20 @@ export const YesNoChoice: React.FC<{
onChange('No')}
- className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'No' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-neutral-200'
+ className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'No' ? 'bg-[--Neutrals-NeutralSlate50]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate50]'
}`}
>
-
No
onChange('Yes')}
- className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'Yes' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-neutral-200'
+ className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'Yes' ? 'bg-[--Neutrals-NeutralSlate50]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate50]'
}`}
>
-
Yes
@@ -517,7 +517,7 @@ export const YesNoChoice: React.FC<{
Next
@@ -560,7 +560,7 @@ export const YesNoChoice: React.FC<{
// Thank You Page Component
export const ThankYouPage: React.FC = () => {
return (
-
+
@@ -581,9 +581,9 @@ export const ThankYouPage: React.FC = () => {
-
-
-
+
+
+
diff --git a/src/components/figma/Sidebar.tsx b/src/components/figma/Sidebar.tsx
index d21ef02..07d1459 100644
--- a/src/components/figma/Sidebar.tsx
+++ b/src/components/figma/Sidebar.tsx
@@ -1,8 +1,9 @@
-import React, { useState, ReactNode } from 'react';
+import React, { useState, ReactNode, useRef, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Button, PlusIcon, CopyIcon } from '../UiKit';
import { useOrg } from '../../contexts/OrgContext';
import { useAuth } from '../../contexts/AuthContext';
+import { useUserOrganizations } from '../../contexts/UserOrganizationsContext';
interface SidebarProps {
companyName?: string;
@@ -11,13 +12,48 @@ interface SidebarProps {
export default function Sidebar({ companyName = "Zitlac Media", collapsed = false }: SidebarProps) {
const { org, issueInviteViaApi } = useOrg();
+ const { createOrganization, selectOrganization, organizations, refreshOrganizations } = useUserOrganizations();
const { signOutUser } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const [showInviteModal, setShowInviteModal] = useState(false);
+ const [showOrgDropdown, setShowOrgDropdown] = useState(false);
+ const [showCreateOrgModal, setShowCreateOrgModal] = useState(false);
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
+ const [createOrgForm, setCreateOrgForm] = useState({ name: '', description: '' });
const [inviteLink, setInviteLink] = useState('');
const [emailLink, setEmailLink] = useState('');
+ const dropdownRef = useRef
(null);
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setShowOrgDropdown(false);
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const handleCreateOrg = async () => {
+ try {
+ let { orgId } = await createOrganization(
+ createOrgForm.name
+ );
+ setCreateOrgForm({ name: '', description: '' });
+ setShowCreateOrgModal(false);
+ selectOrganization(orgId);
+ navigate(`/company-wiki`);
+ } catch (error) {
+ console.error('Failed to create organization:', error);
+ }
+ };
+
+ const handleOrgSwitch = (orgId: string) => {
+ selectOrganization(orgId);
+ setShowOrgDropdown(false);
+ };
const handleInvite = async () => {
try {
@@ -125,67 +161,151 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
const handleNavClick = (path: string) => {
navigate(path);
};
-
return (
{/* Header Section */}
- {/* Company Selector */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {/* Company Selector Dropdown */}
+
+
refreshOrganizations() && setShowOrgDropdown(!showOrgDropdown)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
{org?.name || 'Select Organization'}
+
+
+
+
+
+ {/* Dropdown Menu */}
+ {showOrgDropdown && (
+
+
+ {/* Current Organizations */}
+ {organizations.map((organization) => (
+
handleOrgSwitch(organization.orgId)}
+ >
+
+ {organization.name.charAt(0).toUpperCase()}
+
+
+
{organization.name}
+ {organization.name && (
+
{organization.name}
+ )}
+
+ {org?.orgId === organization.orgId && (
+
+ )}
+
+ ))}
+
+ {/* Divider */}
+ {organizations.length > 0 && (
+
+ )}
+
+ {/* Create New Organization */}
+
{
+ setShowOrgDropdown(false);
+ setShowCreateOrgModal(true);
+ }}
+ >
+
+
+
Create New Organization
+
-
-
-
+ )}
{/* Navigation Items */}
@@ -202,7 +322,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
>
{React.cloneElement(item.icon, {
- stroke: item.active ? "var(--Brand-Orange, #5E48FC)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)"
+ stroke: item.active ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)"
})}
+ {/* Create Organization Modal */}
+ {showCreateOrgModal && (
+
+
+
Create New Organization
+
+
+ Organization Name
+ setCreateOrgForm(prev => ({ ...prev, name: e.target.value }))}
+ className="w-full px-3 py-2.5 border border-[--Neutrals-NeutralSlate200] rounded-xl bg-[--Neutrals-NeutralSlate0] text-[--Neutrals-NeutralSlate950] focus:outline-none focus:ring-2 focus:ring-[--Brand-Orange] focus:border-transparent"
+ placeholder="Enter organization name"
+ />
+
+
+ Description (Optional)
+
+
+
+ {
+ setShowCreateOrgModal(false);
+ setCreateOrgForm({ name: '', description: '' });
+ }}
+ >
+ Cancel
+
+
+ Create Organization
+
+
+
+
+ )}
+
+ {/* Invite Employee Modal */}
{showInviteModal && (
@@ -332,12 +503,22 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
{/* Settings */}
handleNavClick("/settings")}
- className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2 cursor-pointer hover:bg-[--Neutrals-NeutralSlate50]"
+ className={`w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2 cursor-pointer ${location.pathname === "/settings"
+ ? 'bg-[--Neutrals-NeutralSlate100]'
+ : 'hover:bg-[--Neutrals-NeutralSlate50]'
+ }`}
>
- {settingsIcon}
+ {React.cloneElement(settingsIcon, {
+ stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)"
+ })}
+
+
+ Settings
-
Settings
{/* Build Report Card */}
@@ -360,7 +541,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
-
Build [Company]'s Report
+
Build {org.name}'s Report
Share this form with your team members to capture valuable info about your company to train Auditly.
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index c789c19..0f2f581 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -1,206 +1,209 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
-import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
-import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
-import { API_URL } from '../constants';
-
-interface AuthContextType {
- user: User | null;
- loading: boolean;
- signInWithGoogle: () => Promise
;
- signOutUser: () => Promise;
- signInWithEmail: (email: string, password: string) => Promise;
- signUpWithEmail: (email: string, password: string, displayName?: string) => Promise;
- sendOTP: (email: string, inviteCode?: string) => Promise;
- verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise;
-}
-
-const AuthContext = createContext(undefined);
-
-export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const [user, setUser] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
-
- if (isFirebaseConfigured) {
- // Firebase mode: Set up proper Firebase auth state listener
- const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
- console.log('Firebase auth state changed:', firebaseUser?.email);
- if (firebaseUser) {
- setUser(firebaseUser);
- } else {
- // Check for OTP session as fallback
- const sessionUser = localStorage.getItem('auditly_demo_session');
- if (sessionUser) {
- try {
- const parsedUser = JSON.parse(sessionUser);
- console.log('Restoring OTP session for:', parsedUser.email);
- setUser(parsedUser as User);
- } catch (error) {
- console.error('Failed to parse session user:', error);
- localStorage.removeItem('auditly_demo_session');
- setUser(null);
- }
- } else {
- setUser(null);
- }
- }
- setLoading(false);
- });
-
- return unsubscribe;
- } else {
- // Demo/OTP mode: Check localStorage for persisted session
- console.log('Checking for persisted OTP session');
- const sessionUser = localStorage.getItem('auditly_demo_session');
- if (sessionUser) {
- try {
- const parsedUser = JSON.parse(sessionUser);
- console.log('Restoring session for:', parsedUser.email);
- setUser(parsedUser as User);
- } catch (error) {
- console.error('Failed to parse session user:', error);
- localStorage.removeItem('auditly_demo_session');
- setUser(null);
- }
- } else {
- setUser(null);
- }
- setLoading(false);
-
- return () => { };
- }
- }, []);
-
- const signInWithGoogle = async () => {
- if (!isFirebaseConfigured) {
- // No-op in demo mode
- return;
- }
- await signInWithPopup(auth, googleProvider);
- };
-
- const signOutUser = async () => {
- try {
- // Sign out from Firebase if configured and user is signed in via Firebase
- if (isFirebaseConfigured && auth.currentUser) {
- await signOut(auth);
- console.log('Firebase signout completed');
- }
- } catch (error) {
- console.error('Firebase signout error:', error);
- }
-
- // Always clear all local session data
- localStorage.removeItem('auditly_demo_session');
- localStorage.removeItem('auditly_auth_token');
- localStorage.removeItem('auditly_selected_org');
- sessionStorage.clear();
-
- setUser(null);
- console.log('User signed out and all sessions cleared');
- };
-
- const signInWithEmail = async (email: string, password: string) => {
- console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
- try {
- console.log('Attempting Firebase auth');
- await signInWithEmailAndPassword(auth, email, password);
- } catch (e: any) {
- const code = e?.code || '';
- console.error('Firebase Auth Error:', code, e?.message);
- if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
- console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
- const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: email.split('@')[0] } as unknown as User;
- setUser(mock);
- return;
- }
- throw e;
- }
- };
-
- const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
- try {
- const cred = await createUserWithEmailAndPassword(auth, email, password);
- if (displayName) {
- try { await updateProfile(cred.user, { displayName }); } catch { }
- }
- } catch (e: any) {
- const code = e?.code || '';
- if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
- console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
- const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: displayName || email.split('@')[0] } as unknown as User;
- setUser(mock);
- return;
- }
- throw e;
- }
- };
-
- const sendOTP = async (email: string, inviteCode?: string) => {
- const response = await fetch(`${API_URL}/sendOTP`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email, inviteCode })
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || 'Failed to send OTP');
- }
-
- return response.json();
- };
-
- const verifyOTP = async (email: string, otp: string, inviteCode?: string) => {
- const response = await fetch(`${API_URL}/verifyOTP`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email, otp, inviteCode })
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || 'Failed to verify OTP');
- }
-
- const data = await response.json();
-
- // Set user in auth context
- const mockUser = {
- uid: data.user.uid,
- email: data.user.email,
- displayName: data.user.displayName,
- emailVerified: true
- } as unknown as User;
-
- setUser(mockUser);
- localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
- localStorage.setItem('auditly_auth_token', data.token);
-
- return data;
- };
-
-
- return (
-
- {children}
-
- );
-};
-
-export const useAuth = () => {
- const ctx = useContext(AuthContext);
- if (!ctx) throw new Error('useAuth must be used within AuthProvider');
- return ctx;
-};
+import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
+import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
+import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
+import { API_URL } from '../constants';
+
+interface AuthContextType {
+ user: User | null;
+ loading: boolean;
+ signInWithGoogle: () => Promise;
+ signOutUser: () => Promise;
+ signInWithEmail: (email: string, password: string) => Promise;
+ signUpWithEmail: (email: string, password: string, displayName?: string) => Promise;
+ sendOTP: (email: string, inviteCode?: string) => Promise;
+ verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise;
+}
+
+const AuthContext = createContext(undefined);
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
+
+ if (isFirebaseConfigured) {
+ // Firebase mode: Set up proper Firebase auth state listener
+ const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
+ console.log('Firebase auth state changed:', firebaseUser?.email);
+ if (firebaseUser) {
+ setUser(firebaseUser);
+ } else {
+ // Check for OTP session as fallback
+ const sessionUser = localStorage.getItem('auditly_demo_session');
+ if (sessionUser) {
+ try {
+ const parsedUser = JSON.parse(sessionUser);
+ console.log('Restoring OTP session for:', parsedUser.email);
+ setUser(parsedUser as User);
+ } catch (error) {
+ console.error('Failed to parse session user:', error);
+ localStorage.removeItem('auditly_demo_session');
+ setUser(null);
+ }
+ } else {
+ setUser(null);
+ }
+ }
+ setLoading(false);
+ });
+
+ return unsubscribe;
+ } else {
+ // Demo/OTP mode: Check localStorage for persisted session
+ console.log('Checking for persisted OTP session');
+ const sessionUser = localStorage.getItem('auditly_demo_session');
+ if (sessionUser) {
+ try {
+ const parsedUser = JSON.parse(sessionUser);
+ console.log('Restoring session for:', parsedUser.email);
+ setUser(parsedUser as User);
+ } catch (error) {
+ console.error('Failed to parse session user:', error);
+ localStorage.removeItem('auditly_demo_session');
+ setUser(null);
+ }
+ } else {
+ setUser(null);
+ }
+ setLoading(false);
+
+ return () => { };
+ }
+ }, []);
+
+ const signInWithGoogle = useCallback(async () => {
+ if (!isFirebaseConfigured) {
+ // No-op in demo mode
+ return;
+ }
+ await signInWithPopup(auth, googleProvider);
+ }, []);
+
+ const signOutUser = useCallback(async () => {
+ try {
+ // Sign out from Firebase if configured and user is signed in via Firebase
+ if (isFirebaseConfigured && auth.currentUser) {
+ await signOut(auth);
+ console.log('Firebase signout completed');
+ }
+ } catch (error) {
+ console.error('Firebase signout error:', error);
+ }
+
+ // Always clear all local session data
+ localStorage.removeItem('auditly_demo_session');
+ localStorage.removeItem('auditly_auth_token');
+ localStorage.removeItem('auditly_selected_org');
+ sessionStorage.clear();
+
+ setUser(null);
+ console.log('User signed out and all sessions cleared');
+ }, []);
+
+ const signInWithEmail = useCallback(async (email: string, password: string) => {
+ console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
+ try {
+ console.log('Attempting Firebase auth');
+ await signInWithEmailAndPassword(auth, email, password);
+ } catch (e: any) {
+ const code = e?.code || '';
+ console.error('Firebase Auth Error:', code, e?.message);
+ if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
+ console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
+ const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: email.split('@')[0] } as unknown as User;
+ setUser(mock);
+ return;
+ }
+ throw e;
+ }
+ }, []);
+
+ const signUpWithEmail = useCallback(async (email: string, password: string, displayName?: string) => {
+ try {
+ const cred = await createUserWithEmailAndPassword(auth, email, password);
+ if (displayName) {
+ try { await updateProfile(cred.user, { displayName }); } catch { }
+ }
+ } catch (e: any) {
+ const code = e?.code || '';
+ if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
+ console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
+ const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: displayName || email.split('@')[0] } as unknown as User;
+ setUser(mock);
+ return;
+ }
+ throw e;
+ }
+ }, []);
+
+ const sendOTP = useCallback(async (email: string, inviteCode?: string) => {
+ const response = await fetch(`${API_URL}/sendOTP`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, inviteCode })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to send OTP');
+ }
+
+ return response.json();
+ }, []);
+
+ const verifyOTP = useCallback(async (email: string, otp: string, inviteCode?: string) => {
+ const response = await fetch(`${API_URL}/verifyOTP`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, otp, inviteCode })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to verify OTP');
+ }
+
+ const data = await response.json();
+
+ // Set user in auth context
+ const mockUser = {
+ uid: data.user.uid,
+ email: data.user.email,
+ displayName: data.user.displayName,
+ emailVerified: true
+ } as unknown as User;
+
+ setUser(mockUser);
+ localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
+ localStorage.setItem('auditly_auth_token', data.token);
+
+ return data;
+ }, []);
+
+ // Memoize the context value to prevent unnecessary re-renders
+ const contextValue = useMemo(() => ({
+ user,
+ loading,
+ signInWithGoogle,
+ signOutUser,
+ signInWithEmail,
+ signUpWithEmail,
+ sendOTP,
+ verifyOTP,
+ }), [user, loading, signInWithGoogle, signOutUser, signInWithEmail, signUpWithEmail, sendOTP, verifyOTP]);
+
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const ctx = useContext(AuthContext);
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider');
+ return ctx;
+};
diff --git a/src/contexts/OrgContext.tsx b/src/contexts/OrgContext.tsx
index a563797..f3be9df 100644
--- a/src/contexts/OrgContext.tsx
+++ b/src/contexts/OrgContext.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
+import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { useAuth } from './AuthContext';
import { Employee, EmployeeReport, Submission, CompanyReport } from '../types';
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
@@ -9,11 +9,19 @@ import { secureApi } from '../services/secureApi';
interface OrgData {
orgId: string;
+ name?: string;
companyName?: string;
onboardingData?: Record;
companyLogo?: string;
updatedAt?: number;
onboardingCompleted?: boolean;
+ ownerId?: string;
+ ownerInfo?: {
+ id: string;
+ name: string;
+ email: string;
+ joinedAt: number;
+ };
}
interface OrgContextType {
@@ -63,7 +71,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
// Use the provided selectedOrgId instead of deriving from user
const orgId = selectedOrgId;
- // Load initial data using secure API
+ // Load initial data using secure API - memoized to prevent unnecessary re-runs
useEffect(() => {
if (!orgId || !user?.uid) {
setLoading(false);
@@ -76,54 +84,57 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
try {
setLoading(true);
- // Load organization data
- try {
- const orgData = await secureApi.getOrgData();
- setOrg({ orgId, ...orgData });
- } catch (error) {
- console.warn('Could not load org data, creating default:', error);
- // Create default org if not found
+ // Batch all API calls for better performance
+ const [orgData, employeesData, submissionsData, reportsData, companyReportsData] = await Promise.allSettled([
+ secureApi.getOrgData().catch(() => null),
+ secureApi.getEmployees().catch(() => []),
+ secureApi.getSubmissions().catch(() => ({})),
+ secureApi.getReports().catch(() => ({})),
+ secureApi.getCompanyReports().catch(() => [])
+ ]);
+
+ // Process organization data
+ if (orgData.status === 'fulfilled' && orgData.value) {
+ setOrg({ orgId, ...orgData.value });
+ } else {
+ console.warn('Could not load org data, creating default');
const defaultOrg = { name: 'Your Company', onboardingCompleted: false };
await secureApi.updateOrgData(defaultOrg);
setOrg({ orgId, ...defaultOrg });
}
- // Load employees
- try {
- const employeesData = await secureApi.getEmployees();
- setEmployees(employeesData.map(emp => ({
+ // Process employees data
+ if (employeesData.status === 'fulfilled') {
+ setEmployees(employeesData.value.map(emp => ({
...emp,
initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email?.substring(0, 2).toUpperCase() || 'U'
})));
- } catch (error) {
- console.warn('Could not load employees:', error);
+ } else {
+ console.warn('Could not load employees');
setEmployees([]);
}
- // Load submissions
- try {
- const submissionsData = await secureApi.getSubmissions();
- setSubmissions(submissionsData);
- } catch (error) {
- console.warn('Could not load submissions:', error);
+ // Process submissions data
+ if (submissionsData.status === 'fulfilled') {
+ setSubmissions(submissionsData.value);
+ } else {
+ console.warn('Could not load submissions');
setSubmissions({});
}
- // Load reports
- try {
- const reportsData = await secureApi.getReports();
- setReports(reportsData as Record);
- } catch (error) {
- console.warn('Could not load reports:', error);
+ // Process reports data
+ if (reportsData.status === 'fulfilled') {
+ setReports(reportsData.value as Record);
+ } else {
+ console.warn('Could not load reports');
setReports({});
}
- // Load company reports
- try {
- const companyReportsData = await secureApi.getCompanyReports();
- setFullCompanyReports(companyReportsData);
- } catch (error) {
- console.warn('Could not load company reports:', error);
+ // Process company reports data
+ if (companyReportsData.status === 'fulfilled') {
+ setFullCompanyReports(companyReportsData.value);
+ } else {
+ console.warn('Could not load company reports');
setFullCompanyReports([]);
}
@@ -135,9 +146,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
};
loadOrgData();
- }, [orgId, user?.uid]);
+ }, [orgId, user?.uid]); // Only re-run when orgId or user changes
- const upsertOrg = async (data: Partial) => {
+ const upsertOrg = useCallback(async (data: Partial) => {
if (!user?.uid) {
throw new Error('User authentication required');
}
@@ -164,9 +175,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.error('Failed to update organization:', error);
throw error;
}
- };
+ }, [user?.uid, org, orgId]);
- const saveReport = async (employeeId: string, report: EmployeeReport) => {
+ const saveReport = useCallback(async (employeeId: string, report: EmployeeReport) => {
if (!user?.uid) {
throw new Error('User authentication required');
}
@@ -180,7 +191,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.error('Failed to save report:', error);
throw error;
}
- };
+ }, [user?.uid]);
const inviteEmployee = async ({ name, email, role, department }: { name: string; email: string, role?: string, department?: string }) => {
console.log('inviteEmployee called:', { name, email, orgId });
@@ -199,7 +210,6 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
initials: data.employee.name ? data.employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase(),
department: data.employee.department,
role: data.employee.role,
- isOwner: false,
status: data.employee.status
};
@@ -332,17 +342,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}
// Calculate concrete metrics from actual data (no AI needed)
- // Exclude owners from employee counts - they are company wiki contributors, not employees
- const actualEmployees = employees.filter(emp => !emp.isOwner);
+ // Employees collection only contains actual employees (owners are not in this collection)
+ const actualEmployees = employees;
const totalEmployees = actualEmployees.length;
- // Only count submissions from non-owner employees
- const employeeSubmissions = Object.fromEntries(
- Object.entries(submissions).filter(([employeeId]) => {
- const employee = employees.find(emp => emp.id === employeeId);
- return employee && !employee.isOwner;
- })
- );
+ // Count submissions from employees
+ const employeeSubmissions = submissions;
const submittedEmployees = Object.keys(employeeSubmissions).length;
const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0;
@@ -356,7 +361,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
try {
// Use secure API for AI generation
- const data = await secureApi.generateCompanyWiki({
+ let response = await secureApi.generateCompanyWiki({
...org,
metrics: {
totalEmployees,
@@ -368,25 +373,25 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.log('Company insights generated via AI successfully');
// Combine concrete metrics with AI insights
- const report: CompanyReport = {
+ let report: CompanyReport = {
id: Date.now().toString(),
createdAt: Date.now(),
// Use AI-generated insights for subjective analysis
- ...(data as any),
// Override with our concrete metrics
overview: {
totalEmployees,
departmentBreakdown,
submissionRate,
lastUpdated: Date.now(),
- averagePerformanceScore: (data as any)?.overview?.averagePerformanceScore || 0,
- riskLevel: (data as any)?.overview?.riskLevel || 'Unknown'
- }
+ averagePerformanceScore: (response as any)?.overview?.averagePerformanceScore || 0,
+ riskLevel: (response as any)?.overview?.riskLevel || 'Unknown'
+ },
+ ...(response as any)
};
console.log('Final company report object:', report);
- await saveFullCompanyReport(report);
- return report;
+ // await saveFullCompanyReport(report);
+ return response;
} catch (error) {
console.error('generateCompanyReport error:', error);
throw error;
@@ -408,12 +413,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
// Use secure API for wiki generation
try {
console.log('Making API call to generateCompanyWiki...');
- const payload = await secureApi.generateCompanyWiki(orgData, Object.values(submissions || {}));
+ let response = await secureApi.generateCompanyWiki(orgData, Object.values(submissions || {}));
- console.log('API success response:', payload);
+ console.log('API success response:', response);
// Ensure the report has all required fields to prevent undefined errors
- const data: CompanyReport = {
+ const report: CompanyReport = {
id: Date.now().toString(),
createdAt: Date.now(),
overview: {
@@ -430,21 +435,22 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] },
executiveSummary: 'Company report generated successfully.',
// Override with API data if available
- ...(payload as any || {})
+ ...(response as any || {})
};
- await saveFullCompanyReport(data);
- return data;
+ // await saveFullCompanyReport(data);
+ return response;
} catch (e) {
console.error('generateCompanyWiki error, falling back to local synthetic:', e);
return generateCompanyReport();
}
};
- const isOwner = (employeeId?: string): boolean => {
- const currentEmployee = employeeId ? employees.find(e => e.id === employeeId) :
- employees.find(e => e.email === user?.email);
- return currentEmployee?.isOwner === true;
+ const isOwner = (userId?: string): boolean => {
+ // Check if the given user ID matches the org owner ID
+ // If no userId provided, check current user
+ const targetUserId = userId || user?.uid;
+ return targetUserId === org?.ownerId;
};
const getEmployeeReport = async (employeeId: string) => {
@@ -483,7 +489,141 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}
};
- const value = {
+ // Memoize functions that don't need dependencies
+ const issueInviteViaApi = useCallback(async ({ name, email, role, department }) => {
+ try {
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ const data = await secureApi.createInvitation({ name, email, role, department });
+
+ // Optimistically add employee shell (not yet active until consume)
+ setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
+ ...data.employee,
+ initials: data.employee.name ? data.employee.name.split(' ').map((n: string) => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase()
+ } as Employee]);
+ return data;
+ } catch (e) {
+ console.error('issueInviteViaApi error', e);
+ throw e;
+ }
+ }, [user?.uid]);
+
+ const getInviteStatus = useCallback(async (code: string) => {
+ try {
+ return await secureApi.getInvitationStatus(code);
+ } catch (e) {
+ console.error('getInviteStatus error', e);
+ return null;
+ }
+ }, []);
+
+ const consumeInvite = useCallback(async (code: string) => {
+ try {
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ const result = await secureApi.consumeInvitation(code, user.uid);
+
+ // Mark employee as active
+ if (result && (result as any).employee) {
+ setEmployees(prev => prev.find(e => e.id === (result as any).employee.id) ? prev : [...prev, (result as any).employee]);
+ return { ...(result as any), orgId: org?.orgId };
+ }
+ return null;
+ } catch (e) {
+ console.error('consumeInvite error', e);
+ return null;
+ }
+ }, [user?.uid, org?.orgId]);
+
+ const submitEmployeeAnswers = useCallback(async (employeeId: string, answers: Record) => {
+ try {
+ // Use secure API for submission
+ await secureApi.submitEmployeeAnswers(employeeId, answers);
+
+ // Update local state for immediate UI feedback
+ const convertedSubmission: Submission = {
+ employeeId,
+ answers: Object.entries(answers).map(([question, answer]) => ({
+ question,
+ answer
+ }))
+ };
+ setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission }));
+ return true;
+ } catch (e) {
+ console.error('submitEmployeeAnswers error', e);
+ return false;
+ }
+ }, []);
+
+ const generateEmployeeReport = useCallback(async (employee: Employee) => {
+ try {
+ console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
+
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ // Get submission data for this employee
+ const submission = submissions[employee.id];
+ if (!submission) {
+ throw new Error(`No questionnaire submission found for ${employee.name}. Please ensure they have completed the employee questionnaire first.`);
+ }
+
+ // Convert submission format for API
+ let submissionAnswers: Record = {};
+ if (submission.answers) {
+ if (Array.isArray(submission.answers)) {
+ // If answers is an array of {question, answer} objects
+ submissionAnswers = submission.answers.reduce((acc, item: any) => {
+ acc[item.question] = item.answer;
+ return acc;
+ }, {} as Record);
+ } else {
+ // If answers is already a key-value object
+ submissionAnswers = submission.answers as Record;
+ }
+ }
+
+ console.log('Submission data found:', Object.keys(submissionAnswers).length, 'answers');
+
+ // Get company report and wiki data for context
+ let companyWiki = null;
+ try {
+ const companyReports = await getFullCompanyReportHistory();
+ if (companyReports.length > 0) {
+ companyWiki = {
+ org: org,
+ companyReport: companyReports[0]
+ };
+ console.log('Including company context in employee report generation');
+ }
+ } catch (error) {
+ console.warn('Could not fetch company report for context:', error);
+ }
+
+ const data = await secureApi.generateEmployeeReport(employee, submissionAnswers, companyWiki);
+
+ if ((data as any).report) {
+ console.log('Employee report generated successfully');
+ const report = (data as any).report as EmployeeReport;
+ setReports(prev => ({ ...prev, [employee.id]: report }));
+ return report;
+ } else {
+ throw new Error('No report data received from API');
+ }
+ } catch (e) {
+ console.error('generateEmployeeReport error', e);
+ throw e; // Re-throw to allow caller to handle
+ }
+ }, [user?.uid, orgId, submissions, org, getFullCompanyReportHistory]);
+
+ // Memoize the entire context value to prevent unnecessary re-renders
+ const value = useMemo(() => ({
org,
orgId,
employees,
@@ -504,136 +644,42 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
generateCompanyWiki,
seedInitialData,
isOwner,
- issueInviteViaApi: async ({ name, email, role, department }) => {
- try {
- if (!user?.uid) {
- throw new Error('User authentication required');
- }
-
- const data = await secureApi.createInvitation({ name, email, role, department });
-
- // Optimistically add employee shell (not yet active until consume)
- setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
- ...data.employee,
- initials: data.employee.name ? data.employee.name.split(' ').map((n: string) => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase()
- } as Employee]);
- return data;
- } catch (e) {
- console.error('issueInviteViaApi error', e);
- throw e;
- }
- },
- getInviteStatus: async (code: string) => {
- try {
- return await secureApi.getInvitationStatus(code);
- } catch (e) {
- console.error('getInviteStatus error', e);
- return null;
- }
- },
- consumeInvite: async (code: string) => {
- try {
- if (!user?.uid) {
- throw new Error('User authentication required');
- }
-
- const result = await secureApi.consumeInvitation(code, user.uid);
-
- // Mark employee as active
- if (result && (result as any).employee) {
- setEmployees(prev => prev.find(e => e.id === (result as any).employee.id) ? prev : [...prev, (result as any).employee]);
- return { ...(result as any), orgId: org?.orgId };
- }
- return null;
- } catch (e) {
- console.error('consumeInvite error', e);
- return null;
- }
- },
- submitEmployeeAnswers: async (employeeId: string, answers: Record) => {
- try {
- // Use secure API for submission
- await secureApi.submitEmployeeAnswers(employeeId, answers);
-
- // Update local state for immediate UI feedback
- const convertedSubmission: Submission = {
- employeeId,
- answers: Object.entries(answers).map(([question, answer]) => ({
- question,
- answer
- }))
- };
- setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission }));
- return true;
- } catch (e) {
- console.error('submitEmployeeAnswers error', e);
- return false;
- }
- },
- generateEmployeeReport: async (employee: Employee) => {
- try {
- console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
-
- if (!user?.uid) {
- throw new Error('User authentication required');
- }
-
- // Get submission data for this employee
- const submission = submissions[employee.id];
- if (!submission) {
- throw new Error(`No questionnaire submission found for ${employee.name}. Please ensure they have completed the employee questionnaire first.`);
- }
-
- // Convert submission format for API
- let submissionAnswers: Record = {};
- if (submission.answers) {
- if (Array.isArray(submission.answers)) {
- // If answers is an array of {question, answer} objects
- submissionAnswers = submission.answers.reduce((acc, item: any) => {
- acc[item.question] = item.answer;
- return acc;
- }, {} as Record);
- } else {
- // If answers is already a key-value object
- submissionAnswers = submission.answers as Record;
- }
- }
-
- console.log('Submission data found:', Object.keys(submissionAnswers).length, 'answers');
-
- // Get company report and wiki data for context
- let companyWiki = null;
- try {
- const companyReports = await getFullCompanyReportHistory();
- if (companyReports.length > 0) {
- companyWiki = {
- org: org,
- companyReport: companyReports[0]
- };
- console.log('Including company context in employee report generation');
- }
- } catch (error) {
- console.warn('Could not fetch company report for context:', error);
- }
-
- const data = await secureApi.generateEmployeeReport(employee, submissionAnswers, companyWiki);
-
- if ((data as any).report) {
- console.log('Employee report generated successfully');
- const report = (data as any).report as EmployeeReport;
- setReports(prev => ({ ...prev, [employee.id]: report }));
- return report;
- } else {
- throw new Error('No report data received from API');
- }
- } catch (e) {
- console.error('generateEmployeeReport error', e);
- throw e; // Re-throw to allow caller to handle
- }
- },
+ issueInviteViaApi,
+ getInviteStatus,
+ consumeInvite,
+ submitEmployeeAnswers,
+ generateEmployeeReport,
getEmployeeReport,
getEmployeeReports,
- };
+ }), [
+ org,
+ orgId,
+ employees,
+ submissions,
+ reports,
+ loading,
+ upsertOrg,
+ saveReport,
+ inviteEmployee,
+ getReportVersions,
+ saveReportVersion,
+ acceptInvite,
+ saveCompanyReport,
+ getCompanyReportHistory,
+ saveFullCompanyReport,
+ getFullCompanyReportHistory,
+ generateCompanyReport,
+ generateCompanyWiki,
+ seedInitialData,
+ isOwner,
+ issueInviteViaApi,
+ getInviteStatus,
+ consumeInvite,
+ submitEmployeeAnswers,
+ generateEmployeeReport,
+ getEmployeeReport,
+ getEmployeeReports,
+ ]);
return (
diff --git a/src/contexts/UserOrganizationsContext.tsx b/src/contexts/UserOrganizationsContext.tsx
index 86968f8..719b542 100644
--- a/src/contexts/UserOrganizationsContext.tsx
+++ b/src/contexts/UserOrganizationsContext.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
+import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { useAuth } from './AuthContext';
import { isFirebaseConfigured } from '../services/firebase';
import { API_URL } from '../constants';
@@ -32,8 +32,8 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
const [selectedOrgId, setSelectedOrgId] = useState(null);
const [loading, setLoading] = useState(true);
- // Load user's organizations
- const loadOrganizations = async () => {
+ // Load user's organizations - memoized to prevent recreation
+ const loadOrganizations = useCallback(async () => {
if (!user) {
setOrganizations([]);
setLoading(false);
@@ -50,7 +50,7 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
} finally {
setLoading(false);
}
- };
+ }, [user]);
// Initialize selected org from localStorage (persistent across sessions)
useEffect(() => {
@@ -63,7 +63,7 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
// Load organizations when user changes
useEffect(() => {
loadOrganizations();
- }, [user]);
+ }, [loadOrganizations]);
// Listen for organization updates (e.g., onboarding completion)
useEffect(() => {
@@ -92,7 +92,7 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
};
}, []);
- const selectOrganization = (orgId: string) => {
+ const selectOrganization = useCallback((orgId: string) => {
console.log('Switching to organization:', orgId);
// Clear any cached data when switching organizations for security
@@ -107,9 +107,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
window.dispatchEvent(new CustomEvent('organizationChanged', {
detail: { newOrgId: orgId }
}));
- };
+ }, []);
- const createOrganization = async (name: string): Promise<{ orgId: string; requiresSubscription?: boolean }> => {
+ const createOrganization = useCallback(async (name: string): Promise<{ orgId: string; requiresSubscription?: boolean }> => {
if (!user) throw new Error('User not authenticated');
try {
@@ -135,53 +135,12 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
console.error('Failed to create organization:', error);
throw error;
}
- };
+ }, [user]);
- const joinOrganization = async (inviteCode: string): Promise => {
+ const joinOrganization = useCallback(async (inviteCode: string): Promise => {
if (!user) throw new Error('User not authenticated');
try {
- // if (!isFirebaseConfigured) {
- // // Demo mode - use server API to get and consume invite
- // const inviteStatusRes = await fetch(`/api/invitations/${inviteCode}`);
- // if (!inviteStatusRes.ok) {
- // throw new Error('Invalid or expired invite code');
- // }
-
- // const inviteData = await inviteStatusRes.json();
- // if (inviteData.used) {
- // throw new Error('Invite code has already been used');
- // }
-
- // // Consume the invite
- // const consumeRes = await fetch(`/api/invitations/${inviteCode}/consume`, {
- // method: 'POST'
- // });
- // if (!consumeRes.ok) {
- // throw new Error('Failed to consume invite');
- // }
-
- // const consumedData = await consumeRes.json();
- // const orgId = consumedData.orgId;
-
- // // Get organization data (this might be from localStorage for demo mode)
- // const orgData = demoStorage.getOrganization(orgId);
- // if (!orgData) {
- // throw new Error('Organization not found');
- // }
-
- // const userOrg: UserOrganization = {
- // orgId: orgId,
- // name: orgData.name,
- // role: 'employee',
- // onboardingCompleted: orgData.onboardingCompleted || false,
- // joinedAt: Date.now()
- // };
-
- // setOrganizations(prev => [...prev, userOrg]);
- // return orgId;
- // } else {
- // Firebase mode - use Cloud Function
// Use secure API for joining organization
const data = await secureApi.joinOrganization(inviteCode);
@@ -195,19 +154,18 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
setOrganizations(prev => [...prev, userOrg]);
return data.orgId;
- // }
} catch (error) {
console.error('Failed to join organization:', error);
throw error;
}
- };
+ }, [user]);
- const refreshOrganizations = async () => {
+ const refreshOrganizations = useCallback(async () => {
setLoading(true);
await loadOrganizations();
- };
+ }, [loadOrganizations]);
- const createCheckoutSession = async (userEmail: string): Promise<{ sessionUrl: string; sessionId: string }> => {
+ const createCheckoutSession = useCallback(async (userEmail: string): Promise<{ sessionUrl: string; sessionId: string }> => {
if (!user) throw new Error('User not authenticated');
try {
@@ -220,9 +178,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
console.error('Failed to create checkout session:', error);
throw error;
}
- };
+ }, [user]);
- const getSubscriptionStatus = async () => {
+ const getSubscriptionStatus = useCallback(async () => {
try {
const data = await secureApi.getSubscriptionStatus();
return data;
@@ -230,20 +188,33 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
console.error('Failed to get subscription status:', error);
throw error;
}
- };
+ }, []);
+
+ // Memoize the context value to prevent unnecessary re-renders
+ const contextValue = useMemo(() => ({
+ organizations,
+ selectedOrgId,
+ loading,
+ selectOrganization,
+ createOrganization,
+ joinOrganization,
+ refreshOrganizations,
+ createCheckoutSession,
+ getSubscriptionStatus
+ }), [
+ organizations,
+ selectedOrgId,
+ loading,
+ selectOrganization,
+ createOrganization,
+ joinOrganization,
+ refreshOrganizations,
+ createCheckoutSession,
+ getSubscriptionStatus
+ ]);
return (
-
+
{children}
);
@@ -255,4 +226,4 @@ export const useUserOrganizations = () => {
throw new Error('useUserOrganizations must be used within UserOrganizationsProvider');
}
return context;
-};
+};
\ No newline at end of file
diff --git a/src/data/onboardingSteps.ts b/src/data/onboardingSteps.ts
index b5e6dc6..d81c417 100644
--- a/src/data/onboardingSteps.ts
+++ b/src/data/onboardingSteps.ts
@@ -1,6 +1,8 @@
/**
* Complete 63-step onboarding configuration based on Figma designs
*/
+import { OnboardingData } from "../types";
+
export interface OnboardingStep {
id: number;
@@ -18,15 +20,14 @@ export interface OnboardingStep {
rows?: number; // for textarea
}
-export interface OnboardingData {
+export interface OnboardingFormData extends OnboardingData {
companyName: string;
yourName: string;
companyLogo: string;
- [key: string]: string | string[];
}
-export const initializeOnboardingData = (): OnboardingData => {
- const data: OnboardingData = {
+export const initializeOnboardingData = (): OnboardingFormData => {
+ const data = {
// Ensure required form fields are initialized
companyName: '',
yourName: '',
@@ -41,7 +42,7 @@ export const initializeOnboardingData = (): OnboardingData => {
}
}
});
- return data;
+ return data as OnboardingFormData;
};
export const onboardingSteps: OnboardingStep[] = [
diff --git a/src/pages/Chat.tsx b/src/pages/Chat.tsx
index 12ad0b5..021f185 100644
--- a/src/pages/Chat.tsx
+++ b/src/pages/Chat.tsx
@@ -1,677 +1,687 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { useAuth } from '../contexts/AuthContext';
-import { useOrg } from '../contexts/OrgContext';
-import { apiPost } from '../services/api';
-import Sidebar from '../components/figma/Sidebar';
-
-interface Message {
- id: string;
- role: 'user' | 'assistant';
- content: string;
- timestamp: Date;
- mentions?: Array<{ id: string; name: string }>;
- attachments?: Array<{
- name: string;
- type: string;
- size: number;
- data?: string; // Base64 encoded file data
- }>;
-}
-
-interface ChatState {
- messages: Message[];
- isLoading: boolean;
- showEmployeeMenu: boolean;
- mentionQuery: string;
- mentionStartIndex: number;
- selectedEmployeeIndex: number;
- hasUploadedFiles: boolean;
- uploadedFiles: Array<{
- name: string;
- type: string;
- size: number;
- data?: string; // Base64 encoded file data
- }>;
-}
-
-const Chat: React.FC = () => {
- const { user } = useAuth();
- const { employees, orgId, org } = useOrg();
- const navigate = useNavigate();
- const inputRef = useRef(null);
- const fileInputRef = useRef(null);
- const messagesEndRef = useRef(null);
-
- const [state, setState] = useState({
- messages: [],
- isLoading: false,
- showEmployeeMenu: false,
- mentionQuery: '',
- mentionStartIndex: -1,
- selectedEmployeeIndex: 0,
- hasUploadedFiles: false,
- uploadedFiles: []
- });
-
- const [currentInput, setCurrentInput] = useState('');
- const [selectedCategory, setSelectedCategory] = useState('Accountability');
- const [isInputFocused, setIsInputFocused] = useState(false);
-
- // Auto-resize textarea function
- const adjustTextareaHeight = () => {
- if (inputRef.current) {
- inputRef.current.style.height = 'auto';
- const scrollHeight = inputRef.current.scrollHeight;
- const maxHeight = 150; // Maximum height in pixels
- inputRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
- }
- };
-
- useEffect(() => {
- if (!user) {
- navigate('/login');
- }
- }, [user, navigate]);
-
- // Auto-scroll to bottom when new messages arrive
- useEffect(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- }, [state.messages]);
-
- const questionStarters = [
- "How can the company serve them better?",
- "What are our team's main strengths?",
- "Which areas need improvement?",
- "How is employee satisfaction?"
- ];
-
- const categories = ['Accountability', 'Employee Growth', 'Customer Focus', 'Teamwork'];
-
- // Enhanced filtering for Google-style autocomplete
- const filteredEmployees = state.mentionQuery
- ? employees.filter(emp => {
- const query = state.mentionQuery.toLowerCase();
- const nameWords = emp.name.toLowerCase().split(' ');
- const email = emp.email.toLowerCase();
-
- // Match if query starts any word in name, or is contained in email
- return nameWords.some(word => word.startsWith(query)) ||
- email.includes(query) ||
- emp.name.toLowerCase().includes(query);
- }).sort((a, b) => {
- // Prioritize exact matches at start of name
- const aStartsWithQuery = a.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
- const bStartsWithQuery = b.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
-
- if (aStartsWithQuery && !bStartsWithQuery) return -1;
- if (!aStartsWithQuery && bStartsWithQuery) return 1;
-
- // Then alphabetical
- return a.name.localeCompare(b.name);
- })
- : employees.slice(0, 10); // Show max 10 when no query
-
- const handleSendMessage = async () => {
- if (!currentInput.trim() && state.uploadedFiles.length === 0) return;
-
- const messageText = currentInput.trim();
- const mentions: Array<{ id: string; name: string }> = [];
-
- // Extract mentions from the message
- const mentionRegex = /@(\w+(?:\s+\w+)*)/g;
- let match;
- while ((match = mentionRegex.exec(messageText)) !== null) {
- const mentionedName = match[1];
- const employee = employees.find(emp => emp.name === mentionedName);
- if (employee) {
- mentions.push({ id: employee.id, name: employee.name });
- }
- }
-
- const newMessage: Message = {
- id: Date.now().toString(),
- role: 'user',
- content: messageText,
- timestamp: new Date(),
- mentions,
- attachments: state.uploadedFiles.length > 0 ? [...state.uploadedFiles] : undefined
- };
-
- setState(prev => ({
- ...prev,
- messages: [...prev.messages, newMessage],
- isLoading: true,
- // Clear uploaded files after sending
- uploadedFiles: [],
- hasUploadedFiles: false
- }));
-
- setCurrentInput('');
-
- try {
- // Get mentioned employees' data for context
- const mentionedEmployees = mentions.map(mention =>
- employees.find(emp => emp.id === mention.id)
- ).filter(Boolean);
-
- // Call actual AI API with full context
- const res = await apiPost('/chat', {
- message: messageText,
- mentions: mentionedEmployees,
- attachments: state.uploadedFiles.length > 0 ? state.uploadedFiles : undefined,
- context: {
- org: org,
- employees: employees,
- messageHistory: state.messages.slice(-5) // Last 5 messages for context
- }
- }, orgId);
-
- if (!res.ok) {
- const errorData = await res.json();
- throw new Error(errorData.error || 'Failed to get AI response');
- }
-
- const data = await res.json();
-
- const aiResponse: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: data.response || 'I apologize, but I encountered an issue processing your request.',
- timestamp: new Date()
- };
-
- setState(prev => ({
- ...prev,
- messages: [...prev.messages, aiResponse],
- isLoading: false
- }));
- } catch (error) {
- console.error('Chat API error:', error);
-
- // Fallback response with context awareness
- const fallbackMessage: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: `I understand you're asking about ${mentions.length > 0 ? mentions.map(m => m.name).join(', ') : 'your team'}. I'm currently experiencing some connection issues, but I'd be happy to help you analyze employee data, company metrics, or provide insights about your organization once the connection is restored.`,
- timestamp: new Date()
- };
-
- setState(prev => ({
- ...prev,
- messages: [...prev.messages, fallbackMessage],
- isLoading: false
- }));
- }
- };
-
- const handleInputChange = (e: React.ChangeEvent) => {
- const value = e.target.value;
- const cursorPosition = e.target.selectionStart;
-
- setCurrentInput(value);
-
- // Auto-resize textarea
- setTimeout(adjustTextareaHeight, 0);
-
- // Enhanced @ mention detection for real-time search
- const beforeCursor = value.substring(0, cursorPosition);
- const lastAtIndex = beforeCursor.lastIndexOf('@');
-
- if (lastAtIndex !== -1) {
- // Check if we're still within a mention context
- const afterAt = beforeCursor.substring(lastAtIndex + 1);
- const hasSpaceOrNewline = /[\s\n]/.test(afterAt);
-
- if (!hasSpaceOrNewline) {
- // We're in a mention - show menu and filter
- setState(prev => ({
- ...prev,
- showEmployeeMenu: true,
- mentionQuery: afterAt,
- mentionStartIndex: lastAtIndex,
- selectedEmployeeIndex: 0
- }));
- } else {
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false
- }));
- }
- } else {
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false
- }));
- }
- };
-
- const handleEmployeeSelect = (employee: { id: string; name: string }) => {
- if (state.mentionStartIndex === -1) return;
-
- const beforeMention = currentInput.substring(0, state.mentionStartIndex);
- const afterCursor = currentInput.substring(inputRef.current?.selectionStart || currentInput.length);
- const newValue = `${beforeMention}@${employee.name} ${afterCursor}`;
-
- setCurrentInput(newValue);
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false,
- mentionQuery: '',
- mentionStartIndex: -1
- }));
-
- // Focus back to input and position cursor after the mention
- setTimeout(() => {
- if (inputRef.current) {
- const newCursorPosition = beforeMention.length + employee.name.length + 2;
- inputRef.current.focus();
- inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
- }
- }, 0);
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (state.showEmployeeMenu && filteredEmployees.length > 0) {
- switch (e.key) {
- case 'ArrowDown':
- e.preventDefault();
- setState(prev => ({
- ...prev,
- selectedEmployeeIndex: prev.selectedEmployeeIndex < filteredEmployees.length - 1
- ? prev.selectedEmployeeIndex + 1
- : 0
- }));
- break;
- case 'ArrowUp':
- e.preventDefault();
- setState(prev => ({
- ...prev,
- selectedEmployeeIndex: prev.selectedEmployeeIndex > 0
- ? prev.selectedEmployeeIndex - 1
- : filteredEmployees.length - 1
- }));
- break;
- case 'Enter':
- case 'Tab':
- e.preventDefault();
- if (filteredEmployees[state.selectedEmployeeIndex]) {
- handleEmployeeSelect(filteredEmployees[state.selectedEmployeeIndex]);
- }
- break;
- case 'Escape':
- e.preventDefault();
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false
- }));
- break;
- }
- } else if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- handleSendMessage();
- }
- // Allow Shift+Enter and Alt+Enter for line breaks (default behavior)
- };
-
- const handleFileUpload = async (e: React.ChangeEvent) => {
- const files = Array.from(e.target.files || []);
- if (files.length > 0) {
- const uploadedFiles = await Promise.all(files.map(async (file) => {
- // Convert file to base64 for API transmission
- const base64 = await new Promise((resolve) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result as string);
- reader.readAsDataURL(file);
- });
-
- return {
- name: file.name,
- type: file.type,
- size: file.size,
- data: base64 // Add the actual file data
- };
- }));
-
- setState(prev => ({
- ...prev,
- hasUploadedFiles: true,
- uploadedFiles: [...prev.uploadedFiles, ...uploadedFiles]
- }));
- }
- };
-
- const removeFile = (index: number) => {
- setState(prev => ({
- ...prev,
- uploadedFiles: prev.uploadedFiles.filter((_, i) => i !== index),
- hasUploadedFiles: prev.uploadedFiles.length > 1
- }));
- };
-
- const handleQuestionClick = (question: string) => {
- setCurrentInput(question);
- };
-
- const renderEmployeeMenu = () => {
- if (!state.showEmployeeMenu || filteredEmployees.length === 0) return null;
-
- return (
-
- {state.mentionQuery && (
-
- {filteredEmployees.length} employee{filteredEmployees.length !== 1 ? 's' : ''} found
-
- )}
- {filteredEmployees.map((employee, index) => (
-
handleEmployeeSelect({ id: employee.id, name: employee.name })}
- onMouseEnter={() => setState(prev => ({ ...prev, selectedEmployeeIndex: index }))}
- className={`px-3 py-2 rounded-xl flex items-center space-x-3 cursor-pointer transition-colors ${index === state.selectedEmployeeIndex
- ? 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950]'
- : 'hover:bg-[--Neutrals-NeutralSlate50]'
- }`}
- >
-
- {employee.initials || employee.name.split(' ').map(n => n[0]).join('').toUpperCase()}
-
-
-
- {employee.name}
-
-
- {employee.role || employee.email}
-
-
-
- ))}
-
- );
- };
-
- const renderUploadedFiles = () => {
- if (state.uploadedFiles.length === 0) return null;
-
- return (
-
- {state.uploadedFiles.map((file, index) => (
-
-
-
-
removeFile(index)} className="cursor-pointer">
-
-
-
-
-
-
- ))}
-
- );
- };
-
- const renderChatInterface = () => {
- if (state.messages.length === 0) {
- return (
-
-
-
What would you like to understand?
-
- {categories.map((category) => (
-
setSelectedCategory(category)}
- className={`px-3 py-1.5 rounded-lg shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${selectedCategory === category ? 'bg-[--Neutrals-NeutralSlate50]' : ''
- }`}
- >
-
-
- ))}
-
-
-
- {questionStarters.map((question, index) => (
-
handleQuestionClick(question)}
- className="flex-1 h-48 px-3 py-4 bg-[--Neutrals-NeutralSlate50] rounded-2xl inline-flex flex-col justify-between items-start overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
- >
-
-
{question}
-
- ))}
-
-
-
- {/* Enhanced instructions for @ mentions */}
-
-
Ask about your team, company data, or get insights.
-
Use @ to mention team members.
-
- {/* Sample questions */}
-
-
Try asking:
-
-
- "How is the team performing overall?"
-
-
- "What are the main strengths of our organization?"
-
-
- "Tell me about @[employee name]'s recent feedback"
-
-
-
-
-
- {renderChatInput()}
-
- );
- }
-
- return (
-
-
- {state.messages.map((message) => (
-
-
-
{message.content}
- {message.attachments && message.attachments.length > 0 && (
-
- {message.attachments.map((file, index) => (
-
-
- {file.type.startsWith('image/') ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
{file.name}
-
- ))}
-
- )}
-
- {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
-
- {message.mentions && message.mentions.length > 0 && (
-
- Mentioned: {message.mentions.map(m => m.name).join(', ')}
-
- )}
-
-
- ))}
- {state.isLoading && (
-
- )}
-
-
- {renderChatInput()}
-
- );
- };
-
- const renderChatInput = () => {
- return (
-
- {renderUploadedFiles()}
-
-
- {currentInput || "Ask anything, use @ to tag staff and ask questions."}
-
- {/* Custom blinking cursor when focused and has text */}
- {currentInput && isInputFocused && (
-
- )}
- {/* Custom blinking cursor when focused and no text */}
- {!currentInput && isInputFocused && (
-
- )}
-
-
-
-
-
fileInputRef.current?.click()} className="cursor-pointer">
-
-
-
-
-
fileInputRef.current?.click()} className="cursor-pointer">
-
-
-
-
-
-
-
0
- ? 'bg-[--Neutrals-NeutralSlate700]'
- : 'bg-[--Neutrals-NeutralSlate400]'
- }`}
- >
-
-
-
-
- {/* Enhanced help text for keyboard navigation */}
-
- {state.showEmployeeMenu ? '↑↓ Navigate • Enter/Tab Select • Esc Cancel' : 'Enter to send • Shift+Enter new line'}
-
-
- {renderEmployeeMenu()}
-
- );
- };
-
- return (
-
-
-
-
- {renderChatInterface()}
-
-
-
- );
-};
-
+import React, { useState, useRef, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import { useOrg } from '../contexts/OrgContext';
+import { apiPost } from '../services/api';
+import Sidebar from '../components/figma/Sidebar';
+
+interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ timestamp: Date;
+ mentions?: Array<{ id: string; name: string }>;
+ attachments?: Array<{
+ name: string;
+ type: string;
+ size: number;
+ data?: string; // Base64 encoded file data
+ }>;
+}
+
+interface ChatState {
+ messages: Message[];
+ isLoading: boolean;
+ showEmployeeMenu: boolean;
+ mentionQuery: string;
+ mentionStartIndex: number;
+ selectedEmployeeIndex: number;
+ hasUploadedFiles: boolean;
+ uploadedFiles: Array<{
+ name: string;
+ type: string;
+ size: number;
+ data?: string; // Base64 encoded file data
+ }>;
+}
+
+const Chat: React.FC = () => {
+ const { user } = useAuth();
+ const { employees, orgId, org } = useOrg();
+ const navigate = useNavigate();
+ const inputRef = useRef(null);
+ const fileInputRef = useRef(null);
+ const messagesEndRef = useRef(null);
+
+ const [state, setState] = useState({
+ messages: [],
+ isLoading: false,
+ showEmployeeMenu: false,
+ mentionQuery: '',
+ mentionStartIndex: -1,
+ selectedEmployeeIndex: 0,
+ hasUploadedFiles: false,
+ uploadedFiles: []
+ });
+
+ const [currentInput, setCurrentInput] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState('Accountability');
+ const [isInputFocused, setIsInputFocused] = useState(false);
+
+ // Auto-resize textarea function
+ const adjustTextareaHeight = () => {
+ if (inputRef.current) {
+ inputRef.current.style.height = 'auto';
+ const scrollHeight = inputRef.current.scrollHeight;
+ const maxHeight = 150; // Maximum height in pixels
+ inputRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
+ }
+ };
+
+ useEffect(() => {
+ if (!user) {
+ navigate('/login');
+ }
+ }, [user, navigate]);
+
+ // Auto-scroll to bottom when new messages arrive
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [state.messages]);
+
+ const questionStarters = [
+ "How can the company serve them better?",
+ "What are our team's main strengths?",
+ "Which areas need improvement?",
+ "How is employee satisfaction?"
+ ];
+
+ const categories = ['Accountability', 'Employee Growth', 'Customer Focus', 'Teamwork'];
+
+ // Enhanced filtering for Google-style autocomplete
+ const filteredEmployees = state.mentionQuery
+ ? employees.filter(emp => {
+ const query = state.mentionQuery.toLowerCase();
+ const nameWords = emp.name.toLowerCase().split(' ');
+ const email = emp.email.toLowerCase();
+
+ // Match if query starts any word in name, or is contained in email
+ return nameWords.some(word => word.startsWith(query)) ||
+ email.includes(query) ||
+ emp.name.toLowerCase().includes(query);
+ }).sort((a, b) => {
+ // Prioritize exact matches at start of name
+ const aStartsWithQuery = a.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
+ const bStartsWithQuery = b.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
+
+ if (aStartsWithQuery && !bStartsWithQuery) return -1;
+ if (!aStartsWithQuery && bStartsWithQuery) return 1;
+
+ // Then alphabetical
+ return a.name.localeCompare(b.name);
+ })
+ : employees.slice(0, 10); // Show max 10 when no query
+
+ const handleSendMessage = async () => {
+ if (!currentInput.trim() && state.uploadedFiles.length === 0) return;
+
+ const messageText = currentInput.trim();
+ const mentions: Array<{ id: string; name: string }> = [];
+
+ // Extract mentions from the message
+ const mentionRegex = /@(\w+(?:\s+\w+)*)/g;
+ let match;
+ while ((match = mentionRegex.exec(messageText)) !== null) {
+ const mentionedName = match[1];
+ const employee = employees.find(emp => emp.name === mentionedName);
+ if (employee) {
+ mentions.push({ id: employee.id, name: employee.name });
+ }
+ }
+
+ const newMessage: Message = {
+ id: Date.now().toString(),
+ role: 'user',
+ content: messageText,
+ timestamp: new Date(),
+ mentions,
+ attachments: state.uploadedFiles.length > 0 ? [...state.uploadedFiles] : undefined
+ };
+
+ setState(prev => ({
+ ...prev,
+ messages: [...prev.messages, newMessage],
+ isLoading: true,
+ // Clear uploaded files after sending
+ uploadedFiles: [],
+ hasUploadedFiles: false
+ }));
+
+ setCurrentInput('');
+
+ try {
+ // Get mentioned employees' data for context
+ const mentionedEmployees = mentions.map(mention =>
+ employees.find(emp => emp.id === mention.id)
+ ).filter(Boolean);
+
+ // Call actual AI API with full context
+ const res = await apiPost('/chat', {
+ message: messageText,
+ mentions: mentionedEmployees,
+ attachments: state.uploadedFiles.length > 0 ? state.uploadedFiles : undefined,
+ context: {
+ org: org,
+ employees: employees,
+ messageHistory: state.messages.slice(-5) // Last 5 messages for context
+ }
+ }, orgId);
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.error || 'Failed to get AI response');
+ }
+
+ const data = await res.json();
+
+ const aiResponse: Message = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: data.response || 'I apologize, but I encountered an issue processing your request.',
+ timestamp: new Date()
+ };
+
+ setState(prev => ({
+ ...prev,
+ messages: [...prev.messages, aiResponse],
+ isLoading: false
+ }));
+ } catch (error) {
+ console.error('Chat API error:', error);
+
+ // Fallback response with context awareness
+ const fallbackMessage: Message = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: `I understand you're asking about ${mentions.length > 0 ? mentions.map(m => m.name).join(', ') : 'your team'}. I'm currently experiencing some connection issues, but I'd be happy to help you analyze employee data, company metrics, or provide insights about your organization once the connection is restored.`,
+ timestamp: new Date()
+ };
+
+ setState(prev => ({
+ ...prev,
+ messages: [...prev.messages, fallbackMessage],
+ isLoading: false
+ }));
+ }
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ const cursorPosition = e.target.selectionStart;
+
+ setCurrentInput(value);
+
+ // Auto-resize textarea
+ setTimeout(adjustTextareaHeight, 0);
+
+ // Enhanced @ mention detection for real-time search
+ const beforeCursor = value.substring(0, cursorPosition);
+ const lastAtIndex = beforeCursor.lastIndexOf('@');
+
+ if (lastAtIndex !== -1) {
+ // Check if we're still within a mention context
+ const afterAt = beforeCursor.substring(lastAtIndex + 1);
+ const hasSpaceOrNewline = /[\s\n]/.test(afterAt);
+
+ if (!hasSpaceOrNewline) {
+ // We're in a mention - show menu and filter
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: true,
+ mentionQuery: afterAt,
+ mentionStartIndex: lastAtIndex,
+ selectedEmployeeIndex: 0
+ }));
+ } else {
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false
+ }));
+ }
+ } else {
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false
+ }));
+ }
+ };
+
+ const handleEmployeeSelect = (employee: { id: string; name: string }) => {
+ if (state.mentionStartIndex === -1) return;
+
+ const beforeMention = currentInput.substring(0, state.mentionStartIndex);
+ const afterCursor = currentInput.substring(inputRef.current?.selectionStart || currentInput.length);
+ const newValue = `${beforeMention}@${employee.name} ${afterCursor}`;
+
+ setCurrentInput(newValue);
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false,
+ mentionQuery: '',
+ mentionStartIndex: -1
+ }));
+
+ // Focus back to input and position cursor after the mention
+ setTimeout(() => {
+ if (inputRef.current) {
+ const newCursorPosition = beforeMention.length + employee.name.length + 2;
+ inputRef.current.focus();
+ inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
+ }
+ }, 0);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (state.showEmployeeMenu && filteredEmployees.length > 0) {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setState(prev => ({
+ ...prev,
+ selectedEmployeeIndex: prev.selectedEmployeeIndex < filteredEmployees.length - 1
+ ? prev.selectedEmployeeIndex + 1
+ : 0
+ }));
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setState(prev => ({
+ ...prev,
+ selectedEmployeeIndex: prev.selectedEmployeeIndex > 0
+ ? prev.selectedEmployeeIndex - 1
+ : filteredEmployees.length - 1
+ }));
+ break;
+ case 'Enter':
+ case 'Tab':
+ e.preventDefault();
+ if (filteredEmployees[state.selectedEmployeeIndex]) {
+ handleEmployeeSelect(filteredEmployees[state.selectedEmployeeIndex]);
+ }
+ break;
+ case 'Escape':
+ e.preventDefault();
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false
+ }));
+ break;
+ }
+ } else if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ handleSendMessage();
+ }
+ // Allow Shift+Enter and Alt+Enter for line breaks (default behavior)
+ };
+
+ const handleFileUpload = async (e: React.ChangeEvent) => {
+ const files = Array.from(e.target.files || []);
+ if (files.length > 0) {
+ const uploadedFiles = await Promise.all(files.map(async (file) => {
+ // Convert file to base64 for API transmission
+ const base64 = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as string);
+ reader.readAsDataURL(file);
+ });
+
+ return {
+ name: file.name,
+ type: file.type,
+ size: file.size,
+ data: base64 // Add the actual file data
+ };
+ }));
+
+ setState(prev => ({
+ ...prev,
+ hasUploadedFiles: true,
+ uploadedFiles: [...prev.uploadedFiles, ...uploadedFiles]
+ }));
+ }
+ };
+
+ const removeFile = (index: number) => {
+ setState(prev => ({
+ ...prev,
+ uploadedFiles: prev.uploadedFiles.filter((_, i) => i !== index),
+ hasUploadedFiles: prev.uploadedFiles.length > 1
+ }));
+ };
+
+ const handleQuestionClick = (question: string) => {
+ setCurrentInput(question);
+ };
+
+ const renderEmployeeMenu = () => {
+ if (!state.showEmployeeMenu || filteredEmployees.length === 0) return null;
+
+ return (
+
+ {state.mentionQuery && (
+
+ {filteredEmployees.length} employee{filteredEmployees.length !== 1 ? 's' : ''} found
+
+ )}
+ {filteredEmployees.map((employee, index) => (
+
handleEmployeeSelect({ id: employee.id, name: employee.name })}
+ onMouseEnter={() => setState(prev => ({ ...prev, selectedEmployeeIndex: index }))}
+ className={`px-3 py-2 rounded-xl flex items-center space-x-3 cursor-pointer transition-colors ${index === state.selectedEmployeeIndex
+ ? 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950]'
+ : 'hover:bg-[--Neutrals-NeutralSlate50]'
+ }`}
+ >
+
+ {employee.initials || employee.name.split(' ').map(n => n[0]).join('').toUpperCase()}
+
+
+
+ {employee.name}
+
+
+ {employee.role || employee.email}
+
+
+
+ ))}
+
+ );
+ };
+
+ const renderUploadedFiles = () => {
+ if (state.uploadedFiles.length === 0) return null;
+
+ return (
+
+ {state.uploadedFiles.map((file, index) => (
+
+
+
+
removeFile(index)} className="cursor-pointer">
+
+
+
+
+
+
+ ))}
+
+ );
+ };
+
+ const renderChatInterface = () => {
+ if (state.messages.length === 0) {
+ return (
+
+
+
What would you like to understand?
+
+ {categories.map((category) => (
+
setSelectedCategory(category)}
+ className={`px-3 py-1.5 rounded-lg shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${selectedCategory === category ? 'bg-[--Neutrals-NeutralSlate50]' : ''
+ }`}
+ >
+
+
+ ))}
+
+
+
+ {questionStarters.map((question, index) => (
+
handleQuestionClick(question)}
+ className="flex-1 h-48 px-3 py-4 bg-[--Neutrals-NeutralSlate50] rounded-2xl inline-flex flex-col justify-between items-start overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
+ >
+
+
{question}
+
+ ))}
+
+
+
+ {/* Enhanced instructions for @ mentions */}
+
+
Ask about your team, company data, or get insights.
+
Use @ to mention team members.
+
+ {/* Sample questions */}
+
+
Try asking:
+
+
+ "How is the team performing overall?"
+
+
+ "What are the main strengths of our organization?"
+
+
+ "Tell me about @[employee name]'s recent feedback"
+
+
+
+
+
+
+
+ {renderChatInput()}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {state.messages.map((message) => (
+
+
+
{message.content}
+ {message.attachments && message.attachments.length > 0 && (
+
+ {message.attachments.map((file, index) => (
+
+
+ {file.type.startsWith('image/') ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
{file.name}
+
+ ))}
+
+ )}
+
+ {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+ {message.mentions && message.mentions.length > 0 && (
+
+ Mentioned: {message.mentions.map(m => m.name).join(', ')}
+
+ )}
+
+
+ ))}
+ {state.isLoading && (
+
+ )}
+
+
+
+
+
+ {renderChatInput()}
+
+
+
+ );
+ };
+
+ const renderChatInput = () => {
+ return (
+
+ {renderUploadedFiles()}
+
+
+ {currentInput || "Ask anything, use @ to tag staff and ask questions."}
+
+ {/* Custom blinking cursor when focused and has text */}
+ {currentInput && isInputFocused && (
+
+ )}
+ {/* Custom blinking cursor when focused and no text */}
+ {!currentInput && isInputFocused && (
+
+ )}
+
+
+
+
+
fileInputRef.current?.click()} className="cursor-pointer">
+
+
+
+
+
fileInputRef.current?.click()} className="cursor-pointer">
+
+
+
+
+
+
+
0
+ ? 'bg-[--Neutrals-NeutralSlate700]'
+ : 'bg-[--Neutrals-NeutralSlate400]'
+ }`}
+ >
+
+
+
+
+ {/* Enhanced help text for keyboard navigation */}
+
+ {state.showEmployeeMenu ? '↑↓ Navigate • Enter/Tab Select • Esc Cancel' : 'Enter to send • Shift+Enter new line'}
+
+
+ {renderEmployeeMenu()}
+
+ );
+ };
+
+ return (
+
+
+
+
+ {renderChatInterface()}
+
+
+
+ );
+};
+
export default Chat;
\ No newline at end of file
diff --git a/src/pages/HelpAndSettings.tsx b/src/pages/HelpAndSettings.tsx
index 5e02a20..00c12e7 100644
--- a/src/pages/HelpAndSettings.tsx
+++ b/src/pages/HelpAndSettings.tsx
@@ -1,390 +1,394 @@
-import React, { useState } from 'react';
-import { useTheme } from '../contexts/ThemeContext';
-import { useAuth } from '../contexts/AuthContext';
-import { useOrg } from '../contexts/OrgContext';
-import { Card, Button } from '../components/UiKit';
-import { Theme } from '../types';
-
-const HelpAndSettings: React.FC = () => {
- const { theme, setTheme } = useTheme();
- const { user, signOutUser } = useAuth();
- const { org, upsertOrg, issueInviteViaApi } = useOrg();
- const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
- const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
- const [isInviting, setIsInviting] = useState(false);
- const [inviteResult, setInviteResult] = useState(null);
-
- const handleLogout = async () => {
- try {
- await signOutUser();
- } catch (error) {
- console.error('Logout error:', error);
- }
- };
-
- const handleRestartOnboarding = async () => {
- try {
- await upsertOrg({ onboardingCompleted: false });
- // The RequireOnboarding component will redirect automatically
- } catch (error) {
- console.error('Failed to restart onboarding:', error);
- }
- };
-
- const handleInviteEmployee = async () => {
- if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
-
- setIsInviting(true);
- setInviteResult(null);
-
- try {
- const result = await issueInviteViaApi({
- name: inviteForm.name.trim(),
- email: inviteForm.email.trim(),
- role: inviteForm.role.trim() || undefined,
- department: inviteForm.department.trim() || undefined
- });
-
- setInviteResult(JSON.stringify({
- success: true,
- inviteLink: result.inviteLink,
- emailLink: result.emailLink,
- employeeName: result.employee.name
- }));
- setInviteForm({ name: '', email: '', role: '', department: '' });
- } catch (error) {
- console.error('Failed to send invitation:', error);
- setInviteResult('Failed to send invitation. Please try again.');
- } finally {
- setIsInviting(false);
- }
- };
-
- const renderSettings = () => (
-
-
- Appearance
-
-
-
- Theme
-
-
- setTheme(Theme.Light)}
- >
- Light
-
- setTheme(Theme.Dark)}
- >
- Dark
-
- setTheme(Theme.System)}
- >
- System
-
-
-
-
-
-
-
- Organization
-
-
-
Company:
-
{org?.name}
-
-
-
Onboarding:
-
- {org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
-
-
-
-
- Restart Onboarding
-
-
- This will reset your company profile and require you to complete the setup process again.
-
-
-
-
-
-
- Invite Employee
-
-
-
-
- {isInviting ? 'Sending Invitation...' : 'Send Invitation'}
-
-
- {inviteResult && (
-
- {inviteResult.includes('Failed') ? (
-
- {inviteResult}
-
- ) : (
- (() => {
- try {
- const result = JSON.parse(inviteResult);
- return (
-
-
- ✅ Invitation sent to {result.employeeName}!
-
-
-
-
- Direct Link (share this with the employee):
-
-
-
- navigator.clipboard.writeText(result.inviteLink)}
- >
- Copy
-
-
-
-
-
-
- );
- } catch {
- return (
-
- {inviteResult}
-
- );
- }
- })()
- )}
-
- )}
-
-
- The invited employee will receive an email with instructions to join your organization.
-
-
-
-
-
- Account
-
-
-
Email:
-
{user?.email}
-
-
-
User ID:
-
{user?.uid}
-
-
-
- Sign Out
-
-
-
-
-
-
- Data & Privacy
-
-
- Export My Data
-
-
- Privacy Settings
-
-
- Delete Account
-
-
-
-
- );
-
- const renderHelp = () => (
-
-
- Getting Started
-
-
-
1. Set up your organization
-
- Complete the onboarding process to configure your company information and preferences.
-
-
-
-
2. Add employees
-
- Invite team members and add their basic information to start generating reports.
-
-
-
-
3. Generate reports
-
- Use AI-powered reports to gain insights into employee performance and organizational health.
-
-
-
-
-
-
- Frequently Asked Questions
-
-
-
How do I add new employees?
-
- Go to the Reports page and use the "Add Employee" button to invite new team members.
-
-
-
-
How are reports generated?
-
- Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
-
-
-
-
Is my data secure?
-
- Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
-
-
-
-
-
-
- Contact Support
-
-
- 📧 Email Support
-
-
- 💬 Live Chat
-
-
- 📚 Documentation
-
-
-
-
- );
-
- return (
-
-
-
Help & Settings
-
- Manage your account and get help
-
-
-
-
-
- setActiveTab('settings')}
- className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
- ? 'border-blue-500 text-blue-500'
- : 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
- }`}
- >
- Settings
-
- setActiveTab('help')}
- className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
- ? 'border-blue-500 text-blue-500'
- : 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
- }`}
- >
- Help
-
-
-
-
- {activeTab === 'settings' ? renderSettings() : renderHelp()}
-
- );
-};
-
-export default HelpAndSettings;
+// DEPRECATED: This component has been split into separate Help and Settings pages
+// Use /src/pages/HelpNew.tsx and /src/pages/SettingsNew.tsx instead
+// This file can be safely removed in future cleanup
+
+import React, { useState } from 'react';
+import { useTheme } from '../contexts/ThemeContext';
+import { useAuth } from '../contexts/AuthContext';
+import { useOrg } from '../contexts/OrgContext';
+import { Card, Button } from '../components/UiKit';
+import { Theme } from '../types';
+
+const HelpAndSettings: React.FC = () => {
+ const { theme, setTheme } = useTheme();
+ const { user, signOutUser } = useAuth();
+ const { org, upsertOrg, issueInviteViaApi } = useOrg();
+ const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
+ const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
+ const [isInviting, setIsInviting] = useState(false);
+ const [inviteResult, setInviteResult] = useState(null);
+
+ const handleLogout = async () => {
+ try {
+ await signOutUser();
+ } catch (error) {
+ console.error('Logout error:', error);
+ }
+ };
+
+ const handleRestartOnboarding = async () => {
+ try {
+ await upsertOrg({ onboardingCompleted: false });
+ // The RequireOnboarding component will redirect automatically
+ } catch (error) {
+ console.error('Failed to restart onboarding:', error);
+ }
+ };
+
+ const handleInviteEmployee = async () => {
+ if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
+
+ setIsInviting(true);
+ setInviteResult(null);
+
+ try {
+ const result = await issueInviteViaApi({
+ name: inviteForm.name.trim(),
+ email: inviteForm.email.trim(),
+ role: inviteForm.role.trim() || undefined,
+ department: inviteForm.department.trim() || undefined
+ });
+
+ setInviteResult(JSON.stringify({
+ success: true,
+ inviteLink: result.inviteLink,
+ emailLink: result.emailLink,
+ employeeName: result.employee.name
+ }));
+ setInviteForm({ name: '', email: '', role: '', department: '' });
+ } catch (error) {
+ console.error('Failed to send invitation:', error);
+ setInviteResult('Failed to send invitation. Please try again.');
+ } finally {
+ setIsInviting(false);
+ }
+ };
+
+ const renderSettings = () => (
+
+
+ Appearance
+
+
+
+ Theme
+
+
+ setTheme(Theme.Light)}
+ >
+ Light
+
+ setTheme(Theme.Dark)}
+ >
+ Dark
+
+ setTheme(Theme.System)}
+ >
+ System
+
+
+
+
+
+
+
+ Organization
+
+
+
Company:
+
{org?.name}
+
+
+
Onboarding:
+
+ {org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
+
+
+
+
+ Restart Onboarding
+
+
+ This will reset your company profile and require you to complete the setup process again.
+
+
+
+
+
+
+ Invite Employee
+
+
+
+
+ {isInviting ? 'Sending Invitation...' : 'Send Invitation'}
+
+
+ {inviteResult && (
+
+ {inviteResult.includes('Failed') ? (
+
+ {inviteResult}
+
+ ) : (
+ (() => {
+ try {
+ const result = JSON.parse(inviteResult);
+ return (
+
+
+ ✅ Invitation sent to {result.employeeName}!
+
+
+
+
+ Direct Link (share this with the employee):
+
+
+
+ navigator.clipboard.writeText(result.inviteLink)}
+ >
+ Copy
+
+
+
+
+
+
+ );
+ } catch {
+ return (
+
+ {inviteResult}
+
+ );
+ }
+ })()
+ )}
+
+ )}
+
+
+ The invited employee will receive an email with instructions to join your organization.
+
+
+
+
+
+ Account
+
+
+
Email:
+
{user?.email}
+
+
+
User ID:
+
{user?.uid}
+
+
+
+ Sign Out
+
+
+
+
+
+
+ Data & Privacy
+
+
+ Export My Data
+
+
+ Privacy Settings
+
+
+ Delete Account
+
+
+
+
+ );
+
+ const renderHelp = () => (
+
+
+ Getting Started
+
+
+
1. Set up your organization
+
+ Complete the onboarding process to configure your company information and preferences.
+
+
+
+
2. Add employees
+
+ Invite team members and add their basic information to start generating reports.
+
+
+
+
3. Generate reports
+
+ Use AI-powered reports to gain insights into employee performance and organizational health.
+
+
+
+
+
+
+ Frequently Asked Questions
+
+
+
How do I add new employees?
+
+ Go to the Reports page and use the "Add Employee" button to invite new team members.
+
+
+
+
How are reports generated?
+
+ Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
+
+
+
+
Is my data secure?
+
+ Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
+
+
+
+
+
+
+ Contact Support
+
+
+ 📧 Email Support
+
+
+ 💬 Live Chat
+
+
+ 📚 Documentation
+
+
+
+
+ );
+
+ return (
+
+
+
Help & Settings
+
+ Manage your account and get help
+
+
+
+
+
+ setActiveTab('settings')}
+ className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
+ ? 'border-blue-500 text-blue-500'
+ : 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
+ }`}
+ >
+ Settings
+
+ setActiveTab('help')}
+ className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
+ ? 'border-blue-500 text-blue-500'
+ : 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
+ }`}
+ >
+ Help
+
+
+
+
+ {activeTab === 'settings' ? renderSettings() : renderHelp()}
+
+ );
+};
+
+export default HelpAndSettings;
diff --git a/src/pages/HelpNew.tsx b/src/pages/HelpNew.tsx
index 0e73896..c15922a 100644
--- a/src/pages/HelpNew.tsx
+++ b/src/pages/HelpNew.tsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
+import { useOrg } from '../contexts/OrgContext';
import Sidebar from '../components/figma/Sidebar';
interface FAQItem {
@@ -11,6 +12,7 @@ interface FAQItem {
const HelpNew: React.FC = () => {
const { user } = useAuth();
+ const { org } = useOrg();
const navigate = useNavigate();
const [faqItems, setFaqItems] = useState([
@@ -64,64 +66,62 @@ const HelpNew: React.FC = () => {
}
return (
-
-
-
-
-
Help & Support
-
- {faqItems.map((item, index) => (
-
-
toggleFAQ(index)}
- className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2 cursor-pointer"
- >
-
- {item.question}
-
-
- {item.isOpen ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
- {item.isOpen && (
-
- )}
-
- ))}
-
-
-
-
Still have questions?
-
We are available for 24/7
-
+
+
+
+
Help & Support
+
+ {faqItems.map((item, index) => (
-
-
-
Contact Us
+
toggleFAQ(index)}
+ className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2 cursor-pointer"
+ >
+
+ {item.question}
+
+
+ {item.isOpen ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {item.isOpen && (
+
+ )}
+
+ ))}
+
+
+
+
Still have questions?
+
We are available for 24/7
+
+
diff --git a/src/pages/Onboarding.tsx b/src/pages/Onboarding.tsx
index 69d7bd0..42e0abf 100644
--- a/src/pages/Onboarding.tsx
+++ b/src/pages/Onboarding.tsx
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useOrg } from '../contexts/OrgContext';
-import { onboardingSteps, OnboardingData, initializeOnboardingData } from '../data/onboardingSteps';
+import { onboardingSteps, OnboardingFormData, initializeOnboardingData } from '../data/onboardingSteps';
import { secureApi } from '../services/secureApi';
import {
FigmaOnboardingIntro,
@@ -23,12 +23,12 @@ const Onboarding: React.FC = () => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [isGeneratingReport, setIsGeneratingReport] = useState(false);
- const [formData, setFormData] = useState
(initializeOnboardingData());
+ const [formData, setFormData] = useState(initializeOnboardingData());
const currentStep = onboardingSteps[currentStepIndex];
const totalSteps = onboardingSteps.length;
- const updateFormData = (field: keyof OnboardingData, value: string | string[]) => {
+ const updateFormData = (field: keyof OnboardingFormData, value: string | string[]) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
@@ -46,7 +46,7 @@ const Onboarding: React.FC = () => {
case 'question':
// Check if field is filled
if (currentStep.field) {
- const fieldValue = formData[currentStep.field as keyof OnboardingData];
+ const fieldValue = formData[currentStep.field as keyof OnboardingFormData];
return Array.isArray(fieldValue) ? fieldValue.length > 0 : String(fieldValue || '').trim().length > 0;
}
return false;
@@ -54,7 +54,7 @@ const Onboarding: React.FC = () => {
case 'multiple_choice':
// Check if option is selected
if (currentStep.field) {
- const fieldValue = formData[currentStep.field as keyof OnboardingData];
+ const fieldValue = formData[currentStep.field as keyof OnboardingFormData];
return String(fieldValue || '').trim().length > 0;
}
return false;
@@ -76,13 +76,15 @@ const Onboarding: React.FC = () => {
}
// Final step: submit all data and complete onboarding
+
+ const { companyName, companyLogo, ...onboardingData } = formData;
+
setIsGeneratingReport(true);
try {
await upsertOrg({
...org,
- companyName: formData.companyName,
- companyLogo: formData.companyLogo,
- onboardingData: formData,
+ companyLogo,
+ onboardingData,
onboardingCompleted: true,
updatedAt: Date.now(),
});
@@ -154,7 +156,7 @@ const Onboarding: React.FC = () => {
case 'question':
const questionValue = currentStep.field
- ? String(formData[currentStep.field as keyof OnboardingData] || '')
+ ? String(formData[currentStep.field as keyof OnboardingFormData] || '')
: '';
return (
@@ -164,7 +166,7 @@ const Onboarding: React.FC = () => {
value={questionValue}
onChange={(value) => {
if (currentStep.field) {
- updateFormData(currentStep.field as keyof OnboardingData, value);
+ updateFormData(currentStep.field as keyof OnboardingFormData, value);
}
}}
onBack={handleBack}
@@ -180,7 +182,7 @@ const Onboarding: React.FC = () => {
case 'multiple_choice':
const multipleChoiceValue = currentStep.field
- ? String(formData[currentStep.field as keyof OnboardingData] || '')
+ ? String(formData[currentStep.field as keyof OnboardingFormData] || '')
: '';
return (
@@ -190,7 +192,7 @@ const Onboarding: React.FC = () => {
selectedValue={multipleChoiceValue}
onSelect={(value) => {
if (currentStep.field) {
- updateFormData(currentStep.field as keyof OnboardingData, value);
+ updateFormData(currentStep.field as keyof OnboardingFormData, value);
}
}}
onBack={handleBack}
diff --git a/src/pages/Reports.tsx b/src/pages/Reports.tsx
index e95ded5..553f6b2 100644
--- a/src/pages/Reports.tsx
+++ b/src/pages/Reports.tsx
@@ -8,11 +8,11 @@ import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
const Reports: React.FC = () => {
const location = useLocation();
- const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, orgId } = useOrg();
+ const { employees, reports, submissions, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, orgId } = useOrg();
const [companyReport, setCompanyReport] = useState(null);
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string } | null>(null);
const [searchQuery, setSearchQuery] = useState('');
- const [generatingReports, setGeneratingReports] = useState>(new Set());
+ const [generatingEmployeeReport, setGeneratingEmployeeReport] = useState(null);
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
const currentUserIsOwner = isOwner(user?.uid || '');
@@ -20,6 +20,29 @@ const Reports: React.FC = () => {
// Get selected employee ID from navigation state (from Submissions page)
const selectedEmployeeId = location.state?.selectedEmployeeId;
+ const handleGenerateEmployeeReport = async (employee: Employee) => {
+ if (generatingEmployeeReport === employee.id) return; // Prevent double-click
+
+ setGeneratingEmployeeReport(employee.id);
+ try {
+ console.log('Generating employee report for:', employee.name);
+ const newReport = await generateEmployeeReport(employee);
+ if (newReport) {
+ setSelectedReport({
+ report: newReport,
+ type: 'employee',
+ employeeName: employee.name
+ });
+ console.log('Employee report generated successfully');
+ }
+ } catch (error) {
+ console.error('Failed to generate employee report:', error);
+ alert(`Failed to generate report for ${employee.name}. Please try again.`);
+ } finally {
+ setGeneratingEmployeeReport(null);
+ }
+ };
+
// Load company report on component mount
useEffect(() => {
const loadCompanyReport = async () => {
@@ -31,6 +54,9 @@ const Reports: React.FC = () => {
// Auto-select company report by default
setSelectedReport({ report: history[0], type: 'company' });
} else {
+ // FIXED: No automatic generation - only load existing reports
+ // Use sample data when no real reports exist
+ console.log('No company reports found, using sample data. Click "Refresh Report" to generate a new one.');
setCompanyReport(SAMPLE_COMPANY_REPORT);
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' });
}
@@ -42,9 +68,9 @@ const Reports: React.FC = () => {
}
};
loadCompanyReport();
- }, [currentUserIsOwner, getFullCompanyReportHistory]);
+ }, [currentUserIsOwner, getFullCompanyReportHistory]); // FIXED: Removed generateCompanyReport and submissions dependencies
- const handleEmployeeSelect = useCallback((employee: Employee) => {
+ const handleEmployeeSelect = useCallback(async (employee: Employee) => {
const employeeReport = reports[employee.id];
if (employeeReport) {
setSelectedReport({
@@ -52,8 +78,52 @@ const Reports: React.FC = () => {
type: 'employee',
employeeName: employee.name
});
+ } else {
+ // FIXED: Only check if employee has submission - do NOT auto-generate
+ const hasSubmission = submissions[employee.id];
+ if (hasSubmission) {
+ // Show placeholder encouraging manual generation
+ setSelectedReport({
+ report: {
+ employeeId: employee.id,
+ roleAndOutput: {
+ responsibilities: `${employee.name} has completed their questionnaire but no report has been generated yet.`,
+ selfRatedOutput: 'Report generation is available. Click "Generate Report" to create it.'
+ },
+ insights: {
+ personalityTraits: 'Report not generated yet. Employee has completed their questionnaire.',
+ selfAwareness: '',
+ growthDesire: ''
+ },
+ strengths: ['Report generation available - click Generate Report button'],
+ recommendations: ['Generate the report to view detailed analysis and recommendations']
+ } as EmployeeReport,
+ type: 'employee',
+ employeeName: employee.name
+ });
+ } else {
+ // No submission available - show message
+ setSelectedReport({
+ report: {
+ employeeId: employee.id,
+ roleAndOutput: {
+ responsibilities: `${employee.name} has not completed the employee questionnaire yet.`,
+ selfRatedOutput: 'No submission data available.'
+ },
+ insights: {
+ personalityTraits: 'Please ask the employee to complete their questionnaire first.',
+ selfAwareness: '',
+ growthDesire: ''
+ },
+ strengths: ['Complete questionnaire to view strengths'],
+ recommendations: ['Employee should complete the questionnaire first']
+ } as EmployeeReport,
+ type: 'employee',
+ employeeName: employee.name
+ });
+ }
}
- }, [reports]);
+ }, [reports, submissions]); // FIXED: Removed generateEmployeeReport and generatingReports dependencies
// Handle navigation from Submissions page
useEffect(() => {
@@ -81,11 +151,15 @@ const Reports: React.FC = () => {
const handleGenerateCompanyReport = async () => {
setGeneratingCompanyReport(true);
try {
+ console.log('Generating new company report with current data...');
const newReport = await generateCompanyReport();
setCompanyReport(newReport);
setSelectedReport({ report: newReport, type: 'company' });
+ console.log('Company report generated successfully');
} catch (error) {
console.error('Error generating company report:', error);
+ // Show error message to user
+ alert('Failed to generate company report. Please try again.');
} finally {
setGeneratingCompanyReport(false);
}
@@ -139,23 +213,42 @@ const Reports: React.FC = () => {
)}
{/* Employee Items */}
- {visibleEmployees.map((employee) => (
- handleEmployeeSelect(employee)}
- >
-
-
- {employee.initials}
+ {visibleEmployees.map((employee) => {
+ const hasSubmission = submissions[employee.id];
+ const hasReport = reports[employee.id];
+ const isGenerating = generatingEmployeeReport === employee.id;
+
+ return (
+
handleEmployeeSelect(employee)}
+ >
+
+
+ {employee.initials}
+
+ {/* Status indicator */}
+ {isGenerating ? (
+
+ ) : hasReport ? (
+
+ ) : hasSubmission ? (
+
+ ) : (
+
+ )}
+
+ {employee.name}
+
+ {isGenerating && (
+
+ )}
-
- {employee.name}
-
-
- ))}
+ );
+ })}
@@ -170,10 +263,23 @@ const Reports: React.FC = () => {
isGenerating={generatingCompanyReport}
/>
) : (
-
+ (() => {
+ const employeeReport = selectedReport.report as EmployeeReport;
+ const employeeId = employeeReport.employeeId;
+ return (
+
{
+ const employee = employees.find(emp => emp.name === selectedReport.employeeName);
+ if (employee) handleGenerateEmployeeReport(employee);
+ }}
+ isGenerating={generatingEmployeeReport === employeeId}
+ hasSubmission={!!submissions[employeeId]}
+ showGenerateButton={!reports[employeeId] && !!submissions[employeeId]}
+ />
+ );
+ })()
)
) : (
@@ -214,14 +320,37 @@ const CompanyReportContent: React.FC<{
Company Report
-
-
-
-
Download as PDF
+
+
+
+ {isGenerating ? (
+
+ ) : (
+
+
+
+
+ )}
+
+
+
+ {isGenerating ? 'Generating...' : 'Refresh Report'}
+
+
+
+
@@ -484,7 +613,7 @@ const CompanyReportContent: React.FC<{
{/* Department Tabs */}
- {report?.organizationalImpactSummary.map((dept, index) => (
+ {report.organizationalImpactSummary && report.organizationalImpactSummary.map((dept, index) => (
{/* Content for the currently selected department */}
- {(() => {
- const currentImpact = report?.organizationalImpactSummary.find(dept => dept.category === activeImpactSummary);
+ {report.organizationalImpactSummary && (() => {
+ const currentImpact = report.organizationalImpactSummary.find(dept => dept.category === activeImpactSummary);
if (!currentImpact) return null;
return (
@@ -550,7 +679,7 @@ const CompanyReportContent: React.FC<{
{/* Department Tabs */}
- {report?.gradingBreakdown?.map(dept => (
+ {report.gradingBreakdown && report?.gradingBreakdown?.map(dept => (
{/* Content for the currently selected department */}
- {(() => {
+ {report.gradingBreakdown && (() => {
const currentDepartment = report?.gradingBreakdown?.find(dept => dept.departmentNameShort === activeDepartmentTab);
if (!currentDepartment) return null;
@@ -652,23 +781,56 @@ const CompanyReportContent: React.FC<{
const EmployeeReportContent: React.FC<{
report: EmployeeReport;
employeeName: string;
-}> = ({ report, employeeName }) => {
+ onGenerateReport?: () => void;
+ isGenerating?: boolean;
+ hasSubmission?: boolean;
+ showGenerateButton?: boolean;
+}> = ({ report, employeeName, onGenerateReport, isGenerating = false, hasSubmission = false, showGenerateButton = false }) => {
return (
<>
{/* Header */}
- {employeeName}'s Answers
+ {employeeName}'s Report
-
-
-
+
+ {/* Generate Report Button - only show when needed */}
+ {showGenerateButton && hasSubmission && onGenerateReport && (
+
+
+ {isGenerating ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {isGenerating ? 'Generating...' : 'Generate Report'}
+
+
+
+ )}
+
+ {/* Download PDF Button - only show for actual reports */}
+ {!showGenerateButton && (
+
+ )}
@@ -687,7 +849,7 @@ const EmployeeReportContent: React.FC<{
{/* Self-Rated Output */}
- {report.roleAndOutput?.selfRatedOutput && (
+ {report.roleAndOutput && report.roleAndOutput?.selfRatedOutput && (
Self-Rated Output
@@ -748,7 +910,7 @@ const EmployeeReportContent: React.FC<{
diff --git a/src/pages/SettingsNew.tsx b/src/pages/SettingsNew.tsx
index 5fe63e7..8c67b85 100644
--- a/src/pages/SettingsNew.tsx
+++ b/src/pages/SettingsNew.tsx
@@ -1,295 +1,295 @@
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { useAuth } from '../contexts/AuthContext';
-import Sidebar from '../components/figma/Sidebar';
-
-interface UserProfile {
- fullName: string;
- email: string;
- profilePicture?: string;
-}
-
-type ThemeMode = 'system' | 'light' | 'dark';
-
-const SettingsNew: React.FC = () => {
- const { user } = useAuth();
- const navigate = useNavigate();
-
- const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
- const [userProfile, setUserProfile] = useState
({
- fullName: 'John Doe',
- email: 'Johndoe1234@gmail.com'
- });
- const [selectedTheme, setSelectedTheme] = useState('light');
-
- const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
- setUserProfile(prev => ({
- ...prev,
- [field]: value
- }));
- };
-
- const handlePhotoUpload = () => {
- // In a real app, this would open a file picker
- alert('Photo upload functionality would be implemented here');
- };
-
- const handleSaveChanges = () => {
- // In a real app, this would save to backend
- alert('Settings saved successfully!');
- };
-
- const handleReset = () => {
- setUserProfile({
- fullName: 'John Doe',
- email: 'Johndoe1234@gmail.com'
- });
- setSelectedTheme('light');
- };
-
- if (!user) {
- navigate('/login');
- return null;
- }
-
- return (
-
-
-
-
- {/* Tab Navigation */}
-
-
-
setActiveTab('general')}
- className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
- }`}
- >
-
- General Settings
-
- {activeTab === 'general' && (
-
- )}
-
-
setActiveTab('billing')}
- className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
- }`}
- >
-
- Plan & Billings
-
- {activeTab === 'billing' && (
-
- )}
-
-
-
-
-
- {/* General Settings Content */}
- {activeTab === 'general' && (
- <>
- {/* Profile Information Section */}
-
-
-
Profile Information
-
Update your personal details, and keep your profile up to date.
-
-
- {/* Profile Picture Section */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Profile Picture
-
PNG, JPEG, GIF Under 10MB
-
-
-
-
-
- {/* Name and Email Fields */}
-
-
-
-
-
-
-
handleProfileUpdate('fullName', e.target.value)}
- className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
- />
-
-
-
-
-
-
-
-
-
handleProfileUpdate('email', e.target.value)}
- className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
- />
-
-
-
-
-
-
-
- {/* Divider */}
-
-
- {/* Theme Customization Section */}
-
-
-
Theme Customization
-
Personalize your interface with light or dark mode and enhance your visual experience.
-
-
- {/* System Preference */}
-
setSelectedTheme('system')}
- className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
- }`}
- >
-
-
-
-
-
System preference
-
-
- {/* Light Mode */}
-
setSelectedTheme('light')}
- className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
- }`}
- >
-
-
-
-
-
-
Light Mode
-
-
- {/* Dark Mode */}
-
setSelectedTheme('dark')}
- className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
- }`}
- >
-
-
-
-
-
-
Dark Mode
-
-
-
-
- {/* Another Divider */}
-
-
- {/* Action Buttons */}
-
- >
- )}
-
- {/* Billing Content */}
- {activeTab === 'billing' && (
-
-
-
Plan & Billing
-
Billing management features would be implemented here.
-
-
- )}
-
-
-
- );
-};
-
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import Sidebar from '../components/figma/Sidebar';
+
+interface UserProfile {
+ fullName: string;
+ email: string;
+ profilePicture?: string;
+}
+
+type ThemeMode = 'system' | 'light' | 'dark';
+
+const SettingsNew: React.FC = () => {
+ const { user } = useAuth();
+ const navigate = useNavigate();
+
+ const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
+ const [userProfile, setUserProfile] = useState({
+ fullName: 'John Doe',
+ email: 'Johndoe1234@gmail.com'
+ });
+ const [selectedTheme, setSelectedTheme] = useState('light');
+
+ const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
+ setUserProfile(prev => ({
+ ...prev,
+ [field]: value
+ }));
+ };
+
+ const handlePhotoUpload = () => {
+ // In a real app, this would open a file picker
+ alert('Photo upload functionality would be implemented here');
+ };
+
+ const handleSaveChanges = () => {
+ // In a real app, this would save to backend
+ alert('Settings saved successfully!');
+ };
+
+ const handleReset = () => {
+ setUserProfile({
+ fullName: 'John Doe',
+ email: 'Johndoe1234@gmail.com'
+ });
+ setSelectedTheme('light');
+ };
+
+ if (!user) {
+ navigate('/login');
+ return null;
+ }
+
+ return (
+
+
+
+
+ {/* Tab Navigation */}
+
+
+
setActiveTab('general')}
+ className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
+ }`}
+ >
+
+ General Settings
+
+ {activeTab === 'general' && (
+
+ )}
+
+
setActiveTab('billing')}
+ className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
+ }`}
+ >
+
+ Plan & Billings
+
+ {activeTab === 'billing' && (
+
+ )}
+
+
+
+
+
+ {/* General Settings Content */}
+ {activeTab === 'general' && (
+ <>
+ {/* Profile Information Section */}
+
+
+
Profile Information
+
Update your personal details, and keep your profile up to date.
+
+
+ {/* Profile Picture Section */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Profile Picture
+
PNG, JPEG, GIF Under 10MB
+
+
+
+
+
+ {/* Name and Email Fields */}
+
+
+
+
+
+
+
handleProfileUpdate('fullName', e.target.value)}
+ className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
+ />
+
+
+
+
+
+
+
+
+
handleProfileUpdate('email', e.target.value)}
+ className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
+ />
+
+
+
+
+
+
+
+ {/* Divider */}
+
+
+ {/* Theme Customization Section */}
+
+
+
Theme Customization
+
Personalize your interface with light or dark mode and enhance your visual experience.
+
+
+ {/* System Preference */}
+
setSelectedTheme('system')}
+ className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
+ }`}
+ >
+
+
+
+
+
System preference
+
+
+ {/* Light Mode */}
+
setSelectedTheme('light')}
+ className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
+ }`}
+ >
+
+
+
+
+
+
Light Mode
+
+
+ {/* Dark Mode */}
+
setSelectedTheme('dark')}
+ className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
+ }`}
+ >
+
+
+
+
+
+
Dark Mode
+
+
+
+
+ {/* Another Divider */}
+
+
+ {/* Action Buttons */}
+
+ >
+ )}
+
+ {/* Billing Content */}
+ {activeTab === 'billing' && (
+
+
+
Plan & Billing
+
Billing management features would be implemented here.
+
+
+ )}
+
+
+
+ );
+};
+
export default SettingsNew;
\ No newline at end of file
diff --git a/src/pages/Submissions.tsx b/src/pages/Submissions.tsx
index 2cb4100..bd442aa 100644
--- a/src/pages/Submissions.tsx
+++ b/src/pages/Submissions.tsx
@@ -29,19 +29,33 @@ const Submissions: React.FC = () => {
try {
setLoading(true);
// Use the secure API service to get submissions
- // const data = await secureApi.getSubmissions();
- const data = { submissions: [] }; // temp fix
+ const data = await secureApi.getSubmissions();
- if (data) {
- // setSubmissions(data.submissions);
+ if (data && data.submissions) {
+ // Transform submissions to include employee data
+ const submissionsWithEmployees: Record = {};
+
+ Object.entries(data.submissions).forEach(([employeeId, submission]) => {
+ const employee = employees.find(emp => emp.id === employeeId);
+ if (employee) {
+ submissionsWithEmployees[employeeId] = {
+ ...submission as EmployeeSubmission,
+ employee
+ };
+ }
+ });
+
+ setSubmissions(submissionsWithEmployees);
// Auto-select first employee with submission
- const employeesWithSubmissions = employees.filter(emp => data.submissions?.[emp.id]);
+ const employeesWithSubmissions = employees.filter(emp => submissionsWithEmployees[emp.id]);
if (employeesWithSubmissions.length > 0) {
setSelectedEmployee(employeesWithSubmissions[0]);
}
} else {
- // console.error('Failed to load submissions:', response.statusText);
+ console.warn('No submissions data received from API');
+ // Load demo data for development if no real data
+ loadDemoSubmissions();
}
} catch (error) {
console.error('Error loading submissions:', error);
@@ -52,7 +66,11 @@ const Submissions: React.FC = () => {
}
};
- loadSubmissions();
+ if (employees.length > 0) {
+ loadSubmissions();
+ } else {
+ setLoading(false);
+ }
}, [employees]);
const loadDemoSubmissions = () => {
@@ -61,6 +79,7 @@ const Submissions: React.FC = () => {
employees.forEach((employee, index) => {
if (index < 3) { // Only add submissions for first 3 employees
+ console.log(employee);
demoSubmissions[employee.id] = {
employeeId: employee.id,
employee,
@@ -123,17 +142,51 @@ const Submissions: React.FC = () => {
const submission = submissions[selectedEmployee.id];
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
- // Map EMPLOYEE_QUESTIONS to actual answers
- EMPLOYEE_QUESTIONS.forEach(q => {
- const answer = submission.answers[q.id];
- if (answer && answer.trim()) {
- questionsAndAnswers.push({
- question: q.prompt,
- answer: answer,
- isLong: answer.length > 150 // Mark long answers for different styling
- });
+ // Handle different submission formats
+ let submissionAnswers: Record = {};
+
+ if (submission.answers) {
+ if (Array.isArray(submission.answers)) {
+ // If answers is an array of {question, answer} objects
+ submissionAnswers = submission.answers.reduce((acc, item: any) => {
+ if (item.question && item.answer) {
+ acc[item.question] = item.answer;
+ }
+ return acc;
+ }, {} as Record);
+ } else {
+ // If answers is already a key-value object
+ submissionAnswers = submission.answers as Record;
}
- });
+ }
+
+ // If we have structured answers, map them to questions
+ if (Object.keys(submissionAnswers).length > 0) {
+ // Try to match with EMPLOYEE_QUESTIONS first
+ EMPLOYEE_QUESTIONS.forEach(q => {
+ const answer = submissionAnswers[q.id];
+ if (answer && answer.trim()) {
+ questionsAndAnswers.push({
+ question: q.prompt,
+ answer: answer,
+ isLong: answer.length > 150
+ });
+ }
+ });
+
+ // Add any additional answers not in EMPLOYEE_QUESTIONS
+ Object.entries(submissionAnswers).forEach(([key, answer]) => {
+ if (answer && ((typeof answer === 'string' && answer.trim()) || typeof answer === 'boolean' || typeof answer === 'number') && !EMPLOYEE_QUESTIONS.find(q => q.id === key)) {
+ // Format the key as a readable question
+ const formattedQuestion = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+ questionsAndAnswers.push({
+ question: formattedQuestion,
+ answer: answer,
+ isLong: answer.length > 150
+ });
+ }
+ });
+ }
return questionsAndAnswers;
};
@@ -298,7 +351,7 @@ const SubmissionContent: React.FC<{
questionsAndAnswers.map((qa, index) => (
diff --git a/src/services/imageStorageService.ts b/src/services/imageStorageService.ts
index defc1f6..27e9948 100644
--- a/src/services/imageStorageService.ts
+++ b/src/services/imageStorageService.ts
@@ -18,8 +18,6 @@ export interface StoredImage {
* @param file - The image file to upload
* @param collectionName - Collection name (e.g., 'company-logos')
* @param documentId - Document ID (e.g., orgId)
- * @param orgId - Organization ID
- * @param userId - User ID for authentication
* @param maxWidth - Maximum width for resizing (default: 128)
* @param maxHeight - Maximum height for resizing (default: 128)
* @returns Promise with stored image data
@@ -28,8 +26,6 @@ export const uploadImage = async (
file: File,
collectionName: string,
documentId: string,
- orgId: string,
- userId: string,
maxWidth: number = 128,
maxHeight: number = 128
): Promise => {
@@ -58,7 +54,7 @@ export const uploadImage = async (
};
try {
- const result = await secureApi.uploadImage(orgId, userId, imageData);
+ const result = await secureApi.uploadImage(imageData);
if (!result.success) {
throw new Error('Failed to upload image');
@@ -84,18 +80,14 @@ export const uploadImage = async (
* Retrieve an image through secure API
* @param collectionName - Collection name
* @param documentId - Document ID
- * @param orgId - Organization ID
- * @param userId - User ID for authentication
* @returns Promise with stored image data or null if not found
*/
export const getImage = async (
collectionName: string,
- documentId: string,
- orgId: string,
- userId: string
+ documentId: string
): Promise => {
try {
- const result = await secureApi.getImage(orgId, userId, collectionName, documentId);
+ const result = await secureApi.getImage(collectionName, documentId);
return result; // getImage already returns StoredImage | null
} catch (error) {
@@ -108,18 +100,14 @@ export const getImage = async (
* Delete an image through secure API
* @param collectionName - Collection name
* @param documentId - Document ID
- * @param orgId - Organization ID
- * @param userId - User ID for authentication
* @returns Promise indicating success
*/
export const deleteImage = async (
collectionName: string,
documentId: string,
- orgId: string,
- userId: string
): Promise => {
try {
- const result = await secureApi.deleteImage(orgId, userId, collectionName, documentId);
+ const result = await secureApi.deleteImage(collectionName, documentId);
return result; // deleteImage already returns boolean
} catch (error) {
@@ -134,10 +122,9 @@ export const deleteImage = async (
*/
export const uploadCompanyLogo = async (
file: File,
- orgId: string,
- userId: string
+ orgId: string
): Promise => {
- return uploadImage(file, 'company-logos', orgId, orgId, userId, 128, 128);
+ return uploadImage(file, 'company-logos', orgId, 128);
};
/**
@@ -145,10 +132,9 @@ export const uploadCompanyLogo = async (
* Requires authentication context to get userId
*/
export const getCompanyLogo = async (
- orgId: string,
- userId: string
+ orgId: string
): Promise => {
- return getImage('company-logos', orgId, orgId, userId);
+ return getImage('company-logos', orgId);
};
/**
@@ -156,10 +142,9 @@ export const getCompanyLogo = async (
* Requires authentication context to get userId
*/
export const deleteCompanyLogo = async (
- orgId: string,
- userId: string
+ orgId: string
): Promise => {
- return deleteImage('company-logos', orgId, orgId, userId);
+ return deleteImage('company-logos', orgId);
};
/**
diff --git a/src/services/secureApi.ts b/src/services/secureApi.ts
index a07660b..ed63c9a 100644
--- a/src/services/secureApi.ts
+++ b/src/services/secureApi.ts
@@ -47,6 +47,10 @@ interface OrgData {
[key: string]: any;
}
+interface GetSubmissions {
+ submissions: Submission[];
+}
+
interface GetUserOrganizations {
organizations: UserOrganization[];
}
@@ -186,11 +190,11 @@ class SecureApiService {
}
// Submission Methods
- async getSubmissions(): Promise> {
- const response = await this.makeRequest<{ submissions: Record }>(
+ async getSubmissions(): Promise {
+ const response = await this.makeRequest<{ submissions: Submission[] }>(
'getSubmissions'
);
- return response.submissions;
+ return response;
}
// Report Methods
@@ -260,8 +264,9 @@ class SecureApiService {
return this.makeRequest('generateEmployeeReport', 'POST', { employee, submission, companyWiki });
}
- async generateCompanyWiki(org: any, submissions: any[] = []) {
- return this.makeRequest('generateCompanyWiki', 'POST', { org, submissions });
+ async generateCompanyWiki(org: any, submissions: any[] = []): Promise {
+ const response = await this.makeRequest<{ report: CompanyReport }>('generateCompanyWiki', 'POST', { org, submissions });
+ return response.report;
}
async chat(message: string, employeeId?: string, context?: any, mentions?: any[], attachments?: any[]) {
@@ -341,6 +346,11 @@ class SecureApiService {
}
}
+ // Migration Methods
+ async migrateOwnersFromEmployees(): Promise<{ success: boolean; migratedCount: number; ownerInfo?: any }> {
+ return this.makeRequest('migrateOwnersFromEmployees', 'POST');
+ }
+
// Onboarding Methods
async completeOnboarding(onboardingData: any): Promise<{ success: boolean; error?: string }> {
return this.makeRequest('onboarding/complete', 'POST', onboardingData);
diff --git a/src/types.ts b/src/types.ts
index 97af3f2..bc63219 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,6 @@
import React from 'react';
+import { inherits } from 'util';
export enum Theme {
Light = 'light',
@@ -11,13 +12,12 @@ export interface Employee {
id: string;
name: string;
email: string;
- role?: 'owner' | 'admin' | 'employee';
+ role?: string; // Remove 'owner' from the union type since employees are never owners
department?: string | 'General';
joinedAt?: number;
status: 'invited' | 'active';
initials?: string;
inviteCode?: string;
- isOwner?: boolean;
}
@@ -103,6 +103,12 @@ export interface Organization {
updatedAt: number;
onboardingCompleted: boolean;
ownerId: string;
+ ownerInfo?: {
+ id: string;
+ name: string;
+ email: string;
+ joinedAt: number;
+ };
// Subscription fields (will be populated after Stripe setup)
subscription: {
status: 'trial' | 'active' | 'past_due' | 'canceled';
@@ -146,42 +152,24 @@ export interface EmployeeReport {
selfAwareness: string;
emotionalResponses: string;
growthDesire: string;
-
- strengths?: string[];
- weaknesses?: string[];
- value?: number;
};
- actionableItems?: { id: string; title: string; impact: 'High' | 'Medium' | 'Low'; effort: 'High' | 'Medium' | 'Low'; description: string; }[];
- roleFitCandidates?: { employeeId: string; roles: string[]; rationale: string; score: number; }[];
- potentialExits?: { employeeId: string; risk: 'Low' | 'Medium' | 'High'; reason: string; }[];
- traitWeighting?: { trait: string; weight: number; rationale?: string; }[];
strengths: string[];
- weaknesses: {
- isCritical: boolean;
- description: string;
- }[];
+ weaknesses: string[];
opportunities: {
- roleAdjustment: string;
- accountabilitySupport: string;
- description?: string;
+ title: string;
+ description: string;
}[];
risks: string[];
recommendations: string[];
- recommendation: {
- action: 'Keep' | 'Restructure' | 'Terminate';
- details: string[];
- };
- grading: {
- department: string;
- lead: string;
- support: string;
+ gradingOverview: {
+ employeeName: string;
grade: string;
- comment: string;
- scores: { subject: string; value: number; fullMark: number; }[];
- }[];
- suitabilityScore?: number;
- retentionRisk?: 'Low' | 'Medium' | 'High';
- costEffectiveness?: 'Underperforming' | 'Aligned' | 'High Value';
+ reliability: number;
+ roleFit: number;
+ scalability: number;
+ output: number;
+ initiative: number;
+ };
}
export interface Submission {
@@ -240,7 +228,6 @@ export interface CompanyReport {
reasoning: string;
urgency?: 'high' | 'medium' | 'low'; // UI alias
}[];
- recommendations: string[];
forwardOperatingPlan?: {
title: string;
details: string[];
@@ -266,8 +253,6 @@ export interface CompanyReport {
teamScores: {
employeeName: string;
grade: string;
- // Each of the following is out of 10, total being 50 points
- // These gets displayed as radar charts
reliability: number;
roleFit: number;
scalability: number;
@@ -275,7 +260,6 @@ export interface CompanyReport {
initiative: number;
}[];
}[];
-
executiveSummary: string;
}