From 3c9d7cb6208dbb99f154dd4870c3088cd0399b9e Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Thu, 6 Nov 2025 19:47:35 +0530 Subject: [PATCH] centralized time trackerd addd and okta directory user integrated paused for movement for --- src/components/sla/SLATracker.tsx | 7 +- src/hooks/useSLATracking.ts | 6 +- src/pages/CreateRequest/CreateRequest.tsx | 46 ++- src/pages/MyRequests/MyRequests.tsx | 66 +--- src/pages/OpenRequests/OpenRequests.tsx | 165 +++++----- src/pages/RequestDetail/RequestDetail.tsx | 377 ++++++++++++++-------- src/services/userApi.ts | 19 +- src/utils/slaTracker.ts | 130 +++++--- 8 files changed, 474 insertions(+), 342 deletions(-) diff --git a/src/components/sla/SLATracker.tsx b/src/components/sla/SLATracker.tsx index 5f27ab2..b082aa4 100644 --- a/src/components/sla/SLATracker.tsx +++ b/src/components/sla/SLATracker.tsx @@ -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 =
- {getTimeUntilNextWorking()} + {getTimeUntilNextWorking(priority)}
)} diff --git a/src/hooks/useSLATracking.ts b/src/hooks/useSLATracking.ts index d255486..f9d094b 100644 --- a/src/hooks/useSLATracking.ts +++ b/src/hooks/useSLATracking.ts @@ -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(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; } diff --git a/src/pages/CreateRequest/CreateRequest.tsx b/src/pages/CreateRequest/CreateRequest.tsx index 6242198..ad617e1 100644 --- a/src/pages/CreateRequest/CreateRequest.tsx +++ b/src/pages/CreateRequest/CreateRequest.tsx @@ -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
  • { + 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
  • { + 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(), diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx index bfb3693..4b0a677 100644 --- a/src/pages/MyRequests/MyRequests.tsx +++ b/src/pages/MyRequests/MyRequests.tsx @@ -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
    - Current: {request.currentApprover} + Current Approver: {request.currentApprover}
    - Level: {request.approverLevel} + Approval Level: {request.approverLevel}
    - - - {/* SLA Tracker with Working Hours */} - {request.createdAt && request.dueDate && request.status !== 'approved' && request.status !== 'rejected' && ( -
    - +
    + + Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'}
    - )} +
    diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx index a257cc9..1525f1f 100644 --- a/src/pages/OpenRequests/OpenRequests.tsx +++ b/src/pages/OpenRequests/OpenRequests.tsx @@ -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([]); 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 ( - {/* SLA Progress */} -
    -
    -
    - - SLA Progress -
    -
    - - {request.slaRemaining} remaining - - {slaConfig.urgency === 'critical' && ( - - URGENT + {/* SLA Display - Shows backend-calculated SLA */} + {request.currentLevelSLA && ( +
    +
    +
    +
    + + SLA Progress +
    + + {request.currentLevelSLA.percentageUsed}% +
    + + div]:bg-red-600' : + request.currentLevelSLA.status === 'critical' ? '[&>div]:bg-orange-600' : + request.currentLevelSLA.status === 'approaching' ? '[&>div]:bg-yellow-600' : + '[&>div]:bg-green-600' + }`} + /> + +
    + + {request.currentLevelSLA.elapsedText} elapsed + + + {request.currentLevelSLA.remainingText} remaining + +
    + + {request.currentLevelSLA.deadline && ( +

    + Due: {new Date(request.currentLevelSLA.deadline).toLocaleString()} +

    )}
    - -
    + )} {/* Status Info */}
    @@ -519,7 +508,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { - Due: {request.dueDate ? formatDateShort(request.dueDate) : 'Not set'} + Due: {request.currentLevelSLA?.deadline ? formatDateShort(request.currentLevelSLA.deadline) : 'Not set'}
    diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx index d3976d1..fb443f9 100644 --- a/src/pages/RequestDetail/RequestDetail.tsx +++ b/src/pages/RequestDetail/RequestDetail.tsx @@ -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 (
    @@ -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([]); const [workNoteAttachments, setWorkNoteAttachments] = useState([]); - const fileInputRef = useState(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({
    - {/* SLA Progress */} -
    -
    -
    - - SLA Progress -
    - - {request.slaRemaining} - -
    - -

    - Due: {request.slaEndDate ? formatDateTime(request.slaEndDate) : 'Not set'} • {request.slaProgress}% elapsed -

    + {/* SLA Progress Section - Shows OVERALL request SLA from backend */} +
    + {(() => { + const sla = request.summary?.sla || request.sla; + + if (!sla || request.status === 'approved' || request.status === 'rejected') { + return ( +
    + + + {request.status === 'approved' ? '✅ Request Approved' : + request.status === 'rejected' ? '❌ Request Rejected' : 'SLA Not Available'} + +
    + ); + } + + return ( +
    +
    +
    + + SLA Progress +
    + + {sla.percentageUsed || 0}% elapsed + +
    + + div]:bg-red-600' : + sla.status === 'critical' ? '[&>div]:bg-orange-600' : + sla.status === 'approaching' ? '[&>div]:bg-yellow-600' : + '[&>div]:bg-green-600' + }`} + /> + +
    + + {sla.elapsedText || `${sla.elapsedHours || 0}h`} elapsed + + + {sla.remainingText || `${sla.remainingHours || 0}h`} remaining + +
    + + {sla.deadline && ( +

    + Due: {new Date(sla.deadline).toLocaleString()} • {sla.percentageUsed || 0}% elapsed +

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

    ⚠️ Approaching Deadline

    + )} + {sla.status === 'breached' && ( +

    🔴 URGENT - Deadline Passed

    + )} +
    + ); + })()}
    @@ -1254,21 +1240,30 @@ function RequestDetailInner({ Track the approval progress through each step - {request.totalSteps && ( - - Step {request.currentStep} of {request.totalSteps} - - )} + {request.totalSteps && (() => { + const completedCount = request.approvalFlow?.filter((s: any) => s.status === 'approved').length || 0; + return ( + + Step {request.currentStep} of {request.totalSteps} - {completedCount} completed + + ); + })()} {request.approvalFlow && request.approvalFlow.length > 0 ? ( -
    +
    {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 (
    -
    + {/* Header with Approver Label and Status */} +
    -
    -

    - {step.step ? `Step ${step.step}: ` : ''}{step.role} +
    +

    + Approver {index + 1}

    - {step.status} + {isCompleted && actualHours && ( + + {actualHours.toFixed(1)} hours + + )}
    -

    {step.approver}

    +

    {step.approver}

    +

    {step.role}

    - {step.tatHours && ( -

    TAT: {step.tatHours}h

    - )} - {step.elapsedHours !== undefined && step.elapsedHours > 0 && ( -

    Elapsed: {step.elapsedHours}h

    - )} - {step.actualHours !== undefined && ( -

    Done: {step.actualHours.toFixed(2)}h

    - )} +

    Turnaround Time (TAT)

    +

    {tatHours} hours

    - {step.comment && ( -
    -

    {step.comment}

    + {/* Completed Approver - Show Completion Details */} + {isCompleted && actualHours !== undefined && ( +
    +
    + Completed: + {step.timestamp ? formatDateTime(step.timestamp) : 'N/A'} +
    +
    + Completed in: + {actualHours.toFixed(1)} hours +
    + + {/* Progress Bar for Completed */} +
    + +
    + Within TAT + {savedHours > 0 && ( + Saved {savedHours.toFixed(1)} hours + )} +
    +
    + + {/* Conclusion Remark */} + {step.comment && ( +
    +

    💬 Conclusion Remark:

    +

    {step.comment}

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

    + + Current Approver - Time Tracking +

    + +
    +
    + Time elapsed since assigned: + {approval.sla.elapsedText} +
    +
    + Time used: + {approval.sla.elapsedText} / {tatHours}h allocated +
    +
    + + {/* Progress Bar */} +
    + div]:bg-red-600' : + approval.sla.status === 'critical' ? '[&>div]:bg-orange-600' : + '[&>div]:bg-yellow-600' + }`} + /> +
    + + Progress: {approval.sla.percentageUsed}% of TAT used + + + {approval.sla.remainingText} remaining + +
    + {approval.sla.status === 'breached' && ( +

    + 🔴 Deadline Breached +

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

    + ⚠️ Approaching Deadline +

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

    ⏸️ Awaiting Previous Approval

    +

    Will be assigned after previous step

    +

    Allocated {tatHours} hours for approval

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

    ❌ Rejection Reason:

    +

    {step.comment}

    )} @@ -1601,14 +1711,6 @@ function RequestDetailInner({ {workNoteAttachments && workNoteAttachments.length > 0 ? (
    {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 (
    { - const { getWorkNoteAttachmentPreviewUrl } = require('@/services/workflowApi'); setPreviewDocument({ fileName: file.name, fileType: file.type, diff --git a/src/services/userApi.ts b/src/services/userApi.ts index c44fb71..7439e28 100644 --- a/src/services/userApi.ts +++ b/src/services/userApi.ts @@ -17,6 +17,23 @@ export async function searchUsers(query: string, limit: number = 10): Promise { + const res = await apiClient.post('/users/ensure', userData); + return (res.data?.data || res.data) as UserSummary; +} + +export default { searchUsers, ensureUserExists }; diff --git a/src/utils/slaTracker.ts b/src/utils/slaTracker.ts index c42cfcb..bfd665a 100644 --- a/src/utils/slaTracker.ts +++ b/src/utils/slaTracker.ts @@ -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 - if (day < WORK_START_DAY || day > WORK_END_DAY) { - return false; + // 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,26 +62,30 @@ 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 - const day = result.getDay(); - if (day === 0) { // Sunday - result.setDate(result.getDate() + 1); - result.setHours(WORK_START_HOUR, 0, 0, 0); - return result; - } - if (day === 6) { // Saturday - result.setDate(result.getDate() + 2); - result.setHours(WORK_START_HOUR, 0, 0, 0); - return result; + // For standard priority: skip weekends + if (priority === 'standard') { + const day = result.getDay(); + if (day === 0) { // Sunday + result.setDate(result.getDate() + 1); + result.setHours(WORK_START_HOUR, 0, 0, 0); + return result; + } + if (day === 6) { // Saturday + result.setDate(result.getDate() + 2); + result.setHours(WORK_START_HOUR, 0, 0, 0); + return result; + } } // If before work hours, move to work start @@ -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));