Implement comprehensive report system with detailed viewing and AI enhancements

- 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 <noreply@anthropic.com>
This commit is contained in:
Ra
2025-08-18 19:08:29 -07:00
parent 557b113196
commit 1a9e92d7bd
20 changed files with 1793 additions and 635 deletions

5
.gitignore vendored
View File

@@ -51,3 +51,8 @@ dist-ssr
!/.vscode/tasks.json !/.vscode/tasks.json
/database.rules.json /database.rules.json
/functions/auditly*.json /functions/auditly*.json
/CLAUDE.md
/debug_firebase.js
/schema.json
/SECURITY_FIXES.md
/.claude

21
App.tsx
View File

@@ -41,16 +41,28 @@ const RequireOrgSelection: React.FC<{ children: React.ReactNode }> = ({ children
const RequireOnboarding: React.FC<{ children: React.ReactNode }> = ({ children }) => { const RequireOnboarding: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { org } = useOrg(); const { org } = useOrg();
const { user } = useAuth(); const { user } = useAuth();
const { organizations } = useUserOrganizations(); const { organizations, selectedOrgId } = useUserOrganizations();
if (!org) return <div className="p-8">Loading organization...</div>; if (!org) return <div className="p-8">Loading organization...</div>;
if (!org.onboardingCompleted) { // Get the user's relationship to this organization
// Only org owners should be redirected to onboarding const userOrgRelation = organizations.find(o => o.orgId === selectedOrgId);
const userOrgRelation = organizations.find(o => o.orgId === org.orgId);
const isOrgOwner = userOrgRelation?.role === 'owner'; 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) { if (isOrgOwner) {
console.log('Redirecting owner to onboarding');
return <Navigate to="/onboarding" replace />; return <Navigate to="/onboarding" replace />;
} else { } else {
// Non-owners should see a waiting message // Non-owners should see a waiting message
@@ -104,6 +116,7 @@ function App() {
{/* Employee questionnaire - no auth needed, uses invite code */} {/* Employee questionnaire - no auth needed, uses invite code */}
<Route path="/employee-form/:inviteCode" element={<EmployeeQuestionnaire />} /> <Route path="/employee-form/:inviteCode" element={<EmployeeQuestionnaire />} />
<Route path="/questionnaire/:inviteCode" element={<EmployeeQuestionnaire />} />
{/* Organization Selection - after auth, before entering app */} {/* Organization Selection - after auth, before entering app */}
<Route <Route

View File

@@ -13,13 +13,14 @@
"firebase-admin": "^13.4.0", "firebase-admin": "^13.4.0",
"firebase-functions": "^6.4.0", "firebase-functions": "^6.4.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
"openai": "^4.104.0", "openai": "^5.12.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.8.0", "react-router-dom": "^7.8.0",
"recharts": "^3.1.2", "recharts": "^3.1.2",
"tailwindcss": "^4.1.12", "tailwindcss": "^4.1.12",
"zod": "^4.0.17",
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
@@ -410,8 +411,6 @@
"@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
@@ -438,8 +437,6 @@
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -598,11 +595,7 @@
"firebase-functions": ["firebase-functions@6.4.0", "", { "dependencies": { "@types/cors": "^2.8.5", "@types/express": "^4.17.21", "cors": "^2.8.5", "express": "^4.21.0", "protobufjs": "^7.2.2" }, "peerDependencies": { "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0" }, "bin": { "firebase-functions": "lib/bin/firebase-functions.js" } }, "sha512-Q/LGhJrmJEhT0dbV60J4hCkVSeOM6/r7xJS/ccmkXzTWMjo+UPAYX9zlQmGlEjotstZ0U9GtQSJSgbB2Z+TJDg=="], "firebase-functions": ["firebase-functions@6.4.0", "", { "dependencies": { "@types/cors": "^2.8.5", "@types/express": "^4.17.21", "cors": "^2.8.5", "express": "^4.21.0", "protobufjs": "^7.2.2" }, "peerDependencies": { "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0" }, "bin": { "firebase-functions": "lib/bin/firebase-functions.js" } }, "sha512-Q/LGhJrmJEhT0dbV60J4hCkVSeOM6/r7xJS/ccmkXzTWMjo+UPAYX9zlQmGlEjotstZ0U9GtQSJSgbB2Z+TJDg=="],
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], "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=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
@@ -658,8 +651,6 @@
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
@@ -778,8 +769,6 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "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=="], "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=="], "@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=="], "@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=="], "@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=="], "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=="], "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=="], "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=="], "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/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=="], "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/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=="], "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=="], "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=="], "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/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=="], "firebase-functions/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],

View File

@@ -19,91 +19,52 @@ if (import.meta.env.DEV) {
console.log(` API_URL: ${API_URL}`); console.log(` API_URL: ${API_URL}`);
} }
export const EMPLOYEES: Employee[] = [ // DEPRECATED: These are legacy sample data that should not be used in production
{ id: 'AG', name: 'Alex Green', initials: 'AG', email: 'alex.green@zitlac.com', department: 'Influencer Marketing', role: 'Influencer Coordinator & Business Development Outreach' }, // Real data should be generated via AI backend calls or user input
{ id: 'MB', name: 'Michael Brown', initials: 'MB', email: 'michael.brown@zitlac.com', department: 'Engineering', role: 'Senior Developer' }, export const EMPLOYEES: Employee[] = [];
{ 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: Sample report data - real reports should be generated via /generateEmployeeReport API
export const REPORT_DATA: Report = { export const REPORT_DATA: Report = {
employeeId: 'AG', employeeId: 'sample',
department: 'Influencer Marketing', department: 'Sample Department',
role: 'Influencer Coordinator & Business Development Outreach', role: 'Sample Role',
roleAndOutput: { roleAndOutput: {
responsibilities: 'Recruiting influencers, onboarding, campaign support, business development.', responsibilities: 'This is sample data. Real reports are generated via AI.',
clarityOnRole: '10/10 - Feels very clear on responsibilities.', clarityOnRole: 'Sample data',
selfRatedOutput: '7/10 - Indicates decent performance but room to grow.', selfRatedOutput: 'Sample data',
recurringTasks: 'Influencer outreach, onboarding, communications.', recurringTasks: 'Sample data',
}, },
insights: { insights: {
personalityTraits: 'Loyal, well-liked by influencers, eager to grow, client-facing interest.', personalityTraits: 'Sample data - use AI-generated reports',
psychologicalIndicators: [ psychologicalIndicators: ['Sample data'],
'Scores high on optimism and external motivation.', selfAwareness: 'Sample data',
'Shows ambition but lacks self-discipline in execution.', emotionalResponses: 'Sample data',
'Displays a desire for recognition and community; seeks more appreciation.', growthDesire: 'Sample data',
],
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.',
}, },
strengths: [ strengths: ['Sample data - use AI-generated reports'],
'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.',
],
weaknesses: [ weaknesses: [
{ isCritical: true, description: 'Disorganized and late with deliverables — confirmed by previous internal notes.' }, { isCritical: false, description: 'Sample data - use AI-generated reports' }
{ 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.' },
], ],
opportunities: { opportunities: {
roleAdjustment: 'Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities.', roleAdjustment: 'Sample data',
accountabilitySupport: "Pair with a high-output implementer (new hire) to balance Gentry's strategic skills.", accountabilitySupport: 'Sample data',
}, },
risks: [ risks: ['Sample data - use AI-generated reports'],
"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.',
],
recommendation: { recommendation: {
action: 'Keep', action: 'Sample',
details: [ details: ['Sample data - use AI-generated reports'],
'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.",
],
}, },
grading: [], grading: [],
}; };
// DEPRECATED: Sample submission data - real submissions come from employee questionnaires
export const SUBMISSIONS_DATA: Submission = { export const SUBMISSIONS_DATA: Submission = {
employeeId: 'AG', employeeId: 'sample',
answers: [ answers: [
{ {
question: 'What is the mission of your company?', question: 'Sample question',
answer: 'To empower small businesses with AI-driven automation tools that increase efficiency and reduce operational overhead.', answer: 'Sample answer - real data comes from employee questionnaires',
}, }
{
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.',
},
], ],
}; };
@@ -135,74 +96,38 @@ export const FAQ_DATA: FaqItem[] = [
]; ];
export const CHAT_STARTERS = [ export const CHAT_STARTERS = [
"Summarize Alex Green's latest report.", "Summarize the latest employee reports.",
"What are Alex's biggest strengths?", "What are the company's organizational strengths?",
"Identify any risks associated with Alex.", "Identify any risks in our current workforce.",
"Should Alex be considered for a promotion?" "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 = { export const SAMPLE_COMPANY_REPORT: CompanyReport = {
id: 'sample-company-report', id: 'placeholder-report',
createdAt: Date.now() - 86400000, // 1 day ago createdAt: Date.now(),
overview: { overview: {
totalEmployees: 0, // Fixed: Start with 0 employees instead of hardcoded 6 totalEmployees: 0,
departmentBreakdown: [], departmentBreakdown: [],
submissionRate: 0, submissionRate: 0,
lastUpdated: Date.now() - 86400000 lastUpdated: Date.now(),
averagePerformanceScore: 0,
riskLevel: 'Unknown'
}, },
gradingBreakdown: [], gradingBreakdown: [],
operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] }, operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] },
personnelChanges: { newHires: [], promotions: [], departures: [] }, personnelChanges: { newHires: [], promotions: [], departures: [] },
keyPersonnelChanges: [ keyPersonnelChanges: [],
{ employeeName: "Alex Green", department: "Influencer Marketing", role: "Influencer Coordinator", changeType: "newHire" }, immediateHiringNeeds: [],
{ 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'
}
],
forwardOperatingPlan: { forwardOperatingPlan: {
quarterlyGoals: [ quarterlyGoals: [],
'Expand influencer network by 40%', resourceNeeds: [],
'Launch automated campaign tracking system', riskMitigation: []
'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'
]
}, },
organizationalStrengths: [ organizationalStrengths: [],
], organizationalRisks: [],
organizationalRisks: [ gradingOverview: {},
'Key personnel dependency in critical roles', executiveSummary: `Welcome to Auditly! Generate your first AI-powered company report by inviting employees and completing the onboarding process.`
'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.`
}; };

View File

@@ -24,25 +24,55 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
useEffect(() => { useEffect(() => {
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured); console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
if (!isFirebaseConfigured) {
// Demo mode: check for persisted session if (isFirebaseConfigured) {
console.log('Demo mode: checking for persisted session'); // Firebase mode: Set up proper Firebase auth state listener
const sessionUser = sessionStorage.getItem('auditly_demo_session'); 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) { if (sessionUser) {
try {
const parsedUser = JSON.parse(sessionUser); const parsedUser = JSON.parse(sessionUser);
console.log('Restoring demo session for:', parsedUser.email); console.log('Restoring OTP session for:', parsedUser.email);
setUser(parsedUser as User); 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); setLoading(false);
}); });
return () => unsub();
return unsubscribe;
} else {
// Demo/OTP mode: Check localStorage for persisted session
console.log('Checking for persisted OTP session');
const sessionUser = localStorage.getItem('auditly_demo_session');
if (sessionUser) {
try {
const parsedUser = JSON.parse(sessionUser);
console.log('Restoring session for:', parsedUser.email);
setUser(parsedUser as User);
} catch (error) {
console.error('Failed to parse session user:', error);
localStorage.removeItem('auditly_demo_session');
setUser(null);
}
} else {
setUser(null);
}
setLoading(false);
return () => { };
}
}, []); }, []);
const signInWithGoogle = async () => { const signInWithGoogle = async () => {
@@ -54,13 +84,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}; };
const signOutUser = async () => { const signOutUser = async () => {
if (!isFirebaseConfigured) { try {
// Clear demo session // Sign out from Firebase if configured and user is signed in via Firebase
sessionStorage.removeItem('auditly_demo_session'); if (isFirebaseConfigured && auth.currentUser) {
setUser(null);
return;
}
await signOut(auth); await signOut(auth);
console.log('Firebase signout completed');
}
} catch (error) {
console.error('Firebase signout error:', error);
}
// Always clear all local session data
localStorage.removeItem('auditly_demo_session');
localStorage.removeItem('auditly_auth_token');
localStorage.removeItem('auditly_selected_org');
sessionStorage.clear();
setUser(null);
console.log('User signed out and all sessions cleared');
}; };
const signInWithEmail = async (email: string, password: string) => { const signInWithEmail = async (email: string, password: string) => {
@@ -79,7 +120,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} as unknown as User; } as unknown as User;
setUser(mockUser); 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); console.log('Demo login successful for:', email);
} else { } else {
throw new Error('Invalid password'); throw new Error('Invalid password');
@@ -132,7 +173,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} as unknown as User; } as unknown as User;
setUser(mockUser); 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); console.log('Demo signup successful for:', email);
return; return;
} }
@@ -191,8 +232,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} as unknown as User; } as unknown as User;
setUser(mockUser); setUser(mockUser);
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
sessionStorage.setItem('auditly_auth_token', data.token); localStorage.setItem('auditly_auth_token', data.token);
return data; return data;
}; };
@@ -206,8 +247,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} as unknown as User; } as unknown as User;
setUser(mockUser); setUser(mockUser);
sessionStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
sessionStorage.setItem('auditly_auth_token', token); localStorage.setItem('auditly_auth_token', token);
}; };
return ( return (

View File

@@ -3,8 +3,9 @@ import { collection, doc, getDoc, getDocs, onSnapshot, setDoc } from 'firebase/f
import { db, isFirebaseConfigured } from '../services/firebase'; import { db, isFirebaseConfigured } from '../services/firebase';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
import { Employee, Report, Submission, CompanyReport } from '../types'; 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 { demoStorage } from '../services/demoStorage';
import { apiPost, apiPut } from '../services/api';
interface OrgData { interface OrgData {
orgId: string; orgId: string;
@@ -98,65 +99,12 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
// Initialize with empty employee list for clean start // Initialize with empty employee list for clean start
// (Removed automatic seeding of 6 default employees per user feedback) // (Removed automatic seeding of 6 default employees per user feedback)
// Create sample submissions for multiple employees // Don't automatically create sample submissions - let users create real data
const sampleSubmissions = [ // through the proper questionnaire flow
{
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."
}
}
];
// Save all sample submissions // Note: Sample employee reports removed - real reports generated via AI after questionnaire submission
sampleSubmissions.forEach(submission => {
demoStorage.saveSubmission(submission);
});
// Save sample employee report (only for AG initially) // Don't save sample company report - let users generate real AI-powered reports
demoStorage.saveEmployeeReport(orgId, REPORT_DATA.employeeId, REPORT_DATA);
// Save sample company report
demoStorage.saveCompanyReport(orgId, SAMPLE_COMPANY_REPORT);
} }
// Load persistent demo data // Load persistent demo data
@@ -175,7 +123,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
})); }));
setEmployees(convertedEmployees); setEmployees(convertedEmployees);
// Convert submissions to expected format // Load any existing submissions from localStorage
const orgSubmissions = demoStorage.getSubmissionsByOrg(orgId); const orgSubmissions = demoStorage.getSubmissionsByOrg(orgId);
const convertedSubmissions: Record<string, Submission> = {}; const convertedSubmissions: Record<string, Submission> = {};
Object.entries(orgSubmissions).forEach(([employeeId, demoSub]) => { Object.entries(orgSubmissions).forEach(([employeeId, demoSub]) => {
@@ -189,11 +137,11 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}); });
setSubmissions(convertedSubmissions); setSubmissions(convertedSubmissions);
// Convert reports to expected format // Load any existing AI-generated reports from localStorage
const orgReports = demoStorage.getEmployeeReportsByOrg(orgId); const orgReports = demoStorage.getEmployeeReportsByOrg(orgId);
setReports(orgReports); setReports(orgReports);
// Get company reports // Load any existing company reports from localStorage
const companyReports = demoStorage.getCompanyReportsByOrg(orgId); const companyReports = demoStorage.getCompanyReportsByOrg(orgId);
setFullCompanyReports(companyReports); setFullCompanyReports(companyReports);
return; 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; const updatedOrg = { ...(org || { orgId, name: 'Demo Company' }), ...data } as OrgData;
setOrg(updatedOrg); setOrg(updatedOrg);
// Also sync with server for multi-tenant persistence // If onboarding was completed, update localStorage for persistence and notify other contexts
try { if (data.onboardingCompleted) {
const response = await fetch(`${API_URL}/api/organizations/${orgId}`, { const demoOrgData = {
method: 'PUT', orgId: updatedOrg.orgId,
headers: { 'Content-Type': 'application/json' }, name: updatedOrg.name,
body: JSON.stringify(data) 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) { // Signal to UserOrganizationsContext and other components about completion
console.warn('Failed to sync organization data with server'); window.dispatchEvent(new CustomEvent('organizationUpdated', {
} detail: { orgId: updatedOrg.orgId, onboardingCompleted: true }
} catch (error) { }));
console.warn('Failed to sync organization data:', error);
} }
// Organization already exists, no need to sync with server during onboarding
// We'll update Firestore directly in the Firebase mode below
} else { } else {
// Firebase mode - save to Firestore // Firebase mode - save to Firestore
const orgRef = doc(db, 'orgs', orgId); const orgRef = doc(db, 'orgs', orgId);
@@ -261,6 +218,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
// Update local state // Update local state
const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData; const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData;
setOrg(updatedOrg); 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,32 +271,46 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}; };
const inviteEmployee = async ({ name, email }: { name: string; email: string }) => { const inviteEmployee = async ({ name, email }: { name: string; email: string }) => {
// Always use Cloud Functions for invites to ensure multi-tenant compliance console.log('inviteEmployee called:', { name, email, orgId });
const response = await fetch(`${API_URL}/createInvitation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, orgId })
});
if (!response.ok) { try {
throw new Error(`Failed to create invite: ${response.status}`); // 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 response.json(); const data = await res.json();
const { code, employee, inviteLink } = data; const { code, employee, inviteLink } = data;
// Store employee locally for immediate UI update 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) { if (!isFirebaseConfigured) {
const newEmployee = { ...employee, orgId }; const employeeWithOrg = { ...newEmployee, orgId };
setEmployees(prev => { setEmployees(prev => {
if (prev.find(e => e.id === employee.id)) return prev; if (prev.find(e => e.id === employee.id)) return prev;
return [...prev, newEmployee]; return [...prev, newEmployee];
}); });
demoStorage.saveEmployee(newEmployee); demoStorage.saveEmployee(employeeWithOrg);
} else { } else {
// For Firebase, the employee will be created when they accept the invite // For Firebase, add to local state for immediate UI update
// But we can add them to local state for immediate UI update
const newEmployee = { ...employee, orgId };
setEmployees(prev => { setEmployees(prev => {
if (prev.find(e => e.id === employee.id)) return prev; if (prev.find(e => e.id === employee.id)) return prev;
return [...prev, newEmployee]; return [...prev, newEmployee];
@@ -335,6 +318,10 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
} }
return { employeeId: employee.id, inviteLink }; return { employeeId: employee.id, inviteLink };
} catch (error) {
console.error('inviteEmployee error:', error);
throw error;
}
}; };
const getReportVersions = async (employeeId: string) => { const getReportVersions = async (employeeId: string) => {
@@ -407,27 +394,36 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
const seedInitialData = async () => { const seedInitialData = async () => {
if (!isFirebaseConfigured) { if (!isFirebaseConfigured) {
// Start with empty employee list for clean demo experience // Start with completely clean slate - no sample data
setEmployees([]); setEmployees([]);
setSubmissions({ [SUBMISSIONS_DATA.employeeId]: SUBMISSIONS_DATA }); setSubmissions({});
setReports({ [REPORT_DATA.employeeId]: REPORT_DATA }); setReports({});
setFullCompanyReports([SAMPLE_COMPANY_REPORT]); setFullCompanyReports([]);
return; return;
} }
// Start with clean slate - let users invite their own employees // Start with clean slate - let users invite their own employees and generate real data
// (Removed automatic seeding per user feedback)
}; };
const saveFullCompanyReport = async (report: CompanyReport) => { 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]); setFullCompanyReports(prev => [report, ...prev]);
// Persist to localStorage
demoStorage.saveCompanyReport(orgId, report); demoStorage.saveCompanyReport(orgId, report);
return; return;
} }
// Use direct Firestore operations - much more efficient
const ref = doc(db, 'orgs', orgId, 'fullCompanyReports', report.id); const ref = doc(db, 'orgs', orgId, 'fullCompanyReports', report.id);
await setDoc(ref, report); await setDoc(ref, report);
// Update local state after successful save
setFullCompanyReports(prev => [report, ...prev]);
}; };
const getFullCompanyReportHistory = async (): Promise<CompanyReport[]> => { const getFullCompanyReportHistory = async (): Promise<CompanyReport[]> => {
@@ -444,91 +440,132 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}; };
const generateCompanyReport = async (): Promise<CompanyReport> => { const generateCompanyReport = async (): Promise<CompanyReport> => {
// Generate comprehensive company report based on current data console.log('generateCompanyReport called for org:', orgId);
const totalEmployees = employees.length;
const submittedEmployees = Object.keys(submissions).length; // 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; const submissionRate = totalEmployees > 0 ? (submittedEmployees / totalEmployees) * 100 : 0;
// Department breakdown // Department breakdown (concrete data) - exclude owners
const deptMap = new Map<string, number>(); const deptMap = new Map<string, number>();
employees.forEach(emp => { actualEmployees.forEach(emp => {
const dept = emp.department || 'Unassigned'; const dept = emp.department || 'Unassigned';
deptMap.set(dept, (deptMap.get(dept) || 0) + 1); deptMap.set(dept, (deptMap.get(dept) || 0) + 1);
}); });
const departmentBreakdown = Array.from(deptMap.entries()).map(([department, count]) => ({ department, count })); const departmentBreakdown = Array.from(deptMap.entries()).map(([department, count]) => ({ department, count }));
// Analyze employee reports for insights try {
const reportValues = Object.values(reports) as Report[]; // Use AI only for analysis and insights that require reasoning
const organizationalStrengths: string[] = []; const res = await apiPost('/generateCompanyWiki', {
const organizationalRisks: string[] = []; org: org,
submissions: employeeSubmissions, // Only employee submissions, not owner data
reportValues.forEach(report => { metrics: {
if (report.strengths) { totalEmployees,
organizationalStrengths.push(...report.strengths); submissionRate,
departmentBreakdown
} }
if (report.risks) { }, orgId);
organizationalRisks.push(...report.risks);
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');
} }
});
// Remove duplicates and take top items const data = await res.json();
const uniqueStrengths = [...new Set(organizationalStrengths)].slice(0, 5); console.log('Company insights generated via AI successfully');
const uniqueRisks = [...new Set(organizationalRisks)].slice(0, 5); console.log('AI response data:', data);
const gradingBreakdown = [ // Combine concrete metrics with AI insights
{ 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<Record<string, number>>((acc, g) => { acc[g.category.toLowerCase()] = Math.round((g.value / 100) * 5 * 10) / 10; return acc; }, {});
const report: CompanyReport = { const report: CompanyReport = {
id: Date.now().toString(), id: Date.now().toString(),
createdAt: Date.now(), createdAt: Date.now(),
// Use AI-generated insights for subjective analysis
...data.report,
// Override with our concrete metrics
overview: { overview: {
totalEmployees, totalEmployees,
departmentBreakdown, departmentBreakdown,
submissionRate, submissionRate,
lastUpdated: Date.now(), lastUpdated: Date.now(),
averagePerformanceScore: gradingBreakdown.reduce((a, g) => a + g.value, 0) / gradingBreakdown.length / 20, averagePerformanceScore: data.report?.overview?.averagePerformanceScore || 0,
riskLevel: uniqueRisks.length > 4 ? 'High' : uniqueRisks.length > 2 ? 'Medium' : 'Low' riskLevel: data.report?.overview?.riskLevel || 'Unknown'
}, }
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(', ')}.`
}; };
console.log('Final company report object:', report);
await saveFullCompanyReport(report); await saveFullCompanyReport(report);
return report; return report;
} catch (error) {
console.error('generateCompanyReport error:', error);
throw error;
}
}; };
const generateCompanyWiki = async (orgOverride?: OrgData): Promise<CompanyReport> => { const generateCompanyWiki = async (orgOverride?: OrgData): Promise<CompanyReport> => {
const orgData = orgOverride || org; 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 { try {
const res = await fetch(`${API_URL}/generateCompanyWiki`, { console.log('Making API call to generateCompanyWiki...');
method: 'POST', const res = await apiPost('/generateCompanyWiki', {
headers: { 'Content-Type': 'application/json' }, org: orgData,
body: JSON.stringify({ org: orgData, submissions }) submissions: submissions || []
}); }, orgId);
if (!res.ok) throw new Error('Failed to generate company wiki');
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 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); await saveFullCompanyReport(data);
return data; return data;
} catch (e) { } catch (e) {
@@ -605,12 +642,18 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
isOwner, isOwner,
issueInviteViaApi: async ({ name, email, role, department }) => { issueInviteViaApi: async ({ name, email, role, department }) => {
try { try {
const res = await fetch(`${API_URL}/createInvitation`, { const res = await apiPost('/createInvitation', {
method: 'POST', name,
headers: { 'Content-Type': 'application/json' }, email,
body: JSON.stringify({ name, email, role, department, orgId }) role,
}); department
if (!res.ok) throw new Error('invite creation failed'); }, orgId);
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error || 'Invite creation failed');
}
const json = await res.json(); const json = await res.json();
// Optimistically add employee shell (not yet active until consume) // Optimistically add employee shell (not yet active until consume)
setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, { ...json.employee }]); 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 // Save to localStorage for persistence
demoStorage.saveSubmission(submission); 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 employee = employees.find(e => e.id === employeeId);
const res = await fetch(`${API_URL}/submitEmployeeAnswers`, { const res = await apiPost('/submitEmployeeAnswers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
employeeId, employeeId,
answers, answers,
orgId,
employee employee
}) }, orgId);
});
if (!res.ok) throw new Error('Failed to submit to server'); 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 // Update local state for UI with proper typing
const convertedSubmission: Submission = { const convertedSubmission: Submission = {
@@ -727,22 +769,76 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
}, },
generateEmployeeReport: async (employee: Employee) => { generateEmployeeReport: async (employee: Employee) => {
try { try {
const submission = submissions[employee.id]?.answers || submissions[employee.id] || {}; console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
const res = await fetch(`${API_URL}/generateEmployeeReport`, {
method: 'POST', // Get submission data for this employee
headers: { 'Content-Type': 'application/json' }, const submission = submissions[employee.id];
body: JSON.stringify({ employee, submission }) if (!submission) {
}); throw new Error(`No questionnaire submission found for ${employee.name}. Please ensure they have completed the employee questionnaire first.`);
if (!res.ok) throw new Error('failed to generate'); }
// Convert submission format for API
let submissionAnswers: Record<string, string> = {};
if (submission.answers) {
if (Array.isArray(submission.answers)) {
// If answers is an array of {question, answer} objects
submissionAnswers = submission.answers.reduce((acc, item) => {
acc[item.question] = item.answer;
return acc;
}, {} as Record<string, string>);
} else {
// If answers is already a key-value object
submissionAnswers = submission.answers as Record<string, string>;
}
}
console.log('Submission data found:', Object.keys(submissionAnswers).length, 'answers');
// Get company report and wiki data for context
let companyWiki = null;
try {
const companyReports = await getFullCompanyReportHistory();
if (companyReports.length > 0) {
companyWiki = {
org: org,
companyReport: companyReports[0]
};
console.log('Including company context in employee report generation');
}
} catch (error) {
console.warn('Could not fetch company report for context:', error);
}
const 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(); const json = await res.json();
if (json.report) { if (json.report) {
console.log('Employee report generated successfully');
setReports(prev => ({ ...prev, [employee.id]: json.report })); 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; return json.report as Report;
} else {
throw new Error('No report data received from API');
} }
} catch (e) { } catch (e) {
console.error('generateEmployeeReport error', e); console.error('generateEmployeeReport error', e);
throw e; // Re-throw to allow caller to handle
} }
return null;
}, },
getEmployeeReport: async (employeeId: string) => { getEmployeeReport: async (employeeId: string) => {
try { try {

View File

@@ -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(() => { useEffect(() => {
const savedOrgId = sessionStorage.getItem('auditly_selected_org'); const savedOrgId = localStorage.getItem('auditly_selected_org');
if (savedOrgId) { if (savedOrgId) {
setSelectedOrgId(savedOrgId); setSelectedOrgId(savedOrgId);
} }
@@ -83,9 +83,48 @@ export const UserOrganizationsProvider: React.FC<{ children: React.ReactNode }>
loadOrganizations(); loadOrganizations();
}, [user]); }, [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) => { 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); 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 }> => { const createOrganization = async (name: string): Promise<{ orgId: string; requiresSubscription?: boolean }> => {

View File

@@ -20,6 +20,158 @@ const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SEC
apiVersion: '2024-11-20.acacia', apiVersion: '2024-11-20.acacia',
}) : null; }) : null;
const RESPONSE_FORMAT = {
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 // Helper function to generate OTP
const generateOTP = () => { const generateOTP = () => {
return Math.floor(100000 + Math.random() * 900000).toString(); return Math.floor(100000 + Math.random() * 900000).toString();
@@ -29,7 +181,7 @@ const generateOTP = () => {
const cors = (req, res, next) => { const cors = (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*'); res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); 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'); res.set('Access-Control-Max-Age', '3600');
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
@@ -44,7 +196,7 @@ const cors = (req, res, next) => {
const setCorsHeaders = (res) => { const setCorsHeaders = (res) => {
res.set('Access-Control-Allow-Origin', '*'); res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); 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'); 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" }); 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) { if (!orgId || !email || !name) {
return res.status(400).json({ error: "Organization ID and email are required" }); return res.status(400).json({ error: "Organization ID, name, and email are required" });
} }
try { try {
// Generate invite code // Generate invite code
const code = Math.random().toString(36).substring(2, 15); const code = Math.random().toString(36).substring(2, 15);
// Store invitation // Generate employee ID
const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 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 const inviteRef = await db
.collection("orgs") .collection("orgs")
.doc(orgId) .doc(orgId)
@@ -254,20 +420,29 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => {
await inviteRef.set({ await inviteRef.set({
code, code,
employee,
email, email,
role,
orgId, orgId,
status: "pending", status: "pending",
createdAt: Date.now(), createdAt: Date.now(),
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days 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 // 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({ res.json({
success: true, success: true,
inviteCode: code, code,
employee,
inviteLink,
emailLink,
message: "Invitation sent successfully", message: "Invitation sent successfully",
}); });
} catch (error) { } catch (error) {
@@ -315,6 +490,8 @@ exports.getInvitationStatus = functions.https.onRequest(async (req, res) => {
res.json({ res.json({
success: true, success: true,
used: invite.status !== 'pending',
employee: invite.employee,
invite, invite,
}); });
} catch (error) { } catch (error) {
@@ -338,8 +515,8 @@ exports.consumeInvitation = functions.https.onRequest(async (req, res) => {
const { code, userId } = req.body; const { code, userId } = req.body;
if (!code || !userId) { if (!code) {
return res.status(400).json({ error: "Invitation code and user ID are required" }); return res.status(400).json({ error: "Invitation code is required" });
} }
try { try {
@@ -362,25 +539,34 @@ exports.consumeInvitation = functions.https.onRequest(async (req, res) => {
return res.status(400).json({ error: "Invitation has expired" }); 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 // Mark invitation as consumed
await inviteDoc.ref.update({ await inviteDoc.ref.update({
status: "consumed", status: "consumed",
consumedBy: userId, consumedBy: employee.id,
consumedAt: Date.now(), consumedAt: Date.now(),
}); });
// Add user to organization employees // Add employee to organization using data from invite
await db await db
.collection("orgs") .collection("orgs")
.doc(invite.orgId) .doc(invite.orgId)
.collection("employees") .collection("employees")
.doc(userId) .doc(employee.id)
.set({ .set({
id: userId, id: employee.id,
email: invite.email, name: employee.name || employee.email.split("@")[0],
role: invite.role, email: employee.email,
role: employee.role || "employee",
department: employee.department || "General",
joinedAt: Date.now(), joinedAt: Date.now(),
status: "active", status: "active",
inviteCode: code,
}); });
res.json({ res.json({
@@ -407,25 +593,59 @@ exports.submitEmployeeAnswers = functions.https.onRequest(async (req, res) => {
return res.status(405).json({ error: "Method not allowed" }); return res.status(405).json({ error: "Method not allowed" });
} }
const { orgId, employeeId, answers } = req.body; const { orgId, employeeId, answers, inviteCode } = req.body;
// 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) { if (!orgId || !employeeId || !answers) {
return res.status(400).json({ error: "Organization ID, employee ID, and answers are required" }); return res.status(400).json({ error: "Organization ID, employee ID, and answers are required" });
} }
}
try { 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 // Store submission
const submissionRef = await db const submissionRef = await db
.collection("orgs") .collection("orgs")
.doc(orgId) .doc(finalOrgId)
.collection("submissions") .collection("submissions")
.doc(employeeId); .doc(finalEmployeeId);
await submissionRef.set({ await submissionRef.set({
employeeId, employeeId: finalEmployeeId,
answers, answers,
submittedAt: Date.now(), submittedAt: Date.now(),
status: "completed", status: "completed",
submissionType: inviteCode ? "invite" : "regular",
...(inviteCode && { inviteCode })
}); });
res.json({ res.json({
@@ -597,62 +817,40 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
if (openai) { if (openai) {
// Use OpenAI to generate the company report and wiki // Use OpenAI to generate the company report and wiki
const prompt = ` const system = "You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema.";
You are an expert business analyst. Generate a comprehensive company report and wiki based on the following data: const user = [
"Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.",
Organization Information: "Be thorough and professional.",
${JSON.stringify(org, null, 2)} "",
"Organization Information:",
Employee Submissions: JSON.stringify(org, null, 2),
${JSON.stringify(submissions, null, 2)} "",
"Employee Submissions:",
Generate a detailed analysis with two main components: JSON.stringify(submissions, null, 2)
].join("\n");
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 completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: "gpt-4o", model: "gpt-4o",
temperature: 0, // consistency
response_format: RESPONSE_FORMAT,
messages: [ messages: [
{ { role: "system", content: system },
role: "system", { role: "user", content: user }
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,
}); });
const aiResponse = completion.choices[0].message.content; // content is guaranteed to be schema-conformant JSON
const parsedResponse = JSON.parse(aiResponse); const parsed = JSON.parse(completion.choices[0].message.content);
report = { const report = {
generatedAt: Date.now(), generatedAt: Date.now(),
...parsedResponse.report ...parsed.report
}; };
wiki = { const wiki = {
companyName: org.name, companyName: org?.name ?? parsed.wiki.companyName,
generatedAt: Date.now(), generatedAt: Date.now(),
...parsedResponse.wiki ...parsed.wiki,
}; };
} else { } else {
// Fallback to mock data when OpenAI is not available // Fallback to mock data when OpenAI is not available
@@ -1452,3 +1650,48 @@ async function handlePaymentFailed(invoice) {
// response.status(400).json({ error: 'Invalid verification code' }); // response.status(400).json({ error: 'Invalid verification code' });
// } // }
// }); // });
// 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" });
}
});

103
index.css
View File

@@ -1,55 +1,63 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
/* Light theme variables - using new Figma color palette */
--background-primary : #FFFFFF; --background-primary : #FFFFFF;
/* Base White */ --background-secondary: #FDFDFD;
--background-secondary : #FDFDFD;
/* Gray 6 */
--background-tertiary : #FAFAFA; --background-tertiary : #FAFAFA;
/* Gray 5 */
--text-primary : #0A0D12; --text-primary : #0A0D12;
/* Dark 7 */ --text-secondary: #717680;
--text-secondary : #717680;
/* Dark 2 */
--text-tertiary : #A4A7AE; --text-tertiary : #A4A7AE;
/* Gray 1 */
--accent : #5E48FC; --accent : ##3399FF;
/* Brand Main */ --accent-hover: #4C3CF0;
--accent-hover : #4C3CF0;
/* Slightly darker brand */
--accent-text : #FFFFFF; --accent-text : #FFFFFF;
/* Base White */
--border-color : #E9EAEB; --border-color: #E9EAEB;
/* Gray 3 */ --border-light: #F5F5F5;
--border-light : #F5F5F5;
/* Gray 4 */
--sidebar-bg : #FDFDFD; --sidebar-bg : #FDFDFD;
/* Gray 6 */
--sidebar-text : #717680; --sidebar-text : #717680;
/* Dark 2 */ --sidebar-active-bg : #3399FF;
--sidebar-active-bg : #5E48FC; --sidebar-active-text: #FFFFFF;
/* Brand Main */
--sidebar-active-text : #FFFFFF;
/* Base White */
--input-bg : #F5F5F5; --input-bg : #F5F5F5;
/* Gray 4 */
--input-border : #E9EAEB; --input-border : #E9EAEB;
/* Gray 3 */ --input-placeholder: #717680;
--input-placeholder : #717680;
/* Dark 2 */
--button-secondary-bg : #F5F5F5; --button-secondary-bg : #F5F5F5;
/* Gray 4 */
--button-secondary-hover: #E9EAEB; --button-secondary-hover: #E9EAEB;
/* Gray 3 */
--status-red : #F63D68; --color-red : #F63D68;
/* Other Red */ --color-green : #3CCB7F;
--status-green : #3CCB7F; --color-orange : #FF4405;
/* Other Green */ --color-light-orange: #F38744;
--status-orange : #FF4405; --color-yellow : #FEEE95;
/* Other Orange */
--status-yellow : #FEEE95; --gray-0 : #FFFFFF;
/* Other Yellow */ --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 { .dark {
@@ -102,6 +110,25 @@
/* Other Orange */ /* Other Orange */
--status-yellow : #FEEE95; --status-yellow : #FEEE95;
/* Other Yellow */ /* 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;
} }
* { * {

View File

@@ -25,13 +25,14 @@
"firebase-admin": "^13.4.0", "firebase-admin": "^13.4.0",
"firebase-functions": "^6.4.0", "firebase-functions": "^6.4.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
"openai": "^4.104.0", "openai": "^5.12.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.8.0", "react-router-dom": "^7.8.0",
"recharts": "^3.1.2", "recharts": "^3.1.2",
"tailwindcss": "^4.1.12" "tailwindcss": "^4.1.12",
"zod": "^4.0.17"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",

View File

@@ -2,9 +2,10 @@ import React, { useState, useMemo } from 'react';
import { Card, Button } from '../components/UiKit'; import { Card, Button } from '../components/UiKit';
import { useOrg } from '../contexts/OrgContext'; import { useOrg } from '../contexts/OrgContext';
import { CHAT_STARTERS } from '../constants'; import { CHAT_STARTERS } from '../constants';
import { apiPost } from '../services/api';
const Chat: React.FC = () => { const Chat: React.FC = () => {
const { employees, reports, generateEmployeeReport } = useOrg(); const { employees, reports, generateEmployeeReport, orgId } = useOrg();
const [messages, setMessages] = useState<Array<{ id: string, role: 'user' | 'assistant', text: string }>>([]); const [messages, setMessages] = useState<Array<{ id: string, role: 'user' | 'assistant', text: string }>>([]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -34,16 +35,44 @@ const Chat: React.FC = () => {
setInput(''); setInput('');
setIsLoading(true); setIsLoading(true);
// Simulate AI response (placeholder for server /api/chat usage) try {
setTimeout(() => { // 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 = { const aiResponse = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
role: 'assistant' as const, 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]); 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); setIsLoading(false);
}, 1500); }
}; };
return ( return (

View File

@@ -113,19 +113,19 @@ const CompanyWiki: React.FC = () => {
<div className="text-sm text-[--text-secondary]">Employees</div> <div className="text-sm text-[--text-secondary]">Employees</div>
</div> </div>
<div className="text-center p-4 bg-[--background-secondary] rounded-lg"> <div className="text-center p-4 bg-[--background-secondary] rounded-lg">
<div className="text-2xl font-bold text-green-500">{companyReport.overview.departmentBreakdown.length}</div> <div className="text-2xl font-bold text-green-500">{companyReport.overview?.departmentBreakdown?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Departments</div> <div className="text-sm text-[--text-secondary]">Departments</div>
</div> </div>
<div className="text-center p-4 bg-[--background-secondary] rounded-lg"> <div className="text-center p-4 bg-[--background-secondary] rounded-lg">
<div className="text-2xl font-bold text-purple-500">{companyReport.organizationalStrengths.length}</div> <div className="text-2xl font-bold text-purple-500">{companyReport.organizationalStrengths?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Strength Areas</div> <div className="text-sm text-[--text-secondary]">Strength Areas</div>
</div> </div>
<div className="text-center p-4 bg-[--background-secondary] rounded-lg"> <div className="text-center p-4 bg-[--background-secondary] rounded-lg">
<div className="text-2xl font-bold text-orange-500">{companyReport.organizationalRisks.length}</div> <div className="text-2xl font-bold text-orange-500">{companyReport.organizationalRisks?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Risks</div> <div className="text-sm text-[--text-secondary]">Risks</div>
</div> </div>
</div> </div>
{companyReport.gradingOverview && ( {(Array.isArray(companyReport.gradingOverview) && companyReport.gradingOverview.length > 0) && (
<div className="mt-6 p-4 bg-[--background-tertiary] rounded-lg"> <div className="mt-6 p-4 bg-[--background-tertiary] rounded-lg">
<RadarPerformanceChart <RadarPerformanceChart
title="Organizational Grading" title="Organizational Grading"
@@ -142,13 +142,13 @@ const CompanyWiki: React.FC = () => {
<Card> <Card>
<h4 className="text-lg font-semibold text-[--text-primary] mb-3">Strengths</h4> <h4 className="text-lg font-semibold text-[--text-primary] mb-3">Strengths</h4>
<ul className="space-y-2"> <ul className="space-y-2">
{companyReport.organizationalStrengths.map((s: any, i) => <li key={i} className="text-[--text-secondary] text-sm"> {s.area}</li>)} {(companyReport.organizationalStrengths || []).map((s: any, i) => <li key={i} className="text-[--text-secondary] text-sm"> {s.area || s}</li>)}
</ul> </ul>
</Card> </Card>
<Card> <Card>
<h4 className="text-lg font-semibold text-[--text-primary] mb-3">Risks</h4> <h4 className="text-lg font-semibold text-[--text-primary] mb-3">Risks</h4>
<ul className="space-y-2"> <ul className="space-y-2">
{companyReport.organizationalRisks.map((r, i) => <li key={i} className="text-[--text-secondary] text-sm"> {r}</li>)} {(companyReport.organizationalRisks || []).map((r, i) => <li key={i} className="text-[--text-secondary] text-sm"> {r}</li>)}
</ul> </ul>
</Card> </Card>
<Card> <Card>
@@ -172,72 +172,152 @@ const CompanyWiki: React.FC = () => {
</div> </div>
)} )}
{/* Company Profile - Onboarding Data */} {/* Company Profile - Q&A Format from Onboarding */}
<Card className="mt-6"> <div className="mt-6">
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Company Profile</h3> <h3 className="text-2xl font-semibold text-[--text-primary] mb-6">Company Profile</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{org?.mission && ( {org?.mission && (
<Card className="p-4">
<div className="space-y-3">
<div> <div>
<h4 className="font-medium text-[--text-primary] mb-2">Mission</h4> <h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-secondary] text-sm">{org.mission}</p> <p className="text-[--text-primary] font-medium">What is your company's mission?</p>
</div> </div>
)}
{org?.vision && (
<div> <div>
<h4 className="font-medium text-[--text-primary] mb-2">Vision</h4> <h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-secondary] text-sm">{org.vision}</p> <p className="text-[--text-primary] text-sm leading-relaxed">{org.mission}</p>
</div> </div>
)}
{org?.evolution && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Company Evolution</h4>
<p className="text-[--text-secondary] text-sm">{org.evolution}</p>
</div>
)}
{org?.advantages && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Competitive Advantages</h4>
<p className="text-[--text-secondary] text-sm">{org.advantages}</p>
</div>
)}
{org?.vulnerabilities && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Vulnerabilities</h4>
<p className="text-[--text-secondary] text-sm">{org.vulnerabilities}</p>
</div>
)}
{org?.shortTermGoals && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Short Term Goals</h4>
<p className="text-[--text-secondary] text-sm">{org.shortTermGoals}</p>
</div>
)}
{org?.longTermGoals && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Long Term Goals</h4>
<p className="text-[--text-secondary] text-sm">{org.longTermGoals}</p>
</div>
)}
{org?.cultureDescription && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Culture</h4>
<p className="text-[--text-secondary] text-sm">{org.cultureDescription}</p>
</div>
)}
{org?.workEnvironment && (
<div>
<h4 className="font-medium text-[--text-primary] mb-2">Work Environment</h4>
<p className="text-[--text-secondary] text-sm">{org.workEnvironment}</p>
</div>
)}
{org?.additionalContext && (
<div className="md:col-span-2">
<h4 className="font-medium text-[--text-primary] mb-2">Additional Context</h4>
<p className="text-[--text-secondary] text-sm">{org.additionalContext}</p>
</div>
)}
</div> </div>
</Card> </Card>
)}
{org?.vision && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What is your company's vision?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.vision}</p>
</div>
</div>
</Card>
)}
{org?.evolution && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">How has your company evolved over time?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.evolution}</p>
</div>
</div>
</Card>
)}
{org?.advantages && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your competitive advantages?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.advantages}</p>
</div>
</div>
</Card>
)}
{org?.vulnerabilities && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your key vulnerabilities?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.vulnerabilities}</p>
</div>
</div>
</Card>
)}
{org?.shortTermGoals && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your short-term goals?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.shortTermGoals}</p>
</div>
</div>
</Card>
)}
{org?.longTermGoals && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What are your long-term goals?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.longTermGoals}</p>
</div>
</div>
</Card>
)}
{org?.cultureDescription && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">How would you describe your company culture?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.cultureDescription}</p>
</div>
</div>
</Card>
)}
{org?.workEnvironment && (
<Card className="p-4">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">What is your work environment like?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.workEnvironment}</p>
</div>
</div>
</Card>
)}
{org?.additionalContext && (
<Card className="p-4 lg:col-span-2">
<div className="space-y-3">
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Question:</h4>
<p className="text-[--text-primary] font-medium">Any additional context about your company?</p>
</div>
<div>
<h4 className="text-sm font-medium text-[--text-secondary] mb-1">Answer:</h4>
<p className="text-[--text-primary] text-sm leading-relaxed">{org.additionalContext}</p>
</div>
</div>
</Card>
)}
</div>
</div>
{org?.description && ( {org?.description && (
<Card className="mt-6"> <Card className="mt-6">

View File

@@ -5,6 +5,7 @@ import { CompanyReport, Employee, Report } from '../types';
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart'; import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
import ScoreBarList from '../components/charts/ScoreBarList'; import ScoreBarList from '../components/charts/ScoreBarList';
import { SAMPLE_COMPANY_REPORT } from '../constants'; import { SAMPLE_COMPANY_REPORT } from '../constants';
import ReportDetail from './ReportDetail';
interface EmployeeDataProps { interface EmployeeDataProps {
mode: 'submissions' | 'reports'; mode: 'submissions' | 'reports';
@@ -47,7 +48,7 @@ const CompanyReportCard: React.FC<{ report: CompanyReport }> = ({ report }) => {
</div> </div>
<div className="bg-[--background-tertiary] p-4 rounded-lg"> <div className="bg-[--background-tertiary] p-4 rounded-lg">
<h3 className="text-sm font-medium text-[--text-secondary]">Departments</h3> <h3 className="text-sm font-medium text-[--text-secondary]">Departments</h3>
<p className="text-2xl `font-bold text-[--text-primary]">{report.overview.departmentBreakdown.length}</p> <p className="text-2xl font-bold text-[--text-primary]">{report.overview.departmentBreakdown.length}</p>
</div> </div>
<div className="bg-[--background-tertiary] p-4 rounded-lg"> <div className="bg-[--background-tertiary] p-4 rounded-lg">
<h3 className="text-sm font-medium text-[--text-secondary]">Avg Performance</h3> <h3 className="text-sm font-medium text-[--text-secondary]">Avg Performance</h3>
@@ -219,7 +220,8 @@ const EmployeeCard: React.FC<{
isOwner: boolean; isOwner: boolean;
onGenerateReport?: (employee: Employee) => void; onGenerateReport?: (employee: Employee) => void;
isGeneratingReport?: boolean; 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); const [isExpanded, setIsExpanded] = useState(false);
return ( return (
@@ -243,13 +245,22 @@ const EmployeeCard: React.FC<{
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
{report && ( {report && (
<>
<Button
size="sm"
variant="secondary"
onClick={() => onViewReport?.(report, employee.name)}
>
View Full Report
</Button>
<Button <Button
size="sm" size="sm"
variant="secondary" variant="secondary"
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
> >
{isExpanded ? 'Hide' : 'View'} Report {isExpanded ? 'Hide' : 'View'} Summary
</Button> </Button>
</>
)} )}
{isOwner && mode === 'reports' && ( {isOwner && mode === 'reports' && (
<Button <Button
@@ -327,9 +338,11 @@ const EmployeeCard: React.FC<{
}; };
const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => { const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, saveReport, orgId } = useOrg(); const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, saveReport, orgId } = useOrg();
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null); const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set()); const [generatingReports, setGeneratingReports] = useState<Set<string>>(new Set());
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | Report; type: 'company' | 'employee'; employeeName?: string } | null>(null);
useEffect(() => { useEffect(() => {
// Load company report for owners // Load company report for owners
@@ -358,32 +371,19 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
try { try {
console.log('Generating report for employee:', employee.name, 'in org:', orgId); console.log('Generating report for employee:', employee.name, 'in org:', orgId);
// Call the API endpoint with orgId // Use the OrgContext method instead of direct API call
const response = await fetch(`/api/employee-report`, { const report = await generateEmployeeReport(employee);
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
employeeId: employee.id,
orgId: orgId
}),
});
if (response.ok) { if (report) {
const result = await response.json(); console.log('Report generated and saved successfully for:', employee.name);
if (result.success && result.report) {
// Save the report using the context method
await saveReport(employee.id, result.report);
console.log('Report generated and saved successfully');
} else { } else {
console.error('Report generation failed:', result.error || 'Unknown error'); console.error('Report generation failed for:', employee.name);
} // Show user-friendly error
} else { alert(`Failed to generate report for ${employee.name}. Please try again.`);
console.error('API call failed:', response.status, response.statusText);
} }
} catch (error) { } catch (error) {
console.error('Error generating report:', error); console.error('Error generating report:', error);
alert(`Error generating report for ${employee.name}: ${error.message}`);
} finally { } finally {
setGeneratingReports(prev => { setGeneratingReports(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
@@ -391,7 +391,26 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
return newSet; return newSet;
}); });
} }
}; const currentUserIsOwner = isOwner(user?.uid || ''); };
const handleGenerateCompanyReport = async () => {
setGeneratingCompanyReport(true);
try {
console.log('Generating company report for org:', orgId);
const newReport = await generateCompanyReport();
console.log('Received new company report:', newReport);
setCompanyReport(newReport);
console.log('Company report generated and state updated successfully');
} catch (error) {
console.error('Error generating company report:', error);
alert(`Error generating company report: ${error.message}`);
} finally {
setGeneratingCompanyReport(false);
}
};
const currentUserIsOwner = isOwner(user?.uid || '');
// Filter employees based on user access // Filter employees based on user access
const visibleEmployees = currentUserIsOwner const visibleEmployees = currentUserIsOwner
@@ -412,8 +431,51 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
</div> </div>
{/* Company Report - Only visible to owners in reports mode */} {/* Company Report - Only visible to owners in reports mode */}
{currentUserIsOwner && mode === 'reports' && companyReport && ( {currentUserIsOwner && mode === 'reports' && (
<div className="mb-6">
{companyReport ? (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-[--text-primary]">Company Report</h2>
<div className="flex space-x-2">
<Button
size="sm"
variant="secondary"
onClick={() => setSelectedReport({ report: companyReport, type: 'company' })}
>
View Full Report
</Button>
<Button
size="sm"
onClick={handleGenerateCompanyReport}
disabled={generatingCompanyReport}
>
{generatingCompanyReport ? 'Regenerating...' : 'Regenerate Report'}
</Button>
</div>
</div>
<CompanyReportCard report={companyReport} /> <CompanyReportCard report={companyReport} />
</div>
) : (
<Card>
<div className="text-center py-8">
<h3 className="text-lg font-semibold text-[--text-primary] mb-2">
Generate Company Report
</h3>
<p className="text-[--text-secondary] mb-4">
Create a comprehensive AI-powered report analyzing your organization's performance,
strengths, and recommendations based on employee data.
</p>
<Button
onClick={handleGenerateCompanyReport}
disabled={generatingCompanyReport}
>
{generatingCompanyReport ? 'Generating Report...' : 'Generate Company Report'}
</Button>
</div>
</Card>
)}
</div>
)} )}
{/* Employee Cards */} {/* Employee Cards */}
@@ -442,10 +504,21 @@ const EmployeeData: React.FC<EmployeeDataProps> = ({ mode }) => {
isOwner={currentUserIsOwner} isOwner={currentUserIsOwner}
onGenerateReport={handleGenerateReport} onGenerateReport={handleGenerateReport}
isGeneratingReport={generatingReports.has(employee.id)} isGeneratingReport={generatingReports.has(employee.id)}
onViewReport={(report, employeeName) => setSelectedReport({ report, type: 'employee', employeeName })}
/> />
)) ))
)} )}
</div> </div>
{/* Report Detail Modal */}
{selectedReport && (
<ReportDetail
report={selectedReport.report}
type={selectedReport.type}
employeeName={selectedReport.employeeName}
onClose={() => setSelectedReport(null)}
/>
)}
</div> </div>
); );
}; };

View File

@@ -15,17 +15,28 @@ const EmployeeQuestionnaire: React.FC = () => {
const location = useLocation(); const location = useLocation();
const params = useParams(); const params = useParams();
const { user } = useAuth(); 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<EmployeeSubmissionAnswers>({}); const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [inviteEmployee, setInviteEmployee] = useState<any>(null); const [inviteEmployee, setInviteEmployee] = useState<any>(null);
const [isLoadingInvite, setIsLoadingInvite] = useState(false); 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 // Load invite details if this is an invite flow
useEffect(() => { useEffect(() => {
if (inviteCode) { if (inviteCode) {
@@ -36,15 +47,24 @@ const EmployeeQuestionnaire: React.FC = () => {
const loadInviteDetails = async (code: string) => { const loadInviteDetails = async (code: string) => {
setIsLoadingInvite(true); setIsLoadingInvite(true);
try { 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) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data.used) {
setError('This invitation has already been used');
} else if (data.employee) {
setInviteEmployee(data.employee); setInviteEmployee(data.employee);
setError(''); setError('');
} else { } else {
setError('Invalid or expired invitation link'); setError('Invalid invitation data');
}
} else {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
setError(errorData.error || 'Invalid or expired invitation link');
} }
} catch (err) { } catch (err) {
console.error('Error loading invite details:', err);
setError('Failed to load invitation details'); setError('Failed to load invitation details');
} finally { } finally {
setIsLoadingInvite(false); 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 { try {
// First, consume the invite to mark it as used // First, consume the invite to mark it as used
const consumeResponse = await fetch(`${API_URL}/api/invitations/${inviteCode}/consume`, { const consumeResponse = await fetch(`${API_URL}/consumeInvitation`, {
method: 'POST' method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: inviteCode
})
}); });
if (!consumeResponse.ok) { if (!consumeResponse.ok) {
throw new Error('Failed to process invitation'); throw new Error('Failed to process invitation');
} }
// Submit the questionnaire answers // Get orgId from the consume response
const submitResponse = await fetch(`${API_URL}/api/employee-submissions`, { 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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
employeeId: employee.id, inviteCode: inviteCode,
employee: employee, answers: answers,
answers: answers orgId: orgId
}) })
}); });
if (!submitResponse.ok) { 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(); const result = await submitResponse.json();
@@ -223,7 +252,7 @@ const EmployeeQuestionnaire: React.FC = () => {
let result; let result;
if (isInviteFlow) { if (isInviteFlow) {
// Direct API submission for invite flow (no auth needed) // Direct API submission for invite flow (no auth needed)
result = await submitViaInvite(currentEmployee, answers, inviteCode); result = await submitViaInvite(answers, inviteCode);
} else { } else {
// Use org context for authenticated flow // Use org context for authenticated flow
result = await submitEmployeeAnswers(currentEmployee.id, answers); result = await submitEmployeeAnswers(currentEmployee.id, answers);
@@ -338,7 +367,7 @@ const EmployeeQuestionnaire: React.FC = () => {
<LinearProgress value={getProgressPercentage()} /> <LinearProgress value={getProgressPercentage()} />
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div className="space-y-6"> <div className="space-y-6">
{visibleQuestions.map((question, index) => ( {visibleQuestions.map((question, index) => (
<Question <Question
@@ -372,7 +401,7 @@ const EmployeeQuestionnaire: React.FC = () => {
disabled={isSubmitting || getProgressPercentage() < 70} disabled={isSubmitting || getProgressPercentage() < 70}
className="px-8 py-3" className="px-8 py-3"
> >
{isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'} {isSubmitting ? 'Submitting...' : 'Submit'}
</Button> </Button>
</div> </div>

View File

@@ -45,7 +45,12 @@ const HelpAndSettings: React.FC = () => {
department: inviteForm.department.trim() || undefined 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: '' }); setInviteForm({ name: '', email: '', role: '', department: '' });
} catch (error) { } catch (error) {
console.error('Failed to send invitation:', error); console.error('Failed to send invitation:', error);
@@ -178,12 +183,62 @@ const HelpAndSettings: React.FC = () => {
</Button> </Button>
{inviteResult && ( {inviteResult && (
<div className={`p-3 rounded-md text-sm ${inviteResult.includes('Failed') <div>
? 'bg-red-50 text-red-800 border border-red-200' {inviteResult.includes('Failed') ? (
: 'bg-green-50 text-green-800 border border-green-200' <div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
}`}>
{inviteResult} {inviteResult}
</div> </div>
) : (
(() => {
try {
const result = JSON.parse(inviteResult);
return (
<div className="p-4 rounded-md bg-green-50 border border-green-200">
<h4 className="text-sm font-semibold text-green-800 mb-3">
Invitation sent to {result.employeeName}!
</h4>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-green-700 mb-1">
Direct Link (share this with the employee):
</label>
<div className="flex gap-2">
<input
type="text"
value={result.inviteLink}
readOnly
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
/>
<Button
size="sm"
variant="secondary"
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
>
Copy
</Button>
</div>
</div>
<div>
<a
href={result.emailLink}
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
>
📧 Open Email Draft
</a>
</div>
</div>
</div>
);
} catch {
return (
<div className="p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200">
{inviteResult}
</div>
);
}
})()
)}
</div>
)} )}
<p className="text-xs text-[--text-secondary]"> <p className="text-xs text-[--text-secondary]">

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useOrg } from '../contexts/OrgContext'; import { useOrg } from '../contexts/OrgContext';
import { Card, Button } from '../components/UiKit'; import { Card, Button } from '../components/UiKit';
@@ -48,6 +48,13 @@ interface OnboardingData {
const Onboarding: React.FC = () => { const Onboarding: React.FC = () => {
const { org, upsertOrg, generateCompanyWiki } = useOrg(); const { org, upsertOrg, generateCompanyWiki } = useOrg();
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
if (org?.onboardingCompleted) {
navigate('/reports', { replace: true });
}
}, [org, navigate]);
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [isGeneratingReport, setIsGeneratingReport] = useState(false); const [isGeneratingReport, setIsGeneratingReport] = useState(false);
const [formData, setFormData] = useState<OnboardingData>({ const [formData, setFormData] = useState<OnboardingData>({
@@ -152,7 +159,7 @@ const Onboarding: React.FC = () => {
console.log('Org data saved successfully'); console.log('Org data saved successfully');
console.log('Generating company wiki...'); console.log('Generating company wiki...');
await generateCompanyWiki(newOrgData); await generateCompanyWiki({ ...newOrgData, orgId: org!.orgId });
console.log('Company wiki generated successfully'); console.log('Company wiki generated successfully');
// Small delay to ensure states are updated, then redirect // Small delay to ensure states are updated, then redirect

413
pages/ReportDetail.tsx Normal file
View File

@@ -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<ReportDetailProps> = ({ report, type, employeeName, onClose }) => {
if (type === 'company') {
const companyReport = report as CompanyReport;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-[--background-primary] rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-[--text-primary]">Company Report</h1>
<p className="text-[--text-secondary]">Last updated: {new Date(companyReport.createdAt).toLocaleDateString()}</p>
</div>
<div className="flex space-x-2">
<Button size="sm">Download as PDF</Button>
<Button size="sm" variant="secondary" onClick={onClose}>Close</Button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Executive Summary */}
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Executive Summary</h2>
<p className="text-[--text-secondary] whitespace-pre-line leading-relaxed">
{companyReport.executiveSummary}
</p>
</Card>
{/* Overview Stats */}
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Company Overview</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-blue-500 mb-1">{companyReport.overview.totalEmployees}</div>
<div className="text-sm text-[--text-secondary]">Total Employees</div>
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-green-500 mb-1">{companyReport.overview.departmentBreakdown?.length || 0}</div>
<div className="text-sm text-[--text-secondary]">Departments</div>
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-purple-500 mb-1">{companyReport.overview.averagePerformanceScore || 'N/A'}</div>
<div className="text-sm text-[--text-secondary]">Avg Performance</div>
</div>
<div className="bg-[--background-tertiary] p-4 rounded-lg text-center">
<div className="text-3xl font-bold text-orange-500 mb-1">{companyReport.overview.riskLevel || 'Low'}</div>
<div className="text-sm text-[--text-secondary]">Risk Level</div>
</div>
</div>
</Card>
{/* Key Personnel Changes */}
{companyReport.keyPersonnelChanges && companyReport.keyPersonnelChanges.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-orange-500 rounded-full mr-3"></span>
Key Personnel Changes
</h2>
<div className="space-y-3">
{companyReport.keyPersonnelChanges.map((change, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="font-semibold text-[--text-primary]">{change.employeeName}</h3>
<p className="text-sm text-[--text-secondary]">{change.role} {change.department}</p>
</div>
<span className={`px-3 py-1 text-xs rounded-full font-medium ${
change.changeType === 'departure' ? 'bg-red-100 text-red-800' :
change.changeType === 'promotion' ? 'bg-green-100 text-green-800' :
'bg-blue-100 text-blue-800'
}`}>
{change.changeType}
</span>
</div>
<p className="text-[--text-secondary] text-sm">{change.impact}</p>
</div>
))}
</div>
</Card>
)}
{/* Immediate Hiring Needs */}
{companyReport.immediateHiringNeeds && companyReport.immediateHiringNeeds.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-red-500 rounded-full mr-3"></span>
Immediate Hiring Needs
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{companyReport.immediateHiringNeeds.map((need, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<div className="flex justify-between items-start mb-2">
<h3 className="font-semibold text-[--text-primary]">{need.role}</h3>
<span className={`px-2 py-1 text-xs rounded-full font-medium ${
need.urgency === 'high' ? 'bg-red-100 text-red-800' :
need.urgency === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{need.urgency}
</span>
</div>
<p className="text-sm text-[--text-secondary] mb-2">{need.department}</p>
<p className="text-sm text-[--text-secondary]">{need.reason}</p>
</div>
))}
</div>
</Card>
)}
{/* Forward Operating Plan */}
{companyReport.forwardOperatingPlan && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
Forward Operating Plan
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-4 bg-[--background-tertiary] rounded-lg">
<h3 className="font-semibold text-[--text-primary] mb-3">Next Quarter Goals</h3>
<ul className="space-y-2">
{companyReport.forwardOperatingPlan.nextQuarterGoals?.map((goal, idx) => (
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
<span className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
{goal}
</li>
))}
</ul>
</div>
<div className="p-4 bg-[--background-tertiary] rounded-lg">
<h3 className="font-semibold text-[--text-primary] mb-3">Key Initiatives</h3>
<ul className="space-y-2">
{companyReport.forwardOperatingPlan.keyInitiatives?.map((initiative, idx) => (
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
<span className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
{initiative}
</li>
))}
</ul>
</div>
</div>
</Card>
)}
{/* Organizational Strengths */}
{companyReport.organizationalStrengths && companyReport.organizationalStrengths.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
Organizational Strengths
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{companyReport.organizationalStrengths.map((strength, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<div className="flex items-start space-x-3">
<span className="text-3xl">{strength.icon || '💪'}</span>
<div>
<h3 className="font-semibold text-[--text-primary] mb-1">{strength.area || strength}</h3>
<p className="text-sm text-[--text-secondary]">{strength.description}</p>
</div>
</div>
</div>
))}
</div>
</Card>
)}
{/* Grading Overview */}
{companyReport.gradingOverview && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-indigo-500 rounded-full mr-3"></span>
Grading Overview
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{Object.entries(companyReport.gradingOverview).map(([category, score], idx) => (
<div key={idx} className="text-center p-4 bg-[--background-tertiary] rounded-lg">
<div className="text-3xl font-bold text-[--text-primary] mb-2">{score}/5</div>
<div className="text-sm text-[--text-secondary] capitalize">
{category.replace(/([A-Z])/g, ' $1').trim()}
</div>
</div>
))}
</div>
</Card>
)}
{/* Organizational Impact Summary */}
{companyReport.organizationalImpactSummary && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
Organizational Impact Summary
</h2>
<div className="p-4 bg-[--background-tertiary] rounded-lg">
<p className="text-[--text-secondary] leading-relaxed">
{companyReport.organizationalImpactSummary}
</p>
</div>
</Card>
)}
</div>
</div>
</div>
);
} else {
const employeeReport = report as Report;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-[--background-primary] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-[--text-primary]">{employeeName}'s Performance Report</h1>
<p className="text-[--text-secondary]">{employeeReport.employee?.role} • {employeeReport.employee?.department}</p>
</div>
<div className="flex space-x-2">
<Button size="sm">Download as PDF</Button>
<Button size="sm" variant="secondary" onClick={onClose}>Close</Button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Self-Reported Role & Output */}
{employeeReport.roleAndOutput && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Self-Reported Role & Output</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="font-medium text-[--text-primary] mb-2">Responsibilities</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed">{employeeReport.roleAndOutput.responsibilities}</p>
</div>
<div className="space-y-3">
<div>
<h3 className="font-medium text-[--text-primary] mb-1">Clarity on Role</h3>
<p className="text-[--text-secondary] text-sm">{employeeReport.roleAndOutput.clarityOnRole}</p>
</div>
<div>
<h3 className="font-medium text-[--text-primary] mb-1">Self-Rated Output</h3>
<p className="text-[--text-secondary] text-sm">{employeeReport.roleAndOutput.selfRatedOutput}</p>
</div>
</div>
</div>
</Card>
)}
{/* Performance Charts */}
{employeeReport.grading?.[0]?.scores && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Performance Analysis</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-[--background-tertiary] rounded-lg p-6">
<RadarPerformanceChart
title="Performance Profile"
data={employeeReport.grading[0].scores.map(s => ({
label: s.subject,
value: (s.value / s.fullMark) * 100
}))}
/>
</div>
<div className="bg-[--background-tertiary] rounded-lg p-6">
<ScoreBarList
title="Score Breakdown"
items={employeeReport.grading[0].scores.map(s => ({
label: s.subject,
value: s.value,
max: s.fullMark
}))}
/>
</div>
</div>
</Card>
)}
{/* Behavioral & Psychological Insights */}
{employeeReport.insights && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">Behavioral & Psychological Insights</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="font-medium text-[--text-primary] mb-3">Personality Traits</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed mb-4">
{employeeReport.insights.personalityTraits || 'No personality traits data available.'}
</p>
<h3 className="font-medium text-[--text-primary] mb-3">Self-awareness</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed">
{employeeReport.insights.selfAwareness || 'No self-awareness data available.'}
</p>
</div>
<div>
<h3 className="font-medium text-[--text-primary] mb-3">Psychological Indicators</h3>
<ul className="space-y-2 mb-4">
{employeeReport.insights.psychologicalIndicators?.map((indicator, idx) => (
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
{indicator}
</li>
)) || <li className="text-sm text-[--text-secondary]">No psychological indicators available.</li>}
</ul>
<h3 className="font-medium text-[--text-primary] mb-3">Growth Desire</h3>
<p className="text-[--text-secondary] text-sm leading-relaxed">
{employeeReport.insights.growthDesire || 'No growth desire data available.'}
</p>
</div>
</div>
</Card>
)}
{/* Strengths & Weaknesses */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-green-500 rounded-full mr-3"></span>
Strengths
</h2>
<div className="space-y-2">
{employeeReport.insights?.strengths?.map((strength, idx) => (
<div key={idx} className="flex items-center space-x-2">
<span className="text-green-500">✓</span>
<span className="text-sm text-[--text-secondary]">{strength}</span>
</div>
))}
</div>
</Card>
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-orange-500 rounded-full mr-3"></span>
Development Areas
</h2>
<div className="space-y-2">
{employeeReport.insights?.weaknesses?.map((weakness, idx) => (
<div key={idx} className="flex items-center space-x-2">
<span className="text-orange-500">!</span>
<span className="text-sm text-[--text-secondary]">{weakness}</span>
</div>
))}
</div>
</Card>
</div>
{/* Opportunities */}
{employeeReport.opportunities && employeeReport.opportunities.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
Opportunities
</h2>
<div className="space-y-4">
{employeeReport.opportunities.map((opp, idx) => (
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
<h3 className="font-medium text-[--text-primary] mb-2">{opp.roleAdjustment || 'Opportunity'}</h3>
<p className="text-sm text-[--text-secondary]">{opp.accountabilitySupport || opp.description}</p>
</div>
))}
</div>
</Card>
)}
{/* Risks */}
{employeeReport.risks && employeeReport.risks.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-3"></span>
Risks
</h2>
<div className="space-y-2">
{employeeReport.risks.map((risk, idx) => (
<div key={idx} className="flex items-start space-x-2 p-3 bg-yellow-50 rounded-lg">
<span className="text-yellow-500 mt-0.5"></span>
<span className="text-sm text-gray-700">{risk}</span>
</div>
))}
</div>
</Card>
)}
{/* Recommendations */}
{employeeReport.recommendations && employeeReport.recommendations.length > 0 && (
<Card>
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
<span className="w-3 h-3 bg-purple-500 rounded-full mr-3"></span>
Recommendations
</h2>
<div className="space-y-3">
{employeeReport.recommendations.map((rec, idx) => (
<div key={idx} className="flex items-start space-x-3 p-3 bg-[--background-tertiary] rounded-lg">
<span className="w-2 h-2 bg-purple-500 rounded-full mt-2 flex-shrink-0"></span>
<span className="text-sm text-[--text-secondary]">{rec}</span>
</div>
))}
</div>
</Card>
)}
</div>
</div>
</div>
);
}
};
export default ReportDetail;

88
services/api.ts Normal file
View File

@@ -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<Response> => {
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);

View File

@@ -1,6 +1,6 @@
import { initializeApp, getApps } from 'firebase/app'; import { initializeApp, getApps } from 'firebase/app';
import { getAuth, GoogleAuthProvider } from 'firebase/auth'; import { getAuth, GoogleAuthProvider, setPersistence, browserLocalPersistence } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore'; import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
const firebaseConfig = { const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY, apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
@@ -17,9 +17,7 @@ export const isFirebaseConfigured = Boolean(
firebaseConfig.projectId && firebaseConfig.projectId &&
firebaseConfig.apiKey !== 'undefined' && firebaseConfig.apiKey !== 'undefined' &&
firebaseConfig.authDomain !== 'undefined' && firebaseConfig.authDomain !== 'undefined' &&
firebaseConfig.projectId !== 'undefined' && firebaseConfig.projectId !== 'undefined'
// Force demo mode on localhost for testing
!window.location.hostname.includes('localhost')
); );
let app; let app;
@@ -30,12 +28,29 @@ let googleProvider;
if (isFirebaseConfigured) { if (isFirebaseConfigured) {
app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
auth = getAuth(app); auth = getAuth(app);
setPersistence(auth, browserLocalPersistence);
db = getFirestore(app); db = getFirestore(app);
googleProvider = new GoogleAuthProvider(); 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 { } else {
auth = null; auth = null;
db = null; db = null;
googleProvider = null; googleProvider = null;
console.log('⚠️ Firebase not configured - missing environment variables');
} }
export { auth, db, googleProvider }; export { auth, db, googleProvider };