import { useState, useEffect } from 'react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; import { CheckCircle, XCircle, Clock, AlertCircle, AlertTriangle, FastForward, MessageSquare, PauseCircle, Hourglass, AlertOctagon, FileEdit } from 'lucide-react'; import { formatDateTime, formatDateShort } from '@/utils/dateFormatter'; import { formatHoursMinutes } from '@/utils/slaTracker'; import { useAuth, hasManagementAccess } from '@/contexts/AuthContext'; import { updateBreachReason as updateBreachReasonApi } from '@/services/workflowApi'; import { toast } from 'sonner'; export interface ApprovalStep { step: number; levelId: string; role: string; status: string; approver: string; approverId?: string; approverEmail?: string; tatHours: number; elapsedHours?: number; remainingHours?: number; tatPercentageUsed?: number; actualHours?: number; comment?: string; timestamp?: string; levelStartTime?: string; tatAlerts?: any[]; skipReason?: string; isSkipped?: boolean; } interface ApprovalStepCardProps { step: ApprovalStep; index: number; approval?: any; // Raw approval data from backend isCurrentUser?: boolean; isInitiator?: boolean; onSkipApprover?: (data: { levelId: string; approverName: string; levelNumber: number }) => void; onRefresh?: () => void | Promise; // Optional callback to refresh data testId?: string; } // Helper function to format working hours as days (8 hours = 1 working day) const formatWorkingHours = (hours: number): string => { const WORKING_HOURS_PER_DAY = 8; if (hours < WORKING_HOURS_PER_DAY) { return formatHoursMinutes(hours); } const days = Math.floor(hours / WORKING_HOURS_PER_DAY); const remainingHours = hours % WORKING_HOURS_PER_DAY; if (remainingHours > 0) { return `${days}d ${formatHoursMinutes(remainingHours)}`; } return `${days}d`; }; const getStepIcon = (status: string, isSkipped?: boolean) => { if (isSkipped) return ; switch (status) { case 'approved': return ; case 'rejected': return ; case 'pending': case 'in-review': return ; case 'waiting': return ; default: return ; } }; export function ApprovalStepCard({ step, index, approval, isCurrentUser = false, isInitiator = false, onSkipApprover, onRefresh, testId = 'approval-step' }: ApprovalStepCardProps) { const { user } = useAuth(); const [showBreachReasonModal, setShowBreachReasonModal] = useState(false); const [breachReason, setBreachReason] = useState(''); const [savingReason, setSavingReason] = useState(false); // Get existing breach reason from approval or step data const existingBreachReason = (approval as any)?.breachReason || (step as any)?.breachReason || ''; // Reset modal state when it closes useEffect(() => { if (!showBreachReasonModal) { setBreachReason(''); } }, [showBreachReasonModal]); const isActive = step.status === 'pending' || step.status === 'in-review'; const isCompleted = step.status === 'approved'; const isRejected = step.status === 'rejected'; const isWaiting = step.status === 'waiting'; const tatHours = Number(step.tatHours || 0); const actualHours = step.actualHours; const savedHours = isCompleted && actualHours ? Math.max(0, tatHours - actualHours) : 0; // Calculate if breached const progressPercentage = tatHours > 0 ? (actualHours / tatHours) * 100 : 0; const isBreached = progressPercentage >= 100; // Check permissions: ADMIN, MANAGEMENT, or the approver const isAdmin = user?.role === 'ADMIN'; const isManagement = hasManagementAccess(user); const isApprover = step.approverId === user?.userId; const canEditBreachReason = isAdmin || isManagement || isApprover; const handleSaveBreachReason = async () => { if (!breachReason.trim()) { toast.error('Breach Reason Required', { description: 'Please enter a reason for the breach.', }); return; } setSavingReason(true); try { await updateBreachReasonApi(step.levelId, breachReason.trim()); setShowBreachReasonModal(false); setBreachReason(''); toast.success('Breach Reason Updated', { description: 'The breach reason has been saved and will appear in the TAT Breach Report.', duration: 5000, }); // Refresh data if callback provided, otherwise reload page if (onRefresh) { await onRefresh(); } else { // Fallback to page reload if no refresh callback setTimeout(() => { window.location.reload(); }, 1000); } } catch (error: any) { console.error('Error updating breach reason:', error); const errorMessage = error?.response?.data?.error || error?.message || 'Failed to update breach reason. Please try again.'; toast.error('Failed to Update Breach Reason', { description: errorMessage, duration: 5000, }); } finally { setSavingReason(false); } }; return (
{getStepIcon(step.status, step.isSkipped)}
{/* Header with Approver Label and Status */}

Approver {index + 1}

{step.isSkipped ? 'skipped' : step.status} {step.isSkipped && step.skipReason && (

Skip Reason:

{step.skipReason}

)} {isCompleted && actualHours && ( {formatWorkingHours(actualHours)} )}

{isCurrentUser ? You : step.approver}

{step.role}

Turnaround Time (TAT)

{tatHours} hours

{/* Completed Approver - Show Completion Details */} {isCompleted && actualHours !== undefined && (
Completed: {step.timestamp ? formatDateTime(step.timestamp) : 'N/A'}
Completed in: {formatWorkingHours(actualHours)}
{/* Progress Bar for Completed - Shows actual time used vs TAT allocated */}
{(() => { // Calculate actual progress percentage based on time used // If approved in 1 minute out of 24 hours TAT = (1/60) / 24 * 100 = 0.069% const displayPercentage = Math.min(100, progressPercentage); return ( <> div]:bg-red-600' : '[&>div]:bg-green-600'}`} data-testid={`${testId}-progress-bar`} />
{Math.round(displayPercentage)}% of TAT used {isBreached && canEditBreachReason && (

{existingBreachReason ? 'Edit breach reason' : 'Add breach reason'}

)}
{savedHours > 0 && ( Saved {formatHoursMinutes(savedHours)} )}
); })()}
{/* Breach Reason Display for Completed Approver */} {isBreached && existingBreachReason && (

Breach Reason:

{existingBreachReason}

)} {/* Conclusion Remark */} {step.comment && (

Conclusion Remark:

{step.comment}

)}
)} {/* Active Approver - Show Real-time Progress from Backend */} {isActive && approval?.sla && (
Due by: {approval.sla.deadline ? formatDateTime(approval.sla.deadline) : 'Not set'}
{/* Current Approver - Time Tracking */}

Current Approver - Time Tracking

Time elapsed since assigned: {approval.sla.elapsedText}
Time used: {approval.sla.elapsedText} / {formatHoursMinutes(tatHours)} allocated
{/* Progress Bar */}
div]:bg-red-600' : approval.sla.status === 'critical' ? '[&>div]:bg-orange-600' : '[&>div]:bg-yellow-600' }`} data-testid={`${testId}-sla-progress`} />
Progress: {Math.min(100, approval.sla.percentageUsed)}% of TAT used {approval.sla.status === 'breached' && canEditBreachReason && (

{existingBreachReason ? 'Edit breach reason' : 'Add breach reason'}

)}
{approval.sla.remainingText} remaining
{approval.sla.status === 'breached' && ( <>

Deadline Breached

{existingBreachReason && (

Breach Reason:

{existingBreachReason}

)} )} {approval.sla.status === 'critical' && (

Approaching Deadline

)}
)} {/* Waiting Approver - Show Assignment Info */} {isWaiting && (

Awaiting Previous Approval

Will be assigned after previous step

Allocated {tatHours} hours for approval

)} {/* Rejected Status */} {isRejected && step.comment && (

Rejection Reason:

{step.comment}

)} {/* Skipped Status */} {step.isSkipped && step.skipReason && (

Skip Reason:

{step.skipReason}

{step.timestamp && (

Skipped on {formatDateTime(step.timestamp)}

)}
)} {/* TAT Alerts/Reminders */} {step.tatAlerts && step.tatAlerts.length > 0 && (
{step.tatAlerts.map((alert: any, alertIndex: number) => (
{(alert.thresholdPercentage || 0) === 50 && ( )} {(alert.thresholdPercentage || 0) === 75 && ( )} {(alert.thresholdPercentage || 0) === 100 && ( )}

Reminder {alertIndex + 1} - {alert.thresholdPercentage || 0}% TAT

{alert.isBreached ? 'BREACHED' : 'WARNING'}

{alert.thresholdPercentage || 0}% of SLA breach reminder have been sent

{/* Time Tracking Details */}
Allocated: {Number(alert.tatHoursAllocated || 0).toFixed(2)}h
Elapsed: {Number(alert.tatHoursElapsed || 0).toFixed(2)}h {alert.metadata?.tatTestMode && ( ({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m) )}
Remaining: {Number(alert.tatHoursRemaining || 0).toFixed(2)}h {alert.metadata?.tatTestMode && ( ({(Number(alert.tatHoursRemaining || 0) * 60).toFixed(0)}m) )}
Due by: {alert.expectedCompletionTime ? formatDateShort(alert.expectedCompletionTime) : 'N/A'}

Reminder sent by system automatically

{(alert.metadata?.testMode || alert.metadata?.tatTestMode) && ( TEST MODE )}

Sent at: {alert.alertSentAt ? formatDateTime(alert.alertSentAt) : 'N/A'}

{(alert.metadata?.testMode || alert.metadata?.tatTestMode) && (

Note: Test mode active (1 hour = 1 minute)

)}
))}
)} {step.timestamp && (

{isCompleted ? 'Approved' : isRejected ? 'Rejected' : 'Actioned'} on {formatDateTime(step.timestamp)}

)} {/* Skip Approver Button - Only show for initiator on pending/in-review levels */} {isInitiator && (isActive || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && onSkipApprover && (

Skip if approver is unavailable and move to next level

)}
{/* Breach Reason Modal */} {existingBreachReason ? 'Edit Breach Reason' : 'Add Breach Reason'} {existingBreachReason ? 'Update the reason for the TAT breach. This will be reflected in the TAT Breach Report.' : 'Please provide a reason for the TAT breach. This will be reflected in the TAT Breach Report.'}