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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
21
App.tsx
@@ -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
|
||||||
|
|||||||
33
bun.lock
33
bun.lock
@@ -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=="],
|
||||||
|
|||||||
175
constants.ts
175
constants.ts
@@ -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.`
|
|
||||||
};
|
};
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 }> => {
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
89
index.css
89
index.css
@@ -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;
|
|
||||||
/* Brand Main */
|
|
||||||
--sidebar-active-text: #FFFFFF;
|
--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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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]">
|
||||||
|
|||||||
@@ -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
413
pages/ReportDetail.tsx
Normal 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
88
services/api.ts
Normal 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);
|
||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user