Fix organization setup flow: redirect to onboarding for incomplete setup

This commit is contained in:
Ra
2025-08-18 10:33:45 -07:00
commit 557b113196
60 changed files with 16246 additions and 0 deletions

View File

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