centralized time trackerd addd and okta directory user integrated paused for movement for

This commit is contained in:
laxmanhalaki 2025-11-06 19:47:35 +05:30
parent 1b903dc56b
commit 3c9d7cb620
8 changed files with 474 additions and 342 deletions

View File

@ -7,12 +7,13 @@ import { formatWorkingHours, getTimeUntilNextWorking } from '@/utils/slaTracker'
interface SLATrackerProps {
startDate: string | Date;
deadline: string | Date;
priority?: string;
className?: string;
showDetails?: boolean;
}
export function SLATracker({ startDate, deadline, className = '', showDetails = true }: SLATrackerProps) {
const slaStatus = useSLATracking(startDate, deadline);
export function SLATracker({ startDate, deadline, priority, className = '', showDetails = true }: SLATrackerProps) {
const slaStatus = useSLATracking(startDate, deadline, priority);
if (!slaStatus) {
return null;
@ -93,7 +94,7 @@ export function SLATracker({ startDate, deadline, className = '', showDetails =
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded-md border border-gray-200">
<AlertTriangle className="w-4 h-4 text-amber-500 flex-shrink-0" />
<span className="text-xs text-gray-700">
{getTimeUntilNextWorking()}
{getTimeUntilNextWorking(priority)}
</span>
</div>
)}

View File

@ -7,12 +7,14 @@ import { getSLAStatus, SLAStatus } from '@/utils/slaTracker';
*
* @param startDate - When the SLA tracking started
* @param deadline - When the SLA should complete
* @param priority - Priority type ('express' = calendar hours, 'standard' = working hours)
* @param enabled - Whether tracking is enabled (default: true)
* @returns SLAStatus object with real-time updates
*/
export function useSLATracking(
startDate: string | Date | null | undefined,
deadline: string | Date | null | undefined,
priority?: string,
enabled: boolean = true
): SLAStatus | null {
const [slaStatus, setSlaStatus] = useState<SLAStatus | null>(null);
@ -26,7 +28,7 @@ export function useSLATracking(
// Initial calculation
const updateStatus = () => {
try {
const status = getSLAStatus(startDate, deadline);
const status = getSLAStatus(startDate, deadline, priority);
setSlaStatus(status);
} catch (error) {
console.error('[useSLATracking] Error calculating SLA status:', error);
@ -39,7 +41,7 @@ export function useSLATracking(
const interval = setInterval(updateStatus, 60000); // 60 seconds
return () => clearInterval(interval);
}, [startDate, deadline, enabled]);
}, [startDate, deadline, priority, enabled]);
return slaStatus;
}

View File

@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { searchUsers, type UserSummary } from '@/services/userApi';
import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi';
import { createWorkflowMultipart, submitWorkflow, getWorkflowDetails, updateWorkflow, updateWorkflowMultipart } from '@/services/workflowApi';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@ -1457,7 +1457,7 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
<li
key={u.userId}
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
onClick={() => {
onClick={async () => {
// Check if user is already a spectator
const spectatorIds = (formData.spectators || []).map((s: any) => s?.id).filter(Boolean);
const spectatorEmails = (formData.spectators || []).map((s: any) => s?.email?.toLowerCase?.()).filter(Boolean);
@ -1476,12 +1476,30 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
return;
}
// Ensure user exists in database and get the DB userId
let dbUserId = u.userId;
try {
const dbUser = await ensureUserExists({
userId: u.userId,
email: u.email,
displayName: u.displayName,
firstName: u.firstName,
lastName: u.lastName,
department: u.department
});
// Use the database userId (UUID) instead of Okta ID
dbUserId = dbUser.userId;
} catch (err) {
console.error('Failed to ensure user exists:', err);
// Continue with Okta ID if ensure fails
}
const updated = [...formData.approvers];
updated[index] = {
...updated[index],
email: u.email,
name: u.displayName || [u.firstName, u.lastName].filter(Boolean).join(' '),
userId: u.userId,
userId: dbUserId,
level: level,
};
updateFormData('approvers', updated);
@ -1815,7 +1833,7 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
<li
key={u.userId}
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
onClick={() => {
onClick={async () => {
// Check if user is already an approver
const approverIds = (formData.approvers || []).map((a: any) => a?.userId).filter(Boolean);
const approverEmails = (formData.approvers || []).map((a: any) => a?.email?.toLowerCase?.()).filter(Boolean);
@ -1826,9 +1844,27 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
return;
}
// Ensure user exists in database and get the DB userId
let dbUserId = u.userId;
try {
const dbUser = await ensureUserExists({
userId: u.userId,
email: u.email,
displayName: u.displayName,
firstName: u.firstName,
lastName: u.lastName,
department: u.department
});
// Use the database userId (UUID) instead of Okta ID
dbUserId = dbUser.userId;
} catch (err) {
console.error('Failed to ensure user exists:', err);
// Continue with Okta ID if ensure fails
}
// Add selected spectator directly with precise id/name/email
const spectator = {
id: u.userId,
id: dbUserId,
name: u.displayName || [u.firstName, u.lastName].filter(Boolean).join(' ') || u.email.split('@')[0],
email: u.email,
avatar: (u.displayName || u.email).split(' ').map((s: string) => s[0]).join('').slice(0, 2).toUpperCase(),

View File

@ -20,50 +20,13 @@ import {
} from 'lucide-react';
import { motion } from 'framer-motion';
import workflowApi from '@/services/workflowApi';
import { SLATracker } from '@/components/sla/SLATracker';
// SLATracker removed - not needed on MyRequests (only for OpenRequests where user is approver)
interface MyRequestsProps {
onViewRequest: (requestId: string, requestTitle?: string) => void;
dynamicRequests?: any[];
}
// Removed mock data; list renders API data only
// Helper to calculate due date from created date and TAT hours
const calculateDueDate = (createdAt: string, tatHours: number, priority: string): string => {
if (!createdAt || !tatHours) return '';
try {
const startDate = new Date(createdAt);
if (priority === 'express') {
// Express: Calendar days (includes weekends)
const dueDate = new Date(startDate);
dueDate.setHours(dueDate.getHours() + tatHours);
return dueDate.toISOString();
} else {
// Standard: Working days (8 hours per day, skip weekends)
let remainingHours = tatHours;
let currentDate = new Date(startDate);
while (remainingHours > 0) {
// Skip weekends (Saturday = 6, Sunday = 0)
if (currentDate.getDay() !== 0 && currentDate.getDay() !== 6) {
const hoursToAdd = Math.min(remainingHours, 8);
remainingHours -= hoursToAdd;
}
if (remainingHours > 0) {
currentDate.setDate(currentDate.getDate() + 1);
}
}
return currentDate.toISOString();
}
} catch (error) {
console.error('Error calculating due date:', error);
return '';
}
};
const getPriorityConfig = (priority: string) => {
switch (priority) {
case 'express':
@ -157,15 +120,12 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
// Convert API/dynamic requests to the format expected by this component
const sourceRequests = (apiRequests.length ? apiRequests : dynamicRequests);
const convertedDynamicRequests = sourceRequests.map((req: any) => {
// Calculate due date
const totalTatHours = Number(req.totalTatHours || 0);
const createdAt = req.submittedAt || req.submitted_at || req.createdAt || req.created_at;
const priority = (req.priority || '').toString().toLowerCase();
const calculatedDueDate = calculateDueDate(createdAt, totalTatHours, priority);
return {
id: req.requestNumber || req.request_number || req.requestId || req.id || req.request_id, // Use requestNumber as primary identifier
requestId: req.requestId || req.id || req.request_id, // Keep requestId for API calls if needed
id: req.requestNumber || req.request_number || req.requestId || req.id || req.request_id,
requestId: req.requestId || req.id || req.request_id,
displayId: req.requestNumber || req.request_number || req.id,
title: req.title,
description: req.description,
@ -176,7 +136,6 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
createdAt: createdAt,
currentApprover: req.currentApprover?.name || req.currentApprover?.email || '—',
approverLevel: req.currentLevel && req.totalLevels ? `${req.currentLevel} of ${req.totalLevels}` : (req.currentStep && req.totalSteps ? `${req.currentStep} of ${req.totalSteps}` : '—'),
dueDate: calculatedDueDate || (req.dueDate ? new Date(req.dueDate).toISOString().split('T')[0] : undefined),
templateType: req.templateType,
templateName: req.templateName
};
@ -414,28 +373,21 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
<div className="flex items-center gap-2 min-w-0">
<User className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-gray-400 flex-shrink-0" />
<span className="text-xs sm:text-sm truncate">
<span className="text-gray-500">Current:</span> <span className="text-gray-900 font-medium">{request.currentApprover}</span>
<span className="text-gray-500">Current Approver:</span> <span className="text-gray-900 font-medium">{request.currentApprover}</span>
</span>
</div>
<div className="flex items-center gap-2">
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-gray-400 flex-shrink-0" />
<span className="text-xs sm:text-sm">
<span className="text-gray-500">Level:</span> <span className="text-gray-900 font-medium">{request.approverLevel}</span>
<span className="text-gray-500">Approval Level:</span> <span className="text-gray-900 font-medium">{request.approverLevel}</span>
</span>
</div>
</div>
<div className="flex items-center gap-1.5 text-xs text-gray-500">
<Clock className="w-3.5 h-3.5 flex-shrink-0" />
<span>Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'}</span>
</div>
{/* SLA Tracker with Working Hours */}
{request.createdAt && request.dueDate && request.status !== 'approved' && request.status !== 'rejected' && (
<div className="pt-3 border-t border-gray-100">
<SLATracker
startDate={request.createdAt}
deadline={request.dueDate}
showDetails={true}
/>
</div>
)}
</div>
</CardContent>
</Card>

View File

@ -3,13 +3,12 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Progress } from '@/components/ui/progress';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Progress } from '@/components/ui/progress';
import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, Eye, RefreshCw, Settings2, X } from 'lucide-react';
import workflowApi from '@/services/workflowApi';
import { formatDateShort } from '@/utils/dateFormatter';
interface Request {
id: string;
title: string;
@ -17,22 +16,21 @@ interface Request {
status: 'pending' | 'in-review';
priority: 'express' | 'standard';
initiator: { name: string; avatar: string };
currentApprover?: { name: string; avatar: string };
slaProgress: number;
slaRemaining: string;
currentApprover?: {
name: string;
avatar: string;
sla?: any; // Backend-calculated SLA data
};
createdAt: string;
dueDate?: string;
approvalStep?: string;
department?: string;
totalTatHours?: number;
currentLevelSLA?: any; // Backend-provided SLA for current level
}
interface OpenRequestsProps {
onViewRequest?: (requestId: string, requestTitle?: string) => void;
}
// Removed static data; will load from API
// Utility functions
const getPriorityConfig = (priority: string) => {
switch (priority) {
@ -80,11 +78,7 @@ const getStatusConfig = (status: string) => {
}
};
const getSLAUrgency = (progress: number) => {
if (progress >= 80) return { color: 'bg-red-500', textColor: 'text-red-600', urgency: 'critical' };
if (progress >= 60) return { color: 'bg-orange-500', textColor: 'text-orange-600', urgency: 'warning' };
return { color: 'bg-green-500', textColor: 'text-green-600', urgency: 'normal' };
};
// getSLAUrgency removed - now using SLATracker component for real-time SLA display
export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const [searchTerm, setSearchTerm] = useState('');
@ -96,42 +90,6 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const [items, setItems] = useState<Request[]>([]);
const [loading, setLoading] = useState(false);
// Helper to calculate due date from created date and TAT hours
const calculateDueDate = (createdAt: string, tatHours: number, priority: string): string => {
if (!createdAt || !tatHours) return 'Not set';
try {
const startDate = new Date(createdAt);
if (priority === 'express') {
// Express: Calendar days (includes weekends)
const dueDate = new Date(startDate);
dueDate.setHours(dueDate.getHours() + tatHours);
return dueDate.toISOString();
} else {
// Standard: Working days (8 hours per day, skip weekends)
let remainingHours = tatHours;
let currentDate = new Date(startDate);
while (remainingHours > 0) {
// Skip weekends (Saturday = 6, Sunday = 0)
if (currentDate.getDay() !== 0 && currentDate.getDay() !== 6) {
const hoursToAdd = Math.min(remainingHours, 8);
remainingHours -= hoursToAdd;
}
if (remainingHours > 0) {
currentDate.setDate(currentDate.getDate() + 1);
}
}
return currentDate.toISOString();
}
} catch (error) {
console.error('Error calculating due date:', error);
return 'Error';
}
};
useEffect(() => {
let mounted = true;
(async () => {
@ -147,30 +105,29 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
: [];
if (!mounted) return;
const mapped: Request[] = data.map((r: any) => {
// Use totalTatHours directly from backend (already calculated)
const totalTatHours = Number(r.totalTatHours || 0);
const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
const dueDate = calculateDueDate(createdAt, totalTatHours, (r.priority || '').toString().toLowerCase());
return {
id: r.requestNumber || r.request_number || r.requestId, // Use requestNumber as primary identifier
requestId: r.requestId, // Keep requestId for reference
// keep a display id for UI
id: r.requestNumber || r.request_number || r.requestId,
requestId: r.requestId,
displayId: r.requestNumber || r.request_number || r.requestId,
title: r.title,
description: r.description,
status: ((r.status || '').toString().toUpperCase() === 'IN_PROGRESS') ? 'in-review' : 'pending',
priority: (r.priority || '').toString().toLowerCase(),
initiator: { name: (r.initiator?.displayName || r.initiator?.email || '—'), avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()) },
currentApprover: r.currentApprover ? { name: (r.currentApprover.name || r.currentApprover.email || '—'), avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()) } : undefined,
slaProgress: Number(r.sla?.percent || 0),
slaRemaining: r.sla?.remainingText || '—',
initiator: {
name: (r.initiator?.displayName || r.initiator?.email || '—'),
avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase())
},
currentApprover: r.currentApprover ? {
name: (r.currentApprover.name || r.currentApprover.email || '—'),
avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()),
sla: r.currentApprover.sla // ← Backend-calculated SLA
} : undefined,
createdAt: createdAt || '—',
dueDate: dueDate !== 'Not set' && dueDate !== 'Error' ? dueDate : undefined,
approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
department: r.department,
totalTatHours
currentLevelSLA: r.currentLevelSLA, // ← Backend-calculated current level SLA
};
});
setItems(mapped);
@ -204,8 +161,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
bValue = new Date(b.createdAt);
break;
case 'due':
aValue = a.dueDate ? new Date(a.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
bValue = b.dueDate ? new Date(b.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
aValue = a.currentLevelSLA?.deadline ? new Date(a.currentLevelSLA.deadline).getTime() : Number.MAX_SAFE_INTEGER;
bValue = b.currentLevelSLA?.deadline ? new Date(b.currentLevelSLA.deadline).getTime() : Number.MAX_SAFE_INTEGER;
break;
case 'priority':
const priorityOrder = { express: 2, standard: 1 };
@ -213,8 +170,9 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
bValue = priorityOrder[b.priority as keyof typeof priorityOrder];
break;
case 'sla':
aValue = a.slaProgress;
bValue = b.slaProgress;
// Sort by SLA percentage (most urgent first)
aValue = a.currentLevelSLA?.percentageUsed || 0;
bValue = b.currentLevelSLA?.percentageUsed || 0;
break;
default:
return 0;
@ -389,7 +347,6 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{filteredAndSortedRequests.map((request) => {
const priorityConfig = getPriorityConfig(request.priority);
const statusConfig = getStatusConfig(request.status);
const slaConfig = getSLAUrgency(request.slaProgress);
return (
<Card
@ -447,29 +404,61 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</div>
</div>
{/* SLA Progress */}
<div className="bg-gray-50 rounded-lg p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-2">
{/* SLA Display - Shows backend-calculated SLA */}
{request.currentLevelSLA && (
<div className="pt-3 border-t border-gray-100">
<div className={`p-3 rounded-lg ${
request.currentLevelSLA.status === 'breached' ? 'bg-red-50 border border-red-200' :
request.currentLevelSLA.status === 'critical' ? 'bg-orange-50 border border-orange-200' :
request.currentLevelSLA.status === 'approaching' ? 'bg-yellow-50 border border-yellow-200' :
'bg-green-50 border border-green-200'
}`}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Clock className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-gray-500 flex-shrink-0" />
<span className="text-xs sm:text-sm font-medium text-gray-700">SLA Progress</span>
<Clock className="w-4 h-4 text-gray-600" />
<span className="text-sm font-medium text-gray-900">SLA Progress</span>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-xs sm:text-sm font-semibold ${slaConfig.textColor}`}>
{request.slaRemaining} remaining
</span>
{slaConfig.urgency === 'critical' && (
<Badge variant="destructive" className="animate-pulse text-xs shrink-0">
URGENT
<Badge className={`text-xs ${
request.currentLevelSLA.status === 'breached' ? 'bg-red-600 text-white' :
request.currentLevelSLA.status === 'critical' ? 'bg-orange-600 text-white' :
request.currentLevelSLA.status === 'approaching' ? 'bg-yellow-600 text-white' :
'bg-green-600 text-white'
}`}>
{request.currentLevelSLA.percentageUsed}%
</Badge>
</div>
<Progress
value={request.currentLevelSLA.percentageUsed}
className={`h-2 mb-2 ${
request.currentLevelSLA.status === 'breached' ? '[&>div]:bg-red-600' :
request.currentLevelSLA.status === 'critical' ? '[&>div]:bg-orange-600' :
request.currentLevelSLA.status === 'approaching' ? '[&>div]:bg-yellow-600' :
'[&>div]:bg-green-600'
}`}
/>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600">
{request.currentLevelSLA.elapsedText} elapsed
</span>
<span className={`font-semibold ${
request.currentLevelSLA.status === 'breached' ? 'text-red-600' :
request.currentLevelSLA.status === 'critical' ? 'text-orange-600' :
'text-gray-700'
}`}>
{request.currentLevelSLA.remainingText} remaining
</span>
</div>
{request.currentLevelSLA.deadline && (
<p className="text-xs text-gray-500 mt-1">
Due: {new Date(request.currentLevelSLA.deadline).toLocaleString()}
</p>
)}
</div>
</div>
<Progress
value={request.slaProgress}
className="h-2 sm:h-3 bg-gray-200"
/>
</div>
)}
{/* Status Info */}
<div className="flex items-center gap-2 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg">
@ -519,7 +508,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3 flex-shrink-0" />
<span className="truncate">Due: {request.dueDate ? formatDateShort(request.dueDate) : 'Not set'}</span>
<span className="truncate">Due: {request.currentLevelSLA?.deadline ? formatDateShort(request.currentLevelSLA.deadline) : 'Not set'}</span>
</span>
</div>
</div>

View File

@ -3,14 +3,14 @@ import { useParams } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { formatDateTime, formatDateShort } from '@/utils/dateFormatter';
import { FilePreview } from '@/components/common/FilePreview';
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
import workflowApi, { approveLevel, rejectLevel, addApprover, addApproverAtLevel, skipApprover, addSpectator, downloadDocument, getDocumentPreviewUrl } from '@/services/workflowApi';
import workflowApi, { approveLevel, rejectLevel, addApproverAtLevel, skipApprover, addSpectator, downloadDocument, getDocumentPreviewUrl } from '@/services/workflowApi';
import { uploadDocument } from '@/services/documentApi';
import { ApprovalModal } from '@/components/approval/ApprovalModal/ApprovalModal';
import { RejectionModal } from '@/components/approval/RejectionModal/RejectionModal';
@ -58,11 +58,11 @@ class RequestDetailErrorBoundary extends Component<
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('RequestDetail Error:', error, errorInfo);
}
render() {
override render() {
if (this.state.hasError) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6">
@ -144,28 +144,6 @@ const getStatusConfig = (status: string) => {
}
};
const getSLAConfig = (progress: number) => {
if (progress >= 80) {
return {
bg: 'bg-red-50',
color: 'bg-red-500',
textColor: 'text-red-700'
};
} else if (progress >= 60) {
return {
bg: 'bg-orange-50',
color: 'bg-orange-500',
textColor: 'text-orange-700'
};
} else {
return {
bg: 'bg-green-50',
color: 'bg-green-500',
textColor: 'text-green-700'
};
}
};
const getStepIcon = (status: string) => {
switch (status) {
case 'approved':
@ -233,45 +211,8 @@ function RequestDetailInner({
const [unreadWorkNotes, setUnreadWorkNotes] = useState(0);
const [mergedMessages, setMergedMessages] = useState<any[]>([]);
const [workNoteAttachments, setWorkNoteAttachments] = useState<any[]>([]);
const fileInputRef = useState<HTMLInputElement | null>(null)[0];
const { user } = useAuth();
// Helper to calculate due date from created date and TAT hours
const calculateDueDate = (createdAt: string, tatHours: number, priority: string): string => {
if (!createdAt || !tatHours) return '';
try {
const startDate = new Date(createdAt);
if (priority === 'express') {
// Express: Calendar days (includes weekends)
const dueDate = new Date(startDate);
dueDate.setHours(dueDate.getHours() + tatHours);
return dueDate.toISOString();
} else {
// Standard: Working days (8 hours per day, skip weekends)
let remainingHours = tatHours;
let currentDate = new Date(startDate);
while (remainingHours > 0) {
// Skip weekends (Saturday = 6, Sunday = 0)
if (currentDate.getDay() !== 0 && currentDate.getDay() !== 6) {
const hoursToAdd = Math.min(remainingHours, 8);
remainingHours -= hoursToAdd;
}
if (remainingHours > 0) {
currentDate.setDate(currentDate.getDate() + 1);
}
}
return currentDate.toISOString();
}
} catch (error) {
console.error('Error calculating due date:', error);
return '';
}
};
// Shared refresh routine
const refreshDetails = async () => {
try {
@ -345,9 +286,12 @@ function RequestDetailInner({
approverEmail: a.approverEmail,
tatHours: Number(a.tatHours || 0),
elapsedHours: Number(a.elapsedHours || 0),
remainingHours: Number(a.remainingHours || 0),
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
actualHours: a.levelEndTime && a.levelStartTime ? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60)) : undefined,
comment: a.comments || undefined,
timestamp: a.actionDate || undefined,
levelStartTime: a.levelStartTime || a.tatStartTime,
tatAlerts: levelAlerts,
};
});
@ -384,15 +328,6 @@ function RequestDetailInner({
};
});
// Calculate total TAT hours and due date
const totalTatHours = approvals.reduce((sum: number, a: any) => {
return sum + Number(a.tatHours || a.tat_hours || 0);
}, 0);
const createdAt = wf.submittedAt || wf.submitted_at || wf.createdAt || wf.created_at;
const priority = (wf.priority || '').toString().toLowerCase();
const calculatedDueDate = calculateDueDate(createdAt, totalTatHours, priority);
const updatedRequest = {
...wf,
id: wf.requestNumber || wf.requestId,
@ -401,16 +336,13 @@ function RequestDetailInner({
title: wf.title,
description: wf.description,
status: statusMap(wf.status),
priority: priority,
slaProgress: Number(summary?.sla?.percent || 0),
slaRemaining: summary?.sla?.remainingText || '—',
slaEndDate: calculatedDueDate || undefined,
priority: (wf.priority || '').toString().toLowerCase(),
approvalFlow,
approvals,
participants,
documents: mappedDocuments,
spectators,
summary,
summary, // ← Backend provides SLA in summary.sla
initiator: {
name: wf.initiator?.displayName || wf.initiator?.email,
role: wf.initiator?.designation || undefined,
@ -523,6 +455,10 @@ function RequestDetailInner({
setUploadingDocument(true);
try {
const file = files[0];
if (!file) {
alert('No file selected');
return;
}
// Get UUID requestId (not request number) from current request
const requestId = apiRequest?.requestId;
@ -779,9 +715,12 @@ function RequestDetailInner({
approverEmail: a.approverEmail,
tatHours: Number(a.tatHours || 0),
elapsedHours: Number(a.elapsedHours || 0),
remainingHours: Number(a.remainingHours || 0),
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
actualHours: a.levelEndTime && a.levelStartTime ? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60)) : undefined,
comment: a.comments || undefined,
timestamp: a.actionDate || undefined,
levelStartTime: a.levelStartTime || a.tatStartTime,
tatAlerts: levelAlerts,
};
});
@ -826,14 +765,6 @@ function RequestDetailInner({
};
});
// Calculate total TAT hours and due date
const totalTatHours = approvals.reduce((sum: number, a: any) => {
return sum + Number(a.tatHours || a.tat_hours || 0);
}, 0);
const createdAt = wf.submittedAt || wf.submitted_at || wf.createdAt || wf.created_at;
const calculatedDueDate = calculateDueDate(createdAt, totalTatHours, priority);
const mapped = {
id: wf.requestNumber || wf.requestId,
requestId: wf.requestId, // ← UUID for API calls
@ -841,9 +772,7 @@ function RequestDetailInner({
description: wf.description,
priority,
status: statusMap(wf.status),
slaProgress: Number(summary?.sla?.percent || 0),
slaRemaining: summary?.sla?.remainingText || '—',
slaEndDate: calculatedDueDate || undefined,
summary, // ← Backend provides comprehensive SLA in summary.sla
initiator: {
name: wf.initiator?.displayName || wf.initiator?.email,
role: wf.initiator?.designation || undefined,
@ -857,6 +786,7 @@ function RequestDetailInner({
totalSteps: wf.totalLevels,
currentStep: summary?.currentLevel || wf.currentLevel,
approvalFlow,
approvals, // ← Added: Include raw approvals array with levelStartTime/tatStartTime
documents: mappedDocuments,
spectators,
auditTrail: Array.isArray(details.activities) ? details.activities : [],
@ -991,7 +921,6 @@ function RequestDetailInner({
const priorityConfig = getPriorityConfig(request.priority || 'standard');
const statusConfig = getStatusConfig(request.status || 'pending');
const slaConfig = getSLAConfig(request.slaProgress || 0);
return (
<>
@ -1042,21 +971,78 @@ function RequestDetailInner({
</div>
</div>
{/* SLA Progress */}
<div className={`${slaConfig.bg} px-3 sm:px-4 md:px-6 py-3 sm:py-4`}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-2">
{/* SLA Progress Section - Shows OVERALL request SLA from backend */}
<div className="px-3 sm:px-4 md:px-6 py-3 sm:py-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200">
{(() => {
const sla = request.summary?.sla || request.sla;
if (!sla || request.status === 'approved' || request.status === 'rejected') {
return (
<div className="flex items-center gap-2">
<Clock className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${slaConfig.textColor} flex-shrink-0`} />
<span className="text-xs sm:text-sm font-medium text-gray-900">SLA Progress</span>
</div>
<span className={`text-xs sm:text-sm font-semibold ${slaConfig.textColor}`}>
{request.slaRemaining}
<Clock className="h-4 w-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700">
{request.status === 'approved' ? '✅ Request Approved' :
request.status === 'rejected' ? '❌ Request Rejected' : 'SLA Not Available'}
</span>
</div>
<Progress value={request.slaProgress} className="h-2 mb-2" />
<p className="text-[10px] sm:text-xs text-gray-600">
Due: {request.slaEndDate ? formatDateTime(request.slaEndDate) : 'Not set'} {request.slaProgress}% elapsed
);
}
return (
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-blue-600" />
<span className="text-sm font-semibold text-gray-900">SLA Progress</span>
</div>
<Badge className={`text-xs ${
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'
}`}>
{sla.percentageUsed || 0}% elapsed
</Badge>
</div>
<Progress
value={sla.percentageUsed || 0}
className={`h-3 mb-2 ${
sla.status === 'breached' ? '[&>div]:bg-red-600' :
sla.status === 'critical' ? '[&>div]:bg-orange-600' :
sla.status === 'approaching' ? '[&>div]:bg-yellow-600' :
'[&>div]:bg-green-600'
}`}
/>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-gray-600">
{sla.elapsedText || `${sla.elapsedHours || 0}h`} elapsed
</span>
<span className={`font-semibold ${
sla.status === 'breached' ? 'text-red-600' :
sla.status === 'critical' ? 'text-orange-600' :
'text-gray-700'
}`}>
{sla.remainingText || `${sla.remainingHours || 0}h`} remaining
</span>
</div>
{sla.deadline && (
<p className="text-xs text-gray-500">
Due: {new Date(sla.deadline).toLocaleString()} {sla.percentageUsed || 0}% elapsed
</p>
)}
{sla.status === 'critical' && (
<p className="text-xs text-orange-600 font-semibold mt-1"> Approaching Deadline</p>
)}
{sla.status === 'breached' && (
<p className="text-xs text-red-600 font-semibold mt-1">🔴 URGENT - Deadline Passed</p>
)}
</div>
);
})()}
</div>
</div>
@ -1254,22 +1240,31 @@ function RequestDetailInner({
Track the approval progress through each step
</CardDescription>
</div>
{request.totalSteps && (
{request.totalSteps && (() => {
const completedCount = request.approvalFlow?.filter((s: any) => s.status === 'approved').length || 0;
return (
<Badge variant="outline" className="font-medium text-xs sm:text-sm shrink-0">
Step {request.currentStep} of {request.totalSteps}
Step {request.currentStep} of {request.totalSteps} - {completedCount} completed
</Badge>
)}
);
})()}
</div>
</CardHeader>
<CardContent>
{request.approvalFlow && request.approvalFlow.length > 0 ? (
<div className="space-y-3 sm:space-y-4">
<div className="space-y-4 sm:space-y-6">
{request.approvalFlow.map((step: any, index: number) => {
const isActive = step.status === 'pending' || step.status === 'in-review';
const isCompleted = step.status === 'approved';
const isRejected = step.status === 'rejected';
const isWaiting = step.status === 'waiting';
// Get approval details with backend-calculated SLA
const approval = request.approvals?.find((a: any) => a.levelId === step.levelId);
const tatHours = Number(step.tatHours || 0);
const actualHours = step.actualHours;
const savedHours = isCompleted && actualHours ? Math.max(0, tatHours - actualHours) : 0;
return (
<div
key={index}
@ -1297,14 +1292,15 @@ function RequestDetailInner({
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 sm:gap-4 mb-2">
{/* 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-1">
<h4 className="font-semibold text-gray-900 text-sm sm:text-base">
{step.step ? `Step ${step.step}: ` : ''}{step.role}
<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">
Approver {index + 1}
</h4>
<Badge variant="outline" className={`text-xs shrink-0 ${
isActive ? 'bg-blue-100 text-blue-800 border-blue-200' :
<Badge variant="outline" className={`text-xs shrink-0 capitalize ${
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' :
@ -1312,25 +1308,139 @@ function RequestDetailInner({
}`}>
{step.status}
</Badge>
{isCompleted && actualHours && (
<Badge className="bg-green-600 text-white text-xs">
{actualHours.toFixed(1)} hours
</Badge>
)}
</div>
<p className="text-xs sm:text-sm text-gray-600 truncate">{step.approver}</p>
<p className="text-sm font-semibold text-gray-900">{step.approver}</p>
<p className="text-xs text-gray-600">{step.role}</p>
</div>
<div className="text-left sm:text-right flex-shrink-0">
{step.tatHours && (
<p className="text-xs text-gray-500">TAT: {step.tatHours}h</p>
)}
{step.elapsedHours !== undefined && step.elapsedHours > 0 && (
<p className="text-xs text-gray-600 font-medium">Elapsed: {step.elapsedHours}h</p>
)}
{step.actualHours !== undefined && (
<p className="text-xs text-gray-600 font-medium">Done: {step.actualHours.toFixed(2)}h</p>
<p className="text-xs text-gray-500 font-medium">Turnaround Time (TAT)</p>
<p className="text-lg font-bold text-gray-900">{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">{actualHours.toFixed(1)} hours</span>
</div>
{/* Progress Bar for Completed */}
<div className="space-y-2">
<Progress value={100} className="h-2 bg-gray-200" />
<div className="flex items-center justify-between text-xs">
<span className="text-green-600 font-semibold">Within TAT</span>
{savedHours > 0 && (
<span className="text-green-600 font-semibold">Saved {savedHours.toFixed(1)} hours</span>
)}
</div>
</div>
{/* Conclusion Remark */}
{step.comment && (
<div className="mt-2 sm:mt-3 p-2 sm:p-3 bg-white rounded-lg border border-gray-300">
<p className="text-xs sm:text-sm text-gray-700 whitespace-pre-line leading-relaxed break-words">{step.comment}</p>
<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">💬 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} / {tatHours}h 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'
}`}
/>
<div className="flex items-center justify-between">
<span className={`text-xs font-semibold ${
approval.sla.status === 'breached' ? 'text-red-600' :
approval.sla.status === 'critical' ? 'text-orange-600' :
'text-yellow-700'
}`}>
Progress: {approval.sla.percentageUsed}% of TAT used
</span>
<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">
🔴 Deadline Breached
</p>
)}
{approval.sla.status === 'critical' && (
<p className="text-xs font-semibold text-center text-orange-600">
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"> 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"> Rejection Reason:</p>
<p className="text-sm text-gray-700 leading-relaxed">{step.comment}</p>
</div>
)}
@ -1601,14 +1711,6 @@ function RequestDetailInner({
{workNoteAttachments && workNoteAttachments.length > 0 ? (
<div className="space-y-3">
{workNoteAttachments.map((file: any, index: number) => {
const fileType = (file.type || '').toLowerCase();
const displayType = fileType.includes('pdf') ? 'PDF' :
fileType.includes('excel') || fileType.includes('spreadsheet') ? 'Excel' :
fileType.includes('word') || fileType.includes('document') ? 'Word' :
fileType.includes('powerpoint') || fileType.includes('presentation') ? 'PowerPoint' :
fileType.includes('image') || fileType.includes('jpg') || fileType.includes('png') ? 'Image' :
'File';
return (
<div
key={file.attachmentId || index}
@ -1637,7 +1739,6 @@ function RequestDetailInner({
variant="ghost"
size="sm"
onClick={() => {
const { getWorkNoteAttachmentPreviewUrl } = require('@/services/workflowApi');
setPreviewDocument({
fileName: file.name,
fileType: file.type,

View File

@ -17,6 +17,23 @@ export async function searchUsers(query: string, limit: number = 10): Promise<Us
return data as UserSummary[];
}
export default { searchUsers };
/**
* Ensure user exists in database (creates if not exists)
* Call this when a user is selected/tagged to pre-create their record
*/
export async function ensureUserExists(userData: {
userId: string;
email: string;
displayName?: string;
firstName?: string;
lastName?: string;
department?: string;
phone?: string;
}): Promise<UserSummary> {
const res = await apiClient.post('/users/ensure', userData);
return (res.data?.data || res.data) as UserSummary;
}
export default { searchUsers, ensureUserExists };

View File

@ -35,17 +35,22 @@ ensureConfigLoaded().catch(() => {});
/**
* Check if current time is within working hours
* @param date - Date to check
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
*/
export function isWorkingTime(date: Date = new Date()): boolean {
export function isWorkingTime(date: Date = new Date(), priority: string = 'standard'): boolean {
const day = date.getDay(); // 0 = Sunday, 6 = Saturday
const hour = date.getHours();
// Weekend check
// For standard priority: exclude weekends
// For express priority: include weekends (calendar days)
if (priority === 'standard') {
if (day < WORK_START_DAY || day > WORK_END_DAY) {
return false;
}
}
// Working hours check
// Working hours check (applies to both priorities)
if (hour < WORK_START_HOUR || hour >= WORK_END_HOUR) {
return false;
}
@ -57,16 +62,19 @@ export function isWorkingTime(date: Date = new Date()): boolean {
/**
* Get next working time from a given date
* @param date - Current date
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
*/
export function getNextWorkingTime(date: Date = new Date()): Date {
export function getNextWorkingTime(date: Date = new Date(), priority: string = 'standard'): Date {
const result = new Date(date);
// If already in working time, return as is
if (isWorkingTime(result)) {
if (isWorkingTime(result, priority)) {
return result;
}
// If it's weekend, move to next Monday
// For standard priority: skip weekends
if (priority === 'standard') {
const day = result.getDay();
if (day === 0) { // Sunday
result.setDate(result.getDate() + 1);
@ -78,6 +86,7 @@ export function getNextWorkingTime(date: Date = new Date()): Date {
result.setHours(WORK_START_HOUR, 0, 0, 0);
return result;
}
}
// If before work hours, move to work start
if (result.getHours() < WORK_START_HOUR) {
@ -89,39 +98,47 @@ export function getNextWorkingTime(date: Date = new Date()): Date {
if (result.getHours() >= WORK_END_HOUR) {
result.setDate(result.getDate() + 1);
result.setHours(WORK_START_HOUR, 0, 0, 0);
// Check if next day is weekend
return getNextWorkingTime(result);
// Check if next day is weekend (only for standard priority)
return getNextWorkingTime(result, priority);
}
return result;
}
/**
* Calculate elapsed working hours between two dates
* Calculate elapsed working hours between two dates with minute precision
* @param startDate - Start date
* @param endDate - End date (defaults to now)
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
*/
export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = new Date()): number {
export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = new Date(), priority: string = 'standard'): number {
let current = new Date(startDate);
const end = new Date(endDate);
let elapsedHours = 0;
let elapsedMinutes = 0;
// Move hour by hour and count only working hours
// Move minute by minute and count only working minutes
while (current < end) {
if (isWorkingTime(current)) {
elapsedHours++;
if (isWorkingTime(current, priority)) {
elapsedMinutes++;
}
current.setHours(current.getHours() + 1);
current.setMinutes(current.getMinutes() + 1);
// Safety: stop if calculating more than 1 year
if (elapsedHours > 8760) break;
const hoursSoFar = elapsedMinutes / 60;
if (hoursSoFar > 8760) break;
}
return elapsedHours;
// Convert minutes to hours (with decimal precision)
return elapsedMinutes / 60;
}
/**
* Calculate remaining working hours to deadline
* @param deadline - Deadline date
* @param fromDate - Start date (defaults to now)
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
*/
export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = new Date()): number {
export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = new Date(), priority: string = 'standard'): number {
const deadlineTime = new Date(deadline).getTime();
const currentTime = new Date(fromDate).getTime();
@ -131,15 +148,19 @@ export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date =
}
// Calculate remaining working hours
return calculateElapsedWorkingHours(fromDate, deadline);
return calculateElapsedWorkingHours(fromDate, deadline, priority);
}
/**
* Calculate SLA progress percentage
* @param startDate - Start date
* @param deadline - Deadline date
* @param currentDate - Current date (defaults to now)
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
*/
export function calculateSLAProgress(startDate: Date, deadline: Date, currentDate: Date = new Date()): number {
const totalHours = calculateElapsedWorkingHours(startDate, deadline);
const elapsedHours = calculateElapsedWorkingHours(startDate, currentDate);
export function calculateSLAProgress(startDate: Date, deadline: Date, currentDate: Date = new Date(), priority: string = 'standard'): number {
const totalHours = calculateElapsedWorkingHours(startDate, deadline, priority);
const elapsedHours = calculateElapsedWorkingHours(startDate, currentDate, priority);
if (totalHours === 0) return 0;
@ -161,20 +182,22 @@ export interface SLAStatus {
statusText: string;
}
export function getSLAStatus(startDate: string | Date, deadline: string | Date): SLAStatus {
export function getSLAStatus(startDate: string | Date, deadline: string | Date, priority: string = 'standard'): SLAStatus {
const start = new Date(startDate);
const end = new Date(deadline);
const now = new Date();
const isWorking = isWorkingTime(now);
const elapsedHours = calculateElapsedWorkingHours(start, now);
const totalHours = calculateElapsedWorkingHours(start, end);
const isWorking = isWorkingTime(now, priority);
const elapsedHours = calculateElapsedWorkingHours(start, now, priority);
const totalHours = calculateElapsedWorkingHours(start, end, priority);
const remainingHours = Math.max(0, totalHours - elapsedHours);
const progress = calculateSLAProgress(start, end, now);
const progress = calculateSLAProgress(start, end, now, priority);
let statusText = '';
if (!isWorking) {
statusText = 'SLA tracking paused (outside working hours)';
statusText = priority === 'express'
? 'SLA tracking paused (outside working hours)'
: 'SLA tracking paused (outside working hours/days)';
} else if (remainingHours === 0) {
statusText = 'SLA deadline reached';
} else if (progress >= 100) {
@ -194,7 +217,7 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date):
remainingHours,
totalHours,
isPaused: !isWorking,
nextWorkingTime: !isWorking ? getNextWorkingTime(now) : undefined,
nextWorkingTime: !isWorking ? getNextWorkingTime(now, priority) : undefined,
statusText
};
}
@ -204,29 +227,40 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date):
*/
export function formatWorkingHours(hours: number): string {
if (hours === 0) return '0h';
if (hours < 0) return '0h';
const days = Math.floor(hours / 8); // 8 working hours per day
const remainingHours = hours % 8;
const totalMinutes = Math.round(hours * 60);
const days = Math.floor(totalMinutes / (8 * 60)); // 8 working hours per day
const remainingMinutes = totalMinutes % (8 * 60);
const remainingHours = Math.floor(remainingMinutes / 60);
const minutes = remainingMinutes % 60;
if (days > 0 && remainingHours > 0) {
if (days > 0 && remainingHours > 0 && minutes > 0) {
return `${days}d ${remainingHours}h ${minutes}m`;
} else if (days > 0 && remainingHours > 0) {
return `${days}d ${remainingHours}h`;
} else if (days > 0) {
return `${days}d`;
} else {
} else if (remainingHours > 0 && minutes > 0) {
return `${remainingHours}h ${minutes}m`;
} else if (remainingHours > 0) {
return `${remainingHours}h`;
} else {
return `${minutes}m`;
}
}
/**
* Get time until next working period
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
*/
export function getTimeUntilNextWorking(): string {
if (isWorkingTime()) {
export function getTimeUntilNextWorking(priority: string = 'standard'): string {
if (isWorkingTime(new Date(), priority)) {
return 'In working hours';
}
const now = new Date();
const next = getNextWorkingTime(now);
const next = getNextWorkingTime(now, priority);
const diff = next.getTime() - now.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60));