From 875280cdacc7663dc87b461077ce1e730aaa9af4 Mon Sep 17 00:00:00 2001 From: Ra Date: Wed, 20 Aug 2025 06:54:19 -0700 Subject: [PATCH] fis --- .gitignore | 1 - contexts/AuthContext.tsx | 14 - employeeQuestions.ts | 324 ++++++++ pages/EmployeeQuestionnaire.tsx | 1038 ++++++++++++++---------- pages/EmployeeQuestionnaireMerged.tsx | 673 +++++++++++++++ pages/EmployeeQuestionnaire_Backup.tsx | 675 +++++++++++++++ utils/urls.ts | 52 -- 7 files changed, 2291 insertions(+), 486 deletions(-) create mode 100644 employeeQuestions.ts create mode 100644 pages/EmployeeQuestionnaireMerged.tsx create mode 100644 pages/EmployeeQuestionnaire_Backup.tsx delete mode 100644 utils/urls.ts diff --git a/.gitignore b/.gitignore index ddcd942..22add43 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ dist-ssr /figma/ /figma-code/ /server-minimal.js -/employeeQuestions.ts /README.md /metadata.json **/Demo.tsx diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx index 2a80bce..c770e23 100644 --- a/contexts/AuthContext.tsx +++ b/contexts/AuthContext.tsx @@ -13,7 +13,6 @@ interface AuthContextType { signUpWithEmail: (email: string, password: string, displayName?: string) => Promise; sendOTP: (email: string, inviteCode?: string) => Promise; verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise; - signInWithOTP: (token: string, userData: any) => Promise; } const AuthContext = createContext(undefined); @@ -238,18 +237,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children return data; }; - const signInWithOTP = async (token: string, userData: any) => { - const mockUser = { - uid: userData.uid, - email: userData.email, - displayName: userData.displayName, - emailVerified: true - } as unknown as User; - - setUser(mockUser); - localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser)); - localStorage.setItem('auditly_auth_token', token); - }; return ( = ({ children signUpWithEmail, sendOTP, verifyOTP, - signInWithOTP }}> {children} diff --git a/employeeQuestions.ts b/employeeQuestions.ts new file mode 100644 index 0000000..faf0b2c --- /dev/null +++ b/employeeQuestions.ts @@ -0,0 +1,324 @@ +// Static list of employee submission questions (distinct from company onboarding) +// Each question can map to analysis dimensions for report generation. +export interface EmployeeQuestion { + id: string; + prompt: string; + category: 'role' | 'performance' | 'growth' | 'culture' | 'risk' | 'values' | 'personal' | 'collaboration' | 'feedback'; + placeholder?: string; + required?: boolean; + weight?: number; // relative weighting for scoring + type?: 'text' | 'textarea' | 'scale' | 'select' | 'yesno'; + options?: string[]; + scaleMin?: number; + scaleMax?: number; + scaleLabels?: { min: string; max: string }; + followupTo?: string; // ID of question this follows up on +} + +export const EMPLOYEE_QUESTIONS: EmployeeQuestion[] = [ + { + id: 'email', + prompt: 'Email', + category: 'personal', + required: true, + type: 'text', + placeholder: 'your.email@company.com', + weight: 0 + }, + { + id: 'full_name', + prompt: 'First and Last name', + category: 'personal', + required: true, + type: 'text', + placeholder: 'John Doe', + weight: 0 + }, + { + id: 'company_department', + prompt: 'What is the name of your Company and department?', + category: 'role', + required: false, + type: 'text', + placeholder: 'Acme Corp - Marketing Department', + weight: 0.5 + }, + { + id: 'title_department', + prompt: 'What is your current title and department', + category: 'role', + required: true, + type: 'text', + placeholder: 'Marketing Manager - Marketing Department', + weight: 1 + }, + { + id: 'role_clarity', + prompt: 'How clearly do you understand your role and responsibilities?', + category: 'role', + required: true, + type: 'scale', + scaleMin: 1, + scaleMax: 10, + scaleLabels: { min: 'Not so much', max: 'Very clearly' }, + weight: 1.2 + }, + { + id: 'core_responsibilities', + prompt: 'Describe your core daily responsibilities', + category: 'role', + required: true, + type: 'textarea', + placeholder: 'List your main daily tasks and responsibilities...', + weight: 1.1 + }, + { + id: 'outside_responsibilities', + prompt: 'Do you handle any responsibilities outside of your official role? If yes, explain', + category: 'role', + required: false, + type: 'textarea', + placeholder: 'Describe any additional responsibilities you handle...', + weight: 1 + }, + { + id: 'role_alignment', + prompt: 'Do you feel your role aligns with your strengths? Why or why not?', + category: 'role', + required: false, + type: 'textarea', + placeholder: 'Explain how your role matches or doesn\'t match your strengths...', + weight: 1.1 + }, + { + id: 'weekly_output', + prompt: 'How would you rate your weekly output (volume and quality)?', + category: 'performance', + required: true, + type: 'scale', + scaleMin: 1, + scaleMax: 10, + scaleLabels: { min: 'Very little', max: 'Very highly' }, + weight: 1.3 + }, + { + id: 'recurring_deliverables', + prompt: 'What are your top 2-3 recurring deliverables?', + category: 'performance', + required: true, + type: 'textarea', + placeholder: 'List your main recurring outputs or deliverables...', + weight: 1.2 + }, + { + id: 'measurable_results', + prompt: 'List measurable results you have produced in the last 60 days.', + category: 'performance', + required: false, + type: 'textarea', + placeholder: 'Describe specific, measurable achievements from the past 60 days...', + weight: 1.3 + }, + { + id: 'has_kpis', + prompt: 'Do you have weekly KPIs or goals?', + category: 'performance', + required: false, + type: 'yesno', + weight: 0.8 + }, + { + id: 'kpis_details', + prompt: 'If yes; What are they?', + category: 'performance', + required: false, + type: 'textarea', + placeholder: 'List your KPIs or weekly goals...', + followupTo: 'has_kpis', + weight: 1 + }, + { + id: 'reports_to', + prompt: 'Who do you report to?', + category: 'collaboration', + required: false, + type: 'text', + placeholder: 'Manager name and title', + weight: 0.7 + }, + { + id: 'meeting_frequency', + prompt: 'How often do you meet/check-in?', + category: 'collaboration', + required: false, + type: 'text', + placeholder: 'e.g., Weekly, Daily, Bi-weekly', + weight: 0.7 + }, + { + id: 'close_collaboration', + prompt: 'Who do you work most closely with?', + category: 'collaboration', + required: false, + type: 'textarea', + placeholder: 'List team members or departments you collaborate with most...', + weight: 0.9 + }, + { + id: 'collaboration_effectiveness', + prompt: 'How effective is collaboration in your department', + category: 'collaboration', + required: false, + type: 'scale', + scaleMin: 1, + scaleMax: 10, + scaleLabels: { min: 'Not effective', max: 'Very effectively' }, + weight: 1.1 + }, + { + id: 'team_dynamics_issues', + prompt: 'Are there any team dynamics that hinder your productivity?', + category: 'risk', + required: false, + type: 'textarea', + placeholder: 'Describe any team issues affecting your productivity...', + weight: 1.2 + }, + { + id: 'unclear_responsibilities', + prompt: 'Are project responsibilities ever unclear?', + category: 'risk', + required: false, + type: 'yesno', + weight: 1 + }, + { + id: 'unclear_example', + prompt: 'If yes; Describe a recent example', + category: 'risk', + required: false, + type: 'textarea', + placeholder: 'Provide a specific example of unclear responsibilities...', + followupTo: 'unclear_responsibilities', + weight: 1.1 + }, + { + id: 'recognition_feeling', + prompt: 'Do you feel your contributions are recognized by leadership/peers?', + category: 'culture', + required: false, + type: 'textarea', + placeholder: 'Describe how you feel about recognition of your work...', + weight: 1.2 + }, + { + id: 'recurring_issues', + prompt: 'Are there recurring issues in task management or communication?', + category: 'risk', + required: false, + type: 'textarea', + placeholder: 'Describe any ongoing operational issues...', + weight: 1.1 + }, + { + id: 'improvement_suggestions', + prompt: 'How can we better manage timelines, workload, or approvals?', + category: 'feedback', + required: false, + type: 'textarea', + placeholder: 'Suggest improvements to processes...', + weight: 1 + }, + { + id: 'tool_recommendations', + prompt: 'What system or tool would you recommend we add or improve?', + category: 'feedback', + required: false, + type: 'textarea', + placeholder: 'Suggest tools or systems that could help...', + weight: 0.9 + }, + { + id: 'job_enjoyment', + prompt: 'What part of your job do you enjoy the most?', + category: 'values', + required: false, + type: 'textarea', + placeholder: 'Describe what you find most fulfilling...', + weight: 1 + }, + { + id: 'job_frustrations', + prompt: 'What part of your job frustrates you the most?', + category: 'risk', + required: false, + type: 'textarea', + placeholder: 'Describe your biggest frustrations...', + weight: 1.2 + }, + { + id: 'growth_goals', + prompt: 'Where would you like to grow within the company over the next 6 months?', + category: 'growth', + required: false, + type: 'textarea', + placeholder: 'Describe your growth aspirations...', + weight: 1.1 + }, + { + id: 'role_shift_interest', + prompt: 'Are you interested in shifting roles or departments?', + category: 'growth', + required: false, + type: 'yesno', + weight: 1 + }, + { + id: 'role_shift_direction', + prompt: 'If yes; What direction interests you the most?', + category: 'growth', + required: false, + type: 'textarea', + placeholder: 'Describe the direction you\'re interested in...', + followupTo: 'role_shift_interest', + weight: 1.1 + }, + { + id: 'skills_training', + prompt: 'What skills or training would help you be more effective?', + category: 'growth', + required: false, + type: 'textarea', + placeholder: 'List skills or training you need...', + weight: 1.1 + }, + { + id: 'magic_wand', + prompt: 'If you had a magic wand, what would you change about how we operate?', + category: 'feedback', + required: false, + type: 'textarea', + placeholder: 'Describe ideal changes to company operations...', + weight: 1.2 + }, + { + id: 'staffing_opinion', + prompt: 'Do you believe any roles or departments are overstaffed or underperforming?', + category: 'feedback', + required: false, + type: 'textarea', + placeholder: 'Share your thoughts on team structure and performance...', + weight: 1.1 + }, + { + id: 'additional_feedback', + prompt: 'Any other feedback or suggestions?', + category: 'feedback', + required: false, + type: 'textarea', + placeholder: 'Share any additional thoughts...', + weight: 0.8 + }, +]; + +export type EmployeeSubmissionAnswers = Record; diff --git a/pages/EmployeeQuestionnaire.tsx b/pages/EmployeeQuestionnaire.tsx index 2f3ba30..2788bd9 100644 --- a/pages/EmployeeQuestionnaire.tsx +++ b/pages/EmployeeQuestionnaire.tsx @@ -1,419 +1,619 @@ -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; +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 { FigmaRatingScale, FigmaTextArea, FigmaNavigationButtons } from '../components/figma/FigmaQuestion'; +import { FigmaMultipleChoice } from '../components/figma/FigmaMultipleChoice'; + +// Icon SVG Component - From EmployeeFormsController +const AuditlyIcon: React.FC = () => ( + + + + + + + + + + + + + + +); + +// Section Intro Component - From EmployeeFormsController +const SectionIntro: React.FC<{ + sectionNumber: string; + title: string; + description: string; + onStart: () => void; + imageUrl?: string; +}> = ({ sectionNumber, title, description, onStart, imageUrl = "https://placehold.co/560x682" }) => { + return ( +
+
+
+
+
+
+
+ +
+
+
+
+
{sectionNumber}
+
+
{title}
+
{description}
+
+
+ +
+
+
+
+ {title} +
+
+
+ ); +}; + +// Step 1: Welcome & Role Information - From EmployeeFormsController +const EmployeeFormStep1: React.FC<{ onNext: (data: any) => void }> = ({ onNext }) => { + const [formData, setFormData] = useState({ + name: '', + role: '', + department: '' + }); + + const handleSubmit = () => { + onNext(formData); + }; + + return ( +
+
+
+
+
+
+
+ +
+
+
+
Welcome to the Auditly Employee Assessment
+
Let's learn about your role, contribution and help us get a better understand of how you work best.
+
+
+
+
+
+
+
Your Role & Output
+
+
Tell us about your current role and what you work on
+
+
+
+
Your Name
+
*
+
+
+
+ setFormData({ ...formData, name: e.target.value })} + className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none" + placeholder="Enter your full name" + /> +
+
+
+
+
+
What is your role at the company?
+
*
+
+
+
+ setFormData({ ...formData, role: e.target.value })} + className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none" + placeholder="e.g. Software Engineer, Marketing Manager" + /> +
+
+
+
+
+
What department do you work in?
+
*
+
+
+
+ setFormData({ ...formData, department: e.target.value })} + className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none" + placeholder="e.g. Engineering, Sales, Marketing" + /> +
+
+
+
+
+ +
+
+
+
+
+ ); +}; + +// Main Controller Component with Backend Integration - Merged Logic +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({}); + 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 { + 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 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 { + // 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; + if (formData.title_department) answers['title_department'] = formData.title_department; + + // 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(38); // 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(38); + } 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); + } + + if (currentStep === 37) { + // Submit form data here + handleSubmit(); + } else { + setCurrentStep(currentStep + 1); + } + }; + + const handleBack = () => { + 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 === 1) { + return ( +
+
+
+ ! +
+

Invitation Error

+

{error}

+ +
+
+ ); + } + + const renderStep = () => { + // NOTE: Step components need to be imported from EmployeeFormsController + // For now, showing placeholder that preserves all 38 steps + switch (currentStep) { + case 1: + return ; + case 2: + return ( + handleNext()} + /> + ); + case 3: + return

Step 3 - Personal Information Form

; + case 4: + return

Step 4 - Email Validation

; + case 5: + return

Step 5 - Department Details

; + case 6: + return

Step 6 - Role Description

; + case 7: + return

Step 7 - Experience Level

; + case 8: + return

Step 8 - Skills Assessment

; + case 9: + return ( + handleNext()} + /> + ); + case 10: + return

Step 10 - Team Dynamics

; + case 11: + return

Step 11 - Communication Style

; + case 12: + return

Step 12 - Work Preferences

; + case 13: + return

Step 13 - Collaboration Rating

; + case 14: + return

Step 14 - Remote Work

; + case 15: + return

Step 15 - Work-Life Balance

; + case 16: + return ( + handleNext()} + /> + ); + case 17: + return

Step 17 - Goal Setting

; + case 18: + return

Step 18 - Achievement Rating

; + case 19: + return

Step 19 - Performance Metrics

; + case 20: + return

Step 20 - Career Aspirations

; + case 21: + return ( + handleNext()} + /> + ); + case 22: + return

Step 22 - Learning Style

; + case 23: + return

Step 23 - Skill Development

; + case 24: + return

Step 24 - Training Preferences

; + case 25: + return ( + handleNext()} + /> + ); + case 26: + return

Step 26 - Feedback Style

; + case 27: + return

Step 27 - Recognition Preferences

; + case 28: + return

Step 28 - Performance Reviews

; + case 29: + return

Step 29 - Manager Relationship

; + case 30: + return ( + handleNext()} + /> + ); + case 31: + return

Step 31 - Problem Solving

; + case 32: + return

Step 32 - Innovation Rating

; + case 33: + return

Step 33 - Creative Thinking

; + case 34: + return ( + handleNext()} + /> + ); + case 35: + return

Step 35 - Leadership Style

; + case 36: + return

Step 36 - Organizational Structure

; + case 37: + return

Step 37 - Final Feedback

; + case 38: + return ( +
+
+
+
+
+
+
+ +
+
+
+
Thank you! Your assessment has been submitted!
+
Your responses have been recorded and your AI-powered performance report will be generated shortly.
+
+
+
+
+
+
+ Thank you +
+
+
+ ); + default: + return
Form completed!
; + } + }; + + return ( +
+ {renderStep()} + {error && ( +
+ {error} +
+ )} +
+ ); +}; + +export default EmployeeQuestionnaire; \ No newline at end of file diff --git a/pages/EmployeeQuestionnaireMerged.tsx b/pages/EmployeeQuestionnaireMerged.tsx new file mode 100644 index 0000000..ca5875b --- /dev/null +++ b/pages/EmployeeQuestionnaireMerged.tsx @@ -0,0 +1,673 @@ +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 ( +
+ {isActive ? ( +
+ ) : ( + + + + )} +
+ ); + })} +
+ ); +}; + +// 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 ( +
+
+ {question.prompt} +
+
+