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',
|
apiVersion: '2024-11-20.acacia',
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
const RESPONSE_FORMAT = {
|
const RESPONSE_FORMAT_EMPLOYEE = {
|
||||||
type: "json_schema",
|
"type": "object",
|
||||||
json_schema: {
|
"properties": {
|
||||||
name: "company_artifacts",
|
"employeeId": {
|
||||||
strict: true,
|
"type": "string"
|
||||||
schema: {
|
},
|
||||||
type: "object",
|
"department": {
|
||||||
additionalProperties: false,
|
"type": "string"
|
||||||
properties: {
|
},
|
||||||
companyPerformance: {
|
"role": {
|
||||||
type: "object",
|
"type": "string"
|
||||||
additionalProperties: false,
|
},
|
||||||
properties: {
|
"roleAndOutput": {
|
||||||
summary: { type: "string" },
|
"type": "object",
|
||||||
metrics: {
|
"properties": {
|
||||||
type: "array",
|
"responsibilities": {
|
||||||
items: {
|
"type": "string",
|
||||||
type: "object",
|
"examples": [
|
||||||
additionalProperties: false,
|
"Recruiting influencers, onboarding, campaign support, business development."
|
||||||
properties: {
|
]
|
||||||
name: { type: "string" },
|
},
|
||||||
value: { anyOf: [{ type: "string" }, { type: "number" }] },
|
"clarityOnRole": {
|
||||||
trend: { enum: ["up", "down", "flat"] }
|
"type": "string",
|
||||||
},
|
"examples": [
|
||||||
required: ["name", "value", "trend"]
|
"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: {
|
"selfAwareness": {
|
||||||
type: "array",
|
"type": "string",
|
||||||
items: {
|
"examples": [
|
||||||
type: "object",
|
"High – acknowledges weaknesses like lateness and disorganization."
|
||||||
additionalProperties: false,
|
]
|
||||||
properties: {
|
|
||||||
role: { type: "string" },
|
|
||||||
urgency: { enum: ["low", "medium", "high"] },
|
|
||||||
reason: { type: "string" }
|
|
||||||
},
|
|
||||||
required: ["role", "urgency", "reason"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
forwardOperatingPlan: {
|
"emotionalResponses": {
|
||||||
type: "object",
|
"type": "string",
|
||||||
additionalProperties: false,
|
"examples": [
|
||||||
properties: {
|
"Frustrated by campaign disorganization; would prefer closer collaboration."
|
||||||
nextQuarterObjectives: { type: "array", items: { type: "string" } },
|
]
|
||||||
initiatives: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
name: { type: "string" },
|
|
||||||
owner: { type: "string" },
|
|
||||||
kpis: { type: "array", items: { type: "string" } }
|
|
||||||
},
|
|
||||||
required: ["name", "owner", "kpis"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
risks: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
risk: { type: "string" },
|
|
||||||
mitigation: { type: "string" }
|
|
||||||
},
|
|
||||||
required: ["risk", "mitigation"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["nextQuarterObjectives", "initiatives", "risks"]
|
|
||||||
},
|
},
|
||||||
organizationalInsights: {
|
"growthDesire": {
|
||||||
type: "object",
|
"type": "string",
|
||||||
additionalProperties: false,
|
"examples": [
|
||||||
properties: {
|
"Interested in becoming more client-facing and shifting toward biz dev."
|
||||||
culture: { type: "string" },
|
]
|
||||||
teamDynamics: { type: "string" },
|
}
|
||||||
blockers: { type: "array", items: { type: "string" } }
|
}
|
||||||
},
|
},
|
||||||
required: ["culture", "teamDynamics", "blockers"]
|
"strengths": {
|
||||||
},
|
"type": "array",
|
||||||
strengths: { type: "array", items: { type: "string" } },
|
"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
|
// Helper function to generate OTP
|
||||||
@@ -714,22 +1009,22 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
|||||||
const orgData = orgDoc.exists ? orgDoc.data() : {};
|
const orgData = orgDoc.exists ? orgDoc.data() : {};
|
||||||
|
|
||||||
// Prepare company context (onboarding data)
|
// Prepare company context (onboarding data)
|
||||||
const companyContext = {
|
let companyContext = {
|
||||||
name: orgData.name,
|
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
|
// Prepare submission data
|
||||||
const submissionData = {
|
const submissionData = {
|
||||||
employeeId: finalEmployeeId,
|
employeeId: finalEmployeeId,
|
||||||
answers,
|
answers,
|
||||||
submittedAt: Date.now(),
|
submittedAt: Date.now(),
|
||||||
status: "completed"
|
status: "completed",
|
||||||
|
companyContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate the report using the existing function logic
|
// Generate the report using the existing function logic
|
||||||
@@ -737,7 +1032,7 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
|||||||
if (openai) {
|
if (openai) {
|
||||||
// Use OpenAI to generate the report with company context
|
// Use OpenAI to generate the report with company context
|
||||||
const prompt = `
|
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:
|
Employee Information:
|
||||||
- Name: ${employeeData?.name || employeeData?.email || 'Unknown'}
|
- Name: ${employeeData?.name || employeeData?.email || 'Unknown'}
|
||||||
@@ -762,17 +1057,7 @@ Generate a detailed report that:
|
|||||||
8. Provides numerical grading across key performance areas
|
8. Provides numerical grading across key performance areas
|
||||||
|
|
||||||
Return ONLY valid JSON that matches this structure:
|
Return ONLY valid JSON that matches this structure:
|
||||||
{
|
${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)}
|
||||||
"roleAndOutput": { "currentRole": string, "keyResponsibilities": string[], "performanceRating": number },
|
|
||||||
"behavioralInsights": { "workStyle": string, "communicationSkills": string, "teamDynamics": string },
|
|
||||||
"strengths": string[],
|
|
||||||
"weaknesses": string[],
|
|
||||||
"opportunities": string[],
|
|
||||||
"risks": string[],
|
|
||||||
"recommendations": string[],
|
|
||||||
"companyAlignment": { "valuesAlignment": number, "cultureAlignment": number, "missionAlignment": number },
|
|
||||||
"grading": { "overall": number, "technical": number, "communication": number, "teamwork": number, "leadership": number }
|
|
||||||
}
|
|
||||||
|
|
||||||
Be thorough, professional, and focus on actionable insights.
|
Be thorough, professional, and focus on actionable insights.
|
||||||
`.trim();
|
`.trim();
|
||||||
@@ -796,8 +1081,13 @@ Be thorough, professional, and focus on actionable insights.
|
|||||||
const aiResponse = completion.choices[0].message.content;
|
const aiResponse = completion.choices[0].message.content;
|
||||||
const parsedReport = JSON.parse(aiResponse);
|
const parsedReport = JSON.parse(aiResponse);
|
||||||
|
|
||||||
|
console.log(parsedReport);
|
||||||
|
|
||||||
report = {
|
report = {
|
||||||
employeeId: finalEmployeeId,
|
employeeId: finalEmployeeId,
|
||||||
|
employeeName: employeeData?.name || employeeData?.email || 'Employee',
|
||||||
|
role: employeeData?.role || "Team Member",
|
||||||
|
email: employeeData?.email || 'Unknown',
|
||||||
generatedAt: Date.now(),
|
generatedAt: Date.now(),
|
||||||
summary: `AI-generated performance analysis for ${employeeData?.name || employeeData?.email || 'Employee'}`,
|
summary: `AI-generated performance analysis for ${employeeData?.name || employeeData?.email || 'Employee'}`,
|
||||||
submissionId: finalEmployeeId,
|
submissionId: finalEmployeeId,
|
||||||
@@ -910,12 +1200,13 @@ exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => {
|
|||||||
if (openai) {
|
if (openai) {
|
||||||
// Use OpenAI to generate the report
|
// Use OpenAI to generate the report
|
||||||
const prompt = `
|
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:
|
Employee Information:
|
||||||
- Name: ${employee.name || employee.email}
|
- Name: ${employee?.name || employee?.email || 'Unknown'}
|
||||||
- Role: ${employee.role || "Team Member"}
|
- Role: ${employee?.role || "Team Member"}
|
||||||
- Department: ${employee.department || "General"}
|
- Department: ${employee?.department || "General"}
|
||||||
|
- Email: ${employee?.email || 'Unknown'}
|
||||||
|
|
||||||
Employee Submission Data:
|
Employee Submission Data:
|
||||||
${JSON.stringify(submission, null, 2)}
|
${JSON.stringify(submission, null, 2)}
|
||||||
@@ -923,18 +1214,21 @@ ${JSON.stringify(submission, null, 2)}
|
|||||||
Company Context:
|
Company Context:
|
||||||
${companyWiki ? JSON.stringify(companyWiki, null, 2) : "No company context provided"}
|
${companyWiki ? JSON.stringify(companyWiki, null, 2) : "No company context provided"}
|
||||||
|
|
||||||
Generate a detailed report with the following structure:
|
Generate a detailed report that:
|
||||||
- roleAndOutput: Current role assessment and performance rating
|
1. Evaluates how well the employee aligns with company values and culture
|
||||||
- behavioralInsights: Work style, communication, and team dynamics
|
2. Assesses their role performance and output
|
||||||
- strengths: List of employee strengths
|
3. Identifies behavioral insights and work patterns
|
||||||
- weaknesses: Areas for improvement (mark critical issues)
|
4. Highlights strengths and areas for improvement
|
||||||
- opportunities: Growth and development opportunities
|
5. Provides specific recommendations for growth
|
||||||
- risks: Potential risks or concerns
|
6. Suggests opportunities that align with company goals
|
||||||
- recommendations: Specific action items
|
7. Identifies any risks or concerns
|
||||||
- grading: Numerical scores for different performance areas
|
8. Provides numerical grading across key performance areas
|
||||||
|
|
||||||
Return ONLY valid JSON that matches this structure. Be thorough but professional.
|
Return ONLY valid JSON that matches this structure:
|
||||||
`.trim();
|
${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)}
|
||||||
|
|
||||||
|
Be thorough, professional, and focus on actionable insights.
|
||||||
|
`.trim();
|
||||||
|
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4o",
|
model: "gpt-4o",
|
||||||
@@ -1031,63 +1325,82 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
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;
|
const { org, submissions = [] } = req.body;
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
return res.status(400).json({ error: "Organization data is required" });
|
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 {
|
try {
|
||||||
let report, wiki;
|
let report, wiki;
|
||||||
|
|
||||||
if (openai) {
|
if (openai) {
|
||||||
// Use OpenAI to generate the company report and wiki
|
// Use OpenAI to generate the company report
|
||||||
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 = `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:
|
||||||
const user = [
|
|
||||||
"Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.",
|
Employee Submissions:
|
||||||
"Be thorough and professional.",
|
${JSON.stringify(submissions, null, 2)}
|
||||||
"",
|
|
||||||
"Organization Information:",
|
Company Context:
|
||||||
JSON.stringify(org, null, 2),
|
${JSON.stringify(orgData, null, 2)}
|
||||||
"",
|
|
||||||
"Employee Submissions:",
|
Generate a detailed report that:
|
||||||
JSON.stringify(submissions, null, 2)
|
1. Evaluates the company based on all the key sections in the JSON schema, being thorough to touch on all categories and employees
|
||||||
].join("\n");
|
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({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4o",
|
model: "gpt-4o",
|
||||||
temperature: 0, // consistency
|
response_format: { type: "json_object" },
|
||||||
response_format: RESPONSE_FORMAT,
|
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: system },
|
|
||||||
{ role: "user", content: user }
|
{ role: "user", content: user }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// content is guaranteed to be schema-conformant JSON
|
// 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 parsed = JSON.parse(completion.choices[0].message.content);
|
||||||
|
|
||||||
const report = {
|
report = {
|
||||||
generatedAt: Date.now(),
|
generatedAt: Date.now(),
|
||||||
...parsed
|
...parsed
|
||||||
};
|
};
|
||||||
|
|
||||||
const wiki = {
|
const reportRef = db
|
||||||
companyName: org?.name ?? parsed.wiki.companyName,
|
.collection("orgs")
|
||||||
generatedAt: Date.now(),
|
.doc(orgId)
|
||||||
|
.collection("companyReport")
|
||||||
|
.doc("main");
|
||||||
|
|
||||||
};
|
await reportRef.set(report);
|
||||||
|
|
||||||
const companyReport = db.collection("orgs").doc(orgId).collection("companyReport");
|
|
||||||
await companyReport.set(report);
|
|
||||||
const companyWiki = db.collection("orgs").doc(orgId).collection("companyWiki");
|
|
||||||
await companyWiki.set(wiki);
|
|
||||||
|
|
||||||
console.log(report);
|
console.log(report);
|
||||||
console.log(wiki);
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
report
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to mock data when OpenAI is not available
|
// 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",
|
culture: "Collaborative and growth-oriented",
|
||||||
generatedAt: Date.now(),
|
generatedAt: Date.now(),
|
||||||
};
|
};
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
...report
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
...report,
|
|
||||||
...wiki,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Generate company wiki error:", error);
|
console.error("Generate company wiki error:", error);
|
||||||
res.status(500).json({ error: "Failed to generate company wiki" });
|
res.status(500).json({ error: "Failed to generate company wiki" });
|
||||||
@@ -1173,7 +1484,7 @@ exports.chat = onRequest({ cors: true }, async (req, res) => {
|
|||||||
if (openai) {
|
if (openai) {
|
||||||
// Use OpenAI for chat responses
|
// Use OpenAI for chat responses
|
||||||
const systemPrompt = `
|
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.
|
You provide thoughtful, professional advice based on the employee context and company data provided.
|
||||||
|
|
||||||
${context ? `
|
${context ? `
|
||||||
@@ -1186,7 +1497,14 @@ Mentioned Employees:
|
|||||||
${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')}
|
${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();
|
`.trim();
|
||||||
|
|
||||||
// Build the user message content
|
// Build the user message content
|
||||||
@@ -1332,17 +1650,18 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
|||||||
|
|
||||||
const userData = userDoc.data();
|
const userData = userDoc.data();
|
||||||
|
|
||||||
// Add user as owner to organization's employees collection
|
// Add owner info to organization document (owners are NOT employees)
|
||||||
const employeeRef = orgRef.collection("employees").doc(authContext.userId);
|
const ownerInfo = {
|
||||||
await employeeRef.set({
|
|
||||||
id: authContext.userId,
|
id: authContext.userId,
|
||||||
role: "owner",
|
|
||||||
isOwner: true,
|
|
||||||
joinedAt: Date.now(),
|
|
||||||
status: "active",
|
|
||||||
name: userData.displayName || userData.email.split("@")[0],
|
name: userData.displayName || userData.email.split("@")[0],
|
||||||
email: userData.email,
|
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)
|
// 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" });
|
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 employeesSnapshot = await db.collection("orgs").doc(orgId).collection("employees").get();
|
||||||
const employees = [];
|
const employees = [];
|
||||||
|
|
||||||
employeesSnapshot.forEach(doc => {
|
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({
|
res.json({
|
||||||
@@ -2305,13 +2628,15 @@ exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all company reports
|
// Get all company reports
|
||||||
const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("fullCompanyReports").get();
|
const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("companyReport").doc("main").get();
|
||||||
const reports = [];
|
|
||||||
|
|
||||||
reportsSnapshot.forEach(doc => {
|
const reportsData = reportsSnapshot.data();
|
||||||
reports.push({ id: doc.id, ...doc.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)
|
// Sort by creation date (newest first)
|
||||||
reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
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);
|
console.error("Delete image error:", error);
|
||||||
res.status(500).json({ error: "Failed to delete image" });
|
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 {
|
@keyframes blinkLightGreen {
|
||||||
|
|
||||||
0%,
|
0% {
|
||||||
33% {
|
box-shadow: inset 1px 0px 3px -1px rgba(255, 255, 255, 0.2),
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
inset -1px 0px 3px 1px rgba(0, 0, 0, 0.15),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
0px 0px 2px 1px #32ff6f67;
|
||||||
0px 0px 0px 2px #a5ffc075;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
33%,
|
50% {
|
||||||
66% {
|
box-shadow: inset -1px 0px 3px 1px rgba(255, 255, 255, 0.2),
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
inset 1px 0px 3px -1px rgba(0, 0, 0, 0.15);
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 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 {
|
@keyframes blinkLightYellow {
|
||||||
|
|
||||||
0%,
|
0% {
|
||||||
33% {
|
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||||
0px 0px 0px 2px #f7f3c275;
|
0px 0px 2px 1px #ffef3c6c;
|
||||||
}
|
}
|
||||||
|
|
||||||
33%,
|
50% {
|
||||||
66% {
|
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15);
|
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 {
|
@keyframes blinkLightBlue {
|
||||||
|
|
||||||
0%,
|
0% {
|
||||||
33% {
|
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||||
0px 0px 0px 2px #a5d8ff75;
|
0px 0px 2px 1px #39a2f362;
|
||||||
}
|
}
|
||||||
|
|
||||||
33%,
|
50% {
|
||||||
66% {
|
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15);
|
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 {
|
@keyframes blinkLightRed {
|
||||||
|
|
||||||
0%,
|
0% {
|
||||||
33% {
|
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||||
0px 0px 0px 2px #f63d6875;
|
0px 0px 2px 1px #ff2d5e63;
|
||||||
}
|
}
|
||||||
|
|
||||||
33%,
|
50% {
|
||||||
66% {
|
|
||||||
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15);
|
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,
|
.blinkLightBlue,
|
||||||
@@ -93,15 +108,15 @@
|
|||||||
animation-name: blinkLightBlue;
|
animation-name: blinkLightBlue;
|
||||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
||||||
0px 0px 0px 2px #a5d8ff75;
|
0px 0px 2px 1px #39a2f32d;
|
||||||
border: solid 1px #54c2e456;
|
border: solid 1px #39a2f362;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blinkLightGreen {
|
.blinkLightGreen {
|
||||||
animation-name: blinkLightGreen;
|
animation-name: blinkLightGreen;
|
||||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
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);
|
border: solid 1px rgba(187, 248, 185, 0.31);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +124,7 @@
|
|||||||
animation-name: blinkLightRed;
|
animation-name: blinkLightRed;
|
||||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
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;
|
border: solid 1px #e4547656;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +132,7 @@
|
|||||||
animation-name: blinkLightYellow;
|
animation-name: blinkLightYellow;
|
||||||
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.20),
|
||||||
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.15),
|
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;
|
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 { HashRouter, Routes, Route, Navigate, useParams } from 'react-router-dom';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
import { UserOrganizationsProvider, useUserOrganizations } from './contexts/UserOrganizationsContext';
|
import { UserOrganizationsProvider, useUserOrganizations } from './contexts/UserOrganizationsContext';
|
||||||
import { OrgProvider, useOrg } from './contexts/OrgContext';
|
import { OrgProvider, useOrg } from './contexts/OrgContext';
|
||||||
import { Layout } from './components/UiKit';
|
|
||||||
import CompanyWiki from './pages/CompanyWiki';
|
// Lazy load all page components for better performance
|
||||||
// import Report from '../deprecated/pages/EmployeeData';
|
const Layout = React.lazy(() => import('./components/UiKit').then(module => ({ default: module.Layout })));
|
||||||
import Reports from './pages/Reports';
|
const CompanyWiki = React.lazy(() => import('./pages/CompanyWiki'));
|
||||||
import Submissions from './pages/Submissions';
|
const Reports = React.lazy(() => import('./pages/Reports'));
|
||||||
import Chat from './pages/Chat';
|
const Submissions = React.lazy(() => import('./pages/Submissions'));
|
||||||
import HelpNew from './pages/HelpNew';
|
const Chat = React.lazy(() => import('./pages/Chat'));
|
||||||
import SettingsNew from './pages/SettingsNew';
|
const HelpNew = React.lazy(() => import('./pages/HelpNew'));
|
||||||
import HelpAndSettings from './pages/HelpAndSettings';
|
const SettingsNew = React.lazy(() => import('./pages/SettingsNew'));
|
||||||
import ModernLogin from './pages/Login';
|
const ModernLogin = React.lazy(() => import('./pages/Login'));
|
||||||
import OrgSelection from './pages/OrgSelection';
|
const OrgSelection = React.lazy(() => import('./pages/OrgSelection'));
|
||||||
import Onboarding from './pages/Onboarding';
|
const Onboarding = React.lazy(() => import('./pages/Onboarding'));
|
||||||
import EmployeeQuestionnaire from './pages/EmployeeQuestionnaire';
|
const EmployeeQuestionnaire = React.lazy(() => import('./pages/EmployeeQuestionnaire'));
|
||||||
import EmployeeQuestionnaireNew from './pages/EmployeeQuestionnaireNew';
|
const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaireNew'));
|
||||||
import EmployeeQuestionnaireSteps from './pages/EmployeeQuestionnaireSteps';
|
const EmployeeQuestionnaireSteps = React.lazy(() => import('./pages/EmployeeQuestionnaireSteps'));
|
||||||
import QuestionTypesDemo from './pages/QuestionTypesDemo';
|
const QuestionTypesDemo = React.lazy(() => import('./pages/QuestionTypesDemo'));
|
||||||
import FormsDashboard from './pages/FormsDashboard';
|
const FormsDashboard = React.lazy(() => import('./pages/FormsDashboard'));
|
||||||
import QuestionnaireComplete from './pages/QuestionnaireComplete';
|
const QuestionnaireComplete = React.lazy(() => import('./pages/QuestionnaireComplete'));
|
||||||
import SubscriptionSetup from './pages/SubscriptionSetup';
|
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 RequireAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
@@ -107,23 +121,23 @@ function App() {
|
|||||||
<UserOrganizationsProvider>
|
<UserOrganizationsProvider>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<ModernLogin />} />
|
<Route path="/login" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
|
||||||
<Route path="/login/:inviteCode" element={<ModernLogin />} />
|
<Route path="/login/:inviteCode" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
|
||||||
{/* <Route path="/invite/:inviteCode" element={<InviteRedirect />} /> */}
|
{/* <Route path="/invite/:inviteCode" element={<InviteRedirect />} /> */}
|
||||||
|
|
||||||
{/* Employee questionnaire - no auth needed, uses invite code */}
|
{/* Employee questionnaire - no auth needed, uses invite code */}
|
||||||
<Route path="/employee-form/:inviteCode" element={<EmployeeQuestionnaireNew />} />
|
<Route path="/employee-form/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
|
||||||
<Route path="/questionnaire/:inviteCode" element={<EmployeeQuestionnaireNew />} />
|
<Route path="/questionnaire/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
|
||||||
|
|
||||||
{/* Legacy employee questionnaire route for backwards compatibility */}
|
{/* 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 */}
|
{/* Organization Selection - after auth, before entering app */}
|
||||||
<Route
|
<Route
|
||||||
path="/org-selection"
|
path="/org-selection"
|
||||||
element={
|
element={
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<OrgSelection />
|
<SuspenseWrapper><OrgSelection /></SuspenseWrapper>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -133,7 +147,7 @@ function App() {
|
|||||||
path="/subscription-setup"
|
path="/subscription-setup"
|
||||||
element={
|
element={
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<SubscriptionSetup />
|
<SuspenseWrapper><SubscriptionSetup /></SuspenseWrapper>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -145,7 +159,7 @@ function App() {
|
|||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<EmployeeQuestionnaireNew />
|
<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
@@ -159,7 +173,7 @@ function App() {
|
|||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<EmployeeQuestionnaire />
|
<SuspenseWrapper><EmployeeQuestionnaire /></SuspenseWrapper>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
@@ -172,7 +186,7 @@ function App() {
|
|||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<EmployeeQuestionnaireSteps />
|
<SuspenseWrapper><EmployeeQuestionnaireSteps /></SuspenseWrapper>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
@@ -185,14 +199,14 @@ function App() {
|
|||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<Onboarding />
|
<SuspenseWrapper><Onboarding /></SuspenseWrapper>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/questionnaire-complete" element={<QuestionnaireComplete />} />
|
<Route path="/questionnaire-complete" element={<SuspenseWrapper><QuestionnaireComplete /></SuspenseWrapper>} />
|
||||||
|
|
||||||
{/* New Figma Chat Implementation - Standalone route */}
|
{/* New Figma Chat Implementation - Standalone route */}
|
||||||
<Route
|
<Route
|
||||||
@@ -202,7 +216,7 @@ function App() {
|
|||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<RequireOnboarding>
|
<RequireOnboarding>
|
||||||
<Chat />
|
<SuspenseWrapper><Chat /></SuspenseWrapper>
|
||||||
</RequireOnboarding>
|
</RequireOnboarding>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
@@ -212,13 +226,13 @@ function App() {
|
|||||||
|
|
||||||
{/* New Figma Help Implementation - Standalone route */}
|
{/* New Figma Help Implementation - Standalone route */}
|
||||||
<Route
|
<Route
|
||||||
path="/help-new"
|
path="/help"
|
||||||
element={
|
element={
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<RequireOnboarding>
|
<RequireOnboarding>
|
||||||
<HelpNew />
|
<SuspenseWrapper><HelpNew /></SuspenseWrapper>
|
||||||
</RequireOnboarding>
|
</RequireOnboarding>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
@@ -228,13 +242,13 @@ function App() {
|
|||||||
|
|
||||||
{/* New Figma Settings Implementation - Standalone route */}
|
{/* New Figma Settings Implementation - Standalone route */}
|
||||||
<Route
|
<Route
|
||||||
path="/settings-new"
|
path="/settings"
|
||||||
element={
|
element={
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<RequireOnboarding>
|
<RequireOnboarding>
|
||||||
<SettingsNew />
|
<SuspenseWrapper><SettingsNew /></SuspenseWrapper>
|
||||||
</RequireOnboarding>
|
</RequireOnboarding>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
@@ -249,7 +263,7 @@ function App() {
|
|||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<RequireOnboarding>
|
<RequireOnboarding>
|
||||||
<Layout />
|
<SuspenseWrapper><Layout /></SuspenseWrapper>
|
||||||
</RequireOnboarding>
|
</RequireOnboarding>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
@@ -257,11 +271,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route path="/" element={<Navigate to="/reports" replace />} />
|
<Route path="/" element={<Navigate to="/reports" replace />} />
|
||||||
<Route path="/company-wiki" element={<CompanyWiki />} />
|
<Route path="/company-wiki" element={<SuspenseWrapper><CompanyWiki /></SuspenseWrapper>} />
|
||||||
<Route path="/submissions" element={<Submissions />} />
|
<Route path="/submissions" element={<SuspenseWrapper><Submissions /></SuspenseWrapper>} />
|
||||||
<Route path="/reports" element={<Reports />} />
|
<Route path="/reports" element={<SuspenseWrapper><Reports /></SuspenseWrapper>} />
|
||||||
<Route path="/help" element={<HelpAndSettings />} />
|
{/* <Route path="/help" element={<SuspenseWrapper><HelpNew /></SuspenseWrapper>} />
|
||||||
<Route path="/settings" element={<HelpAndSettings />} />
|
<Route path="/settings" element={<SuspenseWrapper><SettingsNew /></SuspenseWrapper>} /> */}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Debug routes */}
|
{/* Debug routes */}
|
||||||
@@ -271,7 +285,7 @@ function App() {
|
|||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<QuestionTypesDemo />
|
<SuspenseWrapper><QuestionTypesDemo /></SuspenseWrapper>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
@@ -283,7 +297,7 @@ function App() {
|
|||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
<OrgProviderWrapper>
|
<OrgProviderWrapper>
|
||||||
<FormsDashboard />
|
<SuspenseWrapper><FormsDashboard /></SuspenseWrapper>
|
||||||
</OrgProviderWrapper>
|
</OrgProviderWrapper>
|
||||||
</RequireOrgSelection>
|
</RequireOrgSelection>
|
||||||
</RequireAuth>
|
</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 }) => {
|
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 = {
|
const variantClasses = {
|
||||||
primary: 'bg-[--accent] text-[--accent-text] hover:bg-[--accent-hover] focus:ring-[--accent]',
|
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] border border-[--border-color]',
|
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',
|
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 = {
|
const sizeClasses = {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection
|
|||||||
sectionName
|
sectionName
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
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">
|
<div className="p-4 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||||
{Array.from({ length: 7 }, (_, index) => {
|
{Array.from({ length: 7 }, (_, index) => {
|
||||||
const isActive = index === currentSection - 1;
|
const isActive = index === currentSection - 1;
|
||||||
@@ -70,11 +70,11 @@ export const SectionProgressBar: React.FC<{ currentSection: number; totalSection
|
|||||||
<div key={index}>
|
<div key={index}>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<svg width="24" height="4" viewBox="0 0 24 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/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>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +139,7 @@ export const SectionIntro: React.FC<{
|
|||||||
description: string;
|
description: string;
|
||||||
onStart: () => void;
|
onStart: () => void;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
}> = ({ sectionNumber, title, description, onStart, imageUrl = "https://placehold.co/560x682" }) => {
|
}> = ({ sectionNumber, title, description, onStart, imageUrl = "/image/onboarding-robot.png" }) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full self-stretch bg-[--Neutrals-NeutralSlate0] 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 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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onStart}
|
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="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
<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>
|
</button>
|
||||||
</div>
|
</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 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">
|
<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} />
|
<img className="self-stretch flex-1" src={imageUrl} alt={title} />
|
||||||
</div>
|
</div>
|
||||||
@@ -286,8 +286,8 @@ export const TextAreaQuestion: React.FC<{
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, placeholder = "Type your answer...." }) => {
|
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, placeholder = "Type your answer...." }) => {
|
||||||
return (
|
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 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 justify-start items-start gap-12">
|
<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 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">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
@@ -341,7 +341,7 @@ export const TextAreaQuestion: React.FC<{
|
|||||||
{onSkip && (
|
{onSkip && (
|
||||||
<button
|
<button
|
||||||
onClick={onSkip}
|
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>
|
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -375,13 +375,13 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
scale?: number;
|
scale?: number;
|
||||||
}> = ({ question, leftLabel, rightLabel, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, scale = 10 }) => {
|
}> = ({ question, leftLabel, rightLabel, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName, scale = 10 }) => {
|
||||||
return (
|
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 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 justify-start items-start gap-12">
|
<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 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">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
</div>
|
</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">
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">
|
||||||
{leftLabel}
|
{leftLabel}
|
||||||
</div>
|
</div>
|
||||||
@@ -392,10 +392,10 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
<button
|
<button
|
||||||
key={ratingValue}
|
key={ratingValue}
|
||||||
onClick={() => onChange(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}
|
{ratingValue}
|
||||||
</div>
|
</div>
|
||||||
@@ -421,7 +421,7 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!value}
|
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="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<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;
|
sectionName?: string;
|
||||||
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
|
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
|
||||||
return (
|
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 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 justify-start items-start gap-12">
|
<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 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">
|
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||||
{question}
|
{question}
|
||||||
@@ -483,20 +483,20 @@ export const YesNoChoice: React.FC<{
|
|||||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange('No')}
|
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
|
No
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange('Yes')}
|
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
|
Yes
|
||||||
</div>
|
</div>
|
||||||
@@ -517,7 +517,7 @@ export const YesNoChoice: React.FC<{
|
|||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!value}
|
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="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<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
|
// Thank You Page Component
|
||||||
export const ThankYouPage: React.FC = () => {
|
export const ThankYouPage: React.FC = () => {
|
||||||
return (
|
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 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="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">
|
<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>
|
||||||
</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-[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 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">
|
||||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Thank you" />
|
<img className="self-stretch flex-1" src="/image/onboarding-robot.png" alt="Thank you" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Button, PlusIcon, CopyIcon } from '../UiKit';
|
import { Button, PlusIcon, CopyIcon } from '../UiKit';
|
||||||
import { useOrg } from '../../contexts/OrgContext';
|
import { useOrg } from '../../contexts/OrgContext';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { useUserOrganizations } from '../../contexts/UserOrganizationsContext';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
@@ -11,13 +12,48 @@ interface SidebarProps {
|
|||||||
|
|
||||||
export default function Sidebar({ companyName = "Zitlac Media", collapsed = false }: SidebarProps) {
|
export default function Sidebar({ companyName = "Zitlac Media", collapsed = false }: SidebarProps) {
|
||||||
const { org, issueInviteViaApi } = useOrg();
|
const { org, issueInviteViaApi } = useOrg();
|
||||||
|
const { createOrganization, selectOrganization, organizations, refreshOrganizations } = useUserOrganizations();
|
||||||
const { signOutUser } = useAuth();
|
const { signOutUser } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||||
|
const [showOrgDropdown, setShowOrgDropdown] = useState(false);
|
||||||
|
const [showCreateOrgModal, setShowCreateOrgModal] = useState(false);
|
||||||
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
||||||
|
const [createOrgForm, setCreateOrgForm] = useState({ name: '', description: '' });
|
||||||
const [inviteLink, setInviteLink] = useState('');
|
const [inviteLink, setInviteLink] = useState('');
|
||||||
const [emailLink, setEmailLink] = 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 () => {
|
const handleInvite = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -125,67 +161,151 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
const handleNavClick = (path: string) => {
|
const handleNavClick = (path: string) => {
|
||||||
navigate(path);
|
navigate(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<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 */}
|
{/* Header Section */}
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
||||||
{/* Company Selector */}
|
{/* Company Selector Dropdown */}
|
||||||
<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="relative w-60" ref={dropdownRef}>
|
||||||
<div className="flex-1 flex justify-start items-center gap-2">
|
<div
|
||||||
<div className="w-8 h-8 rounded-full flex justify-start items-center gap-2.5">
|
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]"
|
||||||
<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">
|
onClick={() => refreshOrganizations() && setShowOrgDropdown(!showOrgDropdown)}
|
||||||
<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">
|
<div className="flex-1 flex justify-start items-center gap-2">
|
||||||
<rect width="32" height="32" fill="url(#paint0_linear_731_19280)" />
|
<div className="w-8 h-8 rounded-full flex justify-start items-center gap-2.5">
|
||||||
<defs>
|
<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">
|
||||||
<linearGradient id="paint0_linear_731_19280" x1="16" y1="3.97364e-07" x2="17.3333" y2="32" gradientUnits="userSpaceOnUse">
|
<div className="w-8 h-8 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||||
<stop stopColor="white" stopOpacity="0" />
|
<div data-svg-wrapper className="left-[7px] top-[7px] absolute">
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.12" />
|
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
</linearGradient>
|
<g filter="url(#filter0_d_1141_1906)">
|
||||||
</defs>
|
<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)" />
|
||||||
</svg>
|
<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>
|
||||||
<div className="left-[8.80px] top-[7.20px] absolute">
|
</div>
|
||||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<div className="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
|
||||||
<g filter="url(#filter0_d_731_19281)">
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">{org?.name || 'Select Organization'}</div>
|
||||||
<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)" />
|
</div>
|
||||||
<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)" />
|
</div>
|
||||||
</g>
|
<div className={`transition-transform duration-200 ${showOrgDropdown ? 'rotate-180' : ''}`}>
|
||||||
<defs>
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<filter id="filter0_d_731_19281" x="0.398828" y="-0.399988" width="19.2014" height="22.4" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
<path d="M5.83301 7.50001L9.99967 11.6667L14.1663 7.50001" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
</svg>
|
||||||
<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" />
|
</div>
|
||||||
<feMorphology radius="1.2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_731_19281" />
|
</div>
|
||||||
<feOffset dy="1.8" />
|
|
||||||
<feGaussianBlur stdDeviation="1.8" />
|
{/* Dropdown Menu */}
|
||||||
<feComposite in2="hardAlpha" operator="out" />
|
{showOrgDropdown && (
|
||||||
<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" />
|
<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">
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_731_19281" />
|
<div className="p-2">
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_731_19281" result="shape" />
|
{/* Current Organizations */}
|
||||||
</filter>
|
{organizations.map((organization) => (
|
||||||
<linearGradient id="paint0_linear_731_19281" x1="3.744" y1="10.4393" x2="3.744" y2="12.1498" gradientUnits="userSpaceOnUse">
|
<div
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
key={organization.orgId}
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
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]' : ''
|
||||||
</linearGradient>
|
}`}
|
||||||
<linearGradient id="paint1_linear_731_19281" x1="6.52467" y1="10.6932" x2="6.52467" y2="14.7239" gradientUnits="userSpaceOnUse">
|
onClick={() => handleOrgSwitch(organization.orgId)}
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
>
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
<div className="w-6 h-6 relative bg-[--Brand-Orange] rounded-full flex items-center justify-center text-white text-xs font-medium">
|
||||||
</linearGradient>
|
{organization.name.charAt(0).toUpperCase()}
|
||||||
</defs>
|
</div>
|
||||||
</svg>
|
<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>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Items */}
|
{/* Navigation Items */}
|
||||||
@@ -202,7 +322,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{React.cloneElement(item.icon, {
|
{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>
|
||||||
<div className={`justify-start text-sm font-medium font-['Inter'] leading-tight ${item.active
|
<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>
|
||||||
</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 && (
|
{showInviteModal && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<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">
|
<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 */}
|
{/* Settings */}
|
||||||
<div
|
<div
|
||||||
onClick={() => handleNavClick("/settings")}
|
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">
|
<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>
|
||||||
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Settings</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Build Report Card */}
|
{/* Build Report Card */}
|
||||||
@@ -360,7 +541,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
|
<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 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>
|
||||||
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
<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 React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||||
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
|
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
|
||||||
import { API_URL } from '../constants';
|
import { API_URL } from '../constants';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
signInWithGoogle: () => Promise<void>;
|
signInWithGoogle: () => Promise<void>;
|
||||||
signOutUser: () => Promise<void>;
|
signOutUser: () => Promise<void>;
|
||||||
signInWithEmail: (email: string, password: string) => Promise<void>;
|
signInWithEmail: (email: string, password: string) => Promise<void>;
|
||||||
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<void>;
|
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<void>;
|
||||||
sendOTP: (email: string, inviteCode?: string) => Promise<any>;
|
sendOTP: (email: string, inviteCode?: string) => Promise<any>;
|
||||||
verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise<void>;
|
verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
||||||
|
|
||||||
if (isFirebaseConfigured) {
|
if (isFirebaseConfigured) {
|
||||||
// Firebase mode: Set up proper Firebase auth state listener
|
// Firebase mode: Set up proper Firebase auth state listener
|
||||||
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
|
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
|
||||||
console.log('Firebase auth state changed:', firebaseUser?.email);
|
console.log('Firebase auth state changed:', firebaseUser?.email);
|
||||||
if (firebaseUser) {
|
if (firebaseUser) {
|
||||||
setUser(firebaseUser);
|
setUser(firebaseUser);
|
||||||
} else {
|
} else {
|
||||||
// Check for OTP session as fallback
|
// Check for OTP session as fallback
|
||||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||||
if (sessionUser) {
|
if (sessionUser) {
|
||||||
try {
|
try {
|
||||||
const parsedUser = JSON.parse(sessionUser);
|
const parsedUser = JSON.parse(sessionUser);
|
||||||
console.log('Restoring OTP session for:', parsedUser.email);
|
console.log('Restoring OTP session for:', parsedUser.email);
|
||||||
setUser(parsedUser as User);
|
setUser(parsedUser as User);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse session user:', error);
|
console.error('Failed to parse session user:', error);
|
||||||
localStorage.removeItem('auditly_demo_session');
|
localStorage.removeItem('auditly_demo_session');
|
||||||
setUser(null);
|
setUser(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
} else {
|
} else {
|
||||||
// Demo/OTP mode: Check localStorage for persisted session
|
// Demo/OTP mode: Check localStorage for persisted session
|
||||||
console.log('Checking for persisted OTP session');
|
console.log('Checking for persisted OTP session');
|
||||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||||
if (sessionUser) {
|
if (sessionUser) {
|
||||||
try {
|
try {
|
||||||
const parsedUser = JSON.parse(sessionUser);
|
const parsedUser = JSON.parse(sessionUser);
|
||||||
console.log('Restoring session for:', parsedUser.email);
|
console.log('Restoring session for:', parsedUser.email);
|
||||||
setUser(parsedUser as User);
|
setUser(parsedUser as User);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse session user:', error);
|
console.error('Failed to parse session user:', error);
|
||||||
localStorage.removeItem('auditly_demo_session');
|
localStorage.removeItem('auditly_demo_session');
|
||||||
setUser(null);
|
setUser(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
return () => { };
|
return () => { };
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const signInWithGoogle = async () => {
|
const signInWithGoogle = useCallback(async () => {
|
||||||
if (!isFirebaseConfigured) {
|
if (!isFirebaseConfigured) {
|
||||||
// No-op in demo mode
|
// No-op in demo mode
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await signInWithPopup(auth, googleProvider);
|
await signInWithPopup(auth, googleProvider);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const signOutUser = async () => {
|
const signOutUser = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// Sign out from Firebase if configured and user is signed in via Firebase
|
// Sign out from Firebase if configured and user is signed in via Firebase
|
||||||
if (isFirebaseConfigured && auth.currentUser) {
|
if (isFirebaseConfigured && auth.currentUser) {
|
||||||
await signOut(auth);
|
await signOut(auth);
|
||||||
console.log('Firebase signout completed');
|
console.log('Firebase signout completed');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Firebase signout error:', error);
|
console.error('Firebase signout error:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always clear all local session data
|
// Always clear all local session data
|
||||||
localStorage.removeItem('auditly_demo_session');
|
localStorage.removeItem('auditly_demo_session');
|
||||||
localStorage.removeItem('auditly_auth_token');
|
localStorage.removeItem('auditly_auth_token');
|
||||||
localStorage.removeItem('auditly_selected_org');
|
localStorage.removeItem('auditly_selected_org');
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
|
||||||
setUser(null);
|
setUser(null);
|
||||||
console.log('User signed out and all sessions cleared');
|
console.log('User signed out and all sessions cleared');
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const signInWithEmail = async (email: string, password: string) => {
|
const signInWithEmail = useCallback(async (email: string, password: string) => {
|
||||||
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
|
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
|
||||||
try {
|
try {
|
||||||
console.log('Attempting Firebase auth');
|
console.log('Attempting Firebase auth');
|
||||||
await signInWithEmailAndPassword(auth, email, password);
|
await signInWithEmailAndPassword(auth, email, password);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const code = e?.code || '';
|
const code = e?.code || '';
|
||||||
console.error('Firebase Auth Error:', code, e?.message);
|
console.error('Firebase Auth Error:', code, e?.message);
|
||||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
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.');
|
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;
|
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: email.split('@')[0] } as unknown as User;
|
||||||
setUser(mock);
|
setUser(mock);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
|
const signUpWithEmail = useCallback(async (email: string, password: string, displayName?: string) => {
|
||||||
try {
|
try {
|
||||||
const cred = await createUserWithEmailAndPassword(auth, email, password);
|
const cred = await createUserWithEmailAndPassword(auth, email, password);
|
||||||
if (displayName) {
|
if (displayName) {
|
||||||
try { await updateProfile(cred.user, { displayName }); } catch { }
|
try { await updateProfile(cred.user, { displayName }); } catch { }
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const code = e?.code || '';
|
const code = e?.code || '';
|
||||||
if (code === 'auth/configuration-not-found' || code === 'auth/operation-not-allowed') {
|
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.');
|
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;
|
const mock = { uid: `demo-${btoa(email).slice(0, 8)}`, email, displayName: displayName || email.split('@')[0] } as unknown as User;
|
||||||
setUser(mock);
|
setUser(mock);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const sendOTP = async (email: string, inviteCode?: string) => {
|
const sendOTP = useCallback(async (email: string, inviteCode?: string) => {
|
||||||
const response = await fetch(`${API_URL}/sendOTP`, {
|
const response = await fetch(`${API_URL}/sendOTP`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email, inviteCode })
|
body: JSON.stringify({ email, inviteCode })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.error || 'Failed to send OTP');
|
throw new Error(error.error || 'Failed to send OTP');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const verifyOTP = async (email: string, otp: string, inviteCode?: string) => {
|
const verifyOTP = useCallback(async (email: string, otp: string, inviteCode?: string) => {
|
||||||
const response = await fetch(`${API_URL}/verifyOTP`, {
|
const response = await fetch(`${API_URL}/verifyOTP`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email, otp, inviteCode })
|
body: JSON.stringify({ email, otp, inviteCode })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.error || 'Failed to verify OTP');
|
throw new Error(error.error || 'Failed to verify OTP');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Set user in auth context
|
// Set user in auth context
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
uid: data.user.uid,
|
uid: data.user.uid,
|
||||||
email: data.user.email,
|
email: data.user.email,
|
||||||
displayName: data.user.displayName,
|
displayName: data.user.displayName,
|
||||||
emailVerified: true
|
emailVerified: true
|
||||||
} as unknown as User;
|
} as unknown as User;
|
||||||
|
|
||||||
setUser(mockUser);
|
setUser(mockUser);
|
||||||
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
|
||||||
localStorage.setItem('auditly_auth_token', data.token);
|
localStorage.setItem('auditly_auth_token', data.token);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
// Memoize the context value to prevent unnecessary re-renders
|
||||||
return (
|
const contextValue = useMemo(() => ({
|
||||||
<AuthContext.Provider value={{
|
user,
|
||||||
user,
|
loading,
|
||||||
loading,
|
signInWithGoogle,
|
||||||
signInWithGoogle,
|
signOutUser,
|
||||||
signOutUser,
|
signInWithEmail,
|
||||||
signInWithEmail,
|
signUpWithEmail,
|
||||||
signUpWithEmail,
|
sendOTP,
|
||||||
sendOTP,
|
verifyOTP,
|
||||||
verifyOTP,
|
}), [user, loading, signInWithGoogle, signOutUser, signInWithEmail, signUpWithEmail, sendOTP, verifyOTP]);
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
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;
|
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 { useAuth } from './AuthContext';
|
||||||
import { Employee, EmployeeReport, Submission, CompanyReport } from '../types';
|
import { Employee, EmployeeReport, Submission, CompanyReport } from '../types';
|
||||||
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
||||||
@@ -9,11 +9,19 @@ import { secureApi } from '../services/secureApi';
|
|||||||
|
|
||||||
interface OrgData {
|
interface OrgData {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
name?: string;
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
onboardingData?: Record<string, any>;
|
onboardingData?: Record<string, any>;
|
||||||
companyLogo?: string;
|
companyLogo?: string;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
onboardingCompleted?: boolean;
|
onboardingCompleted?: boolean;
|
||||||
|
ownerId?: string;
|
||||||
|
ownerInfo?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
joinedAt: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OrgContextType {
|
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
|
// Use the provided selectedOrgId instead of deriving from user
|
||||||
const orgId = selectedOrgId;
|
const orgId = selectedOrgId;
|
||||||
|
|
||||||
// Load initial data using secure API
|
// Load initial data using secure API - memoized to prevent unnecessary re-runs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!orgId || !user?.uid) {
|
if (!orgId || !user?.uid) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -76,54 +84,57 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Load organization data
|
// Batch all API calls for better performance
|
||||||
try {
|
const [orgData, employeesData, submissionsData, reportsData, companyReportsData] = await Promise.allSettled([
|
||||||
const orgData = await secureApi.getOrgData();
|
secureApi.getOrgData().catch(() => null),
|
||||||
setOrg({ orgId, ...orgData });
|
secureApi.getEmployees().catch(() => []),
|
||||||
} catch (error) {
|
secureApi.getSubmissions().catch(() => ({})),
|
||||||
console.warn('Could not load org data, creating default:', error);
|
secureApi.getReports().catch(() => ({})),
|
||||||
// Create default org if not found
|
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 };
|
const defaultOrg = { name: 'Your Company', onboardingCompleted: false };
|
||||||
await secureApi.updateOrgData(defaultOrg);
|
await secureApi.updateOrgData(defaultOrg);
|
||||||
setOrg({ orgId, ...defaultOrg });
|
setOrg({ orgId, ...defaultOrg });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load employees
|
// Process employees data
|
||||||
try {
|
if (employeesData.status === 'fulfilled') {
|
||||||
const employeesData = await secureApi.getEmployees();
|
setEmployees(employeesData.value.map(emp => ({
|
||||||
setEmployees(employeesData.map(emp => ({
|
|
||||||
...emp,
|
...emp,
|
||||||
initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email?.substring(0, 2).toUpperCase() || 'U'
|
initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email?.substring(0, 2).toUpperCase() || 'U'
|
||||||
})));
|
})));
|
||||||
} catch (error) {
|
} else {
|
||||||
console.warn('Could not load employees:', error);
|
console.warn('Could not load employees');
|
||||||
setEmployees([]);
|
setEmployees([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load submissions
|
// Process submissions data
|
||||||
try {
|
if (submissionsData.status === 'fulfilled') {
|
||||||
const submissionsData = await secureApi.getSubmissions();
|
setSubmissions(submissionsData.value);
|
||||||
setSubmissions(submissionsData);
|
} else {
|
||||||
} catch (error) {
|
console.warn('Could not load submissions');
|
||||||
console.warn('Could not load submissions:', error);
|
|
||||||
setSubmissions({});
|
setSubmissions({});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reports
|
// Process reports data
|
||||||
try {
|
if (reportsData.status === 'fulfilled') {
|
||||||
const reportsData = await secureApi.getReports();
|
setReports(reportsData.value as Record<string, EmployeeReport>);
|
||||||
setReports(reportsData as Record<string, EmployeeReport>);
|
} else {
|
||||||
} catch (error) {
|
console.warn('Could not load reports');
|
||||||
console.warn('Could not load reports:', error);
|
|
||||||
setReports({});
|
setReports({});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load company reports
|
// Process company reports data
|
||||||
try {
|
if (companyReportsData.status === 'fulfilled') {
|
||||||
const companyReportsData = await secureApi.getCompanyReports();
|
setFullCompanyReports(companyReportsData.value);
|
||||||
setFullCompanyReports(companyReportsData);
|
} else {
|
||||||
} catch (error) {
|
console.warn('Could not load company reports');
|
||||||
console.warn('Could not load company reports:', error);
|
|
||||||
setFullCompanyReports([]);
|
setFullCompanyReports([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,9 +146,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadOrgData();
|
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) {
|
if (!user?.uid) {
|
||||||
throw new Error('User authentication required');
|
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);
|
console.error('Failed to update organization:', error);
|
||||||
throw 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) {
|
if (!user?.uid) {
|
||||||
throw new Error('User authentication required');
|
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);
|
console.error('Failed to save report:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}, [user?.uid]);
|
||||||
|
|
||||||
const inviteEmployee = async ({ name, email, role, department }: { name: string; email: string, role?: string, department?: string }) => {
|
const inviteEmployee = async ({ name, email, role, department }: { name: string; email: string, role?: string, department?: string }) => {
|
||||||
console.log('inviteEmployee called:', { name, email, orgId });
|
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(),
|
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,
|
department: data.employee.department,
|
||||||
role: data.employee.role,
|
role: data.employee.role,
|
||||||
isOwner: false,
|
|
||||||
status: data.employee.status
|
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)
|
// Calculate concrete metrics from actual data (no AI needed)
|
||||||
// Exclude owners from employee counts - they are company wiki contributors, not employees
|
// Employees collection only contains actual employees (owners are not in this collection)
|
||||||
const actualEmployees = employees.filter(emp => !emp.isOwner);
|
const actualEmployees = employees;
|
||||||
const totalEmployees = actualEmployees.length;
|
const totalEmployees = actualEmployees.length;
|
||||||
|
|
||||||
// Only count submissions from non-owner employees
|
// Count submissions from employees
|
||||||
const employeeSubmissions = Object.fromEntries(
|
const employeeSubmissions = submissions;
|
||||||
Object.entries(submissions).filter(([employeeId]) => {
|
|
||||||
const employee = employees.find(emp => emp.id === employeeId);
|
|
||||||
return employee && !employee.isOwner;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const submittedEmployees = Object.keys(employeeSubmissions).length;
|
const submittedEmployees = Object.keys(employeeSubmissions).length;
|
||||||
const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0;
|
const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0;
|
||||||
|
|
||||||
@@ -356,7 +361,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use secure API for AI generation
|
// Use secure API for AI generation
|
||||||
const data = await secureApi.generateCompanyWiki({
|
let response = await secureApi.generateCompanyWiki({
|
||||||
...org,
|
...org,
|
||||||
metrics: {
|
metrics: {
|
||||||
totalEmployees,
|
totalEmployees,
|
||||||
@@ -368,25 +373,25 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
console.log('Company insights generated via AI successfully');
|
console.log('Company insights generated via AI successfully');
|
||||||
|
|
||||||
// Combine concrete metrics with AI insights
|
// Combine concrete metrics with AI insights
|
||||||
const report: CompanyReport = {
|
let report: CompanyReport = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
// Use AI-generated insights for subjective analysis
|
// Use AI-generated insights for subjective analysis
|
||||||
...(data as any),
|
|
||||||
// Override with our concrete metrics
|
// Override with our concrete metrics
|
||||||
overview: {
|
overview: {
|
||||||
totalEmployees,
|
totalEmployees,
|
||||||
departmentBreakdown,
|
departmentBreakdown,
|
||||||
submissionRate,
|
submissionRate,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
averagePerformanceScore: (data as any)?.overview?.averagePerformanceScore || 0,
|
averagePerformanceScore: (response as any)?.overview?.averagePerformanceScore || 0,
|
||||||
riskLevel: (data as any)?.overview?.riskLevel || 'Unknown'
|
riskLevel: (response as any)?.overview?.riskLevel || 'Unknown'
|
||||||
}
|
},
|
||||||
|
...(response as any)
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Final company report object:', report);
|
console.log('Final company report object:', report);
|
||||||
await saveFullCompanyReport(report);
|
// await saveFullCompanyReport(report);
|
||||||
return report;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('generateCompanyReport error:', error);
|
console.error('generateCompanyReport error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -408,12 +413,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
// Use secure API for wiki generation
|
// Use secure API for wiki generation
|
||||||
try {
|
try {
|
||||||
console.log('Making API call to generateCompanyWiki...');
|
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
|
// Ensure the report has all required fields to prevent undefined errors
|
||||||
const data: CompanyReport = {
|
const report: CompanyReport = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
overview: {
|
overview: {
|
||||||
@@ -430,21 +435,22 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] },
|
forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] },
|
||||||
executiveSummary: 'Company report generated successfully.',
|
executiveSummary: 'Company report generated successfully.',
|
||||||
// Override with API data if available
|
// Override with API data if available
|
||||||
...(payload as any || {})
|
...(response as any || {})
|
||||||
};
|
};
|
||||||
|
|
||||||
await saveFullCompanyReport(data);
|
// await saveFullCompanyReport(data);
|
||||||
return data;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('generateCompanyWiki error, falling back to local synthetic:', e);
|
console.error('generateCompanyWiki error, falling back to local synthetic:', e);
|
||||||
return generateCompanyReport();
|
return generateCompanyReport();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isOwner = (employeeId?: string): boolean => {
|
const isOwner = (userId?: string): boolean => {
|
||||||
const currentEmployee = employeeId ? employees.find(e => e.id === employeeId) :
|
// Check if the given user ID matches the org owner ID
|
||||||
employees.find(e => e.email === user?.email);
|
// If no userId provided, check current user
|
||||||
return currentEmployee?.isOwner === true;
|
const targetUserId = userId || user?.uid;
|
||||||
|
return targetUserId === org?.ownerId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEmployeeReport = async (employeeId: string) => {
|
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,
|
org,
|
||||||
orgId,
|
orgId,
|
||||||
employees,
|
employees,
|
||||||
@@ -504,136 +644,42 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
generateCompanyWiki,
|
generateCompanyWiki,
|
||||||
seedInitialData,
|
seedInitialData,
|
||||||
isOwner,
|
isOwner,
|
||||||
issueInviteViaApi: async ({ name, email, role, department }) => {
|
issueInviteViaApi,
|
||||||
try {
|
getInviteStatus,
|
||||||
if (!user?.uid) {
|
consumeInvite,
|
||||||
throw new Error('User authentication required');
|
submitEmployeeAnswers,
|
||||||
}
|
generateEmployeeReport,
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getEmployeeReport,
|
getEmployeeReport,
|
||||||
getEmployeeReports,
|
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 (
|
return (
|
||||||
<OrgContext.Provider value={value}>
|
<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 { useAuth } from './AuthContext';
|
||||||
import { isFirebaseConfigured } from '../services/firebase';
|
import { isFirebaseConfigured } from '../services/firebase';
|
||||||
import { API_URL } from '../constants';
|
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 [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// Load user's organizations
|
// Load user's organizations - memoized to prevent recreation
|
||||||
const loadOrganizations = async () => {
|
const loadOrganizations = useCallback(async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setOrganizations([]);
|
setOrganizations([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -50,7 +50,7 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [user]);
|
||||||
|
|
||||||
// Initialize selected org from localStorage (persistent across sessions)
|
// Initialize selected org from localStorage (persistent across sessions)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,7 +63,7 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
// Load organizations when user changes
|
// Load organizations when user changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadOrganizations();
|
loadOrganizations();
|
||||||
}, [user]);
|
}, [loadOrganizations]);
|
||||||
|
|
||||||
// Listen for organization updates (e.g., onboarding completion)
|
// Listen for organization updates (e.g., onboarding completion)
|
||||||
useEffect(() => {
|
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);
|
console.log('Switching to organization:', orgId);
|
||||||
|
|
||||||
// Clear any cached data when switching organizations for security
|
// 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', {
|
window.dispatchEvent(new CustomEvent('organizationChanged', {
|
||||||
detail: { newOrgId: orgId }
|
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');
|
if (!user) throw new Error('User not authenticated');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -135,53 +135,12 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
console.error('Failed to create organization:', error);
|
console.error('Failed to create organization:', error);
|
||||||
throw 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');
|
if (!user) throw new Error('User not authenticated');
|
||||||
|
|
||||||
try {
|
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
|
// Use secure API for joining organization
|
||||||
const data = await secureApi.joinOrganization(inviteCode);
|
const data = await secureApi.joinOrganization(inviteCode);
|
||||||
|
|
||||||
@@ -195,19 +154,18 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
|
|
||||||
setOrganizations(prev => [...prev, userOrg]);
|
setOrganizations(prev => [...prev, userOrg]);
|
||||||
return data.orgId;
|
return data.orgId;
|
||||||
// }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to join organization:', error);
|
console.error('Failed to join organization:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}, [user]);
|
||||||
|
|
||||||
const refreshOrganizations = async () => {
|
const refreshOrganizations = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await loadOrganizations();
|
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');
|
if (!user) throw new Error('User not authenticated');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -220,9 +178,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
console.error('Failed to create checkout session:', error);
|
console.error('Failed to create checkout session:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}, [user]);
|
||||||
|
|
||||||
const getSubscriptionStatus = async () => {
|
const getSubscriptionStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await secureApi.getSubscriptionStatus();
|
const data = await secureApi.getSubscriptionStatus();
|
||||||
return data;
|
return data;
|
||||||
@@ -230,20 +188,33 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
|
|||||||
console.error('Failed to get subscription status:', error);
|
console.error('Failed to get subscription status:', error);
|
||||||
throw 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 (
|
return (
|
||||||
<UserOrganizationsContext.Provider value={{
|
<UserOrganizationsContext.Provider value={contextValue}>
|
||||||
organizations,
|
|
||||||
selectedOrgId,
|
|
||||||
loading,
|
|
||||||
selectOrganization,
|
|
||||||
createOrganization,
|
|
||||||
joinOrganization,
|
|
||||||
refreshOrganizations,
|
|
||||||
createCheckoutSession,
|
|
||||||
getSubscriptionStatus
|
|
||||||
}}>
|
|
||||||
{children}
|
{children}
|
||||||
</UserOrganizationsContext.Provider>
|
</UserOrganizationsContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -255,4 +226,4 @@ export const useUserOrganizations = () => {
|
|||||||
throw new Error('useUserOrganizations must be used within UserOrganizationsProvider');
|
throw new Error('useUserOrganizations must be used within UserOrganizationsProvider');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Complete 63-step onboarding configuration based on Figma designs
|
* Complete 63-step onboarding configuration based on Figma designs
|
||||||
*/
|
*/
|
||||||
|
import { OnboardingData } from "../types";
|
||||||
|
|
||||||
|
|
||||||
export interface OnboardingStep {
|
export interface OnboardingStep {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -18,15 +20,14 @@ export interface OnboardingStep {
|
|||||||
rows?: number; // for textarea
|
rows?: number; // for textarea
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnboardingData {
|
export interface OnboardingFormData extends OnboardingData {
|
||||||
companyName: string;
|
companyName: string;
|
||||||
yourName: string;
|
yourName: string;
|
||||||
companyLogo: string;
|
companyLogo: string;
|
||||||
[key: string]: string | string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initializeOnboardingData = (): OnboardingData => {
|
export const initializeOnboardingData = (): OnboardingFormData => {
|
||||||
const data: OnboardingData = {
|
const data = {
|
||||||
// Ensure required form fields are initialized
|
// Ensure required form fields are initialized
|
||||||
companyName: '',
|
companyName: '',
|
||||||
yourName: '',
|
yourName: '',
|
||||||
@@ -41,7 +42,7 @@ export const initializeOnboardingData = (): OnboardingData => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data;
|
return data as OnboardingFormData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onboardingSteps: OnboardingStep[] = [
|
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';
|
// DEPRECATED: This component has been split into separate Help and Settings pages
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
// Use /src/pages/HelpNew.tsx and /src/pages/SettingsNew.tsx instead
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
// This file can be safely removed in future cleanup
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
|
||||||
import { Card, Button } from '../components/UiKit';
|
import React, { useState } from 'react';
|
||||||
import { Theme } from '../types';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
const HelpAndSettings: React.FC = () => {
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
const { theme, setTheme } = useTheme();
|
import { Card, Button } from '../components/UiKit';
|
||||||
const { user, signOutUser } = useAuth();
|
import { Theme } from '../types';
|
||||||
const { org, upsertOrg, issueInviteViaApi } = useOrg();
|
|
||||||
const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
|
const HelpAndSettings: React.FC = () => {
|
||||||
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
const { theme, setTheme } = useTheme();
|
||||||
const [isInviting, setIsInviting] = useState(false);
|
const { user, signOutUser } = useAuth();
|
||||||
const [inviteResult, setInviteResult] = useState<string | null>(null);
|
const { org, upsertOrg, issueInviteViaApi } = useOrg();
|
||||||
|
const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
|
||||||
const handleLogout = async () => {
|
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
||||||
try {
|
const [isInviting, setIsInviting] = useState(false);
|
||||||
await signOutUser();
|
const [inviteResult, setInviteResult] = useState<string | null>(null);
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
const handleLogout = async () => {
|
||||||
}
|
try {
|
||||||
};
|
await signOutUser();
|
||||||
|
} catch (error) {
|
||||||
const handleRestartOnboarding = async () => {
|
console.error('Logout error:', error);
|
||||||
try {
|
}
|
||||||
await upsertOrg({ onboardingCompleted: false });
|
};
|
||||||
// The RequireOnboarding component will redirect automatically
|
|
||||||
} catch (error) {
|
const handleRestartOnboarding = async () => {
|
||||||
console.error('Failed to restart onboarding:', error);
|
try {
|
||||||
}
|
await upsertOrg({ onboardingCompleted: false });
|
||||||
};
|
// The RequireOnboarding component will redirect automatically
|
||||||
|
} catch (error) {
|
||||||
const handleInviteEmployee = async () => {
|
console.error('Failed to restart onboarding:', error);
|
||||||
if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
|
}
|
||||||
|
};
|
||||||
setIsInviting(true);
|
|
||||||
setInviteResult(null);
|
const handleInviteEmployee = async () => {
|
||||||
|
if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
|
||||||
try {
|
|
||||||
const result = await issueInviteViaApi({
|
setIsInviting(true);
|
||||||
name: inviteForm.name.trim(),
|
setInviteResult(null);
|
||||||
email: inviteForm.email.trim(),
|
|
||||||
role: inviteForm.role.trim() || undefined,
|
try {
|
||||||
department: inviteForm.department.trim() || undefined
|
const result = await issueInviteViaApi({
|
||||||
});
|
name: inviteForm.name.trim(),
|
||||||
|
email: inviteForm.email.trim(),
|
||||||
setInviteResult(JSON.stringify({
|
role: inviteForm.role.trim() || undefined,
|
||||||
success: true,
|
department: inviteForm.department.trim() || undefined
|
||||||
inviteLink: result.inviteLink,
|
});
|
||||||
emailLink: result.emailLink,
|
|
||||||
employeeName: result.employee.name
|
setInviteResult(JSON.stringify({
|
||||||
}));
|
success: true,
|
||||||
setInviteForm({ name: '', email: '', role: '', department: '' });
|
inviteLink: result.inviteLink,
|
||||||
} catch (error) {
|
emailLink: result.emailLink,
|
||||||
console.error('Failed to send invitation:', error);
|
employeeName: result.employee.name
|
||||||
setInviteResult('Failed to send invitation. Please try again.');
|
}));
|
||||||
} finally {
|
setInviteForm({ name: '', email: '', role: '', department: '' });
|
||||||
setIsInviting(false);
|
} catch (error) {
|
||||||
}
|
console.error('Failed to send invitation:', error);
|
||||||
};
|
setInviteResult('Failed to send invitation. Please try again.');
|
||||||
|
} finally {
|
||||||
const renderSettings = () => (
|
setIsInviting(false);
|
||||||
<div className="space-y-6">
|
}
|
||||||
<Card>
|
};
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Appearance</h3>
|
|
||||||
<div className="space-y-3">
|
const renderSettings = () => (
|
||||||
<div>
|
<div className="space-y-6">
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
<Card>
|
||||||
Theme
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Appearance</h3>
|
||||||
</label>
|
<div className="space-y-3">
|
||||||
<div className="flex space-x-2">
|
<div>
|
||||||
<Button
|
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||||
variant={theme === Theme.Light ? 'primary' : 'secondary'}
|
Theme
|
||||||
size="sm"
|
</label>
|
||||||
onClick={() => setTheme(Theme.Light)}
|
<div className="flex space-x-2">
|
||||||
>
|
<Button
|
||||||
Light
|
variant={theme === Theme.Light ? 'primary' : 'secondary'}
|
||||||
</Button>
|
size="sm"
|
||||||
<Button
|
onClick={() => setTheme(Theme.Light)}
|
||||||
variant={theme === Theme.Dark ? 'primary' : 'secondary'}
|
>
|
||||||
size="sm"
|
Light
|
||||||
onClick={() => setTheme(Theme.Dark)}
|
</Button>
|
||||||
>
|
<Button
|
||||||
Dark
|
variant={theme === Theme.Dark ? 'primary' : 'secondary'}
|
||||||
</Button>
|
size="sm"
|
||||||
<Button
|
onClick={() => setTheme(Theme.Dark)}
|
||||||
variant={theme === Theme.System ? 'primary' : 'secondary'}
|
>
|
||||||
size="sm"
|
Dark
|
||||||
onClick={() => setTheme(Theme.System)}
|
</Button>
|
||||||
>
|
<Button
|
||||||
System
|
variant={theme === Theme.System ? 'primary' : 'secondary'}
|
||||||
</Button>
|
size="sm"
|
||||||
</div>
|
onClick={() => setTheme(Theme.System)}
|
||||||
</div>
|
>
|
||||||
</div>
|
System
|
||||||
</Card>
|
</Button>
|
||||||
|
</div>
|
||||||
<Card>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Organization</h3>
|
</div>
|
||||||
<div className="space-y-3">
|
</Card>
|
||||||
<div>
|
|
||||||
<span className="text-sm text-[--text-secondary]">Company:</span>
|
<Card>
|
||||||
<div className="font-medium text-[--text-primary]">{org?.name}</div>
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Organization</h3>
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm text-[--text-secondary]">Onboarding:</span>
|
<span className="text-sm text-[--text-secondary]">Company:</span>
|
||||||
<div className="font-medium text-[--text-primary]">
|
<div className="font-medium text-[--text-primary]">{org?.name}</div>
|
||||||
{org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<span className="text-sm text-[--text-secondary]">Onboarding:</span>
|
||||||
<div className="pt-4">
|
<div className="font-medium text-[--text-primary]">
|
||||||
<Button variant="secondary" onClick={handleRestartOnboarding}>
|
{org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
|
||||||
Restart Onboarding
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
<p className="text-xs text-[--text-secondary] mt-2">
|
<div className="pt-4">
|
||||||
This will reset your company profile and require you to complete the setup process again.
|
<Button variant="secondary" onClick={handleRestartOnboarding}>
|
||||||
</p>
|
Restart Onboarding
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
<p className="text-xs text-[--text-secondary] mt-2">
|
||||||
</Card>
|
This will reset your company profile and require you to complete the setup process again.
|
||||||
|
</p>
|
||||||
<Card>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
</div>
|
||||||
<div className="space-y-4">
|
</Card>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
<Card>
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
||||||
Name *
|
<div className="space-y-4">
|
||||||
</label>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<input
|
<div>
|
||||||
type="text"
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||||
value={inviteForm.name}
|
Name *
|
||||||
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
|
</label>
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
<input
|
||||||
placeholder="John Doe"
|
type="text"
|
||||||
/>
|
value={inviteForm.name}
|
||||||
</div>
|
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
|
||||||
<div>
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
placeholder="John Doe"
|
||||||
Email *
|
/>
|
||||||
</label>
|
</div>
|
||||||
<input
|
<div>
|
||||||
type="email"
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||||
value={inviteForm.email}
|
Email *
|
||||||
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
</label>
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
<input
|
||||||
placeholder="john.doe@company.com"
|
type="email"
|
||||||
/>
|
value={inviteForm.email}
|
||||||
</div>
|
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
||||||
<div>
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
placeholder="john.doe@company.com"
|
||||||
Role
|
/>
|
||||||
</label>
|
</div>
|
||||||
<input
|
<div>
|
||||||
type="text"
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||||
value={inviteForm.role}
|
Role
|
||||||
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
|
</label>
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
<input
|
||||||
placeholder="Senior Developer"
|
type="text"
|
||||||
/>
|
value={inviteForm.role}
|
||||||
</div>
|
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
|
||||||
<div>
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
placeholder="Senior Developer"
|
||||||
Department
|
/>
|
||||||
</label>
|
</div>
|
||||||
<input
|
<div>
|
||||||
type="text"
|
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
||||||
value={inviteForm.department}
|
Department
|
||||||
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
|
</label>
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
<input
|
||||||
placeholder="Engineering"
|
type="text"
|
||||||
/>
|
value={inviteForm.department}
|
||||||
</div>
|
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
|
||||||
</div>
|
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"
|
||||||
<Button
|
/>
|
||||||
onClick={handleInviteEmployee}
|
</div>
|
||||||
disabled={!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting}
|
</div>
|
||||||
className="w-full"
|
|
||||||
>
|
<Button
|
||||||
{isInviting ? 'Sending Invitation...' : 'Send Invitation'}
|
onClick={handleInviteEmployee}
|
||||||
</Button>
|
disabled={!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting}
|
||||||
|
className="w-full"
|
||||||
{inviteResult && (
|
>
|
||||||
<div>
|
{isInviting ? 'Sending Invitation...' : 'Send Invitation'}
|
||||||
{inviteResult.includes('Failed') ? (
|
</Button>
|
||||||
<div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
|
|
||||||
{inviteResult}
|
{inviteResult && (
|
||||||
</div>
|
<div>
|
||||||
) : (
|
{inviteResult.includes('Failed') ? (
|
||||||
(() => {
|
<div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
|
||||||
try {
|
{inviteResult}
|
||||||
const result = JSON.parse(inviteResult);
|
</div>
|
||||||
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">
|
try {
|
||||||
✅ Invitation sent to {result.employeeName}!
|
const result = JSON.parse(inviteResult);
|
||||||
</h4>
|
return (
|
||||||
<div className="space-y-3">
|
<div className="p-4 rounded-md bg-green-50 border border-green-200">
|
||||||
<div>
|
<h4 className="text-sm font-semibold text-green-800 mb-3">
|
||||||
<label className="block text-xs font-medium text-green-700 mb-1">
|
✅ Invitation sent to {result.employeeName}!
|
||||||
Direct Link (share this with the employee):
|
</h4>
|
||||||
</label>
|
<div className="space-y-3">
|
||||||
<div className="flex gap-2">
|
<div>
|
||||||
<input
|
<label className="block text-xs font-medium text-green-700 mb-1">
|
||||||
type="text"
|
Direct Link (share this with the employee):
|
||||||
value={result.inviteLink}
|
</label>
|
||||||
readOnly
|
<div className="flex gap-2">
|
||||||
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
|
<input
|
||||||
/>
|
type="text"
|
||||||
<Button
|
value={result.inviteLink}
|
||||||
size="sm"
|
readOnly
|
||||||
variant="secondary"
|
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
|
||||||
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
|
/>
|
||||||
>
|
<Button
|
||||||
Copy
|
size="sm"
|
||||||
</Button>
|
variant="secondary"
|
||||||
</div>
|
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
|
||||||
</div>
|
>
|
||||||
<div>
|
Copy
|
||||||
<a
|
</Button>
|
||||||
href={result.emailLink}
|
</div>
|
||||||
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
|
</div>
|
||||||
>
|
<div>
|
||||||
📧 Open Email Draft
|
<a
|
||||||
</a>
|
href={result.emailLink}
|
||||||
</div>
|
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
</div>
|
>
|
||||||
</div>
|
📧 Open Email Draft
|
||||||
);
|
</a>
|
||||||
} catch {
|
</div>
|
||||||
return (
|
</div>
|
||||||
<div className="p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200">
|
</div>
|
||||||
{inviteResult}
|
);
|
||||||
</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.
|
</div>
|
||||||
</p>
|
)}
|
||||||
</div>
|
|
||||||
</Card>
|
<p className="text-xs text-[--text-secondary]">
|
||||||
|
The invited employee will receive an email with instructions to join your organization.
|
||||||
<Card>
|
</p>
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Account</h3>
|
</div>
|
||||||
<div className="space-y-3">
|
</Card>
|
||||||
<div>
|
|
||||||
<span className="text-sm text-[--text-secondary]">Email:</span>
|
<Card>
|
||||||
<div className="font-medium text-[--text-primary]">{user?.email}</div>
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Account</h3>
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm text-[--text-secondary]">User ID:</span>
|
<span className="text-sm text-[--text-secondary]">Email:</span>
|
||||||
<div className="font-medium text-[--text-primary] font-mono text-xs">{user?.uid}</div>
|
<div className="font-medium text-[--text-primary]">{user?.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4">
|
<div>
|
||||||
<Button variant="secondary" onClick={handleLogout}>
|
<span className="text-sm text-[--text-secondary]">User ID:</span>
|
||||||
Sign Out
|
<div className="font-medium text-[--text-primary] font-mono text-xs">{user?.uid}</div>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
<div className="pt-4">
|
||||||
</div>
|
<Button variant="secondary" onClick={handleLogout}>
|
||||||
</Card>
|
Sign Out
|
||||||
|
</Button>
|
||||||
<Card>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Data & Privacy</h3>
|
</div>
|
||||||
<div className="space-y-3">
|
</Card>
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
|
||||||
Export My Data
|
<Card>
|
||||||
</Button>
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Data & Privacy</h3>
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
<div className="space-y-3">
|
||||||
Privacy Settings
|
<Button variant="secondary" className="w-full justify-start">
|
||||||
</Button>
|
Export My Data
|
||||||
<Button variant="secondary" className="w-full justify-start text-red-600">
|
</Button>
|
||||||
Delete Account
|
<Button variant="secondary" className="w-full justify-start">
|
||||||
</Button>
|
Privacy Settings
|
||||||
</div>
|
</Button>
|
||||||
</Card>
|
<Button variant="secondary" className="w-full justify-start text-red-600">
|
||||||
</div>
|
Delete Account
|
||||||
);
|
</Button>
|
||||||
|
</div>
|
||||||
const renderHelp = () => (
|
</Card>
|
||||||
<div className="space-y-6">
|
</div>
|
||||||
<Card>
|
);
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Getting Started</h3>
|
|
||||||
<div className="space-y-4">
|
const renderHelp = () => (
|
||||||
<div>
|
<div className="space-y-6">
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">1. Set up your organization</h4>
|
<Card>
|
||||||
<p className="text-[--text-secondary] text-sm">
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Getting Started</h3>
|
||||||
Complete the onboarding process to configure your company information and preferences.
|
<div className="space-y-4">
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-[--text-primary] mb-2">1. Set up your organization</h4>
|
||||||
<div>
|
<p className="text-[--text-secondary] text-sm">
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">2. Add employees</h4>
|
Complete the onboarding process to configure your company information and preferences.
|
||||||
<p className="text-[--text-secondary] text-sm">
|
</p>
|
||||||
Invite team members and add their basic information to start generating reports.
|
</div>
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-[--text-primary] mb-2">2. Add employees</h4>
|
||||||
<div>
|
<p className="text-[--text-secondary] text-sm">
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">3. Generate reports</h4>
|
Invite team members and add their basic information to start generating reports.
|
||||||
<p className="text-[--text-secondary] text-sm">
|
</p>
|
||||||
Use AI-powered reports to gain insights into employee performance and organizational health.
|
</div>
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-[--text-primary] mb-2">3. Generate reports</h4>
|
||||||
</div>
|
<p className="text-[--text-secondary] text-sm">
|
||||||
</Card>
|
Use AI-powered reports to gain insights into employee performance and organizational health.
|
||||||
|
</p>
|
||||||
<Card>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Frequently Asked Questions</h3>
|
</div>
|
||||||
<div className="space-y-4">
|
</Card>
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">How do I add new employees?</h4>
|
<Card>
|
||||||
<p className="text-[--text-secondary] text-sm">
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Frequently Asked Questions</h3>
|
||||||
Go to the Reports page and use the "Add Employee" button to invite new team members.
|
<div className="space-y-4">
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-[--text-primary] mb-2">How do I add new employees?</h4>
|
||||||
<div>
|
<p className="text-[--text-secondary] text-sm">
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">How are reports generated?</h4>
|
Go to the Reports page and use the "Add Employee" button to invite new team members.
|
||||||
<p className="text-[--text-secondary] text-sm">
|
</p>
|
||||||
Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
|
</div>
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-[--text-primary] mb-2">How are reports generated?</h4>
|
||||||
<div>
|
<p className="text-[--text-secondary] text-sm">
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">Is my data secure?</h4>
|
Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
|
||||||
<p className="text-[--text-secondary] text-sm">
|
</p>
|
||||||
Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
|
</div>
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-[--text-primary] mb-2">Is my data secure?</h4>
|
||||||
</div>
|
<p className="text-[--text-secondary] text-sm">
|
||||||
</Card>
|
Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
|
||||||
|
</p>
|
||||||
<Card>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Contact Support</h3>
|
</div>
|
||||||
<div className="space-y-3">
|
</Card>
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
|
||||||
📧 Email Support
|
<Card>
|
||||||
</Button>
|
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Contact Support</h3>
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
<div className="space-y-3">
|
||||||
💬 Live Chat
|
<Button variant="secondary" className="w-full justify-start">
|
||||||
</Button>
|
📧 Email Support
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
</Button>
|
||||||
📚 Documentation
|
<Button variant="secondary" className="w-full justify-start">
|
||||||
</Button>
|
💬 Live Chat
|
||||||
</div>
|
</Button>
|
||||||
</Card>
|
<Button variant="secondary" className="w-full justify-start">
|
||||||
</div>
|
📚 Documentation
|
||||||
);
|
</Button>
|
||||||
|
</div>
|
||||||
return (
|
</Card>
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
</div>
|
||||||
<div className="mb-6">
|
);
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary]">Help & Settings</h1>
|
|
||||||
<p className="text-[--text-secondary] mt-1">
|
return (
|
||||||
Manage your account and get help
|
<div className="p-6 max-w-4xl mx-auto">
|
||||||
</p>
|
<div className="mb-6">
|
||||||
</div>
|
<h1 className="text-3xl font-bold text-[--text-primary]">Help & Settings</h1>
|
||||||
|
<p className="text-[--text-secondary] mt-1">
|
||||||
<div className="mb-6">
|
Manage your account and get help
|
||||||
<div className="flex space-x-4 border-b border-[--border-color]">
|
</p>
|
||||||
<button
|
</div>
|
||||||
onClick={() => setActiveTab('settings')}
|
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
|
<div className="mb-6">
|
||||||
? 'border-blue-500 text-blue-500'
|
<div className="flex space-x-4 border-b border-[--border-color]">
|
||||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
<button
|
||||||
}`}
|
onClick={() => setActiveTab('settings')}
|
||||||
>
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
|
||||||
Settings
|
? 'border-blue-500 text-blue-500'
|
||||||
</button>
|
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
||||||
<button
|
}`}
|
||||||
onClick={() => setActiveTab('help')}
|
>
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
|
Settings
|
||||||
? 'border-blue-500 text-blue-500'
|
</button>
|
||||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
<button
|
||||||
}`}
|
onClick={() => setActiveTab('help')}
|
||||||
>
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
|
||||||
Help
|
? 'border-blue-500 text-blue-500'
|
||||||
</button>
|
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
||||||
</div>
|
}`}
|
||||||
</div>
|
>
|
||||||
|
Help
|
||||||
{activeTab === 'settings' ? renderSettings() : renderHelp()}
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
|
||||||
|
{activeTab === 'settings' ? renderSettings() : renderHelp()}
|
||||||
export default HelpAndSettings;
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HelpAndSettings;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import Sidebar from '../components/figma/Sidebar';
|
import Sidebar from '../components/figma/Sidebar';
|
||||||
|
|
||||||
interface FAQItem {
|
interface FAQItem {
|
||||||
@@ -11,6 +12,7 @@ interface FAQItem {
|
|||||||
|
|
||||||
const HelpNew: React.FC = () => {
|
const HelpNew: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { org } = useOrg();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [faqItems, setFaqItems] = useState<FAQItem[]>([
|
const [faqItems, setFaqItems] = useState<FAQItem[]>([
|
||||||
@@ -64,64 +66,62 @@ const HelpNew: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start h-full">
|
||||||
<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={org.name} />
|
||||||
<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="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] 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">
|
||||||
<div className="w-[680px] flex flex-col justify-start items-start gap-4">
|
{faqItems.map((item, index) => (
|
||||||
{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
|
<div
|
||||||
onClick={handleContactUs}
|
key={index}
|
||||||
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"
|
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>
|
<div
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
onClick={() => toggleFAQ(index)}
|
||||||
<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" />
|
className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2 cursor-pointer"
|
||||||
</svg>
|
>
|
||||||
</div>
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">
|
||||||
<div className="px-1 flex justify-center items-center">
|
{item.question}
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Contact Us</div>
|
</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
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 { secureApi } from '../services/secureApi';
|
||||||
import {
|
import {
|
||||||
FigmaOnboardingIntro,
|
FigmaOnboardingIntro,
|
||||||
@@ -23,12 +23,12 @@ const Onboarding: React.FC = () => {
|
|||||||
|
|
||||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||||
const [isGeneratingReport, setIsGeneratingReport] = useState(false);
|
const [isGeneratingReport, setIsGeneratingReport] = useState(false);
|
||||||
const [formData, setFormData] = useState<OnboardingData>(initializeOnboardingData());
|
const [formData, setFormData] = useState<OnboardingFormData>(initializeOnboardingData());
|
||||||
|
|
||||||
const currentStep = onboardingSteps[currentStepIndex];
|
const currentStep = onboardingSteps[currentStepIndex];
|
||||||
const totalSteps = onboardingSteps.length;
|
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 }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ const Onboarding: React.FC = () => {
|
|||||||
case 'question':
|
case 'question':
|
||||||
// Check if field is filled
|
// Check if field is filled
|
||||||
if (currentStep.field) {
|
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 Array.isArray(fieldValue) ? fieldValue.length > 0 : String(fieldValue || '').trim().length > 0;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -54,7 +54,7 @@ const Onboarding: React.FC = () => {
|
|||||||
case 'multiple_choice':
|
case 'multiple_choice':
|
||||||
// Check if option is selected
|
// Check if option is selected
|
||||||
if (currentStep.field) {
|
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 String(fieldValue || '').trim().length > 0;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -76,13 +76,15 @@ const Onboarding: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Final step: submit all data and complete onboarding
|
// Final step: submit all data and complete onboarding
|
||||||
|
|
||||||
|
const { companyName, companyLogo, ...onboardingData } = formData;
|
||||||
|
|
||||||
setIsGeneratingReport(true);
|
setIsGeneratingReport(true);
|
||||||
try {
|
try {
|
||||||
await upsertOrg({
|
await upsertOrg({
|
||||||
...org,
|
...org,
|
||||||
companyName: formData.companyName,
|
companyLogo,
|
||||||
companyLogo: formData.companyLogo,
|
onboardingData,
|
||||||
onboardingData: formData,
|
|
||||||
onboardingCompleted: true,
|
onboardingCompleted: true,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -154,7 +156,7 @@ const Onboarding: React.FC = () => {
|
|||||||
|
|
||||||
case 'question':
|
case 'question':
|
||||||
const questionValue = currentStep.field
|
const questionValue = currentStep.field
|
||||||
? String(formData[currentStep.field as keyof OnboardingData] || '')
|
? String(formData[currentStep.field as keyof OnboardingFormData] || '')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -164,7 +166,7 @@ const Onboarding: React.FC = () => {
|
|||||||
value={questionValue}
|
value={questionValue}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (currentStep.field) {
|
if (currentStep.field) {
|
||||||
updateFormData(currentStep.field as keyof OnboardingData, value);
|
updateFormData(currentStep.field as keyof OnboardingFormData, value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
@@ -180,7 +182,7 @@ const Onboarding: React.FC = () => {
|
|||||||
|
|
||||||
case 'multiple_choice':
|
case 'multiple_choice':
|
||||||
const multipleChoiceValue = currentStep.field
|
const multipleChoiceValue = currentStep.field
|
||||||
? String(formData[currentStep.field as keyof OnboardingData] || '')
|
? String(formData[currentStep.field as keyof OnboardingFormData] || '')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -190,7 +192,7 @@ const Onboarding: React.FC = () => {
|
|||||||
selectedValue={multipleChoiceValue}
|
selectedValue={multipleChoiceValue}
|
||||||
onSelect={(value) => {
|
onSelect={(value) => {
|
||||||
if (currentStep.field) {
|
if (currentStep.field) {
|
||||||
updateFormData(currentStep.field as keyof OnboardingData, value);
|
updateFormData(currentStep.field as keyof OnboardingFormData, value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
|||||||
|
|
||||||
const Reports: React.FC = () => {
|
const Reports: React.FC = () => {
|
||||||
const location = useLocation();
|
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 [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
||||||
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string } | null>(null);
|
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string } | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
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 [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
|
||||||
|
|
||||||
const currentUserIsOwner = isOwner(user?.uid || '');
|
const currentUserIsOwner = isOwner(user?.uid || '');
|
||||||
@@ -20,6 +20,29 @@ const Reports: React.FC = () => {
|
|||||||
// Get selected employee ID from navigation state (from Submissions page)
|
// Get selected employee ID from navigation state (from Submissions page)
|
||||||
const selectedEmployeeId = location.state?.selectedEmployeeId;
|
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
|
// Load company report on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCompanyReport = async () => {
|
const loadCompanyReport = async () => {
|
||||||
@@ -31,6 +54,9 @@ const Reports: React.FC = () => {
|
|||||||
// Auto-select company report by default
|
// Auto-select company report by default
|
||||||
setSelectedReport({ report: history[0], type: 'company' });
|
setSelectedReport({ report: history[0], type: 'company' });
|
||||||
} else {
|
} 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);
|
setCompanyReport(SAMPLE_COMPANY_REPORT);
|
||||||
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' });
|
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' });
|
||||||
}
|
}
|
||||||
@@ -42,9 +68,9 @@ const Reports: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadCompanyReport();
|
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];
|
const employeeReport = reports[employee.id];
|
||||||
if (employeeReport) {
|
if (employeeReport) {
|
||||||
setSelectedReport({
|
setSelectedReport({
|
||||||
@@ -52,8 +78,52 @@ const Reports: React.FC = () => {
|
|||||||
type: 'employee',
|
type: 'employee',
|
||||||
employeeName: employee.name
|
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
|
// Handle navigation from Submissions page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -81,11 +151,15 @@ const Reports: React.FC = () => {
|
|||||||
const handleGenerateCompanyReport = async () => {
|
const handleGenerateCompanyReport = async () => {
|
||||||
setGeneratingCompanyReport(true);
|
setGeneratingCompanyReport(true);
|
||||||
try {
|
try {
|
||||||
|
console.log('Generating new company report with current data...');
|
||||||
const newReport = await generateCompanyReport();
|
const newReport = await generateCompanyReport();
|
||||||
setCompanyReport(newReport);
|
setCompanyReport(newReport);
|
||||||
setSelectedReport({ report: newReport, type: 'company' });
|
setSelectedReport({ report: newReport, type: 'company' });
|
||||||
|
console.log('Company report generated successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating company report:', error);
|
console.error('Error generating company report:', error);
|
||||||
|
// Show error message to user
|
||||||
|
alert('Failed to generate company report. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setGeneratingCompanyReport(false);
|
setGeneratingCompanyReport(false);
|
||||||
}
|
}
|
||||||
@@ -139,23 +213,42 @@ const Reports: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Employee Items */}
|
{/* Employee Items */}
|
||||||
{visibleEmployees.map((employee) => (
|
{visibleEmployees.map((employee) => {
|
||||||
<div
|
const hasSubmission = submissions[employee.id];
|
||||||
key={employee.id}
|
const hasReport = reports[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]' : ''
|
const isGenerating = generatingEmployeeReport === employee.id;
|
||||||
}`}
|
|
||||||
onClick={() => handleEmployeeSelect(employee)}
|
return (
|
||||||
>
|
<div
|
||||||
<div className="w-7 h-7 p-1 bg-[--Neutrals-NeutralSlate100] rounded-[666.67px] flex justify-center items-center">
|
key={employee.id}
|
||||||
<div className="text-center justify-start text-[--Neutrals-NeutralSlate500] text-xs font-medium font-['Inter'] leading-none">
|
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]' : ''
|
||||||
{employee.initials}
|
}`}
|
||||||
|
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>
|
||||||
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
{employee.name}
|
||||||
|
</div>
|
||||||
|
{isGenerating && (
|
||||||
|
<div className="w-4 h-4 animate-spin rounded-full border-2 border-[--Neutrals-NeutralSlate300] border-t-[--Brand-Orange]" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">
|
);
|
||||||
{employee.name}
|
})}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,10 +263,23 @@ const Reports: React.FC = () => {
|
|||||||
isGenerating={generatingCompanyReport}
|
isGenerating={generatingCompanyReport}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmployeeReportContent
|
(() => {
|
||||||
report={selectedReport.report as EmployeeReport}
|
const employeeReport = selectedReport.report as EmployeeReport;
|
||||||
employeeName={selectedReport.employeeName!}
|
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">
|
<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">
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-base font-medium font-['Inter'] leading-normal">
|
||||||
Company Report
|
Company Report
|
||||||
</div>
|
</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="flex justify-start items-center gap-3">
|
||||||
<div className="relative">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
onClick={onRegenerate}
|
||||||
<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" />
|
disabled={isGenerating}
|
||||||
</svg>
|
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>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="relative">
|
||||||
<div className="justify-center text-[--Neutrals-NeutralSlate0] text-sm font-medium font-['Inter'] leading-tight">Download as PDF</div>
|
{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>
|
</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="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">
|
<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 */}
|
{/* Department Tabs */}
|
||||||
{report?.organizationalImpactSummary.map((dept, index) => (
|
{report.organizationalImpactSummary && report.organizationalImpactSummary.map((dept, index) => (
|
||||||
<div
|
<div
|
||||||
key={dept.category}
|
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
|
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>
|
</div>
|
||||||
{/* Content for the currently selected department */}
|
{/* Content for the currently selected department */}
|
||||||
{(() => {
|
{report.organizationalImpactSummary && (() => {
|
||||||
const currentImpact = report?.organizationalImpactSummary.find(dept => dept.category === activeImpactSummary);
|
const currentImpact = report.organizationalImpactSummary.find(dept => dept.category === activeImpactSummary);
|
||||||
if (!currentImpact) return null;
|
if (!currentImpact) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -550,7 +679,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
<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">
|
<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 */}
|
{/* Department Tabs */}
|
||||||
{report?.gradingBreakdown?.map(dept => (
|
{report.gradingBreakdown && report?.gradingBreakdown?.map(dept => (
|
||||||
<div
|
<div
|
||||||
key={dept.departmentNameShort}
|
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
|
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>
|
</div>
|
||||||
{/* Content for the currently selected department */}
|
{/* Content for the currently selected department */}
|
||||||
{(() => {
|
{report.gradingBreakdown && (() => {
|
||||||
const currentDepartment = report?.gradingBreakdown?.find(dept => dept.departmentNameShort === activeDepartmentTab);
|
const currentDepartment = report?.gradingBreakdown?.find(dept => dept.departmentNameShort === activeDepartmentTab);
|
||||||
if (!currentDepartment) return null;
|
if (!currentDepartment) return null;
|
||||||
|
|
||||||
@@ -652,23 +781,56 @@ const CompanyReportContent: React.FC<{
|
|||||||
const EmployeeReportContent: React.FC<{
|
const EmployeeReportContent: React.FC<{
|
||||||
report: EmployeeReport;
|
report: EmployeeReport;
|
||||||
employeeName: string;
|
employeeName: string;
|
||||||
}> = ({ report, employeeName }) => {
|
onGenerateReport?: () => void;
|
||||||
|
isGenerating?: boolean;
|
||||||
|
hasSubmission?: boolean;
|
||||||
|
showGenerateButton?: boolean;
|
||||||
|
}> = ({ report, employeeName, onGenerateReport, isGenerating = false, hasSubmission = false, showGenerateButton = false }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="self-stretch px-5 py-3 inline-flex justify-start items-center gap-2.5">
|
<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">
|
<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>
|
||||||
<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="flex justify-start items-center gap-3">
|
||||||
<div className="relative">
|
{/* Generate Report Button - only show when needed */}
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
{showGenerateButton && hasSubmission && onGenerateReport && (
|
||||||
<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" />
|
<button
|
||||||
</svg>
|
onClick={onGenerateReport}
|
||||||
</div>
|
disabled={isGenerating}
|
||||||
<div className="px-1 flex justify-center items-center">
|
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="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Download as PDF</div>
|
>
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -687,7 +849,7 @@ const EmployeeReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Self-Rated Output */}
|
{/* 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="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="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>
|
<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="w-6 h-6 left-0 top-0 absolute bg-Other-Green rounded-full" />
|
||||||
<div className="left-[5px] top-[5px] absolute">
|
<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">
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,295 +1,295 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import Sidebar from '../components/figma/Sidebar';
|
import Sidebar from '../components/figma/Sidebar';
|
||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
fullName: string;
|
fullName: string;
|
||||||
email: string;
|
email: string;
|
||||||
profilePicture?: string;
|
profilePicture?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeMode = 'system' | 'light' | 'dark';
|
type ThemeMode = 'system' | 'light' | 'dark';
|
||||||
|
|
||||||
const SettingsNew: React.FC = () => {
|
const SettingsNew: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
|
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile>({
|
const [userProfile, setUserProfile] = useState<UserProfile>({
|
||||||
fullName: 'John Doe',
|
fullName: 'John Doe',
|
||||||
email: 'Johndoe1234@gmail.com'
|
email: 'Johndoe1234@gmail.com'
|
||||||
});
|
});
|
||||||
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
|
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
|
||||||
|
|
||||||
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
|
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
|
||||||
setUserProfile(prev => ({
|
setUserProfile(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value
|
[field]: value
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePhotoUpload = () => {
|
const handlePhotoUpload = () => {
|
||||||
// In a real app, this would open a file picker
|
// In a real app, this would open a file picker
|
||||||
alert('Photo upload functionality would be implemented here');
|
alert('Photo upload functionality would be implemented here');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveChanges = () => {
|
const handleSaveChanges = () => {
|
||||||
// In a real app, this would save to backend
|
// In a real app, this would save to backend
|
||||||
alert('Settings saved successfully!');
|
alert('Settings saved successfully!');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setUserProfile({
|
setUserProfile({
|
||||||
fullName: 'John Doe',
|
fullName: 'John Doe',
|
||||||
email: 'Johndoe1234@gmail.com'
|
email: 'Johndoe1234@gmail.com'
|
||||||
});
|
});
|
||||||
setSelectedTheme('light');
|
setSelectedTheme('light');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[1440px] h-[840px] p-4 bg-[--Neutrals-NeutralSlate200] inline-flex justify-start items-start overflow-hidden">
|
<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">
|
<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" />
|
<Sidebar companyName="Zitlac Media" />
|
||||||
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
|
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
|
||||||
{/* Tab Navigation */}
|
{/* 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 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 className="self-stretch inline-flex justify-start items-start gap-6">
|
||||||
<div
|
<div
|
||||||
onClick={() => setActiveTab('general')}
|
onClick={() => setActiveTab('general')}
|
||||||
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
|
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'
|
<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-800 font-semibold'
|
||||||
: 'text-Text-Gray-500 font-normal'
|
: 'text-Text-Gray-500 font-normal'
|
||||||
}`}>
|
}`}>
|
||||||
General Settings
|
General Settings
|
||||||
</div>
|
</div>
|
||||||
{activeTab === 'general' && (
|
{activeTab === 'general' && (
|
||||||
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => setActiveTab('billing')}
|
onClick={() => setActiveTab('billing')}
|
||||||
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
|
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'
|
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
|
||||||
? 'text-Text-Gray-800 font-semibold'
|
? 'text-Text-Gray-800 font-semibold'
|
||||||
: 'text-Text-Gray-500 font-normal'
|
: 'text-Text-Gray-500 font-normal'
|
||||||
}`}>
|
}`}>
|
||||||
Plan & Billings
|
Plan & Billings
|
||||||
</div>
|
</div>
|
||||||
{activeTab === 'billing' && (
|
{activeTab === 'billing' && (
|
||||||
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* General Settings Content */}
|
{/* General Settings Content */}
|
||||||
{activeTab === 'general' && (
|
{activeTab === 'general' && (
|
||||||
<>
|
<>
|
||||||
{/* Profile Information Section */}
|
{/* Profile Information Section */}
|
||||||
<div className="w-[1136px] h-72 p-6 flex flex-col justify-start items-start gap-6">
|
<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="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-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 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>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
{/* Profile Picture Section */}
|
{/* 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="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="flex-1 flex justify-start items-center gap-3">
|
||||||
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
|
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
|
||||||
<div>
|
<div>
|
||||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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)">
|
<g clipPath="url(#clip0_1042_3786)">
|
||||||
<ellipse cx="28" cy="54.6008" rx="22.4" ry="16.8" fill="white" fillOpacity="0.72" />
|
<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" />
|
<circle opacity="0.9" cx="28" cy="22.3992" r="11.2" fill="white" />
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_1042_3786">
|
<clipPath id="clip0_1042_3786">
|
||||||
<rect width="56" height="56" rx="28" fill="white" />
|
<rect width="56" height="56" rx="28" fill="white" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-1">
|
<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-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 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>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={handlePhotoUpload}
|
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]"
|
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>
|
<div>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<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 className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload Photo</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name and Email Fields */}
|
{/* Name and Email Fields */}
|
||||||
<div className="w-[664px] inline-flex justify-start items-center gap-4">
|
<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="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="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 className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Full Name</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<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 className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
<div>
|
<div>
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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 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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={userProfile.fullName}
|
value={userProfile.fullName}
|
||||||
onChange={(e) => handleProfileUpdate('fullName', e.target.value)}
|
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"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
<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="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 className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email Address</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
<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 className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
<div>
|
<div>
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={userProfile.email}
|
value={userProfile.email}
|
||||||
onChange={(e) => handleProfileUpdate('email', e.target.value)}
|
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"
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div>
|
<div>
|
||||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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)" />
|
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Theme Customization Section */}
|
{/* Theme Customization Section */}
|
||||||
<div className="w-[1170px] p-6 flex flex-col justify-start items-start gap-6">
|
<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="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-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 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>
|
||||||
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
|
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
|
||||||
{/* System Preference */}
|
{/* System Preference */}
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedTheme('system')}
|
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'
|
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">
|
<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-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" />
|
<img className="w-24 h-28 rounded-tr-lg rounded-br-lg" src="https://via.placeholder.com/96x107/212529/ffffff?text=Dark" />
|
||||||
</div>
|
</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 className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">System preference</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Light Mode */}
|
{/* Light Mode */}
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedTheme('light')}
|
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'
|
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="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'
|
<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" />
|
<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>
|
</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 className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Light Mode</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dark Mode */}
|
{/* Dark Mode */}
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedTheme('dark')}
|
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'
|
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 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'
|
<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" />
|
<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>
|
</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 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Another Divider */}
|
{/* Another Divider */}
|
||||||
<div>
|
<div>
|
||||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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)" />
|
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
|
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
|
||||||
<div
|
<div
|
||||||
onClick={handleReset}
|
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]"
|
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="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 className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reset</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={handleSaveChanges}
|
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"
|
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="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 className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Save Changes</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Billing Content */}
|
{/* Billing Content */}
|
||||||
{activeTab === 'billing' && (
|
{activeTab === 'billing' && (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-2xl font-semibold text-Text-Gray-800 mb-4">Plan & Billing</h2>
|
<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>
|
<p className="text-Text-Gray-500">Billing management features would be implemented here.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsNew;
|
export default SettingsNew;
|
||||||
@@ -29,19 +29,33 @@ const Submissions: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Use the secure API service to get submissions
|
// Use the secure API service to get submissions
|
||||||
// const data = await secureApi.getSubmissions();
|
const data = await secureApi.getSubmissions();
|
||||||
const data = { submissions: [] }; // temp fix
|
|
||||||
|
|
||||||
if (data) {
|
if (data && data.submissions) {
|
||||||
// setSubmissions(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
|
// 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) {
|
if (employeesWithSubmissions.length > 0) {
|
||||||
setSelectedEmployee(employeesWithSubmissions[0]);
|
setSelectedEmployee(employeesWithSubmissions[0]);
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Error loading submissions:', 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]);
|
}, [employees]);
|
||||||
|
|
||||||
const loadDemoSubmissions = () => {
|
const loadDemoSubmissions = () => {
|
||||||
@@ -61,6 +79,7 @@ const Submissions: React.FC = () => {
|
|||||||
|
|
||||||
employees.forEach((employee, index) => {
|
employees.forEach((employee, index) => {
|
||||||
if (index < 3) { // Only add submissions for first 3 employees
|
if (index < 3) { // Only add submissions for first 3 employees
|
||||||
|
console.log(employee);
|
||||||
demoSubmissions[employee.id] = {
|
demoSubmissions[employee.id] = {
|
||||||
employeeId: employee.id,
|
employeeId: employee.id,
|
||||||
employee,
|
employee,
|
||||||
@@ -123,17 +142,51 @@ const Submissions: React.FC = () => {
|
|||||||
const submission = submissions[selectedEmployee.id];
|
const submission = submissions[selectedEmployee.id];
|
||||||
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
|
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
|
||||||
|
|
||||||
// Map EMPLOYEE_QUESTIONS to actual answers
|
// Handle different submission formats
|
||||||
EMPLOYEE_QUESTIONS.forEach(q => {
|
let submissionAnswers: Record<string, string> = {};
|
||||||
const answer = submission.answers[q.id];
|
|
||||||
if (answer && answer.trim()) {
|
if (submission.answers) {
|
||||||
questionsAndAnswers.push({
|
if (Array.isArray(submission.answers)) {
|
||||||
question: q.prompt,
|
// If answers is an array of {question, answer} objects
|
||||||
answer: answer,
|
submissionAnswers = submission.answers.reduce((acc, item: any) => {
|
||||||
isLong: answer.length > 150 // Mark long answers for different styling
|
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;
|
return questionsAndAnswers;
|
||||||
};
|
};
|
||||||
@@ -298,7 +351,7 @@ const SubmissionContent: React.FC<{
|
|||||||
questionsAndAnswers.map((qa, index) => (
|
questionsAndAnswers.map((qa, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
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">
|
<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 file - The image file to upload
|
||||||
* @param collectionName - Collection name (e.g., 'company-logos')
|
* @param collectionName - Collection name (e.g., 'company-logos')
|
||||||
* @param documentId - Document ID (e.g., orgId)
|
* @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 maxWidth - Maximum width for resizing (default: 128)
|
||||||
* @param maxHeight - Maximum height for resizing (default: 128)
|
* @param maxHeight - Maximum height for resizing (default: 128)
|
||||||
* @returns Promise with stored image data
|
* @returns Promise with stored image data
|
||||||
@@ -28,8 +26,6 @@ export const uploadImage = async (
|
|||||||
file: File,
|
file: File,
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
documentId: string,
|
documentId: string,
|
||||||
orgId: string,
|
|
||||||
userId: string,
|
|
||||||
maxWidth: number = 128,
|
maxWidth: number = 128,
|
||||||
maxHeight: number = 128
|
maxHeight: number = 128
|
||||||
): Promise<StoredImage> => {
|
): Promise<StoredImage> => {
|
||||||
@@ -58,7 +54,7 @@ export const uploadImage = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await secureApi.uploadImage(orgId, userId, imageData);
|
const result = await secureApi.uploadImage(imageData);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error('Failed to upload image');
|
throw new Error('Failed to upload image');
|
||||||
@@ -84,18 +80,14 @@ export const uploadImage = async (
|
|||||||
* Retrieve an image through secure API
|
* Retrieve an image through secure API
|
||||||
* @param collectionName - Collection name
|
* @param collectionName - Collection name
|
||||||
* @param documentId - Document ID
|
* @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
|
* @returns Promise with stored image data or null if not found
|
||||||
*/
|
*/
|
||||||
export const getImage = async (
|
export const getImage = async (
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
documentId: string,
|
documentId: string
|
||||||
orgId: string,
|
|
||||||
userId: string
|
|
||||||
): Promise<StoredImage | null> => {
|
): Promise<StoredImage | null> => {
|
||||||
try {
|
try {
|
||||||
const result = await secureApi.getImage(orgId, userId, collectionName, documentId);
|
const result = await secureApi.getImage(collectionName, documentId);
|
||||||
|
|
||||||
return result; // getImage already returns StoredImage | null
|
return result; // getImage already returns StoredImage | null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -108,18 +100,14 @@ export const getImage = async (
|
|||||||
* Delete an image through secure API
|
* Delete an image through secure API
|
||||||
* @param collectionName - Collection name
|
* @param collectionName - Collection name
|
||||||
* @param documentId - Document ID
|
* @param documentId - Document ID
|
||||||
* @param orgId - Organization ID
|
|
||||||
* @param userId - User ID for authentication
|
|
||||||
* @returns Promise indicating success
|
* @returns Promise indicating success
|
||||||
*/
|
*/
|
||||||
export const deleteImage = async (
|
export const deleteImage = async (
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
documentId: string,
|
documentId: string,
|
||||||
orgId: string,
|
|
||||||
userId: string
|
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const result = await secureApi.deleteImage(orgId, userId, collectionName, documentId);
|
const result = await secureApi.deleteImage(collectionName, documentId);
|
||||||
|
|
||||||
return result; // deleteImage already returns boolean
|
return result; // deleteImage already returns boolean
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -134,10 +122,9 @@ export const deleteImage = async (
|
|||||||
*/
|
*/
|
||||||
export const uploadCompanyLogo = async (
|
export const uploadCompanyLogo = async (
|
||||||
file: File,
|
file: File,
|
||||||
orgId: string,
|
orgId: string
|
||||||
userId: string
|
|
||||||
): Promise<StoredImage> => {
|
): 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
|
* Requires authentication context to get userId
|
||||||
*/
|
*/
|
||||||
export const getCompanyLogo = async (
|
export const getCompanyLogo = async (
|
||||||
orgId: string,
|
orgId: string
|
||||||
userId: string
|
|
||||||
): Promise<StoredImage | null> => {
|
): 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
|
* Requires authentication context to get userId
|
||||||
*/
|
*/
|
||||||
export const deleteCompanyLogo = async (
|
export const deleteCompanyLogo = async (
|
||||||
orgId: string,
|
orgId: string
|
||||||
userId: string
|
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
return deleteImage('company-logos', orgId, orgId, userId);
|
return deleteImage('company-logos', orgId);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ interface OrgData {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetSubmissions {
|
||||||
|
submissions: Submission[];
|
||||||
|
}
|
||||||
|
|
||||||
interface GetUserOrganizations {
|
interface GetUserOrganizations {
|
||||||
organizations: UserOrganization[];
|
organizations: UserOrganization[];
|
||||||
}
|
}
|
||||||
@@ -186,11 +190,11 @@ class SecureApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Submission Methods
|
// Submission Methods
|
||||||
async getSubmissions(): Promise<Record<string, Submission>> {
|
async getSubmissions(): Promise<Submission[]> {
|
||||||
const response = await this.makeRequest<{ submissions: Record<string, Submission> }>(
|
const response = await this.makeRequest<{ submissions: Submission[] }>(
|
||||||
'getSubmissions'
|
'getSubmissions'
|
||||||
);
|
);
|
||||||
return response.submissions;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report Methods
|
// Report Methods
|
||||||
@@ -260,8 +264,9 @@ class SecureApiService {
|
|||||||
return this.makeRequest('generateEmployeeReport', 'POST', { employee, submission, companyWiki });
|
return this.makeRequest('generateEmployeeReport', 'POST', { employee, submission, companyWiki });
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateCompanyWiki(org: any, submissions: any[] = []) {
|
async generateCompanyWiki(org: any, submissions: any[] = []): Promise<CompanyReport> {
|
||||||
return this.makeRequest('generateCompanyWiki', 'POST', { org, submissions });
|
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[]) {
|
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
|
// Onboarding Methods
|
||||||
async completeOnboarding(onboardingData: any): Promise<{ success: boolean; error?: string }> {
|
async completeOnboarding(onboardingData: any): Promise<{ success: boolean; error?: string }> {
|
||||||
return this.makeRequest('onboarding/complete', 'POST', onboardingData);
|
return this.makeRequest('onboarding/complete', 'POST', onboardingData);
|
||||||
|
|||||||
54
src/types.ts
54
src/types.ts
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { inherits } from 'util';
|
||||||
|
|
||||||
export enum Theme {
|
export enum Theme {
|
||||||
Light = 'light',
|
Light = 'light',
|
||||||
@@ -11,13 +12,12 @@ export interface Employee {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role?: 'owner' | 'admin' | 'employee';
|
role?: string; // Remove 'owner' from the union type since employees are never owners
|
||||||
department?: string | 'General';
|
department?: string | 'General';
|
||||||
joinedAt?: number;
|
joinedAt?: number;
|
||||||
status: 'invited' | 'active';
|
status: 'invited' | 'active';
|
||||||
initials?: string;
|
initials?: string;
|
||||||
inviteCode?: string;
|
inviteCode?: string;
|
||||||
isOwner?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -103,6 +103,12 @@ export interface Organization {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
onboardingCompleted: boolean;
|
onboardingCompleted: boolean;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
|
ownerInfo?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
joinedAt: number;
|
||||||
|
};
|
||||||
// Subscription fields (will be populated after Stripe setup)
|
// Subscription fields (will be populated after Stripe setup)
|
||||||
subscription: {
|
subscription: {
|
||||||
status: 'trial' | 'active' | 'past_due' | 'canceled';
|
status: 'trial' | 'active' | 'past_due' | 'canceled';
|
||||||
@@ -146,42 +152,24 @@ export interface EmployeeReport {
|
|||||||
selfAwareness: string;
|
selfAwareness: string;
|
||||||
emotionalResponses: string;
|
emotionalResponses: string;
|
||||||
growthDesire: 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[];
|
strengths: string[];
|
||||||
weaknesses: {
|
weaknesses: string[];
|
||||||
isCritical: boolean;
|
|
||||||
description: string;
|
|
||||||
}[];
|
|
||||||
opportunities: {
|
opportunities: {
|
||||||
roleAdjustment: string;
|
title: string;
|
||||||
accountabilitySupport: string;
|
description: string;
|
||||||
description?: string;
|
|
||||||
}[];
|
}[];
|
||||||
risks: string[];
|
risks: string[];
|
||||||
recommendations: string[];
|
recommendations: string[];
|
||||||
recommendation: {
|
gradingOverview: {
|
||||||
action: 'Keep' | 'Restructure' | 'Terminate';
|
employeeName: string;
|
||||||
details: string[];
|
|
||||||
};
|
|
||||||
grading: {
|
|
||||||
department: string;
|
|
||||||
lead: string;
|
|
||||||
support: string;
|
|
||||||
grade: string;
|
grade: string;
|
||||||
comment: string;
|
reliability: number;
|
||||||
scores: { subject: string; value: number; fullMark: number; }[];
|
roleFit: number;
|
||||||
}[];
|
scalability: number;
|
||||||
suitabilityScore?: number;
|
output: number;
|
||||||
retentionRisk?: 'Low' | 'Medium' | 'High';
|
initiative: number;
|
||||||
costEffectiveness?: 'Underperforming' | 'Aligned' | 'High Value';
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Submission {
|
export interface Submission {
|
||||||
@@ -240,7 +228,6 @@ export interface CompanyReport {
|
|||||||
reasoning: string;
|
reasoning: string;
|
||||||
urgency?: 'high' | 'medium' | 'low'; // UI alias
|
urgency?: 'high' | 'medium' | 'low'; // UI alias
|
||||||
}[];
|
}[];
|
||||||
recommendations: string[];
|
|
||||||
forwardOperatingPlan?: {
|
forwardOperatingPlan?: {
|
||||||
title: string;
|
title: string;
|
||||||
details: string[];
|
details: string[];
|
||||||
@@ -266,8 +253,6 @@ export interface CompanyReport {
|
|||||||
teamScores: {
|
teamScores: {
|
||||||
employeeName: string;
|
employeeName: string;
|
||||||
grade: string;
|
grade: string;
|
||||||
// Each of the following is out of 10, total being 50 points
|
|
||||||
// These gets displayed as radar charts
|
|
||||||
reliability: number;
|
reliability: number;
|
||||||
roleFit: number;
|
roleFit: number;
|
||||||
scalability: number;
|
scalability: number;
|
||||||
@@ -275,7 +260,6 @@ export interface CompanyReport {
|
|||||||
initiative: number;
|
initiative: number;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
executiveSummary: string;
|
executiveSummary: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user