prvios changes pulled
This commit is contained in:
commit
1bebf3a46a
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user