update to use auditly-consulting firebase, gemini, fix settings/help pages to be their own pages (upload image not yet functional).
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -65,4 +65,5 @@ dist-ssr
|
|||||||
/deploy-security.sh
|
/deploy-security.sh
|
||||||
/EMPLOYEE_FORMS_FIGMA_README.md
|
/EMPLOYEE_FORMS_FIGMA_README.md
|
||||||
/TODOS.md
|
/TODOS.md
|
||||||
/SECURITY_MIGRATION.md
|
/SECURITY_MIGRATION.md
|
||||||
|
/employee_report_schema.json
|
||||||
@@ -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."
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "auditly-functions",
|
"name": "auditly-functions",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google-cloud/vertexai": "^1.4.0",
|
||||||
"firebase-admin": "^12.1.1",
|
"firebase-admin": "^12.1.1",
|
||||||
"firebase-functions": "^5.0.1",
|
"firebase-functions": "^5.0.1",
|
||||||
"openai": "^5.12.2",
|
|
||||||
"stripe": "^18.4.0",
|
"stripe": "^18.4.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
@@ -1055,9 +1053,7 @@
|
|||||||
|
|
||||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"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=="],
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
|
|||||||
1498
functions/index.js
1498
functions/index.js
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase-admin": "^12.1.1",
|
"firebase-admin": "^12.1.1",
|
||||||
"firebase-functions": "^5.0.1",
|
"firebase-functions": "^5.0.1",
|
||||||
"openai": "^5.12.2",
|
"@google-cloud/vertexai": "^1.4.0",
|
||||||
"stripe": "^18.4.0"
|
"stripe": "^18.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
BIN
public/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png
Normal file
BIN
public/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 386 KiB |
BIN
public/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png
Normal file
BIN
public/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 445 KiB |
45
src/App.tsx
45
src/App.tsx
@@ -11,17 +11,14 @@ const CompanyWiki = React.lazy(() => import('./pages/CompanyWiki'));
|
|||||||
const Reports = React.lazy(() => import('./pages/Reports'));
|
const Reports = React.lazy(() => import('./pages/Reports'));
|
||||||
const Submissions = React.lazy(() => import('./pages/Submissions'));
|
const Submissions = React.lazy(() => import('./pages/Submissions'));
|
||||||
const Chat = React.lazy(() => import('./pages/Chat'));
|
const Chat = React.lazy(() => import('./pages/Chat'));
|
||||||
const HelpNew = React.lazy(() => import('./pages/HelpNew'));
|
const HelpNew = React.lazy(() => import('./pages/Help'));
|
||||||
const SettingsNew = React.lazy(() => import('./pages/SettingsNew'));
|
const SettingsNew = React.lazy(() => import('./pages/Settings'));
|
||||||
const ModernLogin = React.lazy(() => import('./pages/Login'));
|
const ModernLogin = React.lazy(() => import('./pages/Login'));
|
||||||
const OrgSelection = React.lazy(() => import('./pages/OrgSelection'));
|
const OrgSelection = React.lazy(() => import('./pages/OrgSelection'));
|
||||||
const Onboarding = React.lazy(() => import('./pages/Onboarding'));
|
const Onboarding = React.lazy(() => import('./pages/Onboarding'));
|
||||||
const EmployeeQuestionnaire = React.lazy(() => import('./pages/EmployeeQuestionnaire'));
|
const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaire'));
|
||||||
const EmployeeQuestionnaireNew = React.lazy(() => import('./pages/EmployeeQuestionnaireNew'));
|
|
||||||
const EmployeeQuestionnaireSteps = React.lazy(() => import('./pages/EmployeeQuestionnaireSteps'));
|
|
||||||
const QuestionTypesDemo = React.lazy(() => import('./pages/QuestionTypesDemo'));
|
const QuestionTypesDemo = React.lazy(() => import('./pages/QuestionTypesDemo'));
|
||||||
const FormsDashboard = React.lazy(() => import('./pages/FormsDashboard'));
|
const FormsDashboard = React.lazy(() => import('./pages/FormsDashboard'));
|
||||||
const QuestionnaireComplete = React.lazy(() => import('./pages/QuestionnaireComplete'));
|
|
||||||
const SubscriptionSetup = React.lazy(() => import('./pages/SubscriptionSetup'));
|
const SubscriptionSetup = React.lazy(() => import('./pages/SubscriptionSetup'));
|
||||||
|
|
||||||
// Loading component for Suspense fallback
|
// Loading component for Suspense fallback
|
||||||
@@ -123,15 +120,11 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
|
<Route path="/login" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
|
||||||
<Route path="/login/:inviteCode" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
|
<Route path="/login/:inviteCode" element={<SuspenseWrapper><ModernLogin /></SuspenseWrapper>} />
|
||||||
{/* <Route path="/invite/:inviteCode" element={<InviteRedirect />} /> */}
|
|
||||||
|
|
||||||
{/* Employee questionnaire - no auth needed, uses invite code */}
|
{/* Employee questionnaire - no auth needed, uses invite code */}
|
||||||
<Route path="/employee-form/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
|
<Route path="/employee-form/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
|
||||||
<Route path="/questionnaire/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
|
<Route path="/questionnaire/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaireNew /></SuspenseWrapper>} />
|
||||||
|
|
||||||
{/* Legacy employee questionnaire route for backwards compatibility */}
|
|
||||||
<Route path="/employee-form-legacy/:inviteCode" element={<SuspenseWrapper><EmployeeQuestionnaire /></SuspenseWrapper>} />
|
|
||||||
|
|
||||||
{/* Organization Selection - after auth, before entering app */}
|
{/* Organization Selection - after auth, before entering app */}
|
||||||
<Route
|
<Route
|
||||||
path="/org-selection"
|
path="/org-selection"
|
||||||
@@ -166,33 +159,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Legacy employee questionnaire route for backwards compatibility */}
|
|
||||||
<Route
|
|
||||||
path="/employee-questionnaire-legacy"
|
|
||||||
element={
|
|
||||||
<RequireAuth>
|
|
||||||
<RequireOrgSelection>
|
|
||||||
<OrgProviderWrapper>
|
|
||||||
<SuspenseWrapper><EmployeeQuestionnaire /></SuspenseWrapper>
|
|
||||||
</OrgProviderWrapper>
|
|
||||||
</RequireOrgSelection>
|
|
||||||
</RequireAuth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/employee-questionnaire-steps"
|
|
||||||
element={
|
|
||||||
<RequireAuth>
|
|
||||||
<RequireOrgSelection>
|
|
||||||
<OrgProviderWrapper>
|
|
||||||
<SuspenseWrapper><EmployeeQuestionnaireSteps /></SuspenseWrapper>
|
|
||||||
</OrgProviderWrapper>
|
|
||||||
</RequireOrgSelection>
|
|
||||||
</RequireAuth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/onboarding"
|
path="/onboarding"
|
||||||
element={
|
element={
|
||||||
@@ -206,8 +172,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/questionnaire-complete" element={<SuspenseWrapper><QuestionnaireComplete /></SuspenseWrapper>} />
|
|
||||||
|
|
||||||
{/* New Figma Chat Implementation - Standalone route */}
|
{/* New Figma Chat Implementation - Standalone route */}
|
||||||
<Route
|
<Route
|
||||||
path="/chat"
|
path="/chat"
|
||||||
@@ -223,7 +187,6 @@ function App() {
|
|||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* New Figma Help Implementation - Standalone route */}
|
{/* New Figma Help Implementation - Standalone route */}
|
||||||
<Route
|
<Route
|
||||||
path="/help"
|
path="/help"
|
||||||
@@ -274,8 +237,6 @@ function App() {
|
|||||||
<Route path="/company-wiki" element={<SuspenseWrapper><CompanyWiki /></SuspenseWrapper>} />
|
<Route path="/company-wiki" element={<SuspenseWrapper><CompanyWiki /></SuspenseWrapper>} />
|
||||||
<Route path="/submissions" element={<SuspenseWrapper><Submissions /></SuspenseWrapper>} />
|
<Route path="/submissions" element={<SuspenseWrapper><Submissions /></SuspenseWrapper>} />
|
||||||
<Route path="/reports" element={<SuspenseWrapper><Reports /></SuspenseWrapper>} />
|
<Route path="/reports" element={<SuspenseWrapper><Reports /></SuspenseWrapper>} />
|
||||||
{/* <Route path="/help" element={<SuspenseWrapper><HelpNew /></SuspenseWrapper>} />
|
|
||||||
<Route path="/settings" element={<SuspenseWrapper><SettingsNew /></SuspenseWrapper>} /> */}
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Debug routes */}
|
{/* Debug routes */}
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{React.cloneElement(settingsIcon, {
|
{React.cloneElement(settingsIcon, {
|
||||||
stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)"
|
stroke: location.pathname === "/settings" ? "var(--Brand-Orange)" : "var(--Neutrals-NeutralSlate400)"
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex-1 justify-start text-sm font-medium font-['Inter'] leading-tight ${location.pathname === "/settings"
|
<div className={`flex-1 justify-start text-sm font-medium font-['Inter'] leading-tight ${location.pathname === "/settings"
|
||||||
|
|||||||
@@ -7,16 +7,26 @@ export const SITE_URL = import.meta.env.VITE_SITE_URL || 'http://localhost:5173'
|
|||||||
const isLocalhost = typeof window !== 'undefined' &&
|
const isLocalhost = typeof window !== 'undefined' &&
|
||||||
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
||||||
|
|
||||||
|
// Check if we should force production API (useful for testing invite links locally)
|
||||||
|
const forceProductionAPI = typeof window !== 'undefined' &&
|
||||||
|
window.location.search.includes('use-production-api=true');
|
||||||
|
|
||||||
// Use Firebase Functions emulator in development, production functions in production
|
// Use Firebase Functions emulator in development, production functions in production
|
||||||
export const API_URL = isLocalhost
|
// export const API_URL = (isLocalhost && !forceProductionAPI)
|
||||||
? 'http://127.0.0.1:5002/auditly-c0027/us-central1' // Firebase Functions Emulator
|
// ? 'http://127.0.0.1:5002/auditly-consulting/us-central1' // Firebase Functions Emulator
|
||||||
: 'https://us-central1-auditly-c0027.cloudfunctions.net'; // Production Firebase Functions
|
// : 'https://us-central1-auditly-consulting.cloudfunctions.net'; // Production Firebase Functions
|
||||||
|
|
||||||
|
// export const API_URL = 'https://us-central1-auditly-c0027.cloudfunctions.net';
|
||||||
|
export const API_URL = 'https://us-central1-auditly-consulting.cloudfunctions.net';
|
||||||
|
// export const API_URL = 'http://127.0.0.1:5002/auditly-consulting/us-central1';
|
||||||
|
|
||||||
// Log URL configuration in development
|
// Log URL configuration in development
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.log('🌐 Frontend URL Configuration:');
|
console.log('🌐 Frontend URL Configuration:');
|
||||||
console.log(` SITE_URL: ${SITE_URL}`);
|
console.log(` SITE_URL: ${SITE_URL}`);
|
||||||
console.log(` API_URL: ${API_URL}`);
|
console.log(` API_URL: ${API_URL}`);
|
||||||
|
console.log(` isLocalhost: ${isLocalhost}`);
|
||||||
|
console.log(` forceProductionAPI: ${forceProductionAPI}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED: These are legacy sample data that should not be used in production
|
// DEPRECATED: These are legacy sample data that should not be used in production
|
||||||
@@ -43,21 +53,25 @@ export const REPORT_DATA: EmployeeReport = {
|
|||||||
},
|
},
|
||||||
strengths: ['Sample data - use AI-generated reports'],
|
strengths: ['Sample data - use AI-generated reports'],
|
||||||
weaknesses: [
|
weaknesses: [
|
||||||
{ isCritical: false, description: 'Sample data - use AI-generated reports' }
|
'Sample data - use AI-generated reports'
|
||||||
],
|
],
|
||||||
opportunities: [
|
opportunities: [
|
||||||
{
|
{
|
||||||
roleAdjustment: 'Sample data',
|
title: 'Sample data',
|
||||||
accountabilitySupport: 'Sample data',
|
description: 'Sample data',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
risks: ['Sample data - use AI-generated reports'],
|
risks: ['Sample data - use AI-generated reports'],
|
||||||
recommendations: ['Sample recommendation - use AI-generated reports'],
|
recommendations: ['Sample recommendation - use AI-generated reports'],
|
||||||
recommendation: {
|
gradingOverview: {
|
||||||
action: 'Keep',
|
employeeName: 'Sample Employee',
|
||||||
details: ['Sample data - use AI-generated reports'],
|
scalability: 3,
|
||||||
},
|
reliability: 4,
|
||||||
grading: [],
|
roleFit: 3,
|
||||||
|
output: 3,
|
||||||
|
grade: "A",
|
||||||
|
initiative: 10
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// DEPRECATED: Sample submission data - real submissions come from employee questionnaires
|
// DEPRECATED: Sample submission data - real submissions come from employee questionnaires
|
||||||
@@ -253,12 +267,6 @@ export const SAMPLE_COMPANY_REPORT: CompanyReport = {
|
|||||||
{ department: 'Tech', role: 'Frontend Developer', priority: 'High', reasoning: 'Need to expand web development capacity for growing client demands' },
|
{ department: 'Tech', role: 'Frontend Developer', priority: 'High', reasoning: 'Need to expand web development capacity for growing client demands' },
|
||||||
{ department: 'Social Media', role: 'Content Creator', priority: 'Medium', reasoning: 'Additional support needed for video content production' }
|
{ department: 'Social Media', role: 'Content Creator', priority: 'Medium', reasoning: 'Additional support needed for video content production' }
|
||||||
],
|
],
|
||||||
recommendations: [
|
|
||||||
'Hire additional frontend developer for tech team expansion',
|
|
||||||
'Implement cross-departmental collaboration sessions',
|
|
||||||
'Develop leadership training program for senior staff',
|
|
||||||
'Establish mentorship program for junior employees'
|
|
||||||
],
|
|
||||||
forwardOperatingPlan: [
|
forwardOperatingPlan: [
|
||||||
{
|
{
|
||||||
title: 'Q1 Client Growth Initiative',
|
title: 'Q1 Client Growth Initiative',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
import { signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||||
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
|
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
|
||||||
import { API_URL } from '../constants';
|
import { API_URL } from '../constants';
|
||||||
|
|
||||||
@@ -23,54 +23,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
console.log('AuthContext initializing, isFirebaseConfigured:', isFirebaseConfigured);
|
||||||
|
|
||||||
if (isFirebaseConfigured) {
|
// Demo/OTP mode: Check localStorage for persisted session
|
||||||
// Firebase mode: Set up proper Firebase auth state listener
|
console.log('Checking for persisted OTP session');
|
||||||
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
|
const sessionUser = localStorage.getItem('auditly_demo_session');
|
||||||
console.log('Firebase auth state changed:', firebaseUser?.email);
|
if (sessionUser) {
|
||||||
if (firebaseUser) {
|
try {
|
||||||
setUser(firebaseUser);
|
const parsedUser = JSON.parse(sessionUser);
|
||||||
} else {
|
console.log('Restoring session for:', parsedUser.email);
|
||||||
// Check for OTP session as fallback
|
setUser(parsedUser as User);
|
||||||
const sessionUser = localStorage.getItem('auditly_demo_session');
|
} catch (error) {
|
||||||
if (sessionUser) {
|
console.error('Failed to parse session user:', error);
|
||||||
try {
|
localStorage.removeItem('auditly_demo_session');
|
||||||
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 {
|
|
||||||
setUser(null);
|
setUser(null);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
} else {
|
||||||
|
setUser(null);
|
||||||
return () => { };
|
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
return () => { };
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const signInWithGoogle = useCallback(async () => {
|
const signInWithGoogle = useCallback(async () => {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import { isFirebaseConfigured } from '../services/firebase';
|
|
||||||
import { API_URL } from '../constants';
|
|
||||||
import { secureApi } from '../services/secureApi';
|
import { secureApi } from '../services/secureApi';
|
||||||
|
|
||||||
interface UserOrganization {
|
interface UserOrganization {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,600 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { EmployeeSubmissionAnswers } from '../employeeQuestions';
|
|
||||||
import { API_URL } from '../constants';
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
|
|
||||||
const EmployeeQuestionnaire: React.FC = () => {
|
|
||||||
const params = useParams();
|
|
||||||
const inviteCode = params.inviteCode;
|
|
||||||
|
|
||||||
// Component state
|
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
|
||||||
const [formData, setFormData] = useState<any>({});
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
|
|
||||||
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
|
|
||||||
|
|
||||||
// Load invite details on component mount
|
|
||||||
useEffect(() => {
|
|
||||||
if (inviteCode) {
|
|
||||||
loadInviteDetails(inviteCode);
|
|
||||||
} else {
|
|
||||||
setError('No invite code provided');
|
|
||||||
}
|
|
||||||
}, [inviteCode]);
|
|
||||||
|
|
||||||
const loadInviteDetails = async (code: string) => {
|
|
||||||
setIsLoadingInvite(true);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/getInvitationStatus?code=${code}`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.used) {
|
|
||||||
setError('This invitation has already been used');
|
|
||||||
} else if (data.employee) {
|
|
||||||
setInviteEmployee(data.employee);
|
|
||||||
// Pre-populate form data with invite metadata
|
|
||||||
setFormData({
|
|
||||||
name: data.employee.name || '',
|
|
||||||
email: data.employee.email || '',
|
|
||||||
company: data.employee.company || data.employee.department || ''
|
|
||||||
});
|
|
||||||
setError('');
|
|
||||||
} else {
|
|
||||||
setError('Invalid invitation data');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
||||||
setError(errorData.error || 'Invalid or expired invitation link');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading invite details:', err);
|
|
||||||
setError('Failed to load invitation details');
|
|
||||||
} finally {
|
|
||||||
setIsLoadingInvite(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get employee info from invite data
|
|
||||||
const currentEmployee = inviteEmployee;
|
|
||||||
|
|
||||||
const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => {
|
|
||||||
try {
|
|
||||||
// Submit the questionnaire answers directly using Cloud Function
|
|
||||||
// The cloud function will handle invite consumption and report generation
|
|
||||||
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
inviteCode: inviteCode,
|
|
||||||
answers: answers
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!submitResponse.ok) {
|
|
||||||
const errorData = await submitResponse.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to submit questionnaire');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await submitResponse.json();
|
|
||||||
return { success: true, reportGenerated: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Invite submission error:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Convert form data to answers format for backend
|
|
||||||
const answers: EmployeeSubmissionAnswers = {};
|
|
||||||
|
|
||||||
// Map form data to question IDs
|
|
||||||
if (formData.name) answers['full_name'] = formData.name;
|
|
||||||
if (formData.email) answers['email'] = formData.email;
|
|
||||||
if (formData.company) answers['company_department'] = formData.company;
|
|
||||||
|
|
||||||
// Add all other form data fields
|
|
||||||
Object.keys(formData).forEach(key => {
|
|
||||||
if (formData[key] && !answers[key]) {
|
|
||||||
answers[key] = formData[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Submit answers via invite flow
|
|
||||||
const result = await submitViaInvite(answers, inviteCode!);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Show thank you page
|
|
||||||
setCurrentStep(999);
|
|
||||||
} else {
|
|
||||||
setError(result.error || 'Failed to submit questionnaire');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Submission error:', error);
|
|
||||||
setError('Failed to submit questionnaire. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = (stepData?: any) => {
|
|
||||||
if (stepData) {
|
|
||||||
const newFormData = { ...formData, ...stepData };
|
|
||||||
setFormData(newFormData);
|
|
||||||
}
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Early return for loading state
|
|
||||||
if (isLoadingInvite) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<div className="w-16 h-16 bg-[--Brand-Orange] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Loading Your Invitation...</h1>
|
|
||||||
<p className="text-[--Neutrals-NeutralSlate500]">Please wait while we verify your invitation.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early return for error state
|
|
||||||
if (error && currentStep === 1) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
|
||||||
!
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Invitation Error</h1>
|
|
||||||
<p className="text-[--Neutrals-NeutralSlate500] mb-6">{error}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.href = '/'}
|
|
||||||
className="px-6 py-3 bg-[--Brand-Orange] text-white rounded-lg hover:bg-orange-600"
|
|
||||||
>
|
|
||||||
Return to Homepage
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderStep = () => {
|
|
||||||
switch (currentStep) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<WelcomeScreen
|
|
||||||
onStart={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<PersonalInfoForm
|
|
||||||
formData={{
|
|
||||||
email: formData.email || '',
|
|
||||||
name: formData.name || '',
|
|
||||||
company: formData.company || ''
|
|
||||||
}}
|
|
||||||
onChange={(data) => setFormData({ ...formData, ...data })}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
return (
|
|
||||||
<SectionIntro
|
|
||||||
sectionNumber="1 of 6"
|
|
||||||
title="Your Role & Responsibilities"
|
|
||||||
description="Let's start by understanding your current role and daily responsibilities."
|
|
||||||
onStart={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 4:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What is your current title and department?"
|
|
||||||
value={formData.titleAndDepartment || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, titleAndDepartment: value })}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={1}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Your Role & Responsibilities"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 5:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="Describe your core daily responsibilities"
|
|
||||||
value={formData.dailyResponsibilities || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, dailyResponsibilities: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={2}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Your Role & Responsibilities"
|
|
||||||
placeholder="Describe what you do on a typical day..."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 6:
|
|
||||||
return (
|
|
||||||
<RatingScaleQuestion
|
|
||||||
question="How clearly do you understand your role and responsibilities?"
|
|
||||||
leftLabel="Not clear"
|
|
||||||
rightLabel="Very clear"
|
|
||||||
value={formData.roleClarity}
|
|
||||||
onChange={(value) => setFormData({ ...formData, roleClarity: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={3}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Your Role & Responsibilities"
|
|
||||||
scale={10}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 7:
|
|
||||||
return (
|
|
||||||
<SectionIntro
|
|
||||||
sectionNumber="2 of 6"
|
|
||||||
title="Output & Accountability"
|
|
||||||
description="Let's explore your work output, goals, and accountability measures."
|
|
||||||
onStart={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 8:
|
|
||||||
return (
|
|
||||||
<RatingScaleQuestion
|
|
||||||
question="How would you rate your weekly output (volume & quality)?"
|
|
||||||
leftLabel="Very little"
|
|
||||||
rightLabel="Very High"
|
|
||||||
value={formData.weeklyOutput}
|
|
||||||
onChange={(value) => setFormData({ ...formData, weeklyOutput: value })}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={1}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Output & Accountability"
|
|
||||||
scale={10}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 9:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What are your top 2–3 recurring deliverables?"
|
|
||||||
value={formData.topDeliverables || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, topDeliverables: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={2}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Output & Accountability"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 10:
|
|
||||||
return (
|
|
||||||
<YesNoChoice
|
|
||||||
question="Do you have weekly KPIs or goals?"
|
|
||||||
value={formData.hasKPIs}
|
|
||||||
onChange={(value) => setFormData({ ...formData, hasKPIs: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={3}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Output & Accountability"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 11:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="Who do you report to? How often do you meet/check-in?"
|
|
||||||
value={formData.reportingStructure || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, reportingStructure: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={4}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Output & Accountability"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 12:
|
|
||||||
return (
|
|
||||||
<SectionIntro
|
|
||||||
sectionNumber="3 of 6"
|
|
||||||
title="Team & Collaboration"
|
|
||||||
description="Let's understand your team dynamics and collaboration patterns."
|
|
||||||
onStart={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 13:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="Who do you work most closely with?"
|
|
||||||
value={formData.closeCollaborators || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, closeCollaborators: value })}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={1}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Team & Collaboration"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 14:
|
|
||||||
return (
|
|
||||||
<RatingScaleQuestion
|
|
||||||
question="How would you rate team communication overall?"
|
|
||||||
leftLabel="Poor"
|
|
||||||
rightLabel="Excellent"
|
|
||||||
value={formData.teamCommunication}
|
|
||||||
onChange={(value) => setFormData({ ...formData, teamCommunication: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={2}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Team & Collaboration"
|
|
||||||
scale={10}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 15:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="Do you feel supported by your team? How?"
|
|
||||||
value={formData.teamSupport || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, teamSupport: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={3}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Team & Collaboration"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 16:
|
|
||||||
return (
|
|
||||||
<SectionIntro
|
|
||||||
sectionNumber="4 of 6"
|
|
||||||
title="Tools & Resources"
|
|
||||||
description="Let's examine the tools and resources available to support your work."
|
|
||||||
onStart={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 17:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What tools and software do you currently use?"
|
|
||||||
value={formData.currentTools || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, currentTools: value })}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={1}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Tools & Resources"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 18:
|
|
||||||
return (
|
|
||||||
<RatingScaleQuestion
|
|
||||||
question="How effective are your current tools?"
|
|
||||||
leftLabel="Not effective"
|
|
||||||
rightLabel="Very effective"
|
|
||||||
value={formData.toolEffectiveness}
|
|
||||||
onChange={(value) => setFormData({ ...formData, toolEffectiveness: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={2}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Tools & Resources"
|
|
||||||
scale={10}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 19:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What tools or resources are you missing to do your job more effectively?"
|
|
||||||
value={formData.missingTools || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, missingTools: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={3}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Tools & Resources"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 20:
|
|
||||||
return (
|
|
||||||
<SectionIntro
|
|
||||||
sectionNumber="5 of 6"
|
|
||||||
title="Skills & Development"
|
|
||||||
description="Let's explore your skills, growth opportunities, and career development."
|
|
||||||
onStart={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 21:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What are your key skills and strengths?"
|
|
||||||
value={formData.keySkills || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, keySkills: value })}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={1}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Skills & Development"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 22:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What skills would you like to develop or improve?"
|
|
||||||
value={formData.skillDevelopment || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, skillDevelopment: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={2}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Skills & Development"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 23:
|
|
||||||
return (
|
|
||||||
<YesNoChoice
|
|
||||||
question="Are you aware of current training opportunities?"
|
|
||||||
value={formData.awareOfTraining}
|
|
||||||
onChange={(value) => setFormData({ ...formData, awareOfTraining: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={3}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Skills & Development"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 24:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What are your career goals within the company?"
|
|
||||||
value={formData.careerGoals || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, careerGoals: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={4}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Skills & Development"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 25:
|
|
||||||
return (
|
|
||||||
<SectionIntro
|
|
||||||
sectionNumber="6 of 6"
|
|
||||||
title="Feedback & Improvement"
|
|
||||||
description="Finally, let's gather your thoughts on company improvements and overall satisfaction."
|
|
||||||
onStart={() => handleNext()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 26:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="What improvements would you suggest for the company?"
|
|
||||||
value={formData.companyImprovements || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, companyImprovements: value })}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={1}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Feedback & Improvement"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 27:
|
|
||||||
return (
|
|
||||||
<RatingScaleQuestion
|
|
||||||
question="How satisfied are you with your current job overall?"
|
|
||||||
leftLabel="Not satisfied"
|
|
||||||
rightLabel="Very satisfied"
|
|
||||||
value={formData.jobSatisfaction}
|
|
||||||
onChange={(value) => setFormData({ ...formData, jobSatisfaction: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleNext()}
|
|
||||||
currentStep={2}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Feedback & Improvement"
|
|
||||||
scale={10}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 28:
|
|
||||||
return (
|
|
||||||
<TextAreaQuestion
|
|
||||||
question="Any additional feedback or suggestions for the company?"
|
|
||||||
value={formData.additionalFeedback || ''}
|
|
||||||
onChange={(value) => setFormData({ ...formData, additionalFeedback: value })}
|
|
||||||
onBack={() => handleBack()}
|
|
||||||
onNext={() => handleSubmit()}
|
|
||||||
currentStep={3}
|
|
||||||
totalSteps={7}
|
|
||||||
sectionName="Feedback & Improvement"
|
|
||||||
placeholder="Share any thoughts, suggestions, or feedback..."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 999: // Thank you page
|
|
||||||
return <ThankYouPage />;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return <ThankYouPage />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSubmitting) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<div className="w-16 h-16 bg-[--Brand-Orange] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4 animate-pulse">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--Neutrals-NeutralSlate950] mb-4">Submitting Your Responses...</h1>
|
|
||||||
<p className="text-[--Neutrals-NeutralSlate500]">Please wait while we process your assessment and generate your report.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen bg-[--Neutrals-NeutralSlate0]">
|
|
||||||
{renderStep()}
|
|
||||||
{error && (
|
|
||||||
<div className="fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeQuestionnaire;
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
|
||||||
import { Card, Button } from '../components/UiKit';
|
|
||||||
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
|
|
||||||
import { Question } from '../components/ui/Question';
|
|
||||||
import { QuestionInput } from '../components/ui/QuestionInput';
|
|
||||||
import { FigmaQuestion } from '../components/figma/FigmaQuestion';
|
|
||||||
import { EnhancedFigmaQuestion } from '../components/figma/EnhancedFigmaQuestion';
|
|
||||||
import { LinearProgress } from '../components/ui/Progress';
|
|
||||||
import { Alert } from '../components/ui/Alert';
|
|
||||||
|
|
||||||
const EmployeeQuestionnaireSteps: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { submitEmployeeAnswers, generateEmployeeReport, employees } = useOrg();
|
|
||||||
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
// Get employee info from multiple sources
|
|
||||||
const invitedEmployee = location.state?.invitedEmployee;
|
|
||||||
|
|
||||||
// Find current employee info - try multiple strategies
|
|
||||||
let currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
|
|
||||||
|
|
||||||
// Additional matching strategies for edge cases
|
|
||||||
if (!currentEmployee && user?.email) {
|
|
||||||
// Try case-insensitive email matching
|
|
||||||
currentEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase() === user.email?.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Try matching by name if email doesn't work (for invite flow)
|
|
||||||
if (!currentEmployee && invitedEmployee) {
|
|
||||||
currentEmployee = employees.find(emp =>
|
|
||||||
emp.name === invitedEmployee.name || emp.id === invitedEmployee.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Demo mode fallbacks
|
|
||||||
if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) {
|
|
||||||
currentEmployee = employees[employees.length - 1];
|
|
||||||
}
|
|
||||||
if (!currentEmployee && employees.length === 1) {
|
|
||||||
currentEmployee = employees[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out followup questions that shouldn't be shown yet
|
|
||||||
const getVisibleQuestions = () => {
|
|
||||||
return EMPLOYEE_QUESTIONS.filter(question => {
|
|
||||||
if (!question.followupTo) return true;
|
|
||||||
|
|
||||||
const parentAnswer = answers[question.followupTo];
|
|
||||||
if (question.followupTo === 'has_kpis') {
|
|
||||||
return parentAnswer === 'Yes';
|
|
||||||
}
|
|
||||||
if (question.followupTo === 'unclear_responsibilities') {
|
|
||||||
return parentAnswer === 'Yes';
|
|
||||||
}
|
|
||||||
if (question.followupTo === 'role_shift_interest') {
|
|
||||||
return parentAnswer === 'Yes';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const visibleQuestions = getVisibleQuestions();
|
|
||||||
const currentQuestion = visibleQuestions[currentStep];
|
|
||||||
|
|
||||||
const handleAnswerChange = (value: string) => {
|
|
||||||
if (currentQuestion) {
|
|
||||||
setAnswers(prev => ({ ...prev, [currentQuestion.id]: value }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
if (currentStep < visibleQuestions.length - 1) {
|
|
||||||
setCurrentStep(prev => prev + 1);
|
|
||||||
} else {
|
|
||||||
handleSubmit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
if (currentStep > 0) {
|
|
||||||
setCurrentStep(prev => prev - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate required questions
|
|
||||||
const requiredQuestions = visibleQuestions.filter(q => q.required);
|
|
||||||
const missingAnswers = requiredQuestions.filter(q => !answers[q.id]?.trim());
|
|
||||||
|
|
||||||
if (missingAnswers.length > 0) {
|
|
||||||
setError(`Please answer all required questions: ${missingAnswers.map(q => q.prompt).join(', ')}`);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentEmployee) {
|
|
||||||
// Enhanced fallback logic
|
|
||||||
if (employees.length > 0) {
|
|
||||||
// Try to find employee by matching with the user's email more aggressively
|
|
||||||
let fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
|
|
||||||
);
|
|
||||||
|
|
||||||
// If still no match, use the most recent employee or one with matching domain
|
|
||||||
if (!fallbackEmployee) {
|
|
||||||
const userDomain = user?.email?.split('@')[1];
|
|
||||||
fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.split('@')[1] === userDomain
|
|
||||||
) || employees[employees.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Using enhanced fallback employee:', fallbackEmployee);
|
|
||||||
|
|
||||||
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
|
|
||||||
if (success) {
|
|
||||||
try {
|
|
||||||
const report = await generateEmployeeReport(fallbackEmployee);
|
|
||||||
if (report) {
|
|
||||||
console.log('Report generated successfully for fallback employee:', report);
|
|
||||||
}
|
|
||||||
} catch (reportError) {
|
|
||||||
console.error('Failed to generate report for fallback employee:', reportError);
|
|
||||||
}
|
|
||||||
navigate('/questionnaire-complete', {
|
|
||||||
replace: true,
|
|
||||||
state: {
|
|
||||||
employeeId: fallbackEmployee.id,
|
|
||||||
employeeName: fallbackEmployee.name,
|
|
||||||
message: 'Questionnaire submitted successfully! Your responses have been recorded.'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator to ensure your invite was set up correctly.`);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
const message = result
|
|
||||||
? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.'
|
|
||||||
: 'Questionnaire submitted successfully! Your report will be available shortly.';
|
|
||||||
|
|
||||||
navigate('/questionnaire-complete', {
|
|
||||||
state: {
|
|
||||||
employeeId: currentEmployee.id,
|
|
||||||
employeeName: currentEmployee.name,
|
|
||||||
reportGenerated: result,
|
|
||||||
message: message
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setError(result.message || 'Failed to submit questionnaire');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Submission error:', error);
|
|
||||||
setError('Failed to submit questionnaire. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProgressPercentage = () => {
|
|
||||||
return Math.round(((currentStep + 1) / visibleQuestions.length) * 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCurrentQuestionAnswered = () => {
|
|
||||||
if (!currentQuestion) return false;
|
|
||||||
const answer = answers[currentQuestion.id];
|
|
||||||
return answer && answer.trim().length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!currentQuestion) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<h1 className="text-2xl font-bold text-[--text-primary] mb-4">
|
|
||||||
No questions available
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
|
|
||||||
Welcome to Auditly!
|
|
||||||
</h1>
|
|
||||||
<p className="text-[--text-secondary] mb-4">
|
|
||||||
Please complete this questionnaire to help us understand your role and create personalized insights.
|
|
||||||
</p>
|
|
||||||
{currentEmployee ? (
|
|
||||||
<div className="inline-flex items-center px-4 py-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
|
||||||
<span className="text-sm text-blue-800 dark:text-blue-200">
|
|
||||||
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="inline-flex items-center px-4 py-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
|
|
||||||
<span className="text-sm text-yellow-800 dark:text-yellow-200">
|
|
||||||
⚠️ Employee info not found. User: {user?.email}, Employees: {employees.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[--text-secondary] max-w-md mx-auto">
|
|
||||||
<p>Don't worry - your account was created successfully! This is likely a temporary sync issue.</p>
|
|
||||||
<p className="mt-1">You can still complete the questionnaire, and we'll match it to your profile automatically.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm text-[--text-secondary]">
|
|
||||||
Question {currentStep + 1} of {visibleQuestions.length}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-[--text-secondary]">{getProgressPercentage()}%</span>
|
|
||||||
</div>
|
|
||||||
<LinearProgress value={getProgressPercentage()} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enhanced Question Card */}
|
|
||||||
<div className="flex justify-center mb-8">
|
|
||||||
<div className="w-full max-w-2xl">
|
|
||||||
<EnhancedFigmaQuestion
|
|
||||||
questionNumber={`Q${currentStep + 1}`}
|
|
||||||
question={currentQuestion.prompt}
|
|
||||||
answer={answers[currentQuestion.id] || ''}
|
|
||||||
onAnswerChange={handleAnswerChange}
|
|
||||||
onBack={currentStep > 0 ? handleBack : undefined}
|
|
||||||
onNext={
|
|
||||||
currentStep < visibleQuestions.length - 1
|
|
||||||
? handleNext
|
|
||||||
: (isCurrentQuestionAnswered() || !currentQuestion.required)
|
|
||||||
? handleSubmit
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
nextLabel={
|
|
||||||
currentStep < visibleQuestions.length - 1
|
|
||||||
? 'Next'
|
|
||||||
: isSubmitting
|
|
||||||
? 'Submitting...'
|
|
||||||
: 'Submit & Generate Report'
|
|
||||||
}
|
|
||||||
showNavigation={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Original Figma Question Card for Comparison */}
|
|
||||||
<div className="flex justify-center mb-8">
|
|
||||||
<div className="w-full max-w-2xl">
|
|
||||||
<FigmaQuestion
|
|
||||||
questionNumber={`Q${currentStep + 1}`}
|
|
||||||
title={currentQuestion.prompt}
|
|
||||||
description={currentQuestion.required ? 'Required' : 'Optional'}
|
|
||||||
answer={answers[currentQuestion.id] || ''}
|
|
||||||
onAnswerChange={handleAnswerChange}
|
|
||||||
onBack={currentStep > 0 ? handleBack : undefined}
|
|
||||||
onNext={
|
|
||||||
currentStep < visibleQuestions.length - 1
|
|
||||||
? handleNext
|
|
||||||
: (isCurrentQuestionAnswered() || !currentQuestion.required)
|
|
||||||
? handleSubmit
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
nextLabel={
|
|
||||||
currentStep < visibleQuestions.length - 1
|
|
||||||
? 'Next'
|
|
||||||
: isSubmitting
|
|
||||||
? 'Submitting...'
|
|
||||||
: 'Submit & Generate Report'
|
|
||||||
}
|
|
||||||
showNavigation={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Alternative Input for Different Question Types */}
|
|
||||||
<div className="flex justify-center mb-8">
|
|
||||||
<div className="w-full max-w-2xl">
|
|
||||||
<Card className="p-6">
|
|
||||||
<Question
|
|
||||||
label={`${currentStep + 1}. ${currentQuestion.prompt}`}
|
|
||||||
required={currentQuestion.required}
|
|
||||||
description={`Category: ${currentQuestion.category}`}
|
|
||||||
>
|
|
||||||
<QuestionInput
|
|
||||||
question={currentQuestion}
|
|
||||||
value={answers[currentQuestion.id] || ''}
|
|
||||||
onChange={handleAnswerChange}
|
|
||||||
/>
|
|
||||||
</Question>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Display */}
|
|
||||||
{error && (
|
|
||||||
<div className="mb-6 flex justify-center">
|
|
||||||
<div className="w-full max-w-2xl">
|
|
||||||
<Alert variant="error" title="Error">
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
|
||||||
<div className="flex justify-center gap-4">
|
|
||||||
{currentStep > 0 && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleBack}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep < visibleQuestions.length - 1 ? (
|
|
||||||
<Button
|
|
||||||
onClick={handleNext}
|
|
||||||
disabled={currentQuestion.required && !isCurrentQuestionAnswered()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={
|
|
||||||
isSubmitting ||
|
|
||||||
(currentQuestion.required && !isCurrentQuestionAnswered())
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help Text */}
|
|
||||||
<div className="text-center mt-6">
|
|
||||||
<p className="text-sm text-[--text-secondary]">
|
|
||||||
{currentQuestion.required && !isCurrentQuestionAnswered()
|
|
||||||
? 'Please answer this required question to continue.'
|
|
||||||
: 'You can skip optional questions or come back to them later.'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeQuestionnaireSteps;
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
// DEPRECATED: This component has been split into separate Help and Settings pages
|
|
||||||
// Use /src/pages/HelpNew.tsx and /src/pages/SettingsNew.tsx instead
|
|
||||||
// This file can be safely removed in future cleanup
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
|
||||||
import { Card, Button } from '../components/UiKit';
|
|
||||||
import { Theme } from '../types';
|
|
||||||
|
|
||||||
const HelpAndSettings: React.FC = () => {
|
|
||||||
const { theme, setTheme } = useTheme();
|
|
||||||
const { user, signOutUser } = useAuth();
|
|
||||||
const { org, upsertOrg, issueInviteViaApi } = useOrg();
|
|
||||||
const [activeTab, setActiveTab] = useState<'settings' | 'help'>('settings');
|
|
||||||
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
|
|
||||||
const [isInviting, setIsInviting] = useState(false);
|
|
||||||
const [inviteResult, setInviteResult] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await signOutUser();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRestartOnboarding = async () => {
|
|
||||||
try {
|
|
||||||
await upsertOrg({ onboardingCompleted: false });
|
|
||||||
// The RequireOnboarding component will redirect automatically
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to restart onboarding:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInviteEmployee = async () => {
|
|
||||||
if (!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting) return;
|
|
||||||
|
|
||||||
setIsInviting(true);
|
|
||||||
setInviteResult(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await issueInviteViaApi({
|
|
||||||
name: inviteForm.name.trim(),
|
|
||||||
email: inviteForm.email.trim(),
|
|
||||||
role: inviteForm.role.trim() || undefined,
|
|
||||||
department: inviteForm.department.trim() || undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
setInviteResult(JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
inviteLink: result.inviteLink,
|
|
||||||
emailLink: result.emailLink,
|
|
||||||
employeeName: result.employee.name
|
|
||||||
}));
|
|
||||||
setInviteForm({ name: '', email: '', role: '', department: '' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to send invitation:', error);
|
|
||||||
setInviteResult('Failed to send invitation. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setIsInviting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSettings = () => (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Appearance</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-2">
|
|
||||||
Theme
|
|
||||||
</label>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button
|
|
||||||
variant={theme === Theme.Light ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setTheme(Theme.Light)}
|
|
||||||
>
|
|
||||||
Light
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={theme === Theme.Dark ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setTheme(Theme.Dark)}
|
|
||||||
>
|
|
||||||
Dark
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={theme === Theme.System ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setTheme(Theme.System)}
|
|
||||||
>
|
|
||||||
System
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Organization</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-[--text-secondary]">Company:</span>
|
|
||||||
<div className="font-medium text-[--text-primary]">{org?.name}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-[--text-secondary]">Onboarding:</span>
|
|
||||||
<div className="font-medium text-[--text-primary]">
|
|
||||||
{org?.onboardingCompleted ? 'Completed' : 'Incomplete'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pt-4">
|
|
||||||
<Button variant="secondary" onClick={handleRestartOnboarding}>
|
|
||||||
Restart Onboarding
|
|
||||||
</Button>
|
|
||||||
<p className="text-xs text-[--text-secondary] mt-2">
|
|
||||||
This will reset your company profile and require you to complete the setup process again.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
||||||
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-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="John Doe"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
||||||
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-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="john.doe@company.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
||||||
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-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Senior Developer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[--text-primary] mb-1">
|
|
||||||
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-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Engineering"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleInviteEmployee}
|
|
||||||
disabled={!inviteForm.name.trim() || !inviteForm.email.trim() || isInviting}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{isInviting ? 'Sending Invitation...' : 'Send Invitation'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{inviteResult && (
|
|
||||||
<div>
|
|
||||||
{inviteResult.includes('Failed') ? (
|
|
||||||
<div className="p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200">
|
|
||||||
{inviteResult}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
(() => {
|
|
||||||
try {
|
|
||||||
const result = JSON.parse(inviteResult);
|
|
||||||
return (
|
|
||||||
<div className="p-4 rounded-md bg-green-50 border border-green-200">
|
|
||||||
<h4 className="text-sm font-semibold text-green-800 mb-3">
|
|
||||||
✅ Invitation sent to {result.employeeName}!
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-green-700 mb-1">
|
|
||||||
Direct Link (share this with the employee):
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={result.inviteLink}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 px-2 py-1 text-xs bg-white border border-green-300 rounded font-mono"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => navigator.clipboard.writeText(result.inviteLink)}
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href={result.emailLink}
|
|
||||||
className="inline-flex items-center px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
📧 Open Email Draft
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return (
|
|
||||||
<div className="p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200">
|
|
||||||
{inviteResult}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className="text-xs text-[--text-secondary]">
|
|
||||||
The invited employee will receive an email with instructions to join your organization.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Account</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-[--text-secondary]">Email:</span>
|
|
||||||
<div className="font-medium text-[--text-primary]">{user?.email}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-[--text-secondary]">User ID:</span>
|
|
||||||
<div className="font-medium text-[--text-primary] font-mono text-xs">{user?.uid}</div>
|
|
||||||
</div>
|
|
||||||
<div className="pt-4">
|
|
||||||
<Button variant="secondary" onClick={handleLogout}>
|
|
||||||
Sign Out
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Data & Privacy</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
|
||||||
Export My Data
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
|
||||||
Privacy Settings
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" className="w-full justify-start text-red-600">
|
|
||||||
Delete Account
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderHelp = () => (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Getting Started</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">1. Set up your organization</h4>
|
|
||||||
<p className="text-[--text-secondary] text-sm">
|
|
||||||
Complete the onboarding process to configure your company information and preferences.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">2. Add employees</h4>
|
|
||||||
<p className="text-[--text-secondary] text-sm">
|
|
||||||
Invite team members and add their basic information to start generating reports.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">3. Generate reports</h4>
|
|
||||||
<p className="text-[--text-secondary] text-sm">
|
|
||||||
Use AI-powered reports to gain insights into employee performance and organizational health.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Frequently Asked Questions</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">How do I add new employees?</h4>
|
|
||||||
<p className="text-[--text-secondary] text-sm">
|
|
||||||
Go to the Reports page and use the "Add Employee" button to invite new team members.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">How are reports generated?</h4>
|
|
||||||
<p className="text-[--text-secondary] text-sm">
|
|
||||||
Reports use AI to analyze employee data and provide insights on performance, strengths, and development opportunities.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">Is my data secure?</h4>
|
|
||||||
<p className="text-[--text-secondary] text-sm">
|
|
||||||
Yes, all data is encrypted and stored securely. We follow industry best practices for data protection.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Contact Support</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
|
||||||
📧 Email Support
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
|
||||||
💬 Live Chat
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" className="w-full justify-start">
|
|
||||||
📚 Documentation
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary]">Help & Settings</h1>
|
|
||||||
<p className="text-[--text-secondary] mt-1">
|
|
||||||
Manage your account and get help
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex space-x-4 border-b border-[--border-color]">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('settings')}
|
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'settings'
|
|
||||||
? 'border-blue-500 text-blue-500'
|
|
||||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('help')}
|
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${activeTab === 'help'
|
|
||||||
? 'border-blue-500 text-blue-500'
|
|
||||||
: 'border-transparent text-[--text-secondary] hover:text-[--text-primary]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Help
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeTab === 'settings' ? renderSettings() : renderHelp()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HelpAndSettings;
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { CheckCircle, FileText, Sparkles } from 'lucide-react';
|
|
||||||
|
|
||||||
const QuestionnaireComplete: React.FC = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const {
|
|
||||||
employeeName = 'Employee',
|
|
||||||
reportGenerated = false,
|
|
||||||
message = 'Thank you for completing the questionnaire!'
|
|
||||||
} = location.state || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] flex items-center justify-center p-4">
|
|
||||||
<div className="max-w-md w-full bg-[--background-secondary] rounded-lg shadow-lg p-8 text-center">
|
|
||||||
<div className="flex justify-center mb-6">
|
|
||||||
{reportGenerated ? (
|
|
||||||
<div className="relative">
|
|
||||||
<CheckCircle className="w-16 h-16 text-green-500" />
|
|
||||||
<Sparkles className="w-6 h-6 text-yellow-400 absolute -top-1 -right-1" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<CheckCircle className="w-16 h-16 text-green-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold text-[--text-primary] mb-4">
|
|
||||||
Questionnaire Complete!
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-[--text-secondary] mb-6">
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{reportGenerated && (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
|
|
||||||
<div className="flex items-center justify-center mb-2">
|
|
||||||
<FileText className="w-5 h-5 text-blue-600 mr-2" />
|
|
||||||
<span className="text-sm font-medium text-blue-600">AI Report Generated</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-blue-600 dark:text-blue-300">
|
|
||||||
Your personalized performance report has been created using AI analysis of your responses.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/reports')}
|
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{reportGenerated ? 'View Your Report' : 'Go to Dashboard'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
className="w-full bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Return to Home
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QuestionnaireComplete;
|
|
||||||
297
src/pages/Settings.tsx
Normal file
297
src/pages/Settings.tsx
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import Sidebar from '../components/figma/Sidebar';
|
||||||
|
import { Button } from '../components/UiKit';
|
||||||
|
import { Theme } from '../types';
|
||||||
|
|
||||||
|
interface UserProfile {
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
profilePicture?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeMode = 'system' | 'light' | 'dark';
|
||||||
|
|
||||||
|
const SettingsNew: React.FC = () => {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
|
||||||
|
const [userProfile, setUserProfile] = useState<UserProfile>({
|
||||||
|
fullName: 'John Doe',
|
||||||
|
email: 'Johndoe1234@gmail.com'
|
||||||
|
});
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
|
||||||
|
|
||||||
|
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
|
||||||
|
setUserProfile(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhotoUpload = () => {
|
||||||
|
// In a real app, this would open a file picker
|
||||||
|
alert('Photo upload functionality would be implemented here');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveChanges = () => {
|
||||||
|
// In a real app, this would save to backend
|
||||||
|
alert('Settings saved successfully!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setUserProfile({
|
||||||
|
fullName: 'John Doe',
|
||||||
|
email: 'Johndoe1234@gmail.com'
|
||||||
|
});
|
||||||
|
setSelectedTheme('light');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
navigate('/login');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 self-stretch shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start h-full">
|
||||||
|
<Sidebar companyName="Zitlac Media" />
|
||||||
|
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="self-stretch px-6 pt-6 border-b border-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-end">
|
||||||
|
<div className="self-stretch inline-flex justify-start items-start gap-6">
|
||||||
|
<div
|
||||||
|
onClick={() => setActiveTab('general')}
|
||||||
|
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'general'
|
||||||
|
? 'text-Text-Gray-800 font-semibold'
|
||||||
|
: 'text-Text-Gray-500 font-normal'
|
||||||
|
}`}>
|
||||||
|
General Settings
|
||||||
|
</div>
|
||||||
|
{activeTab === 'general' && (
|
||||||
|
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => setActiveTab('billing')}
|
||||||
|
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
|
||||||
|
? 'text-Text-Gray-800 font-semibold'
|
||||||
|
: 'text-Text-Gray-500 font-normal'
|
||||||
|
}`}>
|
||||||
|
Plan & Billings
|
||||||
|
</div>
|
||||||
|
{activeTab === 'billing' && (
|
||||||
|
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* General Settings Content */}
|
||||||
|
{activeTab === 'general' && (
|
||||||
|
<>
|
||||||
|
{/* Profile Information Section */}
|
||||||
|
<div className="w-full h-72 p-6 flex flex-col justify-start items-start gap-6">
|
||||||
|
<div className="flex flex-col justify-start items-start gap-1">
|
||||||
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-lg font-semibold font-['Inter'] leading-7">Profile Information</div>
|
||||||
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-sm font-normal font-['Inter'] leading-tight">Update your personal details, and keep your profile up to date.</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||||
|
{/* Profile Picture Section */}
|
||||||
|
<div className="w-[664px] px-3 py-2.5 bg-[--Neutrals-NeutralSlate0] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex justify-between items-center">
|
||||||
|
<div className="flex-1 flex justify-start items-center gap-3">
|
||||||
|
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
|
||||||
|
<div>
|
||||||
|
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_1042_3786)">
|
||||||
|
<ellipse cx="28" cy="54.6008" rx="22.4" ry="16.8" fill="white" fillOpacity="0.72" />
|
||||||
|
<circle opacity="0.9" cx="28" cy="22.3992" r="11.2" fill="white" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1042_3786">
|
||||||
|
<rect width="56" height="56" rx="28" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 inline-flex flex-col justify-start items-start gap-1">
|
||||||
|
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate800] text-base font-semibold font-['Inter'] leading-normal">Profile Picture</div>
|
||||||
|
<div className="self-stretch justify-center text-[--Neutrals-NeutralSlate500] text-xs font-normal font-['Inter'] leading-none">PNG, JPEG, GIF Under 10MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={handlePhotoUpload}
|
||||||
|
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.66665 10.8282C1.86266 10.29 1.33331 9.37347 1.33331 8.33333C1.33331 6.77095 2.52765 5.48753 4.05314 5.34625C4.36519 3.44809 6.01348 2 7.99998 2C9.98648 2 11.6348 3.44809 11.9468 5.34625C13.4723 5.48753 14.6666 6.77095 14.6666 8.33333C14.6666 9.37347 14.1373 10.29 13.3333 10.8282M5.33331 10.6667L7.99998 8M7.99998 8L10.6666 10.6667M7.99998 8V14" stroke="var(--Neutrals-NeutralSlate950)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="px-1 flex justify-center items-center">
|
||||||
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload Photo</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Name and Email Fields */}
|
||||||
|
<div className="w-[664px] inline-flex justify-start items-center gap-4">
|
||||||
|
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||||
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Full Name</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
|
<div>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 12.5C7.35828 12.5 5.00901 13.7755 3.51334 15.755C3.19143 16.181 3.03047 16.394 3.03574 16.6819C3.03981 16.9043 3.17948 17.1849 3.35448 17.3222C3.581 17.5 3.8949 17.5 4.5227 17.5H15.4773C16.1051 17.5 16.419 17.5 16.6455 17.3222C16.8205 17.1849 16.9602 16.9043 16.9643 16.6819C16.9695 16.394 16.8086 16.181 16.4867 15.755C14.991 13.7755 12.6417 12.5 10 12.5Z" stroke="var(--Neutrals-NeutralSlate600)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<path d="M10 10C12.0711 10 13.75 8.32107 13.75 6.25C13.75 4.17893 12.0711 2.5 10 2.5C7.92894 2.5 6.25001 4.17893 6.25001 6.25C6.25001 8.32107 7.92894 10 10 10Z" stroke="var(--Neutrals-NeutralSlate600)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={userProfile.fullName}
|
||||||
|
onChange={(e) => handleProfileUpdate('fullName', e.target.value)}
|
||||||
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||||
|
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||||
|
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email Address</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||||
|
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||||
|
<div>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.66669 5.83203L8.47079 10.5949C9.02176 10.9806 9.29725 11.1734 9.59691 11.2481C9.8616 11.3141 10.1384 11.3141 10.4031 11.2481C10.7028 11.1734 10.9783 10.9806 11.5293 10.5949L18.3334 5.83203M5.66669 16.6654H14.3334C15.7335 16.6654 16.4336 16.6654 16.9683 16.3929C17.4387 16.1532 17.8212 15.7707 18.0609 15.3003C18.3334 14.7656 18.3334 14.0655 18.3334 12.6654V7.33203C18.3334 5.9319 18.3334 5.23183 18.0609 4.69705C17.8212 4.22665 17.4387 3.8442 16.9683 3.60451C16.4336 3.33203 15.7335 3.33203 14.3334 3.33203H5.66669C4.26656 3.33203 3.56649 3.33203 3.03171 3.60451C2.56131 3.8442 2.17885 4.22665 1.93917 4.69705C1.66669 5.23183 1.66669 5.9319 1.66669 7.33203V12.6654C1.66669 14.0655 1.66669 14.7656 1.93917 15.3003C2.17885 15.7707 2.56131 16.1532 3.03171 16.3929C3.56649 16.6654 4.26656 16.6654 5.66669 16.6654Z" stroke="var(--Neutrals-NeutralSlate600)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={userProfile.email}
|
||||||
|
onChange={(e) => handleProfileUpdate('email', e.target.value)}
|
||||||
|
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div>
|
||||||
|
<svg width="100%" height="2" viewBox="0 0 2000 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 1H2000" stroke="var(--Neutrals-NeutralSlate200)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Theme Customization Section */}
|
||||||
|
<div className="w-full self-stretch p-6 flex flex-col justify-start items-start gap-6">
|
||||||
|
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
|
||||||
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-lg font-semibold font-['Inter'] leading-7">Theme Customization</div>
|
||||||
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-sm font-normal font-['Inter'] leading-tight">Personalize your interface with light or dark mode and enhance your visual experience.</div>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
|
||||||
|
{/* System Preference */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedTheme('system')}
|
||||||
|
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button className="inline-flex justify-start items-center" onClick={() => setTheme(Theme.System)}>
|
||||||
|
<img className="w-24 h-28 rounded-tl-lg rounded-bl-lg" src="/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png"></img>
|
||||||
|
<img className="w-24 h-28 rounded-tr-lg rounded-br-lg" src="/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png"></img>
|
||||||
|
</button>
|
||||||
|
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">System preference</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Light Mode */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedTheme('light')}
|
||||||
|
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="self-stretch h-28 relative bg-[--Neutrals-NeutralSlate0] rounded-lg overflow-hidden">
|
||||||
|
<div className={`w-48 h-28 left-0 top-0 absolute bg-[--Neutrals-NeutralSlate0] rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-[--Neutrals-NeutralSlate200]'
|
||||||
|
}`}>
|
||||||
|
<button className="w-48 h-28 left-0 top-0 absolute rounded-lg" style={{ backgroundImage: 'url("/image/39a0d5e73dec8bece795a718c5800f02df8f8631.png")', backgroundSize: "cover" }} onClick={() => setTheme(Theme.Light)}></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch h-5 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">Light Mode</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dark Mode */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedTheme('dark')}
|
||||||
|
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="w-48 h-28 relative bg-[--Neutrals-NeutralSlate950] rounded-lg overflow-hidden">
|
||||||
|
<div className={`w-48 h-28 left-0 top-0 absolute bg-[--Neutrals-NeutralSlate0] rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-[--Neutrals-NeutralSlate200]'
|
||||||
|
}`}>
|
||||||
|
<button className="w-48 h-28 left-0 top-0 absolute rounded-lg" style={{ backgroundImage: 'url("/image/bc07fdc9eec8a78357aaf70e9deae41d4b7a7d2d.png")', backgroundSize: "cover" }} onClick={() => setTheme(Theme.Dark)}></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch h-5 justify-start text-[--Neutrals-NeutralSlate800] text-sm font-normal font-['Inter'] leading-tight">Dark Mode</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Another Divider */}
|
||||||
|
<div>
|
||||||
|
<svg width="2000" height="2" viewBox="0 0 2000 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 1H2000" stroke="var(--Neutrals-NeutralSlate200)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
|
||||||
|
<div
|
||||||
|
onClick={handleReset}
|
||||||
|
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
||||||
|
>
|
||||||
|
<div className="px-1 flex justify-center items-center">
|
||||||
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reset</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={handleSaveChanges}
|
||||||
|
className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Brand-Orange]/90"
|
||||||
|
>
|
||||||
|
<div className="px-1 flex justify-center items-center">
|
||||||
|
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Save Changes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Billing Content */}
|
||||||
|
{activeTab === 'billing' && (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-semibold text-[--Neutrals-NeutralSlate800] mb-4">Plan & Billing</h2>
|
||||||
|
<p className="text-[--Neutrals-NeutralSlate500]">Billing management features would be implemented here.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsNew;
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
|
||||||
import Sidebar from '../components/figma/Sidebar';
|
|
||||||
|
|
||||||
interface UserProfile {
|
|
||||||
fullName: string;
|
|
||||||
email: string;
|
|
||||||
profilePicture?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ThemeMode = 'system' | 'light' | 'dark';
|
|
||||||
|
|
||||||
const SettingsNew: React.FC = () => {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'general' | 'billing'>('general');
|
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile>({
|
|
||||||
fullName: 'John Doe',
|
|
||||||
email: 'Johndoe1234@gmail.com'
|
|
||||||
});
|
|
||||||
const [selectedTheme, setSelectedTheme] = useState<ThemeMode>('light');
|
|
||||||
|
|
||||||
const handleProfileUpdate = (field: keyof UserProfile, value: string) => {
|
|
||||||
setUserProfile(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePhotoUpload = () => {
|
|
||||||
// In a real app, this would open a file picker
|
|
||||||
alert('Photo upload functionality would be implemented here');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveChanges = () => {
|
|
||||||
// In a real app, this would save to backend
|
|
||||||
alert('Settings saved successfully!');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setUserProfile({
|
|
||||||
fullName: 'John Doe',
|
|
||||||
email: 'Johndoe1234@gmail.com'
|
|
||||||
});
|
|
||||||
setSelectedTheme('light');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
navigate('/login');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-[1440px] h-[840px] p-4 bg-[--Neutrals-NeutralSlate200] inline-flex justify-start items-start overflow-hidden">
|
|
||||||
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
|
|
||||||
<Sidebar companyName="Zitlac Media" />
|
|
||||||
<div className="flex-1 self-stretch bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-start items-start">
|
|
||||||
{/* Tab Navigation */}
|
|
||||||
<div className="self-stretch px-6 pt-6 border-b border-Outline-Outline-Gray-200 flex flex-col justify-start items-end">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-start gap-6">
|
|
||||||
<div
|
|
||||||
onClick={() => setActiveTab('general')}
|
|
||||||
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'general'
|
|
||||||
? 'text-Text-Gray-800 font-semibold'
|
|
||||||
: 'text-Text-Gray-500 font-normal'
|
|
||||||
}`}>
|
|
||||||
General Settings
|
|
||||||
</div>
|
|
||||||
{activeTab === 'general' && (
|
|
||||||
<div className="self-stretch h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => setActiveTab('billing')}
|
|
||||||
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
|
|
||||||
? 'text-Text-Gray-800 font-semibold'
|
|
||||||
: 'text-Text-Gray-500 font-normal'
|
|
||||||
}`}>
|
|
||||||
Plan & Billings
|
|
||||||
</div>
|
|
||||||
{activeTab === 'billing' && (
|
|
||||||
<div className="w-24 h-0.5 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-24 h-0.5 opacity-0 bg-[--Neutrals-NeutralSlate800] rounded-tl-lg rounded-tr-lg" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* General Settings Content */}
|
|
||||||
{activeTab === 'general' && (
|
|
||||||
<>
|
|
||||||
{/* Profile Information Section */}
|
|
||||||
<div className="w-[1136px] h-72 p-6 flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Profile Information</div>
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Update your personal details, and keep your profile up to date.</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
|
||||||
{/* Profile Picture Section */}
|
|
||||||
<div className="w-[664px] px-3 py-2.5 bg-Text-White-00 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 inline-flex justify-between items-center">
|
|
||||||
<div className="flex-1 flex justify-start items-center gap-3">
|
|
||||||
<div className="w-14 h-14 relative bg-red-200 rounded-[999px]">
|
|
||||||
<div>
|
|
||||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipPath="url(#clip0_1042_3786)">
|
|
||||||
<ellipse cx="28" cy="54.6008" rx="22.4" ry="16.8" fill="white" fillOpacity="0.72" />
|
|
||||||
<circle opacity="0.9" cx="28" cy="22.3992" r="11.2" fill="white" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1042_3786">
|
|
||||||
<rect width="56" height="56" rx="28" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch justify-center text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">Profile Picture</div>
|
|
||||||
<div className="self-stretch justify-center text-Text-Gray-500 text-xs font-normal font-['Inter'] leading-none">PNG, JPEG, GIF Under 10MB</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={handlePhotoUpload}
|
|
||||||
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2.66665 10.8282C1.86266 10.29 1.33331 9.37347 1.33331 8.33333C1.33331 6.77095 2.52765 5.48753 4.05314 5.34625C4.36519 3.44809 6.01348 2 7.99998 2C9.98648 2 11.6348 3.44809 11.9468 5.34625C13.4723 5.48753 14.6666 6.77095 14.6666 8.33333C14.6666 9.37347 14.1373 10.29 13.3333 10.8282M5.33331 10.6667L7.99998 8M7.99998 8L10.6666 10.6667M7.99998 8V14" stroke="var(--Text-Dark-950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Upload Photo</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Name and Email Fields */}
|
|
||||||
<div className="w-[664px] inline-flex justify-start items-center gap-4">
|
|
||||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Full Name</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
|
||||||
<div>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M10 12.5C7.35828 12.5 5.00901 13.7755 3.51334 15.755C3.19143 16.181 3.03047 16.394 3.03574 16.6819C3.03981 16.9043 3.17948 17.1849 3.35448 17.3222C3.581 17.5 3.8949 17.5 4.5227 17.5H15.4773C16.1051 17.5 16.419 17.5 16.6455 17.3222C16.8205 17.1849 16.9602 16.9043 16.9643 16.6819C16.9695 16.394 16.8086 16.181 16.4867 15.755C14.991 13.7755 12.6417 12.5 10 12.5Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
<path d="M10 10C12.0711 10 13.75 8.32107 13.75 6.25C13.75 4.17893 12.0711 2.5 10 2.5C7.92894 2.5 6.25001 4.17893 6.25001 6.25C6.25001 8.32107 7.92894 10 10 10Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={userProfile.fullName}
|
|
||||||
onChange={(e) => handleProfileUpdate('fullName', e.target.value)}
|
|
||||||
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">Email Address</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
|
||||||
<div>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1.66669 5.83203L8.47079 10.5949C9.02176 10.9806 9.29725 11.1734 9.59691 11.2481C9.8616 11.3141 10.1384 11.3141 10.4031 11.2481C10.7028 11.1734 10.9783 10.9806 11.5293 10.5949L18.3334 5.83203M5.66669 16.6654H14.3334C15.7335 16.6654 16.4336 16.6654 16.9683 16.3929C17.4387 16.1532 17.8212 15.7707 18.0609 15.3003C18.3334 14.7656 18.3334 14.0655 18.3334 12.6654V7.33203C18.3334 5.9319 18.3334 5.23183 18.0609 4.69705C17.8212 4.22665 17.4387 3.8442 16.9683 3.60451C16.4336 3.33203 15.7335 3.33203 14.3334 3.33203H5.66669C4.26656 3.33203 3.56649 3.33203 3.03171 3.60451C2.56131 3.8442 2.17885 4.22665 1.93917 4.69705C1.66669 5.23183 1.66669 5.9319 1.66669 7.33203V12.6654C1.66669 14.0655 1.66669 14.7656 1.93917 15.3003C2.17885 15.7707 2.56131 16.1532 3.03171 16.3929C3.56649 16.6654 4.26656 16.6654 5.66669 16.6654Z" stroke="var(--Text-Gray-600, #535862)" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={userProfile.email}
|
|
||||||
onChange={(e) => handleProfileUpdate('email', e.target.value)}
|
|
||||||
className="flex-1 bg-transparent text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div>
|
|
||||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Theme Customization Section */}
|
|
||||||
<div className="w-[1170px] p-6 flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-[584px] flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-800 text-lg font-semibold font-['Inter'] leading-7">Theme Customization</div>
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-500 text-sm font-normal font-['Inter'] leading-tight">Personalize your interface with light or dark mode and enhance your visual experience.</div>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex justify-start items-start gap-3 flex-wrap content-start">
|
|
||||||
{/* System Preference */}
|
|
||||||
<div
|
|
||||||
onClick={() => setSelectedTheme('system')}
|
|
||||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="inline-flex justify-start items-center">
|
|
||||||
<img className="w-24 h-28 rounded-tl-lg rounded-bl-lg" src="https://via.placeholder.com/94x107/f8f9fa/6c757d?text=Light" />
|
|
||||||
<img className="w-24 h-28 rounded-tr-lg rounded-br-lg" src="https://via.placeholder.com/96x107/212529/ffffff?text=Dark" />
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">System preference</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Light Mode */}
|
|
||||||
<div
|
|
||||||
onClick={() => setSelectedTheme('light')}
|
|
||||||
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="self-stretch h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
|
||||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
|
||||||
}`}>
|
|
||||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/f8f9fa/6c757d?text=Light+Mode" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Light Mode</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dark Mode */}
|
|
||||||
<div
|
|
||||||
onClick={() => setSelectedTheme('dark')}
|
|
||||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="w-48 h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
|
||||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
|
||||||
}`}>
|
|
||||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/212529/ffffff?text=Dark+Mode" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch h-5 justify-start text-Text-Gray-800 text-sm font-normal font-['Inter'] leading-tight">Dark Mode</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Another Divider */}
|
|
||||||
<div>
|
|
||||||
<svg width="1136" height="2" viewBox="0 0 1136 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0 1H1136" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="w-[1175px] p-6 inline-flex justify-start items-center gap-2">
|
|
||||||
<div
|
|
||||||
onClick={handleReset}
|
|
||||||
className="px-3 py-2.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Reset</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={handleSaveChanges}
|
|
||||||
className="px-3 py-2.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden cursor-pointer hover:bg-[--Brand-Orange]/90"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Save Changes</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Billing Content */}
|
|
||||||
{activeTab === 'billing' && (
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-2xl font-semibold text-Text-Gray-800 mb-4">Plan & Billing</h2>
|
|
||||||
<p className="text-Text-Gray-500">Billing management features would be implemented here.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsNew;
|
|
||||||
1139
src/server/index.js
1139
src/server/index.js
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
import { initializeApp, getApps } from 'firebase/app';
|
import { initializeApp, getApps } from 'firebase/app';
|
||||||
import { getAuth, GoogleAuthProvider, setPersistence, browserLocalPersistence } from 'firebase/auth';
|
import { getAuth, GoogleAuthProvider, setPersistence, browserLocalPersistence } from 'firebase/auth';
|
||||||
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
|
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||||
@@ -29,22 +28,8 @@ if (isFirebaseConfigured) {
|
|||||||
app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
||||||
auth = getAuth(app);
|
auth = getAuth(app);
|
||||||
setPersistence(auth, browserLocalPersistence);
|
setPersistence(auth, browserLocalPersistence);
|
||||||
db = getFirestore(app);
|
|
||||||
googleProvider = new GoogleAuthProvider();
|
googleProvider = new GoogleAuthProvider();
|
||||||
|
|
||||||
// Connect to emulator in development
|
|
||||||
const isLocalhost = typeof window !== 'undefined' &&
|
|
||||||
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
|
||||||
|
|
||||||
if (isLocalhost && !db._settings?.host?.includes('localhost')) {
|
|
||||||
try {
|
|
||||||
connectFirestoreEmulator(db, 'localhost', 5003);
|
|
||||||
console.log('🔥 Connected to Firestore emulator on localhost:5003');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('⚠️ Firestore emulator already connected or connection failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔥 Firebase initialized successfully');
|
console.log('🔥 Firebase initialized successfully');
|
||||||
} else {
|
} else {
|
||||||
auth = null;
|
auth = null;
|
||||||
|
|||||||
Binary file not shown.
@@ -190,7 +190,7 @@ class SecureApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Submission Methods
|
// Submission Methods
|
||||||
async getSubmissions(): Promise<Submission[]> {
|
async getSubmissions(): Promise<GetSubmissions> {
|
||||||
const response = await this.makeRequest<{ submissions: Submission[] }>(
|
const response = await this.makeRequest<{ submissions: Submission[] }>(
|
||||||
'getSubmissions'
|
'getSubmissions'
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user