600 lines
24 KiB
TypeScript
600 lines
24 KiB
TypeScript
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; |