fix: simplify EmployeeQuestionnaireNew to use invite-only flow
- Remove all authentication and org context dependencies - Simplify component to work only with invite codes from URL - Remove complex user/employee matching logic - Keep exact Figma UI components and styling - Use only submitViaInvite function for API submissions - Employees never need to log in, only use invite link
This commit is contained in:
@@ -110,20 +110,6 @@ const RESPONSE_FORMAT = {
|
|||||||
},
|
},
|
||||||
required: ["summary", "metrics"]
|
required: ["summary", "metrics"]
|
||||||
},
|
},
|
||||||
keyPersonnelChanges: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
person: { type: "string" },
|
|
||||||
change: { type: "string" }, // e.g. "Promoted to VP Eng"
|
|
||||||
impact: { type: "string" },
|
|
||||||
effectiveDate: { type: "string" }
|
|
||||||
},
|
|
||||||
required: ["person", "change", "impact", "effectiveDate"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediateHiringNeeds: {
|
immediateHiringNeeds: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
@@ -181,21 +167,8 @@ const RESPONSE_FORMAT = {
|
|||||||
required: ["culture", "teamDynamics", "blockers"]
|
required: ["culture", "teamDynamics", "blockers"]
|
||||||
},
|
},
|
||||||
strengths: { type: "array", items: { type: "string" } },
|
strengths: { type: "array", items: { type: "string" } },
|
||||||
gradingOverview: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
department: { type: "string" },
|
|
||||||
grade: { enum: ["A", "B", "C", "D", "F"] },
|
|
||||||
notes: { type: "string" }
|
|
||||||
},
|
|
||||||
required: ["department", "grade", "notes"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
required: ["companyPerformance", "keyPersonnelChanges", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths", "gradingOverview"]
|
required: ["companyPerformance", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths"]
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -891,14 +864,6 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
|
|||||||
retention: 88,
|
retention: 88,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
keyPersonnelChanges: [
|
|
||||||
{
|
|
||||||
type: "promotion",
|
|
||||||
employee: "John Doe",
|
|
||||||
details: "Promoted to Senior Developer",
|
|
||||||
impact: "positive",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
immediateHiringNeeds: [
|
immediateHiringNeeds: [
|
||||||
{
|
{
|
||||||
role: "Frontend Developer",
|
role: "Frontend Developer",
|
||||||
@@ -921,17 +886,7 @@ exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
|
|||||||
"Strong technical expertise",
|
"Strong technical expertise",
|
||||||
"Collaborative team culture",
|
"Collaborative team culture",
|
||||||
"Innovative problem-solving approach",
|
"Innovative problem-solving approach",
|
||||||
],
|
]
|
||||||
gradingOverview: {
|
|
||||||
averagePerformance: 82,
|
|
||||||
topPerformers: 3,
|
|
||||||
needsImprovement: 1,
|
|
||||||
departmentBreakdown: {
|
|
||||||
engineering: 85,
|
|
||||||
design: 80,
|
|
||||||
product: 78,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
wiki = {
|
wiki = {
|
||||||
|
|||||||
119
index.css
119
index.css
@@ -14,6 +14,125 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes blinkLightGreen {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
33% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #a5ffc075;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
33%,
|
||||||
|
66% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blinkLightYellow {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
33% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #f7f3c275;
|
||||||
|
}
|
||||||
|
|
||||||
|
33%,
|
||||||
|
66% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blinkLightBlue {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
33% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #a5d8ff75;
|
||||||
|
}
|
||||||
|
|
||||||
|
33%,
|
||||||
|
66% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blinkLightRed {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
33% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #f63d6875;
|
||||||
|
}
|
||||||
|
|
||||||
|
33%,
|
||||||
|
66% {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkLightBlue,
|
||||||
|
.blinkLightGreen,
|
||||||
|
.blinkLightRed,
|
||||||
|
.blinkLightYellow {
|
||||||
|
animation-duration : 5s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
border : solid 1px transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkLightBlue {
|
||||||
|
animation-name: blinkLightBlue;
|
||||||
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #a5d8ff75;
|
||||||
|
border: solid 1px #54c2e456;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkLightGreen {
|
||||||
|
animation-name: blinkLightGreen;
|
||||||
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #a5ffc075;
|
||||||
|
border: solid 1px rgba(187, 248, 185, 0.31);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkLightRed {
|
||||||
|
animation-name: blinkLightRed;
|
||||||
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #f63d6875;
|
||||||
|
border: solid 1px #e4547656;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkLightYellow {
|
||||||
|
animation-name: blinkLightYellow;
|
||||||
|
box-shadow : inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301),
|
||||||
|
0px 0px 0px 2px #f7f3c275;
|
||||||
|
border: solid 1px #e4e25456;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkLightActive {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkLightEmpty {
|
||||||
|
box-shadow: inset 1px 0px 3px 0px rgba(255, 255, 255, 0.329),
|
||||||
|
inset -1px 0px 3px 0px rgba(78, 78, 78, 0.301);
|
||||||
|
animation : none !important;
|
||||||
|
border : none !important;
|
||||||
|
background-color: var(--Neutrals-NeutralSlate400)
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<ModernLogin />} />
|
<Route path="/login" element={<ModernLogin />} />
|
||||||
<Route path="/login/:inviteCode" element={<ModernLogin />} />
|
<Route path="/login/:inviteCode" element={<ModernLogin />} />
|
||||||
<Route path="/invite/:inviteCode" element={<InviteRedirect />} />
|
{/* <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={<EmployeeQuestionnaireNew />} />
|
<Route path="/employee-form/:inviteCode" element={<EmployeeQuestionnaireNew />} />
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const RadarPerformanceChart: React.FC<Props> = ({ title, data, height = 320, col
|
|||||||
<RadarChart data={chartData} margin={{ top: 10, right: 30, bottom: 10, left: 10 }}>
|
<RadarChart data={chartData} margin={{ top: 10, right: 30, bottom: 10, left: 10 }}>
|
||||||
<PolarGrid stroke="var(--border-color)" />
|
<PolarGrid stroke="var(--border-color)" />
|
||||||
<PolarAngleAxis dataKey="subject" tick={{ fill: 'var(--text-secondary)', fontSize: 11 }} />
|
<PolarAngleAxis dataKey="subject" tick={{ fill: 'var(--text-secondary)', fontSize: 11 }} />
|
||||||
<PolarRadiusAxis angle={30} domain={[0, 100]} tick={{ fill: 'var(--text-secondary)', fontSize: 10 }} />
|
<PolarRadiusAxis angle={30} domain={[0, 10]} tick={{ fill: 'var(--text-secondary)', fontSize: 10 }} />
|
||||||
<Radar name={title || 'Score'} dataKey="A" stroke={color} fill={color} fillOpacity={0.35} />
|
<Radar name={title || 'Score'} dataKey="A" stroke={color} fill={color} fillOpacity={0.35} />
|
||||||
<Tooltip wrapperStyle={{ fontSize: 12 }} contentStyle={{ background: 'var(--background-secondary)', border: '1px solid var(--border-color)' }} />
|
<Tooltip wrapperStyle={{ fontSize: 12 }} contentStyle={{ background: 'var(--background-secondary)', border: '1px solid var(--border-color)' }} />
|
||||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||||
|
|||||||
@@ -176,20 +176,20 @@ export const SAMPLE_COMPANY_REPORT: CompanyReport = {
|
|||||||
{
|
{
|
||||||
employeeName: 'Jamie Park',
|
employeeName: 'Jamie Park',
|
||||||
grade: 'B',
|
grade: 'B',
|
||||||
reliability: 6,
|
reliability: 10,
|
||||||
roleFit: 7,
|
roleFit: 1,
|
||||||
scalability: 6,
|
scalability: 6,
|
||||||
output: 6,
|
output: 2,
|
||||||
initiative: 7
|
initiative: 7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
employeeName: 'Taylor Smith',
|
employeeName: 'Taylor Smith',
|
||||||
grade: 'A',
|
grade: 'A',
|
||||||
reliability: 9,
|
reliability: 9,
|
||||||
roleFit: 9,
|
roleFit: 7,
|
||||||
scalability: 8,
|
scalability: 3,
|
||||||
output: 9,
|
output: 1,
|
||||||
initiative: 8
|
initiative: 9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
employeeName: 'Jordan Lee',
|
employeeName: 'Jordan Lee',
|
||||||
@@ -240,7 +240,6 @@ export const SAMPLE_COMPANY_REPORT: CompanyReport = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] },
|
|
||||||
personnelChanges: {
|
personnelChanges: {
|
||||||
newHires: [
|
newHires: [
|
||||||
{ name: 'Emma Wilson', department: 'Creative', role: 'Graphic Designer', impact: 'Will strengthen visual design capabilities' }
|
{ name: 'Emma Wilson', department: 'Creative', role: 'Graphic Designer', impact: 'Will strengthen visual design capabilities' }
|
||||||
@@ -250,7 +249,6 @@ export const SAMPLE_COMPANY_REPORT: CompanyReport = {
|
|||||||
],
|
],
|
||||||
departures: []
|
departures: []
|
||||||
},
|
},
|
||||||
keyPersonnelChanges: [],
|
|
||||||
immediateHiringNeeds: [
|
immediateHiringNeeds: [
|
||||||
{ 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' }
|
||||||
@@ -278,26 +276,64 @@ export const SAMPLE_COMPANY_REPORT: CompanyReport = {
|
|||||||
'Innovative approach to campaign development',
|
'Innovative approach to campaign development',
|
||||||
'Reliable project delivery within deadlines'
|
'Reliable project delivery within deadlines'
|
||||||
],
|
],
|
||||||
organizationalImpactSummary: {
|
organizationalImpactSummary: [
|
||||||
missionCritical: [
|
{
|
||||||
{ employeeName: 'Sarah Johnson', impact: 'Lead Campaign Manager', description: 'Manages top-tier client relationships and drives 40% of company revenue' },
|
category: 'Mission Critical',
|
||||||
{ employeeName: 'Casey Johnson', impact: 'Tech Infrastructure', description: 'Maintains all technical systems and client delivery platforms' }
|
employees: [
|
||||||
],
|
{
|
||||||
highlyValuable: [
|
employeeName: 'Sarah Johnson',
|
||||||
{ employeeName: 'Alex Rivera', impact: 'Social Media Strategy', description: 'Develops social strategies that increase client engagement by 150%' },
|
impact: 'Lead Campaign Manager',
|
||||||
{ employeeName: 'Taylor Smith', impact: 'Creative Director', description: 'Ensures brand consistency and award-winning creative output' }
|
description: 'Manages top-tier client relationships and drives 40% of company revenue',
|
||||||
],
|
suggestedPay: '$120,000'
|
||||||
coreSupport: [
|
},
|
||||||
{ employeeName: 'Mike Chen', impact: 'Campaign Support', description: 'Reliable execution of campaign tasks and client communication' },
|
{
|
||||||
{ employeeName: 'Jordan Lee', impact: 'Design Support', description: 'Produces high-quality visual assets and maintains design standards' }
|
employeeName: 'Casey Johnson',
|
||||||
],
|
impact: 'Tech Infrastructure',
|
||||||
lowCriticality: [
|
description: 'Maintains all technical systems and client delivery platforms',
|
||||||
{ employeeName: 'Jamie Park', impact: 'Content Assistant', description: 'Supports content creation with room for skill development' },
|
suggestedPay: '$110,000'
|
||||||
{ employeeName: 'Morgan Davis', impact: 'Administrative Support', description: 'Handles routine operations and documentation' }
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
organizationalStrengths: [],
|
{
|
||||||
organizationalRisks: [],
|
category: 'Highly Valuable',
|
||||||
gradingOverview: {},
|
employees: [
|
||||||
|
{
|
||||||
|
employeeName: 'Alex Rivera',
|
||||||
|
impact: 'Social Media Strategy',
|
||||||
|
description: 'Develops social strategies that increase client engagement by 150%',
|
||||||
|
suggestedPay: '$95,000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Taylor Smith',
|
||||||
|
impact: 'Creative Director',
|
||||||
|
description: 'Ensures brand consistency and award-winning creative output',
|
||||||
|
suggestedPay: '$92,000'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Core Support',
|
||||||
|
employees: [
|
||||||
|
{
|
||||||
|
employeeName: 'Mike Chen',
|
||||||
|
impact: 'Campaign Support',
|
||||||
|
description: 'Reliable execution of campaign tasks and client communication',
|
||||||
|
suggestedPay: '$70,000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeName: 'Jordan Lee',
|
||||||
|
impact: 'Design Support',
|
||||||
|
description: 'Produces high-quality visual assets and maintains design standards',
|
||||||
|
suggestedPay: '$68,000'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Low Criticality',
|
||||||
|
employees: [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
executiveSummary: `Welcome to Auditly! Generate your first AI-powered company report by inviting employees and completing the onboarding process.`
|
executiveSummary: `Welcome to Auditly! Generate your first AI-powered company report by inviting employees and completing the onboarding process.`
|
||||||
};
|
};
|
||||||
@@ -424,14 +424,9 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
riskLevel: 'Unknown'
|
riskLevel: 'Unknown'
|
||||||
},
|
},
|
||||||
gradingBreakdown: [],
|
gradingBreakdown: [],
|
||||||
operatingPlan: { nextQuarterGoals: [], keyInitiatives: [], resourceNeeds: [], riskMitigation: [] },
|
|
||||||
personnelChanges: { newHires: [], promotions: [], departures: [] },
|
personnelChanges: { newHires: [], promotions: [], departures: [] },
|
||||||
keyPersonnelChanges: [],
|
|
||||||
immediateHiringNeeds: [],
|
immediateHiringNeeds: [],
|
||||||
forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] },
|
forwardOperatingPlan: { quarterlyGoals: [], resourceNeeds: [], riskMitigation: [] },
|
||||||
organizationalStrengths: [],
|
|
||||||
organizationalRisks: [],
|
|
||||||
gradingOverview: {},
|
|
||||||
executiveSummary: 'Company report generated successfully.',
|
executiveSummary: 'Company report generated successfully.',
|
||||||
// Override with API data if available
|
// Override with API data if available
|
||||||
...(payload as any || {})
|
...(payload as any || {})
|
||||||
@@ -556,10 +551,6 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
|
|||||||
},
|
},
|
||||||
submitEmployeeAnswers: async (employeeId: string, answers: Record<string, string>) => {
|
submitEmployeeAnswers: async (employeeId: string, answers: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
if (!user?.uid) {
|
|
||||||
throw new Error('User authentication required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use secure API for submission
|
// Use secure API for submission
|
||||||
await secureApi.submitEmployeeAnswers(employeeId, answers);
|
await secureApi.submitEmployeeAnswers(employeeId, answers);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { EmployeeSubmissionAnswers } from '../employeeQuestions';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
|
||||||
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
|
|
||||||
import { API_URL } from '../constants';
|
import { API_URL } from '../constants';
|
||||||
import {
|
import {
|
||||||
WelcomeScreen,
|
WelcomeScreen,
|
||||||
@@ -15,39 +13,21 @@ import {
|
|||||||
} from '../components/figma/FigmaEmployeeForms';
|
} from '../components/figma/FigmaEmployeeForms';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced Employee Questionnaire with Exact Figma Design Implementation
|
* Employee Questionnaire with Invite-Only Flow
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Exact Figma design system styling
|
|
||||||
* - Invite-based flow (no authentication required)
|
* - Invite-based flow (no authentication required)
|
||||||
* - Company owner can invite employees with metadata
|
* - Company owner invites employees with metadata
|
||||||
* - LLM processing via cloud functions
|
* - Employee uses invite code to access questionnaire
|
||||||
* - Report generation with company context
|
* - LLM processing via cloud functions with company context
|
||||||
* - Firestore storage for reports
|
* - Report generation and Firestore storage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const EmployeeQuestionnaire: React.FC = () => {
|
const EmployeeQuestionnaire: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
// Check if this is an invite-based flow (no auth/org needed)
|
|
||||||
const inviteCode = params.inviteCode;
|
const inviteCode = params.inviteCode;
|
||||||
const isInviteFlow = !!inviteCode;
|
|
||||||
|
|
||||||
// Only use org context for authenticated flows
|
|
||||||
let submitEmployeeAnswers, generateEmployeeReport, employees;
|
|
||||||
if (!isInviteFlow) {
|
|
||||||
const orgContext = useOrg();
|
|
||||||
({ submitEmployeeAnswers, generateEmployeeReport, employees } = orgContext);
|
|
||||||
} else {
|
|
||||||
// For invite flows, we don't need these functions from org context
|
|
||||||
submitEmployeeAnswers = null;
|
|
||||||
generateEmployeeReport = null;
|
|
||||||
employees = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Component state
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const [formData, setFormData] = useState<any>({});
|
const [formData, setFormData] = useState<any>({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -55,10 +35,12 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
|
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
|
||||||
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
|
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
|
||||||
|
|
||||||
// Load invite details if this is an invite flow
|
// Load invite details on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inviteCode) {
|
if (inviteCode) {
|
||||||
loadInviteDetails(inviteCode);
|
loadInviteDetails(inviteCode);
|
||||||
|
} else {
|
||||||
|
setError('No invite code provided');
|
||||||
}
|
}
|
||||||
}, [inviteCode]);
|
}, [inviteCode]);
|
||||||
|
|
||||||
@@ -94,39 +76,8 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get employee info from multiple sources
|
// Get employee info from invite data
|
||||||
const invitedEmployee = location.state?.invitedEmployee;
|
const currentEmployee = inviteEmployee;
|
||||||
|
|
||||||
// Determine current employee - for invite flow, use invite employee data
|
|
||||||
let currentEmployee;
|
|
||||||
if (isInviteFlow) {
|
|
||||||
currentEmployee = inviteEmployee;
|
|
||||||
} else {
|
|
||||||
// Original auth-based logic
|
|
||||||
currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
|
|
||||||
|
|
||||||
if (!currentEmployee && user?.email) {
|
|
||||||
// Try case-insensitive email matching
|
|
||||||
currentEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase() === user.email?.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => {
|
const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -176,7 +127,7 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert form data to EMPLOYEE_QUESTIONS format for backend
|
// Convert form data to answers format for backend
|
||||||
const answers: EmployeeSubmissionAnswers = {};
|
const answers: EmployeeSubmissionAnswers = {};
|
||||||
|
|
||||||
// Map form data to question IDs
|
// Map form data to question IDs
|
||||||
@@ -191,55 +142,14 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Submit answers - different logic for invite vs auth flow
|
// Submit answers via invite flow
|
||||||
let result;
|
const result = await submitViaInvite(answers, inviteCode!);
|
||||||
if (isInviteFlow) {
|
|
||||||
// Direct API submission for invite flow (no auth needed)
|
|
||||||
result = await submitViaInvite(answers, inviteCode);
|
|
||||||
} else {
|
|
||||||
// Use org context for authenticated flow
|
|
||||||
if (!currentEmployee) {
|
|
||||||
// Enhanced fallback logic for authenticated users
|
|
||||||
if (employees.length > 0) {
|
|
||||||
let fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fallbackEmployee) {
|
|
||||||
const userDomain = user?.email?.split('@')[1];
|
|
||||||
fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.split('@')[1] === userDomain
|
|
||||||
) || employees[employees.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
|
|
||||||
if (success) {
|
|
||||||
try {
|
|
||||||
const report = await generateEmployeeReport(fallbackEmployee);
|
|
||||||
console.log('Report generated successfully:', report);
|
|
||||||
} catch (reportError) {
|
|
||||||
console.error('Failed to generate report:', reportError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to completion
|
|
||||||
setCurrentStep(999); // Thank you page
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator.`);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Show thank you page
|
// Show thank you page
|
||||||
setCurrentStep(999);
|
setCurrentStep(999);
|
||||||
} else {
|
} else {
|
||||||
setError(result.message || 'Failed to submit questionnaire');
|
setError(result.error || 'Failed to submit questionnaire');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submission error:', error);
|
console.error('Submission error:', error);
|
||||||
@@ -261,8 +171,8 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
setCurrentStep(currentStep - 1);
|
setCurrentStep(currentStep - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Early return for invite flow loading state
|
// Early return for loading state
|
||||||
if (isInviteFlow && isLoadingInvite) {
|
if (isLoadingInvite) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
<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="max-w-4xl mx-auto text-center">
|
||||||
@@ -276,8 +186,8 @@ const EmployeeQuestionnaire: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early return for invite flow error state
|
// Early return for error state
|
||||||
if (isInviteFlow && error && currentStep === 1) {
|
if (error && currentStep === 1) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[--Neutrals-NeutralSlate0] py-8 px-4 flex items-center justify-center">
|
<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="max-w-4xl mx-auto text-center">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import { onboardingSteps, OnboardingData, initializeOnboardingData } from '../data/onboardingSteps';
|
import { onboardingSteps, OnboardingData, initializeOnboardingData } from '../data/onboardingSteps';
|
||||||
import { secureApiPOST, secureApi } from '../services/secureApi';
|
import { secureApi } from '../services/secureApi';
|
||||||
import {
|
import {
|
||||||
FigmaOnboardingIntro,
|
FigmaOnboardingIntro,
|
||||||
FigmaOnboardingQuestion,
|
FigmaOnboardingQuestion,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import { secureApiPOST, secureApi } from '../services/secureApi';
|
import { secureApi } from '../services/secureApi';
|
||||||
import { CompanyReport, Employee, EmployeeReport } from '../types';
|
import { CompanyReport, Employee, EmployeeReport } from '../types';
|
||||||
import { SAMPLE_COMPANY_REPORT } from '../constants';
|
import { SAMPLE_COMPANY_REPORT } from '../constants';
|
||||||
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
import RadarPerformanceChart from '../components/charts/RadarPerformanceChart';
|
||||||
@@ -44,6 +44,17 @@ const Reports: React.FC = () => {
|
|||||||
loadCompanyReport();
|
loadCompanyReport();
|
||||||
}, [currentUserIsOwner, getFullCompanyReportHistory]);
|
}, [currentUserIsOwner, getFullCompanyReportHistory]);
|
||||||
|
|
||||||
|
const handleEmployeeSelect = useCallback((employee: Employee) => {
|
||||||
|
const employeeReport = reports[employee.id];
|
||||||
|
if (employeeReport) {
|
||||||
|
setSelectedReport({
|
||||||
|
report: employeeReport,
|
||||||
|
type: 'employee',
|
||||||
|
employeeName: employee.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [reports]);
|
||||||
|
|
||||||
// Handle navigation from Submissions page
|
// Handle navigation from Submissions page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEmployeeId) {
|
if (selectedEmployeeId) {
|
||||||
@@ -61,17 +72,6 @@ const Reports: React.FC = () => {
|
|||||||
).sort((a, b) => a.name.localeCompare(b.name))
|
).sort((a, b) => a.name.localeCompare(b.name))
|
||||||
: employees.filter(emp => emp.id === user?.uid);
|
: employees.filter(emp => emp.id === user?.uid);
|
||||||
|
|
||||||
const handleEmployeeSelect = useCallback((employee: Employee) => {
|
|
||||||
const employeeReport = reports[employee.id];
|
|
||||||
if (employeeReport) {
|
|
||||||
setSelectedReport({
|
|
||||||
report: employeeReport,
|
|
||||||
type: 'employee',
|
|
||||||
employeeName: employee.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [reports]);
|
|
||||||
|
|
||||||
const handleCompanyReportSelect = () => {
|
const handleCompanyReportSelect = () => {
|
||||||
if (companyReport) {
|
if (companyReport) {
|
||||||
setSelectedReport({ report: companyReport, type: 'company' });
|
setSelectedReport({ report: companyReport, type: 'company' });
|
||||||
@@ -203,6 +203,9 @@ const CompanyReportContent: React.FC<{
|
|||||||
const [activeDepartmentTab, setActiveDepartmentTab] = useState(() =>
|
const [activeDepartmentTab, setActiveDepartmentTab] = useState(() =>
|
||||||
report?.gradingBreakdown?.[0]?.departmentNameShort || 'Campaigns'
|
report?.gradingBreakdown?.[0]?.departmentNameShort || 'Campaigns'
|
||||||
);
|
);
|
||||||
|
const [activeImpactSummary, setActiveImpactSummary] = useState(() =>
|
||||||
|
report?.organizationalImpactSummary?.[0]?.category || 'Mission Critical'
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -322,24 +325,6 @@ const CompanyReportContent: React.FC<{
|
|||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Immediate Hiring Needs</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Immediate Hiring Needs</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate50] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-start items-start gap-4">
|
|
||||||
{report.immediateHiringNeeds.map((need, index) => (
|
|
||||||
<div key={index} className="self-stretch inline-flex justify-between items-start">
|
|
||||||
<div className="w-48 justify-start text-[--Neutrals-NeutralSlate800] text-base font-semibold font-['Inter'] leading-normal">{need.role} - {need.department}</div>
|
|
||||||
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
<div className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${need.priority === 'High' ? 'bg-red-100 text-red-800' :
|
|
||||||
need.priority === 'Medium' ? 'bg-yellow-100 text-yellow-800' :
|
|
||||||
'bg-green-100 text-green-800'
|
|
||||||
}`}>
|
|
||||||
{need.priority} Priority
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch h-0 outline outline-1 outline-offset-[-0.50px] outline-[--Neutrals-NeutralSlate200]">
|
|
||||||
<hr className="border-0 border-t border-[--Neutrals-NeutralSlate200]" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div> */}
|
|
||||||
<div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate50] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-start items-start gap-4">
|
<div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate50] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-start items-start gap-4">
|
||||||
{report.immediateHiringNeeds.map((need, index) => (
|
{report.immediateHiringNeeds.map((need, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
@@ -420,13 +405,14 @@ const CompanyReportContent: React.FC<{
|
|||||||
{/*
|
{/*
|
||||||
Organizational Impact Summary
|
Organizational Impact Summary
|
||||||
*/}
|
*/}
|
||||||
|
{/**
|
||||||
{report?.organizationalImpactSummary && (
|
{report?.organizationalImpactSummary && (
|
||||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Organizational Impact Summary</div>
|
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Organizational Impact Summary</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate50] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-start gap-6">
|
<div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate50] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-start gap-6">
|
||||||
{/* Mission Critical */}
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
<div className="flex justify-start items-center gap-2">
|
<div className="flex justify-start items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||||
@@ -442,7 +428,6 @@ const CompanyReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Highly Valuable */}
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
<div className="flex justify-start items-center gap-2">
|
<div className="flex justify-start items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
|
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
|
||||||
@@ -458,7 +443,6 @@ const CompanyReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Core Support */}
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
<div className="flex justify-start items-center gap-2">
|
<div className="flex justify-start items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||||
@@ -474,7 +458,6 @@ const CompanyReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Low Criticality */}
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
<div className="flex justify-start items-center gap-2">
|
<div className="flex justify-start items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
@@ -492,6 +475,72 @@ const CompanyReportContent: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
**/}
|
||||||
|
|
||||||
|
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
|
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
||||||
|
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-xl font-medium font-['Neue_Montreal'] leading-normal">Organizational Impact Summary</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="p-1 bg-[--Neutrals-NeutralSlate200] rounded-full outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate50] inline-flex justify-start items-center gap-1">
|
||||||
|
{/* Department Tabs */}
|
||||||
|
{report?.organizationalImpactSummary.map((dept, index) => (
|
||||||
|
<div
|
||||||
|
key={dept.category}
|
||||||
|
className={`px-3 py-1.5 rounded-full flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${activeImpactSummary === dept.category
|
||||||
|
? 'bg-[--Neutrals-NeutralSlate0]'
|
||||||
|
: 'bg-[--Neutrals-NeutralSlate100] shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)]'
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveImpactSummary(dept.category)}
|
||||||
|
>
|
||||||
|
<div className="px-0.5 flex justify-center items-center">
|
||||||
|
<div className={`justify-start text-xs flex items-center font-medium font-['Inter'] leading-none ${activeImpactSummary === dept.category ? 'text-[--Neutrals-NeutralSlate950]' : 'text-zinc-600'
|
||||||
|
}`}>
|
||||||
|
<span className={`flex-shrink-0 w-2 h-2 rounded-full mr-2
|
||||||
|
${dept?.employees.length === 0 ? 'blinkLightEmpty bg-[--Neutrals-NeutralSlate100]' : ''}
|
||||||
|
${dept.category === "Mission Critical" ?
|
||||||
|
'bg-green-400 blinkLightGreen' :
|
||||||
|
dept.category === "Highly Valuable" ?
|
||||||
|
'bg-yellow-400 blinkLightYellow' :
|
||||||
|
dept.category === "Core Support" ?
|
||||||
|
'bg-blue-800 blinkLightBlue' :
|
||||||
|
dept.category === "Low Criticality" ?
|
||||||
|
'bg-red-600 blinkLightRed' :
|
||||||
|
''}
|
||||||
|
`}></span>{dept.category}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Content for the currently selected department */}
|
||||||
|
{(() => {
|
||||||
|
const currentImpact = report?.organizationalImpactSummary.find(dept => dept.category === activeImpactSummary);
|
||||||
|
if (!currentImpact) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="self-stretch p-6 bg-[--Neutrals-NeutralSlate100] rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-start gap-4">
|
||||||
|
{/* Department Overview Section */}
|
||||||
|
{
|
||||||
|
currentImpact && currentImpact?.employees.map((item) => (
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||||
|
<div className="self-stretch inline-flex justify-start items-center">
|
||||||
|
<div className="w-48 justify-start text-[--Neutrals-NeutralSlate800] text-base font-semibold font-['Inter'] leading-normal w-fit">{item.employeeName} - {item.impact}</div>
|
||||||
|
<div className="flex-1 text-right justify-start text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal">{item.suggestedPay}</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal">{item.description}</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch h-0 outline outline-1 outline-offset-[-0.50px] outline-[--Neutrals-NeutralSlate200]" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Grading Overview */}
|
{/* Grading Overview */}
|
||||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
||||||
@@ -553,11 +602,11 @@ const CompanyReportContent: React.FC<{
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||||
{currentDepartment.teamScores?.map((teamScore, index) => {
|
{currentDepartment.teamScores?.map((teamScore, index) => {
|
||||||
const radarData = [
|
const radarData = [
|
||||||
{ label: 'Reliability', value: teamScore.reliability * 10 },
|
{ label: 'Reliability', value: teamScore.reliability },
|
||||||
{ label: 'Role Fit', value: teamScore.roleFit * 10 },
|
{ label: 'Role Fit', value: teamScore.roleFit },
|
||||||
{ label: 'Scalability', value: teamScore.scalability * 10 },
|
{ label: 'Scalability', value: teamScore.scalability },
|
||||||
{ label: 'Output', value: teamScore.output * 10 },
|
{ label: 'Output', value: teamScore.output },
|
||||||
{ label: 'Initiative', value: teamScore.initiative * 10 }
|
{ label: 'Initiative', value: teamScore.initiative }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -577,10 +626,10 @@ const CompanyReportContent: React.FC<{
|
|||||||
data={radarData}
|
data={radarData}
|
||||||
height={240}
|
height={240}
|
||||||
color={
|
color={
|
||||||
teamScore.grade.startsWith('A') ? '#22c55e' :
|
teamScore.grade.startsWith('A') ? 'var(--Brand-Orange)' :
|
||||||
teamScore.grade.startsWith('B') ? '#3b82f6' :
|
teamScore.grade.startsWith('B') ? 'var(--color-green)' :
|
||||||
teamScore.grade.startsWith('C') ? '#f59e0b' :
|
teamScore.grade.startsWith('C') ? 'var(--color-yellow)' :
|
||||||
'#ef4444'
|
'var(--color-red)'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -594,32 +643,6 @@ const CompanyReportContent: React.FC<{
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Company Strengths */}
|
|
||||||
{/* {report?.organizationalStrengths && (
|
|
||||||
<div className="w-full p-3 bg-[--Neutrals-NeutralSlate100] rounded-[20px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-1 overflow-hidden">
|
|
||||||
<div className="self-stretch px-3 py-2 inline-flex justify-start items-center gap-2">
|
|
||||||
<div className="justify-start text-Text-Dark-950 text-xl font-medium font-['Neue_Montreal'] leading-normal">Company Strengths</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch p-6 bg-Light-Grays-l-gray08 rounded-2xl outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 flex flex-col justify-start items-start gap-4">
|
|
||||||
{report.organizationalStrengths.map((strength, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<div className="self-stretch inline-flex justify-between items-start">
|
|
||||||
<div className="w-48 justify-start text-Text-Gray-800 text-base font-semibold font-['Inter'] leading-normal">{strength.title}:</div>
|
|
||||||
<div className="flex-1 justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">{strength.description}</div>
|
|
||||||
</div>
|
|
||||||
{index < report.organizationalStrengths.length - 1 && (
|
|
||||||
<div className="my-4">
|
|
||||||
<svg width="784" height="2" viewBox="0 0 784 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0 1H784" stroke="var(--Text-Gray-200, #E9EAEB)" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
</div >
|
</div >
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
import { useOrg } from '../contexts/OrgContext';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { secureApiPOST, secureApi } from '../services/secureApi';
|
import { secureApi } from '../services/secureApi';
|
||||||
import { Employee } from '../types';
|
import { Employee } from '../types';
|
||||||
import { EMPLOYEE_QUESTIONS } from '../employeeQuestions';
|
import { EMPLOYEE_QUESTIONS } from '../employeeQuestions';
|
||||||
|
|
||||||
|
|||||||
@@ -963,21 +963,13 @@ app.post('/api/company-wiki', (req, res) => {
|
|||||||
averagePerformanceScore: 4.2,
|
averagePerformanceScore: 4.2,
|
||||||
riskLevel: 'Medium'
|
riskLevel: 'Medium'
|
||||||
},
|
},
|
||||||
keyPersonnelChanges: [],
|
|
||||||
immediateHiringNeeds: [],
|
immediateHiringNeeds: [],
|
||||||
forwardOperatingPlan: {
|
forwardOperatingPlan: {
|
||||||
quarterlyGoals: [org.shortTermGoals || 'Drive focused growth'],
|
quarterlyGoals: [org.shortTermGoals || 'Drive focused growth'],
|
||||||
resourceNeeds: ['Structured process improvements'],
|
resourceNeeds: ['Structured process improvements'],
|
||||||
riskMitigation: ['Knowledge sharing cadence']
|
riskMitigation: ['Knowledge sharing cadence']
|
||||||
},
|
},
|
||||||
organizationalStrengths: [org.advantages || 'Mission-aligned core team'].filter(Boolean),
|
|
||||||
organizationalRisks: [org.vulnerabilities || 'Key-person dependencies'].filter(Boolean),
|
organizationalRisks: [org.vulnerabilities || 'Key-person dependencies'].filter(Boolean),
|
||||||
gradingOverview: departmentBreakdown.map(d => ({
|
|
||||||
department: d.department,
|
|
||||||
averageScore: 4 + Math.random(),
|
|
||||||
totalEmployees: d.count,
|
|
||||||
scores: []
|
|
||||||
})),
|
|
||||||
executiveSummary: `${org.name || 'Company'} is a ${org.industry || 'technology'} company with ${org.size || '11-50'} employees. Mission: ${org.mission || 'N/A'} | Vision: ${org.vision || 'N/A'}`
|
executiveSummary: `${org.name || 'Company'} is a ${org.industry || 'technology'} company with ${org.size || '11-50'} employees. Mission: ${org.mission || 'N/A'} | Vision: ${org.vision || 'N/A'}`
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1067,29 +1059,15 @@ app.post('/api/company-report', (req, res) => {
|
|||||||
promotions: [],
|
promotions: [],
|
||||||
departures: []
|
departures: []
|
||||||
},
|
},
|
||||||
keyPersonnelChanges: [
|
|
||||||
{ employeeName: 'Alex Green', role: 'Influencer Coordinator', department: 'Marketing', changeType: 'newHire', impact: 'Expands outreach capability' }
|
|
||||||
],
|
|
||||||
immediateHiringNeeds: [
|
immediateHiringNeeds: [
|
||||||
{ department: 'Engineering', role: 'Senior Developer', priority: 'High', reasoning: 'Roadmap acceleration', urgency: 'high' },
|
{ department: 'Engineering', role: 'Senior Developer', priority: 'High', reasoning: 'Roadmap acceleration', urgency: 'high' },
|
||||||
{ department: 'Marketing', role: 'Content Creator', priority: 'Medium', reasoning: 'Content velocity', urgency: 'medium' }
|
{ department: 'Marketing', role: 'Content Creator', priority: 'Medium', reasoning: 'Content velocity', urgency: 'medium' }
|
||||||
],
|
],
|
||||||
operatingPlan: {
|
|
||||||
nextQuarterGoals: [org.shortTermGoals || 'Improve collaboration', 'Expand market presence', 'Strengthen documentation'],
|
|
||||||
keyInitiatives: ['Launch structured onboarding', 'Implement objective tracking'],
|
|
||||||
resourceNeeds: ['Senior engineering capacity', 'Automation tooling'],
|
|
||||||
riskMitigation: commonBottlenecks.length > 0 ? [`Address: ${commonBottlenecks[0]}`, 'Cross-training plan'] : ['Cross-training plan', 'Process retros']
|
|
||||||
},
|
|
||||||
forwardOperatingPlan: {
|
forwardOperatingPlan: {
|
||||||
quarterlyGoals: [org.shortTermGoals || 'Improve collaboration'],
|
quarterlyGoals: [org.shortTermGoals || 'Improve collaboration'],
|
||||||
resourceNeeds: ['Senior engineering capacity'],
|
resourceNeeds: ['Senior engineering capacity'],
|
||||||
riskMitigation: commonBottlenecks.length > 0 ? [`Address: ${commonBottlenecks[0]}`] : ['Cross-training plan']
|
riskMitigation: commonBottlenecks.length > 0 ? [`Address: ${commonBottlenecks[0]}`] : ['Cross-training plan']
|
||||||
},
|
},
|
||||||
organizationalStrengths: [
|
|
||||||
{ area: org.advantages || 'Technical depth', description: 'Core team demonstrates strong execution speed.' },
|
|
||||||
{ area: 'Culture', description: org.cultureDescription || 'Collaborative, learning oriented.' },
|
|
||||||
...(underutilizedTalents.length > 0 ? [{ area: 'Hidden Talent', description: `Underutilized capabilities: ${underutilizedTalents.join(', ')}` }] : [])
|
|
||||||
],
|
|
||||||
organizationalRisks: [
|
organizationalRisks: [
|
||||||
...(retentionRisks.length > 0 ? retentionRisks.map(risk => `Retention risk: ${risk}`) : []),
|
...(retentionRisks.length > 0 ? retentionRisks.map(risk => `Retention risk: ${risk}`) : []),
|
||||||
...(commonBottlenecks.length > 0 ? [`Common bottleneck: ${commonBottlenecks[0]}`] : []),
|
...(commonBottlenecks.length > 0 ? [`Common bottleneck: ${commonBottlenecks[0]}`] : []),
|
||||||
@@ -1097,7 +1075,6 @@ app.post('/api/company-report', (req, res) => {
|
|||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
organizationalImpactSummary: `Team impact is strong relative to size (${submissionCount}/${totalEmployees} employees surveyed); scaling processes will unlock further leverage.`,
|
organizationalImpactSummary: `Team impact is strong relative to size (${submissionCount}/${totalEmployees} employees surveyed); scaling processes will unlock further leverage.`,
|
||||||
gradingBreakdown: gradingCategories,
|
gradingBreakdown: gradingCategories,
|
||||||
gradingOverview: legacyOverview,
|
|
||||||
executiveSummary: generateExecutiveSummary(org, submissionData)
|
executiveSummary: generateExecutiveSummary(org, submissionData)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -347,20 +347,3 @@ class SecureApiService {
|
|||||||
// Export a singleton instance
|
// Export a singleton instance
|
||||||
export const secureApi = new SecureApiService();
|
export const secureApi = new SecureApiService();
|
||||||
export default secureApi;
|
export default secureApi;
|
||||||
|
|
||||||
// Helper functions for backwards compatibility
|
|
||||||
export const secureApiPOST = async (endpoint: string, data: any) => {
|
|
||||||
return secureApi.makeRequest(endpoint, 'POST', data);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const secureApiGET = async (endpoint: string) => {
|
|
||||||
return secureApi.makeRequest(endpoint, 'GET');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const secureApiPUT = async (endpoint: string, data: any) => {
|
|
||||||
return secureApi.makeRequest(endpoint, 'PUT', data);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const secureApiDELETE = async (endpoint: string, data?: any) => {
|
|
||||||
return secureApi.makeRequest(endpoint, 'DELETE', data);
|
|
||||||
};
|
|
||||||
48
src/types.ts
48
src/types.ts
@@ -243,27 +243,14 @@ export interface CompanyReport {
|
|||||||
}[];
|
}[];
|
||||||
strengths: string[];
|
strengths: string[];
|
||||||
organizationalImpactSummary?: {
|
organizationalImpactSummary?: {
|
||||||
missionCritical: {
|
category: 'Mission Critical' | 'Highly Valuable' | 'Core Support' | 'Low Criticality';
|
||||||
|
employees: {
|
||||||
employeeName: string;
|
employeeName: string;
|
||||||
impact: string;
|
impact: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
suggestedPay: string;
|
||||||
}[];
|
}[];
|
||||||
highlyValuable: {
|
}[];
|
||||||
employeeName: string;
|
|
||||||
impact: string;
|
|
||||||
description: string;
|
|
||||||
}[];
|
|
||||||
coreSupport: {
|
|
||||||
employeeName: string;
|
|
||||||
impact: string;
|
|
||||||
description: string;
|
|
||||||
}[];
|
|
||||||
lowCriticality: {
|
|
||||||
employeeName: string;
|
|
||||||
impact: string;
|
|
||||||
description: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
// Grading: array + map for UI
|
// Grading: array + map for UI
|
||||||
gradingBreakdown: {
|
gradingBreakdown: {
|
||||||
departmentNameShort: string; // For the tab label
|
departmentNameShort: string; // For the tab label
|
||||||
@@ -284,34 +271,7 @@ export interface CompanyReport {
|
|||||||
initiative: number;
|
initiative: number;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
// gradingBreakdown: {
|
|
||||||
// departmentName: string;
|
|
||||||
// lead: string;
|
|
||||||
// support: string;
|
|
||||||
// summary: string;
|
|
||||||
// category: string;
|
|
||||||
// value: number; // 0-100
|
|
||||||
// rationale?: string;
|
|
||||||
// }[];
|
|
||||||
|
|
||||||
// Do not use any of the properties below!!! they need to be safely removed
|
|
||||||
organizationalRisks: string[];
|
|
||||||
operatingPlan: { // Do not use!!!
|
|
||||||
nextQuarterGoals: string[];
|
|
||||||
keyInitiatives: string[];
|
|
||||||
resourceNeeds: string[];
|
|
||||||
riskMitigation: string[];
|
|
||||||
};
|
|
||||||
organizationalStrengths: Array<{ icon?: string; area: string; description: string }>; // Do not use!!! needs to be removed
|
|
||||||
keyPersonnelChanges?: Array<{ // Do not use!!
|
|
||||||
employeeName: string;
|
|
||||||
role: string;
|
|
||||||
newRole?: string;
|
|
||||||
department: string;
|
|
||||||
changeType: 'reassignment' | 'promotion' | 'departure';
|
|
||||||
impact?: string;
|
|
||||||
}>;
|
|
||||||
gradingOverview?: Record<string, number>; // legacy (0-5 scale expected by old UI)
|
|
||||||
executiveSummary: string;
|
executiveSummary: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user