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 { Card, Button } from '../components/UiKit'; import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions'; import { Question } from '../components/ui/Question'; import { QuestionInput } from '../components/ui/QuestionInput'; import { LinearProgress } from '../components/ui/Progress'; import { Alert } from '../components/ui/Alert'; import { API_URL } from '../constants'; 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 [answers, setAnswers] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(''); const [inviteEmployee, setInviteEmployee] = useState(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 { // Use Cloud Function endpoint for invite status 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); // 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 ); } } // If no match by email, and we're in demo mode with only one recent employee, use that if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) { // In demo mode, if there's only one employee or the most recent one, use it currentEmployee = employees[employees.length - 1]; } // If still no match and there's only one employee, assume it's them if (!currentEmployee && employees.length === 1) { currentEmployee = employees[0]; } } // Enhanced debugging console.log('EmployeeQuestionnaire debug:', { userEmail: user?.email, employeesCount: employees.length, employeeEmails: employees.map(e => ({ id: e.id, email: e.email, name: e.name })), invitedEmployee, currentEmployee, locationState: location.state }); const handleAnswerChange = (questionId: string, value: string) => { setAnswers(prev => ({ ...prev, [questionId]: value })); }; // Filter out followup questions that shouldn't be shown yet const getVisibleQuestions = () => { return EMPLOYEE_QUESTIONS.filter(question => { // Hide follow-up questions since they're now integrated into the parent yes/no question if (question.followupTo) return false; return true; }); }; const handleFollowupChange = (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 visibleQuestions = getVisibleQuestions(); 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) { // Generate LLM report for fallback employee console.log('Questionnaire submitted for fallback employee, generating report...'); 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; } // 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 result = await submitEmployeeAnswers(currentEmployee.id, answers); } if (result.success) { // Show success message with AI report info const message = result.reportGenerated ? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.' : 'Questionnaire submitted successfully! Your report will be available shortly.'; setError(null); // Navigate to completion page with success info 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 = () => { const answeredQuestions = Object.keys(answers).filter(key => answers[key]?.trim()).length; return Math.round((answeredQuestions / visibleQuestions.length) * 100); }; // 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) { return (
!

Invitation Error

{error}

); } return (
A

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})`}
) : (
⚠️ Employee info not found. User: {user?.email}, Employees: {employees.length}

Don't worry - your account was created successfully! This is likely a temporary sync issue.

You can still complete the questionnaire, and we'll match it to your profile automatically.

)}
{/* Progress Bar */}
Progress {getProgressPercentage()}%
{ e.preventDefault(); handleSubmit(); }}>
{visibleQuestions.map((question, index) => ( handleAnswerChange(question.id, value)} allAnswers={answers} onFollowupChange={handleFollowupChange} /> ))}
{error && (
{error}
)}
{getProgressPercentage() < 70 && (

Please answer at least 70% of the questions to submit.

)}
); }; export default EmployeeQuestionnaire;