Files
auditly/src/pages/EmployeeQuestionnaireNew.tsx

707 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation, useParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useOrg } from '../contexts/OrgContext';
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
import { API_URL } from '../constants';
import {
WelcomeScreen,
SectionIntro,
PersonalInfoForm,
TextAreaQuestion,
RatingScaleQuestion,
YesNoChoice,
ThankYouPage
} from '../components/figma/FigmaEmployeeForms';
/**
* Enhanced Employee Questionnaire with Exact Figma Design Implementation
*
* Features:
* - Exact Figma design system styling
* - Invite-based flow (no authentication required)
* - Company owner can invite employees with metadata
* - LLM processing via cloud functions
* - Report generation with company context
* - Firestore storage for reports
*/
const EmployeeQuestionnaire: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
const { user } = useAuth();
// Check if this is an invite-based flow (no auth/org needed)
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 = [];
}
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 if this is an invite flow
useEffect(() => {
if (inviteCode) {
loadInviteDetails(inviteCode);
}
}, [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 multiple sources
const invitedEmployee = location.state?.invitedEmployee;
// 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) => {
try {
// First, consume the invite to mark it as used
const consumeResponse = await fetch(`${API_URL}/consumeInvitation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: inviteCode })
});
if (!consumeResponse.ok) {
throw new Error('Failed to process invitation');
}
// Get orgId from the consume response
const consumeData = await consumeResponse.json();
const orgId = consumeData.orgId;
// Submit the questionnaire answers using Cloud Function
// This will include company onboarding questions and answers for LLM context
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
inviteCode: inviteCode,
answers: answers,
orgId: orgId,
includeCompanyContext: true // Flag to include company Q&A in LLM processing
})
});
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: !!result.report };
} 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 EMPLOYEE_QUESTIONS 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 - different logic for invite vs auth flow
let result;
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) {
// Show thank you page
setCurrentStep(999);
} 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 handleNext = (stepData?: any) => {
if (stepData) {
const newFormData = { ...formData, ...stepData };
setFormData(newFormData);
}
setCurrentStep(currentStep + 1);
};
const handleBack = () => {
setCurrentStep(currentStep - 1);
};
// Early return for invite flow loading state
if (isInviteFlow && 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-Other-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 invite flow error state
if (isInviteFlow && 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-Other-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-Other-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 23 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-Other-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="min-h-screen bg-white">
{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;