resume pause and sla progress bar in detail screen modified

This commit is contained in:
laxmanhalaki 2025-11-29 16:17:14 +05:30
parent 91e028fb18
commit 8c2aa60195
3 changed files with 214 additions and 51 deletions

View File

@ -5,7 +5,7 @@ import { formatHoursMinutes } from '@/utils/slaTracker';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
export interface SLAData { export interface SLAData {
status: 'normal' | 'approaching' | 'critical' | 'breached'; status: 'on_track' | 'normal' | 'approaching' | 'critical' | 'breached';
percentageUsed: number; percentageUsed: number;
elapsedText: string; elapsedText: string;
elapsedHours: number; elapsedHours: number;
@ -42,6 +42,47 @@ export function SLAProgressBar({
); );
} }
// Use percentage-based colors to match approver SLA tracker
// Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
const percentageUsed = sla.percentageUsed || 0;
const rawStatus = sla.status || 'on_track';
// Determine colors based on percentage (matching ApprovalStepCard logic)
const getStatusColors = () => {
if (percentageUsed >= 100) {
return {
badge: 'bg-red-600 text-white animate-pulse',
progress: 'bg-red-600',
text: 'text-red-600'
};
} else if (percentageUsed >= 75) {
return {
badge: 'bg-orange-500 text-white',
progress: 'bg-orange-500',
text: 'text-orange-600'
};
} else if (percentageUsed >= 50) {
return {
badge: 'bg-amber-500 text-white',
progress: 'bg-amber-500',
text: 'text-amber-600'
};
} else {
return {
badge: 'bg-green-600 text-white',
progress: 'bg-green-600',
text: 'text-gray-700'
};
}
};
const colors = getStatusColors();
// Normalize status for warning messages (still use status for text warnings)
const normalizedStatus = (rawStatus === 'on_track' || rawStatus === 'normal')
? 'normal'
: rawStatus;
return ( return (
<div data-testid={testId}> <div data-testid={testId}>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@ -50,12 +91,7 @@ export function SLAProgressBar({
<span className="text-sm font-semibold text-gray-900">SLA Progress</span> <span className="text-sm font-semibold text-gray-900">SLA Progress</span>
</div> </div>
<Badge <Badge
className={`text-xs ${ className={`text-xs ${colors.badge}`}
sla.status === 'breached' ? 'bg-red-600 text-white animate-pulse' :
sla.status === 'critical' ? 'bg-orange-600 text-white' :
sla.status === 'approaching' ? 'bg-yellow-600 text-white' :
'bg-green-600 text-white'
}`}
data-testid={`${testId}-badge`} data-testid={`${testId}-badge`}
> >
{sla.percentageUsed || 0}% elapsed {sla.percentageUsed || 0}% elapsed
@ -64,12 +100,8 @@ export function SLAProgressBar({
<Progress <Progress
value={sla.percentageUsed || 0} value={sla.percentageUsed || 0}
className={`h-3 mb-2 ${ className="h-3 mb-2"
sla.status === 'breached' ? '[&>div]:bg-red-600' : indicatorClassName={colors.progress}
sla.status === 'critical' ? '[&>div]:bg-orange-600' :
sla.status === 'approaching' ? '[&>div]:bg-yellow-600' :
'[&>div]:bg-green-600'
}`}
data-testid={`${testId}-bar`} data-testid={`${testId}-bar`}
/> />
@ -79,13 +111,13 @@ export function SLAProgressBar({
</span> </span>
<span <span
className={`font-semibold ${ className={`font-semibold ${
sla.status === 'breached' ? 'text-red-600' : normalizedStatus === 'breached' ? colors.text :
sla.status === 'critical' ? 'text-orange-600' : normalizedStatus === 'critical' ? colors.text :
'text-gray-700' 'text-gray-700'
}`} }`}
data-testid={`${testId}-remaining`} data-testid={`${testId}-remaining`}
> >
{sla.remainingText || formatHoursMinutes(sla.remainingHours || 0)} remaining {formatHoursMinutes(sla.remainingHours || 0)} remaining
</span> </span>
</div> </div>
@ -95,13 +127,13 @@ export function SLAProgressBar({
</p> </p>
)} )}
{sla.status === 'critical' && ( {normalizedStatus === 'critical' && (
<p className="text-xs text-orange-600 font-semibold mt-1 flex items-center gap-1.5" data-testid={`${testId}-warning-critical`}> <p className="text-xs text-orange-600 font-semibold mt-1 flex items-center gap-1.5" data-testid={`${testId}-warning-critical`}>
<AlertTriangle className="h-3.5 w-3.5" /> <AlertTriangle className="h-3.5 w-3.5" />
Approaching Deadline Approaching Deadline
</p> </p>
)} )}
{sla.status === 'breached' && ( {normalizedStatus === 'breached' && (
<p className="text-xs text-red-600 font-semibold mt-1 flex items-center gap-1.5" data-testid={`${testId}-warning-breached`}> <p className="text-xs text-red-600 font-semibold mt-1 flex items-center gap-1.5" data-testid={`${testId}-warning-breached`}>
<AlertOctagon className="h-3.5 w-3.5" /> <AlertOctagon className="h-3.5 w-3.5" />
URGENT - Deadline Passed URGENT - Deadline Passed

View File

@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Users, Settings, Shield, User, CheckCircle, Minus, Plus } from 'lucide-react'; import { Users, Settings, Shield, User, CheckCircle, Minus, Plus, Info, Clock } from 'lucide-react';
import { FormData } from '@/hooks/useCreateRequestForm'; import { FormData } from '@/hooks/useCreateRequestForm';
import { useMultiUserSearch } from '@/hooks/useUserSearch'; import { useMultiUserSearch } from '@/hooks/useUserSearch';
import { ensureUserExists } from '@/services/userApi'; import { ensureUserExists } from '@/services/userApi';
@ -409,6 +409,109 @@ export function ApprovalWorkflowStep({
})} })}
</CardContent> </CardContent>
</Card> </Card>
{/* TAT Summary Section */}
<div className="mt-6 space-y-4">
{/* Approval Flow Summary */}
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<h4 className="font-semibold text-blue-900 mb-1">Approval Flow Summary</h4>
<p className="text-sm text-blue-700">
Your request will follow this sequence: <strong>You (Initiator)</strong> {Array.from({ length: formData.approverCount || 1 }, (_, i) => `Level ${i + 1} Approver`).join(' → ')}. The final approver can close the request.
</p>
</div>
</div>
</div>
{/* TAT Summary */}
<div className="p-4 bg-gradient-to-r from-emerald-50 to-teal-50 rounded-lg border border-emerald-200">
<div className="flex items-start gap-3">
<Clock className="w-5 h-5 text-emerald-600 mt-0.5" />
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-emerald-900">TAT Summary</h4>
<div className="text-right">
{(() => {
// 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 (
<>
<div className="text-lg font-bold text-emerald-800">{displayDays} {displayDays === 1 ? 'Day' : 'Days'}</div>
<div className="text-xs text-emerald-600">Total Duration</div>
</>
);
})()}
</div>
</div>
<div className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{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 (
<div key={idx} className="bg-white/60 p-2 rounded border border-emerald-100">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-emerald-900">Level {idx + 1}</span>
<span className="text-sm text-emerald-700">{hours} {hours === 1 ? 'hour' : 'hours'}</span>
</div>
</div>
);
})}
</div>
{(() => {
// 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 (
<div className="bg-white/80 p-3 rounded border border-emerald-200">
<div className="grid grid-cols-2 gap-4 text-center">
<div>
<div className="text-lg font-bold text-emerald-800">{totalHours}{totalHours === 1 ? 'h' : 'h'}</div>
<div className="text-xs text-emerald-600">Total Hours</div>
</div>
<div>
<div className="text-lg font-bold text-emerald-800">{workingDays}</div>
<div className="text-xs text-emerald-600">Working Days*</div>
</div>
</div>
<p className="text-xs text-emerald-600 mt-2 text-center">*Based on 8-hour working days</p>
</div>
);
})()}
</div>
</div>
</div>
</div>
</div>
</div> </div>
</motion.div> </motion.div>
); );

View File

@ -480,23 +480,54 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</h4> </h4>
{/* SLA Display - Compact Version */} {/* SLA Display - Compact Version */}
{request.currentLevelSLA && ( {request.currentLevelSLA && (() => {
<div className={`p-2 rounded-md ${ // Use percentage-based colors to match approver SLA tracker
request.currentLevelSLA.status === 'breached' ? 'bg-red-50 border border-red-200' : // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
request.currentLevelSLA.status === 'critical' ? 'bg-orange-50 border border-orange-200' : const percentUsed = request.currentLevelSLA.percentageUsed || 0;
request.currentLevelSLA.status === 'approaching' ? 'bg-yellow-50 border border-yellow-200' :
'bg-green-50 border border-green-200' const getSLAColors = () => {
}`}> if (percentUsed >= 100) {
return {
bg: 'bg-red-50 border border-red-200',
progress: 'bg-red-600',
text: 'text-red-600'
};
} else if (percentUsed >= 75) {
return {
bg: 'bg-orange-50 border border-orange-200',
progress: 'bg-orange-500',
text: 'text-orange-600'
};
} else if (percentUsed >= 50) {
return {
bg: 'bg-amber-50 border border-amber-200',
progress: 'bg-amber-500',
text: 'text-amber-600'
};
} else {
return {
bg: 'bg-green-50 border border-green-200',
progress: 'bg-green-600',
text: 'text-gray-700'
};
}
};
const colors = getSLAColors();
return (
<div className={`p-2 rounded-md ${colors.bg}`}>
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-gray-600" /> <Clock className="w-3.5 h-3.5 text-gray-600" />
<span className="text-xs font-medium text-gray-900">TAT: {request.currentLevelSLA.percentageUsed}%</span> <span className="text-xs font-medium text-gray-900">TAT: {percentUsed}%</span>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span> <span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span>
<span className={`font-semibold ${ <span className={`font-semibold ${
request.currentLevelSLA.status === 'breached' ? 'text-red-600' : percentUsed >= 100 ? 'text-red-600' :
request.currentLevelSLA.status === 'critical' ? 'text-orange-600' : percentUsed >= 75 ? 'text-orange-600' :
percentUsed >= 50 ? 'text-amber-600' :
'text-gray-700' 'text-gray-700'
}`}> }`}>
{request.currentLevelSLA.remainingText} left {request.currentLevelSLA.remainingText} left
@ -504,16 +535,13 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</div> </div>
</div> </div>
<Progress <Progress
value={request.currentLevelSLA.percentageUsed} value={percentUsed}
className={`h-1.5 ${ className="h-1.5"
request.currentLevelSLA.status === 'breached' ? '[&>div]:bg-red-600' : indicatorClassName={colors.progress}
request.currentLevelSLA.status === 'critical' ? '[&>div]:bg-orange-600' :
request.currentLevelSLA.status === 'approaching' ? '[&>div]:bg-yellow-600' :
'[&>div]:bg-green-600'
}`}
/> />
</div> </div>
)} );
})()}
{/* Metadata Row */} {/* Metadata Row */}
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-600"> <div className="flex flex-wrap items-center gap-4 text-xs text-gray-600">