Re_Figma_Code/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx

676 lines
31 KiB
TypeScript

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<void>; // 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 <AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600" />;
switch (status) {
case 'approved':
return <CheckCircle className="w-4 h-4 sm:w-5 sm:w-5 text-green-600" />;
case 'rejected':
return <XCircle className="w-4 h-4 sm:w-5 sm:h-5 text-red-600" />;
case 'pending':
case 'in-review':
return <Clock className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />;
case 'waiting':
return <Clock className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400" />;
default:
return <Clock className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400" />;
}
};
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 (
<div
className={`relative p-3 sm:p-4 md:p-5 rounded-lg border-2 transition-all ${
step.isSkipped
? 'border-orange-500 bg-orange-50'
: isActive
? 'border-blue-500 bg-blue-50 shadow-md'
: isCompleted
? 'border-green-500 bg-green-50'
: isRejected
? 'border-red-500 bg-red-50'
: isWaiting
? 'border-gray-300 bg-gray-50'
: 'border-gray-200 bg-white'
}`}
data-testid={`${testId}-${step.step}`}
>
<div className="flex items-start gap-2 sm:gap-3 md:gap-4">
<div className={`p-2 sm:p-2.5 md:p-3 rounded-xl flex-shrink-0 ${
step.isSkipped ? 'bg-orange-100' :
isActive ? 'bg-blue-100' :
isCompleted ? 'bg-green-100' :
isRejected ? 'bg-red-100' :
isWaiting ? 'bg-gray-200' :
'bg-gray-100'
}`}>
{getStepIcon(step.status, step.isSkipped)}
</div>
<div className="flex-1 min-w-0">
{/* Header with Approver Label and Status */}
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 sm:gap-4 mb-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2 mb-2">
<h4 className="font-semibold text-gray-900 text-base sm:text-lg" data-testid={`${testId}-approver-label`}>
Approver {index + 1}
</h4>
<Badge variant="outline" className={`text-xs shrink-0 capitalize ${
step.isSkipped ? 'bg-orange-100 text-orange-800 border-orange-200' :
isActive ? 'bg-yellow-100 text-yellow-800 border-yellow-200' :
isCompleted ? 'bg-green-100 text-green-800 border-green-200' :
isRejected ? 'bg-red-100 text-red-800 border-red-200' :
isWaiting ? 'bg-gray-200 text-gray-600 border-gray-300' :
'bg-gray-100 text-gray-800 border-gray-200'
}`} data-testid={`${testId}-status-badge`}>
{step.isSkipped ? 'skipped' : step.status}
</Badge>
{step.isSkipped && step.skipReason && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity"
onClick={(e) => e.stopPropagation()}
>
<AlertCircle className="w-4 h-4 text-orange-600" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs bg-orange-50 border-orange-200">
<p className="text-xs font-semibold text-orange-900 mb-1 flex items-center gap-1">
<FastForward className="w-3 h-3" />
Skip Reason:
</p>
<p className="text-xs text-gray-700">{step.skipReason}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isCompleted && actualHours && (
<Badge className="bg-green-600 text-white text-xs" data-testid={`${testId}-completion-time`}>
{formatWorkingHours(actualHours)}
</Badge>
)}
</div>
<p className="text-sm font-semibold text-gray-900" data-testid={`${testId}-approver-name`}>
{isCurrentUser ? <span className="text-blue-600">You</span> : step.approver}
</p>
<p className="text-xs text-gray-600" data-testid={`${testId}-role`}>{step.role}</p>
</div>
<div className="text-left sm:text-right flex-shrink-0">
<p className="text-xs text-gray-500 font-medium">Turnaround Time (TAT)</p>
<p className="text-lg font-bold text-gray-900" data-testid={`${testId}-tat-hours`}>{tatHours} hours</p>
</div>
</div>
{/* Completed Approver - Show Completion Details */}
{isCompleted && actualHours !== undefined && (
<div className="space-y-3">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600">Completed:</span>
<span className="font-medium text-gray-900">{step.timestamp ? formatDateTime(step.timestamp) : 'N/A'}</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600">Completed in:</span>
<span className="font-medium text-gray-900">{formatWorkingHours(actualHours)}</span>
</div>
{/* Progress Bar for Completed - Shows actual time used vs TAT allocated */}
<div className="space-y-2">
{(() => {
// 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 (
<>
<Progress
value={displayPercentage}
className={`h-2 bg-gray-200 ${isBreached ? '[&>div]:bg-red-600' : '[&>div]:bg-green-600'}`}
data-testid={`${testId}-progress-bar`}
/>
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<span className={`font-semibold ${isBreached ? 'text-red-600' : 'text-green-600'}`}>
{Math.round(displayPercentage)}% of TAT used
</span>
{isBreached && canEditBreachReason && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => {
setBreachReason(existingBreachReason);
setShowBreachReasonModal(true);
}}
>
<FileEdit className="w-3 h-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{existingBreachReason ? 'Edit breach reason' : 'Add breach reason'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{savedHours > 0 && (
<span className="text-green-600 font-semibold">Saved {formatHoursMinutes(savedHours)}</span>
)}
</div>
</>
);
})()}
</div>
{/* Breach Reason Display for Completed Approver */}
{isBreached && existingBreachReason && (
<div className="mt-4 p-3 sm:p-4 bg-red-50 border-l-4 border-red-500 rounded-r-lg">
<p className="text-xs font-semibold text-red-700 mb-2 flex items-center gap-1.5">
<FileEdit className="w-3.5 h-3.5" />
Breach Reason:
</p>
<p className="text-sm text-gray-700 leading-relaxed whitespace-pre-line">{existingBreachReason}</p>
</div>
)}
{/* Conclusion Remark */}
{step.comment && (
<div className="mt-4 p-3 sm:p-4 bg-blue-50 border-l-4 border-blue-500 rounded-r-lg">
<p className="text-xs font-semibold text-gray-700 mb-2 flex items-center gap-1.5">
<MessageSquare className="w-3.5 h-3.5 text-blue-600" />
Conclusion Remark:
</p>
<p className="text-sm text-gray-700 italic leading-relaxed">{step.comment}</p>
</div>
)}
</div>
)}
{/* Active Approver - Show Real-time Progress from Backend */}
{isActive && approval?.sla && (
<div className="space-y-3">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600">Due by:</span>
<span className="font-medium text-gray-900">
{approval.sla.deadline ? formatDateTime(approval.sla.deadline) : 'Not set'}
</span>
</div>
{/* Current Approver - Time Tracking */}
<div className={`border rounded-lg p-3 ${
approval.sla.status === 'critical' ? 'bg-orange-50 border-orange-200' :
approval.sla.status === 'breached' ? 'bg-red-50 border-red-200' :
'bg-yellow-50 border-yellow-200'
}`}>
<p className="text-xs font-semibold text-gray-900 mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
Current Approver - Time Tracking
</p>
<div className="space-y-2 text-xs mb-3">
<div className="flex justify-between">
<span className="text-gray-600">Time elapsed since assigned:</span>
<span className="font-medium text-gray-900">{approval.sla.elapsedText}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Time used:</span>
<span className="font-medium text-gray-900">{approval.sla.elapsedText} / {formatHoursMinutes(tatHours)} allocated</span>
</div>
</div>
{/* Progress Bar */}
<div className="space-y-2">
<Progress
value={approval.sla.percentageUsed}
className={`h-3 ${
approval.sla.status === 'breached' ? '[&>div]:bg-red-600' :
approval.sla.status === 'critical' ? '[&>div]:bg-orange-600' :
'[&>div]:bg-yellow-600'
}`}
data-testid={`${testId}-sla-progress`}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={`text-xs font-semibold ${
approval.sla.status === 'breached' ? 'text-red-600' :
approval.sla.status === 'critical' ? 'text-orange-600' :
'text-yellow-700'
}`}>
Progress: {Math.min(100, approval.sla.percentageUsed)}% of TAT used
</span>
{approval.sla.status === 'breached' && canEditBreachReason && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => {
setBreachReason(existingBreachReason);
setShowBreachReasonModal(true);
}}
>
<FileEdit className="w-3 h-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{existingBreachReason ? 'Edit breach reason' : 'Add breach reason'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<span className="text-xs font-medium text-gray-700">
{approval.sla.remainingText} remaining
</span>
</div>
{approval.sla.status === 'breached' && (
<>
<p className="text-xs font-semibold text-center text-red-600 flex items-center justify-center gap-1.5">
<AlertOctagon className="w-4 h-4" />
Deadline Breached
</p>
{existingBreachReason && (
<div className="mt-3 p-3 bg-red-50 border-l-4 border-red-500 rounded-r-lg">
<p className="text-xs font-semibold text-red-700 mb-2 flex items-center gap-1.5">
<FileEdit className="w-3.5 h-3.5" />
Breach Reason:
</p>
<p className="text-sm text-gray-700 leading-relaxed whitespace-pre-line">{existingBreachReason}</p>
</div>
)}
</>
)}
{approval.sla.status === 'critical' && (
<p className="text-xs font-semibold text-center text-orange-600 flex items-center justify-center gap-1.5">
<AlertTriangle className="w-4 h-4" />
Approaching Deadline
</p>
)}
</div>
</div>
</div>
)}
{/* Waiting Approver - Show Assignment Info */}
{isWaiting && (
<div className="space-y-2">
<div className="bg-gray-100 border border-gray-300 rounded-lg p-3">
<p className="text-xs text-gray-600 mb-1 flex items-center gap-1.5">
<PauseCircle className="w-3.5 h-3.5 text-gray-500" />
Awaiting Previous Approval
</p>
<p className="text-sm font-medium text-gray-700">Will be assigned after previous step</p>
<p className="text-xs text-gray-500 mt-2">Allocated {tatHours} hours for approval</p>
</div>
</div>
)}
{/* Rejected Status */}
{isRejected && step.comment && (
<div className="mt-3 p-3 sm:p-4 bg-red-50 border-l-4 border-red-500 rounded-r-lg">
<p className="text-xs font-semibold text-red-700 mb-2 flex items-center gap-1.5">
<XCircle className="w-3.5 h-3.5" />
Rejection Reason:
</p>
<p className="text-sm text-gray-700 leading-relaxed">{step.comment}</p>
</div>
)}
{/* Skipped Status */}
{step.isSkipped && step.skipReason && (
<div className="mt-3 p-3 sm:p-4 bg-orange-50 border-l-4 border-orange-500 rounded-r-lg">
<p className="text-xs font-semibold text-orange-700 mb-2 flex items-center gap-1.5">
<FastForward className="w-3.5 h-3.5" />
Skip Reason:
</p>
<p className="text-sm text-gray-700 leading-relaxed">{step.skipReason}</p>
{step.timestamp && (
<p className="text-xs text-gray-500 mt-2">Skipped on {formatDateTime(step.timestamp)}</p>
)}
</div>
)}
{/* TAT Alerts/Reminders */}
{step.tatAlerts && step.tatAlerts.length > 0 && (
<div className="mt-2 sm:mt-3 space-y-2">
{step.tatAlerts.map((alert: any, alertIndex: number) => (
<div
key={alertIndex}
className={`p-2 sm:p-3 rounded-lg border ${
alert.isBreached
? 'bg-red-50 border-red-200'
: (alert.thresholdPercentage || 0) === 75
? 'bg-orange-50 border-orange-200'
: 'bg-yellow-50 border-yellow-200'
}`}
data-testid={`${testId}-tat-alert-${alertIndex}`}
>
<div className="flex items-start gap-2">
<div className="flex-shrink-0 mt-0.5">
{(alert.thresholdPercentage || 0) === 50 && (
<Hourglass className="w-5 h-5 text-yellow-600" />
)}
{(alert.thresholdPercentage || 0) === 75 && (
<AlertTriangle className="w-5 h-5 text-orange-600" />
)}
{(alert.thresholdPercentage || 0) === 100 && (
<AlertOctagon className="w-5 h-5 text-red-600" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2 mb-1">
<p className="text-xs sm:text-sm font-semibold text-gray-900">
Reminder {alertIndex + 1} - {alert.thresholdPercentage || 0}% TAT
</p>
<Badge
variant="outline"
className={`text-[10px] sm:text-xs shrink-0 ${
alert.isBreached
? 'bg-red-100 text-red-800 border-red-300'
: 'bg-amber-100 text-amber-800 border-amber-300'
}`}
>
{alert.isBreached ? 'BREACHED' : 'WARNING'}
</Badge>
</div>
<p className="text-[10px] sm:text-xs md:text-sm text-gray-700 mt-1">
{alert.thresholdPercentage || 0}% of SLA breach reminder have been sent
</p>
{/* Time Tracking Details */}
<div className="mt-2 grid grid-cols-2 gap-1.5 sm:gap-2 text-[10px] sm:text-xs">
<div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Allocated:</span>
<span className="ml-1 font-medium text-gray-900">
{Number(alert.tatHoursAllocated || 0).toFixed(2)}h
</span>
</div>
<div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Elapsed:</span>
<span className="ml-1 font-medium text-gray-900">
{Number(alert.tatHoursElapsed || 0).toFixed(2)}h
{alert.metadata?.tatTestMode && (
<span className="text-purple-600 ml-1">
({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m)
</span>
)}
</span>
</div>
<div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Remaining:</span>
<span className={`ml-1 font-medium ${
(alert.tatHoursRemaining || 0) < 2 ? 'text-red-600' : 'text-gray-900'
}`}>
{Number(alert.tatHoursRemaining || 0).toFixed(2)}h
{alert.metadata?.tatTestMode && (
<span className="text-purple-600 ml-1">
({(Number(alert.tatHoursRemaining || 0) * 60).toFixed(0)}m)
</span>
)}
</span>
</div>
<div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Due by:</span>
<span className="ml-1 font-medium text-gray-900">
{alert.expectedCompletionTime ? formatDateShort(alert.expectedCompletionTime) : 'N/A'}
</span>
</div>
</div>
<div className="mt-2 pt-2 border-t border-gray-200">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
<p className="text-[10px] sm:text-xs text-gray-500">
Reminder sent by system automatically
</p>
{(alert.metadata?.testMode || alert.metadata?.tatTestMode) && (
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-300 text-[10px] px-1.5 py-0 shrink-0">
TEST MODE
</Badge>
)}
</div>
<p className="text-[10px] sm:text-xs text-gray-600 font-medium mt-0.5">
Sent at: {alert.alertSentAt ? formatDateTime(alert.alertSentAt) : 'N/A'}
</p>
{(alert.metadata?.testMode || alert.metadata?.tatTestMode) && (
<p className="text-[10px] text-purple-600 mt-1 italic">
Note: Test mode active (1 hour = 1 minute)
</p>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
{step.timestamp && (
<p className="text-xs text-gray-500 mt-2">
{isCompleted ? 'Approved' : isRejected ? 'Rejected' : 'Actioned'} on {formatDateTime(step.timestamp)}
</p>
)}
{/* Skip Approver Button - Only show for initiator on pending/in-review levels */}
{isInitiator && (isActive || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && onSkipApprover && (
<div className="mt-2 sm:mt-3 pt-2 sm:pt-3 border-t border-gray-200">
<Button
variant="outline"
size="sm"
className="w-full border-orange-300 text-orange-700 hover:bg-orange-50 h-9 sm:h-10 text-xs sm:text-sm"
onClick={() => onSkipApprover({
levelId: step.levelId,
approverName: step.approver,
levelNumber: step.step
})}
data-testid={`${testId}-skip-button`}
>
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" />
Skip This Approver
</Button>
<p className="text-[10px] sm:text-xs text-gray-500 mt-1 text-center">
Skip if approver is unavailable and move to next level
</p>
</div>
)}
</div>
</div>
{/* Breach Reason Modal */}
<Dialog open={showBreachReasonModal} onOpenChange={setShowBreachReasonModal}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{existingBreachReason ? 'Edit Breach Reason' : 'Add Breach Reason'}</DialogTitle>
<DialogDescription>
{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.'}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Textarea
placeholder="Enter the reason for the breach..."
value={breachReason}
onChange={(e) => setBreachReason(e.target.value)}
className="min-h-[100px]"
maxLength={500}
/>
<p className="text-xs text-gray-500 mt-2">
{breachReason.length}/500 characters
</p>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setShowBreachReasonModal(false);
setBreachReason('');
}}
disabled={savingReason}
>
Cancel
</Button>
<Button
onClick={handleSaveBreachReason}
disabled={!breachReason.trim() || savingReason}
className="bg-red-600 hover:bg-red-700"
>
{savingReason ? 'Saving...' : 'Save Reason'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}