import { motion } from 'framer-motion'; import { useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Users, Settings, Shield, User, CheckCircle, Minus, Plus, Info, Clock } from 'lucide-react'; import { FormData, SystemPolicy } from '@/hooks/useCreateRequestForm'; import { useMultiUserSearch } from '@/hooks/useUserSearch'; import { ensureUserExists } from '@/services/userApi'; interface ApprovalWorkflowStepProps { formData: FormData; updateFormData: (field: keyof FormData, value: any) => void; onValidationError: (error: { type: string; email: string; message: string }) => void; systemPolicy: SystemPolicy; onPolicyViolation: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; } /** * Component: ApprovalWorkflowStep * * Purpose: Step 3 - Approval workflow configuration * * Features: * - Configure number of approvers * - Define approval hierarchy with TAT * - User search with @ prefix * - Test IDs for testing * * Note: This is a simplified version. Full implementation includes complex approver management. */ export function ApprovalWorkflowStep({ formData, updateFormData, onValidationError, systemPolicy, onPolicyViolation }: ApprovalWorkflowStepProps) { const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); // Initialize approvers array when approverCount changes - moved from render to useEffect useEffect(() => { const approverCount = formData.approverCount || 1; const currentApprovers = formData.approvers || []; // Ensure we have the correct number of approvers if (currentApprovers.length < approverCount) { const newApprovers = [...currentApprovers]; // Fill missing approver slots for (let i = currentApprovers.length; i < approverCount; i++) { if (!newApprovers[i]) { newApprovers[i] = { email: '', name: '', level: i + 1, tat: '' as any }; } } updateFormData('approvers', newApprovers); } else if (currentApprovers.length > approverCount) { // Trim excess approvers if count was reduced updateFormData('approvers', currentApprovers.slice(0, approverCount)); } }, [formData.approverCount, updateFormData]); const handleApproverEmailChange = (index: number, value: string) => { const newApprovers = [...formData.approvers]; const previousEmail = newApprovers[index]?.email; const emailChanged = previousEmail !== value; newApprovers[index] = { ...newApprovers[index], email: value, level: index + 1, userId: emailChanged ? undefined : newApprovers[index]?.userId, name: emailChanged ? undefined : newApprovers[index]?.name, department: emailChanged ? undefined : newApprovers[index]?.department, avatar: emailChanged ? undefined : newApprovers[index]?.avatar }; updateFormData('approvers', newApprovers); if (!value || !value.startsWith('@') || value.length < 2) { clearSearchForIndex(index); return; } searchUsersForIndex(index, value, 10); }; const handleUserSelect = async (index: number, selectedUser: any) => { try { // Check for duplicates in other approver slots (excluding current index) const isDuplicateApprover = formData.approvers?.some( (approver: any, idx: number) => idx !== index && (approver.userId === selectedUser.userId || approver.email?.toLowerCase() === selectedUser.email?.toLowerCase()) ); if (isDuplicateApprover) { onValidationError({ type: 'error', email: selectedUser.email, message: 'This user is already added as an approver in another level.' }); return; } // Check for duplicates in spectators const isDuplicateSpectator = formData.spectators?.some( (spectator: any) => spectator.userId === selectedUser.userId || spectator.email?.toLowerCase() === selectedUser.email?.toLowerCase() ); if (isDuplicateSpectator) { onValidationError({ type: 'error', email: selectedUser.email, message: 'This user is already added as a spectator. A user cannot be both an approver and a spectator.' }); return; } const dbUser = await ensureUserExists({ userId: selectedUser.userId, email: selectedUser.email, displayName: selectedUser.displayName, firstName: selectedUser.firstName, lastName: selectedUser.lastName, department: selectedUser.department, phone: selectedUser.phone, mobilePhone: selectedUser.mobilePhone, designation: selectedUser.designation, jobTitle: selectedUser.jobTitle, manager: selectedUser.manager, employeeId: selectedUser.employeeId, employeeNumber: selectedUser.employeeNumber, secondEmail: selectedUser.secondEmail, location: selectedUser.location }); const updated = [...formData.approvers]; updated[index] = { ...updated[index], email: selectedUser.email, name: selectedUser.displayName || [selectedUser.firstName, selectedUser.lastName].filter(Boolean).join(' '), userId: dbUser.userId, level: index + 1, }; updateFormData('approvers', updated); clearSearchForIndex(index); } catch (err) { console.error('Failed to ensure user exists:', err); onValidationError({ type: 'error', email: selectedUser.email, message: 'Failed to validate user. Please try again.' }); } }; return (

Approval Workflow

Define the approval hierarchy and assign approvers by email ID.

{/* Number of Approvers */} Approval Configuration Configure how many approvers you need and define the approval sequence.
{formData.approverCount || 1}

Maximum {systemPolicy.maxApprovalLevels} approver{systemPolicy.maxApprovalLevels !== 1 ? 's' : ''} allowed. Each approver will review sequentially.

{/* Approval Hierarchy */} Approval Hierarchy * Define the approval sequence. Each approver will review the request in order from Level 1 to Level {formData.approverCount || 1}. {/* Initiator Card */}
Request Initiator YOU

Creates and submits the request

{/* Dynamic Approver Cards */} {Array.from({ length: formData.approverCount || 1 }, (_, index) => { const level = index + 1; const isLast = level === (formData.approverCount || 1); // Ensure approver exists (should be initialized by useEffect, but provide fallback) const approver = formData.approvers[index] || { email: '', name: '', level: level, tat: '' as any }; return (
{level}
Approver Level {level} {isLast && ( FINAL APPROVER )}
{approver.email && approver.userId && ( Verified )}
handleApproverEmailChange(index, e.target.value)} className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full" data-testid={`approval-workflow-approver-${level}-email-input`} /> {/* Search suggestions dropdown */} {(userSearchLoading[index] || (userSearchResults[index]?.length || 0) > 0) && (
{userSearchLoading[index] ? (
Searching...
) : (
    {userSearchResults[index]?.map((u) => (
  • handleUserSelect(index, u)} data-testid={`approval-workflow-approver-${level}-search-result-${u.userId}`} >
    {u.displayName || u.email}
    {u.email}
  • ))}
)}
)}
{ const newApprovers = [...formData.approvers]; newApprovers[index] = { ...newApprovers[index], tat: parseInt(e.target.value) || '', level: level, tatType: approver.tatType || 'hours' }; updateFormData('approvers', newApprovers); }} className="h-10 border-2 border-gray-300 focus:border-blue-500 flex-1" data-testid={`approval-workflow-approver-${level}-tat-input`} />
); })}
{/* TAT Summary Section */}
{/* Approval Flow Summary */}

Approval Flow Summary

Your request will follow this sequence: You (Initiator) → {Array.from({ length: formData.approverCount || 1 }, (_, i) => `Level ${i + 1} Approver`).join(' → ')}. The final approver can close the request.

{/* TAT Summary */}

TAT Summary

{(() => { // Calculate total calendar days (for display) // Days: count as calendar days // Hours: convert to calendar days (hours / 24) const totalCalendarDays = formData.approvers?.reduce((sum: number, a: any) => { const tat = Number(a.tat || 0); const tatType = a.tatType || 'hours'; if (tatType === 'days') { return sum + tat; // Calendar days } else { return sum + (tat / 24); // Convert hours to calendar days } }, 0) || 0; const displayDays = Math.ceil(totalCalendarDays); return ( <>
{displayDays} {displayDays === 1 ? 'Day' : 'Days'}
Total Duration
); })()}
{formData.approvers?.map((approver: any, idx: number) => { const tat = Number(approver.tat || 0); const tatType = approver.tatType || 'hours'; // Convert days to hours: 1 day = 24 hours const hours = tatType === 'days' ? tat * 24 : tat; if (!tat) return null; return (
Level {idx + 1} {hours} {hours === 1 ? 'hour' : 'hours'}
); })}
{(() => { // Convert all TAT to hours first // Days: 1 day = 24 hours // Hours: already in hours const totalHours = formData.approvers?.reduce((sum: number, a: any) => { const tat = Number(a.tat || 0); const tatType = a.tatType || 'hours'; if (tatType === 'days') { // 1 day = 24 hours return sum + (tat * 24); } else { return sum + tat; } }, 0) || 0; // Convert total hours to working days (8 hours per working day) const workingDays = Math.ceil(totalHours / 8); if (totalHours === 0) return null; return (
{totalHours}{totalHours === 1 ? 'h' : 'h'}
Total Hours
{workingDays}
Working Days*

*Based on 8-hour working days

); })()}
); }