From 1a9e92d7bd71113ab9573e6619404da1c9421349 Mon Sep 17 00:00:00 2001 From: Ra Date: Mon, 18 Aug 2025 19:08:29 -0700 Subject: [PATCH] Implement comprehensive report system with detailed viewing and AI enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add detailed report viewing with full-screen ReportDetail component for both company and employee reports - Fix company wiki to display onboarding Q&A in card format matching Figma designs - Exclude company owners from employee submission counts (owners contribute to wiki, not employee data) - Fix employee report generation to include company context (wiki + company report + employee answers) - Fix company report generation to use filtered employee submissions only - Add proper error handling for submission data format variations - Update Firebase functions to use gpt-4o model instead of deprecated gpt-4.1 - Fix UI syntax errors and improve report display functionality - Add comprehensive logging for debugging report generation flow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 7 +- App.tsx | 23 +- bun.lock | 33 +- constants.ts | 175 +++------ contexts/AuthContext.tsx | 93 +++-- contexts/OrgContext.tsx | 502 +++++++++++++++----------- contexts/UserOrganizationsContext.tsx | 45 ++- functions/index.js | 381 +++++++++++++++---- index.css | 121 ++++--- package.json | 5 +- pages/Chat.tsx | 39 +- pages/CompanyWiki.tsx | 182 +++++++--- pages/EmployeeData.tsx | 143 ++++++-- pages/EmployeeQuestionnaire.tsx | 71 ++-- pages/HelpAndSettings.tsx | 67 +++- pages/Onboarding.tsx | 13 +- pages/OrgSelection.tsx | 2 +- pages/ReportDetail.tsx | 413 +++++++++++++++++++++ services/api.ts | 88 +++++ services/firebase.ts | 25 +- 20 files changed, 1793 insertions(+), 635 deletions(-) create mode 100644 pages/ReportDetail.tsx create mode 100644 services/api.ts diff --git a/.gitignore b/.gitignore index 3af7395..475cbd1 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,9 @@ dist-ssr /.vscode/extensions.json !/.vscode/tasks.json /database.rules.json -/functions/auditly*.json \ No newline at end of file +/functions/auditly*.json +/CLAUDE.md +/debug_firebase.js +/schema.json +/SECURITY_FIXES.md +/.claude \ No newline at end of file diff --git a/App.tsx b/App.tsx index 706108d..cb6baeb 100644 --- a/App.tsx +++ b/App.tsx @@ -41,16 +41,28 @@ const RequireOrgSelection: React.FC<{ children: React.ReactNode }> = ({ children const RequireOnboarding: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { org } = useOrg(); const { user } = useAuth(); - const { organizations } = useUserOrganizations(); + const { organizations, selectedOrgId } = useUserOrganizations(); if (!org) return
Loading organization...
; - if (!org.onboardingCompleted) { - // Only org owners should be redirected to onboarding - const userOrgRelation = organizations.find(o => o.orgId === org.orgId); - const isOrgOwner = userOrgRelation?.role === 'owner'; + // Get the user's relationship to this organization + const userOrgRelation = organizations.find(o => o.orgId === selectedOrgId); + const isOrgOwner = userOrgRelation?.role === 'owner'; + // SINGLE SOURCE OF TRUTH: Organization onboarding completion is the authoritative source + // User organization records are updated to reflect this, but org.onboardingCompleted is primary + const onboardingCompleted = org.onboardingCompleted === true; + + console.log('RequireOnboarding check:', { + orgId: selectedOrgId, + orgOnboardingCompleted: org.onboardingCompleted, + userRole: userOrgRelation?.role, + finalDecision: onboardingCompleted + }); + + if (!onboardingCompleted) { if (isOrgOwner) { + console.log('Redirecting owner to onboarding'); return ; } else { // Non-owners should see a waiting message @@ -104,6 +116,7 @@ function App() { {/* Employee questionnaire - no auth needed, uses invite code */} } /> + } /> {/* Organization Selection - after auth, before entering app */} = 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], @@ -778,8 +769,6 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], @@ -796,7 +785,7 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], + "openai": ["openai@5.12.2", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -952,8 +941,6 @@ "vite": ["vite@7.1.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ=="], - "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], - "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -980,6 +967,8 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@4.0.17", "", {}, "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ=="], + "@google-cloud/storage/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], @@ -994,8 +983,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@types/request/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "firebase-functions/express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], @@ -1018,16 +1005,12 @@ "lru-memoizer/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "openai/@types/node": ["@types/node@18.19.122", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA=="], - "react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "teeny-request/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "@types/request/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "firebase-functions/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "firebase-functions/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], @@ -1060,12 +1043,8 @@ "lru-memoizer/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "@types/request/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "firebase-functions/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "firebase-functions/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], diff --git a/constants.ts b/constants.ts index 4fe2f9e..436c312 100644 --- a/constants.ts +++ b/constants.ts @@ -19,91 +19,52 @@ if (import.meta.env.DEV) { console.log(` API_URL: ${API_URL}`); } -export const EMPLOYEES: Employee[] = [ - { id: 'AG', name: 'Alex Green', initials: 'AG', email: 'alex.green@zitlac.com', department: 'Influencer Marketing', role: 'Influencer Coordinator & Business Development Outreach' }, - { id: 'MB', name: 'Michael Brown', initials: 'MB', email: 'michael.brown@zitlac.com', department: 'Engineering', role: 'Senior Developer' }, - { id: 'KT', name: 'Kevin Taylor', initials: 'KT', email: 'kevin.taylor@zitlac.com', department: 'Marketing', role: 'Marketing Manager' }, - { id: 'LR', name: 'Laura Robinson', initials: 'LR', email: 'laura.robinson@zitlac.com', department: 'HR', role: 'HR Manager', isOwner: true }, - { id: 'DS', name: 'David Stone', initials: 'DS', email: 'david.stone@zitlac.com', department: 'Sales', role: 'Sales Representative' }, - { id: 'SR', name: 'Samantha Reed', initials: 'SR', email: 'samantha.reed@zitlac.com', department: 'Operations', role: 'Operations Specialist' }, -]; +// DEPRECATED: These are legacy sample data that should not be used in production +// Real data should be generated via AI backend calls or user input +export const EMPLOYEES: Employee[] = []; +// DEPRECATED: Sample report data - real reports should be generated via /generateEmployeeReport API export const REPORT_DATA: Report = { - employeeId: 'AG', - department: 'Influencer Marketing', - role: 'Influencer Coordinator & Business Development Outreach', + employeeId: 'sample', + department: 'Sample Department', + role: 'Sample Role', roleAndOutput: { - responsibilities: 'Recruiting influencers, onboarding, campaign support, business development.', - clarityOnRole: '10/10 - Feels very clear on responsibilities.', - selfRatedOutput: '7/10 - Indicates decent performance but room to grow.', - recurringTasks: 'Influencer outreach, onboarding, communications.', + responsibilities: 'This is sample data. Real reports are generated via AI.', + clarityOnRole: 'Sample data', + selfRatedOutput: 'Sample data', + recurringTasks: 'Sample data', }, insights: { - personalityTraits: 'Loyal, well-liked by influencers, eager to grow, client-facing interest.', - psychologicalIndicators: [ - '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: 'High - acknowledges weaknesses like lateness and disorganization.', - emotionalResponses: 'Frustrated by campaign disorganization; would prefer closer collaboration.', - growthDesire: 'Interested in becoming more client-facing and shifting toward biz dev.', + personalityTraits: 'Sample data - use AI-generated reports', + psychologicalIndicators: ['Sample data'], + selfAwareness: 'Sample data', + emotionalResponses: 'Sample data', + growthDesire: 'Sample data', }, - strengths: [ - '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.', - ], + strengths: ['Sample data - use AI-generated reports'], weaknesses: [ - { isCritical: true, description: 'Disorganized and late with deliverables — confirmed by previous internal notes.' }, - { isCritical: false, description: 'Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.' }, - { isCritical: false, description: 'May unintentionally cause friction with campaigns team by stepping outside process boundaries.' }, + { isCritical: false, description: 'Sample data - use AI-generated reports' } ], opportunities: { - roleAdjustment: 'Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities.', - accountabilitySupport: "Pair with a high-output implementer (new hire) to balance Gentry's strategic skills.", + roleAdjustment: 'Sample data', + accountabilitySupport: 'Sample data', }, - risks: [ - "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.', - ], + risks: ['Sample data - use AI-generated reports'], recommendation: { - action: 'Keep', - details: [ - '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.", - ], + action: 'Sample', + details: ['Sample data - use AI-generated reports'], }, grading: [], }; +// DEPRECATED: Sample submission data - real submissions come from employee questionnaires export const SUBMISSIONS_DATA: Submission = { - employeeId: 'AG', + employeeId: 'sample', answers: [ { - question: 'What is the mission of your company?', - answer: 'To empower small businesses with AI-driven automation tools that increase efficiency and reduce operational overhead.', - }, - { - question: 'How has your mission evolved in the last 1-3 years?', - answer: 'We shifted from general SaaS tools to vertical-specific solutions, with deeper integrations and onboarding support.', - }, - { - question: 'What is your 5-year vision for the company?', - answer: 'To become the leading AI operations platform for SMBs in North America, serving over 100,000 customers.', - }, - { - question: "What are your company's top 3 strategic advantages?", - answer: '1. Fast product iteration enabled by in-house AI capabilities\n2. Deep customer understanding from vertical specialization\n3. High customer retention due to integrated onboarding', - }, - { - question: 'What are your biggest vulnerabilities or threats?', - answer: 'Dependence on a single marketing channel, weak middle management, and rising customer acquisition costs.', - }, + question: 'Sample question', + answer: 'Sample answer - real data comes from employee questionnaires', + } ], }; @@ -135,74 +96,38 @@ export const FAQ_DATA: FaqItem[] = [ ]; export const CHAT_STARTERS = [ - "Summarize Alex Green's latest report.", - "What are Alex's biggest strengths?", - "Identify any risks associated with Alex.", - "Should Alex be considered for a promotion?" + "Summarize the latest employee reports.", + "What are the company's organizational strengths?", + "Identify any risks in our current workforce.", + "Which employees should be considered for promotion?", + "What are our immediate hiring needs?", + "How can we improve team performance?" ]; +// DEPRECATED: This should not be used in production - real company reports are AI-generated export const SAMPLE_COMPANY_REPORT: CompanyReport = { - id: 'sample-company-report', - createdAt: Date.now() - 86400000, // 1 day ago + id: 'placeholder-report', + createdAt: Date.now(), overview: { - totalEmployees: 0, // Fixed: Start with 0 employees instead of hardcoded 6 + totalEmployees: 0, departmentBreakdown: [], submissionRate: 0, - lastUpdated: Date.now() - 86400000 + lastUpdated: Date.now(), + averagePerformanceScore: 0, + riskLevel: 'Unknown' }, gradingBreakdown: [], operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] }, personnelChanges: { newHires: [], promotions: [], departures: [] }, - keyPersonnelChanges: [ - { employeeName: "Alex Green", department: "Influencer Marketing", role: "Influencer Coordinator", changeType: "newHire" }, - { employeeName: "Jordan Smith", department: "Engineering", role: "Software Engineer", changeType: "promotion" } - ], - immediateHiringNeeds: [ - { - department: 'Engineering', - role: 'Frontend Developer', - priority: 'High', - reasoning: 'Growing product development workload requires additional frontend expertise' - }, - { - department: 'Marketing', - role: 'Content Creator', - priority: 'Medium', - reasoning: 'Increasing content demands for influencer campaigns' - } - ], + keyPersonnelChanges: [], + immediateHiringNeeds: [], forwardOperatingPlan: { - quarterlyGoals: [ - 'Expand influencer network by 40%', - 'Launch automated campaign tracking system', - 'Implement comprehensive onboarding process', - 'Increase team collaboration efficiency by 25%' - ], - resourceNeeds: [ - 'Additional engineering talent', - 'Enhanced project management tools', - 'Training budget for skill development', - 'Upgraded communication infrastructure' - ], - riskMitigation: [ - 'Cross-train team members to reduce single points of failure', - 'Implement backup processes for critical operations', - 'Regular performance reviews and feedback cycles', - 'Diversify client base to reduce dependency risks' - ] + quarterlyGoals: [], + resourceNeeds: [], + riskMitigation: [] }, - organizationalStrengths: [ - ], - organizationalRisks: [ - 'Key personnel dependency in critical roles', - 'Limited project management oversight', - 'Potential burnout from rapid growth', - 'Communication gaps between departments' - ], - gradingOverview: { - "overallGrade": 4, - "strengths": 3, - "weaknesses": 1 - }, - executiveSummary: `Your organization is ready to get started with employee assessments. Begin by inviting team members to complete their questionnaires and build comprehensive insights about your workforce.` + organizationalStrengths: [], + organizationalRisks: [], + gradingOverview: {}, + executiveSummary: `Welcome to Auditly! Generate your first AI-powered company report by inviting employees and completing the onboarding process.` }; \ No newline at end of file diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx index bf441d8..2a80bce 100644 --- a/contexts/AuthContext.tsx +++ b/contexts/AuthContext.tsx @@ -24,25 +24,55 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children useEffect(() => { console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured); - if (!isFirebaseConfigured) { - // Demo mode: check for persisted session - console.log('Demo mode: checking for persisted session'); - const sessionUser = sessionStorage.getItem('auditly_demo_session'); + + if (isFirebaseConfigured) { + // Firebase mode: Set up proper Firebase auth state listener + const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => { + console.log('Firebase auth state changed:', firebaseUser?.email); + if (firebaseUser) { + setUser(firebaseUser); + } else { + // Check for OTP session as fallback + const sessionUser = localStorage.getItem('auditly_demo_session'); + if (sessionUser) { + try { + const parsedUser = JSON.parse(sessionUser); + console.log('Restoring OTP session for:', parsedUser.email); + setUser(parsedUser as User); + } catch (error) { + console.error('Failed to parse session user:', error); + localStorage.removeItem('auditly_demo_session'); + setUser(null); + } + } else { + setUser(null); + } + } + setLoading(false); + }); + + return unsubscribe; + } else { + // Demo/OTP mode: Check localStorage for persisted session + console.log('Checking for persisted OTP session'); + const sessionUser = localStorage.getItem('auditly_demo_session'); if (sessionUser) { - const parsedUser = JSON.parse(sessionUser); - console.log('Restoring demo session for:', parsedUser.email); - setUser(parsedUser as User); + try { + const parsedUser = JSON.parse(sessionUser); + console.log('Restoring session for:', parsedUser.email); + setUser(parsedUser as User); + } catch (error) { + console.error('Failed to parse session user:', error); + localStorage.removeItem('auditly_demo_session'); + setUser(null); + } + } else { + setUser(null); } setLoading(false); + return () => { }; } - console.log('Setting up Firebase auth listener'); - const unsub = onAuthStateChanged(auth, (u) => { - console.log('Auth state changed:', u); - setUser(u); - setLoading(false); - }); - return () => unsub(); }, []); const signInWithGoogle = async () => { @@ -54,13 +84,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; const signOutUser = async () => { - if (!isFirebaseConfigured) { - // Clear demo session - sessionStorage.removeItem('auditly_demo_session'); - setUser(null); - return; + try { + // Sign out from Firebase if configured and user is signed in via Firebase + if (isFirebaseConfigured && auth.currentUser) { + await signOut(auth); + console.log('Firebase signout completed'); + } + } catch (error) { + console.error('Firebase signout error:', error); } - await signOut(auth); + + // Always clear all local session data + localStorage.removeItem('auditly_demo_session'); + localStorage.removeItem('auditly_auth_token'); + localStorage.removeItem('auditly_selected_org'); + sessionStorage.clear(); + + setUser(null); + console.log('User signed out and all sessions cleared'); }; const signInWithEmail = async (email: string, password: string) => { @@ -79,7 +120,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } as unknown as User; setUser(mockUser); - sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); + localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); console.log('Demo login successful for:', email); } else { throw new Error('Invalid password'); @@ -132,7 +173,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } as unknown as User; setUser(mockUser); - sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); + localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); console.log('Demo signup successful for:', email); return; } @@ -191,8 +232,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } as unknown as User; setUser(mockUser); - sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); - sessionStorage.setItem('auditly_auth_token', data.token); + localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); + localStorage.setItem('auditly_auth_token', data.token); return data; }; @@ -206,8 +247,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } as unknown as User; setUser(mockUser); - sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); - sessionStorage.setItem('auditly_auth_token', token); + localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); + localStorage.setItem('auditly_auth_token', token); }; return ( diff --git a/contexts/OrgContext.tsx b/contexts/OrgContext.tsx index 6bbda74..5b57052 100644 --- a/contexts/OrgContext.tsx +++ b/contexts/OrgContext.tsx @@ -3,8 +3,9 @@ import { collection, doc, getDoc, getDocs, onSnapshot, setDoc } from 'firebase/f import { db, isFirebaseConfigured } from '../services/firebase'; import { useAuth } from './AuthContext'; import { Employee, Report, Submission, CompanyReport } from '../types'; -import { REPORT_DATA, SUBMISSIONS_DATA, SAMPLE_COMPANY_REPORT, API_URL } from '../constants'; +import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants'; import { demoStorage } from '../services/demoStorage'; +import { apiPost, apiPut } from '../services/api'; interface OrgData { orgId: string; @@ -98,65 +99,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s // Initialize with empty employee list for clean start // (Removed automatic seeding of 6 default employees per user feedback) - // Create sample submissions for multiple employees - const sampleSubmissions = [ - { - employeeId: 'AG', - orgId, - createdAt: Date.now(), - answers: { - role_clarity: "I understand my role very clearly as Influencer Coordinator & Business Development Outreach.", - key_outputs: "Recruited 15 new influencers, managed 8 campaigns, initiated 3 business development partnerships.", - bottlenecks: "Campaign organization could be better, sometimes unclear on priorities between recruiting and outreach.", - hidden_talent: "Strong relationship building skills that could be leveraged for client-facing work.", - retention_risk: "Happy with the company but would like more structure and clearer processes.", - energy_distribution: "50% influencer recruiting, 30% campaign support, 20% business development outreach.", - performance_indicators: "Good influencer relationships, but delivery timeline improvements needed.", - workflow: "Morning outreach, afternoon campaign work, weekly business development calls." - } - }, - { - employeeId: 'MB', - orgId, - createdAt: Date.now(), - answers: { - role_clarity: "I understand my role as a Senior Developer very clearly. I'm responsible for architecting solutions, code reviews, and mentoring junior developers.", - key_outputs: "Delivered 3 major features this quarter, reduced technical debt by 20%, and led code review process improvements.", - bottlenecks: "Sometimes waiting for design specs from the product team, and occasional deployment pipeline issues.", - hidden_talent: "I have strong business analysis skills and could help bridge the gap between technical and business requirements.", - retention_risk: "I'm satisfied with my current role and compensation. The only concern would be limited growth opportunities.", - energy_distribution: "80% development work, 15% mentoring, 5% planning and architecture.", - performance_indicators: "Code quality metrics improved, zero production bugs in my recent releases, positive peer feedback.", - workflow: "Morning standup, focused coding blocks, afternoon reviews and collaboration, weekly planning sessions." - } - }, - { - employeeId: 'KT', - orgId, - createdAt: Date.now(), - answers: { - role_clarity: "My role as Marketing Manager is clear - I oversee campaigns, analyze performance metrics, and coordinate with sales.", - key_outputs: "Launched 5 successful campaigns this quarter, increased lead quality by 30%, improved attribution tracking.", - bottlenecks: "Limited budget for premium tools, sometimes slow approval process for creative assets.", - hidden_talent: "I have experience with data science and could help build predictive models for customer behavior.", - retention_risk: "Overall happy, but would like more strategic input in product positioning and pricing decisions.", - energy_distribution: "40% campaign execution, 30% analysis and reporting, 20% strategy, 10% team coordination.", - performance_indicators: "Campaign ROI improved by 25%, lead conversion rates increased, better cross-team collaboration.", - workflow: "Weekly campaign planning, daily performance monitoring, bi-weekly strategy reviews, monthly board reporting." - } - } - ]; + // Don't automatically create sample submissions - let users create real data + // through the proper questionnaire flow - // Save all sample submissions - sampleSubmissions.forEach(submission => { - demoStorage.saveSubmission(submission); - }); + // Note: Sample employee reports removed - real reports generated via AI after questionnaire submission - // Save sample employee report (only for AG initially) - demoStorage.saveEmployeeReport(orgId, REPORT_DATA.employeeId, REPORT_DATA); - - // Save sample company report - demoStorage.saveCompanyReport(orgId, SAMPLE_COMPANY_REPORT); + // Don't save sample company report - let users generate real AI-powered reports } // Load persistent demo data @@ -175,7 +123,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s })); setEmployees(convertedEmployees); - // Convert submissions to expected format + // Load any existing submissions from localStorage const orgSubmissions = demoStorage.getSubmissionsByOrg(orgId); const convertedSubmissions: Record = {}; Object.entries(orgSubmissions).forEach(([employeeId, demoSub]) => { @@ -189,11 +137,11 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s }); setSubmissions(convertedSubmissions); - // Convert reports to expected format + // Load any existing AI-generated reports from localStorage const orgReports = demoStorage.getEmployeeReportsByOrg(orgId); setReports(orgReports); - // Get company reports + // Load any existing company reports from localStorage const companyReports = demoStorage.getCompanyReportsByOrg(orgId); setFullCompanyReports(companyReports); return; @@ -239,20 +187,29 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s const updatedOrg = { ...(org || { orgId, name: 'Demo Company' }), ...data } as OrgData; setOrg(updatedOrg); - // Also sync with server for multi-tenant persistence - try { - const response = await fetch(`${API_URL}/api/organizations/${orgId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) + // If onboarding was completed, update localStorage for persistence and notify other contexts + if (data.onboardingCompleted) { + const demoOrgData = { + orgId: updatedOrg.orgId, + name: updatedOrg.name, + onboardingCompleted: updatedOrg.onboardingCompleted || false, + ...updatedOrg // Include all additional fields + }; + demoStorage.saveOrganization(demoOrgData); + + console.log('OrgContext: Onboarding completed, dispatching update event', { + orgId: updatedOrg.orgId, + onboardingCompleted: true }); - if (!response.ok) { - console.warn('Failed to sync organization data with server'); - } - } catch (error) { - console.warn('Failed to sync organization data:', error); + // Signal to UserOrganizationsContext and other components about completion + window.dispatchEvent(new CustomEvent('organizationUpdated', { + detail: { orgId: updatedOrg.orgId, onboardingCompleted: true } + })); } + + // Organization already exists, no need to sync with server during onboarding + // We'll update Firestore directly in the Firebase mode below } else { // Firebase mode - save to Firestore const orgRef = doc(db, 'orgs', orgId); @@ -261,6 +218,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s // Update local state const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData; setOrg(updatedOrg); + + // If onboarding was completed, notify other contexts + if (data.onboardingCompleted) { + console.log('OrgContext (Firebase): Onboarding completed, dispatching update event', { + orgId: updatedOrg.orgId, + onboardingCompleted: true + }); + + window.dispatchEvent(new CustomEvent('organizationUpdated', { + detail: { orgId: updatedOrg.orgId, onboardingCompleted: true } + })); + } } }; @@ -302,39 +271,57 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s }; const inviteEmployee = async ({ name, email }: { name: string; email: string }) => { - // Always use Cloud Functions for invites to ensure multi-tenant compliance - const response = await fetch(`${API_URL}/createInvitation`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, email, orgId }) - }); + console.log('inviteEmployee called:', { name, email, orgId }); - if (!response.ok) { - throw new Error(`Failed to create invite: ${response.status}`); + try { + // Always use Cloud Functions for invites to ensure multi-tenant compliance + const res = await apiPost('/createInvitation', { + name, + email + }, orgId); + + if (!res.ok) { + const errorData = await res.json(); + console.error('Invite creation failed:', errorData); + throw new Error(errorData.error || `Failed to create invite: ${res.status}`); + } + + const data = await res.json(); + const { code, employee, inviteLink } = data; + + console.log('Invite created successfully:', { code, employee: employee.name, inviteLink }); + + // Store employee locally for immediate UI update with proper typing + const newEmployee: Employee = { + id: employee.id, + name: employee.name, + email: employee.email, + initials: employee.name ? employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : employee.email.substring(0, 2).toUpperCase(), + department: employee.department, + role: employee.role, + isOwner: false + }; + + if (!isFirebaseConfigured) { + const employeeWithOrg = { ...newEmployee, orgId }; + setEmployees(prev => { + if (prev.find(e => e.id === employee.id)) return prev; + return [...prev, newEmployee]; + }); + demoStorage.saveEmployee(employeeWithOrg); + } else { + // For Firebase, add to local state for immediate UI update + setEmployees(prev => { + if (prev.find(e => e.id === employee.id)) return prev; + return [...prev, newEmployee]; + }); + } + + return { employeeId: employee.id, inviteLink }; + } catch (error) { + console.error('inviteEmployee error:', error); + throw error; } - - const data = await response.json(); - const { code, employee, inviteLink } = data; - - // Store employee locally for immediate UI update - if (!isFirebaseConfigured) { - const newEmployee = { ...employee, orgId }; - setEmployees(prev => { - if (prev.find(e => e.id === employee.id)) return prev; - return [...prev, newEmployee]; - }); - demoStorage.saveEmployee(newEmployee); - } else { - // For Firebase, the employee will be created when they accept the invite - // But we can add them to local state for immediate UI update - const newEmployee = { ...employee, orgId }; - setEmployees(prev => { - if (prev.find(e => e.id === employee.id)) return prev; - return [...prev, newEmployee]; - }); - } - - return { employeeId: employee.id, inviteLink }; }; const getReportVersions = async (employeeId: string) => { @@ -407,27 +394,36 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s const seedInitialData = async () => { if (!isFirebaseConfigured) { - // Start with empty employee list for clean demo experience + // Start with completely clean slate - no sample data setEmployees([]); - setSubmissions({ [SUBMISSIONS_DATA.employeeId]: SUBMISSIONS_DATA }); - setReports({ [REPORT_DATA.employeeId]: REPORT_DATA }); - setFullCompanyReports([SAMPLE_COMPANY_REPORT]); + setSubmissions({}); + setReports({}); + setFullCompanyReports([]); return; } - // Start with clean slate - let users invite their own employees - // (Removed automatic seeding per user feedback) + // Start with clean slate - let users invite their own employees and generate real data }; const saveFullCompanyReport = async (report: CompanyReport) => { - if (!isFirebaseConfigured) { + if (!orgId) { + console.error('Cannot save company report: orgId is undefined'); + throw new Error('Organization ID is required to save company report'); + } + + if (!isFirebaseConfigured || !db) { + // Fallback to local storage in demo mode setFullCompanyReports(prev => [report, ...prev]); - // Persist to localStorage demoStorage.saveCompanyReport(orgId, report); return; } + + // Use direct Firestore operations - much more efficient const ref = doc(db, 'orgs', orgId, 'fullCompanyReports', report.id); await setDoc(ref, report); + + // Update local state after successful save + setFullCompanyReports(prev => [report, ...prev]); }; const getFullCompanyReportHistory = async (): Promise => { @@ -444,91 +440,132 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s }; const generateCompanyReport = async (): Promise => { - // Generate comprehensive company report based on current data - const totalEmployees = employees.length; - const submittedEmployees = Object.keys(submissions).length; + console.log('generateCompanyReport called for org:', orgId); + + // Calculate concrete metrics from actual data (no AI needed) + // Exclude owners from employee counts - they are company wiki contributors, not employees + const actualEmployees = employees.filter(emp => !emp.isOwner); + const totalEmployees = actualEmployees.length; + + // Only count submissions from non-owner employees + const employeeSubmissions = Object.fromEntries( + Object.entries(submissions).filter(([employeeId]) => { + const employee = employees.find(emp => emp.id === employeeId); + return employee && !employee.isOwner; + }) + ); + const submittedEmployees = Object.keys(employeeSubmissions).length; const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0; - // Department breakdown + // Department breakdown (concrete data) - exclude owners const deptMap = new Map(); - employees.forEach(emp => { + actualEmployees.forEach(emp => { const dept = emp.department || 'Unassigned'; deptMap.set(dept, (deptMap.get(dept) || 0) + 1); }); const departmentBreakdown = Array.from(deptMap.entries()).map(([department, count]) => ({ department, count })); - // Analyze employee reports for insights - const reportValues = Object.values(reports) as Report[]; - const organizationalStrengths: string[] = []; - const organizationalRisks: string[] = []; + try { + // Use AI only for analysis and insights that require reasoning + const res = await apiPost('/generateCompanyWiki', { + org: org, + submissions: employeeSubmissions, // Only employee submissions, not owner data + metrics: { + totalEmployees, + submissionRate, + departmentBreakdown + } + }, orgId); - reportValues.forEach(report => { - if (report.strengths) { - organizationalStrengths.push(...report.strengths); + if (!res.ok) { + const errorData = await res.json(); + console.error('Company report generation failed:', errorData); + throw new Error(errorData.error || 'Failed to generate company report'); } - if (report.risks) { - organizationalRisks.push(...report.risks); - } - }); - // Remove duplicates and take top items - const uniqueStrengths = [...new Set(organizationalStrengths)].slice(0, 5); - const uniqueRisks = [...new Set(organizationalRisks)].slice(0, 5); + const data = await res.json(); + console.log('Company insights generated via AI successfully'); + console.log('AI response data:', data); - const gradingBreakdown = [ - { category: 'Execution', value: 70 + Math.random() * 15 }, - { category: 'People', value: 70 + Math.random() * 15 }, - { category: 'Strategy', value: 65 + Math.random() * 15 }, - { category: 'Risk', value: 60 + Math.random() * 15 } - ]; - const legacy = gradingBreakdown.reduce>((acc, g) => { acc[g.category.toLowerCase()] = Math.round((g.value / 100) * 5 * 10) / 10; return acc; }, {}); - const report: CompanyReport = { - id: Date.now().toString(), - createdAt: Date.now(), - overview: { - totalEmployees, - departmentBreakdown, - submissionRate, - lastUpdated: Date.now(), - averagePerformanceScore: gradingBreakdown.reduce((a, g) => a + g.value, 0) / gradingBreakdown.length / 20, - riskLevel: uniqueRisks.length > 4 ? 'High' : uniqueRisks.length > 2 ? 'Medium' : 'Low' - }, - personnelChanges: { newHires: [], promotions: [], departures: [] }, - immediateHiringNeeds: [], - operatingPlan: { - nextQuarterGoals: ['Increase productivity', 'Implement review system'], - keyInitiatives: ['Mentorship program'], - resourceNeeds: ['Senior engineer'], - riskMitigation: ['Cross-training'] - }, - forwardOperatingPlan: { // legacy fields - quarterlyGoals: ['Increase productivity'], - resourceNeeds: ['Senior engineer'], - riskMitigation: ['Cross-training'] - }, - organizationalStrengths: uniqueStrengths.map(s => ({ area: s, description: s })), - organizationalRisks: uniqueRisks, - organizationalImpactSummary: 'Impact summary placeholder', - gradingBreakdown, - gradingOverview: legacy, - executiveSummary: `Company overview for ${org?.name || 'Organization'} as of ${new Date().toLocaleDateString()}. Total workforce: ${totalEmployees}. Submission rate: ${submissionRate.toFixed(1)}%. Key strengths: ${uniqueStrengths.slice(0, 2).join(', ')}. Risks: ${uniqueRisks.slice(0, 2).join(', ')}.` - }; + // Combine concrete metrics with AI insights + const report: CompanyReport = { + id: Date.now().toString(), + createdAt: Date.now(), + // Use AI-generated insights for subjective analysis + ...data.report, + // Override with our concrete metrics + overview: { + totalEmployees, + departmentBreakdown, + submissionRate, + lastUpdated: Date.now(), + averagePerformanceScore: data.report?.overview?.averagePerformanceScore || 0, + riskLevel: data.report?.overview?.riskLevel || 'Unknown' + } + }; - await saveFullCompanyReport(report); - return report; + console.log('Final company report object:', report); + await saveFullCompanyReport(report); + return report; + } catch (error) { + console.error('generateCompanyReport error:', error); + throw error; + } }; const generateCompanyWiki = async (orgOverride?: OrgData): Promise => { const orgData = orgOverride || org; + console.log('generateCompanyWiki called with:', { orgData, orgId, submissionsCount: Object.keys(submissions || {}).length, isFirebaseConfigured }); + + if (!orgId) { + throw new Error('Organization ID is required to generate company wiki'); + } + + // ALWAYS use API call for wiki generation, with local fallback try { - const res = await fetch(`${API_URL}/generateCompanyWiki`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ org: orgData, submissions }) - }); - if (!res.ok) throw new Error('Failed to generate company wiki'); + console.log('Making API call to generateCompanyWiki...'); + const res = await apiPost('/generateCompanyWiki', { + org: orgData, + submissions: submissions || [] + }, orgId); + + console.log('API response status:', res.status); + + if (!res.ok) { + const errorData = await res.json(); + console.error('API error response:', errorData); + throw new Error(errorData.error || 'Failed to generate company wiki'); + } + const payload = await res.json(); - const data: CompanyReport = payload.report || payload; // backward compatibility + console.log('API success response:', payload); + + // Ensure the report has all required fields to prevent undefined errors + const data: CompanyReport = { + id: Date.now().toString(), + createdAt: Date.now(), + overview: { + totalEmployees: employees.length, + departmentBreakdown: [], + submissionRate: 0, + lastUpdated: Date.now(), + averagePerformanceScore: 0, + riskLevel: 'Unknown' + }, + gradingBreakdown: [], + operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] }, + personnelChanges: { newHires: [], promotions: [], departures: [] }, + keyPersonnelChanges: [], + immediateHiringNeeds: [], + forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] }, + organizationalStrengths: [], + organizationalRisks: [], + gradingOverview: {}, + executiveSummary: 'Company report generated successfully.', + // Override with API data if available + ...(payload.report || payload) + }; + await saveFullCompanyReport(data); return data; } catch (e) { @@ -605,12 +642,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s isOwner, issueInviteViaApi: async ({ name, email, role, department }) => { try { - const res = await fetch(`${API_URL}/createInvitation`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, email, role, department, orgId }) - }); - if (!res.ok) throw new Error('invite creation failed'); + const res = await apiPost('/createInvitation', { + name, + email, + role, + department + }, orgId); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || 'Invite creation failed'); + } + const json = await res.json(); // Optimistically add employee shell (not yet active until consume) setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, { ...json.employee }]); @@ -685,19 +728,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s // Save to localStorage for persistence demoStorage.saveSubmission(submission); - // Also call Cloud Function for processing with orgId + // Also call Cloud Function for processing with authentication and orgId const employee = employees.find(e => e.id === employeeId); - const res = await fetch(`${API_URL}/submitEmployeeAnswers`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - employeeId, - answers, - orgId, - employee - }) - }); - if (!res.ok) throw new Error('Failed to submit to server'); + const res = await apiPost('/submitEmployeeAnswers', { + employeeId, + answers, + employee + }, orgId); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || 'Failed to submit to server'); + } // Update local state for UI with proper typing const convertedSubmission: Submission = { @@ -727,22 +769,76 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s }, generateEmployeeReport: async (employee: Employee) => { try { - const submission = submissions[employee.id]?.answers || submissions[employee.id] || {}; - const res = await fetch(`${API_URL}/generateEmployeeReport`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ employee, submission }) - }); - if (!res.ok) throw new Error('failed to generate'); + console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId); + + // Get submission data for this employee + const submission = submissions[employee.id]; + if (!submission) { + throw new Error(`No questionnaire submission found for ${employee.name}. Please ensure they have completed the employee questionnaire first.`); + } + + // Convert submission format for API + let submissionAnswers: Record = {}; + if (submission.answers) { + if (Array.isArray(submission.answers)) { + // If answers is an array of {question, answer} objects + submissionAnswers = submission.answers.reduce((acc, item) => { + acc[item.question] = item.answer; + return acc; + }, {} as Record); + } else { + // If answers is already a key-value object + submissionAnswers = submission.answers as Record; + } + } + + console.log('Submission data found:', Object.keys(submissionAnswers).length, 'answers'); + + // Get company report and wiki data for context + let companyWiki = null; + try { + const companyReports = await getFullCompanyReportHistory(); + if (companyReports.length > 0) { + companyWiki = { + org: org, + companyReport: companyReports[0] + }; + console.log('Including company context in employee report generation'); + } + } catch (error) { + console.warn('Could not fetch company report for context:', error); + } + + const res = await apiPost('/generateEmployeeReport', { + employee, + submission: submissionAnswers, + companyWiki + }, orgId); + + if (!res.ok) { + const errorData = await res.json(); + console.error('API error response:', errorData); + throw new Error(errorData.error || 'Failed to generate employee report'); + } + const json = await res.json(); if (json.report) { + console.log('Employee report generated successfully'); setReports(prev => ({ ...prev, [employee.id]: json.report })); + + // Also save to persistent storage in demo mode + if (!isFirebaseConfigured) { + demoStorage.saveEmployeeReport(orgId, employee.id, json.report); + } + return json.report as 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 } - return null; }, getEmployeeReport: async (employeeId: string) => { try { diff --git a/contexts/UserOrganizationsContext.tsx b/contexts/UserOrganizationsContext.tsx index 3300f43..9bb510d 100644 --- a/contexts/UserOrganizationsContext.tsx +++ b/contexts/UserOrganizationsContext.tsx @@ -70,9 +70,9 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }> } }; - // Initialize selected org from session storage + // Initialize selected org from localStorage (persistent across sessions) useEffect(() => { - const savedOrgId = sessionStorage.getItem('auditly_selected_org'); + const savedOrgId = localStorage.getItem('auditly_selected_org'); if (savedOrgId) { setSelectedOrgId(savedOrgId); } @@ -83,9 +83,48 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }> loadOrganizations(); }, [user]); + // Listen for organization updates (e.g., onboarding completion) + useEffect(() => { + const handleOrgUpdate = (event: CustomEvent) => { + const { orgId, onboardingCompleted } = event.detail; + console.log('UserOrganizationsContext received org update:', { orgId, onboardingCompleted }); + + if (onboardingCompleted && orgId) { + // Update the specific organization in the list to reflect onboarding completion + setOrganizations(prev => { + const updated = prev.map(org => + org.orgId === orgId + ? { ...org, onboardingCompleted: true } + : org + ); + console.log('Updated organizations after onboarding completion:', updated); + return updated; + }); + } + }; + + window.addEventListener('organizationUpdated', handleOrgUpdate as EventListener); + + return () => { + window.removeEventListener('organizationUpdated', handleOrgUpdate as EventListener); + }; + }, []); + const selectOrganization = (orgId: string) => { + console.log('Switching to organization:', orgId); + + // Clear any cached data when switching organizations for security + sessionStorage.removeItem('auditly_cached_employees'); + sessionStorage.removeItem('auditly_cached_submissions'); + sessionStorage.removeItem('auditly_cached_reports'); + setSelectedOrgId(orgId); - sessionStorage.setItem('auditly_selected_org', orgId); + localStorage.setItem('auditly_selected_org', orgId); + + // Dispatch event to notify other contexts about the org switch + window.dispatchEvent(new CustomEvent('organizationChanged', { + detail: { newOrgId: orgId } + })); }; const createOrganization = async (name: string): Promise<{ orgId: string; requiresSubscription?: boolean }> => { diff --git a/functions/index.js b/functions/index.js index 08ebbdb..0f9d3b9 100644 --- a/functions/index.js +++ b/functions/index.js @@ -20,6 +20,158 @@ const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SEC apiVersion: '2024-11-20.acacia', }) : null; +const RESPONSE_FORMAT = { + type: "json_schema", + json_schema: { + name: "company_artifacts", + strict: true, + schema: { + type: "object", + additionalProperties: false, + properties: { + report: { + type: "object", + additionalProperties: false, + properties: { + companyPerformance: { + type: "object", + additionalProperties: false, + properties: { + summary: { type: "string" }, + metrics: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + name: { type: "string" }, + value: { anyOf: [{ type: "string" }, { type: "number" }] }, + trend: { enum: ["up", "down", "flat"] } + }, + required: ["name", "value", "trend"] + } + } + }, + required: ["summary", "metrics"] + }, + keyPersonnelChanges: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + person: { type: "string" }, + change: { type: "string" }, // e.g. "Promoted to VP Eng" + impact: { type: "string" }, + effectiveDate: { type: "string" } + }, + required: ["person", "change", "impact", "effectiveDate"] + } + }, + immediateHiringNeeds: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + role: { type: "string" }, + urgency: { enum: ["low", "medium", "high"] }, + reason: { type: "string" } + }, + required: ["role", "urgency", "reason"] + } + }, + forwardOperatingPlan: { + type: "object", + additionalProperties: false, + properties: { + nextQuarterObjectives: { type: "array", items: { type: "string" } }, + initiatives: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + name: { type: "string" }, + owner: { type: "string" }, + kpis: { type: "array", items: { type: "string" } } + }, + required: ["name", "owner", "kpis"] + } + }, + risks: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + risk: { type: "string" }, + mitigation: { type: "string" } + }, + required: ["risk", "mitigation"] + } + } + }, + required: ["nextQuarterObjectives", "initiatives", "risks"] + }, + organizationalInsights: { + type: "object", + additionalProperties: false, + properties: { + culture: { type: "string" }, + teamDynamics: { type: "string" }, + blockers: { type: "array", items: { type: "string" } } + }, + required: ["culture", "teamDynamics", "blockers"] + }, + strengths: { type: "array", items: { type: "string" } }, + gradingOverview: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + department: { type: "string" }, + grade: { enum: ["A", "B", "C", "D", "F"] }, + notes: { type: "string" } + }, + required: ["department", "grade", "notes"] + } + } + }, + required: ["companyPerformance", "keyPersonnelChanges", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths", "gradingOverview"] + }, + wiki: { + type: "object", + additionalProperties: false, + properties: { + companyName: { type: "string" }, + industry: { type: "string" }, + description: { type: "string" }, + mission: { type: "string" }, + values: { type: "array", items: { type: "string" } }, + culture: { type: "string" }, + orgInfo: { + type: "object", + additionalProperties: false, + properties: { + hq: { type: "string" }, + foundedYear: { type: "number" }, + headcount: { type: "number" }, + products: { type: "array", items: { type: "string" } } + }, + required: ["hq", "foundedYear", "headcount", "products"] + } + }, + required: ["companyName", "industry", "description", "mission", "values", "culture", "orgInfo"] + } + }, + required: ["report", "wiki"] + } + } +}; + + // Helper function to generate OTP const generateOTP = () => { return Math.floor(100000 + Math.random() * 900000).toString(); @@ -29,7 +181,7 @@ const generateOTP = () => { const cors = (req, res, next) => { res.set('Access-Control-Allow-Origin', '*'); res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.set('Access-Control-Allow-Headers', 'Content-Type'); + res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization'); res.set('Access-Control-Max-Age', '3600'); if (req.method === 'OPTIONS') { @@ -44,7 +196,7 @@ const cors = (req, res, next) => { const setCorsHeaders = (res) => { res.set('Access-Control-Allow-Origin', '*'); res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.set('Access-Control-Allow-Headers', 'Content-Type'); + res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization'); res.set('Access-Control-Max-Age', '3600'); }; @@ -235,17 +387,31 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => { return res.status(405).json({ error: "Method not allowed" }); } - const { orgId, email, role = "employee" } = req.body; + const { orgId, name, email, role = "employee", department } = req.body; - if (!orgId || !email) { - return res.status(400).json({ error: "Organization ID and email are required" }); + if (!orgId || !email || !name) { + return res.status(400).json({ error: "Organization ID, name, and email are required" }); } try { // Generate invite code const code = Math.random().toString(36).substring(2, 15); + + // Generate employee ID + const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - // Store invitation + // Create employee object for the invite + const employee = { + id: employeeId, + name: name.trim(), + email: email.trim(), + role: role?.trim() || "employee", + department: department?.trim() || "General", + status: "invited", + inviteCode: code + }; + + // Store invitation with employee data const inviteRef = await db .collection("orgs") .doc(orgId) @@ -254,20 +420,29 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => { await inviteRef.set({ code, + employee, email, - role, orgId, status: "pending", createdAt: Date.now(), expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days }); + // Generate invite links + const baseUrl = process.env.CLIENT_URL || 'http://localhost:5174'; + const inviteLink = `${baseUrl}/#/employee-form/${code}`; + const emailLink = `mailto:${email}?subject=You're invited to join our organization&body=Hi ${name},%0A%0AYou've been invited to complete a questionnaire for our organization. Please click the link below to get started:%0A%0A${inviteLink}%0A%0AThis link will expire in 7 days.%0A%0AThank you!`; + // In production, send actual invitation email - console.log(`📧 Invitation sent to ${email} with code: ${code}`); + console.log(`📧 Invitation sent to ${email} (${name}) with code: ${code}`); + console.log(`📧 Invite link: ${inviteLink}`); res.json({ success: true, - inviteCode: code, + code, + employee, + inviteLink, + emailLink, message: "Invitation sent successfully", }); } catch (error) { @@ -315,6 +490,8 @@ exports.getInvitationStatus = functions.https.onRequest(async (req, res) => { res.json({ success: true, + used: invite.status !== 'pending', + employee: invite.employee, invite, }); } catch (error) { @@ -338,8 +515,8 @@ exports.consumeInvitation = functions.https.onRequest(async (req, res) => { const { code, userId } = req.body; - if (!code || !userId) { - return res.status(400).json({ error: "Invitation code and user ID are required" }); + if (!code) { + return res.status(400).json({ error: "Invitation code is required" }); } try { @@ -362,25 +539,34 @@ exports.consumeInvitation = functions.https.onRequest(async (req, res) => { return res.status(400).json({ error: "Invitation has expired" }); } + // Get employee data from the invite + const employee = invite.employee; + if (!employee) { + return res.status(400).json({ error: "Invalid invitation data - missing employee information" }); + } + // Mark invitation as consumed await inviteDoc.ref.update({ status: "consumed", - consumedBy: userId, + consumedBy: employee.id, consumedAt: Date.now(), }); - // Add user to organization employees + // Add employee to organization using data from invite await db .collection("orgs") .doc(invite.orgId) .collection("employees") - .doc(userId) + .doc(employee.id) .set({ - id: userId, - email: invite.email, - role: invite.role, + id: employee.id, + name: employee.name || employee.email.split("@")[0], + email: employee.email, + role: employee.role || "employee", + department: employee.department || "General", joinedAt: Date.now(), status: "active", + inviteCode: code, }); res.json({ @@ -407,25 +593,59 @@ exports.submitEmployeeAnswers = functions.https.onRequest(async (req, res) => { return res.status(405).json({ error: "Method not allowed" }); } - const { orgId, employeeId, answers } = req.body; + const { orgId, employeeId, answers, inviteCode } = req.body; - if (!orgId || !employeeId || !answers) { - return res.status(400).json({ error: "Organization ID, employee ID, and answers are required" }); + // For invite-based submissions, we need inviteCode and answers + // For regular submissions, we need orgId, employeeId, and answers + if (inviteCode) { + if (!inviteCode || !answers) { + return res.status(400).json({ error: "Invite code and answers are required for invite submissions" }); + } + } else { + if (!orgId || !employeeId || !answers) { + return res.status(400).json({ error: "Organization ID, employee ID, and answers are required" }); + } } try { + let finalOrgId, finalEmployeeId; + + if (inviteCode) { + // For invite-based submissions, look up the invite to get employee and org data + const inviteSnapshot = await db + .collectionGroup("invites") + .where("code", "==", inviteCode) + .where("status", "==", "consumed") + .limit(1) + .get(); + + if (inviteSnapshot.empty) { + return res.status(404).json({ error: "Invitation not found or not consumed yet" }); + } + + const invite = inviteSnapshot.docs[0].data(); + finalOrgId = invite.orgId; + finalEmployeeId = invite.employee.id; + } else { + // Regular submission + finalOrgId = orgId; + finalEmployeeId = employeeId; + } + // Store submission const submissionRef = await db .collection("orgs") - .doc(orgId) + .doc(finalOrgId) .collection("submissions") - .doc(employeeId); + .doc(finalEmployeeId); await submissionRef.set({ - employeeId, + employeeId: finalEmployeeId, answers, submittedAt: Date.now(), status: "completed", + submissionType: inviteCode ? "invite" : "regular", + ...(inviteCode && { inviteCode }) }); res.json({ @@ -597,62 +817,40 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => { if (openai) { // Use OpenAI to generate the company report and wiki - const prompt = ` -You are an expert business analyst. Generate a comprehensive company report and wiki based on the following data: - -Organization Information: -${JSON.stringify(org, null, 2)} - -Employee Submissions: -${JSON.stringify(submissions, null, 2)} - -Generate a detailed analysis with two main components: - -1. COMPANY REPORT with: -- companyPerformance: Overall performance metrics and trends -- keyPersonnelChanges: Recent personnel moves and their impact -- immediateHiringNeeds: Urgent staffing requirements -- forwardOperatingPlan: Strategic planning for next quarter -- organizationalInsights: Team dynamics and cultural health -- strengths: Company strengths -- gradingOverview: Performance breakdown by department - -2. COMPANY WIKI with: -- companyName, industry, description -- mission, values, culture -- Key organizational information - -Return ONLY valid JSON with 'report' and 'wiki' objects. Be thorough and professional. - `.trim(); + const system = "You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema."; + const user = [ + "Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.", + "Be thorough and professional.", + "", + "Organization Information:", + JSON.stringify(org, null, 2), + "", + "Employee Submissions:", + JSON.stringify(submissions, null, 2) + ].join("\n"); const completion = await openai.chat.completions.create({ model: "gpt-4o", + temperature: 0, // consistency + response_format: RESPONSE_FORMAT, messages: [ - { - role: "system", - content: "You are an expert business analyst. Generate comprehensive company reports and wikis in JSON format." - }, - { - role: "user", - content: prompt - } - ], - response_format: { type: "json_object" }, - temperature: 0.7, + { role: "system", content: system }, + { role: "user", content: user } + ] }); - const aiResponse = completion.choices[0].message.content; - const parsedResponse = JSON.parse(aiResponse); + // content is guaranteed to be schema-conformant JSON + const parsed = JSON.parse(completion.choices[0].message.content); - report = { + const report = { generatedAt: Date.now(), - ...parsedResponse.report + ...parsed.report }; - wiki = { - companyName: org.name, + const wiki = { + companyName: org?.name ?? parsed.wiki.companyName, generatedAt: Date.now(), - ...parsedResponse.wiki + ...parsed.wiki, }; } else { // Fallback to mock data when OpenAI is not available @@ -1451,4 +1649,49 @@ async function handlePaymentFailed(invoice) { // } else { // response.status(400).json({ error: 'Invalid verification code' }); // } -// }); \ No newline at end of file +// }); + +// Save Company Report Function +exports.saveCompanyReport = functions.https.onRequest(async (req, res) => { + setCorsHeaders(res); + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { orgId, report } = req.body; + + if (!orgId || !report) { + return res.status(400).json({ error: "Organization ID and report are required" }); + } + + try { + // Add ID and timestamp if not present + if (!report.id) { + report.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + if (!report.createdAt) { + report.createdAt = Date.now(); + } + + // Save to Firestore + const reportRef = db.collection("orgs").doc(orgId).collection("fullCompanyReports").doc(report.id); + await reportRef.set(report); + + console.log(`Company report saved successfully for org ${orgId}`); + + res.json({ + success: true, + reportId: report.id, + message: "Company report saved successfully" + }); + } catch (error) { + console.error("Save company report error:", error); + res.status(500).json({ error: "Failed to save company report" }); + } +}); \ No newline at end of file diff --git a/index.css b/index.css index d17f165..9bfb30a 100644 --- a/index.css +++ b/index.css @@ -1,55 +1,63 @@ @import "tailwindcss"; :root { - /* Light theme variables - using new Figma color palette */ - --background-primary : #FFFFFF; - /* Base White */ - --background-secondary : #FDFDFD; - /* Gray 6 */ - --background-tertiary : #FAFAFA; - /* Gray 5 */ - --text-primary : #0A0D12; - /* Dark 7 */ - --text-secondary : #717680; - /* Dark 2 */ - --text-tertiary : #A4A7AE; - /* Gray 1 */ - --accent : #5E48FC; - /* Brand Main */ - --accent-hover : #4C3CF0; - /* Slightly darker brand */ - --accent-text : #FFFFFF; - /* Base White */ - --border-color : #E9EAEB; - /* Gray 3 */ - --border-light : #F5F5F5; - /* Gray 4 */ - --sidebar-bg : #FDFDFD; - /* Gray 6 */ - --sidebar-text : #717680; - /* Dark 2 */ - --sidebar-active-bg : #5E48FC; - /* Brand Main */ - --sidebar-active-text : #FFFFFF; - /* Base White */ - --input-bg : #F5F5F5; - /* Gray 4 */ - --input-border : #E9EAEB; - /* Gray 3 */ - --input-placeholder : #717680; - /* Dark 2 */ + + + --background-primary : #FFFFFF; + --background-secondary: #FDFDFD; + --background-tertiary : #FAFAFA; + + --text-primary : #0A0D12; + --text-secondary: #717680; + --text-tertiary : #A4A7AE; + + --accent : ##3399FF; + --accent-hover: #4C3CF0; + --accent-text : #FFFFFF; + + --border-color: #E9EAEB; + --border-light: #F5F5F5; + + --sidebar-bg : #FDFDFD; + --sidebar-text : #717680; + --sidebar-active-bg : #3399FF; + --sidebar-active-text: #FFFFFF; + + --input-bg : #F5F5F5; + --input-border : #E9EAEB; + --input-placeholder: #717680; + --button-secondary-bg : #F5F5F5; - /* Gray 4 */ --button-secondary-hover: #E9EAEB; - /* Gray 3 */ - --status-red : #F63D68; - /* Other Red */ - --status-green : #3CCB7F; - /* Other Green */ - --status-orange : #FF4405; - /* Other Orange */ - --status-yellow : #FEEE95; - /* Other Yellow */ + + --color-red : #F63D68; + --color-green : #3CCB7F; + --color-orange : #FF4405; + --color-light-orange: #F38744; + --color-yellow : #FEEE95; + + --gray-0 : #FFFFFF; + --gray-50 : #F7F7F8; + --gray-100: #F1F2F4; + --gray-200: #E2E5E9; + --gray-300: #CBD0D7; + --gray-400: #99A1AE; + --gray-500: #6C7889; + --gray-600: #515A67; + --gray-700: #2D3239; + --gray-800: #24282E; + --gray-900: #121417; + + --neutral-100: #A4A7AE; + --neutral-200: #D5D7DA; + --neutral-300: #E9EAEB; + --neutral-400: #f5f5f5; + --neutral-500: #FAFAFA; + --neutral-600: #FDFDFD; + --neutral-700: #FEFEFE; + + --button-bg-primary : #39F; + --button-border-primary: #66B2FF; } .dark { @@ -102,6 +110,25 @@ /* Other Orange */ --status-yellow : #FEEE95; /* Other Yellow */ + --neutral-200 : #717670; + --neutral-300 : #535862; + --neutral-400 : #414651; + --neutral-500 : #252B37; + --neutral-600 : #181D27; + --neutral-700 : #0A0D12; + + --gray-900: #FFFFFF; + --gray-800: #F7F7F8; + --gray-700: #F1F2F4; + --gray-600: #E2E5E9; + --gray-500: #CBD0D7; + --gray-400: #99A1AE; + --gray-300: #6C7889; + --gray-200: #515A67; + --gray-100: #2D3239; + --gray-50 : #24282E; + --gray-0 : #121417; + } * { diff --git a/package.json b/package.json index f83c317..ce7c144 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,14 @@ "firebase-admin": "^13.4.0", "firebase-functions": "^6.4.0", "lucide-react": "^0.539.0", - "openai": "^4.104.0", + "openai": "^5.12.2", "postcss": "^8.5.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.0", "recharts": "^3.1.2", - "tailwindcss": "^4.1.12" + "tailwindcss": "^4.1.12", + "zod": "^4.0.17" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/pages/Chat.tsx b/pages/Chat.tsx index 96f3ba1..ebc511d 100644 --- a/pages/Chat.tsx +++ b/pages/Chat.tsx @@ -2,9 +2,10 @@ import React, { useState, useMemo } from 'react'; import { Card, Button } from '../components/UiKit'; import { useOrg } from '../contexts/OrgContext'; import { CHAT_STARTERS } from '../constants'; +import { apiPost } from '../services/api'; const Chat: React.FC = () => { - const { employees, reports, generateEmployeeReport } = useOrg(); + const { employees, reports, generateEmployeeReport, orgId } = useOrg(); const [messages, setMessages] = useState>([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -34,16 +35,44 @@ const Chat: React.FC = () => { setInput(''); setIsLoading(true); - // Simulate AI response (placeholder for server /api/chat usage) - setTimeout(() => { + try { + // Build context for the AI + const context = { + selectedEmployee: selectedEmployeeId ? employees.find(e => e.id === selectedEmployeeId) : null, + selectedReport: selectedReport, + totalEmployees: employees.length, + organizationScope: !selectedEmployeeId + }; + + const res = await apiPost('/chat', { + message: textToSend, + employeeId: selectedEmployeeId, + context + }, orgId); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || 'Failed to get AI response'); + } + + const data = await res.json(); const aiResponse = { id: (Date.now() + 1).toString(), role: 'assistant' as const, - text: `Based on ${selectedEmployeeId ? 'the selected employee\'s' : 'organizational'} data, here's an insight related to: "${textToSend}".` + text: data.response || 'I apologize, but I couldn\'t generate a response at this time.' }; setMessages(prev => [...prev, aiResponse]); + } catch (error) { + console.error('Chat error:', error); + const errorResponse = { + id: (Date.now() + 1).toString(), + role: 'assistant' as const, + text: `I apologize, but I encountered an error: ${error.message}. Please try again.` + }; + setMessages(prev => [...prev, errorResponse]); + } finally { setIsLoading(false); - }, 1500); + } }; return ( diff --git a/pages/CompanyWiki.tsx b/pages/CompanyWiki.tsx index 7876ac1..40f8b62 100644 --- a/pages/CompanyWiki.tsx +++ b/pages/CompanyWiki.tsx @@ -113,19 +113,19 @@ const CompanyWiki: React.FC = () => {
Employees
-
{companyReport.overview.departmentBreakdown.length}
+
{companyReport.overview?.departmentBreakdown?.length || 0}
Departments
-
{companyReport.organizationalStrengths.length}
+
{companyReport.organizationalStrengths?.length || 0}
Strength Areas
-
{companyReport.organizationalRisks.length}
+
{companyReport.organizationalRisks?.length || 0}
Risks
- {companyReport.gradingOverview && ( + {(Array.isArray(companyReport.gradingOverview) && companyReport.gradingOverview.length > 0) && (
{

Strengths

    - {companyReport.organizationalStrengths.map((s: any, i) =>
  • • {s.area}
  • )} + {(companyReport.organizationalStrengths || []).map((s: any, i) =>
  • • {s.area || s}
  • )}

Risks

    - {companyReport.organizationalRisks.map((r, i) =>
  • • {r}
  • )} + {(companyReport.organizationalRisks || []).map((r, i) =>
  • • {r}
  • )}
@@ -172,72 +172,152 @@ const CompanyWiki: React.FC = () => {
)} - {/* Company Profile - Onboarding Data */} - -

Company Profile

-
+ {/* Company Profile - Q&A Format from Onboarding */} +
+

Company Profile

+
{org?.mission && ( -
-

Mission

-

{org.mission}

-
+ +
+
+

Question:

+

What is your company's mission?

+
+
+

Answer:

+

{org.mission}

+
+
+
)} {org?.vision && ( -
-

Vision

-

{org.vision}

-
+ +
+
+

Question:

+

What is your company's vision?

+
+
+

Answer:

+

{org.vision}

+
+
+
)} {org?.evolution && ( -
-

Company Evolution

-

{org.evolution}

-
+ +
+
+

Question:

+

How has your company evolved over time?

+
+
+

Answer:

+

{org.evolution}

+
+
+
)} {org?.advantages && ( -
-

Competitive Advantages

-

{org.advantages}

-
+ +
+
+

Question:

+

What are your competitive advantages?

+
+
+

Answer:

+

{org.advantages}

+
+
+
)} {org?.vulnerabilities && ( -
-

Vulnerabilities

-

{org.vulnerabilities}

-
+ +
+
+

Question:

+

What are your key vulnerabilities?

+
+
+

Answer:

+

{org.vulnerabilities}

+
+
+
)} {org?.shortTermGoals && ( -
-

Short Term Goals

-

{org.shortTermGoals}

-
+ +
+
+

Question:

+

What are your short-term goals?

+
+
+

Answer:

+

{org.shortTermGoals}

+
+
+
)} {org?.longTermGoals && ( -
-

Long Term Goals

-

{org.longTermGoals}

-
+ +
+
+

Question:

+

What are your long-term goals?

+
+
+

Answer:

+

{org.longTermGoals}

+
+
+
)} {org?.cultureDescription && ( -
-

Culture

-

{org.cultureDescription}

-
+ +
+
+

Question:

+

How would you describe your company culture?

+
+
+

Answer:

+

{org.cultureDescription}

+
+
+
)} {org?.workEnvironment && ( -
-

Work Environment

-

{org.workEnvironment}

-
+ +
+
+

Question:

+

What is your work environment like?

+
+
+

Answer:

+

{org.workEnvironment}

+
+
+
)} {org?.additionalContext && ( -
-

Additional Context

-

{org.additionalContext}

-
+ +
+
+

Question:

+

Any additional context about your company?

+
+
+

Answer:

+

{org.additionalContext}

+
+
+
)}
- +
{org?.description && ( diff --git a/pages/EmployeeData.tsx b/pages/EmployeeData.tsx index 31a7d5b..28bd23c 100644 --- a/pages/EmployeeData.tsx +++ b/pages/EmployeeData.tsx @@ -5,6 +5,7 @@ import { CompanyReport, Employee, Report } from '../types'; import RadarPerformanceChart from '../components/charts/RadarPerformanceChart'; import ScoreBarList from '../components/charts/ScoreBarList'; import { SAMPLE_COMPANY_REPORT } from '../constants'; +import ReportDetail from './ReportDetail'; interface EmployeeDataProps { mode: 'submissions' | 'reports'; @@ -47,7 +48,7 @@ const CompanyReportCard: React.FC<{ report: CompanyReport }> = ({ report }) => {

Departments

-

{report.overview.departmentBreakdown.length}

+

{report.overview.departmentBreakdown.length}

Avg Performance

@@ -219,7 +220,8 @@ const EmployeeCard: React.FC<{ isOwner: boolean; onGenerateReport?: (employee: Employee) => void; isGeneratingReport?: boolean; -}> = ({ employee, report, mode, isOwner, onGenerateReport, isGeneratingReport }) => { + onViewReport?: (report: Report, employeeName: string) => void; +}> = ({ employee, report, mode, isOwner, onGenerateReport, isGeneratingReport, onViewReport }) => { const [isExpanded, setIsExpanded] = useState(false); return ( @@ -243,13 +245,22 @@ const EmployeeCard: React.FC<{
{report && ( - + <> + + + )} {isOwner && mode === 'reports' && (
{/* Company Report - Only visible to owners in reports mode */} - {currentUserIsOwner && mode === 'reports' && companyReport && ( - + {currentUserIsOwner && mode === 'reports' && ( +
+ {companyReport ? ( +
+
+

Company Report

+
+ + +
+
+ +
+ ) : ( + +
+

+ Generate Company Report +

+

+ Create a comprehensive AI-powered report analyzing your organization's performance, + strengths, and recommendations based on employee data. +

+ +
+
+ )} +
)} {/* Employee Cards */} @@ -442,10 +504,21 @@ const EmployeeData: React.FC = ({ mode }) => { isOwner={currentUserIsOwner} onGenerateReport={handleGenerateReport} isGeneratingReport={generatingReports.has(employee.id)} + onViewReport={(report, employeeName) => setSelectedReport({ report, type: 'employee', employeeName })} /> )) )} + + {/* Report Detail Modal */} + {selectedReport && ( + setSelectedReport(null)} + /> + )} ); }; diff --git a/pages/EmployeeQuestionnaire.tsx b/pages/EmployeeQuestionnaire.tsx index 594b2de..2f3ba30 100644 --- a/pages/EmployeeQuestionnaire.tsx +++ b/pages/EmployeeQuestionnaire.tsx @@ -15,17 +15,28 @@ const EmployeeQuestionnaire: React.FC = () => { const location = useLocation(); const params = useParams(); const { user } = useAuth(); - const { submitEmployeeAnswers, generateEmployeeReport, employees } = useOrg(); + + // Check if this is an invite-based flow (no auth/org needed) + const inviteCode = params.inviteCode; + const isInviteFlow = !!inviteCode; + + // Only use org context for authenticated flows + let submitEmployeeAnswers, generateEmployeeReport, employees; + if (!isInviteFlow) { + const orgContext = useOrg(); + ({ submitEmployeeAnswers, generateEmployeeReport, employees } = orgContext); + } else { + // For invite flows, we don't need these functions from org context + submitEmployeeAnswers = null; + generateEmployeeReport = null; + employees = []; + } const [answers, setAnswers] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(''); const [inviteEmployee, setInviteEmployee] = useState(null); const [isLoadingInvite, setIsLoadingInvite] = useState(false); - // Check if this is an invite-based flow (no auth needed) - const inviteCode = params.inviteCode; - const isInviteFlow = !!inviteCode; - // Load invite details if this is an invite flow useEffect(() => { if (inviteCode) { @@ -36,15 +47,24 @@ const EmployeeQuestionnaire: React.FC = () => { const loadInviteDetails = async (code: string) => { setIsLoadingInvite(true); try { - const response = await fetch(`${API_URL}/api/invitations/${code}`); + // Use Cloud Function endpoint for invite status + const response = await fetch(`${API_URL}/getInvitationStatus?code=${code}`); if (response.ok) { const data = await response.json(); - setInviteEmployee(data.employee); - setError(''); + if (data.used) { + setError('This invitation has already been used'); + } else if (data.employee) { + setInviteEmployee(data.employee); + setError(''); + } else { + setError('Invalid invitation data'); + } } else { - setError('Invalid or expired invitation link'); + const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); + setError(errorData.error || 'Invalid or expired invitation link'); } } catch (err) { + console.error('Error loading invite details:', err); setError('Failed to load invitation details'); } finally { setIsLoadingInvite(false); @@ -120,30 +140,39 @@ const EmployeeQuestionnaire: React.FC = () => { })); }; - const submitViaInvite = async (employee: any, answers: EmployeeSubmissionAnswers, inviteCode: string) => { + const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => { try { // First, consume the invite to mark it as used - const consumeResponse = await fetch(`${API_URL}/api/invitations/${inviteCode}/consume`, { - method: 'POST' + const consumeResponse = await fetch(`${API_URL}/consumeInvitation`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: inviteCode + }) }); if (!consumeResponse.ok) { throw new Error('Failed to process invitation'); } - // Submit the questionnaire answers - const submitResponse = await fetch(`${API_URL}/api/employee-submissions`, { + // Get orgId from the consume response + const consumeData = await consumeResponse.json(); + const orgId = consumeData.orgId; + + // Submit the questionnaire answers using Cloud Function + const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - employeeId: employee.id, - employee: employee, - answers: answers + inviteCode: inviteCode, + answers: answers, + orgId: orgId }) }); if (!submitResponse.ok) { - throw new Error('Failed to submit questionnaire'); + const errorData = await submitResponse.json(); + throw new Error(errorData.error || 'Failed to submit questionnaire'); } const result = await submitResponse.json(); @@ -223,7 +252,7 @@ const EmployeeQuestionnaire: React.FC = () => { let result; if (isInviteFlow) { // Direct API submission for invite flow (no auth needed) - result = await submitViaInvite(currentEmployee, answers, inviteCode); + result = await submitViaInvite(answers, inviteCode); } else { // Use org context for authenticated flow result = await submitEmployeeAnswers(currentEmployee.id, answers); @@ -338,7 +367,7 @@ const EmployeeQuestionnaire: React.FC = () => { -
+ { e.preventDefault(); handleSubmit(); }}>
{visibleQuestions.map((question, index) => ( { disabled={isSubmitting || getProgressPercentage() < 70} className="px-8 py-3" > - {isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'} + {isSubmitting ? 'Submitting...' : 'Submit'}
diff --git a/pages/HelpAndSettings.tsx b/pages/HelpAndSettings.tsx index 68acb1e..5e02a20 100644 --- a/pages/HelpAndSettings.tsx +++ b/pages/HelpAndSettings.tsx @@ -45,7 +45,12 @@ const HelpAndSettings: React.FC = () => { department: inviteForm.department.trim() || undefined }); - setInviteResult(`Invitation sent! Share this link: ${result.inviteLink}`); + setInviteResult(JSON.stringify({ + success: true, + inviteLink: result.inviteLink, + emailLink: result.emailLink, + employeeName: result.employee.name + })); setInviteForm({ name: '', email: '', role: '', department: '' }); } catch (error) { console.error('Failed to send invitation:', error); @@ -178,11 +183,61 @@ const HelpAndSettings: React.FC = () => { {inviteResult && ( -
- {inviteResult} +
+ {inviteResult.includes('Failed') ? ( +
+ {inviteResult} +
+ ) : ( + (() => { + try { + const result = JSON.parse(inviteResult); + return ( +
+

+ ✅ Invitation sent to {result.employeeName}! +

+
+
+ +
+ + +
+
+ +
+
+ ); + } catch { + return ( +
+ {inviteResult} +
+ ); + } + })() + )}
)} diff --git a/pages/Onboarding.tsx b/pages/Onboarding.tsx index 5d1bfe6..ae96ea7 100644 --- a/pages/Onboarding.tsx +++ b/pages/Onboarding.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useOrg } from '../contexts/OrgContext'; import { Card, Button } from '../components/UiKit'; @@ -48,6 +48,13 @@ interface OnboardingData { const Onboarding: React.FC = () => { const { org, upsertOrg, generateCompanyWiki } = useOrg(); const navigate = useNavigate(); + + useEffect(() => { + if (org?.onboardingCompleted) { + navigate('/reports', { replace: true }); + } + }, [org, navigate]); + const [step, setStep] = useState(0); const [isGeneratingReport, setIsGeneratingReport] = useState(false); const [formData, setFormData] = useState({ @@ -152,7 +159,7 @@ const Onboarding: React.FC = () => { console.log('Org data saved successfully'); console.log('Generating company wiki...'); - await generateCompanyWiki(newOrgData); + await generateCompanyWiki({ ...newOrgData, orgId: org!.orgId }); console.log('Company wiki generated successfully'); // Small delay to ensure states are updated, then redirect @@ -674,7 +681,7 @@ const Onboarding: React.FC = () => { {/* Progress indicator */}
- ({ number: i + 1, title: s.title }))} /> diff --git a/pages/OrgSelection.tsx b/pages/OrgSelection.tsx index 3f74e77..873c5da 100644 --- a/pages/OrgSelection.tsx +++ b/pages/OrgSelection.tsx @@ -41,7 +41,7 @@ const OrgSelection: React.FC = () => { const handleSelectOrg = (orgId: string) => { selectOrganization(orgId); - + // Check if the organization needs onboarding completion const selectedOrg = organizations.find(org => org.orgId === orgId); if (selectedOrg && !selectedOrg.onboardingCompleted && selectedOrg.role === 'owner') { diff --git a/pages/ReportDetail.tsx b/pages/ReportDetail.tsx new file mode 100644 index 0000000..c71d80c --- /dev/null +++ b/pages/ReportDetail.tsx @@ -0,0 +1,413 @@ +import React from 'react'; +import { Card, Button } from '../components/UiKit'; +import { CompanyReport, Report } from '../types'; +import RadarPerformanceChart from '../components/charts/RadarPerformanceChart'; +import ScoreBarList from '../components/charts/ScoreBarList'; + +interface ReportDetailProps { + report: CompanyReport | Report; + type: 'company' | 'employee'; + employeeName?: string; + onClose: () => void; +} + +const ReportDetail: React.FC = ({ report, type, employeeName, onClose }) => { + if (type === 'company') { + const companyReport = report as CompanyReport; + return ( +
+
+
+
+

Company Report

+

Last updated: {new Date(companyReport.createdAt).toLocaleDateString()}

+
+
+ + +
+
+ +
+ {/* Executive Summary */} + +

Executive Summary

+

+ {companyReport.executiveSummary} +

+
+ + {/* Overview Stats */} + +

Company Overview

+
+
+
{companyReport.overview.totalEmployees}
+
Total Employees
+
+
+
{companyReport.overview.departmentBreakdown?.length || 0}
+
Departments
+
+
+
{companyReport.overview.averagePerformanceScore || 'N/A'}
+
Avg Performance
+
+
+
{companyReport.overview.riskLevel || 'Low'}
+
Risk Level
+
+
+
+ + {/* Key Personnel Changes */} + {companyReport.keyPersonnelChanges && companyReport.keyPersonnelChanges.length > 0 && ( + +

+ + Key Personnel Changes +

+
+ {companyReport.keyPersonnelChanges.map((change, idx) => ( +
+
+
+

{change.employeeName}

+

{change.role} • {change.department}

+
+ + {change.changeType} + +
+

{change.impact}

+
+ ))} +
+
+ )} + + {/* Immediate Hiring Needs */} + {companyReport.immediateHiringNeeds && companyReport.immediateHiringNeeds.length > 0 && ( + +

+ + Immediate Hiring Needs +

+
+ {companyReport.immediateHiringNeeds.map((need, idx) => ( +
+
+

{need.role}

+ + {need.urgency} + +
+

{need.department}

+

{need.reason}

+
+ ))} +
+
+ )} + + {/* Forward Operating Plan */} + {companyReport.forwardOperatingPlan && ( + +

+ + Forward Operating Plan +

+
+
+

Next Quarter Goals

+
    + {companyReport.forwardOperatingPlan.nextQuarterGoals?.map((goal, idx) => ( +
  • + + {goal} +
  • + ))} +
+
+
+

Key Initiatives

+
    + {companyReport.forwardOperatingPlan.keyInitiatives?.map((initiative, idx) => ( +
  • + + {initiative} +
  • + ))} +
+
+
+
+ )} + + {/* Organizational Strengths */} + {companyReport.organizationalStrengths && companyReport.organizationalStrengths.length > 0 && ( + +

+ + Organizational Strengths +

+
+ {companyReport.organizationalStrengths.map((strength, idx) => ( +
+
+ {strength.icon || '💪'} +
+

{strength.area || strength}

+

{strength.description}

+
+
+
+ ))} +
+
+ )} + + {/* Grading Overview */} + {companyReport.gradingOverview && ( + +

+ + Grading Overview +

+
+ {Object.entries(companyReport.gradingOverview).map(([category, score], idx) => ( +
+
{score}/5
+
+ {category.replace(/([A-Z])/g, ' $1').trim()} +
+
+ ))} +
+
+ )} + + {/* Organizational Impact Summary */} + {companyReport.organizationalImpactSummary && ( + +

+ + Organizational Impact Summary +

+
+

+ {companyReport.organizationalImpactSummary} +

+
+
+ )} +
+
+
+ ); + } else { + const employeeReport = report as Report; + return ( +
+
+
+
+

{employeeName}'s Performance Report

+

{employeeReport.employee?.role} • {employeeReport.employee?.department}

+
+
+ + +
+
+ +
+ {/* Self-Reported Role & Output */} + {employeeReport.roleAndOutput && ( + +

Self-Reported Role & Output

+
+
+

Responsibilities

+

{employeeReport.roleAndOutput.responsibilities}

+
+
+
+

Clarity on Role

+

{employeeReport.roleAndOutput.clarityOnRole}

+
+
+

Self-Rated Output

+

{employeeReport.roleAndOutput.selfRatedOutput}

+
+
+
+
+ )} + + {/* Performance Charts */} + {employeeReport.grading?.[0]?.scores && ( + +

Performance Analysis

+
+
+ ({ + label: s.subject, + value: (s.value / s.fullMark) * 100 + }))} + /> +
+
+ ({ + label: s.subject, + value: s.value, + max: s.fullMark + }))} + /> +
+
+
+ )} + + {/* Behavioral & Psychological Insights */} + {employeeReport.insights && ( + +

Behavioral & Psychological Insights

+
+
+

Personality Traits

+

+ {employeeReport.insights.personalityTraits || 'No personality traits data available.'} +

+ +

Self-awareness

+

+ {employeeReport.insights.selfAwareness || 'No self-awareness data available.'} +

+
+
+

Psychological Indicators

+
    + {employeeReport.insights.psychologicalIndicators?.map((indicator, idx) => ( +
  • + + {indicator} +
  • + )) ||
  • No psychological indicators available.
  • } +
+ +

Growth Desire

+

+ {employeeReport.insights.growthDesire || 'No growth desire data available.'} +

+
+
+
+ )} + + {/* Strengths & Weaknesses */} +
+ +

+ + Strengths +

+
+ {employeeReport.insights?.strengths?.map((strength, idx) => ( +
+ + {strength} +
+ ))} +
+
+ + +

+ + Development Areas +

+
+ {employeeReport.insights?.weaknesses?.map((weakness, idx) => ( +
+ ! + {weakness} +
+ ))} +
+
+
+ + {/* Opportunities */} + {employeeReport.opportunities && employeeReport.opportunities.length > 0 && ( + +

+ + Opportunities +

+
+ {employeeReport.opportunities.map((opp, idx) => ( +
+

{opp.roleAdjustment || 'Opportunity'}

+

{opp.accountabilitySupport || opp.description}

+
+ ))} +
+
+ )} + + {/* Risks */} + {employeeReport.risks && employeeReport.risks.length > 0 && ( + +

+ + Risks +

+
+ {employeeReport.risks.map((risk, idx) => ( +
+ + {risk} +
+ ))} +
+
+ )} + + {/* Recommendations */} + {employeeReport.recommendations && employeeReport.recommendations.length > 0 && ( + +

+ + Recommendations +

+
+ {employeeReport.recommendations.map((rec, idx) => ( +
+ + {rec} +
+ ))} +
+
+ )} +
+
+
+ ); + } +}; + +export default ReportDetail; \ No newline at end of file diff --git a/services/api.ts b/services/api.ts new file mode 100644 index 0000000..95019f6 --- /dev/null +++ b/services/api.ts @@ -0,0 +1,88 @@ +import { API_URL } from '../constants'; + +// Get auth token from localStorage (persistent across sessions) +const getAuthToken = (): string | null => { + return localStorage.getItem('auditly_auth_token'); +}; + +// Make authenticated API call +export const makeAuthenticatedRequest = async ( + endpoint: string, + options: RequestInit = {}, + orgId?: string +): Promise => { + const token = getAuthToken(); + + if (!token) { + throw new Error('No authentication token found. Please log in again.'); + } + + const headers = new Headers(options.headers); + headers.set('Authorization', `Bearer ${token}`); + headers.set('Content-Type', 'application/json'); + + // Add orgId to request body if provided and it's a POST/PUT request + let body = options.body; + if (orgId && (options.method === 'POST' || options.method === 'PUT')) { + if (typeof body === 'string') { + try { + const parsedBody = JSON.parse(body); + parsedBody.orgId = orgId; + body = JSON.stringify(parsedBody); + } catch (e) { + // If body isn't JSON, create a new object + body = JSON.stringify({ orgId, data: body }); + } + } else if (body instanceof FormData) { + body.append('orgId', orgId); + } else if (body && typeof body === 'object') { + body = JSON.stringify({ orgId, ...body }); + } else { + body = JSON.stringify({ orgId }); + } + } + + const url = endpoint.startsWith('http') ? endpoint : `${API_URL}${endpoint}`; + + // Add timeout to prevent hanging forever + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout + + try { + const response = await fetch(url, { + ...options, + headers, + body, + signal: controller.signal, + }); + clearTimeout(timeoutId); + return response; + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error('Request timeout: The server took too long to respond'); + } + throw error; + } +}; + +// Convenience methods for different HTTP verbs +export const apiGet = (endpoint: string, orgId?: string) => + makeAuthenticatedRequest(endpoint, { method: 'GET' }, orgId); + +export const apiPost = (endpoint: string, data: any = {}, orgId?: string) => { + console.log('apiPost called with:', { endpoint, data, orgId, hasToken: !!getAuthToken() }); + return makeAuthenticatedRequest(endpoint, { + method: 'POST', + body: JSON.stringify(data) + }, orgId); +}; + +export const apiPut = (endpoint: string, data: any = {}, orgId?: string) => + makeAuthenticatedRequest(endpoint, { + method: 'PUT', + body: JSON.stringify(data) + }, orgId); + +export const apiDelete = (endpoint: string, orgId?: string) => + makeAuthenticatedRequest(endpoint, { method: 'DELETE' }, orgId); \ No newline at end of file diff --git a/services/firebase.ts b/services/firebase.ts index 0048786..f57a4fa 100644 --- a/services/firebase.ts +++ b/services/firebase.ts @@ -1,6 +1,6 @@ import { initializeApp, getApps } from 'firebase/app'; -import { getAuth, GoogleAuthProvider } from 'firebase/auth'; -import { getFirestore } from 'firebase/firestore'; +import { getAuth, GoogleAuthProvider, setPersistence, browserLocalPersistence } from 'firebase/auth'; +import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'; const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, @@ -17,9 +17,7 @@ export const isFirebaseConfigured = Boolean( firebaseConfig.projectId && firebaseConfig.apiKey !== 'undefined' && firebaseConfig.authDomain !== 'undefined' && - firebaseConfig.projectId !== 'undefined' && - // Force demo mode on localhost for testing - !window.location.hostname.includes('localhost') + firebaseConfig.projectId !== 'undefined' ); let app; @@ -30,12 +28,29 @@ let googleProvider; if (isFirebaseConfigured) { app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; auth = getAuth(app); + setPersistence(auth, browserLocalPersistence); db = getFirestore(app); googleProvider = new GoogleAuthProvider(); + + // Connect to emulator in development + const isLocalhost = typeof window !== 'undefined' && + (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); + + if (isLocalhost && !db._settings?.host?.includes('localhost')) { + try { + connectFirestoreEmulator(db, 'localhost', 5003); + console.log('🔥 Connected to Firestore emulator on localhost:5003'); + } catch (error) { + console.log('⚠️ Firestore emulator already connected or connection failed'); + } + } + + console.log('🔥 Firebase initialized successfully'); } else { auth = null; db = null; googleProvider = null; + console.log('⚠️ Firebase not configured - missing environment variables'); } export { auth, db, googleProvider };