diff --git a/.gitignore b/.gitignore index 3940390..2def1a2 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,5 @@ dist-ssr /deploy-security.sh /EMPLOYEE_FORMS_FIGMA_README.md /TODOS.md -/SECURITY_MIGRATION.md \ No newline at end of file +/SECURITY_MIGRATION.md +/employee_report_schema.json \ No newline at end of file diff --git a/employee_report_schema.json b/employee_report_schema.json deleted file mode 100644 index ab793d7..0000000 --- a/employee_report_schema.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "type": "object", - "properties": { - "employeeId": { - "type": "string" - }, - "department": { - "type": "string" - }, - "role": { - "type": "string" - }, - "roleAndOutput": { - "type": "object", - "properties": { - "responsibilities": { - "type": "string", - "examples": [ - "Recruiting influencers, onboarding, campaign support, business development." - ] - }, - "clarityOnRole": { - "type": "string", - "examples": [ - "10/10 – Feels very clear on responsibilities." - ] - }, - "selfRatedOutput": { - "type": "string", - "examples": [ - "7/10 – Indicates decent performance but room to grow." - ] - }, - "recurringTasks": { - "type": "string", - "examples": [ - "Influencer outreach, onboarding, communications." - ] - } - } - }, - "insights": { - "type": "object", - "properties": { - "personalityInsights": { - "type": "string", - "examples": [ - "Loyal, well-liked by influencers, eager to grow, client-facing interest." - ] - }, - "psychologicalIndicators": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Scores high on optimism and external motivation.", - "Shows ambition but lacks self-discipline in execution.", - "Displays a desire for recognition and community; seeks more appreciation." - ] - ] - }, - "selfAwareness": { - "type": "string", - "examples": [ - "High – acknowledges weaknesses like lateness and disorganization." - ] - }, - "emotionalResponses": { - "type": "string", - "examples": [ - "Frustrated by campaign disorganization; would prefer closer collaboration." - ] - }, - "growthDesire": { - "type": "string", - "examples": [ - "Interested in becoming more client-facing and shifting toward biz dev." - ] - } - } - }, - "strengths": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Builds strong relationships with influencers.", - "Has sales and outreach potential.", - "Loyal, driven, and values-aligned with the company mission.", - "Open to feedback and self-improvement." - ] - ] - }, - "weaknessess": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - "Critical Issue: Disorganized and late with deliverables — confirmed by previous internal notes.", - "Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.", - "May unintentionally cause friction with campaigns team by stepping outside process boundaries." - ] - }, - "opportunities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - } - } - }, - "examples": [ - [ - { - "title": "Role Adjustment", - "description": "Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities." - }, - { - "title": "Accountability Support", - "description": "Pair with a high-output implementer (new hire) to balance Gentry’s strategic skills." - } - ] - ] - }, - "risks": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Without strict structure, Gentry’s performance will stay flat or become a bottleneck.", - "If kept in a dual-role (recruiting + outreach), productivity will suffer.", - "He needs system constraints and direct oversight to stay focused." - ] - ] - }, - "recommendations": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.", - "Pair him with a new hire who is ultra-organized and can execute on Gentry’s deals." - ] - ] - } - } -} \ No newline at end of file diff --git a/functions/bun.lock b/functions/bun.lock index f8e2502..08afdc3 100644 --- a/functions/bun.lock +++ b/functions/bun.lock @@ -4,9 +4,9 @@ "": { "name": "auditly-functions", "dependencies": { + "@google-cloud/vertexai": "^1.4.0", "firebase-admin": "^12.1.1", "firebase-functions": "^5.0.1", - "openai": "^5.12.2", "stripe": "^18.4.0", }, "devDependencies": { @@ -123,6 +123,8 @@ "@google-cloud/storage": ["@google-cloud/storage@7.16.0", "", { "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", "uuid": "^8.0.0" } }, "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw=="], + "@google-cloud/vertexai": ["@google-cloud/vertexai@1.10.0", "", { "dependencies": { "google-auth-library": "^9.1.0" } }, "sha512-HqYqoivNtkq59po8m7KI0n+lWKdz4kabENncYQXZCX/hBWJfXtKAfR/2nUQsP+TwSfHKoA7zDL2RrJYIv/j3VQ=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], @@ -677,11 +679,11 @@ "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], - "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=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jwks-rsa": ["jwks-rsa@3.2.0", "", { "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww=="], - "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], @@ -777,8 +779,6 @@ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "openai": ["openai@5.12.2", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], @@ -1007,16 +1007,14 @@ "gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], - "google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "gtoken/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], - "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "jwks-rsa/@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1055,9 +1053,7 @@ "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "google-auth-library/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "gtoken/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + "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=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/functions/index.js b/functions/index.js index 58895fb..51fe624 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,16 +1,443 @@ const { onRequest } = require("firebase-functions/v2/https"); +const { setGlobalOptions } = require("firebase-functions/v2"); const admin = require("firebase-admin"); -const OpenAI = require("openai"); +const { VertexAI } = require('@google-cloud/vertexai'); const Stripe = require("stripe"); -const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json"); +// Set global options for all functions to use us-central1 region +setGlobalOptions({ region: 'us-central1', cors: true }); + +const serviceAccount = require("./auditly-consulting-firebase-adminsdk-fbsvc-e4b51ef5cf.json"); +// const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json") admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); + +// region Interface Clients + const db = admin.firestore(); -// Auth middleware function to validate tokens and extract user context +// Initialize Vertex AI with your project ID +// This automatically uses IAM authentication from the service account +const vertexAI = new VertexAI({ + project: 'auditly-consulting', // Using the project ID from the service account + location: 'us-central1' +}); + +// Get the Gemini model +const geminiModel = vertexAI.getGenerativeModel({ + model: 'gemini-2.5-flash', + generationConfig: { + maxOutputTokens: 8192, + temperature: 0.7, + topP: 0.8, + topK: 10 + } +}); + +// Initialize Stripe if API key is available +const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: '2024-11-20.acacia', +}) : null; + +// endregion + +// region Constants + +const RESPONSE_FORMAT_EMPLOYEE = { + type: "object", + properties: { + employeeId: { + type: "string" + }, + department: { + type: "string" + }, + role: { + type: "string" + }, + roleAndOutput: { + type: "object", + properties: { + responsibilities: { + type: "string", + examples: [ + "Recruiting influencers, onboarding, campaign support, business development." + ] + }, + clarityOnRole: { + type: "string", + examples: [ + "10/10 – Feels very clear on responsibilities." + ] + }, + selfRatedOutput: { + type: "string", + examples: [ + "7/10 – Indicates decent performance but room to grow." + ] + }, + recurringTasks: { + type: "string", + examples: [ + "Influencer outreach, onboarding, communications." + ] + } + } + }, + insights: { + type: "object", + properties: { + personalityInsights: { + type: "string", + examples: [ + "Loyal, well-liked by influencers, eager to grow, client-facing interest." + ] + }, + psychologicalIndicators: { + type: "array", + items: { + type: "string" + }, + examples: [ + [ + "Scores high on optimism and external motivation.", + "Shows ambition but lacks self-discipline in execution.", + "Displays a desire for recognition and community; seeks more appreciation." + ] + ] + }, + selfAwareness: { + type: "string", + examples: [ + "High – acknowledges weaknesses like lateness and disorganization." + ] + }, + emotionalResponses: { + type: "string", + examples: [ + "Frustrated by campaign disorganization; would prefer closer collaboration." + ] + }, + growthDesire: { + type: "string", + examples: [ + "Interested in becoming more client-facing and shifting toward biz dev." + ] + } + } + }, + strengths: { + type: "array", + items: { + type: "string" + }, + examples: [ + [ + "Builds strong relationships with influencers.", + "Has sales and outreach potential.", + "Loyal, driven, and values-aligned with the company mission.", + "Open to feedback and self-improvement." + ] + ] + }, + weaknesses: { + type: "array", + items: { + type: "string" + }, + examples: [ + "Critical Issue: Disorganized and late with deliverables — confirmed by previous internal notes.", + "Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.", + "May unintentionally cause friction with campaigns team by stepping outside process boundaries." + ] + }, + opportunities: { + type: "array", + items: { + type: "object", + properties: { + title: { + type: "string" + }, + description: { + type: "string" + } + } + }, + examples: [ + [ + { + title: "Role Adjustment", + description: "Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities." + }, + { + title: "Accountability Support", + description: "Pair with a high-output implementer (new hire) to balance Gentry’s strategic skills." + } + ] + ] + }, + risks: { + type: "array", + items: { + type: "string" + }, + examples: [ + [ + "Without strict structure, Gentry’s performance will stay flat or become a bottleneck.", + "If kept in a dual-role (recruiting + outreach), productivity will suffer.", + "He needs system constraints and direct oversight to stay focused." + ] + ] + }, + recommendations: { + type: "array", + items: { + type: "string" + }, + examples: [ + [ + "Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.", + "Pair him with a new hire who is ultra-organized and can execute on Gentry’s deals." + ] + ] + }, + gradingOverview: { + grade: { type: "string" }, + reliability: { type: "number" }, + roleFit: { type: "number" }, + scalability: { type: "number" }, + output: { type: "number" }, + initiative: { type: "number" } + } + } +} + +const RESPONSE_FORMAT_COMPANY = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CompanyReport", + "type": "object", + "properties": { + id: { "type": "string" }, + createdAt: { "type": "number" }, + overview: { + type: "object", + properties: { + totalEmployees: { type: "number" }, + departmentBreakdown: { + type: "array", + items: { + type: "object", + properties: { + department: { type: "string" }, + count: { type: "number" } + }, + required: ["department", "count"] + } + }, + submissionRate: { type: "number" }, + lastUpdated: { type: "number" }, + averagePerformanceScore: { type: "number" }, + riskLevel: { + type: "string", + enum: ["Low", "Medium", "High"] + } + }, + required: ["totalEmployees", "departmentBreakdown", "submissionRate", "lastUpdated"] + }, + weaknesses: { + type: "array", + items: { + type: "object", + properties: { + title: { type: "string" }, + description: { type: "string" } + }, + required: ["title", "description"] + } + }, + personnelChanges: { + type: "object", + properties: { + newHires: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + department: { type: "string" }, + role: { type: "string" }, + impact: { type: "string" } + }, + required: ["name", "department", "role"] + } + }, + promotions: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + fromRole: { type: "string" }, + toRole: { type: "string" }, + impact: { type: "string" } + }, + required: ["name", "fromRole", "toRole"] + } + }, + departures: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + department: { type: "string" }, + reason: { type: "string" }, + impact: { type: "string" } + }, + required: ["name", "department", "reason"] + } + } + }, + required: ["newHires", "promotions", "departures"] + }, + immediateHiringNeeds: { + type: "array", + items: { + type: "object", + properties: { + department: { type: "string" }, + role: { type: "string" }, + priority: { + type: "string", + enum: ["High", "Medium", "Low"] + }, + reasoning: { type: "string" }, + urgency: { + type: "string", + enum: ["high", "medium", "low"] + } + }, + required: ["department", "role", "priority", "reasoning"] + } + }, + forwardOperatingPlan: { + type: "array", + items: { + type: "object", + properties: { + title: { type: "string" }, + details: { + type: "array", + items: { type: "string" } + } + }, + required: ["title", "details"] + } + }, + strengths: { + type: "array", + items: { type: "string" } + }, + organizationalImpactSummary: { + type: "array", + items: { + type: "object", + properties: { + category: { + type: "string", + enum: [ + "Mission Critical", + "Highly Valuable", + "Core Support", + "Low Criticality" + ] + }, + employees: { + type: "array", + items: { + type: "object", + properties: { + employeeName: { type: "string" }, + impact: { type: "string" }, + description: { type: "string" }, + suggestedPay: { type: "string", description: "Suggested yearly wage for the employee", example: "$70,000" } + }, + required: ["employeeName", "impact", "description", "suggestedPay"] + } + } + }, + required: ["category", "employees"] + } + }, + gradingBreakdown: { + type: "array", + items: { + type: "object", + properties: { + departmentNameShort: { type: "string" }, + departmentName: { type: "string" }, + lead: { type: "string" }, + support: { type: "string" }, + departmentGrade: { type: "string" }, + executiveSummary: { type: "string" }, + teamScores: { + type: "array", + items: { + type: "object", + properties: { + employeeName: { type: "string" }, + grade: { type: "string" }, + reliability: { type: "number" }, + roleFit: { type: "number" }, + scalability: { type: "number" }, + output: { type: "number" }, + initiative: { type: "number" } + }, + required: [ + "employeeName", + "grade", + "reliability", + "roleFit", + "scalability", + "output", + "initiative" + ] + } + } + }, + required: [ + "departmentNameShort", + "departmentName", + "lead", + "support", + "departmentGrade", + "executiveSummary", + "teamScores" + ] + } + }, + executiveSummary: { type: "string" } + }, + required: [ + "id", + "createdAt", + "overview", + "weaknesses", + "personnelChanges", + "immediateHiringNeeds", + "strengths", + "gradingBreakdown", + "executiveSummary" + ] +} + +// endregion + +// region Helper Functions + const validateAuthAndGetContext = async (req) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { @@ -57,427 +484,146 @@ const validateAuthAndGetContext = async (req) => { }; }; -// Helper function to verify user has access to specific organization -const verifyOrgAccess = (authContext, targetOrgId) => { - if (!targetOrgId) { - return authContext.orgId; // Use default org - } - - if (!authContext.orgIds.includes(targetOrgId)) { - throw new Error('Unauthorized access to organization'); - } - - return targetOrgId; -}; - -// Initialize OpenAI if API key is available -const openai = process.env.OPENAI_API_KEY ? new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, -}) : null; - -// Initialize Stripe if API key is available -const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { - apiVersion: '2024-11-20.acacia', -}) : null; - -const RESPONSE_FORMAT_EMPLOYEE = { - "type": "object", - "properties": { - "employeeId": { - "type": "string" - }, - "department": { - "type": "string" - }, - "role": { - "type": "string" - }, - "roleAndOutput": { - "type": "object", - "properties": { - "responsibilities": { - "type": "string", - "examples": [ - "Recruiting influencers, onboarding, campaign support, business development." - ] - }, - "clarityOnRole": { - "type": "string", - "examples": [ - "10/10 – Feels very clear on responsibilities." - ] - }, - "selfRatedOutput": { - "type": "string", - "examples": [ - "7/10 – Indicates decent performance but room to grow." - ] - }, - "recurringTasks": { - "type": "string", - "examples": [ - "Influencer outreach, onboarding, communications." - ] - } - } - }, - "insights": { - "type": "object", - "properties": { - "personalityInsights": { - "type": "string", - "examples": [ - "Loyal, well-liked by influencers, eager to grow, client-facing interest." - ] - }, - "psychologicalIndicators": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Scores high on optimism and external motivation.", - "Shows ambition but lacks self-discipline in execution.", - "Displays a desire for recognition and community; seeks more appreciation." - ] - ] - }, - "selfAwareness": { - "type": "string", - "examples": [ - "High – acknowledges weaknesses like lateness and disorganization." - ] - }, - "emotionalResponses": { - "type": "string", - "examples": [ - "Frustrated by campaign disorganization; would prefer closer collaboration." - ] - }, - "growthDesire": { - "type": "string", - "examples": [ - "Interested in becoming more client-facing and shifting toward biz dev." - ] - } - } - }, - "strengths": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Builds strong relationships with influencers.", - "Has sales and outreach potential.", - "Loyal, driven, and values-aligned with the company mission.", - "Open to feedback and self-improvement." - ] - ] - }, - "weaknessess": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - "Critical Issue: Disorganized and late with deliverables — confirmed by previous internal notes.", - "Poor implementation and recruiting output — does not effectively close the loop on influencer onboarding.", - "May unintentionally cause friction with campaigns team by stepping outside process boundaries." - ] - }, - "opportunities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - } - } - }, - "examples": [ - [ - { - "title": "Role Adjustment", - "description": "Shift fully to Influencer Manager & Biz Dev Outreach as planned. Remove all execution and recruitment responsibilities." - }, - { - "title": "Accountability Support", - "description": "Pair with a high-output implementer (new hire) to balance Gentry’s strategic skills." - } - ] - ] - }, - "risks": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Without strict structure, Gentry’s performance will stay flat or become a bottleneck.", - "If kept in a dual-role (recruiting + outreach), productivity will suffer.", - "He needs system constraints and direct oversight to stay focused." - ] - ] - }, - "recommendations": { - "type": "array", - "items": { - "type": "string" - }, - "examples": [ - [ - "Keep. But immediately restructure his role: Remove recruiting and logistical tasks. Focus only on influencer relationship-building, pitching, and business development.", - "Pair him with a new hire who is ultra-organized and can execute on Gentry’s deals." - ] - ] - }, - "gradingOverview": { - "grade": { "type": "string" }, - "reliability": { "type": "number" }, - "roleFit": { "type": "number" }, - "scalability": { "type": "number" }, - "output": { "type": "number" }, - "initiative": { "type": "number" } - } - } -} - -RESPONSE_FORMAT_COMPANY = { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CompanyReport", - "type": "object", - "properties": { - "id": { "type": "string" }, - "createdAt": { "type": "number" }, - "overview": { - "type": "object", - "properties": { - "totalEmployees": { "type": "number" }, - "departmentBreakdown": { - "type": "array", - "items": { - "type": "object", - "properties": { - "department": { "type": "string" }, - "count": { "type": "number" } - }, - "required": ["department", "count"] - } - }, - "submissionRate": { "type": "number" }, - "lastUpdated": { "type": "number" }, - "averagePerformanceScore": { "type": "number" }, - "riskLevel": { - "type": "string", - "enum": ["Low", "Medium", "High"] - } - }, - "required": ["totalEmployees", "departmentBreakdown", "submissionRate", "lastUpdated"] - }, - "weaknesses": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { "type": "string" }, - "description": { "type": "string" } - }, - "required": ["title", "description"] - } - }, - "personnelChanges": { - "type": "object", - "properties": { - "newHires": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "department": { "type": "string" }, - "role": { "type": "string" }, - "impact": { "type": "string" } - }, - "required": ["name", "department", "role"] - } - }, - "promotions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "fromRole": { "type": "string" }, - "toRole": { "type": "string" }, - "impact": { "type": "string" } - }, - "required": ["name", "fromRole", "toRole"] - } - }, - "departures": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "department": { "type": "string" }, - "reason": { "type": "string" }, - "impact": { "type": "string" } - }, - "required": ["name", "department", "reason"] - } - } - }, - "required": ["newHires", "promotions", "departures"] - }, - "immediateHiringNeeds": { - "type": "array", - "items": { - "type": "object", - "properties": { - "department": { "type": "string" }, - "role": { "type": "string" }, - "priority": { - "type": "string", - "enum": ["High", "Medium", "Low"] - }, - "reasoning": { "type": "string" }, - "urgency": { - "type": "string", - "enum": ["high", "medium", "low"] - } - }, - "required": ["department", "role", "priority", "reasoning"] - } - }, - "forwardOperatingPlan": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { "type": "string" }, - "details": { - "type": "array", - "items": { "type": "string" } - } - }, - "required": ["title", "details"] - } - }, - "strengths": { - "type": "array", - "items": { "type": "string" } - }, - "organizationalImpactSummary": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "enum": [ - "Mission Critical", - "Highly Valuable", - "Core Support", - "Low Criticality" - ] - }, - "employees": { - "type": "array", - "items": { - "type": "object", - "properties": { - "employeeName": { "type": "string" }, - "impact": { "type": "string" }, - "description": { "type": "string" }, - "suggestedPay": { "type": "string", "description": "Suggested yearly wage for the employee", "example": "$70,000" } - }, - "required": ["employeeName", "impact", "description", "suggestedPay"] - } - } - }, - "required": ["category", "employees"] - } - }, - "gradingBreakdown": { - "type": "array", - "items": { - "type": "object", - "properties": { - "departmentNameShort": { "type": "string" }, - "departmentName": { "type": "string" }, - "lead": { "type": "string" }, - "support": { "type": "string" }, - "departmentGrade": { "type": "string" }, - "executiveSummary": { "type": "string" }, - "teamScores": { - "type": "array", - "items": { - "type": "object", - "properties": { - "employeeName": { "type": "string" }, - "grade": { "type": "string" }, - "reliability": { "type": "number" }, - "roleFit": { "type": "number" }, - "scalability": { "type": "number" }, - "output": { "type": "number" }, - "initiative": { "type": "number" } - }, - "required": [ - "employeeName", - "grade", - "reliability", - "roleFit", - "scalability", - "output", - "initiative" - ] - } - } - }, - "required": [ - "departmentNameShort", - "departmentName", - "lead", - "support", - "departmentGrade", - "executiveSummary", - "teamScores" - ] - } - }, - "executiveSummary": { "type": "string" } - }, - "required": [ - "id", - "createdAt", - "overview", - "weaknesses", - "personnelChanges", - "immediateHiringNeeds", - "strengths", - "gradingBreakdown", - "executiveSummary" - ] -} - - -// Helper function to generate OTP const generateOTP = () => { return Math.floor(100000 + Math.random() * 900000).toString(); }; +async function generateWithGemini(prompt, responseFormat = null) { + try { + let fullPrompt = prompt; + + if (responseFormat) { + fullPrompt += `\n\nIMPORTANT: Return ONLY valid JSON that matches this exact schema:\n${JSON.stringify(responseFormat, null, 2)}`; + } + + const result = await geminiModel.generateContent({ + contents: [{ + role: 'user', + parts: [{ text: fullPrompt }] + }] + }); + + const response = result.response; + let content = response.text(); + + // Clean up the response if it has markdown formatting + if (content.includes('```json')) { + content = content.replace(/```json\n?/g, '').replace(/```\n?/g, ''); + } + + // If we expect JSON, parse and validate it + if (responseFormat) { + try { + return JSON.parse(content.trim()); + } catch (parseError) { + console.error('Failed to parse Gemini JSON response:', parseError); + console.error('Raw response:', content); + throw new Error('Invalid JSON response from Gemini'); + } + } + + return content; + } catch (error) { + console.error('Gemini API error:', error); + throw error; + } +} + +async function handleCheckoutCompleted(session) { + const orgId = session.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'trialing', + 'subscription.stripeSubscriptionId': session.subscription, + 'subscription.checkoutSessionId': session.id, + updatedAt: Date.now(), + }); +} + +async function handleSubscriptionCreated(subscription) { + const orgId = subscription.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': subscription.status, + 'subscription.stripeSubscriptionId': subscription.id, + 'subscription.currentPeriodStart': subscription.current_period_start * 1000, + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, + updatedAt: Date.now(), + }); +} + +async function handleSubscriptionUpdated(subscription) { + const orgId = subscription.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': subscription.status, + 'subscription.currentPeriodStart': subscription.current_period_start * 1000, + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, + updatedAt: Date.now(), + }); +} + +async function handleSubscriptionDeleted(subscription) { + const orgId = subscription.metadata?.orgId; + if (!orgId) return; + + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'canceled', + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + updatedAt: Date.now(), + }); +} + +async function handlePaymentSucceeded(invoice) { + const subscriptionId = invoice.subscription; + if (!subscriptionId) return; + + // Update subscription status to active + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + const orgId = subscription.metadata?.orgId; + + if (orgId) { + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'active', + 'subscription.currentPeriodStart': subscription.current_period_start * 1000, + 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, + updatedAt: Date.now(), + }); + } +} + +async function handlePaymentFailed(invoice) { + const subscriptionId = invoice.subscription; + if (!subscriptionId) return; + + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + const orgId = subscription.metadata?.orgId; + + if (orgId) { + const orgRef = db.collection("orgs").doc(orgId); + await orgRef.update({ + 'subscription.status': 'past_due', + updatedAt: Date.now(), + }); + } +} + +// endregion + +// region Routes // Send OTP Function -exports.sendOTP = onRequest({ cors: true }, async (req, res) => { +exports.sendOTP = onRequest(async (req, res) => { if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } @@ -518,7 +664,7 @@ exports.sendOTP = onRequest({ cors: true }, async (req, res) => { }); // Verify OTP Function -exports.verifyOTP = onRequest({ cors: true }, async (req, res) => { +exports.verifyOTP = onRequest(async (req, res) => { if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } @@ -656,7 +802,7 @@ exports.verifyOTP = onRequest({ cors: true }, async (req, res) => { }); // Create Invitation Function -exports.createInvitation = onRequest({ cors: true }, async (req, res) => { +exports.createInvitation = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); @@ -705,6 +851,19 @@ exports.createInvitation = onRequest({ cors: true }, async (req, res) => { .collection("invites") .doc(code); + const inviteReff = await db + .collection("invites") + .doc(code) + .set({ + code, + employee, + email, + orgId, + status: "pending", + createdAt: Date.now(), + expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days + }); + await inviteRef.set({ code, employee, @@ -763,7 +922,7 @@ exports.createInvitation = onRequest({ cors: true }, async (req, res) => { }); // Get Invitation Status Function -exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => { +exports.getInvitationStatus = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -780,17 +939,25 @@ exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => { } try { - const inviteDoc = await db - .collectionGroup("invites") - .where("code", "==", code) - .limit(1) + const inviteDocc = await db + .collection("invites") + .doc(code) .get(); - if (inviteDoc.empty) { + if (!inviteDocc.exists) { return res.status(404).json({ error: "Invitation not found" }); } - const invite = inviteDoc.docs[0].data(); + const invitee = inviteDocc.data(); + + const inviteDoc = await db + .collection("orgs") + .doc(invitee.orgId) + .collection("invites") + .doc(code) + .get(); + + const invite = inviteDoc.data(); // Check if expired if (Date.now() > invite.expiresAt) { @@ -810,7 +977,7 @@ exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => { }); // Consume Invitation Function -exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => { +exports.consumeInvitation = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -827,19 +994,28 @@ exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => { } try { + const inviteSnapshott = await db + .collection("invites") + .doc(code) + .get(); + if (!inviteSnapshott.exists) { + return res.status(404).json({ error: "Invitation not found" }); + } + + const invitee = inviteSnapshott.data(); + const inviteSnapshot = await db - .collectionGroup("invites") - .where("code", "==", code) - .where("status", "==", "pending") - .limit(1) + .collection("orgs") + .doc(invitee.orgId) + .collection("invites") + .doc(code) .get(); - if (inviteSnapshot.empty) { + if (!inviteSnapshot.exists) { return res.status(404).json({ error: "Invitation not found or already used" }); } - const inviteDoc = inviteSnapshot.docs[0]; - const invite = inviteDoc.data(); + const invite = inviteSnapshot.data(); // Check if expired if (Date.now() > invite.expiresAt) { @@ -853,7 +1029,13 @@ exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => { } // Mark invitation as consumed - await inviteDoc.ref.update({ + + await inviteSnapshott.ref.update({ + status: "consumed", + consumedBy: employee.id, + consumedAt: Date.now(), + }); + await inviteSnapshot.ref.update({ status: "consumed", consumedBy: employee.id, consumedAt: Date.now(), @@ -888,7 +1070,7 @@ exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => { }); // Submit Employee Answers Function -exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { +exports.submitEmployeeAnswers = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); @@ -910,20 +1092,27 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { return res.status(400).json({ error: "Invite code and answers are required for invite submissions" }); } + const inviteSnapshott = await db.collection("invites").doc(inviteCode).get(); + + if (!inviteSnapshott.exists) { + return res.status(404).json({ error: "Invitation not found" }); + } + + const invitee = inviteSnapshott.data(); + // Look up the invite to get employee and org data (should be pending, not consumed yet) const inviteSnapshot = await db - .collectionGroup("invites") - .where("code", "==", inviteCode) - .where("status", "==", "pending") - .limit(1) + .collection("orgs") + .doc(invitee.orgId) + .collection("invites") + .doc(inviteCode) .get(); - if (inviteSnapshot.empty) { + if (!inviteSnapshot.exists) { return res.status(404).json({ error: "Invitation not found or already used" }); } - const inviteDoc = inviteSnapshot.docs[0]; - const invite = inviteDoc.data(); + const invite = inviteSnapshot.data(); // Check if expired if (Date.now() > invite.expiresAt) { @@ -933,11 +1122,22 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { finalOrgId = invite.orgId; finalEmployeeId = invite.employee.id; - // Consume the invitation now - await inviteDoc.ref.update({ + invitee.employee.status = "active"; + invite.employee.status = "active"; + + await inviteSnapshott.ref.update({ status: "consumed", consumedBy: finalEmployeeId, consumedAt: Date.now(), + employee: invitee.employee + }); + + // Consume the invitation now + await inviteSnapshot.ref.update({ + status: "consumed", + consumedBy: finalEmployeeId, + consumedAt: Date.now(), + employee: invite.employee }); // Add employee to organization if not already added @@ -1029,10 +1229,10 @@ exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => { // Generate the report using the existing function logic let report; - if (openai) { - // Use OpenAI to generate the report with company context + try { + // Use Gemini to generate the report with company context const prompt = ` -You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema: +You are a cut-and-dry expert business analyst. Analyze the following employee data and generate a comprehensive performance report. Employee Information: - Name: ${employeeData?.name || employeeData?.email || 'Unknown'} @@ -1056,32 +1256,10 @@ Generate a detailed report that: 7. Identifies any risks or concerns 8. Provides numerical grading across key performance areas -Return ONLY valid JSON that matches this structure: -${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)} - Be thorough, professional, and focus on actionable insights. `.trim(); - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - messages: [ - { - role: "system", - content: "You are an expert HR analyst. Generate comprehensive employee performance reports in JSON format that evaluate company alignment and performance." - }, - { - role: "user", - content: prompt - } - ], - response_format: { type: "json_object" }, - temperature: 0.7, - }); - - const aiResponse = completion.choices[0].message.content; - const parsedReport = JSON.parse(aiResponse); - - console.log(parsedReport); + const parsedReport = await generateWithGemini(prompt, RESPONSE_FORMAT_EMPLOYEE); report = { employeeId: finalEmployeeId, @@ -1094,8 +1272,9 @@ Be thorough, professional, and focus on actionable insights. companyContext: companyContext, ...parsedReport }; - } else { - // Fallback to mock report when OpenAI is not available + } catch (aiError) { + console.error("Gemini report generation error:", aiError); + // Fallback to basic report structure report = { employeeId: finalEmployeeId, generatedAt: Date.now(), @@ -1177,7 +1356,7 @@ Be thorough, professional, and focus on actionable insights. }); // Generate Employee Report Function -exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => { +exports.generateEmployeeReport = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); @@ -1197,10 +1376,10 @@ exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => { try { let report; - if (openai) { - // Use OpenAI to generate the report + try { + // Use Gemini to generate the report const prompt = ` -You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema: +You are a cut-and-dry expert business analyst. Analyze the following employee data and generate a comprehensive performance report. Employee Information: - Name: ${employee?.name || employee?.email || 'Unknown'} @@ -1224,30 +1403,10 @@ Generate a detailed report that: 7. Identifies any risks or concerns 8. Provides numerical grading across key performance areas -Return ONLY valid JSON that matches this structure: -${JSON.stringify(RESPONSE_FORMAT_EMPLOYEE, null, 2)} - Be thorough, professional, and focus on actionable insights. `.trim(); - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - messages: [ - { - role: "system", - content: "You are an expert HR analyst. Generate comprehensive employee performance reports in JSON format." - }, - { - role: "user", - content: prompt - } - ], - response_format: { type: "json_object" }, - temperature: 0.7, - }); - - const aiResponse = completion.choices[0].message.content; - const parsedReport = JSON.parse(aiResponse); + const parsedReport = await generateWithGemini(prompt, RESPONSE_FORMAT_EMPLOYEE); report = { employeeId: employee.id, @@ -1255,8 +1414,9 @@ Be thorough, professional, and focus on actionable insights. summary: `AI-generated performance analysis for ${employee.name || employee.email}`, ...parsedReport }; - } else { - // Fallback to mock report when OpenAI is not available + } catch (aiError) { + console.error("Gemini AI error:", aiError); + // Fallback to mock report when Gemini is not available report = { employeeId: employee.id, generatedAt: Date.now(), @@ -1315,7 +1475,7 @@ Be thorough, professional, and focus on actionable insights. }); // Generate Company Wiki Function -exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => { +exports.generateCompanyWiki = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -1348,10 +1508,9 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => { try { let report, wiki; - if (openai) { - // Use OpenAI to generate the company report - - const user = `You are a cut-and-dry expert business analyst who shys to no truths and with get a business in tip-top shape within swiftness. Return ONLY JSON that conforms to the provided schema: + try { + // Use Gemini to generate the company report + const prompt = `You are a cut-and-dry expert business analyst who shys to no truths and will get a business in tip-top shape with swiftness. Analyze the following company data and generate a comprehensive organizational report. Employee Submissions: ${JSON.stringify(submissions, null, 2)} @@ -1366,28 +1525,16 @@ Generate a detailed report that: 4. Doesn't cater to sugarcoating or vague generalities 5. Will beat the nail into the coffin of inefficiency with precise solutions, getting rid of all weak points. -Return ONLY valid JSON that matches this JSON SCHEMA: -${JSON.stringify(RESPONSE_FORMAT_COMPANY, null, 0)} +Be thorough, professional, and focus on actionable insights.`; -Be thorough, professional, and focus on actionable insights. -`; - - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - response_format: { type: "json_object" }, - messages: [ - { role: "user", content: user } - ] - }); - - // content is guaranteed to be schema-conformant JSON - const parsed = JSON.parse(completion.choices[0].message.content); + const parsed = await generateWithGemini(prompt, RESPONSE_FORMAT_COMPANY); report = { generatedAt: Date.now(), ...parsed }; + // Save the report const reportRef = db .collection("orgs") .doc(orgId) @@ -1402,8 +1549,9 @@ Be thorough, professional, and focus on actionable insights. report }); - } else { - // Fallback to mock data when OpenAI is not available + } catch (aiError) { + console.error("Gemini AI error:", aiError); + // Fallback to mock data when Gemini is not available report = { generatedAt: Date.now(), companyPerformance: { @@ -1461,7 +1609,7 @@ Be thorough, professional, and focus on actionable insights. }); // Chat Function -exports.chat = onRequest({ cors: true }, async (req, res) => { +exports.chat = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); @@ -1481,10 +1629,9 @@ exports.chat = onRequest({ cors: true }, async (req, res) => { try { let response; - if (openai) { - // Use OpenAI for chat responses - const systemPrompt = ` -You are a cut-and-dry expert business analyst. + try { + // Use Gemini for chat responses + const systemPrompt = `You are a cut-and-dry expert business analyst. You provide thoughtful, professional advice based on the employee context and company data provided. ${context ? ` @@ -1497,74 +1644,45 @@ Mentioned Employees: ${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')} ` : ''} -You will discuss employees with the employer to help: -1. Evaluate the company based on all provided data, being thorough to touch on all information gathered from said employee doubled with information known about the company -2. Attempt to at your best effort further the companies success and growth potential -3. Provide clear, concise, and actionable recommendations for improvement -4. Don't cater to sugarcoating or vague generalities -5. Beat the nail into the coffin of inefficiency with precise solutions, getting rid of all weak points. +Instructions: +- Provide specific, actionable business advice +- Reference the provided context when relevant +- Be direct and professional +- Focus on practical solutions and insights +- Keep responses concise but thorough +- When discussing employees, use their names and be respectful`; -Provide helpful, actionable insights while maintaining professional tone and focusing on critical must-know knowledge and actionable recommendations. - `.trim(); + let fullMessage = message; - // Build the user message content - let userContent = [ - { - type: "text", - text: message - } - ]; - - // Add image attachments if present + // Handle file attachments (text only for now with Gemini) if (attachments && attachments.length > 0) { - attachments.forEach(attachment => { - if (attachment.type.startsWith('image/') && attachment.data) { - userContent.push({ - type: "image_url", - image_url: { - url: attachment.data, - detail: "high" - } - }); - } - // For non-image files, add them as text context - else if (attachment.data) { - userContent.push({ - type: "text", - text: `[Attached file: ${attachment.name} (${attachment.type})]` - }); - } - }); + const textAttachments = attachments.filter(att => + att.type?.includes('text') || att.name?.endsWith('.txt') || att.name?.endsWith('.md') + ); + + if (textAttachments.length > 0) { + fullMessage += `\n\nAttached files:\n${textAttachments.map(att => + `- ${att.name}: ${att.data || '[File content not available]'}` + ).join('\n')}`; + } } - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - messages: [ - { - role: "system", - content: systemPrompt - }, - { - role: "user", - content: userContent - } - ], - temperature: 0.7, - max_tokens: 1000, // Increased for more detailed responses when analyzing images - }); + const prompt = `${systemPrompt}\n\nUser question: ${fullMessage}`; - response = completion.choices[0].message.content; - } else { - // Fallback responses when OpenAI is not available + response = await generateWithGemini(prompt); + + } catch (aiError) { + console.error("Gemini AI error:", aiError); + // Fallback responses when Gemini is not available const attachmentText = attachments && attachments.length > 0 ? ` I can see you've attached ${attachments.length} file(s), but I'm currently unable to process attachments.` : ''; const responses = [ - `That's an interesting point about performance metrics.${attachmentText} Based on the data, I'd recommend focusing on...`, - `I can see from the employee report that there are opportunities for growth in...${attachmentText}`, - `The company analysis suggests that this area needs attention.${attachmentText} Here's what I would suggest...`, - `Based on the performance data, this employee shows strong potential in...${attachmentText}`, + `I understand you're asking about "${message}". Based on your team structure, I'd recommend focusing on clear communication and setting measurable goals.${attachmentText}`, + `That's an interesting question about "${message}". Consider reviewing your current processes and identifying areas for improvement.${attachmentText}`, + `Regarding "${message}" - this touches on important organizational dynamics. I'd suggest gathering more data and consulting with your team leads.${attachmentText}`, + `Your question about "${message}" highlights key business considerations. Focus on data-driven decisions and stakeholder alignment.${attachmentText}`, ]; response = responses[Math.floor(Math.random() * responses.length)]; @@ -1582,7 +1700,7 @@ Provide helpful, actionable insights while maintaining professional tone and foc }); // Create Organization Function -exports.createOrganization = onRequest({ cors: true }, async (req, res) => { +exports.createOrganization = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -1696,7 +1814,7 @@ exports.createOrganization = onRequest({ cors: true }, async (req, res) => { }); // Get User Organizations Function -exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => { +exports.getUserOrganizations = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); @@ -1741,7 +1859,7 @@ exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => { }); // Join Organization Function (via invite) -exports.joinOrganization = onRequest({ cors: true }, async (req, res) => { +exports.joinOrganization = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -1856,7 +1974,7 @@ exports.joinOrganization = onRequest({ cors: true }, async (req, res) => { }); // Create Stripe Checkout Session Function -exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => { +exports.createCheckoutSession = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2003,7 +2121,7 @@ exports.stripeWebhook = onRequest(async (req, res) => { }); // Get Subscription Status Function -exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => { +exports.getSubscriptionStatus = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2062,175 +2180,7 @@ exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => { } }); -// Webhook Helper Functions -async function handleCheckoutCompleted(session) { - const orgId = session.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'trialing', - 'subscription.stripeSubscriptionId': session.subscription, - 'subscription.checkoutSessionId': session.id, - updatedAt: Date.now(), - }); -} - -async function handleSubscriptionCreated(subscription) { - const orgId = subscription.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': subscription.status, - 'subscription.stripeSubscriptionId': subscription.id, - 'subscription.currentPeriodStart': subscription.current_period_start * 1000, - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, - updatedAt: Date.now(), - }); -} - -async function handleSubscriptionUpdated(subscription) { - const orgId = subscription.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': subscription.status, - 'subscription.currentPeriodStart': subscription.current_period_start * 1000, - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - 'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null, - updatedAt: Date.now(), - }); -} - -async function handleSubscriptionDeleted(subscription) { - const orgId = subscription.metadata?.orgId; - if (!orgId) return; - - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'canceled', - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - updatedAt: Date.now(), - }); -} - -async function handlePaymentSucceeded(invoice) { - const subscriptionId = invoice.subscription; - if (!subscriptionId) return; - - // Update subscription status to active - const subscription = await stripe.subscriptions.retrieve(subscriptionId); - const orgId = subscription.metadata?.orgId; - - if (orgId) { - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'active', - 'subscription.currentPeriodStart': subscription.current_period_start * 1000, - 'subscription.currentPeriodEnd': subscription.current_period_end * 1000, - updatedAt: Date.now(), - }); - } -} - -async function handlePaymentFailed(invoice) { - const subscriptionId = invoice.subscription; - if (!subscriptionId) return; - - const subscription = await stripe.subscriptions.retrieve(subscriptionId); - const orgId = subscription.metadata?.orgId; - - if (orgId) { - const orgRef = db.collection("orgs").doc(orgId); - await orgRef.update({ - 'subscription.status': 'past_due', - updatedAt: Date.now(), - }); - } -} - -// exports.helloWorld = onRequest((request, response) => { -// response.send("Hello from Firebase!"); -// }); - -// exports.sendOTP = onRequest(async (request, response) => { -// // Set CORS headers -// response.set('Access-Control-Allow-Origin', '*'); -// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); -// response.set('Access-Control-Allow-Headers', 'Content-Type'); - -// if (request.method === 'OPTIONS') { -// response.status(204).send(''); -// return; -// } - -// if (request.method !== 'POST') { -// response.status(405).json({ error: 'Method not allowed' }); -// return; -// } - -// const { email } = request.body; - -// if (!email) { -// response.status(400).json({ error: 'Email is required' }); -// return; -// } - -// // Generate a simple OTP -// const otp = Math.floor(100000 + Math.random() * 900000).toString(); - -// response.json({ -// success: true, -// message: 'Verification code sent to your email', -// otp: otp // Always return OTP in emulator mode -// }); -// }); - -// exports.verifyOTP = onRequest(async (request, response) => { -// // Set CORS headers -// response.set('Access-Control-Allow-Origin', '*'); -// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); -// response.set('Access-Control-Allow-Headers', 'Content-Type'); - -// if (request.method === 'OPTIONS') { -// response.status(204).send(''); -// return; -// } - -// if (request.method !== 'POST') { -// response.status(405).json({ error: 'Method not allowed' }); -// return; -// } - -// const { email, otp } = request.body; - -// if (!email || !otp) { -// response.status(400).json({ error: 'Email and OTP are required' }); -// return; -// } - -// // Mock verification - accept any 6-digit code -// if (otp.length === 6) { -// response.json({ -// success: true, -// user: { -// uid: 'demo-user-123', -// email: email, -// displayName: email.split('@')[0], -// emailVerified: true -// }, -// token: 'demo-token-123' -// }); -// } else { -// response.status(400).json({ error: 'Invalid verification code' }); -// } -// }); - -// Save Company Report Function -exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => { +exports.saveCompanyReport = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2272,7 +2222,6 @@ exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => { } }); -// Helper function to verify user authorization const verifyUserAuthorization = async (userId, orgId) => { if (!userId || !orgId) { return false; @@ -2288,8 +2237,7 @@ const verifyUserAuthorization = async (userId, orgId) => { } }; -// Get Organization Data Function -exports.getOrgData = onRequest({ cors: true }, async (req, res) => { +exports.getOrgData = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2331,7 +2279,7 @@ exports.getOrgData = onRequest({ cors: true }, async (req, res) => { }); // Update Organization Data Function -exports.updateOrgData = onRequest({ cors: true }, async (req, res) => { +exports.updateOrgData = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2377,7 +2325,7 @@ exports.updateOrgData = onRequest({ cors: true }, async (req, res) => { }); // Get Employees Function -exports.getEmployees = onRequest({ cors: true }, async (req, res) => { +exports.getEmployees = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2423,7 +2371,7 @@ exports.getEmployees = onRequest({ cors: true }, async (req, res) => { }); // Get Submissions Function -exports.getSubmissions = onRequest({ cors: true }, async (req, res) => { +exports.getSubmissions = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2465,7 +2413,7 @@ exports.getSubmissions = onRequest({ cors: true }, async (req, res) => { }); // Get Reports Function -exports.getReports = onRequest({ cors: true }, async (req, res) => { +exports.getReports = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2507,7 +2455,7 @@ exports.getReports = onRequest({ cors: true }, async (req, res) => { }); // Create/Update Employee Function -exports.upsertEmployee = onRequest({ cors: true }, async (req, res) => { +exports.upsertEmployee = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2559,7 +2507,7 @@ exports.upsertEmployee = onRequest({ cors: true }, async (req, res) => { }); // Save Report Function -exports.saveReport = onRequest({ cors: true }, async (req, res) => { +exports.saveReport = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2609,7 +2557,7 @@ exports.saveReport = onRequest({ cors: true }, async (req, res) => { }); // Get Company Reports Function -exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => { +exports.getCompanyReports = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2651,7 +2599,7 @@ exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => { }); // Upload Image Function -exports.uploadImage = onRequest({ cors: true }, async (req, res) => { +exports.uploadImage = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2715,7 +2663,7 @@ exports.uploadImage = onRequest({ cors: true }, async (req, res) => { }); // Get Image Function -exports.getImage = onRequest({ cors: true }, async (req, res) => { +exports.getImage = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2771,7 +2719,7 @@ exports.getImage = onRequest({ cors: true }, async (req, res) => { }); // Delete Image Function -exports.deleteImage = onRequest({ cors: true }, async (req, res) => { +exports.deleteImage = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2815,7 +2763,7 @@ exports.deleteImage = onRequest({ cors: true }, async (req, res) => { }); // Migration Function - Remove Owners from Employees Collection -exports.migrateOwnersFromEmployees = onRequest({ cors: true }, async (req, res) => { +exports.migrateOwnersFromEmployees = onRequest(async (req, res) => { if (req.method === 'OPTIONS') { res.status(204).send(''); return; @@ -2890,4 +2838,6 @@ exports.migrateOwnersFromEmployees = onRequest({ cors: true }, async (req, res) } res.status(500).json({ error: "Failed to complete migration" }); } -}); \ No newline at end of file +}); + +// endregion \ No newline at end of file diff --git a/functions/package.json b/functions/package.json index 23acb1b..6cb278e 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,7 +15,7 @@ "dependencies": { "firebase-admin": "^12.1.1", "firebase-functions": "^5.0.1", - "openai": "^5.12.2", + "@google-cloud/vertexai": "^1.4.0", "stripe": "^18.4.0" }, "devDependencies": { diff --git a/public/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png b/public/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png new file mode 100644 index 0000000..7a589f6 Binary files /dev/null and b/public/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png differ diff --git a/public/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png b/public/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png new file mode 100644 index 0000000..0ba6259 Binary files /dev/null and b/public/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png differ diff --git a/src/App.tsx b/src/App.tsx index 6ef0336..742ee27 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,17 +11,14 @@ const CompanyWiki = React.lazy(() => import('./pages/CompanyWiki')); const Reports = React.lazy(() => import('./pages/Reports')); const Submissions = React.lazy(() => import('./pages/Submissions')); const Chat = React.lazy(() => import('./pages/Chat')); -const HelpNew = React.lazy(() => import('./pages/HelpNew')); -const SettingsNew = React.lazy(() => import('./pages/SettingsNew')); +const HelpNew = React.lazy(() => import('./pages/Help')); +const SettingsNew = React.lazy(() => import('./pages/Settings')); const ModernLogin = React.lazy(() => import('./pages/Login')); const OrgSelection = React.lazy(() => import('./pages/OrgSelection')); const Onboarding = React.lazy(() => import('./pages/Onboarding')); -const EmployeeQuestionnaire = React.lazy(() => import('./pages/EmployeeQuestionnaire')); -const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaireNew')); -const EmployeeQuestionnaireSteps = React.lazy(() => import('./pages/EmployeeQuestionnaireSteps')); +const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaire')); const QuestionTypesDemo = React.lazy(() => import('./pages/QuestionTypesDemo')); const FormsDashboard = React.lazy(() => import('./pages/FormsDashboard')); -const QuestionnaireComplete = React.lazy(() => import('./pages/QuestionnaireComplete')); const SubscriptionSetup = React.lazy(() => import('./pages/SubscriptionSetup')); // Loading component for Suspense fallback @@ -123,15 +120,11 @@ function App() { } /> } /> - {/* } /> */} {/* Employee questionnaire - no auth needed, uses invite code */} } /> } /> - {/* Legacy employee questionnaire route for backwards compatibility */} - } /> - {/* Organization Selection - after auth, before entering app */} - {/* Legacy employee questionnaire route for backwards compatibility */} - - - - - - - - } - /> - - - - - - - - - } - /> - - } /> - {/* New Figma Chat Implementation - Standalone route */} } /> - {/* New Figma Help Implementation - Standalone route */} } /> } /> } /> - {/* } /> - } /> */} {/* Debug routes */} diff --git a/src/components/figma/Sidebar.tsx b/src/components/figma/Sidebar.tsx index 07d1459..1bc269d 100644 --- a/src/components/figma/Sidebar.tsx +++ b/src/components/figma/Sidebar.tsx @@ -510,7 +510,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals >
{React.cloneElement(settingsIcon, { - stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)" + stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400)" })}
= ({ children useEffect(() => { console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured); - if (isFirebaseConfigured) { - // Firebase mode: Set up proper Firebase auth state listener - const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => { - console.log('Firebase auth state changed:', firebaseUser?.email); - if (firebaseUser) { - setUser(firebaseUser); - } else { - // Check for OTP session as fallback - const sessionUser = localStorage.getItem('auditly_demo_session'); - if (sessionUser) { - try { - const parsedUser = JSON.parse(sessionUser); - console.log('Restoring OTP session for:', parsedUser.email); - setUser(parsedUser as User); - } catch (error) { - console.error('Failed to parse session user:', error); - localStorage.removeItem('auditly_demo_session'); - setUser(null); - } - } else { - setUser(null); - } - } - setLoading(false); - }); - - return unsubscribe; - } else { - // Demo/OTP mode: Check localStorage for persisted session - console.log('Checking for persisted OTP session'); - const sessionUser = localStorage.getItem('auditly_demo_session'); - if (sessionUser) { - try { - const parsedUser = JSON.parse(sessionUser); - console.log('Restoring session for:', parsedUser.email); - setUser(parsedUser as User); - } catch (error) { - console.error('Failed to parse session user:', error); - localStorage.removeItem('auditly_demo_session'); - setUser(null); - } - } else { + // Demo/OTP mode: Check localStorage for persisted session + console.log('Checking for persisted OTP session'); + const sessionUser = localStorage.getItem('auditly_demo_session'); + if (sessionUser) { + try { + const parsedUser = JSON.parse(sessionUser); + console.log('Restoring session for:', parsedUser.email); + setUser(parsedUser as User); + } catch (error) { + console.error('Failed to parse session user:', error); + localStorage.removeItem('auditly_demo_session'); setUser(null); } - setLoading(false); - - return () => { }; + } else { + setUser(null); } + setLoading(false); + + return () => { }; + }, []); const signInWithGoogle = useCallback(async () => { diff --git a/src/contexts/UserOrganizationsContext.tsx b/src/contexts/UserOrganizationsContext.tsx index 719b542..f651803 100644 --- a/src/contexts/UserOrganizationsContext.tsx +++ b/src/contexts/UserOrganizationsContext.tsx @@ -1,7 +1,5 @@ import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react'; import { useAuth } from './AuthContext'; -import { isFirebaseConfigured } from '../services/firebase'; -import { API_URL } from '../constants'; import { secureApi } from '../services/secureApi'; interface UserOrganization { diff --git a/src/pages/EmployeeQuestionnaire.tsx b/src/pages/EmployeeQuestionnaire.tsx index c6f797c..e40dd73 100644 --- a/src/pages/EmployeeQuestionnaire.tsx +++ b/src/pages/EmployeeQuestionnaire.tsx @@ -1,1507 +1,33 @@ import React, { useState, useEffect } from 'react'; -import { useNavigate, useLocation, useParams } from 'react-router-dom'; -import { useAuth } from '../contexts/AuthContext'; -import { useOrg } from '../contexts/OrgContext'; -import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions'; +import { useParams } from 'react-router-dom'; +import { EmployeeSubmissionAnswers } from '../employeeQuestions'; import { API_URL } from '../constants'; -import { FigmaRatingScale, FigmaTextArea, FigmaNavigationButtons } from '../components/figma/FigmaQuestion'; -import { FigmaMultipleChoice } from '../components/figma/FigmaMultipleChoice'; +import { + WelcomeScreen, + SectionIntro, + PersonalInfoForm, + TextAreaQuestion, + RatingScaleQuestion, + YesNoChoice, + ThankYouPage +} from '../components/figma/FigmaEmployeeForms'; + +/** + * Employee Questionnaire with Invite-Only Flow + * + * Features: + * - Invite-based flow (no authentication required) + * - Company owner invites employees with metadata + * - Employee uses invite code to access questionnaire + * - LLM processing via cloud functions with company context + * - Report generation and Firestore storage + */ -// Icon SVG Component - From EmployeeFormsController -const AuditlyIcon: React.FC = () => ( - - - - - - - - - - - - - - -); - -// Progress Bar Component for Section Headers -const SectionProgressBar: React.FC<{ currentSection: number; totalSections: number }> = ({ currentSection, totalSections }) => { - return ( -
- {Array.from({ length: 7 }, (_, index) => { - const isActive = index === 0; // First step is always active for section start - return ( -
- {index === 0 ? ( -
- ) : ( - - - - )} -
- ); - })} -
- ); -}; - -// Yes/No Choice Component -const YesNoChoice: React.FC<{ - question: string; - value?: string; - onChange: (value: string) => void; - onBack?: () => void; - onNext: () => void; - onSkip?: () => void; - currentStep?: number; - totalSteps?: number; -}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps }) => { - return ( -
-
-
-
{question}
-
-
onChange('No')} - className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'No' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate800]'}`} - > -
- No -
-
-
onChange('Yes')} - className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'Yes' ? 'bg-[--Neutrals-NeutralSlate800]' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate800]'}`} - > -
- Yes -
-
-
-
-
- {onBack && ( - - )} - -
-
- - {/* Skip button */} - {onSkip && ( -
-
Skip
-
- )} - - {/* Progress indicators */} - {currentStep && totalSteps && ( - <> -
-
{currentStep} of {totalSteps}
-
-
- -
Leadership & Organizational Structure
-
- - )} -
- ); -}; - - -// Section Intro Component - From EmployeeFormsController -const SectionIntro: React.FC<{ - sectionNumber: string; - title: string; - description: string; - onStart: () => void; - imageUrl?: string; -}> = ({ sectionNumber, title, description, onStart, imageUrl = "https://placehold.co/560x682" }) => { - return ( -
-
-
-
-
-
-
- -
-
-
-
-
{sectionNumber}
-
-
{title}
-
{description}
-
-
- -
-
-
-
- {title} -
-
-
- ); -}; - -// Step 1: Welcome & Role Information - From EmployeeFormsController -const EmployeeFormStep1: React.FC<{ onNext: (data: any) => void }> = ({ onNext }) => { - const [formData, setFormData] = useState({ - name: '', - role: '', - department: '' - }); - - const handleSubmit = () => { - onNext(formData); - }; - - return ( -
-
-
-
-
-
-
- -
-
-
-
Welcome to the Auditly Employee Assessment
-
Let's learn about your role, contribution and help us get a better understand of how you work best.
-
-
-
-
-
-
-
Your Role & Output
-
-
Tell us about your current role and what you work on
-
-
-
-
Your Name
-
*
-
-
-
- setFormData({ ...formData, name: e.target.value })} - className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none" - placeholder="Enter your full name" - /> -
-
-
-
-
-
What is your role at the company?
-
*
-
-
-
- setFormData({ ...formData, role: e.target.value })} - className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none" - placeholder="e.g. Software Engineer, Marketing Manager" - /> -
-
-
-
-
-
What department do you work in?
-
*
-
-
-
- setFormData({ ...formData, department: e.target.value })} - className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none" - placeholder="e.g. Engineering, Sales, Marketing" - /> -
-
-
-
-
- -
-
-
-
-
- ); -}; - - -// Step 2: Personal Information -const EmployeeFormStep2: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [formData, setFormData] = useState({ - email: '', - name: '', - company: '' - }); - - return ( -
-
-
-
Personal Information
-
-
-
-
Email
-
*
-
-
-
- setFormData({ ...formData, email: e.target.value })} - className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none" - placeholder="Email@gmail.com" - /> -
-
-
-
-
-
Your Name
-
*
-
-
-
- setFormData({ ...formData, name: e.target.value })} - className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none" - placeholder="John Doe" - /> -
-
-
-
-
-
What is the name of your Company and department?
-
*
-
-
-
- setFormData({ ...formData, company: e.target.value })} - className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight placeholder:text-[--Neutrals-NeutralSlate950] outline-none" - placeholder="Doe Enterprises" - /> -
-
-
-
-
-
- - -
-
-
- ); -}; - -// Step 3: Current Title and Department (Text Area) -const EmployeeFormStep3: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ titleAndDepartment: answer })} - onSkip={() => onNext({ titleAndDepartment: '' })} - nextDisabled={!answer.trim()} - currentStep={1} - totalSteps={7} - /> -
-
- ); -}; - -// Step 4: Daily Tasks (Text Area) -const EmployeeFormStep4: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ dailyTasks: answer })} - onSkip={() => onNext({ dailyTasks: '' })} - nextDisabled={!answer.trim()} - currentStep={2} - totalSteps={7} - /> -
-
- ); -}; - -// Step 5: Role Understanding Rating -const EmployeeFormStep5: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [rating, setRating] = useState(); - - return ( -
- -
- onNext({ roleUnderstanding: rating })} - onSkip={() => onNext({ roleUnderstanding: null })} - nextDisabled={!rating} - currentStep={3} - totalSteps={7} - /> -
-
- ); -}; - -// Step 6: Work Satisfaction -const EmployeeFormStep6: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [rating, setRating] = useState(); - - return ( -
- -
- onNext({ workSatisfaction: rating })} - onSkip={() => onNext({ workSatisfaction: null })} - nextDisabled={!rating} - currentStep={4} - totalSteps={7} - /> -
-
- ); -}; - -// Step 7: Communication Rating -const EmployeeFormStep7: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [rating, setRating] = useState(); - - return ( -
- -
- onNext({ communicationRating: rating })} - onSkip={() => onNext({ communicationRating: null })} - nextDisabled={!rating} - currentStep={5} - totalSteps={7} - /> -
-
- ); -}; - -// Step 8: Work Style Preference -const EmployeeFormStep8: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [selectedOption, setSelectedOption] = useState(''); - - const options = [ - 'I prefer working independently', - 'I enjoy collaborative teamwork', - 'I like a mix of both', - 'I prefer clear instructions and structure', - 'I thrive with autonomy and flexibility' - ]; - - return ( -
-
-
-

- What work style best describes you? -

-
- {options.map((option, index) => ( - - ))} -
-
-
-
- onNext({ workStyle: selectedOption })} - onSkip={() => onNext({ workStyle: '' })} - nextDisabled={!selectedOption} - currentStep={6} - totalSteps={7} - /> -
-
- ); -}; - -// Step 9: Section Intro - Output & Accountability -const EmployeeFormStep9: React.FC<{ onNext: () => void }> = ({ onNext }) => { - return ( - - ); -}; - -// Step 10: Weekly Output Rating -const EmployeeFormStep10: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [rating, setRating] = useState(); - - return ( -
- -
- onNext({ weeklyOutput: rating })} - onSkip={() => onNext({ weeklyOutput: null })} - nextDisabled={!rating} - currentStep={1} - totalSteps={7} - /> -
-
- ); -}; - -// Step 11: Top Deliverables -const EmployeeFormStep11: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ topDeliverables: answer })} - onSkip={() => onNext({ topDeliverables: '' })} - nextDisabled={!answer.trim()} - currentStep={2} - totalSteps={7} - /> -
-
- ); -}; - -// Step 12: Measurable Results -const EmployeeFormStep12: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ measurableResults: answer })} - onSkip={() => onNext({ measurableResults: '' })} - nextDisabled={!answer.trim()} - currentStep={3} - totalSteps={7} - /> -
-
- ); -}; - -// Step 13: Weekly KPIs -const EmployeeFormStep13: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( - onNext({ hasKPIs: answer })} - onSkip={() => onNext({ hasKPIs: '' })} - currentStep={4} - totalSteps={7} - /> - ); -}; - -// Step 14: KPI Details -const EmployeeFormStep14: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ kpiDetails: answer })} - onSkip={() => onNext({ kpiDetails: '' })} - currentStep={5} - totalSteps={7} - /> -
-
- ); -}; - -// Step 15: Reporting Structure -const EmployeeFormStep15: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ reportingStructure: answer })} - onSkip={() => onNext({ reportingStructure: '' })} - currentStep={6} - totalSteps={7} - /> -
-
- ); -}; - -// Step 16: Section Intro - Team & Collaboration -const EmployeeFormStep16: React.FC<{ onNext: () => void }> = ({ onNext }) => { - return ( - - ); -}; - -// Step 17: Work Closest With -const EmployeeFormStep17: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ workClosestWith: answer })} - onSkip={() => onNext({ workClosestWith: '' })} - currentStep={1} - totalSteps={7} - /> -
-
- ); -}; - -// Step 18: Collaboration Issues -const EmployeeFormStep18: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ collaborationIssues: answer })} - onSkip={() => onNext({ collaborationIssues: '' })} - currentStep={2} - totalSteps={7} - /> -
-
- ); -}; - -// Step 19: Team Communication Rating -const EmployeeFormStep19: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [rating, setRating] = useState(); - - return ( -
- -
- onNext({ teamCommunication: rating })} - onSkip={() => onNext({ teamCommunication: null })} - nextDisabled={!rating} - currentStep={3} - totalSteps={7} - /> -
-
- ); -}; - -// Step 20: Team Support -const EmployeeFormStep20: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ teamSupport: answer })} - onSkip={() => onNext({ teamSupport: '' })} - currentStep={4} - totalSteps={7} - /> -
-
- ); -}; - -// Step 21: Tools and Resources Section -const EmployeeFormStep21: React.FC<{ onNext: () => void }> = ({ onNext }) => { - return ( - - ); -}; - -// Step 22: Current Tools -const EmployeeFormStep22: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ currentTools: answer })} - onSkip={() => onNext({ currentTools: '' })} - currentStep={1} - totalSteps={7} - /> -
-
- ); -}; - -// Step 23: Tool Effectiveness -const EmployeeFormStep23: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [rating, setRating] = useState(); - - return ( -
- -
- onNext({ toolEffectiveness: rating })} - onSkip={() => onNext({ toolEffectiveness: null })} - nextDisabled={!rating} - currentStep={2} - totalSteps={7} - /> -
-
- ); -}; - -// Step 24: Missing Tools -const EmployeeFormStep24: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ missingTools: answer })} - onSkip={() => onNext({ missingTools: '' })} - currentStep={3} - totalSteps={7} - /> -
-
- ); -}; - -// Step 25: Skills & Development Section -const EmployeeFormStep25: React.FC<{ onNext: () => void }> = ({ onNext }) => { - return ( - - ); -}; - -// Step 26: Key Skills -const EmployeeFormStep26: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ keySkills: answer })} - onSkip={() => onNext({ keySkills: '' })} - currentStep={1} - totalSteps={7} - /> -
-
- ); -}; - -// Step 27: Skill Development -const EmployeeFormStep27: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ skillDevelopment: answer })} - onSkip={() => onNext({ skillDevelopment: '' })} - currentStep={2} - totalSteps={7} - /> -
-
- ); -}; - -// Step 28: Training Opportunities -const EmployeeFormStep28: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( - onNext({ awareOfTraining: answer })} - onSkip={() => onNext({ awareOfTraining: '' })} - currentStep={3} - totalSteps={7} - /> - ); -}; - -// Step 29: Career Goals -const EmployeeFormStep29: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ careerGoals: answer })} - onSkip={() => onNext({ careerGoals: '' })} - currentStep={4} - totalSteps={7} - /> -
-
- ); -}; - -// Step 30: Feedback & Improvement Section -const EmployeeFormStep30: React.FC<{ onNext: () => void }> = ({ onNext }) => { - return ( - - ); -}; - -// Step 31: Company Improvements -const EmployeeFormStep31: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ companyImprovements: answer })} - onSkip={() => onNext({ companyImprovements: '' })} - currentStep={1} - totalSteps={7} - /> -
-
- ); -}; - -// Step 32: Job Satisfaction -const EmployeeFormStep32: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [rating, setRating] = useState(); - - return ( -
- -
- onNext({ jobSatisfaction: rating })} - onSkip={() => onNext({ jobSatisfaction: null })} - nextDisabled={!rating} - currentStep={2} - totalSteps={7} - /> -
-
- ); -}; - -// Step 33: Additional Feedback -const EmployeeFormStep33: React.FC<{ onNext: (data: any) => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = useState(''); - - return ( -
- -
- onNext({ additionalFeedback: answer })} - onSkip={() => onNext({ additionalFeedback: '' })} - currentStep={3} - totalSteps={7} - /> -
-
- ); -}; - -// Step 35: Leadership & Organizational Structure - Magic Wand Question -const EmployeeFormStep35: React.FC<{ onNext: () => void; onBack: () => void }> = ({ onNext, onBack }) => { - const [answer, setAnswer] = React.useState(''); - - return ( -
-
-
-
If you had a magic wand, what would you change about how we operate?
-
-