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, EmployeeQuestion } from '../employeeQuestions';
import { API_URL } from '../constants';
// Icon SVG Component
const AuditlyIcon: React.FC = () => (
);
// Progress Bar Component for Section Headers
const SectionProgressBar: React.FC<{ currentStep: number; totalSteps: number }> = ({ currentStep, totalSteps }) => {
return (
{Array.from({ length: Math.min(7, totalSteps) }, (_, index) => {
const isActive = index < Math.ceil((currentStep / totalSteps) * 7);
return (
);
})}
);
};
// Question Input Component
const QuestionInput: React.FC<{
question: EmployeeQuestion;
value: string;
onChange: (value: string) => void;
}> = ({ question, value, onChange }) => {
switch (question.type) {
case 'scale':
return (
{question.prompt}
{question.scaleLabels?.min}
{Array.from({ length: question.scaleMax! - question.scaleMin! + 1 }, (_, index) => {
const ratingValue = question.scaleMin! + index;
const isSelected = parseInt(value) === ratingValue;
return (
);
})}
{question.scaleLabels?.max}
);
case 'yesno':
return (
{question.prompt}
onChange('No')}
className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'No'
? 'bg-Neutrals-NeutralSlate800'
: 'bg-Neutrals-NeutralSlate100 hover:bg-Neutrals-NeutralSlate200'
}`}
>
No
onChange('Yes')}
className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'Yes'
? 'bg-Neutrals-NeutralSlate800'
: 'bg-Neutrals-NeutralSlate100 hover:bg-Neutrals-NeutralSlate200'
}`}
>
Yes
);
case 'textarea':
return (
);
default: // text input
return (
);
}
};
// Navigation Buttons Component
const NavigationButtons: React.FC<{
onBack?: () => void;
onNext: () => void;
onSkip?: () => void;
nextDisabled?: boolean;
isSubmitting?: boolean;
currentStep: number;
totalSteps: number;
isLastStep?: boolean;
}> = ({ onBack, onNext, onSkip, nextDisabled, isSubmitting, currentStep, totalSteps, isLastStep }) => {
return (
{onBack && (
)}
{onSkip && (
)}
);
};
// Welcome Screen Component
const WelcomeScreen: React.FC<{
onStart: () => void;
currentEmployee?: any;
isInviteFlow: boolean;
error?: string;
}> = ({ onStart, currentEmployee, isInviteFlow, error }) => {
return (
Welcome to Auditly!
Please complete this questionnaire to help us understand your role and create personalized insights.
{currentEmployee && (
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
)}
{error && (
⚠️ {error}
)}
);
};
// Question Step Component
const QuestionStep: React.FC<{
question: EmployeeQuestion;
value: string;
onChange: (value: string) => void;
onNext: () => void;
onBack?: () => void;
onSkip?: () => void;
currentStep: number;
totalSteps: number;
isSubmitting?: boolean;
isLastStep?: boolean;
}> = ({ question, value, onChange, onNext, onBack, onSkip, currentStep, totalSteps, isSubmitting, isLastStep }) => {
const isRequired = question.required;
const isAnswered = value && value.trim().length > 0;
const nextDisabled = isRequired ? !isAnswered : false;
return (
{/* Progress indicators */}
{currentStep} of {totalSteps}
);
};
// Thank You Page Component
const ThankYouPage: React.FC = () => {
return (
Thank you! Your assessment has been submitted!
Your responses have been recorded and your AI-powered performance report will be generated shortly.
);
};
// Main Component
const EmployeeQuestionnaireMerged: 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(0); // 0 = welcome screen
const [answers, setAnswers] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [inviteEmployee, setInviteEmployee] = useState(null);
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
// Get non-followup questions (we'll handle followups conditionally)
const visibleQuestions = EMPLOYEE_QUESTIONS.filter(q => !q.followupTo);
const totalSteps = visibleQuestions.length;
// 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);
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 handleAnswerChange = (questionId: string, value: string) => {
setAnswers(prev => ({ ...prev, [questionId]: value }));
};
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
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
inviteCode: inviteCode,
answers: answers,
orgId: orgId
})
});
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 {
// 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`);
setIsSubmitting(false);
return;
}
// 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(totalSteps + 1); // 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(totalSteps + 1);
} 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 = () => {
if (currentStep === 0) {
// From welcome screen to first question
setCurrentStep(1);
} else if (currentStep === totalSteps) {
// From last question to submission
handleSubmit();
} else {
// Between questions
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSkip = () => {
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
}
};
// Early return for invite flow loading state
if (isInviteFlow && isLoadingInvite) {
return (
A
Loading Your Invitation...
Please wait while we verify your invitation.
);
}
// Early return for invite flow error state
if (isInviteFlow && error && currentStep === 0) {
return (
!
Invitation Error
{error}
);
}
// Render current step
if (currentStep === 0) {
// Welcome screen
return (
);
} else if (currentStep > totalSteps) {
// Thank you page
return ;
} else {
// Question step
const question = visibleQuestions[currentStep - 1];
const isLastStep = currentStep === totalSteps;
return (
handleAnswerChange(question.id, value)}
onNext={handleNext}
onBack={currentStep > 1 ? handleBack : undefined}
onSkip={!question.required ? handleSkip : undefined}
currentStep={currentStep}
totalSteps={totalSteps}
isSubmitting={isSubmitting}
isLastStep={isLastStep}
/>
);
}
};
export default EmployeeQuestionnaireMerged;