/** * ClaimApproverSelectionStep Component * Step 2: Manual approver selection for all 5 steps in dealer claim workflow * Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only, not approval steps * Similar to ApprovalWorkflowStep but fixed to 5 steps with predefined step names */ import { motion } from 'framer-motion'; import { useEffect, useState, useRef } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; 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 { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Users, Shield, CheckCircle, Info, Clock, User, Plus, X, AtSign } from 'lucide-react'; import { useMultiUserSearch } from '@/hooks/useUserSearch'; import { ensureUserExists, searchUsers, type UserSummary } from '@/services/userApi'; import { toast } from 'sonner'; // Fixed 5-step workflow for dealer claims // Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only, not approval steps const CLAIM_STEPS = [ { level: 1, name: 'Dealer Proposal Submission', description: 'Dealer submits proposal documents', defaultTat: 72, isAuto: false, approverType: 'dealer' }, { level: 2, name: 'Requestor Evaluation', description: 'Initiator evaluates dealer proposal', defaultTat: 48, isAuto: false, approverType: 'initiator' }, { level: 3, name: 'Department Lead Approval', description: 'Department lead approves and blocks IO budget', defaultTat: 72, isAuto: false, approverType: 'manual' }, { level: 4, name: 'Dealer Completion Documents', description: 'Dealer submits completion documents', defaultTat: 120, isAuto: false, approverType: 'dealer' }, { level: 5, name: 'Requestor Claim Approval', description: 'Initiator approves completion', defaultTat: 48, isAuto: false, approverType: 'initiator' }, ]; interface ClaimApprover { email: string; name?: string; userId?: string; level: number; tat?: number | string; tatType?: 'hours' | 'days'; isAdditional?: boolean; // Flag to identify additional approvers added between steps insertAfterLevel?: number; // Original level after which this was inserted stepName?: string; // Step name/title for additional approvers originalStepLevel?: number; // Original step level for fixed steps (to track which step this approver belongs to) } interface ClaimApproverSelectionStepProps { formData: { dealerEmail?: string; dealerName?: string; approvers?: ClaimApprover[]; }; updateFormData: (field: string, value: any) => void; onValidationError?: (error: { type: string; email: string; message: string }) => void; currentUserEmail?: string; currentUserId?: string; currentUserName?: string; onValidate?: (isValid: boolean) => void; maxApprovalLevels?: number; onPolicyViolation?: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; } export function ClaimApproverSelectionStep({ formData, updateFormData, onValidationError, currentUserEmail = '', currentUserId = '', currentUserName = '', onValidate, maxApprovalLevels, onPolicyViolation, }: ClaimApproverSelectionStepProps) { const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); // State for add approver modal const [showAddApproverModal, setShowAddApproverModal] = useState(false); const [addApproverEmail, setAddApproverEmail] = useState(''); const [addApproverTat, setAddApproverTat] = useState(24); const [addApproverTatType, setAddApproverTatType] = useState<'hours' | 'days'>('hours'); const [addApproverInsertAfter, setAddApproverInsertAfter] = useState(3); const [addApproverSearchResults, setAddApproverSearchResults] = useState([]); const [isSearchingApprover, setIsSearchingApprover] = useState(false); const [selectedAddApproverUser, setSelectedAddApproverUser] = useState(null); const addApproverSearchTimer = useRef(null); // Validation function to check for missing approvers const validateApprovers = (): { isValid: boolean; missingSteps: string[] } => { const approvers = formData.approvers || []; const missingSteps: string[] = []; CLAIM_STEPS.forEach((step) => { // Skip auto steps (system steps) and pre-filled steps (dealer, initiator) // Step 8 is now a system step, so it should be skipped from validation if (step.isAuto || step.approverType === 'dealer' || step.approverType === 'initiator') { return; } // For manual steps (3 and 8), check if approver is assigned, verified, and has TAT const approver = approvers.find((a: ClaimApprover) => a.level === step.level); if (!approver || !approver.email || !approver.userId || !approver.tat) { missingSteps.push(`${step.name}`); } }); return { isValid: missingSteps.length === 0, missingSteps, }; }; // Expose validation to parent component useEffect(() => { if (onValidate) { const validation = validateApprovers(); onValidate(validation.isValid); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [formData.approvers]); // Initialize approvers array for all 8 steps useEffect(() => { const currentApprovers = formData.approvers || []; // If we already have approvers (including additional ones), don't reinitialize // This prevents creating duplicates when approvers have been shifted if (currentApprovers.length > 0) { // Just ensure all fixed steps have their approvers, but don't recreate shifted ones const newApprovers: ClaimApprover[] = []; const additionalApprovers = currentApprovers.filter((a: ClaimApprover) => a.isAdditional); CLAIM_STEPS.forEach((step) => { // Find existing approver by originalStepLevel (handles shifted levels) const existing = currentApprovers.find((a: ClaimApprover) => a.originalStepLevel === step.level || (!a.originalStepLevel && !a.isAdditional && a.level === step.level) ); if (existing) { // Use existing approver (preserves shifted level) newApprovers.push(existing); } else { // Create new approver only if it doesn't exist if (step.isAuto) { // System steps const systemEmail = step.level === 8 ? 'finance@{{API_DOMAIN}}' : 'system@{{API_DOMAIN}}'; const systemName = step.level === 8 ? 'System/Finance' : 'System'; newApprovers.push({ email: systemEmail, name: systemName, level: step.level, tat: step.defaultTat, tatType: 'hours', originalStepLevel: step.level, }); } else if (step.approverType === 'dealer') { newApprovers.push({ email: formData.dealerEmail || '', name: formData.dealerName || '', level: step.level, tat: step.defaultTat, tatType: 'hours', originalStepLevel: step.level, }); } else if (step.approverType === 'initiator') { newApprovers.push({ email: currentUserEmail || '', name: currentUserName || currentUserEmail || 'User', userId: currentUserId, level: step.level, tat: step.defaultTat, tatType: 'hours', originalStepLevel: step.level, }); } else { newApprovers.push({ email: '', name: '', level: step.level, tat: step.defaultTat, tatType: 'hours', originalStepLevel: step.level, }); } } }); // Add back all additional approvers additionalApprovers.forEach((addApprover: ClaimApprover) => { newApprovers.push(addApprover); }); // Sort by level newApprovers.sort((a, b) => a.level - b.level); // Only update if there are actual changes (to avoid infinite loops) const hasChanges = JSON.stringify(currentApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))) !== JSON.stringify(newApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))); if (hasChanges) { updateFormData('approvers', newApprovers); } } else { // Initial setup - create approvers for all 8 steps const newApprovers: ClaimApprover[] = []; CLAIM_STEPS.forEach((step) => { // System steps (Activity Creation, E-Invoice Generation, Credit Note Confirmation) are no longer approval steps // They are handled as activity logs only, so skip them if (step.isAuto) { // Skip system steps - they are now activity logs only return; } else if (step.approverType === 'dealer') { newApprovers.push({ email: formData.dealerEmail || '', name: formData.dealerName || '', level: step.level, tat: step.defaultTat, tatType: 'hours', originalStepLevel: step.level, }); } else if (step.approverType === 'initiator') { newApprovers.push({ email: currentUserEmail || '', name: currentUserName || currentUserEmail || 'User', userId: currentUserId, level: step.level, tat: step.defaultTat, tatType: 'hours', originalStepLevel: step.level, }); } else { newApprovers.push({ email: '', name: '', level: step.level, tat: step.defaultTat, tatType: 'hours', originalStepLevel: step.level, }); } }); updateFormData('approvers', newApprovers); } }, [formData.dealerEmail, formData.dealerName, currentUserEmail, currentUserId, currentUserName]); const handleApproverEmailChange = (level: number, value: string) => { const approvers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility const index = approvers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); if (index === -1) { // Create new approver entry const step = CLAIM_STEPS.find(s => s.level === level); approvers.push({ email: value, name: '', level: level, tat: step?.defaultTat || 48, tatType: 'hours', originalStepLevel: level, // Track original step }); } else { // Update existing approver const existingApprover = approvers[index]; if (existingApprover) { const previousEmail = existingApprover.email; approvers[index] = { ...existingApprover, email: value, // Clear name and userId if email changed name: value !== previousEmail ? '' : existingApprover.name, userId: value !== previousEmail ? undefined : existingApprover.userId, }; } } updateFormData('approvers', approvers); if (!value || !value.startsWith('@') || value.length < 2) { clearSearchForIndex(level - 1); return; } searchUsersForIndex(level - 1, value, 10); }; const handleUserSelect = async (level: number, selectedUser: any) => { try { // Check if user is trying to select themselves for non-initiator steps const step = CLAIM_STEPS.find(s => s.level === level); if (step && !step.isAuto && step.approverType !== 'initiator' && selectedUser.email?.toLowerCase() === currentUserEmail?.toLowerCase()) { toast.error(`You cannot assign yourself as ${step.name} approver.`); if (onValidationError) { onValidationError({ type: 'self-assign', email: selectedUser.email, message: `You cannot assign yourself as ${step.name} approver.` }); } return; } // Check for duplicates across other steps const approvers = formData.approvers || []; const isDuplicate = approvers.some( (a: ClaimApprover) => a.level !== level && (a.userId === selectedUser.userId || a.email?.toLowerCase() === selectedUser.email?.toLowerCase()) ); if (isDuplicate) { toast.error('This user is already assigned to another step.'); if (onValidationError) { onValidationError({ type: 'error', email: selectedUser.email, message: 'This user is already assigned to another step.' }); } return; } // Ensure user exists in database (create from Okta if needed) 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 }); // Update approver in array const updatedApprovers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility const approverIndex = updatedApprovers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); if (approverIndex === -1) { const step = CLAIM_STEPS.find(s => s.level === level); updatedApprovers.push({ email: selectedUser.email, name: selectedUser.displayName || [selectedUser.firstName, selectedUser.lastName].filter(Boolean).join(' '), userId: dbUser.userId, level: level, tat: step?.defaultTat || 48, tatType: 'hours' as const, originalStepLevel: level, // Track original step }); } else { const existingApprover = updatedApprovers[approverIndex]; if (existingApprover) { updatedApprovers[approverIndex] = { ...existingApprover, email: selectedUser.email, name: selectedUser.displayName || [selectedUser.firstName, selectedUser.lastName].filter(Boolean).join(' '), userId: dbUser.userId, // Preserve originalStepLevel if it exists originalStepLevel: existingApprover.originalStepLevel || level, }; } } updateFormData('approvers', updatedApprovers); clearSearchForIndex(level - 1); toast.success(`Approver for ${CLAIM_STEPS.find(s => s.level === level)?.name} selected successfully.`); } catch (err) { console.error('Failed to ensure user exists:', err); toast.error('Failed to validate user. Please try again.'); if (onValidationError) { onValidationError({ type: 'error', email: selectedUser.email, message: 'Failed to validate user. Please try again.' }); } } }; const handleTatChange = (level: number, tat: number | string) => { const approvers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility const index = approvers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); if (index !== -1) { const existingApprover = approvers[index]; if (existingApprover) { approvers[index] = { ...existingApprover, tat: tat, }; updateFormData('approvers', approvers); } } }; const handleTatTypeChange = (level: number, tatType: 'hours' | 'days') => { const approvers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility const index = approvers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); if (index !== -1) { const existingApprover = approvers[index]; if (existingApprover) { approvers[index] = { ...existingApprover, tatType: tatType, tat: '', // Clear TAT when changing type }; updateFormData('approvers', approvers); } } }; // Handle adding additional approver between steps const handleAddApproverEmailChange = (value: string) => { setAddApproverEmail(value); // Clear selectedUser when manually editing if (selectedAddApproverUser && selectedAddApproverUser.email.toLowerCase() !== value.toLowerCase()) { setSelectedAddApproverUser(null); } // Clear existing timer if (addApproverSearchTimer.current) { clearTimeout(addApproverSearchTimer.current); } // Only trigger search when using @ sign if (!value || !value.startsWith('@') || value.length < 2) { setAddApproverSearchResults([]); setIsSearchingApprover(false); return; } // Start search with debounce setIsSearchingApprover(true); addApproverSearchTimer.current = setTimeout(async () => { try { const term = value.slice(1); // Remove @ prefix const response = await searchUsers(term, 10); const results = response.data?.data || []; setAddApproverSearchResults(results); } catch (error) { console.error('Search failed:', error); setAddApproverSearchResults([]); } finally { setIsSearchingApprover(false); } }, 300); }; const handleSelectAddApproverUser = async (user: UserSummary) => { try { await ensureUserExists({ userId: user.userId, email: user.email, displayName: user.displayName, firstName: user.firstName, lastName: user.lastName, department: user.department, phone: user.phone, mobilePhone: user.mobilePhone, designation: user.designation, jobTitle: user.jobTitle, manager: user.manager, employeeId: user.employeeId, employeeNumber: user.employeeNumber, secondEmail: user.secondEmail, location: user.location }); setAddApproverEmail(user.email); setSelectedAddApproverUser(user); setAddApproverSearchResults([]); setIsSearchingApprover(false); } catch (error) { console.error('Failed to ensure user exists:', error); toast.error('Failed to verify user. Please try again.'); } }; const handleConfirmAddApprover = async () => { const emailToAdd = addApproverEmail.trim().toLowerCase(); if (!emailToAdd) { toast.error('Please enter an email address'); return; } // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(emailToAdd)) { toast.error('Please enter a valid email address'); return; } // Validate TAT const tatNumber = typeof addApproverTat === 'string' ? Number(addApproverTat) : addApproverTat; if (!tatNumber || tatNumber <= 0 || isNaN(tatNumber)) { toast.error('Please enter valid TAT (minimum 1)'); return; } const maxTat = addApproverTatType === 'days' ? 30 : 720; const tatValue = addApproverTatType === 'days' ? tatNumber * 24 : tatNumber; if (tatValue > 720) { toast.error(`TAT cannot exceed ${maxTat} ${addApproverTatType === 'days' ? 'days' : 'hours'}`); return; } // Validate insert after level - don't allow after "Requestor Claim Approval" const requestorClaimApprovalStep = CLAIM_STEPS.find(s => s.name === 'Requestor Claim Approval'); if (requestorClaimApprovalStep && addApproverInsertAfter >= requestorClaimApprovalStep.level) { toast.error('Additional approvers cannot be added after "Requestor Claim Approval" as it is considered final.'); return; } // Check if user is trying to add themselves if (emailToAdd === currentUserEmail?.toLowerCase()) { toast.error('You cannot add yourself as an additional approver.'); return; } // Check for duplicates const approvers = formData.approvers || []; const isDuplicate = approvers.some( (a: ClaimApprover) => (a.userId && selectedAddApproverUser?.userId && a.userId === selectedAddApproverUser.userId) || a.email?.toLowerCase() === emailToAdd ); if (isDuplicate) { toast.error('This user is already assigned as an approver.'); return; } // Find the approver for the selected step by its originalStepLevel // This handles cases where steps have been shifted due to previous additional approvers const approverAfter = approvers.find((a: ClaimApprover) => a.originalStepLevel === addApproverInsertAfter || (!a.originalStepLevel && !a.isAdditional && a.level === addApproverInsertAfter) ); // Get the current level of the approver we're inserting after // If the step has been shifted, use its current level; otherwise use the original level const currentLevelAfter = approverAfter ? approverAfter.level : addApproverInsertAfter; // Calculate insert level based on current shifted level const insertLevel = currentLevelAfter + 1; // Validate max approval levels if (maxApprovalLevels) { // Calculate total levels after adding the new approver // After shifting, we'll have the same number of unique levels + 1 (the new approver) const currentUniqueLevels = new Set(approvers.map((a: ClaimApprover) => a.level)).size; const newTotalLevels = currentUniqueLevels + 1; if (newTotalLevels > maxApprovalLevels) { const violations = [{ type: 'max_approval_levels', message: `Adding this approver would create ${newTotalLevels} approval levels, which exceeds the maximum allowed (${maxApprovalLevels}). Please remove some approvers before adding a new one.`, currentValue: newTotalLevels, maxValue: maxApprovalLevels }]; if (onPolicyViolation) { onPolicyViolation(violations); } else { toast.error(violations[0]?.message || 'Maximum approval levels exceeded'); } return; } } // If user was NOT selected via @ search, validate against Okta if (!selectedAddApproverUser || selectedAddApproverUser.email.toLowerCase() !== emailToAdd) { try { const response = await searchUsers(emailToAdd, 1); const searchOktaResults = response.data?.data || []; if (searchOktaResults.length === 0) { toast.error('User not found in organization directory. Please use @ to search for users.'); return; } const foundUser = searchOktaResults[0]; await ensureUserExists({ userId: foundUser.userId, email: foundUser.email, displayName: foundUser.displayName, firstName: foundUser.firstName, lastName: foundUser.lastName, department: foundUser.department, phone: foundUser.phone, mobilePhone: foundUser.mobilePhone, designation: foundUser.designation, jobTitle: foundUser.jobTitle, manager: foundUser.manager, employeeId: foundUser.employeeId, employeeNumber: foundUser.employeeNumber, secondEmail: foundUser.secondEmail, location: foundUser.location }); // Use found user - insert at integer level and shift subsequent approvers // insertLevel is already calculated above based on current shifted level const newApprover: ClaimApprover = { email: foundUser.email, name: foundUser.displayName || [foundUser.firstName, foundUser.lastName].filter(Boolean).join(' '), userId: foundUser.userId, level: insertLevel, // Use current shifted level + 1 tat: typeof addApproverTat === 'string' ? Number(addApproverTat) : addApproverTat, tatType: addApproverTatType, isAdditional: true, insertAfterLevel: addApproverInsertAfter, // Store original step level for reference stepName: `Additional Approver - ${foundUser.displayName || foundUser.email}`, }; // Shift all approvers with level >= insertLevel up by 1 (both fixed and additional) const updatedApprovers = approvers.map((a: ClaimApprover) => { if (a.level >= insertLevel) { return { ...a, level: a.level + 1 }; } return a; }); // Insert the new approver updatedApprovers.push(newApprover); // Sort by level to maintain order updatedApprovers.sort((a, b) => a.level - b.level); updateFormData('approvers', updatedApprovers); toast.success(`Additional approver added and subsequent steps shifted`); } catch (error) { console.error('Failed to validate approver:', error); toast.error('Failed to validate user. Please try again.'); return; } } else { // User was selected via @ search - insert at integer level and shift subsequent approvers // insertLevel is already calculated above based on current shifted level const newApprover: ClaimApprover = { email: selectedAddApproverUser.email, name: selectedAddApproverUser.displayName || [selectedAddApproverUser.firstName, selectedAddApproverUser.lastName].filter(Boolean).join(' '), userId: selectedAddApproverUser.userId, level: insertLevel, // Use current shifted level + 1 tat: addApproverTat, tatType: addApproverTatType, isAdditional: true, insertAfterLevel: addApproverInsertAfter, // Store original step level for reference stepName: `Additional Approver - ${selectedAddApproverUser.displayName || selectedAddApproverUser.email}`, }; // Shift all approvers with level >= insertLevel up by 1 (both fixed and additional) const updatedApprovers = approvers.map((a: ClaimApprover) => { if (a.level >= insertLevel) { return { ...a, level: a.level + 1 }; } return a; }); // Insert the new approver updatedApprovers.push(newApprover); // Sort by level to maintain order updatedApprovers.sort((a, b) => a.level - b.level); updateFormData('approvers', updatedApprovers); toast.success(`Additional approver added and subsequent steps shifted`); } // Reset modal state setAddApproverEmail(''); setAddApproverTat(24); setAddApproverTatType('hours'); setAddApproverInsertAfter(3); setSelectedAddApproverUser(null); setAddApproverSearchResults([]); setShowAddApproverModal(false); }; const handleRemoveAdditionalApprover = (level: number) => { const approvers = [...(formData.approvers || [])]; const approverToRemove = approvers.find((a: ClaimApprover) => a.level === level); if (!approverToRemove) return; // Remove the additional approver const filtered = approvers.filter((a: ClaimApprover) => a.level !== level); // Shift all approvers with level > removed level down by 1 const updatedApprovers = filtered.map((a: ClaimApprover) => { if (a.level > level && !a.isAdditional) { return { ...a, level: a.level - 1 }; } return a; }); // Sort by level to maintain order updatedApprovers.sort((a, b) => a.level - b.level); updateFormData('approvers', updatedApprovers); toast.success('Additional approver removed and subsequent steps shifted back'); }; // Get all approvers sorted by level (including additional ones) const getAllApproversSorted = () => { const approvers = formData.approvers || []; return [...approvers].sort((a, b) => a.level - b.level); }; const approvers = formData.approvers || []; const sortedApprovers = getAllApproversSorted(); return (

Approver Selection

Assign approvers for workflow steps with TAT (Turn Around Time)

{/* Info Card */} Workflow Steps Information Some steps are pre-filled (Dealer, Initiator, System). You need to assign approvers for "Department Lead Approval" only. Additional approvers cannot be added after "Requestor Claim Approval" as it is considered final. {maxApprovalLevels && ( Max: {maxApprovalLevels} level{maxApprovalLevels !== 1 ? 's' : ''} {(() => { const approvers = formData.approvers || []; const allLevels = new Set(approvers.map((a: ClaimApprover) => a.level)); const currentCount = allLevels.size; return currentCount > 0 ? ( ({currentCount}/{maxApprovalLevels}) ) : null; })()} )} {/* Approval Hierarchy */} Approval Hierarchy Define approvers and TAT for each step. Some steps are pre-filled (Dealer, Initiator, System). Only "Department Lead Approval" requires manual assignment. {/* Add Additional Approver Button */}
{maxApprovalLevels && (

Max: {maxApprovalLevels} level{maxApprovalLevels !== 1 ? 's' : ''} {(() => { const approvers = formData.approvers || []; const allLevels = new Set(approvers.map((a: ClaimApprover) => a.level)); const currentCount = allLevels.size; return currentCount > 0 ? ( ({currentCount}/{maxApprovalLevels}) ) : null; })()}

)}
{/* Initiator Card */}
Request Initiator YOU

Creates and submits the request

{/* Dynamic Approver Cards - Filter out system steps (auto-processed) */} {(() => { // Count additional approvers before first step const additionalBeforeFirst = sortedApprovers.filter((a: ClaimApprover) => a.isAdditional && a.insertAfterLevel === 0 ); let displayIndex = additionalBeforeFirst.length; // Start index after additional approvers before first step return CLAIM_STEPS.filter((step) => !step.isAuto).map((step, index, filteredSteps) => { // Find approver by originalStepLevel first, then fallback to level const approver = approvers.find((a: ClaimApprover) => a.originalStepLevel === step.level || (!a.originalStepLevel && a.level === step.level && !a.isAdditional) ) || { email: '', name: '', level: step.level, tat: step.defaultTat, tatType: 'hours' as const, originalStepLevel: step.level, }; const isLast = index === filteredSteps.length - 1; const isPreFilled = step.isAuto || step.approverType === 'dealer' || step.approverType === 'initiator'; const isEditable = !step.isAuto; // Find additional approvers that should be shown after this step // Additional approvers inserted after this step will have insertAfterLevel === step.level // and their level will be step.level + 1 (or higher if multiple are added) const additionalApproversAfter = sortedApprovers.filter( (a: ClaimApprover) => a.isAdditional && a.insertAfterLevel === step.level ).sort((a, b) => a.level - b.level); // Calculate current step's display number const currentStepDisplayNumber = displayIndex + 1; // Increment display index for this step displayIndex++; // Increment display index for each additional approver after this step displayIndex += additionalApproversAfter.length; return (
{/* Render additional approvers before this step if any */} {index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => { const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers return (
{addDisplayNumber}
Additional Approver ADDITIONAL

{addApprover.name || addApprover.email}

Email: {addApprover.email}
TAT: {addApprover.tat} {addApprover.tatType}
); })}
{currentStepDisplayNumber}
{step.name} {isLast && ( FINAL )} {isPreFilled && ( PRE-FILLED )}

{step.description}

{isEditable && (() => { const isVerified = !!(approver.email && approver.userId); const isEmpty = !approver.email && !isPreFilled; return (
{isVerified && ( Verified )}
{ const newValue = e.target.value; if (!isPreFilled) { handleApproverEmailChange(step.level, newValue); } }} disabled={isPreFilled || step.isAuto} className={`h-9 border-2 transition-all mt-1 w-full text-sm ${isPreFilled ? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed font-medium' : isVerified ? 'bg-green-50/50 border-green-600 focus:border-green-700 ring-offset-green-50 focus:ring-1 focus:ring-green-100 font-semibold text-gray-900' : 'bg-white border-blue-300 shadow-sm shadow-blue-100/50 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900' }`} /> {/* Search suggestions dropdown */} {!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && (
{userSearchLoading[step.level - 1] ? (
Searching...
) : (
    {userSearchResults[step.level - 1]?.map((u) => (
  • handleUserSelect(step.level, u)} >
    {u.displayName || u.email}
    {u.email}
    {u.department && (
    {u.department}
    )}
  • ))}
)}
)}
{approver.name && (

Selected: {approver.name}

)}
handleTatChange(step.level, parseInt(e.target.value) || '')} disabled={step.isAuto} className={`h-9 border-2 transition-all flex-1 text-sm ${isPreFilled ? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed font-medium' : isVerified ? 'bg-green-50/50 border-green-600 focus:border-green-700 focus:ring-1 focus:ring-green-100 font-semibold text-gray-900' : 'bg-white border-blue-300 shadow-sm shadow-blue-100/50 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900' }`} />
); })()}
{/* Render additional approvers after this step */} {additionalApproversAfter.map((addApprover: ClaimApprover, addIndex: number) => { // Additional approvers come after the current step, so they should be numbered after it const addDisplayNumber = currentStepDisplayNumber + addIndex + 1; return (
{addDisplayNumber}
{addApprover.stepName || 'Additional Approver'} ADDITIONAL {addApprover.email && addApprover.userId && ( Verified )}

{addApprover.name || addApprover.email || 'No approver assigned'}

{addApprover.email && (
Email: {addApprover.email}
{addApprover.tat && (
TAT: {addApprover.tat} {addApprover.tatType}
)}
)}
); })}
); }); })()}
{/* TAT Summary */} TAT Summary
{sortedApprovers.map((approver: ClaimApprover) => { // Skip system/auto steps // Find step by originalStepLevel first, then fallback to level const step = approver.originalStepLevel ? CLAIM_STEPS.find(s => s.level === approver.originalStepLevel) : CLAIM_STEPS.find(s => s.level === approver.level && !approver.isAdditional); if (step?.isAuto) return null; const tat = Number(approver.tat || 0); const tatType = approver.tatType || 'hours'; const hours = tatType === 'days' ? tat * 24 : tat; if (!tat) return null; // Handle additional approvers if (approver.isAdditional) { const afterStep = CLAIM_STEPS.find(s => s.level === approver.insertAfterLevel); return (
{approver.stepName || `Additional Approver (after "${afterStep?.name || 'Unknown'}")`} {hours} hours
); } return (
{step?.name || 'Unknown'} {hours} hours
); })}
{/* Add Additional Approver Modal */} Add Additional Approver Add an additional approver between workflow steps. The approver will be inserted after the selected step. Additional approvers cannot be added after "Requestor Claim Approval".
{/* Insert After Level Selection */}

The new approver will be inserted after the selected step.

⚠️ Additional approvers cannot be added after "Requestor Claim Approval" as it is considered final.

{/* Max Approval Levels Note */} {maxApprovalLevels && (

Max: {maxApprovalLevels} level{maxApprovalLevels !== 1 ? 's' : ''} {(() => { const approvers = formData.approvers || []; const allLevels = new Set(approvers.map((a: ClaimApprover) => a.level)); const currentCount = allLevels.size; return currentCount > 0 ? ( ({currentCount}/{maxApprovalLevels}) ) : null; })()}

)}
{/* TAT Input */}
{ const value = e.target.value; // Allow empty input while typing if (value === '') { setAddApproverTat(''); } else { const numValue = Number(value); if (!isNaN(numValue) && numValue >= 0) { setAddApproverTat(numValue); } } }} onBlur={(e) => { // If empty on blur, reset to default const value = e.target.value; if (!value || value === '' || Number(value) <= 0) { setAddApproverTat(24); } }} className="h-11 border-gray-300 flex-1" placeholder="24" />

Maximum time for this approver to respond (1-{addApproverTatType === 'days' ? '30 days' : '720 hours'})

{/* Email Input with @ Search */}
handleAddApproverEmailChange(e.target.value)} className="pl-10 h-11 border-gray-300" autoFocus /> {/* Search Results Dropdown */} {(isSearchingApprover || addApproverSearchResults.length > 0) && (
{isSearchingApprover ? (
Searching users...
) : addApproverSearchResults.length > 0 ? (
    {addApproverSearchResults.map((user) => (
  • handleSelectAddApproverUser(user)} >

    {user.displayName || [user.firstName, user.lastName].filter(Boolean).join(' ') || user.email}

    {user.email}

    {user.designation && (

    {user.designation}

    )}
  • ))}
) : null}
)}

Type @username to search for users, or enter email directly.

); }