update 9/23

This commit is contained in:
Ra
2025-09-23 09:53:05 -07:00
parent b41f489fe6
commit 5b5bf76c9a
22 changed files with 8769 additions and 8316 deletions

18
.gitignore vendored
View File

@@ -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

View File

@@ -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=="],

View File

@@ -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
};

File diff suppressed because it is too large Load Diff

View File

@@ -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
};

View File

@@ -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();

View File

@@ -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[]
);

View File

@@ -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
};

View File

@@ -7,11 +7,15 @@
"name": "functions", "name": "functions",
"dependencies": { "dependencies": {
"@google-cloud/vertexai": "^1.10.0", "@google-cloud/vertexai": "^1.10.0",
"firebase-admin": "^12.7.0", "@neondatabase/serverless": "0.9.5",
"firebase-admin": "^13.5.0",
"firebase-functions": "^6.4.0", "firebase-functions": "^6.4.0",
"nodemailer": "6.10.1",
"pg": "^8.16.3",
"stripe": "^18.5.0" "stripe": "^18.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/pg": "^8.11.6",
"firebase-functions-test": "^3.4.1" "firebase-functions-test": "^3.4.1"
}, },
"engines": { "engines": {
@@ -612,88 +616,104 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@firebase/app-check-interop-types": { "node_modules/@firebase/app-check-interop-types": {
"version": "0.3.2", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz",
"integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==", "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@firebase/app-types": { "node_modules/@firebase/app-types": {
"version": "0.9.2", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
"integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@firebase/auth-interop-types": { "node_modules/@firebase/auth-interop-types": {
"version": "0.2.3", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz",
"integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==", "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@firebase/component": { "node_modules/@firebase/component": {
"version": "0.6.9", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.9.tgz", "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz",
"integrity": "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==", "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@firebase/util": "1.10.0", "@firebase/util": "1.13.0",
"tslib": "^2.1.0" "tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
} }
}, },
"node_modules/@firebase/database": { "node_modules/@firebase/database": {
"version": "1.0.8", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz",
"integrity": "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==", "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@firebase/app-check-interop-types": "0.3.2", "@firebase/app-check-interop-types": "0.3.3",
"@firebase/auth-interop-types": "0.2.3", "@firebase/auth-interop-types": "0.2.4",
"@firebase/component": "0.6.9", "@firebase/component": "0.7.0",
"@firebase/logger": "0.4.2", "@firebase/logger": "0.5.0",
"@firebase/util": "1.10.0", "@firebase/util": "1.13.0",
"faye-websocket": "0.11.4", "faye-websocket": "0.11.4",
"tslib": "^2.1.0" "tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
} }
}, },
"node_modules/@firebase/database-compat": { "node_modules/@firebase/database-compat": {
"version": "1.0.8", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz",
"integrity": "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==", "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.9", "@firebase/component": "0.7.0",
"@firebase/database": "1.0.8", "@firebase/database": "1.1.0",
"@firebase/database-types": "1.0.5", "@firebase/database-types": "1.0.16",
"@firebase/logger": "0.4.2", "@firebase/logger": "0.5.0",
"@firebase/util": "1.10.0", "@firebase/util": "1.13.0",
"tslib": "^2.1.0" "tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
} }
}, },
"node_modules/@firebase/database-types": { "node_modules/@firebase/database-types": {
"version": "1.0.5", "version": "1.0.16",
"resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz",
"integrity": "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==", "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@firebase/app-types": "0.9.2", "@firebase/app-types": "0.9.3",
"@firebase/util": "1.10.0" "@firebase/util": "1.13.0"
} }
}, },
"node_modules/@firebase/logger": { "node_modules/@firebase/logger": {
"version": "0.4.2", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz",
"integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
} }
}, },
"node_modules/@firebase/util": { "node_modules/@firebase/util": {
"version": "1.10.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz",
"integrity": "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==", "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==",
"hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
} }
}, },
"node_modules/@google-cloud/firestore": { "node_modules/@google-cloud/firestore": {
@@ -1393,6 +1413,83 @@
"url": "https://opencollective.com/js-sdsl" "url": "https://opencollective.com/js-sdsl"
} }
}, },
"node_modules/@neondatabase/serverless": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.9.5.tgz",
"integrity": "sha512-siFas6gItqv6wD/pZnvdu34wEqgG3nSE6zWZdq5j2DEsa+VvX8i/5HXJOo06qrw5axPXn+lGCxeR+NLaSPIXug==",
"license": "MIT",
"dependencies": {
"@types/pg": "8.11.6"
}
},
"node_modules/@neondatabase/serverless/node_modules/@types/pg": {
"version": "8.11.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
"integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^4.0.1"
}
},
"node_modules/@neondatabase/serverless/node_modules/pg-types": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.1.0.tgz",
"integrity": "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==",
"license": "MIT",
"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"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@neondatabase/serverless/node_modules/postgres-array": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/@neondatabase/serverless/node_modules/postgres-bytea": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
"license": "MIT",
"dependencies": {
"obuf": "~1.1.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@neondatabase/serverless/node_modules/postgres-date": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/@neondatabase/serverless/node_modules/postgres-interval": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/@opentelemetry/api": { "node_modules/@opentelemetry/api": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -1722,6 +1819,18 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/pg": {
"version": "8.15.5",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -2985,8 +3094,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
@@ -3101,27 +3209,29 @@
} }
}, },
"node_modules/firebase-admin": { "node_modules/firebase-admin": {
"version": "12.7.0", "version": "13.5.0",
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.5.0.tgz",
"integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", "integrity": "sha512-QZOpv1DJRJpH8NcWiL1xXE10tw3L/bdPFlgjcWrqU3ufyOJDYfxB1MMtxiVTwxK16NlybQbEM6ciSich2uWEIQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fastify/busboy": "^3.0.0", "@fastify/busboy": "^3.0.0",
"@firebase/database-compat": "1.0.8", "@firebase/database-compat": "^2.0.0",
"@firebase/database-types": "1.0.5", "@firebase/database-types": "^1.0.6",
"@types/node": "^22.0.1", "@types/node": "^22.8.7",
"farmhash-modern": "^1.1.0", "farmhash-modern": "^1.1.0",
"fast-deep-equal": "^3.1.1",
"google-auth-library": "^9.14.2",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"uuid": "^10.0.0" "uuid": "^11.0.2"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@google-cloud/firestore": "^7.7.0", "@google-cloud/firestore": "^7.11.0",
"@google-cloud/storage": "^7.7.0" "@google-cloud/storage": "^7.14.0"
} }
}, },
"node_modules/firebase-functions": { "node_modules/firebase-functions": {
@@ -5034,6 +5144,15 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -5090,6 +5209,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"license": "MIT"
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -5289,6 +5414,104 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-numeric": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5336,6 +5559,51 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-range": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
"license": "MIT"
},
"node_modules/pretty-format": { "node_modules/pretty-format": {
"version": "30.0.5", "version": "30.0.5",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
@@ -5820,6 +6088,15 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -6366,16 +6643,16 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "10.0.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [ "funding": [
"https://github.com/sponsors/broofa", "https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
@@ -6528,6 +6805,15 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>

View 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;

View File

@@ -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: '',
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); setInviteLink(result.inviteLink);
// if (process.env.SENDGRID_API_KEY) { // if (process.env.SENDGRID_API_KEY) {
// setEmailLink(result.emailLink); // setEmailLink(result.emailLink);
// } // }
setInviteForm({ name: '', email: '', role: '', department: '' }); }
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>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Name</label>
<input
type="text"
value={inviteForm.name}
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Employee name"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Email</label>
<input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="employee@company.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Role</label>
<input
type="text"
value={inviteForm.role}
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Software Engineer"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Department</label>
<input
type="text"
value={inviteForm.department}
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Engineering"
/>
</div>
{(inviteLink || emailLink) && (
<div className="space-y-3">
{emailLink && (
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">
Email Link (for GET requests)
</label>
<div className="flex space-x-2">
<input
type="text"
value={emailLink}
readOnly
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
/>
<Button size="sm" onClick={() => copyToClipboard(emailLink)}>
<CopyIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
{inviteLink && (
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">
App Link (direct)
</label>
<div className="flex space-x-2">
<input
type="text"
value={inviteLink}
readOnly
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
/>
<Button size="sm" onClick={() => copyToClipboard(inviteLink)}>
<CopyIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
</div>
)}
</div>
<div className="flex space-x-2 mt-6">
<Button
variant="secondary"
className="flex-1"
onClick={() => {
setShowInviteModal(false); setShowInviteModal(false);
setInviteLink(''); setInviteLink('');
setInviteForm({ name: '', email: '', role: '', department: '' }); setEmailLink('');
}} }}
> onInvite={handleInvite}
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>

View File

@@ -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"

View File

@@ -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">

View File

@@ -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([]);
}; };

View File

@@ -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) {

View File

@@ -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">
<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>
) : (
<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>

View File

@@ -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();
console.log('Company report history loaded:', history.length, 'reports');
if (history.length > 0) { if (history.length > 0) {
console.log('Setting existing company report as default');
setCompanyReport(history[0]); setCompanyReport(history[0]);
// Auto-select company report by default // Auto-select company report by default
setSelectedReport({ report: history[0], type: 'company' }); setSelectedReport({ report: history[0], type: 'company' });
} else { } else {
// FIXED: No automatic generation - only load existing reports console.log('No company reports found, setting placeholder report');
// Use sample data when no real reports exist // Create a placeholder that shows generate button
console.log('No company reports found, using sample data. Click "Refresh Report" to generate a new one.'); const placeholderReport = {
setCompanyReport(SAMPLE_COMPANY_REPORT); ...SAMPLE_COMPANY_REPORT,
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' }); isPlaceholder: true
} as CompanyReport;
setCompanyReport(placeholderReport);
setSelectedReport({ report: placeholderReport, type: 'company' });
} }
} catch (error) { } catch (error) {
console.error('Failed to load company report:', error); console.error('Failed to load company report:', error);
setCompanyReport(SAMPLE_COMPANY_REPORT); // On error, show placeholder with generate option
setSelectedReport({ report: SAMPLE_COMPANY_REPORT, type: 'company' }); 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>

View File

@@ -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,51 +151,56 @@ 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,
return acc; answer: item.answer,
}, {} as Record<string, string>); isLong: item.answer.length > 150
} else { });
// If answers is already a key-value object
submissionAnswers = submission.answers as Record<string, string>;
}
} }
});
} else if (typeof submission.answers === 'object') {
// Handle object format: {questionId: answer, ...}
const submissionAnswers = submission.answers as Record<string, any>;
// If we have structured answers, map them to questions // Try to match with EMPLOYEE_QUESTIONS first to get proper question text
if (Object.keys(submissionAnswers).length > 0) {
// Try to match with EMPLOYEE_QUESTIONS first
EMPLOYEE_QUESTIONS.forEach(q => { EMPLOYEE_QUESTIONS.forEach(q => {
const answer = submissionAnswers[q.id]; const answer = submissionAnswers[q.id];
if (answer && answer.trim()) { if (answer && (typeof answer === 'string' ? answer.trim() : answer)) {
const answerText = typeof answer === 'string' ? answer :
typeof answer === 'boolean' ? (answer ? 'Yes' : 'No') :
String(answer);
questionsAndAnswers.push({ questionsAndAnswers.push({
question: q.prompt, question: q.prompt,
answer: answer, answer: answerText,
isLong: answer.length > 150 isLong: answerText.length > 150
}); });
} }
}); });
// Add any additional answers not in EMPLOYEE_QUESTIONS // Add any additional answers not in EMPLOYEE_QUESTIONS
Object.entries(submissionAnswers).forEach(([key, answer]) => { 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)) { 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 // Format the key as a readable question
const formattedQuestion = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); const formattedQuestion = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
questionsAndAnswers.push({ questionsAndAnswers.push({
question: formattedQuestion, question: formattedQuestion,
answer: answer, answer: answerText,
isLong: answer.length > 150 isLong: answerText.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>