fis
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,7 +31,6 @@ dist-ssr
|
||||
/figma/
|
||||
/figma-code/
|
||||
/server-minimal.js
|
||||
/employeeQuestions.ts
|
||||
/README.md
|
||||
/metadata.json
|
||||
**/Demo.tsx
|
||||
|
||||
@@ -13,7 +13,6 @@ interface AuthContextType {
|
||||
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<void>;
|
||||
sendOTP: (email: string, inviteCode?: string) => Promise<any>;
|
||||
verifyOTP: (email: string, otp: string, inviteCode?: string) => Promise<void>;
|
||||
signInWithOTP: (token: string, userData: any) => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(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 (
|
||||
<AuthContext.Provider value={{
|
||||
@@ -261,7 +248,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
signUpWithEmail,
|
||||
sendOTP,
|
||||
verifyOTP,
|
||||
signInWithOTP
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
|
||||
324
employeeQuestions.ts
Normal file
324
employeeQuestions.ts
Normal file
@@ -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<string, string>;
|
||||
@@ -2,14 +2,180 @@ 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';
|
||||
import { FigmaRatingScale, FigmaTextArea, FigmaNavigationButtons } from '../components/figma/FigmaQuestion';
|
||||
import { FigmaMultipleChoice } from '../components/figma/FigmaMultipleChoice';
|
||||
|
||||
// Icon SVG Component - From EmployeeFormsController
|
||||
const AuditlyIcon: React.FC = () => (
|
||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57408 17.8138C3.11835 18.3649 3.11834 19.2585 2.57406 19.8097L2.54619 19.8379C2.00191 20.389 1.11946 20.389 0.57519 19.8379C0.030919 19.2867 0.0309274 18.3931 0.575208 17.842L0.603083 17.8137C1.14736 17.2626 2.02981 17.2626 2.57408 17.8138Z" fill="url(#paint0_linear_981_10577)" />
|
||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12583 18.2374C9.66912 18.7896 9.66752 19.6832 9.12226 20.2333L5.2617 24.1286C4.71644 24.6787 3.83399 24.6771 3.2907 24.125C2.74741 23.5728 2.74901 22.6792 3.29427 22.1291L7.15483 18.2338C7.70009 17.6837 8.58254 17.6853 9.12583 18.2374Z" fill="url(#paint1_linear_981_10577)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_981_10577" x1="1.57463" y1="17.4004" x2="1.57463" y2="20.2513" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_981_10577" x1="6.20827" y1="17.8223" x2="6.20827" y2="24.5401" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// 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 (
|
||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||
<div className="left-[12px] top-[9.33px] absolute">
|
||||
<AuditlyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="px-3 py-1.5 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">{sectionNumber}</div>
|
||||
</div>
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">{title}</div>
|
||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
||||
<img className="self-stretch flex-1" src={imageUrl} alt={title} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||
<div className="left-[12px] top-[9.33px] absolute">
|
||||
<AuditlyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-2xl font-semibold font-['Inter'] leading-8">Welcome to the Auditly Employee Assessment</div>
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">Let's learn about your role, contribution and help us get a better understand of how you work best.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight">Your Role & Output</div>
|
||||
</div>
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-sm font-normal font-['Inter'] leading-tight">Tell us about your current role and what you work on</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">What is your role at the company?</div>
|
||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.role}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">What department do you work in?</div>
|
||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.department}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-3">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.name || !formData.role || !formData.department}
|
||||
className="flex-1 px-6 py-3.5 bg-Brand-Orange rounded-[999px] inline-flex justify-center items-center gap-2 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="justify-center text-Neutrals-NeutralSlate0 text-base font-medium font-['Inter'] leading-normal">Continue</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Controller Component with Backend Integration - Merged Logic
|
||||
const EmployeeQuestionnaire: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -31,7 +197,9 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
generateEmployeeReport = null;
|
||||
employees = [];
|
||||
}
|
||||
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
|
||||
@@ -47,7 +215,6 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
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();
|
||||
@@ -82,14 +249,12 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
// 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
|
||||
@@ -97,58 +262,23 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// If no match by email, and we're in demo mode with only one recent employee, use that
|
||||
// Demo mode fallbacks
|
||||
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
|
||||
})
|
||||
body: JSON.stringify({ code: inviteCode })
|
||||
});
|
||||
|
||||
if (!consumeResponse.ok) {
|
||||
@@ -183,70 +313,26 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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());
|
||||
// Convert form data to EMPLOYEE_QUESTIONS format for backend
|
||||
const answers: EmployeeSubmissionAnswers = {};
|
||||
|
||||
if (missingAnswers.length > 0) {
|
||||
setError(`Please answer all required questions: ${missingAnswers.map(q => q.prompt).join(', ')}`);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
|
||||
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;
|
||||
}
|
||||
// Add all other form data fields
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (formData[key] && !answers[key]) {
|
||||
answers[key] = formData[key];
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -255,26 +341,46 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
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 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
|
||||
}
|
||||
});
|
||||
// Show thank you page
|
||||
setCurrentStep(38);
|
||||
} else {
|
||||
setError(result.message || 'Failed to submit questionnaire');
|
||||
}
|
||||
@@ -286,15 +392,28 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getProgressPercentage = () => {
|
||||
const answeredQuestions = Object.keys(answers).filter(key => answers[key]?.trim()).length;
|
||||
return Math.round((answeredQuestions / visibleQuestions.length) * 100);
|
||||
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 (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
A
|
||||
@@ -307,111 +426,192 @@ const EmployeeQuestionnaire: React.FC = () => {
|
||||
}
|
||||
|
||||
// Early return for invite flow error state
|
||||
if (isInviteFlow && error) {
|
||||
if (isInviteFlow && error && currentStep === 1) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
!
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Invitation Error</h1>
|
||||
<p className="text-[--text-secondary] mb-6">{error}</p>
|
||||
<Button onClick={() => window.location.href = '/'}>
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Return to Homepage
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 <EmployeeFormStep1 onNext={handleNext} />;
|
||||
case 2:
|
||||
return (
|
||||
<SectionIntro
|
||||
sectionNumber="1 of 7"
|
||||
title="Personal Information"
|
||||
description="Let's start by getting to know you better. We'll ask about your role and background."
|
||||
onStart={() => handleNext()}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return <div className="p-8 text-center"><h2>Step 3 - Personal Information Form</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 4:
|
||||
return <div className="p-8 text-center"><h2>Step 4 - Email Validation</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 5:
|
||||
return <div className="p-8 text-center"><h2>Step 5 - Department Details</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 6:
|
||||
return <div className="p-8 text-center"><h2>Step 6 - Role Description</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 7:
|
||||
return <div className="p-8 text-center"><h2>Step 7 - Experience Level</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 8:
|
||||
return <div className="p-8 text-center"><h2>Step 8 - Skills Assessment</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 9:
|
||||
return (
|
||||
<SectionIntro
|
||||
sectionNumber="2 of 7"
|
||||
title="Work Environment & Culture"
|
||||
description="Let's explore your work environment and team dynamics."
|
||||
onStart={() => handleNext()}
|
||||
/>
|
||||
);
|
||||
case 10:
|
||||
return <div className="p-8 text-center"><h2>Step 10 - Team Dynamics</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 11:
|
||||
return <div className="p-8 text-center"><h2>Step 11 - Communication Style</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 12:
|
||||
return <div className="p-8 text-center"><h2>Step 12 - Work Preferences</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 13:
|
||||
return <div className="p-8 text-center"><h2>Step 13 - Collaboration Rating</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 14:
|
||||
return <div className="p-8 text-center"><h2>Step 14 - Remote Work</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 15:
|
||||
return <div className="p-8 text-center"><h2>Step 15 - Work-Life Balance</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 16:
|
||||
return (
|
||||
<SectionIntro
|
||||
sectionNumber="3 of 7"
|
||||
title="Performance & Goals"
|
||||
description="Let's discuss your performance metrics and career goals."
|
||||
onStart={() => handleNext()}
|
||||
/>
|
||||
);
|
||||
case 17:
|
||||
return <div className="p-8 text-center"><h2>Step 17 - Goal Setting</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 18:
|
||||
return <div className="p-8 text-center"><h2>Step 18 - Achievement Rating</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 19:
|
||||
return <div className="p-8 text-center"><h2>Step 19 - Performance Metrics</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 20:
|
||||
return <div className="p-8 text-center"><h2>Step 20 - Career Aspirations</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 21:
|
||||
return (
|
||||
<SectionIntro
|
||||
sectionNumber="4 of 7"
|
||||
title="Training & Development"
|
||||
description="Let's explore your learning preferences and development needs."
|
||||
onStart={() => handleNext()}
|
||||
/>
|
||||
);
|
||||
case 22:
|
||||
return <div className="p-8 text-center"><h2>Step 22 - Learning Style</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 23:
|
||||
return <div className="p-8 text-center"><h2>Step 23 - Skill Development</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 24:
|
||||
return <div className="p-8 text-center"><h2>Step 24 - Training Preferences</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 25:
|
||||
return (
|
||||
<SectionIntro
|
||||
sectionNumber="5 of 7"
|
||||
title="Feedback & Recognition"
|
||||
description="Let's discuss feedback mechanisms and recognition preferences."
|
||||
onStart={() => handleNext()}
|
||||
/>
|
||||
);
|
||||
case 26:
|
||||
return <div className="p-8 text-center"><h2>Step 26 - Feedback Style</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 27:
|
||||
return <div className="p-8 text-center"><h2>Step 27 - Recognition Preferences</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 28:
|
||||
return <div className="p-8 text-center"><h2>Step 28 - Performance Reviews</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 29:
|
||||
return <div className="p-8 text-center"><h2>Step 29 - Manager Relationship</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 30:
|
||||
return (
|
||||
<SectionIntro
|
||||
sectionNumber="6 of 7"
|
||||
title="Innovation & Problem Solving"
|
||||
description="Let's explore your approach to innovation and problem-solving."
|
||||
onStart={() => handleNext()}
|
||||
/>
|
||||
);
|
||||
case 31:
|
||||
return <div className="p-8 text-center"><h2>Step 31 - Problem Solving</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 32:
|
||||
return <div className="p-8 text-center"><h2>Step 32 - Innovation Rating</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 33:
|
||||
return <div className="p-8 text-center"><h2>Step 33 - Creative Thinking</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 34:
|
||||
return (
|
||||
<SectionIntro
|
||||
sectionNumber="7 of 7"
|
||||
title="Leadership & Organizational Structure"
|
||||
description="Now we'll explore your thoughts on our organizational structure and leadership effectiveness."
|
||||
onStart={() => handleNext()}
|
||||
/>
|
||||
);
|
||||
case 35:
|
||||
return <div className="p-8 text-center"><h2>Step 35 - Leadership Style</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 36:
|
||||
return <div className="p-8 text-center"><h2>Step 36 - Organizational Structure</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Next</button></div>;
|
||||
case 37:
|
||||
return <div className="p-8 text-center"><h2>Step 37 - Final Feedback</h2><button onClick={() => handleNext()} className="px-4 py-2 bg-blue-600 text-white rounded mt-4">Submit</button></div>;
|
||||
case 38:
|
||||
return (
|
||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||
<div className="left-[12px] top-[9.33px] absolute">
|
||||
<AuditlyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">Thank you! Your assessment has been submitted!</div>
|
||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">Your responses have been recorded and your AI-powered performance report will be generated shortly.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-[810px] px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Thank you" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div className="p-8 text-center">Form completed!</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-[--accent] 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 className="min-h-screen bg-white">
|
||||
{renderStep()}
|
||||
{error && (
|
||||
<div className="fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm text-[--text-secondary]">Progress</span>
|
||||
<span className="text-sm text-[--text-secondary]">{getProgressPercentage()}%</span>
|
||||
</div>
|
||||
<LinearProgress value={getProgressPercentage()} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<div className="space-y-6">
|
||||
{visibleQuestions.map((question, index) => (
|
||||
<Question
|
||||
key={question.id}
|
||||
label={`${index + 1}. ${question.prompt}`}
|
||||
required={question.required}
|
||||
description={`Category: ${question.category}`}
|
||||
>
|
||||
<QuestionInput
|
||||
question={question}
|
||||
value={answers[question.id] || ''}
|
||||
onChange={(value) => handleAnswerChange(question.id, value)}
|
||||
allAnswers={answers}
|
||||
onFollowupChange={handleFollowupChange}
|
||||
/>
|
||||
</Question>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-6">
|
||||
<Alert variant="error" title="Error">
|
||||
{error}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-center">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || getProgressPercentage() < 70}
|
||||
className="px-8 py-3"
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{getProgressPercentage() < 70 && (
|
||||
<p className="text-center text-sm text-[--text-secondary] mt-4">
|
||||
Please answer at least 70% of the questions to submit.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
673
pages/EmployeeQuestionnaireMerged.tsx
Normal file
673
pages/EmployeeQuestionnaireMerged.tsx
Normal file
@@ -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 = () => (
|
||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57408 17.8138C3.11835 18.3649 3.11834 19.2585 2.57406 19.8097L2.54619 19.8379C2.00191 20.389 1.11946 20.389 0.57519 19.8379C0.030919 19.2867 0.0309274 18.3931 0.575208 17.842L0.603083 17.8137C1.14736 17.2626 2.02981 17.2626 2.57408 17.8138Z" fill="url(#paint0_linear_981_10577)" />
|
||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12583 18.2374C9.66912 18.7896 9.66752 19.6832 9.12226 20.2333L5.2617 24.1286C4.71644 24.6787 3.83399 24.6771 3.2907 24.125C2.74741 23.5728 2.74901 22.6792 3.29427 22.1291L7.15483 18.2338C7.70009 17.6837 8.58254 17.6853 9.12583 18.2374Z" fill="url(#paint1_linear_981_10577)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_981_10577" x1="1.57463" y1="17.4004" x2="1.57463" y2="20.2513" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_981_10577" x1="6.20827" y1="17.8223" x2="6.20827" y2="24.5401" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Progress Bar Component for Section Headers
|
||||
const SectionProgressBar: React.FC<{ currentStep: number; totalSteps: number }> = ({ currentStep, totalSteps }) => {
|
||||
return (
|
||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
{Array.from({ length: Math.min(7, totalSteps) }, (_, index) => {
|
||||
const isActive = index < Math.ceil((currentStep / totalSteps) * 7);
|
||||
return (
|
||||
<div key={index}>
|
||||
{isActive ? (
|
||||
<div className="w-6 h-1 bg-Brand-Orange rounded-3xl" />
|
||||
) : (
|
||||
<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="4" height="4" rx="2" fill="var(--Neutrals-NeutralSlate300, #D5D7DA)" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Question Input Component
|
||||
const QuestionInput: React.FC<{
|
||||
question: EmployeeQuestion;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}> = ({ question, value, onChange }) => {
|
||||
switch (question.type) {
|
||||
case 'scale':
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch flex justify-between items-center gap-2">
|
||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.min}</span>
|
||||
<div className="flex gap-2">
|
||||
{Array.from({ length: question.scaleMax! - question.scaleMin! + 1 }, (_, index) => {
|
||||
const ratingValue = question.scaleMin! + index;
|
||||
const isSelected = parseInt(value) === ratingValue;
|
||||
return (
|
||||
<button
|
||||
key={ratingValue}
|
||||
onClick={() => onChange(ratingValue.toString())}
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
|
||||
isSelected
|
||||
? 'bg-Brand-Orange text-white'
|
||||
: 'bg-Neutrals-NeutralSlate100 text-Neutrals-NeutralSlate700 hover:bg-Neutrals-NeutralSlate200'
|
||||
}`}
|
||||
>
|
||||
{ratingValue}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.max}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'yesno':
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${
|
||||
value === 'No' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
||||
}`}>
|
||||
No
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${
|
||||
value === 'Yes' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
||||
}`}>
|
||||
Yes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch min-h-40 p-5 relative bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-start gap-2.5">
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 outline-none resize-none"
|
||||
placeholder={question.placeholder || "Type your answer...."}
|
||||
rows={6}
|
||||
/>
|
||||
<div className="w-3 h-3 absolute right-5 bottom-5">
|
||||
<div className="w-2 h-2 absolute top-0.5 left-0.5 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
||||
<div className="w-1 h-1 absolute bottom-0 right-0 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default: // text input
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<input
|
||||
type={question.id === 'email' ? 'email' : 'text'}
|
||||
value={value}
|
||||
onChange={(e) => onChange(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={question.placeholder || "Enter your answer..."}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||
{onBack && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={nextDisabled || isSubmitting}
|
||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">
|
||||
{isSubmitting ? 'Submitting...' : (isLastStep ? 'Submit' : 'Next')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{onSkip && (
|
||||
<div
|
||||
onClick={onSkip}
|
||||
className="px-3 py-1.5 absolute right-6 top-6 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-Neutrals-NeutralSlate200"
|
||||
>
|
||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Welcome Screen Component
|
||||
const WelcomeScreen: React.FC<{
|
||||
onStart: () => void;
|
||||
currentEmployee?: any;
|
||||
isInviteFlow: boolean;
|
||||
error?: string;
|
||||
}> = ({ onStart, currentEmployee, isInviteFlow, error }) => {
|
||||
return (
|
||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||
<div className="left-[12px] top-[9.33px] absolute">
|
||||
<AuditlyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||
Welcome to Auditly!
|
||||
</div>
|
||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
||||
Please complete this questionnaire to help us understand your role and create personalized insights.
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
{error && (
|
||||
<div className="inline-flex items-center px-4 py-2 bg-red-100 dark:bg-red-900 rounded-lg">
|
||||
<span className="text-sm text-red-800 dark:text-red-200">
|
||||
⚠️ {error}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start Assessment</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Welcome" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="w-full h-screen py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||
<QuestionInput question={question} value={value} onChange={onChange} />
|
||||
<NavigationButtons
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
onSkip={onSkip}
|
||||
nextDisabled={nextDisabled}
|
||||
isSubmitting={isSubmitting}
|
||||
currentStep={currentStep}
|
||||
totalSteps={totalSteps}
|
||||
isLastStep={isLastStep}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress indicators */}
|
||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||
{currentStep} of {totalSteps}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
||||
<SectionProgressBar currentStep={currentStep} totalSteps={totalSteps} />
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate500 text-base font-medium font-['Neue_Montreal'] leading-normal">
|
||||
Employee Assessment
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Thank You Page Component
|
||||
const ThankYouPage: React.FC = () => {
|
||||
return (
|
||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||
<div className="left-[12px] top-[9.33px] absolute">
|
||||
<AuditlyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||
Thank you! Your assessment has been submitted!
|
||||
</div>
|
||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
||||
Your responses have been recorded and your AI-powered performance report will be generated shortly.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Thank you" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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<EmployeeSubmissionAnswers>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [inviteEmployee, setInviteEmployee] = useState<any>(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 (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-[--accent] 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-4">Loading Your Invitation...</h1>
|
||||
<p className="text-[--text-secondary]">Please wait while we verify your invitation.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Early return for invite flow error state
|
||||
if (isInviteFlow && error && currentStep === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
!
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Invitation Error</h1>
|
||||
<p className="text-[--text-secondary] mb-6">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Return to Homepage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render current step
|
||||
if (currentStep === 0) {
|
||||
// Welcome screen
|
||||
return (
|
||||
<WelcomeScreen
|
||||
onStart={handleNext}
|
||||
currentEmployee={currentEmployee}
|
||||
isInviteFlow={isInviteFlow}
|
||||
error={!currentEmployee && !isInviteFlow ? `Employee info not found. User: ${user?.email}` : undefined}
|
||||
/>
|
||||
);
|
||||
} else if (currentStep > totalSteps) {
|
||||
// Thank you page
|
||||
return <ThankYouPage />;
|
||||
} else {
|
||||
// Question step
|
||||
const question = visibleQuestions[currentStep - 1];
|
||||
const isLastStep = currentStep === totalSteps;
|
||||
|
||||
return (
|
||||
<QuestionStep
|
||||
question={question}
|
||||
value={answers[question.id] || ''}
|
||||
onChange={(value) => 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;
|
||||
675
pages/EmployeeQuestionnaire_Backup.tsx
Normal file
675
pages/EmployeeQuestionnaire_Backup.tsx
Normal file
@@ -0,0 +1,675 @@
|
||||
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
|
||||
const AuditlyIcon: React.FC = () => (
|
||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57408 17.8138C3.11835 18.3649 3.11834 19.2585 2.57406 19.8097L2.54619 19.8379C2.00191 20.389 1.11946 20.389 0.57519 19.8379C0.030919 19.2867 0.0309274 18.3931 0.575208 17.842L0.603083 17.8137C1.14736 17.2626 2.02981 17.2626 2.57408 17.8138Z" fill="url(#paint0_linear_981_10577)" />
|
||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12583 18.2374C9.66912 18.7896 9.66752 19.6832 9.12226 20.2333L5.2617 24.1286C4.71644 24.6787 3.83399 24.6771 3.2907 24.125C2.74741 23.5728 2.74901 22.6792 3.29427 22.1291L7.15483 18.2338C7.70009 17.6837 8.58254 17.6853 9.12583 18.2374Z" fill="url(#paint1_linear_981_10577)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_981_10577" x1="1.57463" y1="17.4004" x2="1.57463" y2="20.2513" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_981_10577" x1="6.20827" y1="17.8223" x2="6.20827" y2="24.5401" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Progress Bar Component for Section Headers
|
||||
const SectionProgressBar: React.FC<{ currentStep: number; totalSteps: number }> = ({ currentStep, totalSteps }) => {
|
||||
return (
|
||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
{Array.from({ length: Math.min(7, totalSteps) }, (_, index) => {
|
||||
const isActive = index < Math.ceil((currentStep / totalSteps) * 7);
|
||||
return (
|
||||
<div key={index}>
|
||||
{isActive ? (
|
||||
<div className="w-6 h-1 bg-Brand-Orange rounded-3xl" />
|
||||
) : (
|
||||
<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="4" height="4" rx="2" fill="var(--Neutrals-NeutralSlate300, #D5D7DA)" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Question Input Component
|
||||
const QuestionInput: React.FC<{
|
||||
question: EmployeeQuestion;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}> = ({ question, value, onChange }) => {
|
||||
switch (question.type) {
|
||||
case 'scale':
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch flex justify-between items-center gap-2">
|
||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.min}</span>
|
||||
<div className="flex gap-2">
|
||||
{Array.from({ length: question.scaleMax! - question.scaleMin! + 1 }, (_, index) => {
|
||||
const ratingValue = question.scaleMin! + index;
|
||||
const isSelected = parseInt(value) === ratingValue;
|
||||
return (
|
||||
<button
|
||||
key={ratingValue}
|
||||
onClick={() => onChange(ratingValue.toString())}
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
|
||||
isSelected
|
||||
? 'bg-Brand-Orange text-white'
|
||||
: 'bg-Neutrals-NeutralSlate100 text-Neutrals-NeutralSlate700 hover:bg-Neutrals-NeutralSlate200'
|
||||
}`}
|
||||
>
|
||||
{ratingValue}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.max}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'yesno':
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${
|
||||
value === 'No' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
||||
}`}>
|
||||
No
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${
|
||||
value === 'Yes' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
||||
}`}>
|
||||
Yes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch min-h-40 p-5 relative bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-start gap-2.5">
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 outline-none resize-none"
|
||||
placeholder={question.placeholder || "Type your answer...."}
|
||||
rows={6}
|
||||
/>
|
||||
<div className="w-3 h-3 absolute right-5 bottom-5">
|
||||
<div className="w-2 h-2 absolute top-0.5 left-0.5 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
||||
<div className="w-1 h-1 absolute bottom-0 right-0 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default: // text input
|
||||
return (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question.prompt}
|
||||
</div>
|
||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<input
|
||||
type={question.id === 'email' ? 'email' : 'text'}
|
||||
value={value}
|
||||
onChange={(e) => onChange(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={question.placeholder || "Enter your answer..."}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||
{onBack && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={nextDisabled || isSubmitting}
|
||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">
|
||||
{isSubmitting ? 'Submitting...' : (isLastStep ? 'Submit' : 'Next')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{onSkip && (
|
||||
<div
|
||||
onClick={onSkip}
|
||||
className="px-3 py-1.5 absolute right-6 top-6 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-Neutrals-NeutralSlate200"
|
||||
>
|
||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Welcome Screen Component
|
||||
const WelcomeScreen: React.FC<{
|
||||
onStart: () => void;
|
||||
currentEmployee?: any;
|
||||
isInviteFlow: boolean;
|
||||
error?: string;
|
||||
}> = ({ onStart, currentEmployee, isInviteFlow, error }) => {
|
||||
return (
|
||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||
<div className="left-[12px] top-[9.33px] absolute">
|
||||
<AuditlyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||
Welcome to Auditly!
|
||||
</div>
|
||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
||||
Please complete this questionnaire to help us understand your role and create personalized insights.
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
{error && (
|
||||
<div className="inline-flex items-center px-4 py-2 bg-red-100 dark:bg-red-900 rounded-lg">
|
||||
<span className="text-sm text-red-800 dark:text-red-200">
|
||||
⚠️ {error}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start Assessment</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Welcome" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="w-full h-screen py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||
<QuestionInput question={question} value={value} onChange={onChange} />
|
||||
<NavigationButtons
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
onSkip={onSkip}
|
||||
nextDisabled={nextDisabled}
|
||||
isSubmitting={isSubmitting}
|
||||
currentStep={currentStep}
|
||||
totalSteps={totalSteps}
|
||||
isLastStep={isLastStep}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress indicators */}
|
||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||
{currentStep} of {totalSteps}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
||||
<SectionProgressBar currentStep={currentStep} totalSteps={totalSteps} />
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate500 text-base font-medium font-['Neue_Montreal'] leading-normal">
|
||||
Employee Assessment
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Thank You Page Component
|
||||
const ThankYouPage: React.FC = () => {
|
||||
return (
|
||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
||||
<div className="left-[12px] top-[9.33px] absolute">
|
||||
<AuditlyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
||||
Thank you! Your assessment has been submitted!
|
||||
</div>
|
||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
||||
Your responses have been recorded and your AI-powered performance report will be generated shortly.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Thank you" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Component
|
||||
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(0); // 0 = welcome screen
|
||||
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [inviteEmployee, setInviteEmployee] = useState<any>(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 (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-[--accent] 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-4">Loading Your Invitation...</h1>
|
||||
<p className="text-[--text-secondary]">Please wait while we verify your invitation.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Early return for invite flow error state
|
||||
if (isInviteFlow && error && currentStep === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
!
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Invitation Error</h1>
|
||||
<p className="text-[--text-secondary] mb-6">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Return to Homepage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render current step
|
||||
if (currentStep === 0) {
|
||||
// Welcome screen
|
||||
return (
|
||||
<WelcomeScreen
|
||||
onStart={handleNext}
|
||||
currentEmployee={currentEmployee}
|
||||
isInviteFlow={isInviteFlow}
|
||||
error={!currentEmployee && !isInviteFlow ? `Employee info not found. User: ${user?.email}` : undefined}
|
||||
/>
|
||||
);
|
||||
} else if (currentStep > totalSteps) {
|
||||
// Thank you page
|
||||
return <ThankYouPage />;
|
||||
} else {
|
||||
// Question step
|
||||
const question = visibleQuestions[currentStep - 1];
|
||||
const isLastStep = currentStep === totalSteps;
|
||||
|
||||
return (
|
||||
<QuestionStep
|
||||
question={question}
|
||||
value={answers[question.id] || ''}
|
||||
onChange={(value) => 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 EmployeeQuestionnaire;
|
||||
@@ -1,52 +0,0 @@
|
||||
// URL utilities for consistent URL management across the application
|
||||
|
||||
// Get site URL from environment variable or default
|
||||
export const getSiteUrl = (): string => {
|
||||
// For client-side (Vite)
|
||||
if (typeof window !== 'undefined') {
|
||||
return import.meta.env.VITE_SITE_URL || window.location.origin;
|
||||
}
|
||||
|
||||
// For server-side (Node.js)
|
||||
return process.env.SITE_URL || 'http://localhost:5173';
|
||||
};
|
||||
|
||||
// Get API URL from environment variable or default
|
||||
export const getApiUrl = (): string => {
|
||||
// For client-side (Vite)
|
||||
if (typeof window !== 'undefined') {
|
||||
return import.meta.env.VITE_API_URL || 'http://localhost:5050';
|
||||
}
|
||||
|
||||
// For server-side (Node.js)
|
||||
return process.env.API_URL || 'http://localhost:5050';
|
||||
};
|
||||
|
||||
// Build full API endpoint URL
|
||||
export const buildApiUrl = (endpoint: string): string => {
|
||||
const baseUrl = getApiUrl();
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
return `${baseUrl}${cleanEndpoint}`;
|
||||
};
|
||||
|
||||
// Build full site URL
|
||||
export const buildSiteUrl = (path: string): string => {
|
||||
const baseUrl = getSiteUrl();
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${baseUrl}${cleanPath}`;
|
||||
};
|
||||
|
||||
// For hash-based routing (React Router with HashRouter)
|
||||
export const buildHashUrl = (path: string): string => {
|
||||
const baseUrl = getSiteUrl();
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${baseUrl}/#${cleanPath}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
getSiteUrl,
|
||||
getApiUrl,
|
||||
buildApiUrl,
|
||||
buildSiteUrl,
|
||||
buildHashUrl
|
||||
};
|
||||
Reference in New Issue
Block a user