Files
auditly/pages/EmployeeQuestionnaireSteps.tsx

382 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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 } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useOrg } from '../contexts/OrgContext';
import { Card, Button } from '../components/UiKit';
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
import { Question } from '../components/ui/Question';
import { QuestionInput } from '../components/ui/QuestionInput';
import { FigmaQuestion } from '../components/figma/FigmaQuestion';
import { EnhancedFigmaQuestion } from '../components/figma/EnhancedFigmaQuestion';
import { LinearProgress } from '../components/ui/Progress';
import { Alert } from '../components/ui/Alert';
const EmployeeQuestionnaireSteps: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { user } = useAuth();
const { submitEmployeeAnswers, generateEmployeeReport, employees } = useOrg();
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
const [currentStep, setCurrentStep] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
// Get employee info from multiple sources
const invitedEmployee = location.state?.invitedEmployee;
// Find current employee info - try multiple strategies
let currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
// Additional matching strategies for edge cases
if (!currentEmployee && user?.email) {
// Try case-insensitive email matching
currentEmployee = employees.find(emp =>
emp.email?.toLowerCase() === user.email?.toLowerCase()
);
// Try matching by name if email doesn't work (for invite flow)
if (!currentEmployee && invitedEmployee) {
currentEmployee = employees.find(emp =>
emp.name === invitedEmployee.name || emp.id === invitedEmployee.id
);
}
}
// Demo mode fallbacks
if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) {
currentEmployee = employees[employees.length - 1];
}
if (!currentEmployee && employees.length === 1) {
currentEmployee = employees[0];
}
// Filter out followup questions that shouldn't be shown yet
const getVisibleQuestions = () => {
return EMPLOYEE_QUESTIONS.filter(question => {
if (!question.followupTo) return true;
const parentAnswer = answers[question.followupTo];
if (question.followupTo === 'has_kpis') {
return parentAnswer === 'Yes';
}
if (question.followupTo === 'unclear_responsibilities') {
return parentAnswer === 'Yes';
}
if (question.followupTo === 'role_shift_interest') {
return parentAnswer === 'Yes';
}
return false;
});
};
const visibleQuestions = getVisibleQuestions();
const currentQuestion = visibleQuestions[currentStep];
const handleAnswerChange = (value: string) => {
if (currentQuestion) {
setAnswers(prev => ({ ...prev, [currentQuestion.id]: value }));
}
};
const handleNext = () => {
if (currentStep < visibleQuestions.length - 1) {
setCurrentStep(prev => prev + 1);
} else {
handleSubmit();
}
};
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep(prev => prev - 1);
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
setError('');
try {
// Validate required questions
const requiredQuestions = visibleQuestions.filter(q => q.required);
const missingAnswers = requiredQuestions.filter(q => !answers[q.id]?.trim());
if (missingAnswers.length > 0) {
setError(`Please answer all required questions: ${missingAnswers.map(q => q.prompt).join(', ')}`);
setIsSubmitting(false);
return;
}
if (!currentEmployee) {
// Enhanced fallback logic
if (employees.length > 0) {
// Try to find employee by matching with the user's email more aggressively
let fallbackEmployee = employees.find(emp =>
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
);
// If still no match, use the most recent employee or one with matching domain
if (!fallbackEmployee) {
const userDomain = user?.email?.split('@')[1];
fallbackEmployee = employees.find(emp =>
emp.email?.split('@')[1] === userDomain
) || employees[employees.length - 1];
}
console.log('Using enhanced fallback employee:', fallbackEmployee);
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
if (success) {
try {
const report = await generateEmployeeReport(fallbackEmployee);
if (report) {
console.log('Report generated successfully for fallback employee:', report);
}
} catch (reportError) {
console.error('Failed to generate report for fallback employee:', reportError);
}
navigate('/questionnaire-complete', {
replace: true,
state: {
employeeId: fallbackEmployee.id,
employeeName: fallbackEmployee.name,
message: 'Questionnaire submitted successfully! Your responses have been recorded.'
}
});
return;
}
}
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator to ensure your invite was set up correctly.`);
setIsSubmitting(false);
return;
}
const result = await submitEmployeeAnswers(currentEmployee.id, answers);
if (result.success) {
const message = result.reportGenerated
? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.'
: 'Questionnaire submitted successfully! Your report will be available shortly.';
navigate('/questionnaire-complete', {
state: {
employeeId: currentEmployee.id,
employeeName: currentEmployee.name,
reportGenerated: result.reportGenerated,
message: message
}
});
} else {
setError(result.message || 'Failed to submit questionnaire');
}
} catch (error) {
console.error('Submission error:', error);
setError('Failed to submit questionnaire. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const getProgressPercentage = () => {
return Math.round(((currentStep + 1) / visibleQuestions.length) * 100);
};
const isCurrentQuestionAnswered = () => {
if (!currentQuestion) return false;
const answer = answers[currentQuestion.id];
return answer && answer.trim().length > 0;
};
if (!currentQuestion) {
return (
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
<div className="max-w-4xl mx-auto text-center">
<h1 className="text-2xl font-bold text-[--text-primary] mb-4">
No questions available
</h1>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
A
</div>
<h1 className="text-3xl font-bold text-[--text-primary] mb-2">
Welcome to Auditly!
</h1>
<p className="text-[--text-secondary] mb-4">
Please complete this questionnaire to help us understand your role and create personalized insights.
</p>
{currentEmployee ? (
<div className="inline-flex items-center px-4 py-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<span className="text-sm text-blue-800 dark:text-blue-200">
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
</span>
</div>
) : (
<div className="space-y-2">
<div className="inline-flex items-center px-4 py-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<span className="text-sm text-yellow-800 dark:text-yellow-200">
Employee info not found. User: {user?.email}, Employees: {employees.length}
</span>
</div>
<div className="text-xs text-[--text-secondary] max-w-md mx-auto">
<p>Don't worry - your account was created successfully! This is likely a temporary sync issue.</p>
<p className="mt-1">You can still complete the questionnaire, and we'll match it to your profile automatically.</p>
</div>
</div>
)}
</div>
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-[--text-secondary]">
Question {currentStep + 1} of {visibleQuestions.length}
</span>
<span className="text-sm text-[--text-secondary]">{getProgressPercentage()}%</span>
</div>
<LinearProgress value={getProgressPercentage()} />
</div>
{/* Enhanced Question Card */}
<div className="flex justify-center mb-8">
<div className="w-full max-w-2xl">
<EnhancedFigmaQuestion
questionNumber={`Q${currentStep + 1}`}
question={currentQuestion}
answer={answers[currentQuestion.id] || ''}
onAnswerChange={handleAnswerChange}
onBack={currentStep > 0 ? handleBack : undefined}
onNext={
currentStep < visibleQuestions.length - 1
? handleNext
: (isCurrentQuestionAnswered() || !currentQuestion.required)
? handleSubmit
: undefined
}
nextLabel={
currentStep < visibleQuestions.length - 1
? 'Next'
: isSubmitting
? 'Submitting...'
: 'Submit & Generate Report'
}
showNavigation={true}
/>
</div>
</div>
{/* Original Figma Question Card for Comparison */}
<div className="flex justify-center mb-8">
<div className="w-full max-w-2xl">
<FigmaQuestion
questionNumber={`Q${currentStep + 1}`}
title={currentQuestion.prompt}
description={currentQuestion.required ? 'Required' : 'Optional'}
answer={answers[currentQuestion.id] || ''}
onAnswerChange={handleAnswerChange}
onBack={currentStep > 0 ? handleBack : undefined}
onNext={
currentStep < visibleQuestions.length - 1
? handleNext
: (isCurrentQuestionAnswered() || !currentQuestion.required)
? handleSubmit
: undefined
}
nextLabel={
currentStep < visibleQuestions.length - 1
? 'Next'
: isSubmitting
? 'Submitting...'
: 'Submit & Generate Report'
}
showNavigation={true}
/>
</div>
</div>
{/* Alternative Input for Different Question Types */}
<div className="flex justify-center mb-8">
<div className="w-full max-w-2xl">
<Card className="p-6">
<Question
label={`${currentStep + 1}. ${currentQuestion.prompt}`}
required={currentQuestion.required}
description={`Category: ${currentQuestion.category}`}
>
<QuestionInput
question={currentQuestion}
value={answers[currentQuestion.id] || ''}
onChange={handleAnswerChange}
/>
</Question>
</Card>
</div>
</div>
{/* Error Display */}
{error && (
<div className="mb-6 flex justify-center">
<div className="w-full max-w-2xl">
<Alert variant="error" title="Error">
{error}
</Alert>
</div>
</div>
)}
{/* Navigation Buttons */}
<div className="flex justify-center gap-4">
{currentStep > 0 && (
<Button
variant="secondary"
onClick={handleBack}
disabled={isSubmitting}
>
Back
</Button>
)}
{currentStep < visibleQuestions.length - 1 ? (
<Button
onClick={handleNext}
disabled={currentQuestion.required && !isCurrentQuestionAnswered()}
>
Next
</Button>
) : (
<Button
onClick={handleSubmit}
disabled={
isSubmitting ||
(currentQuestion.required && !isCurrentQuestionAnswered())
}
>
{isSubmitting ? 'Submitting & Generating Report...' : 'Submit & Generate AI Report'}
</Button>
)}
</div>
{/* Help Text */}
<div className="text-center mt-6">
<p className="text-sm text-[--text-secondary]">
{currentQuestion.required && !isCurrentQuestionAnswered()
? 'Please answer this required question to continue.'
: 'You can skip optional questions or come back to them later.'
}
</p>
</div>
</div>
</div>
);
};
export default EmployeeQuestionnaireSteps;