fix most of the listed bugs
This commit is contained in:
161
employee_report_schema.json
Normal file
161
employee_report_schema.json
Normal 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 Gentry’s strategic skills."
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"risks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"Without strict structure, Gentry’s performance will stay flat or become a bottleneck.",
|
||||
"If kept in a dual-role (recruiting + outreach), productivity will suffer.",
|
||||
"He needs system constraints and direct oversight to stay focused."
|
||||
]
|
||||
]
|
||||
},
|
||||
"recommendations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.",
|
||||
"Pair him with a new hire who is ultra-organized and can execute on Gentry’s deals."
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,99 +80,394 @@ const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SEC
|
||||
apiVersion: '2024-11-20.acacia',
|
||||
}) : null;
|
||||
|
||||
const RESPONSE_FORMAT = {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "company_artifacts",
|
||||
strict: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
companyPerformance: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
summary: { type: "string" },
|
||||
metrics: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
value: { anyOf: [{ type: "string" }, { type: "number" }] },
|
||||
trend: { enum: ["up", "down", "flat"] }
|
||||
},
|
||||
required: ["name", "value", "trend"]
|
||||
}
|
||||
}
|
||||
const RESPONSE_FORMAT_EMPLOYEE = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"employeeId": {
|
||||
"type": "string"
|
||||
},
|
||||
"department": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"roleAndOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"responsibilities": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Recruiting influencers, onboarding, campaign support, business development."
|
||||
]
|
||||
},
|
||||
"clarityOnRole": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"10/10 – Feels very clear on responsibilities."
|
||||
]
|
||||
},
|
||||
"selfRatedOutput": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"7/10 – Indicates decent performance but room to grow."
|
||||
]
|
||||
},
|
||||
"recurringTasks": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Influencer outreach, onboarding, communications."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"insights": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"personalityInsights": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Loyal, well-liked by influencers, eager to grow, client-facing interest."
|
||||
]
|
||||
},
|
||||
"psychologicalIndicators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
required: ["summary", "metrics"]
|
||||
"examples": [
|
||||
[
|
||||
"Scores high on optimism and external motivation.",
|
||||
"Shows ambition but lacks self-discipline in execution.",
|
||||
"Displays a desire for recognition and community; seeks more appreciation."
|
||||
]
|
||||
]
|
||||
},
|
||||
immediateHiringNeeds: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
role: { type: "string" },
|
||||
urgency: { enum: ["low", "medium", "high"] },
|
||||
reason: { type: "string" }
|
||||
},
|
||||
required: ["role", "urgency", "reason"]
|
||||
}
|
||||
"selfAwareness": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"High – acknowledges weaknesses like lateness and disorganization."
|
||||
]
|
||||
},
|
||||
forwardOperatingPlan: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
nextQuarterObjectives: { type: "array", items: { type: "string" } },
|
||||
initiatives: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
owner: { type: "string" },
|
||||
kpis: { type: "array", items: { type: "string" } }
|
||||
},
|
||||
required: ["name", "owner", "kpis"]
|
||||
}
|
||||
},
|
||||
risks: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
risk: { type: "string" },
|
||||
mitigation: { type: "string" }
|
||||
},
|
||||
required: ["risk", "mitigation"]
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["nextQuarterObjectives", "initiatives", "risks"]
|
||||
"emotionalResponses": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Frustrated by campaign disorganization; would prefer closer collaboration."
|
||||
]
|
||||
},
|
||||
organizationalInsights: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
culture: { type: "string" },
|
||||
teamDynamics: { type: "string" },
|
||||
blockers: { type: "array", items: { type: "string" } }
|
||||
},
|
||||
required: ["culture", "teamDynamics", "blockers"]
|
||||
},
|
||||
strengths: { type: "array", items: { type: "string" } },
|
||||
"growthDesire": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Interested in becoming more client-facing and shifting toward biz dev."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"strengths": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
required: ["companyPerformance", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths"]
|
||||
|
||||
"examples": [
|
||||
[
|
||||
"Builds strong relationships with influencers.",
|
||||
"Has sales and outreach potential.",
|
||||
"Loyal, driven, and values-aligned with the company mission.",
|
||||
"Open to feedback and self-improvement."
|
||||
]
|
||||
]
|
||||
},
|
||||
"weaknessess": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
"Critical Issue: Disorganized and late with deliverables — confirmed by previous internal notes.",
|
||||
"Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.",
|
||||
"May unintentionally cause friction with campaigns team by stepping outside process boundaries."
|
||||
]
|
||||
},
|
||||
"opportunities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
{
|
||||
"title": "Role Adjustment",
|
||||
"description": "Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities."
|
||||
},
|
||||
{
|
||||
"title": "Accountability Support",
|
||||
"description": "Pair with a high-output implementer (new hire) to balance Gentry’s strategic skills."
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"risks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"Without strict structure, Gentry’s performance will stay flat or become a bottleneck.",
|
||||
"If kept in a dual-role (recruiting + outreach), productivity will suffer.",
|
||||
"He needs system constraints and direct oversight to stay focused."
|
||||
]
|
||||
]
|
||||
},
|
||||
"recommendations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.",
|
||||
"Pair him with a new hire who is ultra-organized and can execute on Gentry’s deals."
|
||||
]
|
||||
]
|
||||
},
|
||||
"gradingOverview": {
|
||||
"grade": { "type": "string" },
|
||||
"reliability": { "type": "number" },
|
||||
"roleFit": { "type": "number" },
|
||||
"scalability": { "type": "number" },
|
||||
"output": { "type": "number" },
|
||||
"initiative": { "type": "number" }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
RESPONSE_FORMAT_COMPANY = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "CompanyReport",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"createdAt": { "type": "number" },
|
||||
"overview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalEmployees": { "type": "number" },
|
||||
"departmentBreakdown": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"department": { "type": "string" },
|
||||
"count": { "type": "number" }
|
||||
},
|
||||
"required": ["department", "count"]
|
||||
}
|
||||
},
|
||||
"submissionRate": { "type": "number" },
|
||||
"lastUpdated": { "type": "number" },
|
||||
"averagePerformanceScore": { "type": "number" },
|
||||
"riskLevel": {
|
||||
"type": "string",
|
||||
"enum": ["Low", "Medium", "High"]
|
||||
}
|
||||
},
|
||||
"required": ["totalEmployees", "departmentBreakdown", "submissionRate", "lastUpdated"]
|
||||
},
|
||||
"weaknesses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" }
|
||||
},
|
||||
"required": ["title", "description"]
|
||||
}
|
||||
},
|
||||
"personnelChanges": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"newHires": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"department": { "type": "string" },
|
||||
"role": { "type": "string" },
|
||||
"impact": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "department", "role"]
|
||||
}
|
||||
},
|
||||
"promotions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"fromRole": { "type": "string" },
|
||||
"toRole": { "type": "string" },
|
||||
"impact": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "fromRole", "toRole"]
|
||||
}
|
||||
},
|
||||
"departures": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"department": { "type": "string" },
|
||||
"reason": { "type": "string" },
|
||||
"impact": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "department", "reason"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["newHires", "promotions", "departures"]
|
||||
},
|
||||
"immediateHiringNeeds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"department": { "type": "string" },
|
||||
"role": { "type": "string" },
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["High", "Medium", "Low"]
|
||||
},
|
||||
"reasoning": { "type": "string" },
|
||||
"urgency": {
|
||||
"type": "string",
|
||||
"enum": ["high", "medium", "low"]
|
||||
}
|
||||
},
|
||||
"required": ["department", "role", "priority", "reasoning"]
|
||||
}
|
||||
},
|
||||
"forwardOperatingPlan": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["title", "details"]
|
||||
}
|
||||
},
|
||||
"strengths": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"organizationalImpactSummary": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Mission Critical",
|
||||
"Highly Valuable",
|
||||
"Core Support",
|
||||
"Low Criticality"
|
||||
]
|
||||
},
|
||||
"employees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"employeeName": { "type": "string" },
|
||||
"impact": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"suggestedPay": { "type": "string", "description": "Suggested yearly wage for the employee", "example": "$70,000" }
|
||||
},
|
||||
"required": ["employeeName", "impact", "description", "suggestedPay"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["category", "employees"]
|
||||
}
|
||||
},
|
||||
"gradingBreakdown": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"departmentNameShort": { "type": "string" },
|
||||
"departmentName": { "type": "string" },
|
||||
"lead": { "type": "string" },
|
||||
"support": { "type": "string" },
|
||||
"departmentGrade": { "type": "string" },
|
||||
"executiveSummary": { "type": "string" },
|
||||
"teamScores": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"employeeName": { "type": "string" },
|
||||
"grade": { "type": "string" },
|
||||
"reliability": { "type": "number" },
|
||||
"roleFit": { "type": "number" },
|
||||
"scalability": { "type": "number" },
|
||||
"output": { "type": "number" },
|
||||
"initiative": { "type": "number" }
|
||||
},
|
||||
"required": [
|
||||
"employeeName",
|
||||
"grade",
|
||||
"reliability",
|
||||
"roleFit",
|
||||
"scalability",
|
||||
"output",
|
||||
"initiative"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"departmentNameShort",
|
||||
"departmentName",
|
||||
"lead",
|
||||
"support",
|
||||
"departmentGrade",
|
||||
"executiveSummary",
|
||||
"teamScores"
|
||||
]
|
||||
}
|
||||
},
|
||||
"executiveSummary": { "type": "string" }
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"overview",
|
||||
"weaknesses",
|
||||
"personnelChanges",
|
||||
"immediateHiringNeeds",
|
||||
"strengths",
|
||||
"gradingBreakdown",
|
||||
"executiveSummary"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
// Helper function to generate OTP
|
||||
@@ -714,22 +1009,22 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
||||
const orgData = orgDoc.exists ? orgDoc.data() : {};
|
||||
|
||||
// Prepare company context (onboarding data)
|
||||
const companyContext = {
|
||||
let companyContext = {
|
||||
name: orgData.name,
|
||||
industry: orgData.industry,
|
||||
mission: orgData.mission,
|
||||
values: orgData.values,
|
||||
culture: orgData.cultureDescription,
|
||||
size: orgData.size,
|
||||
onboardingData: orgData // Include all org data for comprehensive context
|
||||
};
|
||||
|
||||
if (orgData.onboardingData) {
|
||||
companyContext = {
|
||||
...companyContext,
|
||||
...orgData.onboardingData
|
||||
};
|
||||
}
|
||||
// Prepare submission data
|
||||
const submissionData = {
|
||||
employeeId: finalEmployeeId,
|
||||
answers,
|
||||
submittedAt: Date.now(),
|
||||
status: "completed"
|
||||
status: "completed",
|
||||
companyContext,
|
||||
};
|
||||
|
||||
// Generate the report using the existing function logic
|
||||
@@ -737,7 +1032,7 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
||||
if (openai) {
|
||||
// Use OpenAI to generate the report with company context
|
||||
const prompt = `
|
||||
You are an expert HR analyst. Generate a comprehensive employee performance report based on the following data:
|
||||
You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema:
|
||||
|
||||
Employee Information:
|
||||
- Name: ${employeeData?.name || employeeData?.email || 'Unknown'}
|
||||
@@ -762,17 +1057,7 @@ Generate a detailed report that:
|
||||
8. Provides numerical grading across key performance areas
|
||||
|
||||
Return ONLY valid JSON that matches this structure:
|
||||
{
|
||||
"roleAndOutput": { "currentRole": string, "keyResponsibilities": string[], "performanceRating": number },
|
||||
"behavioralInsights": { "workStyle": string, "communicationSkills": string, "teamDynamics": string },
|
||||
"strengths": string[],
|
||||
"weaknesses": string[],
|
||||
"opportunities": string[],
|
||||
"risks": string[],
|
||||
"recommendations": string[],
|
||||
"companyAlignment": { "valuesAlignment": number, "cultureAlignment": number, "missionAlignment": number },
|
||||
"grading": { "overall": number, "technical": number, "communication": number, "teamwork": number, "leadership": number }
|
||||
}
|
||||
${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)}
|
||||
|
||||
Be thorough, professional, and focus on actionable insights.
|
||||
`.trim();
|
||||
@@ -796,8 +1081,13 @@ Be thorough, professional, and focus on actionable insights.
|
||||
const aiResponse = completion.choices[0].message.content;
|
||||
const parsedReport = JSON.parse(aiResponse);
|
||||
|
||||
console.log(parsedReport);
|
||||
|
||||
report = {
|
||||
employeeId: finalEmployeeId,
|
||||
employeeName: employeeData?.name || employeeData?.email || 'Employee',
|
||||
role: employeeData?.role || "Team Member",
|
||||
email: employeeData?.email || 'Unknown',
|
||||
generatedAt: Date.now(),
|
||||
summary: `AI-generated performance analysis for ${employeeData?.name || employeeData?.email || 'Employee'}`,
|
||||
submissionId: finalEmployeeId,
|
||||
@@ -910,12 +1200,13 @@ exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => {
|
||||
if (openai) {
|
||||
// Use OpenAI to generate the report
|
||||
const prompt = `
|
||||
You are an expert HR analyst. Generate a comprehensive employee performance report based on the following data:
|
||||
You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema:
|
||||
|
||||
Employee Information:
|
||||
- Name: ${employee.name || employee.email}
|
||||
- Role: ${employee.role || "Team Member"}
|
||||
- Department: ${employee.department || "General"}
|
||||
- Name: ${employee?.name || employee?.email || 'Unknown'}
|
||||
- Role: ${employee?.role || "Team Member"}
|
||||
- Department: ${employee?.department || "General"}
|
||||
- Email: ${employee?.email || 'Unknown'}
|
||||
|
||||
Employee Submission Data:
|
||||
${JSON.stringify(submission, null, 2)}
|
||||
@@ -923,18 +1214,21 @@ ${JSON.stringify(submission, null, 2)}
|
||||
Company Context:
|
||||
${companyWiki ? JSON.stringify(companyWiki, null, 2) : "No company context provided"}
|
||||
|
||||
Generate a detailed report with the following structure:
|
||||
- roleAndOutput: Current role assessment and performance rating
|
||||
- behavioralInsights: Work style, communication, and team dynamics
|
||||
- strengths: List of employee strengths
|
||||
- weaknesses: Areas for improvement (mark critical issues)
|
||||
- opportunities: Growth and development opportunities
|
||||
- risks: Potential risks or concerns
|
||||
- recommendations: Specific action items
|
||||
- grading: Numerical scores for different performance areas
|
||||
Generate a detailed report that:
|
||||
1. Evaluates how well the employee aligns with company values and culture
|
||||
2. Assesses their role performance and output
|
||||
3. Identifies behavioral insights and work patterns
|
||||
4. Highlights strengths and areas for improvement
|
||||
5. Provides specific recommendations for growth
|
||||
6. Suggests opportunities that align with company goals
|
||||
7. Identifies any risks or concerns
|
||||
8. Provides numerical grading across key performance areas
|
||||
|
||||
Return ONLY valid JSON that matches this structure. Be thorough but professional.
|
||||
`.trim();
|
||||
Return ONLY valid JSON that matches this structure:
|
||||
${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)}
|
||||
|
||||
Be thorough, professional, and focus on actionable insights.
|
||||
`.trim();
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
@@ -1031,63 +1325,82 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const authContext = await validateAuthAndGetContext(req);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
const { org, submissions = [] } = req.body;
|
||||
|
||||
if (!org) {
|
||||
return res.status(400).json({ error: "Organization data is required" });
|
||||
}
|
||||
|
||||
const orgData = {
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
contextualData: org.onboardingData,
|
||||
metrics: org.metrics
|
||||
}
|
||||
|
||||
try {
|
||||
let report, wiki;
|
||||
|
||||
if (openai) {
|
||||
// Use OpenAI to generate the company report and wiki
|
||||
db.collection("orgs").doc(orgId)
|
||||
const system = "You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema.";
|
||||
const user = [
|
||||
"Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.",
|
||||
"Be thorough and professional.",
|
||||
"",
|
||||
"Organization Information:",
|
||||
JSON.stringify(org, null, 2),
|
||||
"",
|
||||
"Employee Submissions:",
|
||||
JSON.stringify(submissions, null, 2)
|
||||
].join("\n");
|
||||
// Use OpenAI to generate the company report
|
||||
|
||||
const user = `You are a cut-and-dry expert business analyst who shys to no truths and with get a business in tip-top shape within swiftness. Return ONLY JSON that conforms to the provided schema:
|
||||
|
||||
Employee Submissions:
|
||||
${JSON.stringify(submissions, null, 2)}
|
||||
|
||||
Company Context:
|
||||
${JSON.stringify(orgData, null, 2)}
|
||||
|
||||
Generate a detailed report that:
|
||||
1. Evaluates the company based on all the key sections in the JSON schema, being thorough to touch on all categories and employees
|
||||
2. Attempts to at your best effort further the companies success and growth potential
|
||||
3. Provides clear, concise, and actionable recommendations for improvement
|
||||
4. Doesn't cater to sugarcoating or vague generalities
|
||||
5. Will beat the nail into the coffin of inefficiency with precise solutions, getting rid of all weak points.
|
||||
|
||||
Return ONLY valid JSON that matches this JSON SCHEMA:
|
||||
${JSON.stringify(RESPONSE_FORMAT_COMPANY, null, 0)}
|
||||
|
||||
Be thorough, professional, and focus on actionable insights.
|
||||
`;
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
temperature: 0, // consistency
|
||||
response_format: RESPONSE_FORMAT,
|
||||
response_format: { type: "json_object" },
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: user }
|
||||
]
|
||||
});
|
||||
|
||||
// content is guaranteed to be schema-conformant JSON
|
||||
console.log(completion.choices[0].message);
|
||||
console.log(completion.choices[0].message.content);
|
||||
const parsed = JSON.parse(completion.choices[0].message.content);
|
||||
|
||||
const report = {
|
||||
report = {
|
||||
generatedAt: Date.now(),
|
||||
...parsed
|
||||
};
|
||||
|
||||
const wiki = {
|
||||
companyName: org?.name ?? parsed.wiki.companyName,
|
||||
generatedAt: Date.now(),
|
||||
const reportRef = db
|
||||
.collection("orgs")
|
||||
.doc(orgId)
|
||||
.collection("companyReport")
|
||||
.doc("main");
|
||||
|
||||
};
|
||||
|
||||
const companyReport = db.collection("orgs").doc(orgId).collection("companyReport");
|
||||
await companyReport.set(report);
|
||||
const companyWiki = db.collection("orgs").doc(orgId).collection("companyWiki");
|
||||
await companyWiki.set(wiki);
|
||||
await reportRef.set(report);
|
||||
|
||||
console.log(report);
|
||||
console.log(wiki);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
report
|
||||
});
|
||||
|
||||
} else {
|
||||
// Fallback to mock data when OpenAI is not available
|
||||
@@ -1136,13 +1449,11 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
|
||||
culture: "Collaborative and growth-oriented",
|
||||
generatedAt: Date.now(),
|
||||
};
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
...report
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
...report,
|
||||
...wiki,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Generate company wiki error:", error);
|
||||
res.status(500).json({ error: "Failed to generate company wiki" });
|
||||
@@ -1173,7 +1484,7 @@ exports.chat = onRequest({ cors: true }, async (req, res) => {
|
||||
if (openai) {
|
||||
// Use OpenAI for chat responses
|
||||
const systemPrompt = `
|
||||
You are an expert HR consultant and business analyst with access to employee performance data and company analytics.
|
||||
You are a cut-and-dry expert business analyst.
|
||||
You provide thoughtful, professional advice based on the employee context and company data provided.
|
||||
|
||||
${context ? `
|
||||
@@ -1186,7 +1497,14 @@ Mentioned Employees:
|
||||
${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')}
|
||||
` : ''}
|
||||
|
||||
Provide helpful, actionable insights while maintaining professional confidentiality and focusing on constructive feedback.
|
||||
You will discuss employees with the employer to help:
|
||||
1. Evaluate the company based on all provided data, being thorough to touch on all information gathered from said employee doubled with information known about the company
|
||||
2. Attempt to at your best effort further the companies success and growth potential
|
||||
3. Provide clear, concise, and actionable recommendations for improvement
|
||||
4. Don't cater to sugarcoating or vague generalities
|
||||
5. Beat the nail into the coffin of inefficiency with precise solutions, getting rid of all weak points.
|
||||
|
||||
Provide helpful, actionable insights while maintaining professional tone and focusing on critical must-know knowledge and actionable recommendations.
|
||||
`.trim();
|
||||
|
||||
// Build the user message content
|
||||
@@ -1332,17 +1650,18 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
||||
|
||||
const userData = userDoc.data();
|
||||
|
||||
// Add user as owner to organization's employees collection
|
||||
const employeeRef = orgRef.collection("employees").doc(authContext.userId);
|
||||
await employeeRef.set({
|
||||
// Add owner info to organization document (owners are NOT employees)
|
||||
const ownerInfo = {
|
||||
id: authContext.userId,
|
||||
role: "owner",
|
||||
isOwner: true,
|
||||
joinedAt: Date.now(),
|
||||
status: "active",
|
||||
name: userData.displayName || userData.email.split("@")[0],
|
||||
email: userData.email,
|
||||
department: "Management",
|
||||
joinedAt: Date.now()
|
||||
};
|
||||
|
||||
// Update org document with owner info
|
||||
await orgRef.update({
|
||||
ownerInfo: ownerInfo,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// Add organization to user's organizations (for multi-org support)
|
||||
@@ -2077,12 +2396,16 @@ exports.getEmployees = onRequest({ cors: true }, async (req, res) => {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Get all employees
|
||||
// Get all employees (excluding owners - they should not be in employees collection)
|
||||
const employeesSnapshot = await db.collection("orgs").doc(orgId).collection("employees").get();
|
||||
const employees = [];
|
||||
|
||||
employeesSnapshot.forEach(doc => {
|
||||
employees.push({ id: doc.id, ...doc.data() });
|
||||
const employeeData = doc.data();
|
||||
// Skip any owner records that might still exist (defensive programming)
|
||||
if (employeeData.role !== "owner" && !employeeData.isOwner) {
|
||||
employees.push({ id: doc.id, ...employeeData });
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -2305,13 +2628,15 @@ exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => {
|
||||
}
|
||||
|
||||
// Get all company reports
|
||||
const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("fullCompanyReports").get();
|
||||
const reports = [];
|
||||
const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("companyReport").doc("main").get();
|
||||
|
||||
reportsSnapshot.forEach(doc => {
|
||||
reports.push({ id: doc.id, ...doc.data() });
|
||||
});
|
||||
const reportsData = reportsSnapshot.data();
|
||||
const reports = reportsData ? [reportsData] : [];
|
||||
|
||||
// Convert the reports object to an array
|
||||
// for (const [id, report] of Object.entries(reportsData || {})) {
|
||||
// reports.push({ id, ...report });
|
||||
// }
|
||||
// Sort by creation date (newest first)
|
||||
reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
||||
|
||||
@@ -2487,4 +2812,82 @@ exports.deleteImage = onRequest({ cors: true }, async (req, res) => {
|
||||
console.error("Delete image error:", error);
|
||||
res.status(500).json({ error: "Failed to delete image" });
|
||||
}
|
||||
});
|
||||
|
||||
// Migration Function - Remove Owners from Employees Collection
|
||||
exports.migrateOwnersFromEmployees = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Get organization document
|
||||
const orgRef = db.collection("orgs").doc(orgId);
|
||||
const orgDoc = await orgRef.get();
|
||||
|
||||
if (!orgDoc.exists) {
|
||||
return res.status(404).json({ error: "Organization not found" });
|
||||
}
|
||||
|
||||
const orgData = orgDoc.data();
|
||||
let migratedCount = 0;
|
||||
let ownerInfo = null;
|
||||
|
||||
// Find and remove any owners from employees collection
|
||||
const employeesSnapshot = await orgRef.collection("employees").where("role", "==", "owner").get();
|
||||
|
||||
if (!employeesSnapshot.empty) {
|
||||
// Get owner info before deletion
|
||||
const ownerEmployee = employeesSnapshot.docs[0].data();
|
||||
ownerInfo = {
|
||||
id: ownerEmployee.id,
|
||||
name: ownerEmployee.name,
|
||||
email: ownerEmployee.email,
|
||||
joinedAt: ownerEmployee.joinedAt || Date.now()
|
||||
};
|
||||
|
||||
// Delete all owner records from employees collection
|
||||
const batch = db.batch();
|
||||
employeesSnapshot.docs.forEach(doc => {
|
||||
batch.delete(doc.ref);
|
||||
migratedCount++;
|
||||
});
|
||||
await batch.commit();
|
||||
}
|
||||
|
||||
// Update org document with owner info if we found it
|
||||
if (ownerInfo) {
|
||||
await orgRef.update({
|
||||
ownerInfo: ownerInfo,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Migration completed. Removed ${migratedCount} owner record(s) from employees collection.`,
|
||||
migratedCount,
|
||||
ownerInfo
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Migration error:", error);
|
||||
if (error.message.includes('Missing or invalid authorization') ||
|
||||
error.message.includes('Token')) {
|
||||
return res.status(401).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: "Failed to complete migration" });
|
||||
}
|
||||
});
|
||||
75
index.css
75
index.css
@@ -16,67 +16,82 @@
|
||||
|
||||
@keyframes blinkLightGreen {
|
||||
|
||||
0%,
|
||||
33% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #a5ffc075;
|
||||
|
||||
0% {
|
||||
box-shadow: inset 1px 0px 3px -1px rgba(255, 255, 255, 0.2),
|
||||
inset -1px 0px 3px 1px rgba(0, 0, 0, 0.15),
|
||||
0px 0px 2px 1px #32ff6f67;
|
||||
}
|
||||
|
||||
33%,
|
||||
66% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15);
|
||||
50% {
|
||||
box-shadow: inset -1px 0px 3px 1px rgba(255, 255, 255, 0.2),
|
||||
inset 1px 0px 3px -1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: inset 1px 0px 3px -1px rgba(255, 255, 255, 0.2),
|
||||
inset -1px 0px 3px 1px rgba(0, 0, 0, 0.15),
|
||||
0px 0px 2px 1px #32ff6f67;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinkLightYellow {
|
||||
|
||||
0%,
|
||||
33% {
|
||||
0% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #f7f3c275;
|
||||
0px 0px 2px 1px #ffef3c6c;
|
||||
}
|
||||
|
||||
33%,
|
||||
66% {
|
||||
50% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 2px 1px #ffef3c6c;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinkLightBlue {
|
||||
|
||||
0%,
|
||||
33% {
|
||||
0% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #a5d8ff75;
|
||||
0px 0px 2px 1px #39a2f362;
|
||||
}
|
||||
|
||||
33%,
|
||||
66% {
|
||||
50% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 2px 1px #39a2f362;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinkLightRed {
|
||||
|
||||
0%,
|
||||
33% {
|
||||
0% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #f63d6875;
|
||||
0px 0px 2px 1px #ff2d5e63;
|
||||
}
|
||||
|
||||
33%,
|
||||
66% {
|
||||
50% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 2px 1px #ff2d5e63;
|
||||
}
|
||||
}
|
||||
|
||||
.blinkLightBlue,
|
||||
@@ -93,15 +108,15 @@
|
||||
animation-name: blinkLightBlue;
|
||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #a5d8ff75;
|
||||
border: solid 1px #54c2e456;
|
||||
0px 0px 2px 1px #39a2f32d;
|
||||
border: solid 1px #39a2f362;
|
||||
}
|
||||
|
||||
.blinkLightGreen {
|
||||
animation-name: blinkLightGreen;
|
||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #a5ffc075;
|
||||
0px 0px 2px 1px #a5ffc075;
|
||||
border: solid 1px rgba(187, 248, 185, 0.31);
|
||||
}
|
||||
|
||||
@@ -109,7 +124,7 @@
|
||||
animation-name: blinkLightRed;
|
||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #f63d6875;
|
||||
0px 0px 2px 1px #f63d6875;
|
||||
border: solid 1px #e4547656;
|
||||
}
|
||||
|
||||
@@ -117,7 +132,7 @@
|
||||
animation-name: blinkLightYellow;
|
||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||
0px 0px 0px 2px #f7f3c275;
|
||||
0px 0px 2px 1px #f7f3c275;
|
||||
border: solid 1px #e4e25456;
|
||||
}
|
||||
|
||||
|
||||
104
src/App.tsx
104
src/App.tsx
@@ -1,28 +1,42 @@
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { HashRouter, Routes, Route, Navigate, useParams } from 'react-router-dom';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { UserOrganizationsProvider, useUserOrganizations } from './contexts/UserOrganizationsContext';
|
||||
import { OrgProvider, useOrg } from './contexts/OrgContext';
|
||||
import { Layout } from './components/UiKit';
|
||||
import CompanyWiki from './pages/CompanyWiki';
|
||||
// import Report from '../deprecated/pages/EmployeeData';
|
||||
import Reports from './pages/Reports';
|
||||
import Submissions from './pages/Submissions';
|
||||
import Chat from './pages/Chat';
|
||||
import HelpNew from './pages/HelpNew';
|
||||
import SettingsNew from './pages/SettingsNew';
|
||||
import HelpAndSettings from './pages/HelpAndSettings';
|
||||
import ModernLogin from './pages/Login';
|
||||
import OrgSelection from './pages/OrgSelection';
|
||||
import Onboarding from './pages/Onboarding';
|
||||
import EmployeeQuestionnaire from './pages/EmployeeQuestionnaire';
|
||||
import EmployeeQuestionnaireNew from './pages/EmployeeQuestionnaireNew';
|
||||
import EmployeeQuestionnaireSteps from './pages/EmployeeQuestionnaireSteps';
|
||||
import QuestionTypesDemo from './pages/QuestionTypesDemo';
|
||||
import FormsDashboard from './pages/FormsDashboard';
|
||||
import QuestionnaireComplete from './pages/QuestionnaireComplete';
|
||||
import SubscriptionSetup from './pages/SubscriptionSetup';
|
||||
|
||||
// Lazy load all page components for better performance
|
||||
const Layout = React.lazy(() => import('./components/UiKit').then(module => ({ default: module.Layout })));
|
||||
const CompanyWiki = React.lazy(() => import('./pages/CompanyWiki'));
|
||||
const Reports = React.lazy(() => import('./pages/Reports'));
|
||||
const Submissions = React.lazy(() => import('./pages/Submissions'));
|
||||
const Chat = React.lazy(() => import('./pages/Chat'));
|
||||
const HelpNew = React.lazy(() => import('./pages/HelpNew'));
|
||||
const SettingsNew = React.lazy(() => import('./pages/SettingsNew'));
|
||||
const ModernLogin = React.lazy(() => import('./pages/Login'));
|
||||
const OrgSelection = React.lazy(() => import('./pages/OrgSelection'));
|
||||
const Onboarding = React.lazy(() => import('./pages/Onboarding'));
|
||||
const EmployeeQuestionnaire = React.lazy(() => import('./pages/EmployeeQuestionnaire'));
|
||||
const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaireNew'));
|
||||
const EmployeeQuestionnaireSteps = React.lazy(() => import('./pages/EmployeeQuestionnaireSteps'));
|
||||
const QuestionTypesDemo = React.lazy(() => import('./pages/QuestionTypesDemo'));
|
||||
const FormsDashboard = React.lazy(() => import('./pages/FormsDashboard'));
|
||||
const QuestionnaireComplete = React.lazy(() => import('./pages/QuestionnaireComplete'));
|
||||
const SubscriptionSetup = React.lazy(() => import('./pages/SubscriptionSetup'));
|
||||
|
||||
// Loading component for Suspense fallback
|
||||
const LoadingSpinner: React.FC = () => (
|
||||
<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>
|
||||
|
||||
113
src/components/OwnerMigrationUtility.tsx
Normal file
113
src/components/OwnerMigrationUtility.tsx
Normal 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;
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,67 +161,151 @@ 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">
|
||||
<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>
|
||||
{/* 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="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_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_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_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_1141_1906" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1141_1906" result="shape" />
|
||||
</filter>
|
||||
<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_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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="left-[8.80px] top-[7.20px] 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>
|
||||
<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" />
|
||||
<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" />
|
||||
<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" />
|
||||
</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>
|
||||
<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>
|
||||
</defs>
|
||||
</svg>
|
||||
</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">{org?.name || 'Select Organization'}</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 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 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>
|
||||
</div>
|
||||
<div>
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Items */}
|
||||
@@ -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">
|
||||
|
||||
@@ -1,206 +1,209 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
|
||||
import { API_URL } from '../constants';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
signInWithGoogle: () => Promise<void>;
|
||||
signOutUser: () => Promise<void>;
|
||||
signInWithEmail: (email: string, password: string) => Promise<void>;
|
||||
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<void>;
|
||||
sendOTP: (email: string, inviteCode?: string) => Promise<any>;
|
||||
verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
||||
|
||||
if (isFirebaseConfigured) {
|
||||
// Firebase mode: Set up proper Firebase auth state listener
|
||||
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
|
||||
console.log('Firebase auth state changed:', firebaseUser?.email);
|
||||
if (firebaseUser) {
|
||||
setUser(firebaseUser);
|
||||
} else {
|
||||
// Check for OTP session as fallback
|
||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||
if (sessionUser) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring OTP session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse session user:', error);
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
} else {
|
||||
// Demo/OTP mode: Check localStorage for persisted session
|
||||
console.log('Checking for persisted OTP session');
|
||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||
if (sessionUser) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse session user:', error);
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
return () => { };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// No-op in demo mode
|
||||
return;
|
||||
}
|
||||
await signInWithPopup(auth, googleProvider);
|
||||
};
|
||||
|
||||
const signOutUser = async () => {
|
||||
try {
|
||||
// Sign out from Firebase if configured and user is signed in via Firebase
|
||||
if (isFirebaseConfigured && auth.currentUser) {
|
||||
await signOut(auth);
|
||||
console.log('Firebase signout completed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Firebase signout error:', error);
|
||||
}
|
||||
|
||||
// Always clear all local session data
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
localStorage.removeItem('auditly_auth_token');
|
||||
localStorage.removeItem('auditly_selected_org');
|
||||
sessionStorage.clear();
|
||||
|
||||
setUser(null);
|
||||
console.log('User signed out and all sessions cleared');
|
||||
};
|
||||
|
||||
const signInWithEmail = async (email: string, password: string) => {
|
||||
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
|
||||
try {
|
||||
console.log('Attempting Firebase auth');
|
||||
await signInWithEmailAndPassword(auth, email, password);
|
||||
} catch (e: any) {
|
||||
const code = e?.code || '';
|
||||
console.error('Firebase Auth Error:', code, e?.message);
|
||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
||||
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
|
||||
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: email.split('@')[0] } as unknown as User;
|
||||
setUser(mock);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
|
||||
try {
|
||||
const cred = await createUserWithEmailAndPassword(auth, email, password);
|
||||
if (displayName) {
|
||||
try { await updateProfile(cred.user, { displayName }); } catch { }
|
||||
}
|
||||
} catch (e: any) {
|
||||
const code = e?.code || '';
|
||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
||||
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
|
||||
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: displayName || email.split('@')[0] } as unknown as User;
|
||||
setUser(mock);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const sendOTP = async (email: string, inviteCode?: string) => {
|
||||
const response = await fetch(`${API_URL}/sendOTP`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, inviteCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to send OTP');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const verifyOTP = async (email: string, otp: string, inviteCode?: string) => {
|
||||
const response = await fetch(`${API_URL}/verifyOTP`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, otp, inviteCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to verify OTP');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Set user in auth context
|
||||
const mockUser = {
|
||||
uid: data.user.uid,
|
||||
email: data.user.email,
|
||||
displayName: data.user.displayName,
|
||||
emailVerified: true
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
localStorage.setItem('auditly_auth_token', data.token);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
user,
|
||||
loading,
|
||||
signInWithGoogle,
|
||||
signOutUser,
|
||||
signInWithEmail,
|
||||
signUpWithEmail,
|
||||
sendOTP,
|
||||
verifyOTP,
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
||||
return ctx;
|
||||
};
|
||||
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
|
||||
import { API_URL } from '../constants';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
signInWithGoogle: () => Promise<void>;
|
||||
signOutUser: () => Promise<void>;
|
||||
signInWithEmail: (email: string, password: string) => Promise<void>;
|
||||
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<void>;
|
||||
sendOTP: (email: string, inviteCode?: string) => Promise<any>;
|
||||
verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
||||
|
||||
if (isFirebaseConfigured) {
|
||||
// Firebase mode: Set up proper Firebase auth state listener
|
||||
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
|
||||
console.log('Firebase auth state changed:', firebaseUser?.email);
|
||||
if (firebaseUser) {
|
||||
setUser(firebaseUser);
|
||||
} else {
|
||||
// Check for OTP session as fallback
|
||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||
if (sessionUser) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring OTP session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse session user:', error);
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
} else {
|
||||
// Demo/OTP mode: Check localStorage for persisted session
|
||||
console.log('Checking for persisted OTP session');
|
||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||
if (sessionUser) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
console.log('Restoring session for:', parsedUser.email);
|
||||
setUser(parsedUser as User);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse session user:', error);
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
return () => { };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const signInWithGoogle = useCallback(async () => {
|
||||
if (!isFirebaseConfigured) {
|
||||
// No-op in demo mode
|
||||
return;
|
||||
}
|
||||
await signInWithPopup(auth, googleProvider);
|
||||
}, []);
|
||||
|
||||
const signOutUser = useCallback(async () => {
|
||||
try {
|
||||
// Sign out from Firebase if configured and user is signed in via Firebase
|
||||
if (isFirebaseConfigured && auth.currentUser) {
|
||||
await signOut(auth);
|
||||
console.log('Firebase signout completed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Firebase signout error:', error);
|
||||
}
|
||||
|
||||
// Always clear all local session data
|
||||
localStorage.removeItem('auditly_demo_session');
|
||||
localStorage.removeItem('auditly_auth_token');
|
||||
localStorage.removeItem('auditly_selected_org');
|
||||
sessionStorage.clear();
|
||||
|
||||
setUser(null);
|
||||
console.log('User signed out and all sessions cleared');
|
||||
}, []);
|
||||
|
||||
const signInWithEmail = useCallback(async (email: string, password: string) => {
|
||||
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
|
||||
try {
|
||||
console.log('Attempting Firebase auth');
|
||||
await signInWithEmailAndPassword(auth, email, password);
|
||||
} catch (e: any) {
|
||||
const code = e?.code || '';
|
||||
console.error('Firebase Auth Error:', code, e?.message);
|
||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
||||
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
|
||||
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: email.split('@')[0] } as unknown as User;
|
||||
setUser(mock);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const signUpWithEmail = useCallback(async (email: string, password: string, displayName?: string) => {
|
||||
try {
|
||||
const cred = await createUserWithEmailAndPassword(auth, email, password);
|
||||
if (displayName) {
|
||||
try { await updateProfile(cred.user, { displayName }); } catch { }
|
||||
}
|
||||
} catch (e: any) {
|
||||
const code = e?.code || '';
|
||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
||||
console.warn('Email/Password provider disabled in Firebase. Falling back to local mock user for development.');
|
||||
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: displayName || email.split('@')[0] } as unknown as User;
|
||||
setUser(mock);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sendOTP = useCallback(async (email: string, inviteCode?: string) => {
|
||||
const response = await fetch(`${API_URL}/sendOTP`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, inviteCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to send OTP');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}, []);
|
||||
|
||||
const verifyOTP = useCallback(async (email: string, otp: string, inviteCode?: string) => {
|
||||
const response = await fetch(`${API_URL}/verifyOTP`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, otp, inviteCode })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to verify OTP');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Set user in auth context
|
||||
const mockUser = {
|
||||
uid: data.user.uid,
|
||||
email: data.user.email,
|
||||
displayName: data.user.displayName,
|
||||
emailVerified: true
|
||||
} as unknown as User;
|
||||
|
||||
setUser(mockUser);
|
||||
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||
localStorage.setItem('auditly_auth_token', data.token);
|
||||
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
// Memoize the context value to prevent unnecessary re-renders
|
||||
const contextValue = useMemo(() => ({
|
||||
user,
|
||||
loading,
|
||||
signInWithGoogle,
|
||||
signOutUser,
|
||||
signInWithEmail,
|
||||
signUpWithEmail,
|
||||
sendOTP,
|
||||
verifyOTP,
|
||||
}), [user, loading, signInWithGoogle, signOutUser, signInWithEmail, signUpWithEmail, sendOTP, verifyOTP]);
|
||||
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
||||
return ctx;
|
||||
};
|
||||
|
||||
@@ -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,7 +489,141 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
}
|
||||
};
|
||||
|
||||
const value = {
|
||||
// Memoize functions that don't need dependencies
|
||||
const issueInviteViaApi = useCallback(async ({ name, email, role, department }) => {
|
||||
try {
|
||||
if (!user?.uid) {
|
||||
throw new Error('User authentication required');
|
||||
}
|
||||
|
||||
const data = await secureApi.createInvitation({ name, email, role, department });
|
||||
|
||||
// Optimistically add employee shell (not yet active until consume)
|
||||
setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
|
||||
...data.employee,
|
||||
initials: data.employee.name ? data.employee.name.split(' ').map((n: string) => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase()
|
||||
} as Employee]);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('issueInviteViaApi error', e);
|
||||
throw e;
|
||||
}
|
||||
}, [user?.uid]);
|
||||
|
||||
const getInviteStatus = useCallback(async (code: string) => {
|
||||
try {
|
||||
return await secureApi.getInvitationStatus(code);
|
||||
} catch (e) {
|
||||
console.error('getInviteStatus error', e);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const consumeInvite = useCallback(async (code: string) => {
|
||||
try {
|
||||
if (!user?.uid) {
|
||||
throw new Error('User authentication required');
|
||||
}
|
||||
|
||||
const result = await secureApi.consumeInvitation(code, user.uid);
|
||||
|
||||
// Mark employee as active
|
||||
if (result && (result as any).employee) {
|
||||
setEmployees(prev => prev.find(e => e.id === (result as any).employee.id) ? prev : [...prev, (result as any).employee]);
|
||||
return { ...(result as any), orgId: org?.orgId };
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error('consumeInvite error', e);
|
||||
return null;
|
||||
}
|
||||
}, [user?.uid, org?.orgId]);
|
||||
|
||||
const submitEmployeeAnswers = useCallback(async (employeeId: string, answers: Record<string, string>) => {
|
||||
try {
|
||||
// Use secure API for submission
|
||||
await secureApi.submitEmployeeAnswers(employeeId, answers);
|
||||
|
||||
// Update local state for immediate UI feedback
|
||||
const convertedSubmission: Submission = {
|
||||
employeeId,
|
||||
answers: Object.entries(answers).map(([question, answer]) => ({
|
||||
question,
|
||||
answer
|
||||
}))
|
||||
};
|
||||
setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission }));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('submitEmployeeAnswers error', e);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const generateEmployeeReport = useCallback(async (employee: Employee) => {
|
||||
try {
|
||||
console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
|
||||
|
||||
if (!user?.uid) {
|
||||
throw new Error('User authentication required');
|
||||
}
|
||||
|
||||
// Get submission data for this employee
|
||||
const submission = submissions[employee.id];
|
||||
if (!submission) {
|
||||
throw new Error(`No questionnaire submission found for ${employee.name}. Please ensure they have completed the employee questionnaire first.`);
|
||||
}
|
||||
|
||||
// Convert submission format for API
|
||||
let submissionAnswers: Record<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) => {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Submission data found:', Object.keys(submissionAnswers).length, 'answers');
|
||||
|
||||
// Get company report and wiki data for context
|
||||
let companyWiki = null;
|
||||
try {
|
||||
const companyReports = await getFullCompanyReportHistory();
|
||||
if (companyReports.length > 0) {
|
||||
companyWiki = {
|
||||
org: org,
|
||||
companyReport: companyReports[0]
|
||||
};
|
||||
console.log('Including company context in employee report generation');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch company report for context:', error);
|
||||
}
|
||||
|
||||
const data = await secureApi.generateEmployeeReport(employee, submissionAnswers, companyWiki);
|
||||
|
||||
if ((data as any).report) {
|
||||
console.log('Employee report generated successfully');
|
||||
const report = (data as any).report as EmployeeReport;
|
||||
setReports(prev => ({ ...prev, [employee.id]: report }));
|
||||
return report;
|
||||
} else {
|
||||
throw new Error('No report data received from API');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('generateEmployeeReport error', e);
|
||||
throw e; // Re-throw to allow caller to handle
|
||||
}
|
||||
}, [user?.uid, orgId, submissions, org, getFullCompanyReportHistory]);
|
||||
|
||||
// Memoize the entire context value to prevent unnecessary re-renders
|
||||
const value = useMemo(() => ({
|
||||
org,
|
||||
orgId,
|
||||
employees,
|
||||
@@ -504,136 +644,42 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
generateCompanyWiki,
|
||||
seedInitialData,
|
||||
isOwner,
|
||||
issueInviteViaApi: async ({ name, email, role, department }) => {
|
||||
try {
|
||||
if (!user?.uid) {
|
||||
throw new Error('User authentication required');
|
||||
}
|
||||
|
||||
const data = await secureApi.createInvitation({ name, email, role, department });
|
||||
|
||||
// Optimistically add employee shell (not yet active until consume)
|
||||
setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
|
||||
...data.employee,
|
||||
initials: data.employee.name ? data.employee.name.split(' ').map((n: string) => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase()
|
||||
} as Employee]);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('issueInviteViaApi error', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
getInviteStatus: async (code: string) => {
|
||||
try {
|
||||
return await secureApi.getInvitationStatus(code);
|
||||
} catch (e) {
|
||||
console.error('getInviteStatus error', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
consumeInvite: async (code: string) => {
|
||||
try {
|
||||
if (!user?.uid) {
|
||||
throw new Error('User authentication required');
|
||||
}
|
||||
|
||||
const result = await secureApi.consumeInvitation(code, user.uid);
|
||||
|
||||
// Mark employee as active
|
||||
if (result && (result as any).employee) {
|
||||
setEmployees(prev => prev.find(e => e.id === (result as any).employee.id) ? prev : [...prev, (result as any).employee]);
|
||||
return { ...(result as any), orgId: org?.orgId };
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error('consumeInvite error', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
submitEmployeeAnswers: async (employeeId: string, answers: Record<string, string>) => {
|
||||
try {
|
||||
// Use secure API for submission
|
||||
await secureApi.submitEmployeeAnswers(employeeId, answers);
|
||||
|
||||
// Update local state for immediate UI feedback
|
||||
const convertedSubmission: Submission = {
|
||||
employeeId,
|
||||
answers: Object.entries(answers).map(([question, answer]) => ({
|
||||
question,
|
||||
answer
|
||||
}))
|
||||
};
|
||||
setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission }));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('submitEmployeeAnswers error', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
generateEmployeeReport: async (employee: Employee) => {
|
||||
try {
|
||||
console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
|
||||
|
||||
if (!user?.uid) {
|
||||
throw new Error('User authentication required');
|
||||
}
|
||||
|
||||
// Get submission data for this employee
|
||||
const submission = submissions[employee.id];
|
||||
if (!submission) {
|
||||
throw new Error(`No questionnaire submission found for ${employee.name}. Please ensure they have completed the employee questionnaire first.`);
|
||||
}
|
||||
|
||||
// Convert submission format for API
|
||||
let submissionAnswers: Record<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) => {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Submission data found:', Object.keys(submissionAnswers).length, 'answers');
|
||||
|
||||
// Get company report and wiki data for context
|
||||
let companyWiki = null;
|
||||
try {
|
||||
const companyReports = await getFullCompanyReportHistory();
|
||||
if (companyReports.length > 0) {
|
||||
companyWiki = {
|
||||
org: org,
|
||||
companyReport: companyReports[0]
|
||||
};
|
||||
console.log('Including company context in employee report generation');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch company report for context:', error);
|
||||
}
|
||||
|
||||
const data = await secureApi.generateEmployeeReport(employee, submissionAnswers, companyWiki);
|
||||
|
||||
if ((data as any).report) {
|
||||
console.log('Employee report generated successfully');
|
||||
const report = (data as any).report as EmployeeReport;
|
||||
setReports(prev => ({ ...prev, [employee.id]: report }));
|
||||
return report;
|
||||
} else {
|
||||
throw new Error('No report data received from API');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('generateEmployeeReport error', e);
|
||||
throw e; // Re-throw to allow caller to handle
|
||||
}
|
||||
},
|
||||
issueInviteViaApi,
|
||||
getInviteStatus,
|
||||
consumeInvite,
|
||||
submitEmployeeAnswers,
|
||||
generateEmployeeReport,
|
||||
getEmployeeReport,
|
||||
getEmployeeReports,
|
||||
};
|
||||
}), [
|
||||
org,
|
||||
orgId,
|
||||
employees,
|
||||
submissions,
|
||||
reports,
|
||||
loading,
|
||||
upsertOrg,
|
||||
saveReport,
|
||||
inviteEmployee,
|
||||
getReportVersions,
|
||||
saveReportVersion,
|
||||
acceptInvite,
|
||||
saveCompanyReport,
|
||||
getCompanyReportHistory,
|
||||
saveFullCompanyReport,
|
||||
getFullCompanyReportHistory,
|
||||
generateCompanyReport,
|
||||
generateCompanyWiki,
|
||||
seedInitialData,
|
||||
isOwner,
|
||||
issueInviteViaApi,
|
||||
getInviteStatus,
|
||||
consumeInvite,
|
||||
submitEmployeeAnswers,
|
||||
generateEmployeeReport,
|
||||
getEmployeeReport,
|
||||
getEmployeeReports,
|
||||
]);
|
||||
|
||||
return (
|
||||
<OrgContext.Provider value={value}>
|
||||
|
||||
@@ -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,20 +188,33 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
||||
console.error('Failed to get subscription status:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Memoize the context value to prevent unnecessary re-renders
|
||||
const contextValue = useMemo(() => ({
|
||||
organizations,
|
||||
selectedOrgId,
|
||||
loading,
|
||||
selectOrganization,
|
||||
createOrganization,
|
||||
joinOrganization,
|
||||
refreshOrganizations,
|
||||
createCheckoutSession,
|
||||
getSubscriptionStatus
|
||||
}), [
|
||||
organizations,
|
||||
selectedOrgId,
|
||||
loading,
|
||||
selectOrganization,
|
||||
createOrganization,
|
||||
joinOrganization,
|
||||
refreshOrganizations,
|
||||
createCheckoutSession,
|
||||
getSubscriptionStatus
|
||||
]);
|
||||
|
||||
return (
|
||||
<UserOrganizationsContext.Provider value={{
|
||||
organizations,
|
||||
selectedOrgId,
|
||||
loading,
|
||||
selectOrganization,
|
||||
createOrganization,
|
||||
joinOrganization,
|
||||
refreshOrganizations,
|
||||
createCheckoutSession,
|
||||
getSubscriptionStatus
|
||||
}}>
|
||||
<UserOrganizationsContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</UserOrganizationsContext.Provider>
|
||||
);
|
||||
@@ -255,4 +226,4 @@ export const useUserOrganizations = () => {
|
||||
throw new Error('useUserOrganizations must be used within UserOrganizationsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
};
|
||||
@@ -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[] = [
|
||||
|
||||
1362
src/pages/Chat.tsx
1362
src/pages/Chat.tsx
File diff suppressed because it is too large
Load Diff
@@ -1,390 +1,394 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { Theme } from '../types';
|
||||
|
||||
const HelpAndSettings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { user, signOutUser } = useAuth();
|
||||
const { org, upsertOrg, issueInviteViaApi } = useOrg();
|
||||
const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
|
||||
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
||||
const [isInviting, setIsInviting] = useState(false);
|
||||
const [inviteResult, setInviteResult] = useState<string | null>(null);
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await signOutUser();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartOnboarding = async () => {
|
||||
try {
|
||||
await upsertOrg({ onboardingCompleted: false });
|
||||
// The RequireOnboarding component will redirect automatically
|
||||
} catch (error) {
|
||||
console.error('Failed to restart onboarding:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInviteEmployee = async () => {
|
||||
if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
|
||||
|
||||
setIsInviting(true);
|
||||
setInviteResult(null);
|
||||
|
||||
try {
|
||||
const result = await issueInviteViaApi({
|
||||
name: inviteForm.name.trim(),
|
||||
email: inviteForm.email.trim(),
|
||||
role: inviteForm.role.trim() || undefined,
|
||||
department: inviteForm.department.trim() || undefined
|
||||
});
|
||||
|
||||
setInviteResult(JSON.stringify({
|
||||
success: true,
|
||||
inviteLink: result.inviteLink,
|
||||
emailLink: result.emailLink,
|
||||
employeeName: result.employee.name
|
||||
}));
|
||||
setInviteForm({ name: '', email: '', role: '', department: '' });
|
||||
} catch (error) {
|
||||
console.error('Failed to send invitation:', error);
|
||||
setInviteResult('Failed to send invitation. Please try again.');
|
||||
} finally {
|
||||
setIsInviting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSettings = () => (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Appearance</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Theme
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant={theme === Theme.Light ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTheme(Theme.Light)}
|
||||
>
|
||||
Light
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === Theme.Dark ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTheme(Theme.Dark)}
|
||||
>
|
||||
Dark
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === Theme.System ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTheme(Theme.System)}
|
||||
>
|
||||
System
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Organization</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Company:</span>
|
||||
<div className="font-medium text-[--text-primary]">{org?.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Onboarding:</span>
|
||||
<div className="font-medium text-[--text-primary]">
|
||||
{org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Button variant="secondary" onClick={handleRestartOnboarding}>
|
||||
Restart Onboarding
|
||||
</Button>
|
||||
<p className="text-xs text-[--text-secondary] mt-2">
|
||||
This will reset your company profile and require you to complete the setup process again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.name}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={inviteForm.email}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="john.doe@company.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Role
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.role}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Senior Developer"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Department
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.department}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Engineering"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleInviteEmployee}
|
||||
disabled={!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting}
|
||||
className="w-full"
|
||||
>
|
||||
{isInviting ? 'Sending Invitation...' : 'Send Invitation'}
|
||||
</Button>
|
||||
|
||||
{inviteResult && (
|
||||
<div>
|
||||
{inviteResult.includes('Failed') ? (
|
||||
<div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
|
||||
{inviteResult}
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
try {
|
||||
const result = JSON.parse(inviteResult);
|
||||
return (
|
||||
<div className="p-4 rounded-md bg-green-50 border border-green-200">
|
||||
<h4 className="text-sm font-semibold text-green-800 mb-3">
|
||||
✅ Invitation sent to {result.employeeName}!
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-green-700 mb-1">
|
||||
Direct Link (share this with the employee):
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={result.inviteLink}
|
||||
readOnly
|
||||
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href={result.emailLink}
|
||||
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
📧 Open Email Draft
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
return (
|
||||
<div className="p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200">
|
||||
{inviteResult}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-[--text-secondary]">
|
||||
The invited employee will receive an email with instructions to join your organization.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Account</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Email:</span>
|
||||
<div className="font-medium text-[--text-primary]">{user?.email}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">User ID:</span>
|
||||
<div className="font-medium text-[--text-primary] font-mono text-xs">{user?.uid}</div>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Button variant="secondary" onClick={handleLogout}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Data & Privacy</h3>
|
||||
<div className="space-y-3">
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
Export My Data
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
Privacy Settings
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start text-red-600">
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderHelp = () => (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Getting Started</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">1. Set up your organization</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Complete the onboarding process to configure your company information and preferences.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">2. Add employees</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Invite team members and add their basic information to start generating reports.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">3. Generate reports</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Use AI-powered reports to gain insights into employee performance and organizational health.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Frequently Asked Questions</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">How do I add new employees?</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Go to the Reports page and use the "Add Employee" button to invite new team members.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">How are reports generated?</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Is my data secure?</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Contact Support</h3>
|
||||
<div className="space-y-3">
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
📧 Email Support
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
💬 Live Chat
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
📚 Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary]">Help & Settings</h1>
|
||||
<p className="text-[--text-secondary] mt-1">
|
||||
Manage your account and get help
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex space-x-4 border-b border-[--border-color]">
|
||||
<button
|
||||
onClick={() => setActiveTab('settings')}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
|
||||
? 'border-blue-500 text-blue-500'
|
||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
||||
}`}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('help')}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
|
||||
? 'border-blue-500 text-blue-500'
|
||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
||||
}`}
|
||||
>
|
||||
Help
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'settings' ? renderSettings() : renderHelp()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpAndSettings;
|
||||
// DEPRECATED: This component has been split into separate Help and Settings pages
|
||||
// Use /src/pages/HelpNew.tsx and /src/pages/SettingsNew.tsx instead
|
||||
// This file can be safely removed in future cleanup
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { Theme } from '../types';
|
||||
|
||||
const HelpAndSettings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { user, signOutUser } = useAuth();
|
||||
const { org, upsertOrg, issueInviteViaApi } = useOrg();
|
||||
const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
|
||||
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
||||
const [isInviting, setIsInviting] = useState(false);
|
||||
const [inviteResult, setInviteResult] = useState<string | null>(null);
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await signOutUser();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartOnboarding = async () => {
|
||||
try {
|
||||
await upsertOrg({ onboardingCompleted: false });
|
||||
// The RequireOnboarding component will redirect automatically
|
||||
} catch (error) {
|
||||
console.error('Failed to restart onboarding:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInviteEmployee = async () => {
|
||||
if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
|
||||
|
||||
setIsInviting(true);
|
||||
setInviteResult(null);
|
||||
|
||||
try {
|
||||
const result = await issueInviteViaApi({
|
||||
name: inviteForm.name.trim(),
|
||||
email: inviteForm.email.trim(),
|
||||
role: inviteForm.role.trim() || undefined,
|
||||
department: inviteForm.department.trim() || undefined
|
||||
});
|
||||
|
||||
setInviteResult(JSON.stringify({
|
||||
success: true,
|
||||
inviteLink: result.inviteLink,
|
||||
emailLink: result.emailLink,
|
||||
employeeName: result.employee.name
|
||||
}));
|
||||
setInviteForm({ name: '', email: '', role: '', department: '' });
|
||||
} catch (error) {
|
||||
console.error('Failed to send invitation:', error);
|
||||
setInviteResult('Failed to send invitation. Please try again.');
|
||||
} finally {
|
||||
setIsInviting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSettings = () => (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Appearance</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Theme
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant={theme === Theme.Light ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTheme(Theme.Light)}
|
||||
>
|
||||
Light
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === Theme.Dark ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTheme(Theme.Dark)}
|
||||
>
|
||||
Dark
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === Theme.System ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTheme(Theme.System)}
|
||||
>
|
||||
System
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Organization</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Company:</span>
|
||||
<div className="font-medium text-[--text-primary]">{org?.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Onboarding:</span>
|
||||
<div className="font-medium text-[--text-primary]">
|
||||
{org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Button variant="secondary" onClick={handleRestartOnboarding}>
|
||||
Restart Onboarding
|
||||
</Button>
|
||||
<p className="text-xs text-[--text-secondary] mt-2">
|
||||
This will reset your company profile and require you to complete the setup process again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.name}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={inviteForm.email}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="john.doe@company.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Role
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.role}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Senior Developer"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||
Department
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.department}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Engineering"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleInviteEmployee}
|
||||
disabled={!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting}
|
||||
className="w-full"
|
||||
>
|
||||
{isInviting ? 'Sending Invitation...' : 'Send Invitation'}
|
||||
</Button>
|
||||
|
||||
{inviteResult && (
|
||||
<div>
|
||||
{inviteResult.includes('Failed') ? (
|
||||
<div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
|
||||
{inviteResult}
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
try {
|
||||
const result = JSON.parse(inviteResult);
|
||||
return (
|
||||
<div className="p-4 rounded-md bg-green-50 border border-green-200">
|
||||
<h4 className="text-sm font-semibold text-green-800 mb-3">
|
||||
✅ Invitation sent to {result.employeeName}!
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-green-700 mb-1">
|
||||
Direct Link (share this with the employee):
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={result.inviteLink}
|
||||
readOnly
|
||||
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href={result.emailLink}
|
||||
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
📧 Open Email Draft
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
return (
|
||||
<div className="p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200">
|
||||
{inviteResult}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-[--text-secondary]">
|
||||
The invited employee will receive an email with instructions to join your organization.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Account</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">Email:</span>
|
||||
<div className="font-medium text-[--text-primary]">{user?.email}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-[--text-secondary]">User ID:</span>
|
||||
<div className="font-medium text-[--text-primary] font-mono text-xs">{user?.uid}</div>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Button variant="secondary" onClick={handleLogout}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Data & Privacy</h3>
|
||||
<div className="space-y-3">
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
Export My Data
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
Privacy Settings
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start text-red-600">
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderHelp = () => (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Getting Started</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">1. Set up your organization</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Complete the onboarding process to configure your company information and preferences.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">2. Add employees</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Invite team members and add their basic information to start generating reports.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">3. Generate reports</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Use AI-powered reports to gain insights into employee performance and organizational health.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Frequently Asked Questions</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">How do I add new employees?</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Go to the Reports page and use the "Add Employee" button to invite new team members.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">How are reports generated?</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-[--text-primary] mb-2">Is my data secure?</h4>
|
||||
<p className="text-[--text-secondary] text-sm">
|
||||
Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Contact Support</h3>
|
||||
<div className="space-y-3">
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
📧 Email Support
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
💬 Live Chat
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full justify-start">
|
||||
📚 Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary]">Help & Settings</h1>
|
||||
<p className="text-[--text-secondary] mt-1">
|
||||
Manage your account and get help
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex space-x-4 border-b border-[--border-color]">
|
||||
<button
|
||||
onClick={() => setActiveTab('settings')}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
|
||||
? 'border-blue-500 text-blue-500'
|
||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
||||
}`}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('help')}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
|
||||
? 'border-blue-500 text-blue-500'
|
||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
||||
}`}
|
||||
>
|
||||
Help
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'settings' ? renderSettings() : renderHelp()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpAndSettings;
|
||||
|
||||
@@ -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,64 +66,62 @@ 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 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] flex flex-col justify-start items-start gap-4">
|
||||
{faqItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="self-stretch 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
|
||||
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">
|
||||
{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" />
|
||||
</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" />
|
||||
</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 justify-start text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal">
|
||||
{item.answer}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
<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-[--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
|
||||
onClick={handleContactUs}
|
||||
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 cursor-pointer hover:bg-[--Brand-Orange]/90"
|
||||
key={index}
|
||||
className="self-stretch 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>
|
||||
<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" />
|
||||
</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">Contact Us</div>
|
||||
<div
|
||||
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-[--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(--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(--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-[--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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</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-[--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}
|
||||
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 cursor-pointer hover:bg-[--Brand-Orange]/90"
|
||||
>
|
||||
<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)" 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">Contact Us</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => (
|
||||
<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="text-center justify-start text-[--Neutrals-NeutralSlate500] text-xs font-medium font-['Inter'] leading-none">
|
||||
{employee.initials}
|
||||
{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 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 className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">
|
||||
{employee.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,10 +263,23 @@ const Reports: React.FC = () => {
|
||||
isGenerating={generatingCompanyReport}
|
||||
/>
|
||||
) : (
|
||||
<EmployeeReportContent
|
||||
report={selectedReport.report as EmployeeReport}
|
||||
employeeName={selectedReport.employeeName!}
|
||||
/>
|
||||
(() => {
|
||||
const employeeReport = selectedReport.report as EmployeeReport;
|
||||
const employeeId = employeeReport.employeeId;
|
||||
return (
|
||||
<EmployeeReportContent
|
||||
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,14 +320,37 @@ 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="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">
|
||||
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--Neutrals-NeutralSlate0)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate0] text-sm font-medium font-['Inter'] leading-tight">Download as PDF</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">
|
||||
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--Neutrals-NeutralSlate0)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate0] text-sm font-medium font-['Inter'] leading-tight">Download as PDF</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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,23 +781,56 @@ const CompanyReportContent: React.FC<{
|
||||
const EmployeeReportContent: React.FC<{
|
||||
report: EmployeeReport;
|
||||
employeeName: string;
|
||||
}> = ({ report, employeeName }) => {
|
||||
onGenerateReport?: () => void;
|
||||
isGenerating?: boolean;
|
||||
hasSubmission?: boolean;
|
||||
showGenerateButton?: boolean;
|
||||
}> = ({ report, employeeName, onGenerateReport, isGenerating = false, hasSubmission = false, showGenerateButton = false }) => {
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
<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="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">
|
||||
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--white, 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">Download as PDF</div>
|
||||
</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">
|
||||
<path d="M14 14H2M12 7.33333L8 11.3333M8 11.3333L4 7.33333M8 11.3333V2" stroke="var(--white, 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">Download as PDF</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,295 +1,295 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import Sidebar from '../components/figma/Sidebar';
|
||||
|
||||
interface UserProfile {
|
||||
fullName: string;
|
||||
email: string;
|
||||
profilePicture?: string;
|
||||
}
|
||||
|
||||
type ThemeMode = 'system' | 'light' | 'dark';
|
||||
|
||||
const SettingsNew: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
|
||||
const [userProfile, setUserProfile] = useState<UserProfile>({
|
||||
fullName: 'John Doe',
|
||||
email: 'Johndoe1234@gmail.com'
|
||||
});
|
||||
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
|
||||
|
||||
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
|
||||
setUserProfile(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePhotoUpload = () => {
|
||||
// In a real app, this would open a file picker
|
||||
alert('Photo upload functionality would be implemented here');
|
||||
};
|
||||
|
||||
const handleSaveChanges = () => {
|
||||
// In a real app, this would save to backend
|
||||
alert('Settings saved successfully!');
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setUserProfile({
|
||||
fullName: 'John Doe',
|
||||
email: 'Johndoe1234@gmail.com'
|
||||
});
|
||||
setSelectedTheme('light');
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
|
||||
{/* Tab Navigation */}
|
||||
<div className="self-stretch px-6 pt-6 border-b border-Outline-Outline-Gray-200 flex flex-col justify-start items-end">
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-6">
|
||||
<div
|
||||
onClick={() => setActiveTab('general')}
|
||||
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'general'
|
||||
? 'text-Text-Gray-800 font-semibold'
|
||||
: 'text-Text-Gray-500 font-normal'
|
||||
}`}>
|
||||
General Settings
|
||||
</div>
|
||||
{activeTab === 'general' && (
|
||||
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setActiveTab('billing')}
|
||||
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
|
||||
? 'text-Text-Gray-800 font-semibold'
|
||||
: 'text-Text-Gray-500 font-normal'
|
||||
}`}>
|
||||
Plan & Billings
|
||||
</div>
|
||||
{activeTab === 'billing' && (
|
||||
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||
</div>
|
||||
|
||||
{/* General Settings Content */}
|
||||
{activeTab === 'general' && (
|
||||
<>
|
||||
{/* Profile Information Section */}
|
||||
<div className="w-[1136px] h-72 p-6 flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Profile Information</div>
|
||||
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Update your personal details, and keep your profile up to date.</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
{/* Profile Picture Section */}
|
||||
<div className="w-[664px] px-3 py-2.5 bg-Text-White-00 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 inline-flex justify-between items-center">
|
||||
<div className="flex-1 flex justify-start items-center gap-3">
|
||||
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
|
||||
<div>
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_1042_3786)">
|
||||
<ellipse cx="28" cy="54.6008" rx="22.4" ry="16.8" fill="white" fillOpacity="0.72" />
|
||||
<circle opacity="0.9" cx="28" cy="22.3992" r="11.2" fill="white" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1042_3786">
|
||||
<rect width="56" height="56" rx="28" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-center text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Profile Picture</div>
|
||||
<div className="self-stretch justify-center text-Text-Gray-500 text-xs font-normal font-['Inter'] leading-none">PNG, JPEG, GIF Under 10MB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={handlePhotoUpload}
|
||||
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
||||
>
|
||||
<div>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66665 10.8282C1.86266 10.29 1.33331 9.37347 1.33331 8.33333C1.33331 6.77095 2.52765 5.48753 4.05314 5.34625C4.36519 3.44809 6.01348 2 7.99998 2C9.98648 2 11.6348 3.44809 11.9468 5.34625C13.4723 5.48753 14.6666 6.77095 14.6666 8.33333C14.6666 9.37347 14.1373 10.29 13.3333 10.8282M5.33331 10.6667L7.99998 8M7.99998 8L10.6666 10.6667M7.99998 8V14" stroke="var(--Text-Dark-950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload Photo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name and Email Fields */}
|
||||
<div className="w-[664px] inline-flex justify-start items-center gap-4">
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Full Name</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12.5C7.35828 12.5 5.00901 13.7755 3.51334 15.755C3.19143 16.181 3.03047 16.394 3.03574 16.6819C3.03981 16.9043 3.17948 17.1849 3.35448 17.3222C3.581 17.5 3.8949 17.5 4.5227 17.5H15.4773C16.1051 17.5 16.419 17.5 16.6455 17.3222C16.8205 17.1849 16.9602 16.9043 16.9643 16.6819C16.9695 16.394 16.8086 16.181 16.4867 15.755C14.991 13.7755 12.6417 12.5 10 12.5Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10 10C12.0711 10 13.75 8.32107 13.75 6.25C13.75 4.17893 12.0711 2.5 10 2.5C7.92894 2.5 6.25001 4.17893 6.25001 6.25C6.25001 8.32107 7.92894 10 10 10Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={userProfile.fullName}
|
||||
onChange={(e) => handleProfileUpdate('fullName', e.target.value)}
|
||||
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email Address</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66669 5.83203L8.47079 10.5949C9.02176 10.9806 9.29725 11.1734 9.59691 11.2481C9.8616 11.3141 10.1384 11.3141 10.4031 11.2481C10.7028 11.1734 10.9783 10.9806 11.5293 10.5949L18.3334 5.83203M5.66669 16.6654H14.3334C15.7335 16.6654 16.4336 16.6654 16.9683 16.3929C17.4387 16.1532 17.8212 15.7707 18.0609 15.3003C18.3334 14.7656 18.3334 14.0655 18.3334 12.6654V7.33203C18.3334 5.9319 18.3334 5.23183 18.0609 4.69705C17.8212 4.22665 17.4387 3.8442 16.9683 3.60451C16.4336 3.33203 15.7335 3.33203 14.3334 3.33203H5.66669C4.26656 3.33203 3.56649 3.33203 3.03171 3.60451C2.56131 3.8442 2.17885 4.22665 1.93917 4.69705C1.66669 5.23183 1.66669 5.9319 1.66669 7.33203V12.6654C1.66669 14.0655 1.66669 14.7656 1.93917 15.3003C2.17885 15.7707 2.56131 16.1532 3.03171 16.3929C3.56649 16.6654 4.26656 16.6654 5.66669 16.6654Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={userProfile.email}
|
||||
onChange={(e) => handleProfileUpdate('email', e.target.value)}
|
||||
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div>
|
||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Theme Customization Section */}
|
||||
<div className="w-[1170px] p-6 flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Theme Customization</div>
|
||||
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Personalize your interface with light or dark mode and enhance your visual experience.</div>
|
||||
</div>
|
||||
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
|
||||
{/* System Preference */}
|
||||
<div
|
||||
onClick={() => setSelectedTheme('system')}
|
||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
<div className="inline-flex justify-start items-center">
|
||||
<img className="w-24 h-28 rounded-tl-lg rounded-bl-lg" src="https://via.placeholder.com/94x107/f8f9fa/6c757d?text=Light" />
|
||||
<img className="w-24 h-28 rounded-tr-lg rounded-br-lg" src="https://via.placeholder.com/96x107/212529/ffffff?text=Dark" />
|
||||
</div>
|
||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">System preference</div>
|
||||
</div>
|
||||
|
||||
{/* Light Mode */}
|
||||
<div
|
||||
onClick={() => setSelectedTheme('light')}
|
||||
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
<div className="self-stretch h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
||||
}`}>
|
||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/f8f9fa/6c757d?text=Light+Mode" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Light Mode</div>
|
||||
</div>
|
||||
|
||||
{/* Dark Mode */}
|
||||
<div
|
||||
onClick={() => setSelectedTheme('dark')}
|
||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
<div className="w-48 h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
||||
}`}>
|
||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/212529/ffffff?text=Dark+Mode" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Dark Mode</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Another Divider */}
|
||||
<div>
|
||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
|
||||
<div
|
||||
onClick={handleReset}
|
||||
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reset</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={handleSaveChanges}
|
||||
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 cursor-pointer hover:bg-[--Brand-Orange]/90"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Save Changes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Billing Content */}
|
||||
{activeTab === 'billing' && (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-semibold text-Text-Gray-800 mb-4">Plan & Billing</h2>
|
||||
<p className="text-Text-Gray-500">Billing management features would be implemented here.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import Sidebar from '../components/figma/Sidebar';
|
||||
|
||||
interface UserProfile {
|
||||
fullName: string;
|
||||
email: string;
|
||||
profilePicture?: string;
|
||||
}
|
||||
|
||||
type ThemeMode = 'system' | 'light' | 'dark';
|
||||
|
||||
const SettingsNew: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
|
||||
const [userProfile, setUserProfile] = useState<UserProfile>({
|
||||
fullName: 'John Doe',
|
||||
email: 'Johndoe1234@gmail.com'
|
||||
});
|
||||
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
|
||||
|
||||
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
|
||||
setUserProfile(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePhotoUpload = () => {
|
||||
// In a real app, this would open a file picker
|
||||
alert('Photo upload functionality would be implemented here');
|
||||
};
|
||||
|
||||
const handleSaveChanges = () => {
|
||||
// In a real app, this would save to backend
|
||||
alert('Settings saved successfully!');
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setUserProfile({
|
||||
fullName: 'John Doe',
|
||||
email: 'Johndoe1234@gmail.com'
|
||||
});
|
||||
setSelectedTheme('light');
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
|
||||
{/* Tab Navigation */}
|
||||
<div className="self-stretch px-6 pt-6 border-b border-Outline-Outline-Gray-200 flex flex-col justify-start items-end">
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-6">
|
||||
<div
|
||||
onClick={() => setActiveTab('general')}
|
||||
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'general'
|
||||
? 'text-Text-Gray-800 font-semibold'
|
||||
: 'text-Text-Gray-500 font-normal'
|
||||
}`}>
|
||||
General Settings
|
||||
</div>
|
||||
{activeTab === 'general' && (
|
||||
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setActiveTab('billing')}
|
||||
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
|
||||
? 'text-Text-Gray-800 font-semibold'
|
||||
: 'text-Text-Gray-500 font-normal'
|
||||
}`}>
|
||||
Plan & Billings
|
||||
</div>
|
||||
{activeTab === 'billing' && (
|
||||
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||
</div>
|
||||
|
||||
{/* General Settings Content */}
|
||||
{activeTab === 'general' && (
|
||||
<>
|
||||
{/* Profile Information Section */}
|
||||
<div className="w-[1136px] h-72 p-6 flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Profile Information</div>
|
||||
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Update your personal details, and keep your profile up to date.</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
{/* Profile Picture Section */}
|
||||
<div className="w-[664px] px-3 py-2.5 bg-Text-White-00 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 inline-flex justify-between items-center">
|
||||
<div className="flex-1 flex justify-start items-center gap-3">
|
||||
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
|
||||
<div>
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_1042_3786)">
|
||||
<ellipse cx="28" cy="54.6008" rx="22.4" ry="16.8" fill="white" fillOpacity="0.72" />
|
||||
<circle opacity="0.9" cx="28" cy="22.3992" r="11.2" fill="white" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1042_3786">
|
||||
<rect width="56" height="56" rx="28" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-center text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Profile Picture</div>
|
||||
<div className="self-stretch justify-center text-Text-Gray-500 text-xs font-normal font-['Inter'] leading-none">PNG, JPEG, GIF Under 10MB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={handlePhotoUpload}
|
||||
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
||||
>
|
||||
<div>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66665 10.8282C1.86266 10.29 1.33331 9.37347 1.33331 8.33333C1.33331 6.77095 2.52765 5.48753 4.05314 5.34625C4.36519 3.44809 6.01348 2 7.99998 2C9.98648 2 11.6348 3.44809 11.9468 5.34625C13.4723 5.48753 14.6666 6.77095 14.6666 8.33333C14.6666 9.37347 14.1373 10.29 13.3333 10.8282M5.33331 10.6667L7.99998 8M7.99998 8L10.6666 10.6667M7.99998 8V14" stroke="var(--Text-Dark-950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload Photo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name and Email Fields */}
|
||||
<div className="w-[664px] inline-flex justify-start items-center gap-4">
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Full Name</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12.5C7.35828 12.5 5.00901 13.7755 3.51334 15.755C3.19143 16.181 3.03047 16.394 3.03574 16.6819C3.03981 16.9043 3.17948 17.1849 3.35448 17.3222C3.581 17.5 3.8949 17.5 4.5227 17.5H15.4773C16.1051 17.5 16.419 17.5 16.6455 17.3222C16.8205 17.1849 16.9602 16.9043 16.9643 16.6819C16.9695 16.394 16.8086 16.181 16.4867 15.755C14.991 13.7755 12.6417 12.5 10 12.5Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10 10C12.0711 10 13.75 8.32107 13.75 6.25C13.75 4.17893 12.0711 2.5 10 2.5C7.92894 2.5 6.25001 4.17893 6.25001 6.25C6.25001 8.32107 7.92894 10 10 10Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={userProfile.fullName}
|
||||
onChange={(e) => handleProfileUpdate('fullName', e.target.value)}
|
||||
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email Address</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66669 5.83203L8.47079 10.5949C9.02176 10.9806 9.29725 11.1734 9.59691 11.2481C9.8616 11.3141 10.1384 11.3141 10.4031 11.2481C10.7028 11.1734 10.9783 10.9806 11.5293 10.5949L18.3334 5.83203M5.66669 16.6654H14.3334C15.7335 16.6654 16.4336 16.6654 16.9683 16.3929C17.4387 16.1532 17.8212 15.7707 18.0609 15.3003C18.3334 14.7656 18.3334 14.0655 18.3334 12.6654V7.33203C18.3334 5.9319 18.3334 5.23183 18.0609 4.69705C17.8212 4.22665 17.4387 3.8442 16.9683 3.60451C16.4336 3.33203 15.7335 3.33203 14.3334 3.33203H5.66669C4.26656 3.33203 3.56649 3.33203 3.03171 3.60451C2.56131 3.8442 2.17885 4.22665 1.93917 4.69705C1.66669 5.23183 1.66669 5.9319 1.66669 7.33203V12.6654C1.66669 14.0655 1.66669 14.7656 1.93917 15.3003C2.17885 15.7707 2.56131 16.1532 3.03171 16.3929C3.56649 16.6654 4.26656 16.6654 5.66669 16.6654Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={userProfile.email}
|
||||
onChange={(e) => handleProfileUpdate('email', e.target.value)}
|
||||
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div>
|
||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Theme Customization Section */}
|
||||
<div className="w-[1170px] p-6 flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Theme Customization</div>
|
||||
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Personalize your interface with light or dark mode and enhance your visual experience.</div>
|
||||
</div>
|
||||
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
|
||||
{/* System Preference */}
|
||||
<div
|
||||
onClick={() => setSelectedTheme('system')}
|
||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
<div className="inline-flex justify-start items-center">
|
||||
<img className="w-24 h-28 rounded-tl-lg rounded-bl-lg" src="https://via.placeholder.com/94x107/f8f9fa/6c757d?text=Light" />
|
||||
<img className="w-24 h-28 rounded-tr-lg rounded-br-lg" src="https://via.placeholder.com/96x107/212529/ffffff?text=Dark" />
|
||||
</div>
|
||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">System preference</div>
|
||||
</div>
|
||||
|
||||
{/* Light Mode */}
|
||||
<div
|
||||
onClick={() => setSelectedTheme('light')}
|
||||
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
<div className="self-stretch h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
||||
}`}>
|
||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/f8f9fa/6c757d?text=Light+Mode" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Light Mode</div>
|
||||
</div>
|
||||
|
||||
{/* Dark Mode */}
|
||||
<div
|
||||
onClick={() => setSelectedTheme('dark')}
|
||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
<div className="w-48 h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
||||
}`}>
|
||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/212529/ffffff?text=Dark+Mode" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Dark Mode</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Another Divider */}
|
||||
<div>
|
||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
|
||||
<div
|
||||
onClick={handleReset}
|
||||
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reset</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={handleSaveChanges}
|
||||
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 cursor-pointer hover:bg-[--Brand-Orange]/90"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Save Changes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Billing Content */}
|
||||
{activeTab === 'billing' && (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-semibold text-Text-Gray-800 mb-4">Plan & Billing</h2>
|
||||
<p className="text-Text-Gray-500">Billing management features would be implemented here.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsNew;
|
||||
@@ -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 = () => {
|
||||
}
|
||||
};
|
||||
|
||||
loadSubmissions();
|
||||
if (employees.length > 0) {
|
||||
loadSubmissions();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [employees]);
|
||||
|
||||
const loadDemoSubmissions = () => {
|
||||
@@ -61,6 +79,7 @@ const Submissions: React.FC = () => {
|
||||
|
||||
employees.forEach((employee, index) => {
|
||||
if (index < 3) { // Only add submissions for first 3 employees
|
||||
console.log(employee);
|
||||
demoSubmissions[employee.id] = {
|
||||
employeeId: employee.id,
|
||||
employee,
|
||||
@@ -123,17 +142,51 @@ const Submissions: React.FC = () => {
|
||||
const submission = submissions[selectedEmployee.id];
|
||||
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
|
||||
|
||||
// Map EMPLOYEE_QUESTIONS to actual answers
|
||||
EMPLOYEE_QUESTIONS.forEach(q => {
|
||||
const answer = submission.answers[q.id];
|
||||
if (answer && answer.trim()) {
|
||||
questionsAndAnswers.push({
|
||||
question: q.prompt,
|
||||
answer: answer,
|
||||
isLong: answer.length > 150 // Mark long answers for different styling
|
||||
});
|
||||
// Handle different submission formats
|
||||
let submissionAnswers: Record<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 = submissionAnswers[q.id];
|
||||
if (answer && answer.trim()) {
|
||||
questionsAndAnswers.push({
|
||||
question: q.prompt,
|
||||
answer: answer,
|
||||
isLong: answer.length > 150
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add any additional answers not in EMPLOYEE_QUESTIONS
|
||||
Object.entries(submissionAnswers).forEach(([key, answer]) => {
|
||||
if (answer && ((typeof answer === 'string' && answer.trim()) || typeof answer === 'boolean' || typeof answer === 'number') && !EMPLOYEE_QUESTIONS.find(q => q.id === key)) {
|
||||
// Format the key as a readable question
|
||||
const formattedQuestion = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
questionsAndAnswers.push({
|
||||
question: formattedQuestion,
|
||||
answer: answer,
|
||||
isLong: answer.length > 150
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return questionsAndAnswers;
|
||||
};
|
||||
@@ -298,7 +351,7 @@ const SubmissionContent: React.FC<{
|
||||
questionsAndAnswers.map((qa, index) => (
|
||||
<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">
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
54
src/types.ts
54
src/types.ts
@@ -1,5 +1,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { inherits } from 'util';
|
||||
|
||||
export enum Theme {
|
||||
Light = 'light',
|
||||
@@ -11,13 +12,12 @@ export interface Employee {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role?: 'owner' | 'admin' | 'employee';
|
||||
role?: string; // Remove 'owner' from the union type since employees are never owners
|
||||
department?: string | 'General';
|
||||
joinedAt?: number;
|
||||
status: 'invited' | 'active';
|
||||
initials?: string;
|
||||
inviteCode?: string;
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,12 @@ export interface Organization {
|
||||
updatedAt: number;
|
||||
onboardingCompleted: boolean;
|
||||
ownerId: string;
|
||||
ownerInfo?: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
joinedAt: number;
|
||||
};
|
||||
// Subscription fields (will be populated after Stripe setup)
|
||||
subscription: {
|
||||
status: 'trial' | 'active' | 'past_due' | 'canceled';
|
||||
@@ -146,42 +152,24 @@ export interface EmployeeReport {
|
||||
selfAwareness: string;
|
||||
emotionalResponses: string;
|
||||
growthDesire: string;
|
||||
|
||||
strengths?: string[];
|
||||
weaknesses?: string[];
|
||||
value?: number;
|
||||
};
|
||||
actionableItems?: { id: string; title: string; impact: 'High' | 'Medium' | 'Low'; effort: 'High' | 'Medium' | 'Low'; description: string; }[];
|
||||
roleFitCandidates?: { employeeId: string; roles: string[]; rationale: string; score: number; }[];
|
||||
potentialExits?: { employeeId: string; risk: 'Low' | 'Medium' | 'High'; reason: string; }[];
|
||||
traitWeighting?: { trait: string; weight: number; rationale?: string; }[];
|
||||
strengths: string[];
|
||||
weaknesses: {
|
||||
isCritical: boolean;
|
||||
description: string;
|
||||
}[];
|
||||
weaknesses: string[];
|
||||
opportunities: {
|
||||
roleAdjustment: string;
|
||||
accountabilitySupport: string;
|
||||
description?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[];
|
||||
risks: string[];
|
||||
recommendations: string[];
|
||||
recommendation: {
|
||||
action: 'Keep' | 'Restructure' | 'Terminate';
|
||||
details: string[];
|
||||
};
|
||||
grading: {
|
||||
department: string;
|
||||
lead: string;
|
||||
support: string;
|
||||
gradingOverview: {
|
||||
employeeName: string;
|
||||
grade: string;
|
||||
comment: string;
|
||||
scores: { subject: string; value: number; fullMark: number; }[];
|
||||
}[];
|
||||
suitabilityScore?: number;
|
||||
retentionRisk?: 'Low' | 'Medium' | 'High';
|
||||
costEffectiveness?: 'Underperforming' | 'Aligned' | 'High Value';
|
||||
reliability: number;
|
||||
roleFit: number;
|
||||
scalability: number;
|
||||
output: number;
|
||||
initiative: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Submission {
|
||||
@@ -240,7 +228,6 @@ export interface CompanyReport {
|
||||
reasoning: string;
|
||||
urgency?: 'high' | 'medium' | 'low'; // UI alias
|
||||
}[];
|
||||
recommendations: string[];
|
||||
forwardOperatingPlan?: {
|
||||
title: string;
|
||||
details: string[];
|
||||
@@ -266,8 +253,6 @@ export interface CompanyReport {
|
||||
teamScores: {
|
||||
employeeName: string;
|
||||
grade: string;
|
||||
// Each of the following is out of 10, total being 50 points
|
||||
// These gets displayed as radar charts
|
||||
reliability: number;
|
||||
roleFit: number;
|
||||
scalability: number;
|
||||
@@ -275,7 +260,6 @@ export interface CompanyReport {
|
||||
initiative: number;
|
||||
}[];
|
||||
}[];
|
||||
|
||||
executiveSummary: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user