update 9/23
This commit is contained in:
18
.gitignore
vendored
18
.gitignore
vendored
@@ -72,9 +72,15 @@ dist-ssr
|
||||
*.ignore
|
||||
*.py
|
||||
*.md
|
||||
.github/instructions
|
||||
schema.sql
|
||||
setup-neondb.sh
|
||||
functions/migrations/seed.js
|
||||
dev-setup.sh
|
||||
docker-compose.yml
|
||||
/.github/instructions
|
||||
/schema.sql
|
||||
/functions/setup-neondb.sh
|
||||
/functions/migrations/seed.js
|
||||
/dev-setup.sh
|
||||
/docker-compose.yml
|
||||
/migrate-firestore-to-postgres.js
|
||||
/migration-package.json
|
||||
/functions/index-old.js
|
||||
/functions/migrations/
|
||||
/functions/migrated_functions.js
|
||||
/functions/database.js
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
"name": "auditly-functions",
|
||||
"dependencies": {
|
||||
"@google-cloud/vertexai": "^1.10.0",
|
||||
"@neondatabase/serverless": "^0.9.5",
|
||||
"@neondatabase/serverless": "0.9.5",
|
||||
"firebase-admin": "^13.5.0",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"pg": "^8.12.0",
|
||||
"nodemailer": "6.10.1",
|
||||
"pg": "^8.16.3",
|
||||
"stripe": "^18.5.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -97,23 +99,23 @@
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
|
||||
|
||||
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.2", "", {}, "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ=="],
|
||||
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.3", "", {}, "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A=="],
|
||||
|
||||
"@firebase/app-types": ["@firebase/app-types@0.9.2", "", {}, "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ=="],
|
||||
"@firebase/app-types": ["@firebase/app-types@0.9.3", "", {}, "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="],
|
||||
|
||||
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.3", "", {}, "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ=="],
|
||||
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.4", "", {}, "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA=="],
|
||||
|
||||
"@firebase/component": ["@firebase/component@0.6.9", "", { "dependencies": { "@firebase/util": "1.10.0", "tslib": "^2.1.0" } }, "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q=="],
|
||||
"@firebase/component": ["@firebase/component@0.7.0", "", { "dependencies": { "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg=="],
|
||||
|
||||
"@firebase/database": ["@firebase/database@1.0.8", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.2", "@firebase/auth-interop-types": "0.2.3", "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg=="],
|
||||
"@firebase/database": ["@firebase/database@1.1.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg=="],
|
||||
|
||||
"@firebase/database-compat": ["@firebase/database-compat@1.0.8", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/database": "1.0.8", "@firebase/database-types": "1.0.5", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" } }, "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg=="],
|
||||
"@firebase/database-compat": ["@firebase/database-compat@2.1.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/database": "1.1.0", "@firebase/database-types": "1.0.16", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg=="],
|
||||
|
||||
"@firebase/database-types": ["@firebase/database-types@1.0.5", "", { "dependencies": { "@firebase/app-types": "0.9.2", "@firebase/util": "1.10.0" } }, "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ=="],
|
||||
"@firebase/database-types": ["@firebase/database-types@1.0.16", "", { "dependencies": { "@firebase/app-types": "0.9.3", "@firebase/util": "1.13.0" } }, "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw=="],
|
||||
|
||||
"@firebase/logger": ["@firebase/logger@0.4.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A=="],
|
||||
"@firebase/logger": ["@firebase/logger@0.5.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g=="],
|
||||
|
||||
"@firebase/util": ["@firebase/util@1.10.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ=="],
|
||||
"@firebase/util": ["@firebase/util@1.13.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ=="],
|
||||
|
||||
"@google-cloud/firestore": ["@google-cloud/firestore@7.11.3", "", { "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", "protobufjs": "^7.2.6" } }, "sha512-qsM3/WHpawF07SRVvEJJVRwhYzM7o9qtuksyuqnrMig6fxIrwWnsezECWsG/D5TyYru51Fv5c/RTqNDQ2yU+4w=="],
|
||||
|
||||
@@ -425,7 +427,7 @@
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
|
||||
|
||||
@@ -509,7 +511,7 @@
|
||||
|
||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
||||
"firebase-admin": ["firebase-admin@12.7.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "1.0.8", "@firebase/database-types": "1.0.5", "@types/node": "^22.0.1", "farmhash-modern": "^1.1.0", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^10.0.0" }, "optionalDependencies": { "@google-cloud/firestore": "^7.7.0", "@google-cloud/storage": "^7.7.0" } }, "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA=="],
|
||||
"firebase-admin": ["firebase-admin@13.5.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", "@firebase/database-types": "^1.0.6", "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", "fast-deep-equal": "^3.1.1", "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^11.0.2" }, "optionalDependencies": { "@google-cloud/firestore": "^7.11.0", "@google-cloud/storage": "^7.14.0" } }, "sha512-QZOpv1DJRJpH8NcWiL1xXE10tw3L/bdPFlgjcWrqU3ufyOJDYfxB1MMtxiVTwxK16NlybQbEM6ciSich2uWEIQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -769,6 +771,8 @@
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"nodemailer": ["nodemailer@6.10.1", "", {}, "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
@@ -981,7 +985,7 @@
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||
|
||||
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
|
||||
|
||||
@@ -1019,16 +1023,12 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@babel/core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"@google-cloud/storage/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
@@ -1043,28 +1043,24 @@
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"http-proxy-agent/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"https-proxy-agent/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"istanbul-lib-source-maps/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"jwks-rsa/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
@@ -1073,6 +1069,8 @@
|
||||
|
||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
@@ -1095,11 +1093,17 @@
|
||||
|
||||
"@neondatabase/serverless/@types/pg/pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"teeny-request/https-proxy-agent/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
"teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
const { Pool } = require('pg');
|
||||
const { neon } = require('@neondatabase/serverless');
|
||||
|
||||
// Database configuration
|
||||
const DB_CONFIG = {
|
||||
// Use Neon serverless for edge deployments
|
||||
connectionString: process.env.DATABASE_URL || process.env.NEON_DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
};
|
||||
|
||||
// Create connection pool for traditional deployment
|
||||
const pool = new Pool(DB_CONFIG);
|
||||
|
||||
// Create Neon serverless client for edge/serverless deployment
|
||||
let neonClient = null;
|
||||
if (DB_CONFIG.connectionString) {
|
||||
neonClient = neon(DB_CONFIG.connectionString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database client - either pool connection or Neon serverless
|
||||
* @returns {Promise<any>} Database client
|
||||
*/
|
||||
async function getDbClient() {
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true' && neonClient) {
|
||||
return neonClient;
|
||||
}
|
||||
|
||||
return pool.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query using the appropriate client
|
||||
* @param {string} query - SQL query
|
||||
* @param {any[]} params - Query parameters
|
||||
* @returns {Promise<any>} Query result
|
||||
*/
|
||||
async function executeQuery(query, params = []) {
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true' && neonClient) {
|
||||
// Neon serverless client
|
||||
return await neonClient(query, params);
|
||||
} else {
|
||||
// Traditional pool connection
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(query, params);
|
||||
return result.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a transaction
|
||||
* @param {Function} callback - Function to execute within transaction
|
||||
* @returns {Promise<any>} Transaction result
|
||||
*/
|
||||
async function executeTransaction(callback) {
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true' && neonClient) {
|
||||
// For Neon serverless, we'll need to handle this differently
|
||||
// Note: Neon serverless doesn't support traditional transactions
|
||||
// Each query is automatically committed
|
||||
return await callback(neonClient);
|
||||
} else {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const result = await callback(client);
|
||||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all database connections
|
||||
*/
|
||||
async function closeConnections() {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool,
|
||||
neonClient,
|
||||
getDbClient,
|
||||
executeQuery,
|
||||
executeTransaction,
|
||||
closeConnections,
|
||||
DB_CONFIG
|
||||
};
|
||||
1293
functions/index.js
1293
functions/index.js
File diff suppressed because it is too large
Load Diff
@@ -1,358 +0,0 @@
|
||||
// Complete Migration Helper Script for remaining functions
|
||||
// This script contains the migrated versions of key functions
|
||||
|
||||
const { executeQuery, executeTransaction } = require('./database');
|
||||
|
||||
/**
|
||||
* Get User Organizations - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getUserOrganizations = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get user's organizations
|
||||
const userOrgsRows = await executeQuery(
|
||||
`SELECT uo.organization_id as orgId, uo.role, uo.joined_at as joinedAt,
|
||||
uo.onboarding_completed as onboardingCompleted, o.name
|
||||
FROM user_organizations uo
|
||||
JOIN organizations o ON uo.organization_id = o.id
|
||||
WHERE uo.user_id = $1`,
|
||||
[authContext.userId]
|
||||
);
|
||||
|
||||
const organizations = userOrgsRows.map(row => ({
|
||||
orgId: row.orgid,
|
||||
name: row.name,
|
||||
role: row.role,
|
||||
onboardingCompleted: row.onboardingcompleted,
|
||||
joinedAt: row.joinedat
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
organizations,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get user organizations error:", error);
|
||||
res.status(500).json({ error: "Failed to get user organizations" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Organization Data - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Get organization data
|
||||
const orgRows = await executeQuery(
|
||||
'SELECT * FROM organizations WHERE id = $1',
|
||||
[orgId]
|
||||
);
|
||||
|
||||
if (orgRows.length === 0) {
|
||||
return res.status(404).json({ error: "Organization not found" });
|
||||
}
|
||||
|
||||
const orgData = {
|
||||
id: orgRows[0].id,
|
||||
name: orgRows[0].name,
|
||||
industry: orgRows[0].industry,
|
||||
description: orgRows[0].description,
|
||||
mission: orgRows[0].mission,
|
||||
vision: orgRows[0].vision,
|
||||
values: orgRows[0].values,
|
||||
onboardingCompleted: orgRows[0].onboarding_completed,
|
||||
onboardingData: orgRows[0].onboarding_data,
|
||||
createdAt: orgRows[0].created_at,
|
||||
updatedAt: orgRows[0].updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
org: orgData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get org data error:", error);
|
||||
res.status(500).json({ error: "Failed to get organization data" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update Organization Data - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "PUT") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = req.body;
|
||||
|
||||
if (!data) {
|
||||
return res.status(400).json({ error: "Data is required" });
|
||||
}
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Build dynamic update query
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
let paramCount = 1;
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// Convert camelCase to snake_case for database columns
|
||||
const dbKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||
updateFields.push(`${dbKey} = $${paramCount}`);
|
||||
values.push(typeof value === 'object' ? JSON.stringify(value) : value);
|
||||
paramCount++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: "No valid fields to update" });
|
||||
}
|
||||
|
||||
updateFields.push(`updated_at = $${paramCount}`);
|
||||
values.push(Date.now());
|
||||
values.push(orgId); // for WHERE clause
|
||||
|
||||
const query = `UPDATE organizations SET ${updateFields.join(', ')} WHERE id = $${paramCount + 1}`;
|
||||
await executeQuery(query, values);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Organization data updated successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Update org data error:", error);
|
||||
res.status(500).json({ error: "Failed to update organization data" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Employees - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Get all employees
|
||||
const employeesRows = await executeQuery(
|
||||
'SELECT * FROM employees WHERE organization_id = $1 AND status != $2 ORDER BY name',
|
||||
[orgId, 'deleted']
|
||||
);
|
||||
|
||||
const employees = employeesRows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
role: row.role,
|
||||
department: row.department,
|
||||
status: row.status,
|
||||
joinedAt: row.joined_at,
|
||||
createdAt: row.created_at
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
employees
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get employees error:", error);
|
||||
res.status(500).json({ error: "Failed to get employees" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create Invitation - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, email, role = "employee", department } = req.body;
|
||||
|
||||
if (!email || !name) {
|
||||
return res.status(400).json({ error: "Name and email are required" });
|
||||
}
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
}
|
||||
|
||||
// Generate invite code
|
||||
const code = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
// Generate employee ID
|
||||
const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Create employee data
|
||||
const employeeData = {
|
||||
id: employeeId,
|
||||
name: name.trim(),
|
||||
email: email.trim(),
|
||||
role: role?.trim() || "employee",
|
||||
department: department?.trim() || "General",
|
||||
status: "invited",
|
||||
inviteCode: code
|
||||
};
|
||||
|
||||
await executeTransaction(async (client) => {
|
||||
// Store invitation
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
await client(
|
||||
`INSERT INTO invites (code, organization_id, email, employee_data, status, created_at, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
code, orgId, email, JSON.stringify(employeeData), 'pending',
|
||||
currentTime, currentTime + (7 * 24 * 60 * 60 * 1000) // 7 days
|
||||
]
|
||||
);
|
||||
|
||||
// Create employee record
|
||||
await client(
|
||||
`INSERT INTO employees (id, organization_id, name, email, role, department, status, invite_code, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[employeeId, orgId, name.trim(), email.trim(), role || 'employee', department || 'General', 'invited', code, currentTime]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO invites (code, organization_id, email, employee_data, status, created_at, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
code, orgId, email, JSON.stringify(employeeData), 'pending',
|
||||
currentTime, currentTime + (7 * 24 * 60 * 60 * 1000) // 7 days
|
||||
]
|
||||
);
|
||||
|
||||
// Create employee record
|
||||
await client.query(
|
||||
`INSERT INTO employees (id, organization_id, name, email, role, department, status, invite_code, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[employeeId, orgId, name.trim(), email.trim(), role || 'employee', department || 'General', 'invited', code, currentTime]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate invite links
|
||||
const baseUrl = process.env.CLIENT_URL || 'https://auditly-one.vercel.app';
|
||||
const inviteLink = `${baseUrl}/#/employee-form/${code}`;
|
||||
|
||||
console.log(`📧 Invite link: ${inviteLink}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code,
|
||||
employee: employeeData,
|
||||
inviteLink,
|
||||
message: "Invitation sent successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Create invitation error:", error);
|
||||
res.status(500).json({ error: "Failed to create invitation" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Invitation Status - Migrated to PostgreSQL
|
||||
*/
|
||||
exports.getInvitationStatus = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { code } = req.query;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: "Invitation code is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const inviteRows = await executeQuery(
|
||||
'SELECT * FROM invites WHERE code = $1',
|
||||
[code]
|
||||
);
|
||||
|
||||
if (inviteRows.length === 0) {
|
||||
return res.status(404).json({ error: "Invitation not found" });
|
||||
}
|
||||
|
||||
const invite = inviteRows[0];
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() > invite.expires_at) {
|
||||
return res.status(400).json({ error: "Invitation has expired" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
used: invite.status !== 'pending',
|
||||
employee: invite.employee_data,
|
||||
invite: {
|
||||
...invite,
|
||||
employee: invite.employee_data
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Get invitation status error:", error);
|
||||
res.status(500).json({ error: "Failed to get invitation status" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
// Export these functions so they can be used to replace the existing ones
|
||||
};
|
||||
@@ -1,227 +0,0 @@
|
||||
-- Initial migration to create all tables for the Auditly application
|
||||
-- Migration: 001_create_initial_schema.sql
|
||||
|
||||
-- Create users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
email_verified BOOLEAN DEFAULT false,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
last_login_at BIGINT,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
|
||||
-- Create organizations table
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
owner_id VARCHAR(255) NOT NULL REFERENCES users(id),
|
||||
industry VARCHAR(255),
|
||||
size VARCHAR(255),
|
||||
description TEXT,
|
||||
mission TEXT,
|
||||
vision TEXT,
|
||||
values TEXT,
|
||||
founding_year VARCHAR(255),
|
||||
evolution TEXT,
|
||||
major_milestones TEXT,
|
||||
advantages TEXT,
|
||||
vulnerabilities TEXT,
|
||||
competitors TEXT,
|
||||
market_position TEXT,
|
||||
current_challenges TEXT,
|
||||
short_term_goals TEXT,
|
||||
long_term_goals TEXT,
|
||||
key_metrics TEXT,
|
||||
culture_description TEXT,
|
||||
work_environment TEXT,
|
||||
leadership_style TEXT,
|
||||
communication_style TEXT,
|
||||
additional_context TEXT,
|
||||
onboarding_completed BOOLEAN DEFAULT false,
|
||||
onboarding_data JSONB,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
-- Subscription fields
|
||||
subscription_status VARCHAR(50) DEFAULT 'trial',
|
||||
stripe_customer_id VARCHAR(255),
|
||||
stripe_subscription_id VARCHAR(255),
|
||||
current_period_start BIGINT,
|
||||
current_period_end BIGINT,
|
||||
trial_end BIGINT,
|
||||
|
||||
-- Usage tracking
|
||||
employee_count INTEGER DEFAULT 0,
|
||||
reports_generated INTEGER DEFAULT 0,
|
||||
last_report_generation BIGINT,
|
||||
|
||||
-- Settings
|
||||
allowed_employee_count INTEGER DEFAULT 50,
|
||||
features_enabled JSONB DEFAULT '{
|
||||
"aiReports": true,
|
||||
"chat": true,
|
||||
"analytics": true
|
||||
}'::jsonb
|
||||
);
|
||||
|
||||
-- Create user_organizations junction table for multi-org support
|
||||
CREATE TABLE IF NOT EXISTS user_organizations (
|
||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
|
||||
organization_id VARCHAR(255) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'employee', -- owner, admin, employee
|
||||
joined_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
onboarding_completed BOOLEAN DEFAULT false,
|
||||
PRIMARY KEY (user_id, organization_id)
|
||||
);
|
||||
|
||||
-- Create employees table
|
||||
CREATE TABLE IF NOT EXISTS employees (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL, -- nullable for invite-only employees
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(255) DEFAULT 'employee',
|
||||
department VARCHAR(255) DEFAULT 'General',
|
||||
status VARCHAR(50) DEFAULT 'invited', -- invited, active, inactive
|
||||
invite_code VARCHAR(255),
|
||||
joined_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
UNIQUE(organization_id, email)
|
||||
);
|
||||
|
||||
-- Create auth_tokens table
|
||||
CREATE TABLE IF NOT EXISTS auth_tokens (
|
||||
token VARCHAR(255) PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
expires_at BIGINT NOT NULL,
|
||||
last_used_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
-- Create otps table for email verification
|
||||
CREATE TABLE IF NOT EXISTS otps (
|
||||
email VARCHAR(255) PRIMARY KEY,
|
||||
otp VARCHAR(6) NOT NULL,
|
||||
expires_at BIGINT NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
invite_code VARCHAR(255),
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
|
||||
-- Create invites table
|
||||
CREATE TABLE IF NOT EXISTS invites (
|
||||
code VARCHAR(255) PRIMARY KEY,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
employee_data JSONB NOT NULL, -- Contains employee info like name, role, department
|
||||
status VARCHAR(50) DEFAULT 'pending', -- pending, consumed, expired
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
expires_at BIGINT NOT NULL,
|
||||
consumed_at BIGINT,
|
||||
consumed_by VARCHAR(255)
|
||||
);
|
||||
|
||||
-- Create submissions table
|
||||
CREATE TABLE IF NOT EXISTS submissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id VARCHAR(255) NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
answers JSONB NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'completed',
|
||||
submission_type VARCHAR(50) DEFAULT 'regular', -- regular, invite
|
||||
invite_code VARCHAR(255),
|
||||
submitted_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
UNIQUE(employee_id) -- One submission per employee
|
||||
);
|
||||
|
||||
-- Create employee_reports table
|
||||
CREATE TABLE IF NOT EXISTS employee_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id VARCHAR(255) NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
employee_name VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
summary TEXT,
|
||||
submission_id INTEGER REFERENCES submissions(id),
|
||||
company_context JSONB,
|
||||
|
||||
-- Report sections
|
||||
role_and_output JSONB,
|
||||
insights JSONB,
|
||||
strengths TEXT[],
|
||||
weaknesses TEXT[],
|
||||
opportunities JSONB[],
|
||||
risks TEXT[],
|
||||
recommendations TEXT[],
|
||||
grading_overview JSONB,
|
||||
|
||||
generated_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
|
||||
UNIQUE(employee_id) -- One report per employee
|
||||
);
|
||||
|
||||
-- Create company_reports table
|
||||
CREATE TABLE IF NOT EXISTS company_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
organization_id VARCHAR(255) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
report_data JSONB NOT NULL, -- Contains the full company report structure
|
||||
executive_summary TEXT,
|
||||
generated_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
created_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000,
|
||||
updated_at BIGINT DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_organizations_owner ON organizations(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_organizations_user ON user_organizations(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_organizations_org ON user_organizations(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_org ON employees(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_user ON employees(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_email ON employees(organization_id, email);
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user ON auth_tokens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_tokens_expires ON auth_tokens(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_invites_org ON invites(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invites_email ON invites(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_invites_status ON invites(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_employee ON submissions(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_org ON submissions(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employee_reports_employee ON employee_reports(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employee_reports_org ON employee_reports(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_company_reports_org ON company_reports(organization_id);
|
||||
|
||||
-- Create trigger function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = EXTRACT(epoch FROM NOW()) * 1000;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Create triggers to automatically update updated_at
|
||||
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_organizations_updated_at BEFORE UPDATE ON organizations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_employees_updated_at BEFORE UPDATE ON employees
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_employee_reports_updated_at BEFORE UPDATE ON employee_reports
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_company_reports_updated_at BEFORE UPDATE ON company_reports
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -1,106 +0,0 @@
|
||||
DROP TABLE IF EXISTS company_reports; -- it's empty
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
DO $a$
|
||||
BEGIN
|
||||
CREATE TYPE department_breakdown_item AS (
|
||||
"department" VARCHAR,
|
||||
"count" int
|
||||
);
|
||||
CREATE TYPE company_report_overview AS (
|
||||
"total_employees" int,
|
||||
"department_breakdown" department_breakdown_item[],
|
||||
"submissionRate" double precision,
|
||||
"last_updated" TIMESTAMP,
|
||||
"average_performance_score" double precision,
|
||||
"risk_level" VARCHAR
|
||||
);
|
||||
CREATE TYPE weaknesses AS (
|
||||
"title" VARCHAR,
|
||||
"description" TEXT
|
||||
);
|
||||
CREATE TYPE new_hire AS (
|
||||
"name" VARCHAR,
|
||||
"department" VARCHAR,
|
||||
"role" VARCHAR,
|
||||
"impact" TEXT
|
||||
);
|
||||
CREATE TYPE promotion AS (
|
||||
"name" VARCHAR,
|
||||
"from_role" VARCHAR,
|
||||
"to_role" VARCHAR,
|
||||
"impact" TEXT
|
||||
);
|
||||
CREATE TYPE departure AS (
|
||||
"name" VARCHAR,
|
||||
"department" VARCHAR,
|
||||
"reason" TEXT,
|
||||
"impact" TEXT
|
||||
|
||||
);
|
||||
CREATE TYPE personnel_changes AS (
|
||||
"new_hires" new_hire[],
|
||||
"promotions" promotion[],
|
||||
"departures" departure[]
|
||||
);
|
||||
CREATE TYPE immediate_hiring_needs AS (
|
||||
"department" VARCHAR,
|
||||
"role" VARCHAR,
|
||||
"priority" VARCHAR,
|
||||
"reasoning" TEXT
|
||||
);
|
||||
CREATE TYPE forward_operating_plan AS (
|
||||
"title" VARCHAR,
|
||||
"details" TEXT
|
||||
);
|
||||
CREATE TYPE organizational_impact_summary_employee AS (
|
||||
"employee_name" VARCHAR,
|
||||
"impact" TEXT,
|
||||
"description" TEXT,
|
||||
"suggested_pay" double precision
|
||||
);
|
||||
CREATE TYPE organizational_impact_summary AS (
|
||||
"category" VARCHAR,
|
||||
"employees" organizational_impact_summary_employee[]
|
||||
);
|
||||
CREATE TYPE team_score AS (
|
||||
"employee_name" VARCHAR,
|
||||
"grade" VARCHAR,
|
||||
"reliability" double precision,
|
||||
"role_fit" double precision,
|
||||
"scalability" double precision,
|
||||
"output" double precision,
|
||||
"initiative" double precision
|
||||
);
|
||||
CREATE TYPE grading_breakdown AS (
|
||||
"department_name_short" VARCHAR,
|
||||
"department_name" VARCHAR,
|
||||
"lead" VARCHAR,
|
||||
"support" VARCHAR,
|
||||
"department_grade" VARCHAR,
|
||||
"executive_summary" TEXT,
|
||||
"team_scores" team_score[]
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN RAISE NOTICE 'Type department_breakdown_item or company_report_overview already exists, skipping creation.';
|
||||
END
|
||||
$a$ LANGUAGE plpgsql;
|
||||
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS company_reports (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"organization_id" VARCHAR NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
"prompt_used" TEXT,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"overview" company_report_overview,
|
||||
"strengths" TEXT[],
|
||||
"weaknesses" weaknesses[],
|
||||
"personnel_changes" personnel_changes,
|
||||
"immediate_hiring_needs" immediate_hiring_needs[],
|
||||
"forward_operating_plan" forward_operating_plan[],
|
||||
"organizational_impact_summary" organizational_impact_summary[],
|
||||
"grading_breakdown" grading_breakdown[]
|
||||
);
|
||||
@@ -1,131 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { executeQuery, executeTransaction } = require('../database');
|
||||
|
||||
/**
|
||||
* Run all database migrations
|
||||
*/
|
||||
async function runMigrations() {
|
||||
console.log('🚀 Starting database migrations...');
|
||||
|
||||
try {
|
||||
// Create migrations table to track what's been run
|
||||
await executeQuery(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL UNIQUE,
|
||||
executed_at BIGINT NOT NULL DEFAULT EXTRACT(epoch FROM NOW()) * 1000
|
||||
);
|
||||
`);
|
||||
|
||||
// Get list of executed migrations
|
||||
const executedMigrations = await executeQuery(
|
||||
'SELECT filename FROM migrations ORDER BY executed_at'
|
||||
);
|
||||
const executedFiles = new Set(executedMigrations.map(m => m.filename));
|
||||
|
||||
// Read migration files
|
||||
const migrationsDir = path.join(__dirname);
|
||||
const migrationFiles = fs.readdirSync(migrationsDir)
|
||||
.filter(file => file.endsWith('.sql'))
|
||||
.sort();
|
||||
|
||||
console.log(`📁 Found ${migrationFiles.length} migration files`);
|
||||
|
||||
for (const file of migrationFiles) {
|
||||
if (executedFiles.has(file)) {
|
||||
console.log(`⏭️ Skipping ${file} (already executed)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🔄 Executing ${file}...`);
|
||||
|
||||
const filePath = path.join(migrationsDir, file);
|
||||
const sql = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
await executeTransaction(async (client) => {
|
||||
// Execute the migration SQL
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
// For Neon serverless, split by statements and execute individually
|
||||
const statements = sql
|
||||
.split(';')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.trim()) {
|
||||
await client(statement);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await client.query(sql);
|
||||
}
|
||||
|
||||
// Record that this migration was executed
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
await client('INSERT INTO migrations (filename) VALUES ($1)', [file]);
|
||||
} else {
|
||||
await client.query('INSERT INTO migrations (filename) VALUES ($1)', [file]);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Completed ${file}`);
|
||||
}
|
||||
|
||||
console.log('🎉 All migrations completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new migration file
|
||||
* @param {string} name - Migration name
|
||||
*/
|
||||
function createMigration(name) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
||||
const filename = `${timestamp}_${name}.sql`;
|
||||
const filepath = path.join(__dirname, filename);
|
||||
|
||||
const template = `-- Migration: ${filename}
|
||||
-- Description: ${name}
|
||||
|
||||
-- Add your SQL here
|
||||
|
||||
`;
|
||||
|
||||
fs.writeFileSync(filepath, template);
|
||||
console.log(`📝 Created migration: ${filename}`);
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Run migrations if this file is executed directly
|
||||
if (require.main === module) {
|
||||
const command = process.argv[2];
|
||||
|
||||
if (command === 'create') {
|
||||
const name = process.argv[3];
|
||||
if (!name) {
|
||||
console.error('❌ Please provide a migration name: npm run db:migrate create migration_name');
|
||||
process.exit(1);
|
||||
}
|
||||
createMigration(name);
|
||||
} else {
|
||||
runMigrations()
|
||||
.then(() => {
|
||||
console.log('🏁 Migration process completed');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('💥 Migration process failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runMigrations,
|
||||
createMigration
|
||||
};
|
||||
13458
functions/package-lock.json
generated
13458
functions/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/vertexai": "^1.10.0",
|
||||
"@neondatabase/serverless": "^0.9.5",
|
||||
"pg": "^8.12.0",
|
||||
"stripe": "^18.5.0",
|
||||
"firebase-functions": "^6.4.0"
|
||||
"@neondatabase/serverless": "0.9.5",
|
||||
"firebase-admin": "^13.5.0",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"nodemailer": "6.10.1",
|
||||
"pg": "^8.16.3",
|
||||
"stripe": "^18.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.6",
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Theme, NavItem } from '../types';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import FigmaSidebar from './figma/Sidebar';
|
||||
import FigmaPrimaryButton from './figma/FigmaButton';
|
||||
import { FigmaIcons } from './figma/figmaIcon';
|
||||
|
||||
// ========== ICONS ==========
|
||||
|
||||
@@ -151,11 +153,11 @@ const Sidebar = () => {
|
||||
<Button size="sm" className="w-full" onClick={() => setShowInviteModal(true)}>
|
||||
<PlusIcon className="w-4 h-4 mr-1" /> Invite
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" className="w-full" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)}>
|
||||
<FigmaPrimaryButton iconLeft={<FigmaIcons.LinkIcon size="8" />} size="tiny" text="Copy" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)} />
|
||||
{/* <Button size="sm" variant="secondary" className="w-full" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)}>
|
||||
<CopyIcon className="w-4 h-4 mr-1" /> Copy
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" className="w-full mt-2" onClick={signOutUser}>Sign out</Button>
|
||||
</div>
|
||||
|
||||
{/* Invite Modal */}
|
||||
@@ -280,7 +282,7 @@ export const Layout = () => {
|
||||
<div className="w-[100vw] h-[100vh] p-4 bg-[--Neutrals-NeutralSlate200] inline-flex justify-start items-start overflow-y-hidden">
|
||||
<div className="flex-1 h-full rounded-3xl self-stretch shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-y-auto relative">
|
||||
<FigmaSidebar companyName={org?.companyName || "Orbitly"} />
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<main className="flex-1 h-full overflow-y-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -32,12 +32,6 @@ export const FigmaPrimaryButton: React.FC<FigmaButtonProps> = ({
|
||||
big: 'px-4 py-3'
|
||||
};
|
||||
|
||||
const iconSize = {
|
||||
tiny: 16,
|
||||
small: 16,
|
||||
big: 20
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`p-[0.1rem] ${grow ? 'flex-1 flex-grow' : ''} self-stretch justify-center flex rounded-[999px] gap-1 ${!!containerExtra && containerExtra}`} style={{ scale: '95%', background: 'var(--button-border-primary' }}>
|
||||
<button
|
||||
@@ -45,11 +39,11 @@ export const FigmaPrimaryButton: React.FC<FigmaButtonProps> = ({
|
||||
onClick={onClick}
|
||||
className={`text-white hover:bg-[--Brand-Orange]/90 bg-[--Brand-Orange] flex-grow ${sizeClasses[size]} rounded-[999px] inline-flex items-center justify-center transition-colors ${disabled ? 'disabled:bg-[--Neutrals-NeutralSlate100] disabled:cursor-not-allowed' : ''} ${!!buttonExtra && buttonExtra}`}
|
||||
>
|
||||
{iconLeft && <div className="relative items-center justify-center flex pr-1">{React.cloneElement(iconLeft, { 'size': iconSize[size] })}</div>}
|
||||
{iconLeft && <div className="relative items-center justify-center flex pr-1">{iconLeft}</div>}
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-sm font-medium font-['Inter'] leading-tight">{text}</div>
|
||||
</div>
|
||||
{iconRight && <div className="relative items-center justify-center flex pl-1">{React.cloneElement(iconRight, { 'size': iconSize[size] })}</div>}
|
||||
{iconRight && <div className="relative items-center justify-center flex pl-1">{iconRight}</div>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -73,24 +67,18 @@ export const FigmaSecondaryButton: React.FC<FigmaButtonProps> = ({
|
||||
big: 'px-4 py-3.5'
|
||||
};
|
||||
|
||||
const iconSize = {
|
||||
tiny: 16,
|
||||
small: 16,
|
||||
big: 20
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`p-[0.1rem] ${grow ? 'flex-1 flex-grow' : ''} self-stretch justify-center flex rounded-[999px] gap-1`} style={{ scale: '95%', background: 'transparent' }}>
|
||||
<div className={`p-[0.1rem] ${grow ? 'flex-1 flex-grow' : ''} self-stretch justify-center flex rounded-[999px] gap-1 bg-[--Neutrals-NeutralSlate100]`} style={{ scale: '95%' }}>
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={`text-[--Neutrals-NeutralSlate950] flex-grow ${sizeClasses[size]} bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex items-center justify-center hover:bg-[--Neutrals-NeutralSlate100] transition-colors ${disabled ? 'disabled:bg-[--Neutrals-NeutralSlate100] disabled:cursor-not-allowed' : ''} ${!!buttonExtra && buttonExtra}`}
|
||||
>
|
||||
{iconLeft && <div className="relative items-center justify-center flex pr-1">{React.cloneElement(iconLeft, { 'size': iconSize[size] })}</div>}
|
||||
{iconLeft && <div className="relative items-center justify-center flex pr-1">{iconLeft}</div>}
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-sm font-medium font-['Inter'] leading-tight">{text}</div>
|
||||
</div>
|
||||
{iconRight && <div className="relative items-center justify-center flex pl-1">{React.cloneElement(iconRight, { 'size': iconSize[size] })}</div>}
|
||||
{iconRight && <div className="relative items-center justify-center flex pl-1">{iconRight}</div>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import FigmaPrimaryButton from './FigmaButton';
|
||||
|
||||
// Icon SVG Component - From Figma designs
|
||||
export const OrbitlyIcon: React.FC = () => (
|
||||
@@ -113,14 +114,15 @@ export const WelcomeScreen: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<FigmaPrimaryButton size="big" onClick={onStart} text="Start" buttonExtra="self-stretch" />
|
||||
{/* <button
|
||||
onClick={onStart}
|
||||
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||
</div>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden h-fit">
|
||||
@@ -165,14 +167,15 @@ export const SectionIntro: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<FigmaPrimaryButton size="big" onClick={onStart} text="Start" buttonExtra="self-stretch" />
|
||||
{/* <button
|
||||
onClick={onStart}
|
||||
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||
</div>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-max px-20 py-16 flex justify-center items-center gap-2.5 flex-shrink">
|
||||
@@ -258,7 +261,8 @@ export const PersonalInfoForm: React.FC<{
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<FigmaPrimaryButton size="big" disabled={!isValid} onClick={onNext} text="Next" />
|
||||
{/* <button
|
||||
onClick={onNext}
|
||||
disabled={!isValid}
|
||||
className="self-stretch h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
|
||||
@@ -266,7 +270,7 @@ export const PersonalInfoForm: React.FC<{
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||
</div>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -317,14 +321,15 @@ export const TextAreaQuestion: React.FC<{
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
<FigmaPrimaryButton size="big" onClick={onNext} text="Next" buttonExtra="flex-1" />
|
||||
{/* <button
|
||||
onClick={onNext}
|
||||
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||
</div>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -392,10 +397,10 @@ export const RatingScaleQuestion: React.FC<{
|
||||
<button
|
||||
key={ratingValue}
|
||||
onClick={() => onChange(ratingValue)}
|
||||
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden transition-colors ${isSelected ? 'bg-[--Neutrals-NeutralSlate50]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate50]'
|
||||
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden transition-colors ${isSelected ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate50]'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-[--Text-Gray-950] bg-[--Neutrals-NeutralSlate50]' : 'text-[--Text-Gray-950] hover:text-[--Text-Gray-950]'
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-[--Text-White-00] bg-[--Neutrals-NeutralSlate800]' : 'text-[--Text-Gray-950] hover:text-[--Text-Gray-950]'
|
||||
}`}>
|
||||
{ratingValue}
|
||||
</div>
|
||||
@@ -418,7 +423,8 @@ export const RatingScaleQuestion: React.FC<{
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
<FigmaPrimaryButton size="big" disabled={!value} onClick={onNext} text="Next" />
|
||||
{/* <button
|
||||
onClick={onNext}
|
||||
disabled={!value}
|
||||
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
|
||||
@@ -426,7 +432,7 @@ export const RatingScaleQuestion: React.FC<{
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||
</div>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -473,6 +479,7 @@ export const YesNoChoice: React.FC<{
|
||||
totalSteps?: number;
|
||||
sectionName?: string;
|
||||
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
|
||||
let SelectedValue = value;
|
||||
return (
|
||||
<div className="w-full h-full py-6 relative bg-[--Neutrals-NeutralSlate0] grid grid-cols-12 grid-rows-5 justify-center items-center gap-3">
|
||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col row-start-3 col-span-12 self-center justify-self-center justify-center gap-12">
|
||||
@@ -482,21 +489,21 @@ export const YesNoChoice: React.FC<{
|
||||
</div>
|
||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||
<button
|
||||
onClick={() => onChange('No')}
|
||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'No' ? 'bg-[--Neutrals-NeutralSlate50]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate50]'
|
||||
onClick={() => { onChange('No'); SelectedValue = 'No'; }}
|
||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${SelectedValue === 'No' ? 'bg-[--Neutrals-NeutralSlate100]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate100]'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'No' ? 'text-[--Text-Gray-950] bg-[--Neutrals-NeutralSlate50]' : 'text-[--Text-Gray-950] hover:bg-[--Neutrals-NeutralSlate50] hover:text-[--Text-Gray-950]'
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${SelectedValue === 'No' ? 'text-[--Text-Gray-950] bg-[--Neutrals-NeutralSlate100]' : 'text-[--Text-Gray-100] hover:bg-[--Neutrals-NeutralSlate800] hover:text-[--Text-Gray-100]'
|
||||
}`}>
|
||||
No
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChange('Yes')}
|
||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'Yes' ? 'bg-[--Neutrals-NeutralSlate50]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate50]'
|
||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${SelectedValue === 'Yes' ? 'bg-[--Neutrals-NeutralSlate100]' : 'bg-[--Neutrals-NeutralSlate800] hover:bg-[--Neutrals-NeutralSlate100]'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${value === 'Yes' ? 'text-[--Text-Gray-950] bg-[--Neutrals-NeutralSlate50]' : 'text-[--Text-Gray-950] hover:bg-[--Neutrals-NeutralSlate50] hover:text-[--Text-Gray-950]'
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-base font-normal font-['Inter'] leading-normal ${SelectedValue === 'Yes' ? 'text-[--Text-Gray-950] bg-[--Neutrals-NeutralSlate100]' : 'text-[--Text-Gray-0] hover:bg-[--Neutrals-NeutralSlate100] hover:text-[--Text-Gray-800]'
|
||||
}`}>
|
||||
Yes
|
||||
</div>
|
||||
@@ -514,7 +521,8 @@ export const YesNoChoice: React.FC<{
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
<FigmaPrimaryButton size="big" disabled={!value} onClick={onNext} text="Next" />
|
||||
{/* <button
|
||||
onClick={onNext}
|
||||
disabled={!value}
|
||||
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
|
||||
@@ -522,7 +530,7 @@ export const YesNoChoice: React.FC<{
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||
</div>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
300
src/components/figma/FigmaInviteEmployeesModal.tsx
Normal file
300
src/components/figma/FigmaInviteEmployeesModal.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { FigmaPrimaryButton } from './FigmaButton';
|
||||
|
||||
interface Employee {
|
||||
email: string;
|
||||
initials: string;
|
||||
}
|
||||
|
||||
interface FigmaInviteEmployeesModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onInvite: (emails: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export const FigmaInviteEmployeesModal: React.FC<FigmaInviteEmployeesModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onInvite,
|
||||
}) => {
|
||||
console.log('FigmaInviteEmployeesModal rendered with isOpen:', isOpen);
|
||||
const [currentEmail, setCurrentEmail] = useState('');
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Generate initials from email
|
||||
const generateInitials = (email: string): string => {
|
||||
const parts = email.split('@')[0].split('.');
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
}
|
||||
return email.substring(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
// Validate email format
|
||||
const isValidEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
// Add employee to the list
|
||||
const addEmployee = (email: string) => {
|
||||
const trimmedEmail = email.trim().toLowerCase();
|
||||
if (isValidEmail(trimmedEmail) && !employees.find(emp => emp.email === trimmedEmail)) {
|
||||
const newEmployee: Employee = {
|
||||
email: trimmedEmail,
|
||||
initials: generateInitials(trimmedEmail),
|
||||
};
|
||||
setEmployees(prev => [...prev, newEmployee]);
|
||||
}
|
||||
setCurrentEmail('');
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
// Remove employee from the list
|
||||
const removeEmployee = (email: string) => {
|
||||
setEmployees(prev => prev.filter(emp => emp.email !== email));
|
||||
};
|
||||
|
||||
// Handle key press in input
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && currentEmail) {
|
||||
e.preventDefault();
|
||||
addEmployee(currentEmail);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle CSV file upload
|
||||
const handleCsvUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
const lines = content.split('\n');
|
||||
const emails: string[] = [];
|
||||
|
||||
lines.forEach(line => {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine && isValidEmail(trimmedLine)) {
|
||||
emails.push(trimmedLine.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
// Add all valid emails from CSV
|
||||
emails.forEach(email => {
|
||||
if (!employees.find(emp => emp.email === email)) {
|
||||
const newEmployee: Employee = {
|
||||
email,
|
||||
initials: generateInitials(email),
|
||||
};
|
||||
setEmployees(prev => [...prev, newEmployee]);
|
||||
}
|
||||
});
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Handle invite action
|
||||
const handleInvite = async () => {
|
||||
if (employees.length === 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onInvite(employees.map(emp => emp.email));
|
||||
setEmployees([]);
|
||||
setCurrentEmail('');
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to send invites:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-zinc-800/10 backdrop-blur-[2px] z-50" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 flex items-center justify-center z-50 p-4">
|
||||
<div className="w-[496px] p-8 bg-[--Neutrals-NeutralSlate0] rounded-[20px] shadow-[0px_48px_48px_-24px_rgba(51,51,51,0.04)] shadow-[0px_24px_24px_-12px_rgba(51,51,51,0.04)] shadow-[0px_12px_12px_-6px_rgba(51,51,51,0.04)] shadow-[0px_6px_6px_-3px_rgba(51,51,51,0.04)] shadow-[0px_3px_3px_-1.5px_rgba(51,51,51,0.02)] shadow-[0px_1px_1px_0.5px_rgba(51,51,51,0.04)] shadow-[0px_0px_0px_1px_rgba(51,51,51,0.04)] shadow-[inset_0px_-1px_1px_-0.5px_rgba(51,51,51,0.06)] flex flex-col gap-5 overflow-hidden">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
Invite employees
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-7 h-7 bg-[--Neutrals-NeutralSlate100] rounded-[100px] flex items-center justify-center overflow-hidden hover:bg-[--Neutrals-NeutralSlate200] transition-colors"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.75 5.25L5.25 12.75M5.25 5.25L12.75 12.75"
|
||||
stroke="var(--Neutrals-NeutralSlate900)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal">
|
||||
Share this form with your team members to capture valuable info about your company to train Auditly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Email Input Section */}
|
||||
<div className="flex gap-2 relative">
|
||||
{/* CSV Upload Button (Hidden File Input) */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".csv,.txt"
|
||||
onChange={handleCsvUpload}
|
||||
className="hidden"
|
||||
id="csv-upload"
|
||||
/>
|
||||
<label
|
||||
htmlFor="csv-upload"
|
||||
className="px-2.5 py-1.5 bg-[--Neutrals-NeutralSlate100] h-9.5 rounded-[999px] border border-[--Neutrals-NeutralSlate200] flex items-center justify-center hover:bg-[--Neutrals-NeutralSlate200] transition-colors cursor-pointer"
|
||||
title="Upload CSV file with email addresses"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 10.6667V2M8 2L5.33333 4.66667M8 2L10.6667 4.66667M2.66667 10.6667V12C2.66667 12.7364 3.26362 13.3333 4 13.3333H12C12.7364 13.3333 13.3333 12.7364 13.3333 12V10.6667"
|
||||
stroke="var(--Neutrals-NeutralSlate600)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
{/* Email Input Field */}
|
||||
<div className="relative h-9">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
value={currentEmail}
|
||||
onChange={(e) => {
|
||||
setCurrentEmail(e.target.value);
|
||||
setShowSuggestions(e.target.value.length > 0);
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||
onFocus={() => setShowSuggestions(currentEmail.length > 0)}
|
||||
className={`w-full px-4 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] text-sm font-normal font-['Inter'] leading-tight text-[--Neutrals-NeutralSlate950] placeholder-[--Neutrals-NeutralSlate500] focus:outline-none ${currentEmail && !isValidEmail(currentEmail)
|
||||
? 'outline outline-1 outline-offset-[-1px] outline-red-400'
|
||||
: 'outline outline-1 outline-offset-[-1px] outline-[--Brand-Orange] focus:outline-[--Brand-Orange]'
|
||||
}`}
|
||||
placeholder="Enter email address"
|
||||
/>
|
||||
|
||||
{/* Email Suggestion Dropdown */}
|
||||
{showSuggestions && currentEmail && isValidEmail(currentEmail) && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 bg-[--Neutrals-NeutralSlate0] rounded-3xl shadow-[0px_4px_8px_1px_rgba(10,13,18,0.12)] z-10">
|
||||
<div className="px-4 pt-14 pb-4 flex flex-col gap-2">
|
||||
<div className="text-[--Neutrals-NeutralSlate500] text-xs font-medium font-['Inter'] leading-none">
|
||||
Select email
|
||||
</div>
|
||||
<button
|
||||
onClick={() => addEmployee(currentEmail)}
|
||||
className="text-left text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal hover:bg-[--Neutrals-NeutralSlate50] px-2 py-1 rounded"
|
||||
>
|
||||
{currentEmail}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add/Invite Button */}
|
||||
<FigmaPrimaryButton
|
||||
text={currentEmail && isValidEmail(currentEmail) ? "Add" : (employees.length > 0 ? "Invite" : "Add")}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (currentEmail && isValidEmail(currentEmail)) {
|
||||
addEmployee(currentEmail);
|
||||
} else if (employees.length > 0) {
|
||||
handleInvite();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading || (!currentEmail && employees.length === 0)}
|
||||
buttonExtra="px-7 py-1.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Employee Tags */}
|
||||
{employees.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{employees.map((employee) => (
|
||||
<div
|
||||
key={employee.email}
|
||||
className="pl-1.5 pr-2 py-1 bg-[--Neutrals-NeutralSlate100] rounded-[100px] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate100] flex items-center gap-1.5"
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className="w-6 h-6 bg-[--Neutrals-NeutralSlate0] rounded-[125px] relative">
|
||||
<div className="w-6 h-6 absolute bg-black/10 rounded-[125px]" />
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[--Neutrals-NeutralSlate400] text-[10px] font-semibold font-['Inter'] leading-none">
|
||||
{employee.initials}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<span className="text-[--Text-Gray-900] text-xs font-normal font-['Inter'] leading-none">
|
||||
{employee.email}
|
||||
</span>
|
||||
|
||||
{/* Remove Button */}
|
||||
<button
|
||||
onClick={() => removeEmployee(employee.email)}
|
||||
className="hover:bg-[--Neutrals-NeutralSlate200] rounded p-0.5 transition-colors"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9 3L3 9M3 3L9 9"
|
||||
stroke="var(--Neutrals-NeutralSlate500)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Help Text */}
|
||||
{employees.length > 0 && (
|
||||
<div className="text-[--Neutrals-NeutralSlate500] text-sm font-normal font-['Inter'] leading-tight">
|
||||
{employees.length} employee{employees.length !== 1 ? 's' : ''} selected. {currentEmail && isValidEmail(currentEmail) ? 'Add email above or click "Invite" to send invitations.' : 'Click "Invite" to send invitations.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FigmaInviteEmployeesModal;
|
||||
@@ -4,6 +4,9 @@ import { Button, PlusIcon, CopyIcon } from '../UiKit';
|
||||
import { useOrg } from '../../contexts/OrgContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useUserOrganizations } from '../../contexts/UserOrganizationsContext';
|
||||
import { FigmaPrimaryButton, FigmaSecondaryButton } from './FigmaButton';
|
||||
import { FigmaIcons } from './figmaIcon';
|
||||
import { FigmaInviteEmployeesModal } from './FigmaInviteEmployeesModal';
|
||||
|
||||
interface SidebarProps {
|
||||
companyName?: string;
|
||||
@@ -19,7 +22,6 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||
const [showOrgDropdown, setShowOrgDropdown] = useState(false);
|
||||
const [showCreateOrgModal, setShowCreateOrgModal] = useState(false);
|
||||
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
||||
const [createOrgForm, setCreateOrgForm] = useState({ name: '', description: '' });
|
||||
const [inviteLink, setInviteLink] = useState('');
|
||||
const [emailLink, setEmailLink] = useState('');
|
||||
@@ -55,21 +57,33 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
||||
setShowOrgDropdown(false);
|
||||
};
|
||||
|
||||
const handleInvite = async () => {
|
||||
const handleInvite = async (emails: string[]) => {
|
||||
try {
|
||||
const result = await issueInviteViaApi({
|
||||
name: inviteForm.name,
|
||||
email: inviteForm.email,
|
||||
role: inviteForm.role,
|
||||
department: inviteForm.department
|
||||
});
|
||||
setInviteLink(result.inviteLink);
|
||||
// if (process.env.SENDGRID_API_KEY) {
|
||||
// setEmailLink(result.emailLink);
|
||||
// }
|
||||
setInviteForm({ name: '', email: '', role: '', department: '' });
|
||||
// Send invitations for multiple emails
|
||||
const results = await Promise.allSettled(
|
||||
emails.map(email => issueInviteViaApi({
|
||||
name: '', // Name can be extracted from email or left empty
|
||||
email: email,
|
||||
role: '',
|
||||
department: ''
|
||||
}))
|
||||
);
|
||||
|
||||
// Check for any successful invites to generate links
|
||||
const successfulResults = results.filter(result => result.status === 'fulfilled') as PromiseFulfilledResult<any>[];
|
||||
if (successfulResults.length > 0) {
|
||||
// Use the first successful result for link generation
|
||||
const result = successfulResults[0].value;
|
||||
setInviteLink(result.inviteLink);
|
||||
// if (process.env.SENDGRID_API_KEY) {
|
||||
// setEmailLink(result.emailLink);
|
||||
// }
|
||||
}
|
||||
|
||||
console.log(`Successfully sent ${successfulResults.length} invitations out of ${emails.length}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to invite employee:', error);
|
||||
console.error('Failed to invite employees:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -387,116 +401,16 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Invite Employee Modal */}
|
||||
{showInviteModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-[--background-secondary] p-6 rounded-lg max-w-md w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.name}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Employee name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={inviteForm.email}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="employee@company.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">Role</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.role}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="e.g. Software Engineer"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">Department</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteForm.department}
|
||||
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="e.g. Engineering"
|
||||
/>
|
||||
</div>
|
||||
{(inviteLink || emailLink) && (
|
||||
<div className="space-y-3">
|
||||
{emailLink && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
Email Link (for GET requests)
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={emailLink}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
|
||||
/>
|
||||
<Button size="sm" onClick={() => copyToClipboard(emailLink)}>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{inviteLink && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
||||
App Link (direct)
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inviteLink}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
|
||||
/>
|
||||
<Button size="sm" onClick={() => copyToClipboard(inviteLink)}>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2 mt-6">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setShowInviteModal(false);
|
||||
setInviteLink('');
|
||||
setInviteForm({ name: '', email: '', role: '', department: '' });
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={handleInvite}
|
||||
disabled={!inviteForm.name || !inviteForm.email}
|
||||
>
|
||||
Generate Invite
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Figma Invite Employee Modal */}
|
||||
<FigmaInviteEmployeesModal
|
||||
isOpen={showInviteModal}
|
||||
onClose={() => {
|
||||
setShowInviteModal(false);
|
||||
setInviteLink('');
|
||||
setEmailLink('');
|
||||
}}
|
||||
onInvite={handleInvite}
|
||||
/>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||
@@ -546,13 +460,18 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
||||
</div>
|
||||
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||
<Button variant="secondary" size="sm" className="w-full rounded-[999px]" onClick={() => setShowInviteModal(true)}>
|
||||
{/* <Button variant="secondary" size="sm" className="w-full rounded-[999px]" onClick={() => setShowInviteModal(true)}>
|
||||
<PlusIcon className="w-4 h-4 mr-1" /> Invite
|
||||
</Button>
|
||||
|
||||
</Button> */}
|
||||
<FigmaSecondaryButton iconLeft={<FigmaIcons.PlusIcon size="16" />} buttonExtra="w-full" size="tiny" text="Invite" onClick={() => {
|
||||
console.log('Figma Sidebar invite button clicked');
|
||||
setShowInviteModal(true);
|
||||
}} />
|
||||
<FigmaPrimaryButton iconLeft={<FigmaIcons.LinkIcon size="16" />} buttonExtra="w-full" size="tiny" text="Copy" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)} />
|
||||
{/*
|
||||
<Button size="sm" variant="primary" className="w-full rounded-[999px]" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)}>
|
||||
<CopyIcon className="mr-2" /> Copy
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
export interface FigmaIconProps {
|
||||
className?: string;
|
||||
size?: number;
|
||||
size?: number | string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const PlaceholderIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="placeholder"
|
||||
@@ -42,7 +42,7 @@ export const ShareIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox={`0 0 24 24`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="share"
|
||||
@@ -68,7 +68,7 @@ export const SearchIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="search"
|
||||
@@ -94,7 +94,7 @@ export const EyeIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="eye"
|
||||
@@ -155,7 +155,7 @@ export const ChevronLeftIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="chevron-left"
|
||||
@@ -181,7 +181,7 @@ export const ChevronRightIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="chevron-right"
|
||||
@@ -207,7 +207,7 @@ export const ChevronUpIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="chevron-up"
|
||||
@@ -233,7 +233,7 @@ export const ChevronDownIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="chevron-down"
|
||||
@@ -259,7 +259,7 @@ export const CheckCircleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="check-circle"
|
||||
@@ -285,7 +285,7 @@ export const LineChartCircleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="line-chart-circle"
|
||||
@@ -312,7 +312,7 @@ export const FolderIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="folder"
|
||||
@@ -338,7 +338,7 @@ export const CloudIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="cloud"
|
||||
@@ -364,7 +364,7 @@ export const DatabaseIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="database"
|
||||
@@ -390,7 +390,7 @@ export const UsersIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="users"
|
||||
@@ -416,7 +416,7 @@ export const SettingsIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="settings"
|
||||
@@ -442,7 +442,7 @@ export const BellIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="bell"
|
||||
@@ -468,7 +468,7 @@ export const StreamIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="stream"
|
||||
@@ -494,7 +494,7 @@ export const TrashIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="trash"
|
||||
@@ -520,7 +520,7 @@ export const PlusIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="plus"
|
||||
@@ -546,7 +546,7 @@ export const FilterIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="filter"
|
||||
@@ -572,7 +572,7 @@ export const EditIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="edit"
|
||||
@@ -598,7 +598,7 @@ export const XIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="x"
|
||||
@@ -624,7 +624,7 @@ export const LoadingIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="loading"
|
||||
@@ -650,7 +650,7 @@ export const InfoCircleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="info-circle"
|
||||
@@ -676,7 +676,7 @@ export const DownloadIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="download"
|
||||
@@ -702,7 +702,7 @@ export const RefreshIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="refresh"
|
||||
@@ -728,7 +728,7 @@ export const MailIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="mail"
|
||||
@@ -754,7 +754,7 @@ export const KeyIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="key"
|
||||
@@ -780,7 +780,7 @@ export const AIIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="ai"
|
||||
@@ -806,7 +806,7 @@ export const DashboardIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="dashboard"
|
||||
@@ -832,7 +832,7 @@ export const ThreeDotsVerticalIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="three-dots-vertical"
|
||||
@@ -872,7 +872,7 @@ export const MoveFolderIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="move-folder"
|
||||
@@ -898,7 +898,7 @@ export const UploadFolderIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="upload-folder"
|
||||
@@ -931,7 +931,7 @@ export const LinkIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="link"
|
||||
@@ -957,7 +957,7 @@ export const ChatIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="chat"
|
||||
@@ -983,7 +983,7 @@ export const CheckmarkIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="checkmark"
|
||||
@@ -1009,7 +1009,7 @@ export const StarIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="star"
|
||||
@@ -1035,7 +1035,7 @@ export const CopyIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="copy"
|
||||
@@ -1061,7 +1061,7 @@ export const GoogleGIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="google-g"
|
||||
@@ -1086,7 +1086,7 @@ export const GoogleLogoIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="google-logo"
|
||||
@@ -1128,7 +1128,7 @@ export const BackArrowIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="back-arrow"
|
||||
@@ -1154,7 +1154,7 @@ export const DownArrowIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="down-arrow"
|
||||
@@ -1180,7 +1180,7 @@ export const ForwardArrowIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="forward-arrow"
|
||||
@@ -1206,7 +1206,7 @@ export const UpArrowIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="up-arrow"
|
||||
@@ -1232,7 +1232,7 @@ export const HelpIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="help"
|
||||
@@ -1258,7 +1258,7 @@ export const HomeIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="home"
|
||||
@@ -1284,7 +1284,7 @@ export const PieChartIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="pie-chart"
|
||||
@@ -1310,7 +1310,7 @@ export const SupportIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="support"
|
||||
@@ -1336,7 +1336,7 @@ export const ChevronSelectorReverseIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="chevron-selector-reverse"
|
||||
@@ -1362,7 +1362,7 @@ export const UploadCloudIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="upload-cloud"
|
||||
@@ -1388,7 +1388,7 @@ export const PhoneIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="phone"
|
||||
@@ -1421,7 +1421,7 @@ export const EmailIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="email"
|
||||
@@ -1447,7 +1447,7 @@ export const ImageIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="image"
|
||||
@@ -1473,7 +1473,7 @@ export const ArrowDiagonalUpRightIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="arrow-diagonal-up-right"
|
||||
@@ -1499,7 +1499,7 @@ export const DocumentIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="document"
|
||||
@@ -1525,7 +1525,7 @@ export const WebIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="web"
|
||||
@@ -1551,7 +1551,7 @@ export const GreenCheckCircleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="green-check-circle"
|
||||
@@ -1577,7 +1577,7 @@ export const GreenStatusCircleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="green-status-circle"
|
||||
@@ -1609,7 +1609,7 @@ export const RedXCircleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="red-x-circle"
|
||||
@@ -1640,7 +1640,7 @@ export const YellowWarningCircleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="yellow-warning-circle"
|
||||
@@ -1677,7 +1677,7 @@ export const BookIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="book"
|
||||
@@ -1703,7 +1703,7 @@ export const AdvancedSearchIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="advanced-search"
|
||||
@@ -1729,7 +1729,7 @@ export const WarningTriangleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="warning-triangle"
|
||||
@@ -1755,7 +1755,7 @@ export const LineChartXYPlaneIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="line-chart-xy-plane"
|
||||
@@ -1781,7 +1781,7 @@ export const FlagIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="flag"
|
||||
@@ -1807,7 +1807,7 @@ export const GenericFlagIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="generic-flag"
|
||||
@@ -1833,7 +1833,7 @@ export const UploadIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="upload"
|
||||
@@ -1859,7 +1859,7 @@ export const ChevronSelectorVerticalIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="chevron-selector-vertical"
|
||||
@@ -1885,7 +1885,7 @@ export const SettingsGearIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="settings-gear"
|
||||
@@ -1918,7 +1918,7 @@ export const ComplexWarningTriangleIcon: React.FC<FigmaIconProps> = ({
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="complex-warning-triangle"
|
||||
|
||||
@@ -164,6 +164,7 @@ export const FigmaOnboardingQuestion: React.FC<FigmaOnboardingQuestionProps> = (
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 bg-transparent text-[--Text-Gray-950] text-base font-normal font-['Inter'] leading-normal placeholder:text-[--Text-Gray-950] outline-none resize-none"
|
||||
placeholder={placeholder}
|
||||
autoFocus={true}
|
||||
rows={rows}
|
||||
/>
|
||||
<div className="w-3 h-3 absolute right-3 bottom-3">
|
||||
|
||||
@@ -26,7 +26,7 @@ interface OrgContextType {
|
||||
user?: User;
|
||||
orgId: string;
|
||||
employees: Employee[];
|
||||
submissions: Submission[];
|
||||
submissions: Record<string, Submission>;
|
||||
reports: Record<string, EmployeeReport>;
|
||||
loading: boolean;
|
||||
upsertOrg: (data: Partial<OrgData>) => Promise<void>;
|
||||
@@ -58,7 +58,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
const { user } = useAuth();
|
||||
const [org, setOrg] = useState<OrgData | null>(null);
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [submissions, setSubmissions] = useState<Submission[]>([]);
|
||||
const [submissions, setSubmissions] = useState<Record<string, Submission>>({});
|
||||
const [reports, setReports] = useState<Record<string, EmployeeReport>>({});
|
||||
const [reportVersions, setReportVersions] = useState<Record<string, Array<{ id: string; createdAt: number; report: EmployeeReport }>>>({});
|
||||
const [companyReports, setCompanyReports] = useState<Array<{ id: string; createdAt: number; summary: string }>>([]);
|
||||
@@ -113,10 +113,17 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
|
||||
// Process submissions data
|
||||
if (submissionsData.status === 'fulfilled') {
|
||||
setSubmissions(submissionsData.value);
|
||||
// Handle both direct submissions and wrapped response
|
||||
const submissionsValue = submissionsData.value;
|
||||
if (submissionsValue && typeof submissionsValue === 'object' && !Array.isArray(submissionsValue)) {
|
||||
// If it has a submissions property, use that; otherwise use the whole object
|
||||
setSubmissions((submissionsValue as any).submissions || submissionsValue as Record<string, Submission>);
|
||||
} else {
|
||||
setSubmissions({});
|
||||
}
|
||||
} else {
|
||||
console.warn('Could not load submissions');
|
||||
setSubmissions([]);
|
||||
setSubmissions({});
|
||||
}
|
||||
|
||||
// Process reports data
|
||||
@@ -291,7 +298,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
||||
const seedInitialData = async () => {
|
||||
// Start with clean slate - let users invite their own employees and generate real data
|
||||
setEmployees([]);
|
||||
setSubmissions([]);
|
||||
setSubmissions({});
|
||||
setReports({});
|
||||
setFullCompanyReports([]);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { apiPost } from '../services/api';
|
||||
@@ -40,6 +40,7 @@ const Chat: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const { employees, orgId, org } = useOrg();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -159,6 +160,65 @@ const Chat: React.FC = () => {
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
// Handle initial context from Reports page navigation
|
||||
useEffect(() => {
|
||||
const handleInitialContext = async () => {
|
||||
const navigationState = location.state as {
|
||||
initialContext?: string;
|
||||
companyContext?: string;
|
||||
contextType?: 'company' | 'employee';
|
||||
employeeName?: string;
|
||||
companyName?: string;
|
||||
} | null;
|
||||
|
||||
if (navigationState?.initialContext && navigationState.contextType) {
|
||||
let contextMessage = '';
|
||||
let aiResponse = '';
|
||||
|
||||
if (navigationState.contextType === 'company') {
|
||||
contextMessage = `I'm sharing our company report data with you for analysis:\n\n${navigationState.initialContext}`;
|
||||
aiResponse = `Thank you for sharing your company report! I now have context about ${navigationState.companyName || 'your company'} and can help you analyze the data, discuss insights, and answer questions about your organization's performance, strengths, weaknesses, and recommendations. How can I help you better understand your company?`;
|
||||
} else if (navigationState.contextType === 'employee') {
|
||||
// For employee reports, include both employee and company context
|
||||
let fullContext = `Employee Report for ${navigationState.employeeName}:\n${navigationState.initialContext}`;
|
||||
|
||||
if (navigationState.companyContext) {
|
||||
fullContext += `\n\nCompany Report Context:\n${navigationState.companyContext}`;
|
||||
}
|
||||
|
||||
contextMessage = `I'm sharing employee and company report data with you for analysis:\n\n${fullContext}`;
|
||||
aiResponse = `Thank you for sharing the report data! I now have context about ${navigationState.employeeName}'s performance and ${navigationState.companyName || 'your company'}'s overall situation. I can help you analyze their strengths, development areas, and how they fit into the broader company picture. How can I help you better understand ${navigationState.employeeName}?`;
|
||||
}
|
||||
|
||||
// Create initial messages
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: contextMessage,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: aiResponse,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// Set initial messages
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
messages: [userMessage, aiMessage]
|
||||
}));
|
||||
|
||||
// Clear the navigation state to prevent re-triggering
|
||||
navigate(location.pathname, { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleInitialContext();
|
||||
}, [location.state, navigate, location.pathname]);
|
||||
|
||||
// Recalculate cursor position when input or cursor position changes
|
||||
useEffect(() => {
|
||||
if (isInputFocused) {
|
||||
|
||||
@@ -12,6 +12,9 @@ const CompanyWiki: React.FC = () => {
|
||||
const [activeSection, setActiveSection] = useState(1);
|
||||
const [onboardingCompleted, setOnboardingCompleted] = useState(false);
|
||||
const [lastStep, setLastStep] = useState<number | undefined>();
|
||||
const [editingField, setEditingField] = useState<string | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadOnboardingData();
|
||||
@@ -40,6 +43,54 @@ const CompanyWiki: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnswerClick = (field: string) => {
|
||||
if (isSaving) return;
|
||||
setEditingField(field);
|
||||
setEditValue(onboardingData?.[field] || '');
|
||||
};
|
||||
|
||||
const handleSaveAnswer = async () => {
|
||||
if (!editingField || isSaving) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
|
||||
// Update the onboarding data
|
||||
const updatedData = {
|
||||
...onboardingData,
|
||||
[editingField]: editValue
|
||||
};
|
||||
|
||||
await secureApi.updateOrgData({
|
||||
onboardingData: updatedData
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setOnboardingData(updatedData);
|
||||
setEditingField(null);
|
||||
setEditValue('');
|
||||
} catch (err: any) {
|
||||
console.error('Failed to save answer:', err);
|
||||
// Optionally show error to user
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingField(null);
|
||||
setEditValue('');
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSaveAnswer();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancelEdit();
|
||||
}
|
||||
};
|
||||
|
||||
const getSectionTitle = (section: number): string => {
|
||||
const titles = {
|
||||
1: 'Company Overview & Vision',
|
||||
@@ -238,7 +289,6 @@ const CompanyWiki: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex h-full self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start">
|
||||
{/* Left Sidebar - Company Navigation */}
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] rounded-tr-3xl rounded-br-3xl inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch flex-1 inline-flex justify-start items-center">
|
||||
@@ -286,9 +336,38 @@ const CompanyWiki: React.FC = () => {
|
||||
<div className="self-stretch px-3 py-2 bg-[--Neutrals-NeutralSlate0] rounded-[10px] inline-flex justify-between items-center">
|
||||
<div className="flex-1 flex justify-start items-start gap-3">
|
||||
<div className="w-3.5 h-6 justify-center text-[--Text-Gray-300] text-base font-semibold font-['Inter'] leading-normal">A</div>
|
||||
<div className="flex-1 justify-start text-[--Text-Gray-800] text-base font-normal font-['Inter'] leading-normal whitespace-pre-wrap">
|
||||
{qa.answer}
|
||||
</div>
|
||||
{editingField === qa.field ? (
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
onBlur={handleSaveAnswer}
|
||||
autoFocus
|
||||
className="w-full bg-[--Neutrals-NeutralSlate50] border border-[--Neutrals-NeutralSlate200] rounded-lg px-3 py-2 text-[--Text-Gray-800] text-base font-normal font-['Inter'] leading-normal resize-none overflow-hidden min-h-[24px]"
|
||||
rows={1}
|
||||
style={{ height: 'auto' }}
|
||||
ref={(textarea) => {
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = `${Math.max(24, textarea.scrollHeight)}px`;
|
||||
}
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
{isSaving && (
|
||||
<div className="text-xs text-[--Text-Gray-500] mt-1">Saving...</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex-1 justify-start text-[--Text-Gray-800] text-base font-normal font-['Inter'] leading-normal whitespace-pre-wrap cursor-pointer hover:bg-[--Neutrals-NeutralSlate50] rounded-md px-2 py-1 -mx-2 -my-1 transition-colors duration-150"
|
||||
onClick={() => handleAnswerClick(qa.field)}
|
||||
title="Click to edit"
|
||||
>
|
||||
{qa.answer || 'Click to add an answer'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { secureApi } from '../services/secureApi';
|
||||
import { CompanyReport, Employee, EmployeeReport } from '../types';
|
||||
@@ -8,12 +8,153 @@ import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
||||
import { downloadCompanyReportPDF, downloadEmployeeReportPDF } from '../utils/pdfUtils';
|
||||
import FigmaPrimaryButton from '../components/figma/FigmaButton';
|
||||
import { DownloadIcon } from '../components/figma/figmaIcon';
|
||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||
|
||||
const Reports: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Helper function to extract company report context for AI
|
||||
const extractCompanyReportContext = (report: CompanyReport, orgName: string): string => {
|
||||
const sections = [];
|
||||
|
||||
if (report.overview) {
|
||||
sections.push(`Company Overview:
|
||||
- Total Employees: ${report.overview.totalEmployees}
|
||||
- Average Performance Score: ${report.overview.averagePerformanceScore}
|
||||
- Risk Level: ${report.overview.riskLevel}
|
||||
- Submission Rate: ${report.overview.submissionRate}%`);
|
||||
|
||||
if (report.overview.departmentBreakdown?.length > 0) {
|
||||
const deptInfo = report.overview.departmentBreakdown
|
||||
.map(dept => `${dept.department}: ${dept.count} employees`)
|
||||
.join(', ');
|
||||
sections.push(`Department Breakdown: ${deptInfo}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.strengths?.length > 0) {
|
||||
sections.push(`Company Strengths:\n${report.strengths.map(s => `- ${s}`).join('\n')}`);
|
||||
}
|
||||
|
||||
if (report.weaknesses?.length > 0) {
|
||||
sections.push(`Company Weaknesses:\n${report.weaknesses.map(w => `- ${w.title}: ${w.description}`).join('\n')}`);
|
||||
}
|
||||
|
||||
if (report.immediateHiringNeeds?.length > 0) {
|
||||
sections.push(`Immediate Hiring Needs:\n${report.immediateHiringNeeds.map(need => `- ${need.role}: ${need.priority} priority - ${need.reasoning}`).join('\n')}`);
|
||||
}
|
||||
|
||||
if (report.personnelChanges) {
|
||||
const changes = [];
|
||||
if (report.personnelChanges.newHires?.length > 0) {
|
||||
changes.push(`New Hires: ${report.personnelChanges.newHires.map(hire => `${hire.name} (${hire.role} in ${hire.department})`).join(', ')}`);
|
||||
}
|
||||
if (report.personnelChanges.promotions?.length > 0) {
|
||||
changes.push(`Promotions: ${report.personnelChanges.promotions.map(promo => `${promo.name}: ${promo.fromRole} → ${promo.toRole}`).join(', ')}`);
|
||||
}
|
||||
if (report.personnelChanges.departures?.length > 0) {
|
||||
changes.push(`Departures: ${report.personnelChanges.departures.map(dep => `${dep.name} (${dep.department}) - ${dep.reason}`).join(', ')}`);
|
||||
}
|
||||
if (changes.length > 0) {
|
||||
sections.push(`Personnel Changes:\n${changes.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.forwardOperatingPlan?.length > 0) {
|
||||
sections.push(`Forward Operating Plan:\n${report.forwardOperatingPlan.map(plan => `- ${plan.title}: ${plan.details.join(', ')}`).join('\n')}`);
|
||||
}
|
||||
|
||||
if (report.gradingBreakdown?.length > 0) {
|
||||
const gradingInfo = report.gradingBreakdown.map(dept =>
|
||||
`${dept.departmentName}: ${dept.departmentGrade} (Lead: ${dept.lead})`
|
||||
).join('\n');
|
||||
sections.push(`Performance Grading by Department:\n${gradingInfo}`);
|
||||
}
|
||||
|
||||
return sections.join('\n\n');
|
||||
};
|
||||
|
||||
// Helper function to extract employee report context for AI
|
||||
const extractEmployeeReportContext = (report: EmployeeReport, employeeName: string, companyReport?: CompanyReport): { employeeContext: string; companyContext?: string } => {
|
||||
const employeeSections = [];
|
||||
|
||||
if (report.roleAndOutput) {
|
||||
employeeSections.push(`Role & Responsibilities: ${report.roleAndOutput.responsibilities}`);
|
||||
if (report.roleAndOutput.selfRatedOutput) {
|
||||
employeeSections.push(`Self-Rated Output: ${report.roleAndOutput.selfRatedOutput}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.insights) {
|
||||
employeeSections.push(`Personality Traits: ${report.insights.personalityTraits}`);
|
||||
if (report.insights.selfAwareness) {
|
||||
employeeSections.push(`Self-Awareness: ${report.insights.selfAwareness}`);
|
||||
}
|
||||
if (report.insights.growthDesire) {
|
||||
employeeSections.push(`Growth Desire: ${report.insights.growthDesire}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.strengths?.length > 0) {
|
||||
employeeSections.push(`Strengths:\n${report.strengths.map(s => `- ${s}`).join('\n')}`);
|
||||
}
|
||||
|
||||
if (report.weaknesses?.length > 0) {
|
||||
employeeSections.push(`Areas for Improvement:\n${report.weaknesses.map(w => `- ${w}`).join('\n')}`);
|
||||
}
|
||||
|
||||
if (report.recommendations?.length > 0) {
|
||||
employeeSections.push(`Recommendations:\n${report.recommendations.map(r => `- ${r}`).join('\n')}`);
|
||||
}
|
||||
|
||||
if (report.gradingOverview) {
|
||||
const gradingInfo = `Grade: ${report.gradingOverview.grade}, Reliability: ${report.gradingOverview.reliability}, Role Fit: ${report.gradingOverview.roleFit}, Scalability: ${report.gradingOverview.scalability}, Output: ${report.gradingOverview.output}, Initiative: ${report.gradingOverview.initiative}`;
|
||||
employeeSections.push(`Performance Scores: ${gradingInfo}`);
|
||||
}
|
||||
|
||||
const employeeContext = employeeSections.join('\n\n');
|
||||
let companyContext;
|
||||
|
||||
if (companyReport && org?.companyName) {
|
||||
companyContext = extractCompanyReportContext(companyReport, org.companyName);
|
||||
}
|
||||
|
||||
return { employeeContext, companyContext };
|
||||
};
|
||||
|
||||
// Handle Chat with AI navigation for company reports
|
||||
const handleCompanyChatWithAI = () => {
|
||||
if (!companyReport || !org?.companyName) return;
|
||||
|
||||
const context = extractCompanyReportContext(companyReport, org.companyName);
|
||||
|
||||
navigate('/chat', {
|
||||
state: {
|
||||
initialContext: context,
|
||||
contextType: 'company',
|
||||
companyName: org.companyName
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Handle Chat with AI navigation for employee reports
|
||||
const handleEmployeeChatWithAI = (employeeName: string, employeeReport: EmployeeReport) => {
|
||||
const { employeeContext, companyContext } = extractEmployeeReportContext(employeeReport, employeeName, companyReport);
|
||||
|
||||
navigate('/chat', {
|
||||
state: {
|
||||
initialContext: employeeContext,
|
||||
companyContext: companyContext,
|
||||
contextType: 'employee',
|
||||
employeeName: employeeName,
|
||||
companyName: org?.companyName
|
||||
}
|
||||
});
|
||||
};
|
||||
const { employees, reports, submissions, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, orgId, org } = useOrg();
|
||||
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
||||
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string } | null>(null);
|
||||
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string; employeeEmail?: string } | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [generatingEmployeeReport, setGeneratingEmployeeReport] = useState<string | null>(null);
|
||||
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
|
||||
@@ -23,6 +164,16 @@ const Reports: React.FC = () => {
|
||||
// Get selected employee ID from navigation state (from Submissions page)
|
||||
const selectedEmployeeId = location.state?.selectedEmployeeId;
|
||||
|
||||
console.log('Reports page state:', {
|
||||
employeesCount: employees.length,
|
||||
reportsCount: Object.keys(reports).length,
|
||||
submissionsCount: Object.keys(submissions).length,
|
||||
currentUserIsOwner,
|
||||
companyReport: !!companyReport,
|
||||
selectedReport: selectedReport?.type,
|
||||
employeesList: employees.map(emp => ({ id: emp.id, name: emp.name }))
|
||||
});
|
||||
|
||||
const handleGenerateEmployeeReport = async (employee: Employee) => {
|
||||
if (generatingEmployeeReport === employee.id) return; // Prevent double-click
|
||||
|
||||
@@ -49,29 +200,39 @@ const Reports: React.FC = () => {
|
||||
// Load company report on component mount
|
||||
useEffect(() => {
|
||||
const loadCompanyReport = async () => {
|
||||
if (currentUserIsOwner) {
|
||||
try {
|
||||
const history = await getFullCompanyReportHistory();
|
||||
if (history.length > 0) {
|
||||
setCompanyReport(history[0]);
|
||||
// Auto-select company report by default
|
||||
setSelectedReport({ report: history[0], type: 'company' });
|
||||
} else {
|
||||
// FIXED: No automatic generation - only load existing reports
|
||||
// Use sample data when no real reports exist
|
||||
console.log('No company reports found, using sample data. Click "Refresh Report" to generate a new one.');
|
||||
setCompanyReport(SAMPLE_COMPANY_REPORT);
|
||||
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load company report:', error);
|
||||
setCompanyReport(SAMPLE_COMPANY_REPORT);
|
||||
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' });
|
||||
try {
|
||||
console.log('Loading company report history...');
|
||||
const history = await getFullCompanyReportHistory();
|
||||
console.log('Company report history loaded:', history.length, 'reports');
|
||||
|
||||
if (history.length > 0) {
|
||||
console.log('Setting existing company report as default');
|
||||
setCompanyReport(history[0]);
|
||||
// Auto-select company report by default
|
||||
setSelectedReport({ report: history[0], type: 'company' });
|
||||
} else {
|
||||
console.log('No company reports found, setting placeholder report');
|
||||
// Create a placeholder that shows generate button
|
||||
const placeholderReport = {
|
||||
...SAMPLE_COMPANY_REPORT,
|
||||
isPlaceholder: true
|
||||
} as CompanyReport;
|
||||
setCompanyReport(placeholderReport);
|
||||
setSelectedReport({ report: placeholderReport, type: 'company' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load company report:', error);
|
||||
// On error, show placeholder with generate option
|
||||
const placeholderReport = {
|
||||
...SAMPLE_COMPANY_REPORT,
|
||||
isPlaceholder: true
|
||||
} as CompanyReport;
|
||||
setCompanyReport(placeholderReport);
|
||||
setSelectedReport({ report: placeholderReport, type: 'company' });
|
||||
}
|
||||
};
|
||||
loadCompanyReport();
|
||||
}, [currentUserIsOwner, getFullCompanyReportHistory]); // FIXED: Removed generateCompanyReport and submissions dependencies
|
||||
}, [currentUserIsOwner, getFullCompanyReportHistory, employees, user?.uid]);
|
||||
|
||||
const handleEmployeeSelect = useCallback(async (employee: Employee) => {
|
||||
const employeeReport = reports[employee.id];
|
||||
@@ -79,7 +240,8 @@ const Reports: React.FC = () => {
|
||||
setSelectedReport({
|
||||
report: employeeReport,
|
||||
type: 'employee',
|
||||
employeeName: employee.name
|
||||
employeeName: employee.name,
|
||||
employeeEmail: employee.email
|
||||
});
|
||||
} else {
|
||||
// FIXED: Only check if employee has submission - do NOT auto-generate
|
||||
@@ -102,7 +264,8 @@ const Reports: React.FC = () => {
|
||||
recommendations: ['Generate the report to view detailed analysis and recommendations']
|
||||
} as EmployeeReport,
|
||||
type: 'employee',
|
||||
employeeName: employee.name
|
||||
employeeName: employee.name,
|
||||
employeeEmail: employee.email
|
||||
});
|
||||
} else {
|
||||
// No submission available - show message
|
||||
@@ -122,7 +285,8 @@ const Reports: React.FC = () => {
|
||||
recommendations: ['Employee should complete the questionnaire first']
|
||||
} as EmployeeReport,
|
||||
type: 'employee',
|
||||
employeeName: employee.name
|
||||
employeeName: employee.name,
|
||||
employeeEmail: employee.email
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -139,11 +303,11 @@ const Reports: React.FC = () => {
|
||||
}, [selectedEmployeeId, employees, handleEmployeeSelect]);
|
||||
|
||||
// Filter and sort employees
|
||||
const visibleEmployees = currentUserIsOwner
|
||||
? employees.filter(emp =>
|
||||
emp.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
).sort((a, b) => a.name.localeCompare(b.name))
|
||||
: employees.filter(emp => emp.id === user?.uid);
|
||||
const visibleEmployees = employees;
|
||||
|
||||
console.log('Visible employees:', visibleEmployees.length, 'out of', employees.length, 'total employees');
|
||||
console.log('Search query:', searchQuery);
|
||||
console.log('Current user is owner:', currentUserIsOwner);
|
||||
|
||||
const handleCompanyReportSelect = () => {
|
||||
if (companyReport) {
|
||||
@@ -171,7 +335,7 @@ const Reports: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Middle Section - Employee List */}
|
||||
<div className="h-100vh flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] flex justify-start items-center">
|
||||
<div className="h-full flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] flex justify-start items-center">
|
||||
<div className="flex-1 self-stretch max-w-64 min-w-64 border-r border-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 justify-start text-[--Text-Gray-950] text-base font-medium font-['Inter'] leading-normal">Employees</div>
|
||||
@@ -199,8 +363,8 @@ const Reports: React.FC = () => {
|
||||
|
||||
{/* Employee List */}
|
||||
<div className="self-stretch px-3 flex flex-col justify-start items-start">
|
||||
{/* Company Report Item */}
|
||||
{currentUserIsOwner && (
|
||||
{/* Company Report Item - Always show for consistency */}
|
||||
{companyReport && (
|
||||
<div
|
||||
className={`self-stretch p-2 rounded-full shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer ${selectedReport?.type === 'company' ? 'bg-[--Neutrals-NeutralSlate100]' : ''
|
||||
}`}
|
||||
@@ -208,10 +372,18 @@ const Reports: React.FC = () => {
|
||||
>
|
||||
<div className="w-7 h-7 p-1 bg-[--Brand-Orange] rounded-[666.67px] flex justify-center items-center">
|
||||
<div className="text-center justify-start text-[--Text-Gray-0] text-xs font-medium font-['Inter'] leading-none">
|
||||
C
|
||||
{org?.companyName?.charAt(0)?.toUpperCase() || 'C'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-[--Text-Gray-950] text-sm font-normal font-['Inter'] leading-tight">Company Report</div>
|
||||
<div className="flex-1 justify-start text-[--Text-Gray-950] text-sm font-normal font-['Inter'] leading-tight">
|
||||
{org?.companyName || 'Company Report'}
|
||||
</div>
|
||||
{/* Status indicator for company report */}
|
||||
{(companyReport as any)?.isPlaceholder ? (
|
||||
<div className="w-3 h-3 bg-gray-300 rounded-full" title="Report not generated yet" />
|
||||
) : (
|
||||
<div className="w-3 h-3 bg-green-400 rounded-full" title="Report available" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -224,12 +396,12 @@ const Reports: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
key={employee.id}
|
||||
className={`self-stretch p-2 rounded-full shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer ${selectedReport?.type === 'employee' && selectedReport?.employeeName === employee.name ? 'bg-[--Neutrals-NeutralSlate100]' : ''
|
||||
className={`self-stretch p-2 rounded-full shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer ${selectedReport?.type === 'employee' && selectedReport?.employeeEmail === employee.email ? 'bg-[--Neutrals-NeutralSlate100]' : ''
|
||||
}`}
|
||||
onClick={() => handleEmployeeSelect(employee)}
|
||||
>
|
||||
<div className="w-7 h-7 p-1 bg-[--Neutrals-NeutralSlate100] rounded-[666.67px] flex justify-center items-center relative">
|
||||
<div className="text-center justify-start text-[--Text-Gray-500] text-xs font-medium font-['Inter'] leading-none">
|
||||
<div className={`w-7 h-7 p-1 ${selectedReport?.type === 'employee' && selectedReport?.employeeEmail === employee.email ? 'bg-[--Neutrals-NeutralSlate700] text-[--Neutrals-NeutralSlate0]' : 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate500]'} rounded-[666.67px] flex justify-center items-center relative`}>
|
||||
<div className="text-center justify-start text-xs font-medium font-['Inter'] leading-none">
|
||||
{employee.initials}
|
||||
</div>
|
||||
{/* Status indicator */}
|
||||
@@ -265,6 +437,7 @@ const Reports: React.FC = () => {
|
||||
onRegenerate={handleGenerateCompanyReport}
|
||||
isGenerating={generatingCompanyReport}
|
||||
org={org}
|
||||
onChatWithAI={handleCompanyChatWithAI}
|
||||
/>
|
||||
) : (
|
||||
(() => {
|
||||
@@ -274,8 +447,9 @@ const Reports: React.FC = () => {
|
||||
<EmployeeReportContent
|
||||
report={employeeReport}
|
||||
employeeName={selectedReport.employeeName!}
|
||||
employeeEmail={selectedReport.employeeEmail!}
|
||||
onGenerateReport={() => {
|
||||
const employee = employees.find(emp => emp.name === selectedReport.employeeName);
|
||||
const employee = employees.find(emp => emp.email === selectedReport.employeeEmail);
|
||||
if (employee) handleGenerateEmployeeReport(employee);
|
||||
}}
|
||||
isGenerating={generatingEmployeeReport === employeeId}
|
||||
@@ -283,6 +457,7 @@ const Reports: React.FC = () => {
|
||||
showGenerateButton={!reports[employeeId] && !!submissions[employeeId]}
|
||||
employees={employees}
|
||||
org={org}
|
||||
onChatWithAI={() => handleEmployeeChatWithAI(selectedReport.employeeName!, employeeReport)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
@@ -311,7 +486,8 @@ const CompanyReportContent: React.FC<{
|
||||
onRegenerate: () => void;
|
||||
isGenerating: boolean;
|
||||
org?: any;
|
||||
}> = ({ report, onRegenerate, isGenerating, org }) => {
|
||||
onChatWithAI?: () => void;
|
||||
}> = ({ report, onRegenerate, isGenerating, org, onChatWithAI }) => {
|
||||
// Default to the first department in the array
|
||||
const [activeDepartmentTab, setActiveDepartmentTab] = useState(() =>
|
||||
report?.gradingBreakdown?.[0]?.departmentNameShort || 'Campaigns'
|
||||
@@ -349,6 +525,22 @@ const CompanyReportContent: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onChatWithAI}
|
||||
className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
<div className="relative">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6667 7.33333C14.6667 10.647 11.9804 13.3333 8.66667 13.3333C7.95 13.3333 7.27333 13.18 6.66667 12.9067L2 14L3.09333 9.33333C2.82 8.72667 2.66667 8.05 2.66667 7.33333C2.66667 4.01967 5.353 1.33333 8.66667 1.33333C11.9804 1.33333 14.6667 4.01967 14.6667 7.33333Z" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M6 6.66667H6.00667M8.66667 6.66667H8.67333M11.3333 6.66667H11.34" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">
|
||||
Chat with AI
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<FigmaPrimaryButton
|
||||
onClick={() => downloadCompanyReportPDF(report, org?.companyName || 'Company')}
|
||||
text="Download as PDF"
|
||||
@@ -361,7 +553,7 @@ const CompanyReportContent: React.FC<{
|
||||
{/* Content */}
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
||||
{/* Company Weaknesses */}
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Company Weaknesses</div>
|
||||
</div>
|
||||
@@ -391,7 +583,7 @@ const CompanyReportContent: React.FC<{
|
||||
|
||||
{/* Personnel Changes */}
|
||||
{report?.personnelChanges && (
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Personnel Changes</div>
|
||||
</div>
|
||||
@@ -453,7 +645,7 @@ const CompanyReportContent: React.FC<{
|
||||
|
||||
{/* Hiring Needs */}
|
||||
{report?.immediateHiringNeeds && report.immediateHiringNeeds.length > 0 && (
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Immediate Hiring Needs</div>
|
||||
</div>
|
||||
@@ -479,7 +671,7 @@ const CompanyReportContent: React.FC<{
|
||||
|
||||
{/* Forward Plan */}
|
||||
{report?.forwardOperatingPlan && (
|
||||
<div className="w-full self-stretch p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full self-stretch p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Forward Plan</div>
|
||||
</div>
|
||||
@@ -512,7 +704,7 @@ const CompanyReportContent: React.FC<{
|
||||
Strengths goes here
|
||||
*/}
|
||||
{report?.strengths && (
|
||||
<div className="self-stretch p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] inline-flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="self-stretch p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] inline-flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Strengths</div>
|
||||
</div>
|
||||
@@ -609,7 +801,7 @@ const CompanyReportContent: React.FC<{
|
||||
)}
|
||||
**/}
|
||||
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Organizational Impact Summary</div>
|
||||
</div>
|
||||
@@ -675,7 +867,7 @@ const CompanyReportContent: React.FC<{
|
||||
|
||||
|
||||
{/* Grading Overview */}
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Grading Overview</div>
|
||||
</div>
|
||||
@@ -784,13 +976,15 @@ const CompanyReportContent: React.FC<{
|
||||
const EmployeeReportContent: React.FC<{
|
||||
report: EmployeeReport;
|
||||
employeeName: string;
|
||||
employeeEmail: string;
|
||||
onGenerateReport?: () => void;
|
||||
isGenerating?: boolean;
|
||||
hasSubmission?: boolean;
|
||||
showGenerateButton?: boolean;
|
||||
employees?: Employee[];
|
||||
org?: any;
|
||||
}> = ({ report, employeeName, onGenerateReport, isGenerating = false, hasSubmission = false, showGenerateButton = false, employees, org }) => {
|
||||
onChatWithAI?: () => void;
|
||||
}> = ({ report, employeeName, onGenerateReport, isGenerating = false, hasSubmission = false, showGenerateButton = false, employees, org, onChatWithAI }) => {
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
@@ -823,13 +1017,33 @@ const EmployeeReportContent: React.FC<{
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Chat with AI Button - only show for actual reports */}
|
||||
{!showGenerateButton && onChatWithAI && (
|
||||
<button
|
||||
onClick={onChatWithAI}
|
||||
className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
<div className="relative">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6667 7.33333C14.6667 10.647 11.9804 13.3333 8.66667 13.3333C7.95 13.3333 7.27333 13.18 6.66667 12.9067L2 14L3.09333 9.33333C2.82 8.72667 2.66667 8.05 2.66667 7.33333C2.66667 4.01967 5.353 1.33333 8.66667 1.33333C11.9804 1.33333 14.6667 4.01967 14.6667 7.33333Z" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M6 6.66667H6.00667M8.66667 6.66667H8.67333M11.3333 6.66667H11.34" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">
|
||||
Chat with AI
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Download PDF Button - only show for actual reports */}
|
||||
{!showGenerateButton && (
|
||||
<FigmaPrimaryButton
|
||||
containerExtra='h-9.5'
|
||||
text="Download as PDF"
|
||||
onClick={() => {
|
||||
const employee = employees.find(emp => emp.name === employeeName);
|
||||
const employee = employees.find(emp => emp.email === employeeName);
|
||||
if (employee) {
|
||||
downloadEmployeeReportPDF(employee, report, org?.companyName || 'Company');
|
||||
}
|
||||
@@ -844,7 +1058,7 @@ const EmployeeReportContent: React.FC<{
|
||||
{/* Content */}
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
||||
{/* Role & Responsibilities */}
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Role & Responsibilities</div>
|
||||
</div>
|
||||
@@ -857,7 +1071,7 @@ const EmployeeReportContent: React.FC<{
|
||||
|
||||
{/* Self-Rated Output */}
|
||||
{report.roleAndOutput && report.roleAndOutput?.selfRatedOutput && (
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Self-Rated Output</div>
|
||||
</div>
|
||||
@@ -871,7 +1085,7 @@ const EmployeeReportContent: React.FC<{
|
||||
|
||||
{/* Insights */}
|
||||
{report.insights && (
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Insights & Traits</div>
|
||||
</div>
|
||||
@@ -906,7 +1120,7 @@ const EmployeeReportContent: React.FC<{
|
||||
|
||||
{/* Strengths */}
|
||||
{report.strengths && report.strengths.length > 0 && (
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Strengths</div>
|
||||
</div>
|
||||
@@ -930,14 +1144,14 @@ const EmployeeReportContent: React.FC<{
|
||||
|
||||
{/* Recommendations */}
|
||||
{report.recommendations && report.recommendations.length > 0 && (
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||
<div className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Recommendations</div>
|
||||
</div>
|
||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
||||
{report.recommendations.map((recommendation, index) => (
|
||||
<div key={index} className="self-stretch text-[--Text-Gray-800] text-base font-normal font-['Inter'] leading-normal">
|
||||
• {recommendation}
|
||||
<MarkdownRenderer content={recommendation} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { EMPLOYEE_QUESTIONS } from '../employeeQuestions';
|
||||
|
||||
interface EmployeeSubmission {
|
||||
employeeId: string;
|
||||
answers: Record<string, string>;
|
||||
answers: { question: string; answer: string; }[] | Record<string, any>;
|
||||
submittedAt: number;
|
||||
employee?: Employee;
|
||||
}
|
||||
@@ -29,17 +29,19 @@ const Submissions: React.FC = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Use the secure API service to get submissions
|
||||
const data = await secureApi.getSubmissions();
|
||||
const submissionsArray = await secureApi.getSubmissions();
|
||||
|
||||
if (data && data.submissions) {
|
||||
// Transform submissions to include employee data
|
||||
if (submissionsArray && submissionsArray.length > 0) {
|
||||
// Transform submissions array to include employee data
|
||||
const submissionsWithEmployees: Record<string, EmployeeSubmission> = {};
|
||||
|
||||
Object.entries(data.submissions).forEach(([employeeId, submission]) => {
|
||||
const employee = employees.find(emp => emp.id === employeeId);
|
||||
submissionsArray.forEach((submission) => {
|
||||
const employee = employees.find(emp => emp.id === submission.employeeId);
|
||||
if (employee) {
|
||||
submissionsWithEmployees[employeeId] = {
|
||||
...submission as EmployeeSubmission,
|
||||
submissionsWithEmployees[submission.employeeId] = {
|
||||
employeeId: submission.employeeId,
|
||||
answers: submission.answers,
|
||||
submittedAt: Date.now(), // Will be added from DB later
|
||||
employee
|
||||
};
|
||||
}
|
||||
@@ -80,24 +82,31 @@ const Submissions: React.FC = () => {
|
||||
employees.forEach((employee, index) => {
|
||||
if (index < 3) { // Only add submissions for first 3 employees
|
||||
console.log(employee);
|
||||
// Create demo answers in the format that would come from the database
|
||||
const demoAnswers = {
|
||||
full_name: employee.name,
|
||||
email: employee.email,
|
||||
title_department: employee.role || 'Team Member',
|
||||
role_clarity: '8',
|
||||
core_responsibilities: 'Managing day-to-day operations, coordinating with team members, and ensuring project deliverables are met on time.',
|
||||
weekly_output: '7',
|
||||
recurring_deliverables: 'Weekly status reports, project coordination meetings, and client communication updates.',
|
||||
measurable_results: 'Successfully completed 5 projects in the last 60 days, improved team efficiency by 15%, and maintained 95% client satisfaction rating.',
|
||||
has_kpis: 'Yes',
|
||||
kpis_details: 'Complete 3-4 projects per month, maintain client satisfaction above 90%, respond to emails within 2 hours.',
|
||||
collaboration_effectiveness: '6',
|
||||
recognition_feeling: 'I feel reasonably recognized for my contributions, though there could be more regular feedback sessions.',
|
||||
job_enjoyment: 'I most enjoy the problem-solving aspects and working directly with clients to understand their needs.',
|
||||
job_frustrations: 'Sometimes unclear priorities and last-minute changes to project requirements can be frustrating.',
|
||||
growth_goals: 'I would like to develop more technical skills and potentially take on a team lead role.',
|
||||
additional_feedback: 'Overall, I appreciate the supportive work environment and would welcome more structured processes.'
|
||||
};
|
||||
|
||||
demoSubmissions[employee.id] = {
|
||||
employeeId: employee.id,
|
||||
employee,
|
||||
submittedAt: Date.now() - (index * 86400000), // Stagger dates
|
||||
answers: {
|
||||
full_name: employee.name,
|
||||
email: employee.email,
|
||||
title_department: employee.role || 'Team Member',
|
||||
mission: 'To empower small businesses with AI-driven automation tools that increase efficiency and reduce operational overhead.',
|
||||
mission_evolution: 'We shifted from general SaaS tools to vertical-specific solutions, with deeper integrations and onboarding support.',
|
||||
vision: 'To become the leading AI operations platform for SMBs in North America, serving over 100,000 customers.',
|
||||
advantages: 'Fast product iteration enabled by in-house AI capabilities\nDeep customer understanding from vertical specialization\nHigh customer retention due to integrated onboarding',
|
||||
vulnerabilities: 'Dependence on a single marketing channel, weak middle management, and rising customer acquisition costs.',
|
||||
role_clarity: 'I understand my role clearly and feel aligned with company objectives.',
|
||||
performance_output: 'I consistently deliver high-quality work and meet deadlines.',
|
||||
collaboration: 'I work well with my team and communicate effectively.',
|
||||
additional_feedback: 'I would appreciate more structured processes and clearer communication channels.'
|
||||
}
|
||||
answers: demoAnswers
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -142,52 +151,57 @@ const Submissions: React.FC = () => {
|
||||
const submission = submissions[selectedEmployee.id];
|
||||
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
|
||||
|
||||
// Handle different submission formats
|
||||
let submissionAnswers: Record<string, string> = {};
|
||||
|
||||
if (submission.answers) {
|
||||
if (Array.isArray(submission.answers)) {
|
||||
// If answers is an array of {question, answer} objects
|
||||
submissionAnswers = submission.answers.reduce((acc, item: any) => {
|
||||
if (item.question && item.answer) {
|
||||
acc[item.question] = item.answer;
|
||||
// Handle array format from database: [{question, answer}, ...]
|
||||
submission.answers.forEach((item: any) => {
|
||||
if (item.question && item.answer && item.answer.trim()) {
|
||||
questionsAndAnswers.push({
|
||||
question: item.question,
|
||||
answer: item.answer,
|
||||
isLong: item.answer.length > 150
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
} else {
|
||||
// If answers is already a key-value object
|
||||
submissionAnswers = submission.answers as Record<string, string>;
|
||||
});
|
||||
} else if (typeof submission.answers === 'object') {
|
||||
// Handle object format: {questionId: answer, ...}
|
||||
const submissionAnswers = submission.answers as Record<string, any>;
|
||||
|
||||
// Try to match with EMPLOYEE_QUESTIONS first to get proper question text
|
||||
EMPLOYEE_QUESTIONS.forEach(q => {
|
||||
const answer = submissionAnswers[q.id];
|
||||
if (answer && (typeof answer === 'string' ? answer.trim() : answer)) {
|
||||
const answerText = typeof answer === 'string' ? answer :
|
||||
typeof answer === 'boolean' ? (answer ? 'Yes' : 'No') :
|
||||
String(answer);
|
||||
questionsAndAnswers.push({
|
||||
question: q.prompt,
|
||||
answer: answerText,
|
||||
isLong: answerText.length > 150
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add any additional answers not in EMPLOYEE_QUESTIONS
|
||||
Object.entries(submissionAnswers).forEach(([key, answer]) => {
|
||||
if (answer && !EMPLOYEE_QUESTIONS.find(q => q.id === key)) {
|
||||
const answerText = typeof answer === 'string' ? answer.trim() :
|
||||
typeof answer === 'boolean' ? (answer ? 'Yes' : 'No') :
|
||||
String(answer);
|
||||
if (answerText) {
|
||||
// Format the key as a readable question
|
||||
const formattedQuestion = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
questionsAndAnswers.push({
|
||||
question: formattedQuestion,
|
||||
answer: answerText,
|
||||
isLong: answerText.length > 150
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If we have structured answers, map them to questions
|
||||
if (Object.keys(submissionAnswers).length > 0) {
|
||||
// Try to match with EMPLOYEE_QUESTIONS first
|
||||
EMPLOYEE_QUESTIONS.forEach(q => {
|
||||
const answer = submissionAnswers[q.id];
|
||||
if (answer && answer.trim()) {
|
||||
questionsAndAnswers.push({
|
||||
question: q.prompt,
|
||||
answer: answer,
|
||||
isLong: answer.length > 150
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add any additional answers not in EMPLOYEE_QUESTIONS
|
||||
Object.entries(submissionAnswers).forEach(([key, answer]) => {
|
||||
if (answer && ((typeof answer === 'string' && answer.trim()) || typeof answer === 'boolean' || typeof answer === 'number') && !EMPLOYEE_QUESTIONS.find(q => q.id === key)) {
|
||||
// Format the key as a readable question
|
||||
const formattedQuestion = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
questionsAndAnswers.push({
|
||||
question: formattedQuestion,
|
||||
answer: answer,
|
||||
isLong: answer.length > 150
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return questionsAndAnswers;
|
||||
};
|
||||
|
||||
@@ -206,7 +220,7 @@ const Submissions: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Middle Section - Employee List */}
|
||||
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] flex justify-start items-center">
|
||||
<div className="h-full flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] flex justify-start items-center">
|
||||
<div className="flex-1 self-stretch max-w-64 min-w-64 border-r border-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 justify-start text-[--Text-Gray-950] text-base font-medium font-['Inter'] leading-normal">Employees</div>
|
||||
|
||||
Reference in New Issue
Block a user