update 9/23
This commit is contained in:
18
.gitignore
vendored
18
.gitignore
vendored
@@ -72,9 +72,15 @@ dist-ssr
|
|||||||
*.ignore
|
*.ignore
|
||||||
*.py
|
*.py
|
||||||
*.md
|
*.md
|
||||||
.github/instructions
|
/.github/instructions
|
||||||
schema.sql
|
/schema.sql
|
||||||
setup-neondb.sh
|
/functions/setup-neondb.sh
|
||||||
functions/migrations/seed.js
|
/functions/migrations/seed.js
|
||||||
dev-setup.sh
|
/dev-setup.sh
|
||||||
docker-compose.yml
|
/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",
|
"name": "auditly-functions",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/vertexai": "^1.10.0",
|
"@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",
|
"firebase-functions": "^6.4.0",
|
||||||
"pg": "^8.12.0",
|
"nodemailer": "6.10.1",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"stripe": "^18.5.0",
|
"stripe": "^18.5.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -97,23 +99,23 @@
|
|||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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/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/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/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=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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/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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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/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=="],
|
"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=="],
|
"@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=="],
|
"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=="],
|
"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": {
|
"dependencies": {
|
||||||
"@google-cloud/vertexai": "^1.10.0",
|
"@google-cloud/vertexai": "^1.10.0",
|
||||||
"@neondatabase/serverless": "^0.9.5",
|
"@neondatabase/serverless": "0.9.5",
|
||||||
"pg": "^8.12.0",
|
"firebase-admin": "^13.5.0",
|
||||||
"stripe": "^18.5.0",
|
"firebase-functions": "^6.4.0",
|
||||||
"firebase-functions": "^6.4.0"
|
"nodemailer": "6.10.1",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"stripe": "^18.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/pg": "^8.11.6",
|
"@types/pg": "^8.11.6",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Theme, NavItem } from '../types';
|
|||||||
import { useOrg } from '../contexts/OrgContext';
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import FigmaSidebar from './figma/Sidebar';
|
import FigmaSidebar from './figma/Sidebar';
|
||||||
|
import FigmaPrimaryButton from './figma/FigmaButton';
|
||||||
|
import { FigmaIcons } from './figma/figmaIcon';
|
||||||
|
|
||||||
// ========== ICONS ==========
|
// ========== ICONS ==========
|
||||||
|
|
||||||
@@ -151,11 +153,11 @@ const Sidebar = () => {
|
|||||||
<Button size="sm" className="w-full" onClick={() => setShowInviteModal(true)}>
|
<Button size="sm" className="w-full" onClick={() => setShowInviteModal(true)}>
|
||||||
<PlusIcon className="w-4 h-4 mr-1" /> Invite
|
<PlusIcon className="w-4 h-4 mr-1" /> Invite
|
||||||
</Button>
|
</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
|
<CopyIcon className="w-4 h-4 mr-1" /> Copy
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="ghost" className="w-full mt-2" onClick={signOutUser}>Sign out</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Invite Modal */}
|
{/* 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="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">
|
<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"} />
|
<FigmaSidebar companyName={org?.companyName || "Orbitly"} />
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 h-full overflow-y-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ export const FigmaPrimaryButton: React.FC<FigmaButtonProps> = ({
|
|||||||
big: 'px-4 py-3'
|
big: 'px-4 py-3'
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconSize = {
|
|
||||||
tiny: 16,
|
|
||||||
small: 16,
|
|
||||||
big: 20
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
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' }}>
|
<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
|
<button
|
||||||
@@ -45,11 +39,11 @@ export const FigmaPrimaryButton: React.FC<FigmaButtonProps> = ({
|
|||||||
onClick={onClick}
|
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}`}
|
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="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-sm font-medium font-['Inter'] leading-tight">{text}</div>
|
<div className="justify-center text-sm font-medium font-['Inter'] leading-tight">{text}</div>
|
||||||
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -73,24 +67,18 @@ export const FigmaSecondaryButton: React.FC<FigmaButtonProps> = ({
|
|||||||
big: 'px-4 py-3.5'
|
big: 'px-4 py-3.5'
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconSize = {
|
|
||||||
tiny: 16,
|
|
||||||
small: 16,
|
|
||||||
big: 20
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
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
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
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}`}
|
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="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-sm font-medium font-['Inter'] leading-tight">{text}</div>
|
<div className="justify-center text-sm font-medium font-['Inter'] leading-tight">{text}</div>
|
||||||
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import FigmaPrimaryButton from './FigmaButton';
|
||||||
|
|
||||||
// Icon SVG Component - From Figma designs
|
// Icon SVG Component - From Figma designs
|
||||||
export const OrbitlyIcon: React.FC = () => (
|
export const OrbitlyIcon: React.FC = () => (
|
||||||
@@ -113,14 +114,15 @@ export const WelcomeScreen: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<FigmaPrimaryButton size="big" onClick={onStart} text="Start" buttonExtra="self-stretch" />
|
||||||
|
{/* <button
|
||||||
onClick={onStart}
|
onClick={onStart}
|
||||||
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
|
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden h-fit">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<FigmaPrimaryButton size="big" onClick={onStart} text="Start" buttonExtra="self-stretch" />
|
||||||
|
{/* <button
|
||||||
onClick={onStart}
|
onClick={onStart}
|
||||||
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
|
className="self-stretch px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-500 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 h-max px-20 py-16 flex justify-center items-center gap-2.5 flex-shrink">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<FigmaPrimaryButton size="big" disabled={!isValid} onClick={onNext} text="Next" />
|
||||||
|
{/* <button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!isValid}
|
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"
|
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="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -317,14 +321,15 @@ export const TextAreaQuestion: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<FigmaPrimaryButton size="big" onClick={onNext} text="Next" buttonExtra="flex-1" />
|
||||||
|
{/* <button
|
||||||
onClick={onNext}
|
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"
|
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="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -392,10 +397,10 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
<button
|
<button
|
||||||
key={ratingValue}
|
key={ratingValue}
|
||||||
onClick={() => onChange(ratingValue)}
|
onClick={() => onChange(ratingValue)}
|
||||||
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden transition-colors ${isSelected ? 'bg-[--Neutrals-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}
|
{ratingValue}
|
||||||
</div>
|
</div>
|
||||||
@@ -418,7 +423,8 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<FigmaPrimaryButton size="big" disabled={!value} onClick={onNext} text="Next" />
|
||||||
|
{/* <button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!value}
|
disabled={!value}
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
|
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
|
||||||
@@ -426,7 +432,7 @@ export const RatingScaleQuestion: React.FC<{
|
|||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -473,6 +479,7 @@ export const YesNoChoice: React.FC<{
|
|||||||
totalSteps?: number;
|
totalSteps?: number;
|
||||||
sectionName?: string;
|
sectionName?: string;
|
||||||
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
|
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps, sectionName }) => {
|
||||||
|
let SelectedValue = value;
|
||||||
return (
|
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 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">
|
<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>
|
||||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange('No')}
|
onClick={() => { onChange('No'); SelectedValue = '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]'
|
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
|
No
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange('Yes')}
|
onClick={() => onChange('Yes')}
|
||||||
className={`w-20 h-20 relative rounded-[999px] overflow-hidden transition-colors ${value === 'Yes' ? 'bg-[--Neutrals-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
|
Yes
|
||||||
</div>
|
</div>
|
||||||
@@ -514,7 +521,8 @@ export const YesNoChoice: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<FigmaPrimaryButton size="big" disabled={!value} onClick={onNext} text="Next" />
|
||||||
|
{/* <button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!value}
|
disabled={!value}
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
|
className="flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-colors"
|
||||||
@@ -522,7 +530,7 @@ export const YesNoChoice: React.FC<{
|
|||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</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 { useOrg } from '../../contexts/OrgContext';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useUserOrganizations } from '../../contexts/UserOrganizationsContext';
|
import { useUserOrganizations } from '../../contexts/UserOrganizationsContext';
|
||||||
|
import { FigmaPrimaryButton, FigmaSecondaryButton } from './FigmaButton';
|
||||||
|
import { FigmaIcons } from './figmaIcon';
|
||||||
|
import { FigmaInviteEmployeesModal } from './FigmaInviteEmployeesModal';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
@@ -19,7 +22,6 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||||
const [showOrgDropdown, setShowOrgDropdown] = useState(false);
|
const [showOrgDropdown, setShowOrgDropdown] = useState(false);
|
||||||
const [showCreateOrgModal, setShowCreateOrgModal] = useState(false);
|
const [showCreateOrgModal, setShowCreateOrgModal] = useState(false);
|
||||||
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
|
||||||
const [createOrgForm, setCreateOrgForm] = useState({ name: '', description: '' });
|
const [createOrgForm, setCreateOrgForm] = useState({ name: '', description: '' });
|
||||||
const [inviteLink, setInviteLink] = useState('');
|
const [inviteLink, setInviteLink] = useState('');
|
||||||
const [emailLink, setEmailLink] = useState('');
|
const [emailLink, setEmailLink] = useState('');
|
||||||
@@ -55,21 +57,33 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
setShowOrgDropdown(false);
|
setShowOrgDropdown(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = async () => {
|
const handleInvite = async (emails: string[]) => {
|
||||||
try {
|
try {
|
||||||
const result = await issueInviteViaApi({
|
// Send invitations for multiple emails
|
||||||
name: inviteForm.name,
|
const results = await Promise.allSettled(
|
||||||
email: inviteForm.email,
|
emails.map(email => issueInviteViaApi({
|
||||||
role: inviteForm.role,
|
name: '', // Name can be extracted from email or left empty
|
||||||
department: inviteForm.department
|
email: email,
|
||||||
});
|
role: '',
|
||||||
setInviteLink(result.inviteLink);
|
department: ''
|
||||||
// if (process.env.SENDGRID_API_KEY) {
|
}))
|
||||||
// setEmailLink(result.emailLink);
|
);
|
||||||
// }
|
|
||||||
setInviteForm({ name: '', 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) {
|
} 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Invite Employee Modal */}
|
{/* Figma Invite Employee Modal */}
|
||||||
{showInviteModal && (
|
<FigmaInviteEmployeesModal
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
isOpen={showInviteModal}
|
||||||
<div className="bg-[--background-secondary] p-6 rounded-lg max-w-md w-full mx-4">
|
onClose={() => {
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
setShowInviteModal(false);
|
||||||
<div className="space-y-4">
|
setInviteLink('');
|
||||||
<div>
|
setEmailLink('');
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">Name</label>
|
}}
|
||||||
<input
|
onInvite={handleInvite}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bottom Section */}
|
{/* Bottom Section */}
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
@@ -546,13 +460,18 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
</div>
|
</div>
|
||||||
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
<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
|
<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)}>
|
<Button size="sm" variant="primary" className="w-full rounded-[999px]" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)}>
|
||||||
<CopyIcon className="mr-2" /> Copy
|
<CopyIcon className="mr-2" /> Copy
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
export interface FigmaIconProps {
|
export interface FigmaIconProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: number;
|
size?: number | string;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export const PlaceholderIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="placeholder"
|
aria-label="placeholder"
|
||||||
@@ -42,7 +42,7 @@ export const ShareIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox={`0 0 24 24`}
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="share"
|
aria-label="share"
|
||||||
@@ -68,7 +68,7 @@ export const SearchIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="search"
|
aria-label="search"
|
||||||
@@ -94,7 +94,7 @@ export const EyeIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="eye"
|
aria-label="eye"
|
||||||
@@ -155,7 +155,7 @@ export const ChevronLeftIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="chevron-left"
|
aria-label="chevron-left"
|
||||||
@@ -181,7 +181,7 @@ export const ChevronRightIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="chevron-right"
|
aria-label="chevron-right"
|
||||||
@@ -207,7 +207,7 @@ export const ChevronUpIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="chevron-up"
|
aria-label="chevron-up"
|
||||||
@@ -233,7 +233,7 @@ export const ChevronDownIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="chevron-down"
|
aria-label="chevron-down"
|
||||||
@@ -259,7 +259,7 @@ export const CheckCircleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="check-circle"
|
aria-label="check-circle"
|
||||||
@@ -285,7 +285,7 @@ export const LineChartCircleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="line-chart-circle"
|
aria-label="line-chart-circle"
|
||||||
@@ -312,7 +312,7 @@ export const FolderIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="folder"
|
aria-label="folder"
|
||||||
@@ -338,7 +338,7 @@ export const CloudIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="cloud"
|
aria-label="cloud"
|
||||||
@@ -364,7 +364,7 @@ export const DatabaseIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="database"
|
aria-label="database"
|
||||||
@@ -390,7 +390,7 @@ export const UsersIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="users"
|
aria-label="users"
|
||||||
@@ -416,7 +416,7 @@ export const SettingsIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="settings"
|
aria-label="settings"
|
||||||
@@ -442,7 +442,7 @@ export const BellIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="bell"
|
aria-label="bell"
|
||||||
@@ -468,7 +468,7 @@ export const StreamIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="stream"
|
aria-label="stream"
|
||||||
@@ -494,7 +494,7 @@ export const TrashIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="trash"
|
aria-label="trash"
|
||||||
@@ -520,7 +520,7 @@ export const PlusIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="plus"
|
aria-label="plus"
|
||||||
@@ -546,7 +546,7 @@ export const FilterIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="filter"
|
aria-label="filter"
|
||||||
@@ -572,7 +572,7 @@ export const EditIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="edit"
|
aria-label="edit"
|
||||||
@@ -598,7 +598,7 @@ export const XIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="x"
|
aria-label="x"
|
||||||
@@ -624,7 +624,7 @@ export const LoadingIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="loading"
|
aria-label="loading"
|
||||||
@@ -650,7 +650,7 @@ export const InfoCircleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="info-circle"
|
aria-label="info-circle"
|
||||||
@@ -676,7 +676,7 @@ export const DownloadIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="download"
|
aria-label="download"
|
||||||
@@ -702,7 +702,7 @@ export const RefreshIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="refresh"
|
aria-label="refresh"
|
||||||
@@ -728,7 +728,7 @@ export const MailIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="mail"
|
aria-label="mail"
|
||||||
@@ -754,7 +754,7 @@ export const KeyIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="key"
|
aria-label="key"
|
||||||
@@ -780,7 +780,7 @@ export const AIIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="ai"
|
aria-label="ai"
|
||||||
@@ -806,7 +806,7 @@ export const DashboardIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="dashboard"
|
aria-label="dashboard"
|
||||||
@@ -832,7 +832,7 @@ export const ThreeDotsVerticalIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="three-dots-vertical"
|
aria-label="three-dots-vertical"
|
||||||
@@ -872,7 +872,7 @@ export const MoveFolderIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="move-folder"
|
aria-label="move-folder"
|
||||||
@@ -898,7 +898,7 @@ export const UploadFolderIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="upload-folder"
|
aria-label="upload-folder"
|
||||||
@@ -931,7 +931,7 @@ export const LinkIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="link"
|
aria-label="link"
|
||||||
@@ -957,7 +957,7 @@ export const ChatIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="chat"
|
aria-label="chat"
|
||||||
@@ -983,7 +983,7 @@ export const CheckmarkIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="checkmark"
|
aria-label="checkmark"
|
||||||
@@ -1009,7 +1009,7 @@ export const StarIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="star"
|
aria-label="star"
|
||||||
@@ -1035,7 +1035,7 @@ export const CopyIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="copy"
|
aria-label="copy"
|
||||||
@@ -1061,7 +1061,7 @@ export const GoogleGIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="google-g"
|
aria-label="google-g"
|
||||||
@@ -1086,7 +1086,7 @@ export const GoogleLogoIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="google-logo"
|
aria-label="google-logo"
|
||||||
@@ -1128,7 +1128,7 @@ export const BackArrowIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="back-arrow"
|
aria-label="back-arrow"
|
||||||
@@ -1154,7 +1154,7 @@ export const DownArrowIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="down-arrow"
|
aria-label="down-arrow"
|
||||||
@@ -1180,7 +1180,7 @@ export const ForwardArrowIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="forward-arrow"
|
aria-label="forward-arrow"
|
||||||
@@ -1206,7 +1206,7 @@ export const UpArrowIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="up-arrow"
|
aria-label="up-arrow"
|
||||||
@@ -1232,7 +1232,7 @@ export const HelpIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="help"
|
aria-label="help"
|
||||||
@@ -1258,7 +1258,7 @@ export const HomeIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="home"
|
aria-label="home"
|
||||||
@@ -1284,7 +1284,7 @@ export const PieChartIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="pie-chart"
|
aria-label="pie-chart"
|
||||||
@@ -1310,7 +1310,7 @@ export const SupportIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="support"
|
aria-label="support"
|
||||||
@@ -1336,7 +1336,7 @@ export const ChevronSelectorReverseIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="chevron-selector-reverse"
|
aria-label="chevron-selector-reverse"
|
||||||
@@ -1362,7 +1362,7 @@ export const UploadCloudIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="upload-cloud"
|
aria-label="upload-cloud"
|
||||||
@@ -1388,7 +1388,7 @@ export const PhoneIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="phone"
|
aria-label="phone"
|
||||||
@@ -1421,7 +1421,7 @@ export const EmailIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="email"
|
aria-label="email"
|
||||||
@@ -1447,7 +1447,7 @@ export const ImageIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="image"
|
aria-label="image"
|
||||||
@@ -1473,7 +1473,7 @@ export const ArrowDiagonalUpRightIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="arrow-diagonal-up-right"
|
aria-label="arrow-diagonal-up-right"
|
||||||
@@ -1499,7 +1499,7 @@ export const DocumentIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="document"
|
aria-label="document"
|
||||||
@@ -1525,7 +1525,7 @@ export const WebIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="web"
|
aria-label="web"
|
||||||
@@ -1551,7 +1551,7 @@ export const GreenCheckCircleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="green-check-circle"
|
aria-label="green-check-circle"
|
||||||
@@ -1577,7 +1577,7 @@ export const GreenStatusCircleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="green-status-circle"
|
aria-label="green-status-circle"
|
||||||
@@ -1609,7 +1609,7 @@ export const RedXCircleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="red-x-circle"
|
aria-label="red-x-circle"
|
||||||
@@ -1640,7 +1640,7 @@ export const YellowWarningCircleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="yellow-warning-circle"
|
aria-label="yellow-warning-circle"
|
||||||
@@ -1677,7 +1677,7 @@ export const BookIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="book"
|
aria-label="book"
|
||||||
@@ -1703,7 +1703,7 @@ export const AdvancedSearchIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="advanced-search"
|
aria-label="advanced-search"
|
||||||
@@ -1729,7 +1729,7 @@ export const WarningTriangleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="warning-triangle"
|
aria-label="warning-triangle"
|
||||||
@@ -1755,7 +1755,7 @@ export const LineChartXYPlaneIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="line-chart-xy-plane"
|
aria-label="line-chart-xy-plane"
|
||||||
@@ -1781,7 +1781,7 @@ export const FlagIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="flag"
|
aria-label="flag"
|
||||||
@@ -1807,7 +1807,7 @@ export const GenericFlagIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="generic-flag"
|
aria-label="generic-flag"
|
||||||
@@ -1833,7 +1833,7 @@ export const UploadIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="upload"
|
aria-label="upload"
|
||||||
@@ -1859,7 +1859,7 @@ export const ChevronSelectorVerticalIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="chevron-selector-vertical"
|
aria-label="chevron-selector-vertical"
|
||||||
@@ -1885,7 +1885,7 @@ export const SettingsGearIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="settings-gear"
|
aria-label="settings-gear"
|
||||||
@@ -1918,7 +1918,7 @@ export const ComplexWarningTriangleIcon: React.FC<FigmaIconProps> = ({
|
|||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="complex-warning-triangle"
|
aria-label="complex-warning-triangle"
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export const FigmaOnboardingQuestion: React.FC<FigmaOnboardingQuestionProps> = (
|
|||||||
onChange={(e) => onChange(e.target.value)}
|
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"
|
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}
|
placeholder={placeholder}
|
||||||
|
autoFocus={true}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
/>
|
/>
|
||||||
<div className="w-3 h-3 absolute right-3 bottom-3">
|
<div className="w-3 h-3 absolute right-3 bottom-3">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface OrgContextType {
|
|||||||
user?: User;
|
user?: User;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
employees: Employee[];
|
employees: Employee[];
|
||||||
submissions: Submission[];
|
submissions: Record<string, Submission>;
|
||||||
reports: Record<string, EmployeeReport>;
|
reports: Record<string, EmployeeReport>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
upsertOrg: (data: Partial<OrgData>) => Promise<void>;
|
upsertOrg: (data: Partial<OrgData>) => Promise<void>;
|
||||||
@@ -58,7 +58,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [org, setOrg] = useState<OrgData | null>(null);
|
const [org, setOrg] = useState<OrgData | null>(null);
|
||||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
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 [reports, setReports] = useState<Record<string, EmployeeReport>>({});
|
||||||
const [reportVersions, setReportVersions] = useState<Record<string, Array<{ id: string; createdAt: number; report: 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 }>>([]);
|
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
|
// Process submissions data
|
||||||
if (submissionsData.status === 'fulfilled') {
|
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 {
|
} else {
|
||||||
console.warn('Could not load submissions');
|
console.warn('Could not load submissions');
|
||||||
setSubmissions([]);
|
setSubmissions({});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process reports data
|
// Process reports data
|
||||||
@@ -291,7 +298,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
const seedInitialData = async () => {
|
const seedInitialData = async () => {
|
||||||
// Start with clean slate - let users invite their own employees and generate real data
|
// Start with clean slate - let users invite their own employees and generate real data
|
||||||
setEmployees([]);
|
setEmployees([]);
|
||||||
setSubmissions([]);
|
setSubmissions({});
|
||||||
setReports({});
|
setReports({});
|
||||||
setFullCompanyReports([]);
|
setFullCompanyReports([]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
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 { useAuth } from '../contexts/AuthContext';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import { apiPost } from '../services/api';
|
import { apiPost } from '../services/api';
|
||||||
@@ -40,6 +40,7 @@ const Chat: React.FC = () => {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { employees, orgId, org } = useOrg();
|
const { employees, orgId, org } = useOrg();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -159,6 +160,65 @@ const Chat: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [user, navigate]);
|
}, [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
|
// Recalculate cursor position when input or cursor position changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInputFocused) {
|
if (isInputFocused) {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const CompanyWiki: React.FC = () => {
|
|||||||
const [activeSection, setActiveSection] = useState(1);
|
const [activeSection, setActiveSection] = useState(1);
|
||||||
const [onboardingCompleted, setOnboardingCompleted] = useState(false);
|
const [onboardingCompleted, setOnboardingCompleted] = useState(false);
|
||||||
const [lastStep, setLastStep] = useState<number | undefined>();
|
const [lastStep, setLastStep] = useState<number | undefined>();
|
||||||
|
const [editingField, setEditingField] = useState<string | null>(null);
|
||||||
|
const [editValue, setEditValue] = useState('');
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadOnboardingData();
|
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 getSectionTitle = (section: number): string => {
|
||||||
const titles = {
|
const titles = {
|
||||||
1: 'Company Overview & Vision',
|
1: 'Company Overview & Vision',
|
||||||
@@ -238,7 +289,6 @@ const CompanyWiki: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<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 */}
|
{/* 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="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">
|
<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="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="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="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">
|
{editingField === qa.field ? (
|
||||||
{qa.answer}
|
<div className="flex-1">
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
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 { useOrg } from '../contexts/OrgContext';
|
||||||
import { secureApi } from '../services/secureApi';
|
import { secureApi } from '../services/secureApi';
|
||||||
import { CompanyReport, Employee, EmployeeReport } from '../types';
|
import { CompanyReport, Employee, EmployeeReport } from '../types';
|
||||||
@@ -8,12 +8,153 @@ import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
|||||||
import { downloadCompanyReportPDF, downloadEmployeeReportPDF } from '../utils/pdfUtils';
|
import { downloadCompanyReportPDF, downloadEmployeeReportPDF } from '../utils/pdfUtils';
|
||||||
import FigmaPrimaryButton from '../components/figma/FigmaButton';
|
import FigmaPrimaryButton from '../components/figma/FigmaButton';
|
||||||
import { DownloadIcon } from '../components/figma/figmaIcon';
|
import { DownloadIcon } from '../components/figma/figmaIcon';
|
||||||
|
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||||
|
|
||||||
const Reports: React.FC = () => {
|
const Reports: React.FC = () => {
|
||||||
const location = useLocation();
|
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 { employees, reports, submissions, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, orgId, org } = useOrg();
|
||||||
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
const [companyReport, setCompanyReport] = useState<CompanyReport | null>(null);
|
||||||
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string } | null>(null);
|
const [selectedReport, setSelectedReport] = useState<{ report: CompanyReport | EmployeeReport; type: 'company' | 'employee'; employeeName?: string; employeeEmail?: string } | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [generatingEmployeeReport, setGeneratingEmployeeReport] = useState<string | null>(null);
|
const [generatingEmployeeReport, setGeneratingEmployeeReport] = useState<string | null>(null);
|
||||||
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
|
const [generatingCompanyReport, setGeneratingCompanyReport] = useState(false);
|
||||||
@@ -23,6 +164,16 @@ const Reports: React.FC = () => {
|
|||||||
// Get selected employee ID from navigation state (from Submissions page)
|
// Get selected employee ID from navigation state (from Submissions page)
|
||||||
const selectedEmployeeId = location.state?.selectedEmployeeId;
|
const selectedEmployeeId = location.state?.selectedEmployeeId;
|
||||||
|
|
||||||
|
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) => {
|
const handleGenerateEmployeeReport = async (employee: Employee) => {
|
||||||
if (generatingEmployeeReport === employee.id) return; // Prevent double-click
|
if (generatingEmployeeReport === employee.id) return; // Prevent double-click
|
||||||
|
|
||||||
@@ -49,29 +200,39 @@ const Reports: React.FC = () => {
|
|||||||
// Load company report on component mount
|
// Load company report on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCompanyReport = async () => {
|
const loadCompanyReport = async () => {
|
||||||
if (currentUserIsOwner) {
|
try {
|
||||||
try {
|
console.log('Loading company report history...');
|
||||||
const history = await getFullCompanyReportHistory();
|
const history = await getFullCompanyReportHistory();
|
||||||
if (history.length > 0) {
|
console.log('Company report history loaded:', history.length, 'reports');
|
||||||
setCompanyReport(history[0]);
|
|
||||||
// Auto-select company report by default
|
if (history.length > 0) {
|
||||||
setSelectedReport({ report: history[0], type: 'company' });
|
console.log('Setting existing company report as default');
|
||||||
} else {
|
setCompanyReport(history[0]);
|
||||||
// FIXED: No automatic generation - only load existing reports
|
// Auto-select company report by default
|
||||||
// Use sample data when no real reports exist
|
setSelectedReport({ report: history[0], type: 'company' });
|
||||||
console.log('No company reports found, using sample data. Click "Refresh Report" to generate a new one.');
|
} else {
|
||||||
setCompanyReport(SAMPLE_COMPANY_REPORT);
|
console.log('No company reports found, setting placeholder report');
|
||||||
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' });
|
// Create a placeholder that shows generate button
|
||||||
}
|
const placeholderReport = {
|
||||||
} catch (error) {
|
...SAMPLE_COMPANY_REPORT,
|
||||||
console.error('Failed to load company report:', error);
|
isPlaceholder: true
|
||||||
setCompanyReport(SAMPLE_COMPANY_REPORT);
|
} as CompanyReport;
|
||||||
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' });
|
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();
|
loadCompanyReport();
|
||||||
}, [currentUserIsOwner, getFullCompanyReportHistory]); // FIXED: Removed generateCompanyReport and submissions dependencies
|
}, [currentUserIsOwner, getFullCompanyReportHistory, employees, user?.uid]);
|
||||||
|
|
||||||
const handleEmployeeSelect = useCallback(async (employee: Employee) => {
|
const handleEmployeeSelect = useCallback(async (employee: Employee) => {
|
||||||
const employeeReport = reports[employee.id];
|
const employeeReport = reports[employee.id];
|
||||||
@@ -79,7 +240,8 @@ const Reports: React.FC = () => {
|
|||||||
setSelectedReport({
|
setSelectedReport({
|
||||||
report: employeeReport,
|
report: employeeReport,
|
||||||
type: 'employee',
|
type: 'employee',
|
||||||
employeeName: employee.name
|
employeeName: employee.name,
|
||||||
|
employeeEmail: employee.email
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// FIXED: Only check if employee has submission - do NOT auto-generate
|
// 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']
|
recommendations: ['Generate the report to view detailed analysis and recommendations']
|
||||||
} as EmployeeReport,
|
} as EmployeeReport,
|
||||||
type: 'employee',
|
type: 'employee',
|
||||||
employeeName: employee.name
|
employeeName: employee.name,
|
||||||
|
employeeEmail: employee.email
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No submission available - show message
|
// No submission available - show message
|
||||||
@@ -122,7 +285,8 @@ const Reports: React.FC = () => {
|
|||||||
recommendations: ['Employee should complete the questionnaire first']
|
recommendations: ['Employee should complete the questionnaire first']
|
||||||
} as EmployeeReport,
|
} as EmployeeReport,
|
||||||
type: 'employee',
|
type: 'employee',
|
||||||
employeeName: employee.name
|
employeeName: employee.name,
|
||||||
|
employeeEmail: employee.email
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,11 +303,11 @@ const Reports: React.FC = () => {
|
|||||||
}, [selectedEmployeeId, employees, handleEmployeeSelect]);
|
}, [selectedEmployeeId, employees, handleEmployeeSelect]);
|
||||||
|
|
||||||
// Filter and sort employees
|
// Filter and sort employees
|
||||||
const visibleEmployees = currentUserIsOwner
|
const visibleEmployees = employees;
|
||||||
? employees.filter(emp =>
|
|
||||||
emp.name.toLowerCase().includes(searchQuery.toLowerCase())
|
console.log('Visible employees:', visibleEmployees.length, 'out of', employees.length, 'total employees');
|
||||||
).sort((a, b) => a.name.localeCompare(b.name))
|
console.log('Search query:', searchQuery);
|
||||||
: employees.filter(emp => emp.id === user?.uid);
|
console.log('Current user is owner:', currentUserIsOwner);
|
||||||
|
|
||||||
const handleCompanyReportSelect = () => {
|
const handleCompanyReportSelect = () => {
|
||||||
if (companyReport) {
|
if (companyReport) {
|
||||||
@@ -171,7 +335,7 @@ const Reports: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Middle Section - Employee List */}
|
{/* 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="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="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>
|
<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 */}
|
{/* Employee List */}
|
||||||
<div className="self-stretch px-3 flex flex-col justify-start items-start">
|
<div className="self-stretch px-3 flex flex-col justify-start items-start">
|
||||||
{/* Company Report Item */}
|
{/* Company Report Item - Always show for consistency */}
|
||||||
{currentUserIsOwner && (
|
{companyReport && (
|
||||||
<div
|
<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]' : ''
|
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="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">
|
<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>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -224,12 +396,12 @@ const Reports: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={employee.id}
|
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)}
|
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={`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-[--Text-Gray-500] text-xs font-medium font-['Inter'] leading-none">
|
<div className="text-center justify-start text-xs font-medium font-['Inter'] leading-none">
|
||||||
{employee.initials}
|
{employee.initials}
|
||||||
</div>
|
</div>
|
||||||
{/* Status indicator */}
|
{/* Status indicator */}
|
||||||
@@ -265,6 +437,7 @@ const Reports: React.FC = () => {
|
|||||||
onRegenerate={handleGenerateCompanyReport}
|
onRegenerate={handleGenerateCompanyReport}
|
||||||
isGenerating={generatingCompanyReport}
|
isGenerating={generatingCompanyReport}
|
||||||
org={org}
|
org={org}
|
||||||
|
onChatWithAI={handleCompanyChatWithAI}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
@@ -274,8 +447,9 @@ const Reports: React.FC = () => {
|
|||||||
<EmployeeReportContent
|
<EmployeeReportContent
|
||||||
report={employeeReport}
|
report={employeeReport}
|
||||||
employeeName={selectedReport.employeeName!}
|
employeeName={selectedReport.employeeName!}
|
||||||
|
employeeEmail={selectedReport.employeeEmail!}
|
||||||
onGenerateReport={() => {
|
onGenerateReport={() => {
|
||||||
const employee = employees.find(emp => emp.name === selectedReport.employeeName);
|
const employee = employees.find(emp => emp.email === selectedReport.employeeEmail);
|
||||||
if (employee) handleGenerateEmployeeReport(employee);
|
if (employee) handleGenerateEmployeeReport(employee);
|
||||||
}}
|
}}
|
||||||
isGenerating={generatingEmployeeReport === employeeId}
|
isGenerating={generatingEmployeeReport === employeeId}
|
||||||
@@ -283,6 +457,7 @@ const Reports: React.FC = () => {
|
|||||||
showGenerateButton={!reports[employeeId] && !!submissions[employeeId]}
|
showGenerateButton={!reports[employeeId] && !!submissions[employeeId]}
|
||||||
employees={employees}
|
employees={employees}
|
||||||
org={org}
|
org={org}
|
||||||
|
onChatWithAI={() => handleEmployeeChatWithAI(selectedReport.employeeName!, employeeReport)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
@@ -311,7 +486,8 @@ const CompanyReportContent: React.FC<{
|
|||||||
onRegenerate: () => void;
|
onRegenerate: () => void;
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
org?: any;
|
org?: any;
|
||||||
}> = ({ report, onRegenerate, isGenerating, org }) => {
|
onChatWithAI?: () => void;
|
||||||
|
}> = ({ report, onRegenerate, isGenerating, org, onChatWithAI }) => {
|
||||||
// Default to the first department in the array
|
// Default to the first department in the array
|
||||||
const [activeDepartmentTab, setActiveDepartmentTab] = useState(() =>
|
const [activeDepartmentTab, setActiveDepartmentTab] = useState(() =>
|
||||||
report?.gradingBreakdown?.[0]?.departmentNameShort || 'Campaigns'
|
report?.gradingBreakdown?.[0]?.departmentNameShort || 'Campaigns'
|
||||||
@@ -349,6 +525,22 @@ const CompanyReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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
|
<FigmaPrimaryButton
|
||||||
onClick={() => downloadCompanyReportPDF(report, org?.companyName || 'Company')}
|
onClick={() => downloadCompanyReportPDF(report, org?.companyName || 'Company')}
|
||||||
text="Download as PDF"
|
text="Download as PDF"
|
||||||
@@ -361,7 +553,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
||||||
{/* Company Weaknesses */}
|
{/* 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="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 className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Company Weaknesses</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,7 +583,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Personnel Changes */}
|
{/* Personnel Changes */}
|
||||||
{report?.personnelChanges && (
|
{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="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 className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Personnel Changes</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,7 +645,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Hiring Needs */}
|
{/* Hiring Needs */}
|
||||||
{report?.immediateHiringNeeds && report.immediateHiringNeeds.length > 0 && (
|
{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="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 className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Immediate Hiring Needs</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -479,7 +671,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Forward Plan */}
|
{/* Forward Plan */}
|
||||||
{report?.forwardOperatingPlan && (
|
{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="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 className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Forward Plan</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,7 +704,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
Strengths goes here
|
Strengths goes here
|
||||||
*/}
|
*/}
|
||||||
{report?.strengths && (
|
{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="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 className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Strengths</div>
|
||||||
</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="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 className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Organizational Impact Summary</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -675,7 +867,7 @@ const CompanyReportContent: React.FC<{
|
|||||||
|
|
||||||
|
|
||||||
{/* Grading Overview */}
|
{/* 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="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 className="justify-start text-[--Text-Gray-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Grading Overview</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -784,13 +976,15 @@ const CompanyReportContent: React.FC<{
|
|||||||
const EmployeeReportContent: React.FC<{
|
const EmployeeReportContent: React.FC<{
|
||||||
report: EmployeeReport;
|
report: EmployeeReport;
|
||||||
employeeName: string;
|
employeeName: string;
|
||||||
|
employeeEmail: string;
|
||||||
onGenerateReport?: () => void;
|
onGenerateReport?: () => void;
|
||||||
isGenerating?: boolean;
|
isGenerating?: boolean;
|
||||||
hasSubmission?: boolean;
|
hasSubmission?: boolean;
|
||||||
showGenerateButton?: boolean;
|
showGenerateButton?: boolean;
|
||||||
employees?: Employee[];
|
employees?: Employee[];
|
||||||
org?: any;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -823,13 +1017,33 @@ const EmployeeReportContent: React.FC<{
|
|||||||
</button>
|
</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 */}
|
{/* Download PDF Button - only show for actual reports */}
|
||||||
{!showGenerateButton && (
|
{!showGenerateButton && (
|
||||||
<FigmaPrimaryButton
|
<FigmaPrimaryButton
|
||||||
containerExtra='h-9.5'
|
containerExtra='h-9.5'
|
||||||
text="Download as PDF"
|
text="Download as PDF"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const employee = employees.find(emp => emp.name === employeeName);
|
const employee = employees.find(emp => emp.email === employeeName);
|
||||||
if (employee) {
|
if (employee) {
|
||||||
downloadEmployeeReportPDF(employee, report, org?.companyName || 'Company');
|
downloadEmployeeReportPDF(employee, report, org?.companyName || 'Company');
|
||||||
}
|
}
|
||||||
@@ -844,7 +1058,7 @@ const EmployeeReportContent: React.FC<{
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
<div className="self-stretch flex flex-col justify-start items-start gap-4 px-5 pb-6 overflow-y-auto">
|
||||||
{/* Role & Responsibilities */}
|
{/* 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="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 className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Role & Responsibilities</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -857,7 +1071,7 @@ const EmployeeReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Self-Rated Output */}
|
{/* Self-Rated Output */}
|
||||||
{report.roleAndOutput && report.roleAndOutput?.selfRatedOutput && (
|
{report.roleAndOutput && report.roleAndOutput?.selfRatedOutput && (
|
||||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1">
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Self-Rated Output</div>
|
<div className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Self-Rated Output</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -871,7 +1085,7 @@ const EmployeeReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Insights */}
|
{/* Insights */}
|
||||||
{report.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="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 className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Insights & Traits</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -906,7 +1120,7 @@ const EmployeeReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Strengths */}
|
{/* Strengths */}
|
||||||
{report.strengths && report.strengths.length > 0 && (
|
{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="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 className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Strengths</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -930,14 +1144,14 @@ const EmployeeReportContent: React.FC<{
|
|||||||
|
|
||||||
{/* Recommendations */}
|
{/* Recommendations */}
|
||||||
{report.recommendations && report.recommendations.length > 0 && (
|
{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="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 className="justify-start text-[--Text-Dark-950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Recommendations</div>
|
||||||
</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">
|
<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) => (
|
{report.recommendations.map((recommendation, index) => (
|
||||||
<div key={index} className="self-stretch text-[--Text-Gray-800] text-base font-normal font-['Inter'] leading-normal">
|
<div key={index} className="self-stretch text-[--Text-Gray-800] text-base font-normal font-['Inter'] leading-normal">
|
||||||
• {recommendation}
|
<MarkdownRenderer content={recommendation} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { EMPLOYEE_QUESTIONS } from '../employeeQuestions';
|
|||||||
|
|
||||||
interface EmployeeSubmission {
|
interface EmployeeSubmission {
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
answers: Record<string, string>;
|
answers: { question: string; answer: string; }[] | Record<string, any>;
|
||||||
submittedAt: number;
|
submittedAt: number;
|
||||||
employee?: Employee;
|
employee?: Employee;
|
||||||
}
|
}
|
||||||
@@ -29,17 +29,19 @@ const Submissions: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Use the secure API service to get submissions
|
// Use the secure API service to get submissions
|
||||||
const data = await secureApi.getSubmissions();
|
const submissionsArray = await secureApi.getSubmissions();
|
||||||
|
|
||||||
if (data && data.submissions) {
|
if (submissionsArray && submissionsArray.length > 0) {
|
||||||
// Transform submissions to include employee data
|
// Transform submissions array to include employee data
|
||||||
const submissionsWithEmployees: Record<string, EmployeeSubmission> = {};
|
const submissionsWithEmployees: Record<string, EmployeeSubmission> = {};
|
||||||
|
|
||||||
Object.entries(data.submissions).forEach(([employeeId, submission]) => {
|
submissionsArray.forEach((submission) => {
|
||||||
const employee = employees.find(emp => emp.id === employeeId);
|
const employee = employees.find(emp => emp.id === submission.employeeId);
|
||||||
if (employee) {
|
if (employee) {
|
||||||
submissionsWithEmployees[employeeId] = {
|
submissionsWithEmployees[submission.employeeId] = {
|
||||||
...submission as EmployeeSubmission,
|
employeeId: submission.employeeId,
|
||||||
|
answers: submission.answers,
|
||||||
|
submittedAt: Date.now(), // Will be added from DB later
|
||||||
employee
|
employee
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -80,24 +82,31 @@ const Submissions: React.FC = () => {
|
|||||||
employees.forEach((employee, index) => {
|
employees.forEach((employee, index) => {
|
||||||
if (index < 3) { // Only add submissions for first 3 employees
|
if (index < 3) { // Only add submissions for first 3 employees
|
||||||
console.log(employee);
|
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] = {
|
demoSubmissions[employee.id] = {
|
||||||
employeeId: employee.id,
|
employeeId: employee.id,
|
||||||
employee,
|
employee,
|
||||||
submittedAt: Date.now() - (index * 86400000), // Stagger dates
|
submittedAt: Date.now() - (index * 86400000), // Stagger dates
|
||||||
answers: {
|
answers: demoAnswers
|
||||||
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.'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -142,52 +151,57 @@ const Submissions: React.FC = () => {
|
|||||||
const submission = submissions[selectedEmployee.id];
|
const submission = submissions[selectedEmployee.id];
|
||||||
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
|
const questionsAndAnswers: Array<{ question: string; answer: string; isLong?: boolean }> = [];
|
||||||
|
|
||||||
// Handle different submission formats
|
|
||||||
let submissionAnswers: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (submission.answers) {
|
if (submission.answers) {
|
||||||
if (Array.isArray(submission.answers)) {
|
if (Array.isArray(submission.answers)) {
|
||||||
// If answers is an array of {question, answer} objects
|
// Handle array format from database: [{question, answer}, ...]
|
||||||
submissionAnswers = submission.answers.reduce((acc, item: any) => {
|
submission.answers.forEach((item: any) => {
|
||||||
if (item.question && item.answer) {
|
if (item.question && item.answer && item.answer.trim()) {
|
||||||
acc[item.question] = item.answer;
|
questionsAndAnswers.push({
|
||||||
|
question: item.question,
|
||||||
|
answer: item.answer,
|
||||||
|
isLong: item.answer.length > 150
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return acc;
|
});
|
||||||
}, {} as Record<string, string>);
|
} else if (typeof submission.answers === 'object') {
|
||||||
} else {
|
// Handle object format: {questionId: answer, ...}
|
||||||
// If answers is already a key-value object
|
const submissionAnswers = submission.answers as Record<string, any>;
|
||||||
submissionAnswers = submission.answers as Record<string, string>;
|
|
||||||
|
// 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;
|
return questionsAndAnswers;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,7 +220,7 @@ const Submissions: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Middle Section - Employee List */}
|
{/* 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="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="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>
|
<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