fix most of the listed bugs

This commit is contained in:
Ra
2025-08-26 11:23:27 -07:00
parent 772d1d4c10
commit ad15aaa35e
22 changed files with 3493 additions and 2375 deletions

161
employee_report_schema.json Normal file
View File

@@ -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 Gentrys strategic skills."
}
]
]
},
"risks": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"Without strict structure, Gentrys 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 Gentrys deals."
]
]
}
}
}

View File

@@ -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"] }
const RESPONSE_FORMAT_EMPLOYEE = {
"type": "object",
"properties": {
"employeeId": {
"type": "string"
},
required: ["name", "value", "trend"]
"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."
]
}
}
},
required: ["summary", "metrics"]
"insights": {
"type": "object",
"properties": {
"personalityInsights": {
"type": "string",
"examples": [
"Loyal, well-liked by influencers, eager to grow, client-facing interest."
]
},
immediateHiringNeeds: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
role: { type: "string" },
urgency: { enum: ["low", "medium", "high"] },
reason: { type: "string" }
"psychologicalIndicators": {
"type": "array",
"items": {
"type": "string"
},
required: ["role", "urgency", "reason"]
}
"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."
]
]
},
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" } }
"selfAwareness": {
"type": "string",
"examples": [
"High acknowledges weaknesses like lateness and disorganization."
]
},
required: ["name", "owner", "kpis"]
}
"emotionalResponses": {
"type": "string",
"examples": [
"Frustrated by campaign disorganization; would prefer closer collaboration."
]
},
risks: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
risk: { type: "string" },
mitigation: { type: "string" }
},
required: ["risk", "mitigation"]
"growthDesire": {
"type": "string",
"examples": [
"Interested in becoming more client-facing and shifting toward biz dev."
]
}
}
},
required: ["nextQuarterObjectives", "initiatives", "risks"]
"strengths": {
"type": "array",
"items": {
"type": "string"
},
organizationalInsights: {
type: "object",
additionalProperties: false,
properties: {
culture: { type: "string" },
teamDynamics: { type: "string" },
blockers: { 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."
]
]
},
required: ["culture", "teamDynamics", "blockers"]
"weaknessess": {
"type": "array",
"items": {
"type": "string"
},
strengths: { 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."
]
},
required: ["companyPerformance", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths"]
"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 Gentrys strategic skills."
}
]
]
},
"risks": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"Without strict structure, Gentrys 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 Gentrys 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,17 +1214,20 @@ ${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.
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({
@@ -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(),
};
}
res.json({
return res.status(200).json({
success: true,
...report,
...wiki,
...report
});
}
} 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));
@@ -2488,3 +2813,81 @@ exports.deleteImage = onRequest({ cors: true }, async (req, res) => {
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" });
}
});

View File

@@ -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;
}

View File

@@ -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 = () => (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-gray-900 border-t-transparent"></div>
</div>
);
// Suspense wrapper for lazy components
const SuspenseWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<Suspense fallback={<LoadingSpinner />}>
{children}
</Suspense>
);
const RequireAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { user, loading } = useAuth();
@@ -107,23 +121,23 @@ function App() {
<UserOrganizationsProvider>
<HashRouter>
<Routes>
<Route path="/login" element={<ModernLogin />} />
<Route path="/login/:inviteCode" element={<ModernLogin />} />
<Route path="/login" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
<Route path="/login/:inviteCode" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
{/* <Route path="/invite/:inviteCode" element={<InviteRedirect />} /> */}
{/* Employee questionnaire - no auth needed, uses invite code */}
<Route path="/employee-form/:inviteCode" element={<EmployeeQuestionnaireNew />} />
<Route path="/questionnaire/:inviteCode" element={<EmployeeQuestionnaireNew />} />
<Route path="/employee-form/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
<Route path="/questionnaire/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
{/* Legacy employee questionnaire route for backwards compatibility */}
<Route path="/employee-form-legacy/:inviteCode" element={<EmployeeQuestionnaire />} />
<Route path="/employee-form-legacy/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaire /></SuspenseWrapper>} />
{/* Organization Selection - after auth, before entering app */}
<Route
path="/org-selection"
element={
<RequireAuth>
<OrgSelection />
<SuspenseWrapper><OrgSelection /></SuspenseWrapper>
</RequireAuth>
}
/>
@@ -133,7 +147,7 @@ function App() {
path="/subscription-setup"
element={
<RequireAuth>
<SubscriptionSetup />
<SuspenseWrapper><SubscriptionSetup /></SuspenseWrapper>
</RequireAuth>
}
/>
@@ -145,7 +159,7 @@ function App() {
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<EmployeeQuestionnaireNew />
<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>
@@ -159,7 +173,7 @@ function App() {
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<EmployeeQuestionnaire />
<SuspenseWrapper><EmployeeQuestionnaire /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>
@@ -172,7 +186,7 @@ function App() {
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<EmployeeQuestionnaireSteps />
<SuspenseWrapper><EmployeeQuestionnaireSteps /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>
@@ -185,14 +199,14 @@ function App() {
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<Onboarding />
<SuspenseWrapper><Onboarding /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>
}
/>
<Route path="/questionnaire-complete" element={<QuestionnaireComplete />} />
<Route path="/questionnaire-complete" element={<SuspenseWrapper><QuestionnaireComplete /></SuspenseWrapper>} />
{/* New Figma Chat Implementation - Standalone route */}
<Route
@@ -202,7 +216,7 @@ function App() {
<RequireOrgSelection>
<OrgProviderWrapper>
<RequireOnboarding>
<Chat />
<SuspenseWrapper><Chat /></SuspenseWrapper>
</RequireOnboarding>
</OrgProviderWrapper>
</RequireOrgSelection>
@@ -212,13 +226,13 @@ function App() {
{/* New Figma Help Implementation - Standalone route */}
<Route
path="/help-new"
path="/help"
element={
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<RequireOnboarding>
<HelpNew />
<SuspenseWrapper><HelpNew /></SuspenseWrapper>
</RequireOnboarding>
</OrgProviderWrapper>
</RequireOrgSelection>
@@ -228,13 +242,13 @@ function App() {
{/* New Figma Settings Implementation - Standalone route */}
<Route
path="/settings-new"
path="/settings"
element={
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<RequireOnboarding>
<SettingsNew />
<SuspenseWrapper><SettingsNew /></SuspenseWrapper>
</RequireOnboarding>
</OrgProviderWrapper>
</RequireOrgSelection>
@@ -249,7 +263,7 @@ function App() {
<RequireOrgSelection>
<OrgProviderWrapper>
<RequireOnboarding>
<Layout />
<SuspenseWrapper><Layout /></SuspenseWrapper>
</RequireOnboarding>
</OrgProviderWrapper>
</RequireOrgSelection>
@@ -257,11 +271,11 @@ function App() {
}
>
<Route path="/" element={<Navigate to="/reports" replace />} />
<Route path="/company-wiki" element={<CompanyWiki />} />
<Route path="/submissions" element={<Submissions />} />
<Route path="/reports" element={<Reports />} />
<Route path="/help" element={<HelpAndSettings />} />
<Route path="/settings" element={<HelpAndSettings />} />
<Route path="/company-wiki" element={<SuspenseWrapper><CompanyWiki /></SuspenseWrapper>} />
<Route path="/submissions" element={<SuspenseWrapper><Submissions /></SuspenseWrapper>} />
<Route path="/reports" element={<SuspenseWrapper><Reports /></SuspenseWrapper>} />
{/* <Route path="/help" element={<SuspenseWrapper><HelpNew /></SuspenseWrapper>} />
<Route path="/settings" element={<SuspenseWrapper><SettingsNew /></SuspenseWrapper>} /> */}
</Route>
{/* Debug routes */}
@@ -271,7 +285,7 @@ function App() {
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<QuestionTypesDemo />
<SuspenseWrapper><QuestionTypesDemo /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>
@@ -283,7 +297,7 @@ function App() {
<RequireAuth>
<RequireOrgSelection>
<OrgProviderWrapper>
<FormsDashboard />
<SuspenseWrapper><FormsDashboard /></SuspenseWrapper>
</OrgProviderWrapper>
</RequireOrgSelection>
</RequireAuth>

View File

@@ -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<any>(null);
const [error, setError] = useState<string | null>(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 (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 max-w-2xl mx-auto">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
Data Migration Required
</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
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.
</p>
</div>
<div className="mt-4">
<div className="flex">
<button
type="button"
onClick={runMigration}
disabled={isRunning}
className="bg-yellow-100 px-2 py-1.5 rounded-md text-sm font-medium text-yellow-800 hover:bg-yellow-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-yellow-50 focus:ring-yellow-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRunning ? 'Running Migration...' : 'Run Migration'}
</button>
</div>
</div>
</div>
</div>
{result && (
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-green-800">
Migration Completed Successfully
</h3>
<div className="mt-2 text-sm text-green-700">
<p>Migrated {result.migratedCount} owner record(s) from employees collection.</p>
{result.ownerInfo && (
<p className="mt-1">
Owner: {result.ownerInfo.name} ({result.ownerInfo.email})
</p>
)}
</div>
</div>
</div>
</div>
)}
{error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
Migration Failed
</h3>
<div className="mt-2 text-sm text-red-700">
<p>{error}</p>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default OwnerMigrationUtility;

View File

@@ -309,13 +309,13 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
}
export const Button: React.FC<ButtonProps> = ({ 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 = {

View File

@@ -62,7 +62,7 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection
sectionName
}) => {
return (
<div className="w-[464px] max-w-[464px] min-w-[464px] absolute top-[24px] left-1/2 transform -translate-x-1/2 flex flex-col justify-start items-center gap-4">
<div className="min-w-[464px] col-span-2 col-start-6 inline-flex flex-col justify-self-center justify-center items-center gap-4 self-start">
<div className="p-4 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
{Array.from({ length: 7 }, (_, index) => {
const isActive = index === currentSection - 1;
@@ -70,11 +70,11 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection
<div key={index}>
{isActive ? (
<svg width="24" height="4" viewBox="0 0 24 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="4" rx="2" fill="var(--Brand-Orange, #3399FF)" />
<rect width="24" height="4" rx="2" fill="var(--Brand-Orange)" />
</svg>
) : (
<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="4" height="4" rx="2" fill="var(--Neutrals-NeutralSlate300, #D5D7DA)" />
<rect width="4" height="4" rx="2" fill="var(--Neutrals-NeutralSlate300)" />
</svg>
)}
</div>
@@ -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 (
<div className="w-full self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex justify-start items-center overflow-hidden">
<div className="flex-1 h-[810px] px-32 py-48 bg-[--Neutrals-NeutralSlate0] flex justify-center items-center gap-2.5 overflow-hidden">
@@ -167,7 +167,7 @@ export const SectionIntro: React.FC<{
</div>
<button
onClick={onStart}
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-600 transition-colors"
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
@@ -175,7 +175,7 @@ export const SectionIntro: React.FC<{
</button>
</div>
</div>
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
<div className="flex-1 h-max px-20 py-16 flex justify-center items-center gap-2.5 flex-shrink">
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
<img className="self-stretch flex-1" src={imageUrl} alt={title} />
</div>
@@ -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 (
<div className="w-full self-stretch h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
<div className="w-full h-full py-6 relative bg-[--Neutrals-NeutralSlate0] grid grid-cols-12 grid-rows-5 justify-center items-center gap-3">
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col row-start-3 col-span-12 self-center justify-self-center justify-center gap-12">
<div className="self-stretch flex flex-col justify-start items-start gap-8">
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
{question}
@@ -341,7 +341,7 @@ export const TextAreaQuestion: React.FC<{
{onSkip && (
<button
onClick={onSkip}
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-[--Neutrals-NeutralSlate200 transition-colors"
className="px-3 py-1.5 right-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden hover:bg-[--Neutrals-NeutralSlate200] transition-colors"
>
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">Skip</div>
</button>
@@ -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 (
<div className="w-full self-stretch h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
<div className="w-full h-full py-6 relative bg-[--Neutrals-NeutralSlate0] grid grid-cols-12 grid-rows-5 justify-center items-center gap-3">
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col row-start-3 col-span-12 self-center justify-self-center justify-center gap-12">
<div className="self-stretch flex flex-col justify-center items-center gap-8">
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
{question}
</div>
<div className="inline-flex justify-center items-center gap-3">
<div className="inline-flex justify-center w-max items-center gap-3">
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">
{leftLabel}
</div>
@@ -392,10 +392,10 @@ export const RatingScaleQuestion: React.FC<{
<button
key={ratingValue}
onClick={() => 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]'
}`}
>
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-[--Neutrals-NeutralSlate950] bg-[--Neutrals-NeutralSlate50]' : 'text-[--Neutrals-NeutralSlate0] hover:text-[--Neutrals-NeutralSlate800]'
}`}>
{ratingValue}
</div>
@@ -421,7 +421,7 @@ export const RatingScaleQuestion: React.FC<{
<button
onClick={onNext}
disabled={!value}
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
@@ -474,8 +474,8 @@ export const YesNoChoice: React.FC<{
sectionName?: string;
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
return (
<div className="w-full self-stretch h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9">
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
<div className="w-full h-full py-6 relative bg-[--Neutrals-NeutralSlate0] grid grid-cols-12 grid-rows-5 justify-center items-center gap-3">
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col row-start-3 col-span-12 self-center justify-self-center justify-center gap-12">
<div className="self-stretch flex flex-col justify-start items-start gap-8">
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
{question}
@@ -483,20 +483,20 @@ export const YesNoChoice: React.FC<{
<div className="self-stretch inline-flex justify-center items-center gap-3">
<button
onClick={() => 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]'
}`}
>
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'No' ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'No' ? 'text-[--Neutrals-NeutralSlate950] bg-[--Neutrals-NeutralSlate50]' : 'text-[--Neutrals-NeutralSlate50] hover:bg-[--Neutrals-NeutralSlate50] hover:text-[--Neutrals-NeutralSlate950]'
}`}>
No
</div>
</button>
<button
onClick={() => 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]'
}`}
>
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'Yes' ? 'text-[--Neutrals-NeutralSlate0]' : 'text-[--Neutrals-NeutralSlate0]'
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'Yes' ? 'text-[--Neutrals-NeutralSlate950] bg-[--Neutrals-NeutralSlate50]' : 'text-[--Neutrals-NeutralSlate50] hover:bg-[--Neutrals-NeutralSlate50] hover:text-[--Neutrals-NeutralSlate950]'
}`}>
Yes
</div>
@@ -517,7 +517,7 @@ export const YesNoChoice: React.FC<{
<button
onClick={onNext}
disabled={!value}
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
@@ -560,7 +560,7 @@ export const YesNoChoice: React.FC<{
// Thank You Page Component
export const ThankYouPage: React.FC = () => {
return (
<div className="w-full self-stretch bg-white inline-flex justify-start items-center overflow-hidden">
<div className="w-full self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex justify-start items-center overflow-hidden">
<div className="flex-1 h-[810px] px-32 py-48 bg-[--Neutrals-NeutralSlate0] flex justify-center items-center gap-2.5 overflow-hidden">
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
<div className="self-stretch flex flex-col justify-start items-start gap-6">
@@ -581,9 +581,9 @@ export const ThankYouPage: React.FC = () => {
</div>
</div>
</div>
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Thank you" />
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5">
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5">
<img className="self-stretch flex-1" src="/image/onboarding-robot.png" alt="Thank you" />
</div>
</div>
</div>

View File

@@ -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<HTMLDivElement>(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,52 +161,80 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
const handleNavClick = (path: string) => {
navigate(path);
};
return (
<div className="h-full w-64 max-w-64 min-w-64 px-3 pt-4 pb-3 bg-[--Neutrals-NeutralSlate0] border-r border-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-between items-center overflow-hidden">
{/* Header Section */}
<div className="self-stretch flex flex-col justify-start items-start gap-5">
{/* Company Selector */}
<div className="w-60 pl-2 pr-4 py-2 bg-[--Neutrals-NeutralSlate0] rounded-3xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex justify-between items-center overflow-hidden">
{/* Company Selector Dropdown */}
<div className="relative w-60" ref={dropdownRef}>
<div
className="w-60 pl-2 pr-4 py-2 bg-[--Neutrals-NeutralSlate0] rounded-3xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex justify-between items-center overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate50]"
onClick={() => refreshOrganizations() && setShowOrgDropdown(!showOrgDropdown)}
>
<div className="flex-1 flex justify-start items-center gap-2">
<div className="w-8 h-8 rounded-full flex justify-start items-center gap-2.5">
<div className="w-8 h-8 relative bg-[--Brand-Orange] rounded-full outline outline-[1.60px] outline-offset-[-1.60px] outline-white/10 overflow-hidden">
<div className="left-0 top-0 absolute">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="url(#paint0_linear_731_19280)" />
<defs>
<linearGradient id="paint0_linear_731_19280" x1="16" y1="3.97364e-07" x2="17.3333" y2="32" gradientUnits="userSpaceOnUse">
<stop stopColor="white" stopOpacity="0" />
<stop offset="1" stopColor="white" stopOpacity="0.12" />
</linearGradient>
</defs>
</svg>
</div>
<div className="left-[8.80px] top-[7.20px] absolute">
<div className="w-8 h-8 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
<div data-svg-wrapper className="left-[7px] top-[7px] absolute">
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_731_19281)">
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M4.34367 10.6873C4.67023 11.018 4.67022 11.5541 4.34366 11.8848L4.32693 11.9018C4.00036 12.2325 3.47089 12.2325 3.14433 11.9018C2.81777 11.5711 2.81778 11.0349 3.14434 10.7042L3.16107 10.6873C3.48764 10.3566 4.0171 10.3566 4.34367 10.6873Z" fill="url(#paint0_linear_731_19281)" />
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M8.2752 10.9423C8.60118 11.2736 8.60022 11.8097 8.27306 12.1398L5.95673 14.477C5.62957 14.8071 5.1001 14.8061 4.77413 14.4748C4.44815 14.1435 4.44911 13.6074 4.77627 13.2773L7.09261 10.9401C7.41976 10.61 7.94923 10.611 8.2752 10.9423Z" fill="url(#paint1_linear_731_19281)" />
<g filter="url(#filter0_d_1141_1906)">
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M4.34354 10.6855C4.67011 11.0162 4.6701 11.5524 4.34353 11.8831L4.32681 11.9C4.00024 12.2307 3.47077 12.2307 3.14421 11.9C2.81765 11.5693 2.81765 11.0331 3.14422 10.7024L3.16095 10.6855C3.48751 10.3548 4.01698 10.3548 4.34354 10.6855Z" fill="url(#paint0_linear_1141_1906)" />
<path opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M8.27545 10.9405C8.60142 11.2718 8.60046 11.808 8.27331 12.1381L5.95697 14.4752C5.62981 14.8053 5.10035 14.8043 4.77437 14.473C4.4484 14.1417 4.44936 13.6056 4.77651 13.2755L7.09285 10.9383C7.42001 10.6082 7.94947 10.6092 8.27545 10.9405Z" fill="url(#paint1_linear_1141_1906)" />
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M11.4179 14.9651C11.6741 14.5759 12.1932 14.4708 12.5775 14.7302L12.6277 14.7641C13.012 15.0235 13.1158 15.5492 12.8596 15.9384C12.6034 16.3275 12.0842 16.4326 11.7 16.1732L11.6498 16.1393C11.2655 15.8799 11.1617 15.3542 11.4179 14.9651Z" fill="url(#paint2_linear_1141_1906)" />
<path opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M16.9375 10.6347C17.264 10.9654 17.264 11.5016 16.9375 11.8323L15.8002 12.9839C15.4736 13.3146 14.9442 13.3146 14.6176 12.9839C14.291 12.6532 14.291 12.1171 14.6176 11.7864L15.7549 10.6347C16.0814 10.304 16.6109 10.304 16.9375 10.6347Z" fill="url(#paint3_linear_1141_1906)" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9542 6.37693C17.2808 6.70762 17.2808 7.24378 16.9542 7.57447L8.5502 16.0847C8.22364 16.4154 7.69417 16.4154 7.3676 16.0847C7.04104 15.754 7.04104 15.2179 7.3676 14.8872L15.7717 6.37693C16.0982 6.04623 16.6277 6.04623 16.9542 6.37693Z" fill="url(#paint4_linear_1141_1906)" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3649 3.75974C15.6915 4.09043 15.6915 4.62659 15.3649 4.95728L10.5315 9.85174C10.205 10.1824 9.67549 10.1824 9.34893 9.85174C9.02236 9.52104 9.02236 8.98489 9.34893 8.65419L14.1823 3.75974C14.5089 3.42905 15.0383 3.42905 15.3649 3.75974Z" fill="url(#paint5_linear_1141_1906)" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.8146 2.09918C13.1414 2.42965 13.1417 2.96581 12.8154 3.29672L6.60224 9.59685C6.27589 9.92777 5.74642 9.92813 5.41964 9.59766C5.09285 9.26719 5.0925 8.73103 5.41884 8.40011L11.632 2.09998C11.9583 1.76907 12.4878 1.76871 12.8146 2.09918Z" fill="url(#paint6_linear_1141_1906)" />
<path opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M6.66127 4.11624C6.98727 4.4475 6.98636 4.98366 6.65923 5.31378L4.22582 7.76948C3.89869 8.0996 3.36923 8.09868 3.04322 7.76741C2.71722 7.43615 2.71813 6.9 3.04526 6.56987L5.47867 4.11418C5.8058 3.78405 6.33526 3.78498 6.66127 4.11624Z" fill="url(#paint7_linear_1141_1906)" />
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M8.15116 1.66602C8.613 1.66602 8.98739 2.04514 8.98739 2.51281V2.59749C8.98739 3.06516 8.613 3.44428 8.15116 3.44428C7.68933 3.44428 7.31494 3.06516 7.31494 2.59749V2.51281C7.31494 2.04514 7.68933 1.66602 8.15116 1.66602Z" fill="url(#paint8_linear_1141_1906)" />
</g>
<defs>
<filter id="filter0_d_731_19281" x="0.398828" y="-0.399988" width="19.2014" height="22.4" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<filter id="filter0_d_1141_1906" x="0.399316" y="-0.400781" width="19.2008" height="22.3996" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feMorphology radius="1.2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_731_19281" />
<feMorphology radius="1.2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_1141_1906" />
<feOffset dy="1.8" />
<feGaussianBlur stdDeviation="1.8" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0.141176 0 0 0 0 0.141176 0 0 0 0 0.141176 0 0 0 0.1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_731_19281" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_731_19281" result="shape" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1141_1906" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1141_1906" result="shape" />
</filter>
<linearGradient id="paint0_linear_731_19281" x1="3.744" y1="10.4393" x2="3.744" y2="12.1498" gradientUnits="userSpaceOnUse">
<stop stopColor="white" stopOpacity="0.8" />
<stop offset="1" stopColor="white" stopOpacity="0.5" />
<linearGradient id="paint0_linear_1141_1906" x1="3.74388" y1="10.4375" x2="3.74388" y2="12.148" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint1_linear_731_19281" x1="6.52467" y1="10.6932" x2="6.52467" y2="14.7239" gradientUnits="userSpaceOnUse">
<stop stopColor="white" stopOpacity="0.8" />
<stop offset="1" stopColor="white" stopOpacity="0.5" />
<linearGradient id="paint1_linear_1141_1906" x1="6.52491" y1="10.6914" x2="6.52491" y2="14.7221" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint2_linear_1141_1906" x1="12.1387" y1="14.5879" x2="12.1387" y2="16.3155" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint3_linear_1141_1906" x1="15.7775" y1="10.3867" x2="15.7775" y2="13.2319" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint4_linear_1141_1906" x1="12.1609" y1="6.12891" x2="12.1609" y2="16.3327" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint5_linear_1141_1906" x1="12.3569" y1="3.51172" x2="12.3569" y2="10.0998" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint6_linear_1141_1906" x1="9.11711" y1="1.85156" x2="9.11711" y2="9.84527" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint7_linear_1141_1906" x1="4.85224" y1="3.86719" x2="4.85224" y2="8.01647" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
<linearGradient id="paint8_linear_1141_1906" x1="8.15117" y1="1.66602" x2="8.15117" y2="3.44428" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient>
</defs>
</svg>
@@ -178,16 +242,72 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
</div>
</div>
<div className="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">{companyName}</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">{org?.name || 'Select Organization'}</div>
</div>
</div>
<div>
<div className={`transition-transform duration-200 ${showOrgDropdown ? 'rotate-180' : ''}`}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.83301 12.5L9.99967 16.6667L14.1663 12.5M5.83301 7.50001L9.99967 3.33334L14.1663 7.50001" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.83301 7.50001L9.99967 11.6667L14.1663 7.50001" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
{/* Dropdown Menu */}
{showOrgDropdown && (
<div className="absolute top-full left-0 right-0 mt-2 bg-[--Neutrals-NeutralSlate0] rounded-2xl shadow-[0px_10px_30px_0px_rgba(14,18,27,0.15)] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] z-50 max-h-80 overflow-y-auto">
<div className="p-2">
{/* Current Organizations */}
{organizations.map((organization) => (
<div
key={organization.orgId}
className={`w-full px-3 py-2.5 rounded-xl flex items-center gap-3 cursor-pointer hover:bg-[--Neutrals-NeutralSlate50] ${org?.orgId === organization.orgId ? 'bg-[--Neutrals-NeutralSlate100]' : ''
}`}
onClick={() => handleOrgSwitch(organization.orgId)}
>
<div className="w-6 h-6 relative bg-[--Brand-Orange] rounded-full flex items-center justify-center text-white text-xs font-medium">
{organization.name.charAt(0).toUpperCase()}
</div>
<div className="flex-1">
<div className="text-[--Neutrals-NeutralSlate950] text-sm font-medium">{organization.name}</div>
{organization.name && (
<div className="text-[--Neutrals-NeutralSlate500] text-xs">{organization.name}</div>
)}
</div>
{org?.orgId === organization.orgId && (
<div className="w-4 h-4 text-[--Brand-Orange]">
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
</div>
)}
</div>
))}
{/* Divider */}
{organizations.length > 0 && (
<div className="my-2 border-t border-[--Neutrals-NeutralSlate200]"></div>
)}
{/* Create New Organization */}
<div
className="w-full px-3 py-2.5 rounded-xl flex items-center gap-3 cursor-pointer hover:bg-[--Neutrals-NeutralSlate50] text-[--Neutrals-NeutralSlate600]"
onClick={() => {
setShowOrgDropdown(false);
setShowCreateOrgModal(true);
}}
>
<div className="w-6 h-6 border-2 border-dashed border-[--Neutrals-NeutralSlate300] rounded-full flex items-center justify-center">
<PlusIcon className="w-3 h-3" />
</div>
<div className="flex-1">
<div className="text-sm font-medium">Create New Organization</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* Navigation Items */}
<div className="self-stretch flex flex-col justify-start items-start gap-5">
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
@@ -202,7 +322,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
>
<div className="relative">
{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)"
})}
</div>
<div className={`justify-start text-sm font-medium font-['Inter'] leading-tight ${item.active
@@ -217,6 +337,57 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
</div>
</div>
{/* Create Organization Modal */}
{showCreateOrgModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-[--Neutrals-NeutralSlate0] p-6 rounded-2xl max-w-md w-full mx-4 shadow-[0px_20px_40px_0px_rgba(14,18,27,0.25)]">
<h3 className="text-lg font-semibold text-[--Neutrals-NeutralSlate950] mb-4">Create New Organization</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[--Neutrals-NeutralSlate700] mb-2">Organization Name</label>
<input
type="text"
value={createOrgForm.name}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--Neutrals-NeutralSlate700] mb-2">Description (Optional)</label>
<textarea
value={createOrgForm.description}
onChange={(e) => setCreateOrgForm(prev => ({ ...prev, description: 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 resize-none"
rows={3}
placeholder="Brief description of your organization"
/>
</div>
</div>
<div className="flex space-x-3 mt-6">
<Button
variant="secondary"
className="flex-1"
onClick={() => {
setShowCreateOrgModal(false);
setCreateOrgForm({ name: '', description: '' });
}}
>
Cancel
</Button>
<Button
className="flex-1"
onClick={handleCreateOrg}
disabled={!createOrgForm.name.trim()}
>
Create Organization
</Button>
</div>
</div>
</div>
)}
{/* Invite Employee Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-[--background-secondary] p-6 rounded-lg max-w-md w-full mx-4">
@@ -332,12 +503,22 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
{/* Settings */}
<div
onClick={() => 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]'
}`}
>
<div className="relative">
{settingsIcon}
{React.cloneElement(settingsIcon, {
stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)"
})}
</div>
<div className={`flex-1 justify-start text-sm font-medium font-['Inter'] leading-tight ${location.pathname === "/settings"
? 'text-[--Neutrals-NeutralSlate950]'
: 'text-[--Neutrals-NeutralSlate500]'
}`}>
Settings
</div>
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Settings</div>
</div>
{/* Build Report Card */}
@@ -360,7 +541,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
</div>
</div>
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-sm font-semibold font-['Inter'] leading-tight">Build [Company]'s Report</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-sm font-semibold font-['Inter'] leading-tight">Build {org.name}'s Report</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-xs font-normal font-['Inter'] leading-none">Share this form with your team members to capture valuable info about your company to train Auditly.</div>
</div>
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
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';
@@ -73,15 +73,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
}, []);
const signInWithGoogle = async () => {
const signInWithGoogle = useCallback(async () => {
if (!isFirebaseConfigured) {
// No-op in demo mode
return;
}
await signInWithPopup(auth, googleProvider);
};
}, []);
const signOutUser = async () => {
const signOutUser = useCallback(async () => {
try {
// Sign out from Firebase if configured and user is signed in via Firebase
if (isFirebaseConfigured && auth.currentUser) {
@@ -100,9 +100,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setUser(null);
console.log('User signed out and all sessions cleared');
};
}, []);
const signInWithEmail = async (email: string, password: string) => {
const signInWithEmail = useCallback(async (email: string, password: string) => {
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
try {
console.log('Attempting Firebase auth');
@@ -118,9 +118,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
throw e;
}
};
}, []);
const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
const signUpWithEmail = useCallback(async (email: string, password: string, displayName?: string) => {
try {
const cred = await createUserWithEmailAndPassword(auth, email, password);
if (displayName) {
@@ -136,9 +136,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
throw e;
}
};
}, []);
const sendOTP = async (email: string, inviteCode?: string) => {
const sendOTP = useCallback(async (email: string, inviteCode?: string) => {
const response = await fetch(`${API_URL}/sendOTP`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -151,9 +151,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
return response.json();
};
}, []);
const verifyOTP = async (email: string, otp: string, inviteCode?: string) => {
const verifyOTP = useCallback(async (email: string, otp: string, inviteCode?: string) => {
const response = await fetch(`${API_URL}/verifyOTP`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -180,11 +180,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
localStorage.setItem('auditly_auth_token', data.token);
return data;
};
}, []);
return (
<AuthContext.Provider value={{
// Memoize the context value to prevent unnecessary re-renders
const contextValue = useMemo(() => ({
user,
loading,
signInWithGoogle,
@@ -193,7 +192,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
signUpWithEmail,
sendOTP,
verifyOTP,
}}>
}), [user, loading, signInWithGoogle, signOutUser, signInWithEmail, signUpWithEmail, sendOTP, verifyOTP]);
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);

View File

@@ -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<string, any>;
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<string, EmployeeReport>);
} catch (error) {
console.warn('Could not load reports:', error);
// Process reports data
if (reportsData.status === 'fulfilled') {
setReports(reportsData.value as Record<string, EmployeeReport>);
} 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<OrgData>) => {
const upsertOrg = useCallback(async (data: Partial<OrgData>) => {
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,28 +489,8 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}
};
const value = {
org,
orgId,
employees,
submissions,
reports,
loading,
upsertOrg,
saveReport,
inviteEmployee,
getReportVersions,
saveReportVersion,
acceptInvite,
saveCompanyReport,
getCompanyReportHistory,
saveFullCompanyReport,
getFullCompanyReportHistory,
generateCompanyReport,
generateCompanyWiki,
seedInitialData,
isOwner,
issueInviteViaApi: async ({ name, email, role, department }) => {
// 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');
@@ -522,16 +508,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.error('issueInviteViaApi error', e);
throw e;
}
},
getInviteStatus: async (code: string) => {
}, [user?.uid]);
const getInviteStatus = useCallback(async (code: string) => {
try {
return await secureApi.getInvitationStatus(code);
} catch (e) {
console.error('getInviteStatus error', e);
return null;
}
},
consumeInvite: async (code: string) => {
}, []);
const consumeInvite = useCallback(async (code: string) => {
try {
if (!user?.uid) {
throw new Error('User authentication required');
@@ -549,8 +537,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.error('consumeInvite error', e);
return null;
}
},
submitEmployeeAnswers: async (employeeId: string, answers: Record<string, string>) => {
}, [user?.uid, org?.orgId]);
const submitEmployeeAnswers = useCallback(async (employeeId: string, answers: Record<string, string>) => {
try {
// Use secure API for submission
await secureApi.submitEmployeeAnswers(employeeId, answers);
@@ -569,8 +558,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.error('submitEmployeeAnswers error', e);
return false;
}
},
generateEmployeeReport: async (employee: Employee) => {
}, []);
const generateEmployeeReport = useCallback(async (employee: Employee) => {
try {
console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
@@ -630,10 +620,66 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
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,
submissions,
reports,
loading,
upsertOrg,
saveReport,
inviteEmployee,
getReportVersions,
saveReportVersion,
acceptInvite,
saveCompanyReport,
getCompanyReportHistory,
saveFullCompanyReport,
getFullCompanyReportHistory,
generateCompanyReport,
generateCompanyWiki,
seedInitialData,
isOwner,
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 (
<OrgContext.Provider value={value}>

View File

@@ -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<string | null>(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<string> => {
const joinOrganization = useCallback(async (inviteCode: string): Promise<string> => {
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,10 +188,10 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
console.error('Failed to get subscription status:', error);
throw error;
}
};
}, []);
return (
<UserOrganizationsContext.Provider value={{
// Memoize the context value to prevent unnecessary re-renders
const contextValue = useMemo(() => ({
organizations,
selectedOrgId,
loading,
@@ -243,7 +201,20 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
refreshOrganizations,
createCheckoutSession,
getSubscriptionStatus
}}>
}), [
organizations,
selectedOrgId,
loading,
selectOrganization,
createOrganization,
joinOrganization,
refreshOrganizations,
createCheckoutSession,
getSubscriptionStatus
]);
return (
<UserOrganizationsContext.Provider value={contextValue}>
{children}
</UserOrganizationsContext.Provider>
);

View File

@@ -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[] = [

View File

@@ -403,7 +403,7 @@ const Chat: React.FC = () => {
<div className="w-6 h-6 relative bg-[--Neutrals-NeutralSlate600] rounded-full overflow-hidden">
<div className="left-[6px] top-[6px] absolute">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.13477V3.20004C7 3.48006 7 3.62007 7.0545 3.72703C7.10243 3.82111 7.17892 3.8976 7.273 3.94554C7.37996 4.00004 7.51997 4.00004 7.8 4.00004H9.86527M8 6.5H4M8 8.5H4M5 4.5H4M7 1H4.4C3.55992 1 3.13988 1 2.81901 1.16349C2.53677 1.3073 2.3073 1.53677 2.16349 1.81901C2 2.13988 2 2.55992 2 3.4V8.6C2 9.44008 2 9.86012 2.16349 10.181C2.3073 10.4632 2.53677 10.6927 2.81901 10.8365C3.13988 11 3.55992 11 4.4 11H7.6C8.44008 11 8.86012 11 9.18099 10.8365C9.46323 10.6927 9.6927 10.4632 9.83651 10.181C10 9.86012 10 9.44008 10 8.6V4L7 1Z" stroke="[--Text-White-00, #FDFDFD]" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7 1.13477V3.20004C7 3.48006 7 3.62007 7.0545 3.72703C7.10243 3.82111 7.17892 3.8976 7.273 3.94554C7.37996 4.00004 7.51997 4.00004 7.8 4.00004H9.86527M8 6.5H4M8 8.5H4M5 4.5H4M7 1H4.4C3.55992 1 3.13988 1 2.81901 1.16349C2.53677 1.3073 2.3073 1.53677 2.16349 1.81901C2 2.13988 2 2.55992 2 3.4V8.6C2 9.44008 2 9.86012 2.16349 10.181C2.3073 10.4632 2.53677 10.6927 2.81901 10.8365C3.13988 11 3.55992 11 4.4 11H7.6C8.44008 11 8.86012 11 9.18099 10.8365C9.46323 10.6927 9.6927 10.4632 9.83651 10.181C10 9.86012 10 9.44008 10 8.6V4L7 1Z" stroke="var(--Neutrals-NeutralSlate950)" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
@@ -411,7 +411,7 @@ const Chat: React.FC = () => {
</div>
<div onClick={() => removeFile(index)} className="cursor-pointer">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4L4 12M4 4L12 12" stroke="[--Icon-Gray-400, #A4A7AE]" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12 4L4 12M4 4L12 12" stroke="var(--Neutrals-NeutralSlate400)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
@@ -424,8 +424,8 @@ const Chat: React.FC = () => {
const renderChatInterface = () => {
if (state.messages.length === 0) {
return (
<div className="w-[736px] flex-1 max-w-[736px] pt-48 flex flex-col justify-between items-center">
<div className="self-stretch flex flex-col justify-start items-center gap-6">
<div className="h-full flex flex-col p-6">
<div className="w-fit flex-1 flex flex-shrink-0 flex-col justify-center items-center gap-4 self-center">
<div className="justify-start text-[--Neutrals-NeutralSlate800] text-2xl font-medium font-['Neue_Montreal'] leading-normal">What would you like to understand?</div>
<div className="p-1 bg-[--Neutrals-NeutralSlate100] rounded-xl inline-flex justify-start items-center gap-1">
{categories.map((category) => (
@@ -469,7 +469,7 @@ const Chat: React.FC = () => {
</div>
{/* Enhanced instructions for @ mentions */}
<div className="text-center text-[--Neutrals-NeutralSlate500] mt-8">
<div className="text-center text-[--Neutrals-NeutralSlate500] mt-8 max-w-[600px] mx-auto">
<div className="text-sm mb-2">Ask about your team, company data, or get insights.</div>
<div className="text-sm">Use <span className="bg-[--Neutrals-NeutralSlate100] px-2 py-1 rounded text-[--Neutrals-NeutralSlate800] font-mono">@</span> to mention team members.</div>
@@ -490,16 +490,21 @@ const Chat: React.FC = () => {
</div>
</div>
</div>
<div className="flex-shrink-0 w-full">
<div className="max-w-[800px] mx-auto">
{renderChatInput()}
</div>
</div>
</div>
);
}
return (
<div className="w-[736px] flex-1 max-w-[736px] flex flex-col">
<div className="flex-1 overflow-y-auto py-6">
<div className="h-full flex flex-col p-6">
<div className="flex-1 overflow-y-auto w-full">
<div className="py-6 space-y-4 max-w-[800px] mx-auto">
{state.messages.map((message) => (
<div key={message.id} className={`mb-4 flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] p-4 rounded-2xl ${message.role === 'user'
? 'bg-[--Brand-Orange] text-white'
: 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950] border border-[--Neutrals-NeutralSlate200]'
@@ -540,7 +545,7 @@ const Chat: React.FC = () => {
</div>
))}
{state.isLoading && (
<div className="flex justify-start mb-4">
<div className="flex justify-start">
<div className="bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950] border border-[--Neutrals-NeutralSlate200] p-4 rounded-2xl">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[--Brand-Orange]"></div>
@@ -551,8 +556,13 @@ const Chat: React.FC = () => {
)}
<div ref={messagesEndRef} />
</div>
</div>
<div className="flex-shrink-0 w-full">
<div className="max-w-[800px] mx-auto">
{renderChatInput()}
</div>
</div>
</div>
);
};
@@ -663,13 +673,13 @@ const Chat: React.FC = () => {
};
return (
<div className="w-full h-full inline-flex justify-start items-start overflow-hidden">
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
<Sidebar companyName="Zitlac Media" />
<div className="flex-1 self-stretch py-6 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-2.5">
<div className="flex h-screen bg-[--Neutrals-NeutralSlate0]">
<Sidebar companyName={org?.companyName || "Auditly"} />
<main className="flex-1 overflow-hidden flex flex-col">
<div className="flex-1 overflow-hidden">
{renderChatInterface()}
</div>
</div>
</main>
</div>
);
};

View File

@@ -1,3 +1,7 @@
// 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';

View File

@@ -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<FAQItem[]>([
@@ -64,11 +66,10 @@ const HelpNew: React.FC = () => {
}
return (
<div className="w-[1440px] h-[840px] p-4 bg-[--Neutrals-NeutralSlate200] inline-flex justify-start items-start overflow-hidden">
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
<Sidebar companyName="Zitlac Media" />
<div className="flex-1 self-stretch shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start h-full">
<Sidebar companyName={org.name} />
<div className="flex-1 self-stretch pt-8 pb-6 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-center gap-6">
<div className="w-[680px] justify-start text-Text-Gray-800 text-2xl font-medium font-['Neue_Montreal'] leading-normal">Help & Support</div>
<div className="w-[680px] justify-start text-[--Neutrals-NeutralSlate800] text-2xl font-medium font-['Neue_Montreal'] leading-normal">Help & Support</div>
<div className="w-[680px] flex flex-col justify-start items-start gap-4">
{faqItems.map((item, index) => (
<div
@@ -79,24 +80,24 @@ const HelpNew: React.FC = () => {
onClick={() => toggleFAQ(index)}
className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2 cursor-pointer"
>
<div className="flex-1 justify-start text-Text-Dark-950 text-base font-medium font-['Inter'] leading-normal">
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">
{item.question}
</div>
<div>
{item.isOpen ? (
<svg width="12" height="2" viewBox="0 0 12 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 1L1 1" stroke="var(--Text-Gray-500, #717680)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11 1L1 1" stroke="var(--Neutrals-NeutralSlate500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99996 4.16797V15.8346M4.16663 10.0013H15.8333" stroke="var(--Text-Gray-400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.99996 4.16797V15.8346M4.16663 10.0013H15.8333" stroke="var(--Neutrals-NeutralSlate400)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</div>
<div className="w-5 h-5 opacity-0 border border-zinc-800" />
</div>
{item.isOpen && (
<div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate0] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--$1] flex flex-col justify-start items-start gap-4">
<div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate0] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-start gap-4">
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal">
{item.answer}
</div>
@@ -107,8 +108,8 @@ const HelpNew: React.FC = () => {
</div>
<div className="w-[680px] px-5 py-4 bg-[--Neutrals-NeutralSlate800] rounded-2xl backdrop-blur-blur inline-flex justify-center items-center gap-12 overflow-hidden">
<div className="flex-1 inline-flex flex-col justify-center items-start gap-2">
<div className="self-stretch justify-start text-Text-White-00 text-base font-medium font-['Inter'] leading-normal">Still have questions?</div>
<div className="self-stretch justify-start text-Text-Gray-400 text-sm font-normal font-['Inter'] leading-tight">We are available for 24/7</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate0] text-base font-medium font-['Inter'] leading-normal">Still have questions?</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate400] text-sm font-normal font-['Inter'] leading-tight">We are available for 24/7</div>
</div>
<div
onClick={handleContactUs}
@@ -116,7 +117,7 @@ const HelpNew: React.FC = () => {
>
<div>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.58685 5.90223C6.05085 6.86865 6.68337 7.77441 7.48443 8.57546C8.28548 9.37651 9.19124 10.009 10.1577 10.473C10.2408 10.5129 10.2823 10.5329 10.3349 10.5482C10.5218 10.6027 10.7513 10.5636 10.9096 10.4502C10.9542 10.4183 10.9923 10.3802 11.0685 10.304C11.3016 10.071 11.4181 9.95443 11.5353 9.87824C11.9772 9.59091 12.5469 9.59091 12.9889 9.87824C13.106 9.95443 13.2226 10.071 13.4556 10.304L13.5856 10.4339C13.9398 10.7882 14.117 10.9654 14.2132 11.1556C14.4046 11.534 14.4046 11.9809 14.2132 12.3592C14.117 12.5495 13.9399 12.7266 13.5856 13.0809L13.4805 13.186C13.1274 13.5391 12.9508 13.7156 12.7108 13.8505C12.4445 14.0001 12.0308 14.1077 11.7253 14.1068C11.45 14.1059 11.2619 14.0525 10.8856 13.9457C8.86333 13.3718 6.95509 12.2888 5.36311 10.6968C3.77112 9.10479 2.68814 7.19655 2.11416 5.17429C2.00735 4.79799 1.95395 4.60984 1.95313 4.33455C1.95222 4.02906 2.0598 3.6154 2.20941 3.34907C2.34424 3.10904 2.52078 2.9325 2.87386 2.57942L2.97895 2.47433C3.33325 2.12004 3.5104 1.94289 3.70065 1.84666C4.07903 1.65528 4.52587 1.65528 4.90424 1.84666C5.0945 1.94289 5.27164 2.12004 5.62594 2.47433L5.75585 2.60424C5.98892 2.83732 6.10546 2.95385 6.18165 3.07104C6.46898 3.51296 6.46898 4.08268 6.18165 4.52461C6.10546 4.6418 5.98892 4.75833 5.75585 4.9914C5.67964 5.06761 5.64154 5.10571 5.60965 5.15026C5.4963 5.30854 5.45717 5.53805 5.51165 5.72495C5.52698 5.77754 5.54694 5.81911 5.58685 5.90223Z" stroke="var(--white, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.58685 5.90223C6.05085 6.86865 6.68337 7.77441 7.48443 8.57546C8.28548 9.37651 9.19124 10.009 10.1577 10.473C10.2408 10.5129 10.2823 10.5329 10.3349 10.5482C10.5218 10.6027 10.7513 10.5636 10.9096 10.4502C10.9542 10.4183 10.9923 10.3802 11.0685 10.304C11.3016 10.071 11.4181 9.95443 11.5353 9.87824C11.9772 9.59091 12.5469 9.59091 12.9889 9.87824C13.106 9.95443 13.2226 10.071 13.4556 10.304L13.5856 10.4339C13.9398 10.7882 14.117 10.9654 14.2132 11.1556C14.4046 11.534 14.4046 11.9809 14.2132 12.3592C14.117 12.5495 13.9399 12.7266 13.5856 13.0809L13.4805 13.186C13.1274 13.5391 12.9508 13.7156 12.7108 13.8505C12.4445 14.0001 12.0308 14.1077 11.7253 14.1068C11.45 14.1059 11.2619 14.0525 10.8856 13.9457C8.86333 13.3718 6.95509 12.2888 5.36311 10.6968C3.77112 9.10479 2.68814 7.19655 2.11416 5.17429C2.00735 4.79799 1.95395 4.60984 1.95313 4.33455C1.95222 4.02906 2.0598 3.6154 2.20941 3.34907C2.34424 3.10904 2.52078 2.9325 2.87386 2.57942L2.97895 2.47433C3.33325 2.12004 3.5104 1.94289 3.70065 1.84666C4.07903 1.65528 4.52587 1.65528 4.90424 1.84666C5.0945 1.94289 5.27164 2.12004 5.62594 2.47433L5.75585 2.60424C5.98892 2.83732 6.10546 2.95385 6.18165 3.07104C6.46898 3.51296 6.46898 4.08268 6.18165 4.52461C6.10546 4.6418 5.98892 4.75833 5.75585 4.9914C5.67964 5.06761 5.64154 5.10571 5.60965 5.15026C5.4963 5.30854 5.45717 5.53805 5.51165 5.72495C5.52698 5.77754 5.54694 5.81911 5.58685 5.90223Z" stroke="var(--white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="px-1 flex justify-center items-center">
@@ -126,7 +127,6 @@ const HelpNew: React.FC = () => {
</div>
</div>
</div>
</div>
);
};

View File

@@ -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<OnboardingData>(initializeOnboardingData());
const [formData, setFormData] = useState<OnboardingFormData>(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}

View File

@@ -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<CompanyReport | null>(null);
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string } | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set());
const [generatingEmployeeReport, setGeneratingEmployeeReport] = useState<string | null>(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) => (
{visibleEmployees.map((employee) => {
const hasSubmission = submissions[employee.id];
const hasReport = reports[employee.id];
const isGenerating = generatingEmployeeReport === employee.id;
return (
<div
key={employee.id}
className={`self-stretch p-2 rounded-full shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer ${selectedReport?.type === 'employee' && selectedReport?.employeeName === employee.name ? 'bg-[--Neutrals-NeutralSlate100]' : ''
}`}
onClick={() => handleEmployeeSelect(employee)}
>
<div className="w-7 h-7 p-1 bg-[--Neutrals-NeutralSlate100] rounded-[666.67px] flex justify-center items-center">
<div className="w-7 h-7 p-1 bg-[--Neutrals-NeutralSlate100] rounded-[666.67px] flex justify-center items-center relative">
<div className="text-center justify-start text-[--Neutrals-NeutralSlate500] text-xs font-medium font-['Inter'] leading-none">
{employee.initials}
</div>
{/* Status indicator */}
{isGenerating ? (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-yellow-400 rounded-full animate-pulse" title="Generating report..." />
) : hasReport ? (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full" title="Report available" />
) : hasSubmission ? (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-400 rounded-full" title="Submission available, click to generate report" />
) : (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-gray-300 rounded-full" title="No submission yet" />
)}
</div>
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">
{employee.name}
</div>
{isGenerating && (
<div className="w-4 h-4 animate-spin rounded-full border-2 border-[--Neutrals-NeutralSlate300] border-t-[--Brand-Orange]" />
)}
</div>
))}
);
})}
</div>
</div>
</div>
@@ -170,10 +263,23 @@ const Reports: React.FC = () => {
isGenerating={generatingCompanyReport}
/>
) : (
(() => {
const employeeReport = selectedReport.report as EmployeeReport;
const employeeId = employeeReport.employeeId;
return (
<EmployeeReportContent
report={selectedReport.report as EmployeeReport}
report={employeeReport}
employeeName={selectedReport.employeeName!}
onGenerateReport={() => {
const employee = employees.find(emp => emp.name === selectedReport.employeeName);
if (employee) handleGenerateEmployeeReport(employee);
}}
isGenerating={generatingEmployeeReport === employeeId}
hasSubmission={!!submissions[employeeId]}
showGenerateButton={!reports[employeeId] && !!submissions[employeeId]}
/>
);
})()
)
) : (
<div className="flex-1 flex items-center justify-center">
@@ -214,6 +320,28 @@ const CompanyReportContent: React.FC<{
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-base font-medium font-['Inter'] leading-normal">
Company Report
</div>
<div className="flex justify-start items-center gap-3">
<button
onClick={onRegenerate}
disabled={isGenerating}
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-[--Neutrals-NeutralSlate200] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="relative">
{isGenerating ? (
<div className="w-4 h-4 animate-spin rounded-full border-2 border-[--Neutrals-NeutralSlate300] border-t-[--Brand-Orange]" />
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33337 8.00001C1.33337 8.00001 3.00004 4.66668 8.00004 4.66668C13 4.66668 14.6667 8.00001 14.6667 8.00001C14.6667 8.00001 13 11.3333 8.00004 11.3333C3.00004 11.3333 1.33337 8.00001 1.33337 8.00001Z" stroke="var(--Neutrals-NeutralSlate800)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8 9.33334C8.73638 9.33334 9.33333 8.73639 9.33333 8.00001C9.33333 7.26363 8.73638 6.66668 8 6.66668C7.26362 6.66668 6.66667 7.26363 6.66667 8.00001C6.66667 8.73639 7.26362 9.33334 8 9.33334Z" stroke="var(--Neutrals-NeutralSlate800)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</div>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-[--Neutrals-NeutralSlate800] text-sm font-medium font-['Inter'] leading-tight">
{isGenerating ? 'Generating...' : 'Refresh Report'}
</div>
</div>
</button>
<div className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden">
<div className="relative">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -225,6 +353,7 @@ const CompanyReportContent: React.FC<{
</div>
</div>
</div>
</div>
{/* Content */}
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
@@ -484,7 +613,7 @@ const CompanyReportContent: React.FC<{
<div className="self-stretch flex flex-col justify-start items-start gap-3">
<div className="p-1 bg-[--Neutrals-NeutralSlate200] rounded-full outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate50] inline-flex justify-start items-center gap-1">
{/* Department Tabs */}
{report?.organizationalImpactSummary.map((dept, index) => (
{report.organizationalImpactSummary && report.organizationalImpactSummary.map((dept, index) => (
<div
key={dept.category}
className={`px-3 py-1.5 rounded-full flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${activeImpactSummary === dept.category
@@ -514,8 +643,8 @@ const CompanyReportContent: React.FC<{
))}
</div>
{/* 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<{
<div className="self-stretch flex flex-col justify-start items-start gap-3">
<div className="p-1 bg-[--Neutrals-NeutralSlate200] rounded-full outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate50] inline-flex justify-start items-center gap-1">
{/* Department Tabs */}
{report?.gradingBreakdown?.map(dept => (
{report.gradingBreakdown && report?.gradingBreakdown?.map(dept => (
<div
key={dept.departmentNameShort}
className={`px-3 py-1.5 rounded-full flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${activeDepartmentTab === dept.departmentNameShort
@@ -569,7 +698,7 @@ const CompanyReportContent: React.FC<{
))}
</div>
{/* Content for the currently selected department */}
{(() => {
{report.gradingBreakdown && (() => {
const currentDepartment = report?.gradingBreakdown?.find(dept => dept.departmentNameShort === activeDepartmentTab);
if (!currentDepartment) return null;
@@ -652,14 +781,45 @@ 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 */}
<div className="self-stretch px-5 py-3 inline-flex justify-start items-center gap-2.5">
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-base font-medium font-['Inter'] leading-normal">
{employeeName}'s Answers
{employeeName}'s Report
</div>
<div className="flex justify-start items-center gap-3">
{/* Generate Report Button - only show when needed */}
{showGenerateButton && hasSubmission && onGenerateReport && (
<button
onClick={onGenerateReport}
disabled={isGenerating}
className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="relative">
{isGenerating ? (
<div className="w-4 h-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 3V13M3 8H13" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</div>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">
{isGenerating ? 'Generating...' : 'Generate Report'}
</div>
</div>
</button>
)}
{/* Download PDF Button - only show for actual reports */}
{!showGenerateButton && (
<div className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden">
<div className="relative">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -670,6 +830,8 @@ const EmployeeReportContent: React.FC<{
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Download as PDF</div>
</div>
</div>
)}
</div>
</div>
{/* Content */}
@@ -687,7 +849,7 @@ const EmployeeReportContent: React.FC<{
</div>
{/* Self-Rated Output */}
{report.roleAndOutput?.selfRatedOutput && (
{report.roleAndOutput && report.roleAndOutput?.selfRatedOutput && (
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Self-Rated Output</div>
@@ -748,7 +910,7 @@ const EmployeeReportContent: React.FC<{
<div className="w-6 h-6 left-0 top-0 absolute bg-Other-Green rounded-full" />
<div className="left-[5px] top-[5px] absolute">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6666 3.5L5.24998 9.91667L2.33331 7" stroke="var(--Neutrals-NeutralSlate0, #FDFDFD)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.6666 3.5L5.24998 9.91667L2.33331 7" stroke="var(--Other-Green)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>

View File

@@ -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<string, EmployeeSubmission> = {};
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 = () => {
}
};
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,18 +142,52 @@ const Submissions: React.FC = () => {
const submission = submissions[selectedEmployee.id];
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
// Map EMPLOYEE_QUESTIONS to actual answers
// Handle different submission formats
let submissionAnswers: Record<string, string> = {};
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<string, string>);
} else {
// If answers is already a key-value object
submissionAnswers = submission.answers as Record<string, string>;
}
}
// 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 = submission.answers[q.id];
const answer = submissionAnswers[q.id];
if (answer && answer.trim()) {
questionsAndAnswers.push({
question: q.prompt,
answer: answer,
isLong: answer.length > 150 // Mark long answers for different styling
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) => (
<div
key={index}
className={`self-stretch p-3 rounded-2xl shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-2 overflow-hidden ${qa.isLong ? 'bg-[--Neutrals-NeutralSlate100]' : 'bg-[--Neutrals-NeutralSlate100]'
className={`self-stretch p-3 rounded-2xl shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-2 ${qa.isLong ? 'bg-[--Neutrals-NeutralSlate100]' : 'bg-[--Neutrals-NeutralSlate100]'
}`}
>
<div className="self-stretch px-3 py-px inline-flex justify-start items-center gap-2.5">

View File

@@ -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<StoredImage> => {
@@ -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<StoredImage | null> => {
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<boolean> => {
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<StoredImage> => {
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<StoredImage | null> => {
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<boolean> => {
return deleteImage('company-logos', orgId, orgId, userId);
return deleteImage('company-logos', orgId);
};
/**

View File

@@ -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<Record<string, Submission>> {
const response = await this.makeRequest<{ submissions: Record<string, Submission> }>(
async getSubmissions(): Promise<Submission[]> {
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<CompanyReport> {
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);

View File

@@ -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;
}