commit before mass find + replace

This commit is contained in:
Ra
2025-08-24 16:18:58 -07:00
parent f2145edf56
commit 1ed3e16ff6
28 changed files with 4850 additions and 1181 deletions

View File

@@ -0,0 +1,707 @@
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;