)}
- {/* Status Info */}
-
-
-
-
-
- {request.initiator.avatar}
-
-
-
-
{request.initiator.name}
-
Initiator
-
-
-
- {request.currentApprover && (
-
-
-
- {request.currentApprover.avatar}
-
-
-
-
{request.currentApprover.name}
-
Current Approver
-
-
- )}
+ {/* Metadata Row */}
+
+
+
+
+ {request.initiator.avatar}
+
+
+
{request.initiator.name}
-
-
-
-
- Created: {request.createdAt !== 'β' ? formatDateShort(request.createdAt) : 'β'}
-
-
-
- Due: {request.currentLevelSLA?.deadline ? formatDateShort(request.currentLevelSLA.deadline) : 'Not set'}
-
+ {request.currentApprover && (
+
+
+
+ {request.currentApprover.avatar}
+
+
+
{request.currentApprover.name}
+ )}
+
+ {request.approvalStep && (
+
+
+
{request.approvalStep}
+
+ )}
+
+
+
+ Created: {request.createdAt !== 'β' ? formatDateShort(request.createdAt) : 'β'}
+
+ {/* Right: Arrow */}
+
@@ -574,6 +567,67 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
)}
+
+ {/* Pagination Controls */}
+ {totalPages > 1 && !loading && (
+
+
+
+
+ Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} open requests
+
+
+
+
+
+ {currentPage > 3 && totalPages > 5 && (
+ <>
+
+
...
+ >
+ )}
+
+ {getPageNumbers().map((pageNum) => (
+
+ ))}
+
+ {currentPage < totalPages - 2 && totalPages > 5 && (
+ <>
+
...
+
+ >
+ )}
+
+
+
+
+
+
+ )}
);
}
diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx
deleted file mode 100644
index e15067a..0000000
--- a/src/pages/RequestDetail/RequestDetail.tsx
+++ /dev/null
@@ -1,2475 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-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 { Avatar, AvatarFallback } from '@/components/ui/avatar';
-import { Progress } from '@/components/ui/progress';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
-import { Textarea } from '@/components/ui/textarea';
-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, 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';
-import { SkipApproverModal } from '@/components/approval/SkipApproverModal';
-import { AddApproverModal } from '@/components/participant/AddApproverModal';
-import { AddSpectatorModal } from '@/components/participant/AddSpectatorModal';
-import { ActionStatusModal } from '@/components/common/ActionStatusModal';
-import { WorkNoteChat } from '@/components/workNote/WorkNoteChat/WorkNoteChat';
-import { useAuth } from '@/contexts/AuthContext';
-import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket';
-import { getWorkNotes } from '@/services/workflowApi';
-import { Component, ErrorInfo, ReactNode } from 'react';
-import {
- ArrowLeft,
- Clock,
- User,
- FileText,
- MessageSquare,
- CheckCircle,
- XCircle,
- Download,
- Eye,
- TrendingUp,
- RefreshCw,
- Activity,
- Mail,
- Phone,
- Upload,
- UserPlus,
- ClipboardList,
- Paperclip,
- AlertTriangle,
- AlertCircle,
- Loader2
-} from 'lucide-react';
-
-// Simple Error Boundary for RequestDetail
-class RequestDetailErrorBoundary extends Component<
- { children: ReactNode },
- { hasError: boolean; error: Error | null }
-> {
- constructor(props: { children: ReactNode }) {
- super(props);
- this.state = { hasError: false, error: null };
- }
-
- static getDerivedStateFromError(error: Error) {
- return { hasError: true, error };
- }
-
- override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
- console.error('RequestDetail Error:', error, errorInfo);
- }
-
- override render() {
- if (this.state.hasError) {
- return (
-
-
-
-
Error Loading Request
-
- {this.state.error?.message || 'An unexpected error occurred'}
-
-
-
-
-
- );
- }
- return this.props.children;
- }
-}
-
-interface RequestDetailProps {
- requestId: string;
- onBack?: () => void;
- dynamicRequests?: any[];
-}
-
-// Utility functions
-const getPriorityConfig = (priority: string) => {
- switch (priority) {
- case 'express':
- case 'urgent':
- return {
- color: 'bg-red-100 text-red-800 border-red-200',
- label: 'urgent priority'
- };
- case 'standard':
- return {
- color: 'bg-blue-100 text-blue-800 border-blue-200',
- label: 'standard priority'
- };
- default:
- return {
- color: 'bg-gray-100 text-gray-800 border-gray-200',
- label: 'normal priority'
- };
- }
-};
-
-const getStatusConfig = (status: string) => {
- switch (status) {
- case 'pending':
- return {
- color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
- label: 'pending'
- };
- case 'in-review':
- return {
- color: 'bg-blue-100 text-blue-800 border-blue-200',
- label: 'in-review'
- };
- case 'approved':
- return {
- color: 'bg-green-100 text-green-800 border-green-200',
- label: 'approved'
- };
- case 'rejected':
- return {
- color: 'bg-red-100 text-red-800 border-red-200',
- label: 'rejected'
- };
- case 'closed':
- return {
- color: 'bg-gray-100 text-gray-800 border-gray-300',
- label: 'closed'
- };
- case 'skipped':
- return {
- color: 'bg-orange-100 text-orange-800 border-orange-200',
- label: 'skipped'
- };
- default:
- return {
- color: 'bg-gray-100 text-gray-800 border-gray-200',
- label: status
- };
- }
-};
-
-const getStepIcon = (status: string, isSkipped?: boolean) => {
- if (isSkipped) {
- return
;
- }
-
- switch (status) {
- case 'approved':
- return
;
- case 'rejected':
- return
;
- case 'pending':
- case 'in-review':
- return
;
- case 'waiting':
- return
;
- default:
- return
;
- }
-};
-
-const getActionTypeIcon = (type: string) => {
- switch (type) {
- case 'approval':
- case 'approved':
- return
;
- case 'rejection':
- case 'rejected':
- return
;
- case 'comment':
- return
;
- case 'status_change':
- case 'updated':
- return
;
- case 'assignment':
- return
;
- case 'created':
- return
;
- case 'reminder':
- return
;
- case 'document_added':
- return
;
- case 'sla_warning':
- return
;
- default:
- return
;
- }
-};
-
-function RequestDetailInner({
- requestId: propRequestId,
- onBack,
- dynamicRequests = []
-}: RequestDetailProps) {
- const params = useParams<{ requestId: string }>();
- // Use requestNumber from URL params (which now contains requestNumber), fallback to prop
- const requestIdentifier = params.requestId || propRequestId || '';
-
- // Read tab from URL query parameter (e.g., ?tab=worknotes)
- const urlParams = new URLSearchParams(window.location.search);
- const initialTab = urlParams.get('tab') || 'overview';
-
- const [activeTab, setActiveTab] = useState(initialTab);
- const [apiRequest, setApiRequest] = useState
(null);
- const [isSpectator, setIsSpectator] = useState(false);
- // approving/rejecting local states are managed inside modals now
- const [currentApprovalLevel, setCurrentApprovalLevel] = useState(null);
- const [showApproveModal, setShowApproveModal] = useState(false);
- const [showRejectModal, setShowRejectModal] = useState(false);
- const [showAddApproverModal, setShowAddApproverModal] = useState(false);
- const [showAddSpectatorModal, setShowAddSpectatorModal] = useState(false);
- const [showSkipApproverModal, setShowSkipApproverModal] = useState(false);
- const [skipApproverData, setSkipApproverData] = useState<{ levelId: string; approverName: string; levelNumber: number } | null>(null);
- const [previewDocument, setPreviewDocument] = useState<{ fileName: string; fileType: string; documentId: string; fileSize?: number } | null>(null);
- const [uploadingDocument, setUploadingDocument] = useState(false);
- const [unreadWorkNotes, setUnreadWorkNotes] = useState(0);
- const [mergedMessages, setMergedMessages] = useState([]);
- const [workNoteAttachments, setWorkNoteAttachments] = useState([]);
- const [refreshing, setRefreshing] = useState(false);
- const [showActionStatusModal, setShowActionStatusModal] = useState(false);
- const [actionStatus, setActionStatus] = useState<{ success: boolean; title: string; message: string } | null>(null);
- const [conclusionLoading, setConclusionLoading] = useState(false);
- const [conclusionRemark, setConclusionRemark] = useState('');
- const [conclusionSubmitting, setConclusionSubmitting] = useState(false);
- const [aiGenerated, setAiGenerated] = useState(false);
- const { user } = useAuth();
-
- // Auto-switch tab when URL query parameter changes (e.g., from notifications)
- useEffect(() => {
- const urlParams = new URLSearchParams(window.location.search);
- const tabParam = urlParams.get('tab');
- if (tabParam) {
- console.log('[RequestDetail] Auto-switching to tab:', tabParam);
- setActiveTab(tabParam);
- }
- }, [requestIdentifier]); // Re-run when navigating to different request
-
- // Shared refresh routine
- const refreshDetails = async () => {
- setRefreshing(true);
- try {
- const details = await workflowApi.getWorkflowDetails(requestIdentifier);
- if (!details) {
- console.warn('[RequestDetail] No details returned from API');
- return;
- }
- const wf = details.workflow || {};
- const approvals = Array.isArray(details.approvals) ? details.approvals : [];
- const participants = Array.isArray(details.participants) ? details.participants : [];
- const documents = Array.isArray(details.documents) ? details.documents : [];
- const summary = details.summary || {};
- const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
-
- // Debug: Log TAT alerts to console
- if (tatAlerts.length > 0) {
- console.log(`[RequestDetail] Found ${tatAlerts.length} TAT alerts for request:`, tatAlerts);
- } else {
- console.log('[RequestDetail] No TAT alerts found for this request');
- }
-
- const toInitials = (name?: string, email?: string) => {
- const base = (name || email || 'NA').toString();
- return base.split(' ').map(s => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
- };
-
- const statusMap = (s: string) => {
- const val = (s || '').toUpperCase();
- if (val === 'IN_PROGRESS') return 'in-review';
- if (val === 'PENDING') return 'pending';
- if (val === 'APPROVED') return 'approved';
- if (val === 'REJECTED') return 'rejected';
- if (val === 'CLOSED') return 'closed';
- if (val === 'SKIPPED') return 'skipped';
- return (s || '').toLowerCase();
- };
-
- const currentLevel = summary?.currentLevel || wf.currentLevel || 1;
-
- const approvalFlow = approvals.map((a: any) => {
- const levelNumber = a.levelNumber || 0;
- const levelStatus = (a.status || '').toString().toUpperCase();
- const levelId = a.levelId || a.level_id;
-
- // Determine display status based on level and current status
- let displayStatus = statusMap(a.status);
-
- // If level hasn't been reached yet and status is not completed/rejected
- if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') {
- displayStatus = 'waiting';
- }
- // If it's the current level and status is PENDING
- else if (levelNumber === currentLevel && levelStatus === 'PENDING') {
- displayStatus = 'pending';
- }
-
- // Get TAT alerts for this level
- const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
-
- // Debug log
- if (levelAlerts.length > 0) {
- console.log(`[RequestDetail] Level ${levelNumber} (${levelId}) has ${levelAlerts.length} TAT alerts:`, levelAlerts);
- }
-
- return {
- step: levelNumber,
- levelId,
- role: a.levelName || a.approverName || 'Approver',
- status: displayStatus,
- approver: a.approverName || a.approverEmail,
- approverId: a.approverId || a.approver_id,
- 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,
- skipReason: a.skipReason || undefined,
- isSkipped: levelStatus === 'SKIPPED' || a.isSkipped || false,
- };
- });
-
- // Map spectators
- const spectators = participants
- .filter((p: any) => (p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR')
- .map((p: any) => ({
- name: p.userName || p.user_name || p.userEmail || p.user_email,
- role: 'Spectator',
- email: p.userEmail || p.user_email,
- avatar: toInitials(p.userName || p.user_name, p.userEmail || p.user_email),
- }));
-
- const participantNameById = (uid?: string) => {
- if (!uid) return undefined;
- const p = participants.find((x: any) => x.userId === uid || x.user_id === uid);
- if (p?.userName || p?.user_name) return p.userName || p.user_name;
- if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email;
- return uid;
- };
-
- const mappedDocuments = documents.map((d: any) => {
- const sizeBytes = Number(d.fileSize || d.file_size || 0);
- const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB';
- return {
- documentId: d.documentId || d.document_id,
- name: d.originalFileName || d.fileName || d.file_name,
- fileType: d.fileType || d.file_type || '',
- size: sizeMb,
- sizeBytes: sizeBytes,
- uploadedBy: participantNameById(d.uploadedBy || d.uploaded_by),
- uploadedAt: d.uploadedAt || d.uploaded_at,
- };
- });
-
- // Filter out TAT breach activities as they're not important for activity timeline
- const filteredActivities = Array.isArray(details.activities)
- ? details.activities.filter((activity: any) => {
- const activityType = (activity.type || '').toLowerCase();
- return activityType !== 'sla_warning'; // Exclude TAT breach/warning activities
- })
- : [];
-
- const updatedRequest = {
- ...wf,
- id: wf.requestNumber || wf.requestId,
- requestId: wf.requestId,
- requestNumber: wf.requestNumber,
- title: wf.title,
- description: wf.description,
- status: statusMap(wf.status),
- priority: (wf.priority || '').toString().toLowerCase(),
- approvalFlow,
- approvals,
- participants,
- documents: mappedDocuments,
- spectators,
- summary, // β Backend provides SLA in summary.sla
- initiator: {
- name: wf.initiator?.displayName || wf.initiator?.email,
- role: wf.initiator?.designation || undefined,
- department: wf.initiator?.department || undefined,
- email: wf.initiator?.email || undefined,
- phone: wf.initiator?.phone || undefined,
- avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email),
- },
- createdAt: wf.createdAt,
- updatedAt: wf.updatedAt,
- totalSteps: wf.totalLevels,
- currentStep: summary?.currentLevel || wf.currentLevel,
- auditTrail: filteredActivities,
- conclusionRemark: wf.conclusionRemark || null,
- closureDate: wf.closureDate || null,
- };
- setApiRequest(updatedRequest);
-
- const userEmail = (user as any)?.email?.toLowerCase();
- const newCurrentLevel = approvals.find((a: any) => {
- const st = (a.status || '').toString().toUpperCase();
- const approverEmail = (a.approverEmail || '').toLowerCase();
- return (st === 'PENDING' || st === 'IN_PROGRESS') && approverEmail === userEmail;
- });
- setCurrentApprovalLevel(newCurrentLevel || null);
-
- // Update viewer role
- const viewerId = (user as any)?.userId;
- if (viewerId) {
- const isSpec = participants.some((p: any) => (p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR' && (p.userId || p.user_id) === viewerId);
- setIsSpectator(isSpec);
- } else {
- setIsSpectator(false);
- }
- } catch (error) {
- console.error('[RequestDetail] Error refreshing details:', error);
- alert('Failed to refresh request details. Please try again.');
- } finally {
- setRefreshing(false);
- }
- };
-
- const handleRefresh = () => {
- refreshDetails();
- };
-
- const fetchExistingConclusion = async () => {
- try {
- const { getConclusion } = await import('@/services/conclusionApi');
- const result = await getConclusion(request.requestId || requestIdentifier);
- if (result && result.aiGeneratedRemark) {
- setConclusionRemark(result.finalRemark || result.aiGeneratedRemark);
- setAiGenerated(!!result.aiGeneratedRemark);
- }
- } catch (err) {
- // No conclusion yet - that's okay
- console.log('[RequestDetail] No existing conclusion found');
- }
- };
-
- const handleGenerateConclusion = async () => {
- try {
- setConclusionLoading(true);
- const { generateConclusion } = await import('@/services/conclusionApi');
- const result = await generateConclusion(request.requestId || requestIdentifier);
- setConclusionRemark(result.aiGeneratedRemark);
- setAiGenerated(true);
- } catch (err) {
- // Silently fail - user can write manually
- setConclusionRemark('');
- setAiGenerated(false);
- } finally {
- setConclusionLoading(false);
- }
- };
-
- const handleFinalizeConclusion = async () => {
- if (!conclusionRemark.trim()) {
- setActionStatus({
- success: false,
- title: 'Validation Error',
- message: 'Conclusion remark cannot be empty'
- });
- setShowActionStatusModal(true);
- return;
- }
-
- try {
- setConclusionSubmitting(true);
- const { finalizeConclusion } = await import('@/services/conclusionApi');
- await finalizeConclusion(request.requestId || requestIdentifier, conclusionRemark);
-
- setActionStatus({
- success: true,
- title: 'Request Closed with Successful Completion',
- message: 'The request has been finalized and moved to Closed Requests.'
- });
- setShowActionStatusModal(true);
-
- // Refresh to get updated status
- await refreshDetails();
-
- // Navigate to Closed Requests after a short delay (for user to see the success message)
- setTimeout(() => {
- // Use onBack if provided, otherwise navigate programmatically
- if (onBack) {
- onBack();
- // After going back, trigger navigation to closed requests
- setTimeout(() => {
- window.location.hash = '#/closed-requests';
- }, 100);
- } else {
- window.location.hash = '#/closed-requests';
- }
- }, 2000); // 2 second delay to show success message
- } catch (err: any) {
- setActionStatus({
- success: false,
- title: 'Error',
- message: err.response?.data?.error || 'Failed to finalize conclusion'
- });
- setShowActionStatusModal(true);
- } finally {
- setConclusionSubmitting(false);
- }
- };
-
- // Work notes load
-
- // Approve modal onConfirm
- async function handleApproveConfirm(description: string) {
- const levelId = currentApprovalLevel?.levelId || currentApprovalLevel?.level_id;
- if (!levelId) { alert('Approval level not found'); return; }
-
- await approveLevel(requestIdentifier, levelId, description || '');
- await refreshDetails();
- // Close modal + notify (assumes global handlers, replace as needed)
- (window as any)?.closeModal?.();
- (window as any)?.toast?.('Approved successfully');
- }
-
- // Reject modal onConfirm (UI uses only comments/remarks; map it to both fields)
- async function handleRejectConfirm(description: string) {
- if (!description?.trim()) { alert('Comments & remarks are required'); return; }
-
- const levelId = currentApprovalLevel?.levelId || currentApprovalLevel?.level_id;
- if (!levelId) { alert('Approval level not found'); return; }
-
- await rejectLevel(requestIdentifier, levelId, description.trim(), description.trim());
- await refreshDetails();
- // Close modal + notify (assumes global handlers, replace as needed)
- (window as any)?.closeModal?.();
- (window as any)?.toast?.('Rejected successfully');
- }
-
- // Add approver modal handler (enhanced with level and TAT)
- async function handleAddApprover(email: string, tatHours: number, level: number) {
- try {
- await addApproverAtLevel(requestIdentifier, email, tatHours, level);
- await refreshDetails();
- setShowAddApproverModal(false);
- setActionStatus({
- success: true,
- title: 'Approver Added',
- message: `Approver added successfully at Level ${level} with ${tatHours}h TAT`
- });
- setShowActionStatusModal(true);
- } catch (error: any) {
- setActionStatus({
- success: false,
- title: 'Failed to Add Approver',
- message: error?.response?.data?.error || 'Failed to add approver. Please try again.'
- });
- setShowActionStatusModal(true);
- throw error;
- }
- }
-
- // Skip approver handler
- async function handleSkipApprover(reason: string) {
- if (!skipApproverData) return;
-
- try {
- await skipApprover(requestIdentifier, skipApproverData.levelId, reason);
- await refreshDetails();
- setShowSkipApproverModal(false);
- setSkipApproverData(null);
- setActionStatus({
- success: true,
- title: 'Approver Skipped',
- message: 'Approver skipped successfully. The workflow has moved to the next level.'
- });
- setShowActionStatusModal(true);
- } catch (error: any) {
- setActionStatus({
- success: false,
- title: 'Failed to Skip Approver',
- message: error?.response?.data?.error || 'Failed to skip approver. Please try again.'
- });
- setShowActionStatusModal(true);
- throw error;
- }
- }
-
- // Add spectator modal handler
- async function handleAddSpectator(email: string) {
- try {
- await addSpectator(requestIdentifier, email);
- await refreshDetails();
- setShowAddSpectatorModal(false);
- setActionStatus({
- success: true,
- title: 'Spectator Added',
- message: 'Spectator added successfully. They can now view this request.'
- });
- setShowActionStatusModal(true);
- } catch (error: any) {
- setActionStatus({
- success: false,
- title: 'Failed to Add Spectator',
- message: error?.response?.data?.error || 'Failed to add spectator. Please try again.'
- });
- setShowActionStatusModal(true);
- throw error;
- }
- }
-
- // Document upload handler
- const handleDocumentUpload = async (event: React.ChangeEvent) => {
- const files = event.target.files;
- if (!files || files.length === 0) return;
-
- 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;
- if (!requestId) {
- alert('Request ID not found');
- return;
- }
-
- await uploadDocument(file, requestId, 'SUPPORTING');
-
- // Refresh the details to show new document and activity
- await refreshDetails();
-
- alert('Document uploaded successfully');
- } catch (error: any) {
- console.error('Upload error:', error);
- alert(error?.response?.data?.error || 'Failed to upload document');
- } finally {
- setUploadingDocument(false);
- // Clear the input
- if (event.target) {
- event.target.value = '';
- }
- }
- };
-
- // Trigger file input
- const triggerFileInput = () => {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.gif';
- input.onchange = handleDocumentUpload as any;
- input.click();
- };
-
- // Establish socket connection and join request room when component mounts
- useEffect(() => {
- if (!requestIdentifier) {
- console.warn('[RequestDetail] No requestIdentifier, cannot join socket room');
- return;
- }
-
- console.log('[RequestDetail] Initializing socket connection for:', requestIdentifier);
-
- let mounted = true;
- let actualRequestId = requestIdentifier;
-
- (async () => {
- try {
- // Fetch UUID if we have request number
- const details = await workflowApi.getWorkflowDetails(requestIdentifier);
- if (details?.workflow?.requestId && mounted) {
- actualRequestId = details.workflow.requestId; // Use UUID for room
- console.log('[RequestDetail] Resolved UUID:', actualRequestId);
- }
- } catch (error) {
- console.error('[RequestDetail] Failed to resolve UUID:', error);
- }
-
- if (!mounted) return;
-
- // Get socket instance with base URL
- const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000';
- const socket = getSocket(baseUrl);
-
- if (!socket) {
- console.error('[RequestDetail] Socket not available after getSocket()');
- return;
- }
-
- console.log('[RequestDetail] Socket instance obtained, connected:', socket.connected);
-
- const userId = (user as any)?.userId;
- console.log('[RequestDetail] Current userId:', userId);
-
- // Wait for socket to connect before joining room
- const handleConnect = () => {
- console.log('[RequestDetail] Socket connected, joining room with UUID:', actualRequestId);
- joinRequestRoom(socket, actualRequestId, userId);
- console.log(`[RequestDetail] β
Joined request room: ${actualRequestId} - User is now ONLINE`);
- };
-
- // If already connected, join immediately
- if (socket.connected) {
- console.log('[RequestDetail] Socket already connected, joining immediately');
- handleConnect();
- } else {
- console.log('[RequestDetail] Socket not connected yet, waiting for connect event');
- socket.on('connect', handleConnect);
- }
-
- // Cleanup - only runs when component unmounts or requestIdentifier changes
- return () => {
- if (mounted) {
- console.log('[RequestDetail] Cleaning up socket listeners and leaving room');
- socket.off('connect', handleConnect);
- leaveRequestRoom(socket, actualRequestId);
- console.log(`[RequestDetail] β
Left request room: ${actualRequestId} - User is now OFFLINE`);
- }
- };
- })();
-
- return () => {
- mounted = false;
- };
- }, [requestIdentifier, user]);
-
- // Fetch and merge work notes with activities
- useEffect(() => {
- if (!requestIdentifier || !apiRequest) return;
-
- (async () => {
- try {
- const workNotes = await getWorkNotes(requestIdentifier);
- const activities = apiRequest.auditTrail || [];
-
- // Merge work notes and activities
- const merged = [...workNotes, ...activities];
-
- // Sort by timestamp
- merged.sort((a, b) => {
- const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime();
- const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime();
- return timeA - timeB;
- });
-
- setMergedMessages(merged);
- console.log(`[RequestDetail] Merged ${workNotes.length} work notes with ${activities.length} activities`);
- } catch (error) {
- console.error('[RequestDetail] Failed to fetch and merge messages:', error);
- }
- })();
- }, [requestIdentifier, apiRequest]);
-
- // Separate effect to listen for new work notes and update badge
- useEffect(() => {
- if (!requestIdentifier) return;
-
- const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000';
- const socket = getSocket(baseUrl);
-
- if (!socket) return;
-
- // Listen for new work notes to update badge count when not on work notes tab
- const handleNewWorkNote = (data: any) => {
- console.log(`[RequestDetail] New work note received:`, data);
- // Only increment badge if not currently viewing work notes tab
- if (activeTab !== 'worknotes') {
- setUnreadWorkNotes(prev => prev + 1);
- }
-
- // Refresh merged messages when new work note arrives
- (async () => {
- try {
- const workNotes = await getWorkNotes(requestIdentifier);
- const activities = apiRequest?.auditTrail || [];
- const merged = [...workNotes, ...activities].sort((a, b) => {
- const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime();
- const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime();
- return timeA - timeB;
- });
- setMergedMessages(merged);
- } catch (error) {
- console.error('[RequestDetail] Failed to refresh messages:', error);
- }
- })();
- };
-
- socket.on('noteHandler', handleNewWorkNote);
- socket.on('worknote:new', handleNewWorkNote); // Also listen to worknote:new
-
- // Listen for real-time TAT alerts
- const handleTatAlert = (data: any) => {
- console.log(`[RequestDetail] π Real-time TAT alert received:`, data);
-
- // Show visual feedback (you can replace with a toast notification)
- const alertEmoji = data.type === 'breach' ? 'β°' : data.type === 'threshold2' ? 'β οΈ' : 'β³';
- console.log(`%c${alertEmoji} TAT Alert: ${data.message}`, 'color: #ff6600; font-size: 14px; font-weight: bold;');
-
- // Refresh the request to get updated TAT alerts
- (async () => {
- try {
- const details = await workflowApi.getWorkflowDetails(requestIdentifier);
-
- if (details) {
- setApiRequest(details);
-
- // Update approval steps with new TAT alerts
- const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
- console.log(`[RequestDetail] Refreshed TAT alerts after real-time update:`, tatAlerts);
-
- // Optional: Show browser notification if user granted permission
- if ('Notification' in window && Notification.permission === 'granted') {
- new Notification(`${alertEmoji} TAT Alert`, {
- body: data.message,
- icon: '/favicon.ico',
- tag: `tat-${data.requestId}-${data.type}`,
- requireInteraction: false
- });
- }
- }
- } catch (error) {
- console.error('[RequestDetail] Failed to refresh after TAT alert:', error);
- }
- })();
- };
-
- socket.on('tat:alert', handleTatAlert);
-
- // Cleanup
- return () => {
- socket.off('noteHandler', handleNewWorkNote);
- socket.off('worknote:new', handleNewWorkNote);
- socket.off('tat:alert', handleTatAlert);
- };
- }, [requestIdentifier, activeTab, apiRequest]);
-
- // Clear unread count when switching to work notes tab
- useEffect(() => {
- if (activeTab === 'worknotes') {
- setUnreadWorkNotes(0);
- }
- }, [activeTab]);
-
- useEffect(() => {
- let mounted = true;
- (async () => {
- try {
-
- // Use requestIdentifier (which should now be requestNumber) for API call
- const details = await workflowApi.getWorkflowDetails(requestIdentifier);
- if (!mounted || !details) return;
- const wf = details.workflow || {};
- const approvals = Array.isArray(details.approvals) ? details.approvals : [];
- const participants = Array.isArray(details.participants) ? details.participants : [];
- const documents = Array.isArray(details.documents) ? details.documents : [];
- const summary = details.summary || {};
- const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
-
- // Debug: Log TAT alerts to console
- console.log('[RequestDetail] TAT Alerts received from API:', tatAlerts.length, tatAlerts);
-
- // Map to UI shape without changing UI
- const toInitials = (name?: string, email?: string) => {
- const base = (name || email || 'NA').toString();
- return base.split(' ').map(s => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
- };
-
- const statusMap = (s: string) => {
- const val = (s || '').toUpperCase();
- if (val === 'IN_PROGRESS') return 'in-review';
- if (val === 'PENDING') return 'pending';
- if (val === 'APPROVED') return 'approved';
- if (val === 'REJECTED') return 'rejected';
- if (val === 'CLOSED') return 'closed';
- return (s || '').toLowerCase();
- };
-
- const priority = (wf.priority || '').toString().toLowerCase();
- const currentLevel = summary?.currentLevel || wf.currentLevel || 1;
-
- const approvalFlow = approvals.map((a: any) => {
- const levelNumber = a.levelNumber || 0;
- const levelStatus = (a.status || '').toString().toUpperCase();
- const levelId = a.levelId || a.level_id;
-
- // Determine display status based on level and current status
- let displayStatus = statusMap(a.status);
-
- // If level hasn't been reached yet and status is not completed/rejected
- if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') {
- displayStatus = 'waiting';
- }
- // If it's the current level and status is PENDING
- else if (levelNumber === currentLevel && levelStatus === 'PENDING') {
- displayStatus = 'pending';
- }
-
- // Get TAT alerts for this level
- const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
-
- // Debug log
- if (levelAlerts.length > 0) {
- console.log(`[RequestDetail useEffect] Level ${levelNumber} (${levelId}) has ${levelAlerts.length} TAT alerts:`, levelAlerts);
- }
-
- return {
- step: levelNumber,
- levelId,
- role: a.levelName || a.approverName || 'Approver',
- status: displayStatus,
- approver: a.approverName || a.approverEmail,
- approverId: a.approverId || a.approver_id,
- 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,
- };
- });
-
- // Find current approval level for logged-in user
- const userEmail = (user as any)?.email?.toLowerCase();
- const userCurrentLevel = approvals.find((a: any) => {
- const status = (a.status || '').toString().toUpperCase();
- const approverEmail = (a.approverEmail || '').toLowerCase();
- return (status === 'PENDING' || status === 'IN_PROGRESS') &&
- approverEmail === userEmail;
- });
- setCurrentApprovalLevel(userCurrentLevel || null);
-
- const spectators = participants
- .filter((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR')
- .map((p: any) => ({
- name: p.userName || p.userEmail,
- role: 'Spectator',
- avatar: toInitials(p.userName, p.userEmail),
- }));
-
- const participantNameById = (uid?: string) => {
- if (!uid) return undefined;
- const p = participants.find((x: any) => x.userId === uid);
- if (p?.userName) return p.userName;
- if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email;
- return uid;
- };
-
- const mappedDocuments = documents.map((d: any) => {
- const sizeBytes = Number(d.fileSize || 0);
- const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB';
- return {
- documentId: d.documentId || d.document_id,
- name: d.originalFileName || d.fileName,
- fileType: d.fileType || d.file_type || '',
- size: sizeMb,
- sizeBytes: sizeBytes,
- uploadedBy: participantNameById(d.uploadedBy),
- uploadedAt: d.uploadedAt,
- };
- });
-
- // Filter out TAT breach activities as they're not important for activity timeline
- const filteredActivities = Array.isArray(details.activities)
- ? details.activities.filter((activity: any) => {
- const activityType = (activity.type || '').toLowerCase();
- return activityType !== 'sla_warning'; // Exclude TAT breach/warning activities
- })
- : [];
-
- const mapped = {
- id: wf.requestNumber || wf.requestId,
- requestId: wf.requestId, // β UUID for API calls
- title: wf.title,
- description: wf.description,
- priority,
- status: statusMap(wf.status),
- summary, // β Backend provides comprehensive SLA in summary.sla
- initiator: {
- name: wf.initiator?.displayName || wf.initiator?.email,
- role: wf.initiator?.designation || undefined,
- department: wf.initiator?.department || undefined,
- email: wf.initiator?.email || undefined,
- phone: wf.initiator?.phone || undefined,
- avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email),
- },
- createdAt: wf.createdAt,
- updatedAt: wf.updatedAt,
- totalSteps: wf.totalLevels,
- currentStep: summary?.currentLevel || wf.currentLevel,
- approvalFlow,
- approvals, // β Added: Include raw approvals array with levelStartTime/tatStartTime
- documents: mappedDocuments,
- spectators,
- auditTrail: filteredActivities,
- conclusionRemark: wf.conclusionRemark || null,
- closureDate: wf.closureDate || null,
- };
- setApiRequest(mapped);
- // Determine viewer role (spectator only means comment-only)
- const viewerId = (user as any)?.userId;
- if (viewerId) {
- const isSpec = participants.some((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR' && p.userId === viewerId);
- setIsSpectator(isSpec);
- } else {
- setIsSpectator(false);
- }
- } catch (error) {
- console.error('[RequestDetail] Error loading request details:', error);
- if (mounted) {
- // Set a minimal request object to prevent complete failure
- setApiRequest(null);
- }
- } finally {
-
- }
- })();
- return () => { mounted = false; };
- }, [requestIdentifier]);
-
- // Get request from any database or dynamic requests
- const request = useMemo(() => {
- if (apiRequest) return apiRequest;
- // First check custom request database (by requestNumber or requestId)
- const customRequest = CUSTOM_REQUEST_DATABASE[requestIdentifier];
- if (customRequest) return customRequest;
-
- // Then check claim management database
- const claimRequest = CLAIM_MANAGEMENT_DATABASE[requestIdentifier];
- if (claimRequest) return claimRequest;
-
- // Then check dynamic requests (match by requestNumber or id)
- const dynamicRequest = dynamicRequests.find((req: any) =>
- req.id === requestIdentifier ||
- req.requestNumber === requestIdentifier ||
- req.request_number === requestIdentifier
- );
- if (dynamicRequest) return dynamicRequest;
-
- return null;
- }, [requestIdentifier, dynamicRequests, apiRequest]);
-
- // Check if current user is the initiator
- const isInitiator = useMemo(() => {
- if (!request || !user) return false;
- const userEmail = (user as any)?.email?.toLowerCase();
- const initiatorEmail = request.initiator?.email?.toLowerCase();
- return userEmail === initiatorEmail;
- }, [request, user]);
-
- // Fetch existing conclusion when request is approved (generated by final approver)
- useEffect(() => {
- if (request?.status === 'approved' && isInitiator && !conclusionRemark) {
- fetchExistingConclusion();
- }
- }, [request?.status, isInitiator]);
-
- // Get all existing participants for validation
- const existingParticipants = useMemo(() => {
- if (!request) return [];
-
- const participants: Array<{ email: string; participantType: string; name?: string }> = [];
-
- // Add initiator
- if (request.initiator?.email) {
- participants.push({
- email: request.initiator.email.toLowerCase(),
- participantType: 'INITIATOR',
- name: request.initiator.name
- });
- }
-
- // Add approvers from approval flow
- if (request.approvalFlow && Array.isArray(request.approvalFlow)) {
- request.approvalFlow.forEach((approval: any) => {
- if (approval.approverEmail) {
- participants.push({
- email: approval.approverEmail.toLowerCase(),
- participantType: 'APPROVER',
- name: approval.approver
- });
- }
- });
- }
-
- // Add spectators
- if (request.spectators && Array.isArray(request.spectators)) {
- request.spectators.forEach((spectator: any) => {
- if (spectator.email) {
- participants.push({
- email: spectator.email.toLowerCase(),
- participantType: 'SPECTATOR',
- name: spectator.name
- });
- }
- });
- }
-
- // Add from participants array if available
- if (request.participants && Array.isArray(request.participants)) {
- request.participants.forEach((p: any) => {
- const email = (p.userEmail || p.email || '').toLowerCase();
- const participantType = (p.participantType || p.participant_type || '').toUpperCase();
- const name = p.userName || p.user_name || p.name;
-
- if (email && participantType && !participants.find(x => x.email === email)) {
- participants.push({ email, participantType, name });
- }
- });
- }
-
- return participants;
- }, [request]);
-
- // Loading state
- if (!request && !apiRequest) {
- return (
-
-
-
-
Loading request details...
-
-
- );
- }
-
- if (!request) {
- return (
-
-
-
Request Not Found
-
The request you're looking for doesn't exist.
-
-
-
- );
- }
-
- const priorityConfig = getPriorityConfig(request.priority || 'standard');
- const statusConfig = getStatusConfig(request.status || 'pending');
-
- // Check if request is approved and needs closure by initiator
- const needsClosure = request.status === 'approved' && isInitiator;
-
- return (
- <>
-
-
- {/* Header Section */}
-
- {/* Top Header */}
-
-
-
-
-
-
-
-
-
-
-
-
{request.id || 'N/A'}
-
-
- {priorityConfig.label}
-
-
- {statusConfig.label}
-
-
-
-
-
-
-
-
-
-
-
{request.title}
-
-
-
- {/* SLA Progress Section - Shows OVERALL request SLA from backend */}
-
- {(() => {
- const sla = request.summary?.sla || request.sla;
-
- if (!sla || request.status === 'approved' || request.status === 'rejected' || request.status === 'closed') {
- return (
-
-
-
- {request.status === 'closed' ? 'π Request Closed' :
- request.status === 'approved' ? 'β
Request Approved' :
- request.status === 'rejected' ? 'β Request Rejected' : 'SLA Not Available'}
-
-
- );
- }
-
- return (
-
-
-
-
- SLA Progress
-
-
- {sla.percentageUsed || 0}% elapsed
-
-
-
-
- );
- })()}
-
-
-
- {/* Tabs */}
-
-
-
-
- Overview
-
-
-
- Workflow
-
-
-
- Docs
-
-
-
- Activity
-
-
-
- Work Notes
- {unreadWorkNotes > 0 && (
-
- {unreadWorkNotes > 9 ? '9+' : unreadWorkNotes}
-
- )}
-
-
-
- {/* Main Layout - Full width for Work Notes, Grid with sidebar for others */}
-
- {/* Left Column - Tab Content (2/3 width for most tabs, full width for work notes) */}
-
-
- {/* Overview Tab */}
-
-
- {/* Request Initiator */}
-
-
-
-
- Request Initiator
-
-
-
-
-
-
- {request.initiator?.avatar || 'U'}
-
-
-
-
{request.initiator?.name || 'N/A'}
-
{request.initiator?.role || 'N/A'}
-
{request.initiator?.department || 'N/A'}
-
-
-
-
- {request.initiator?.email || 'N/A'}
-
-
-
-
{request.initiator?.phone || 'N/A'}
-
-
-
-
-
-
-
- {/* Request Details */}
-
-
-
-
- Request Details
-
-
-
-
-
-
-
- {request.description}
-
-
-
-
- {/* Additional Details */}
- {(request.category || request.subcategory) && (
-
- {request.category && (
-
-
-
{request.category}
-
- )}
- {request.subcategory && (
-
-
-
{request.subcategory}
-
- )}
-
- )}
-
- {request.amount && (
-
-
-
{request.amount}
-
- )}
-
-
-
-
-
{formatDateTime(request.createdAt)}
-
-
-
-
{formatDateTime(request.updatedAt)}
-
-
-
-
-
- {/* Claim Management Details - Show only for claim management requests */}
- {request.claimDetails && (
-
-
-
-
- Claim Management Details
-
-
-
-
-
-
-
{request.claimDetails.activityName || 'N/A'}
-
-
-
-
{request.claimDetails.activityType || 'N/A'}
-
-
-
-
{request.claimDetails.location || 'N/A'}
-
-
-
-
{request.claimDetails.activityDate ? formatDateShort(request.claimDetails.activityDate) : 'N/A'}
-
-
-
-
{request.claimDetails.dealerCode || 'N/A'}
-
-
-
-
{request.claimDetails.dealerName || 'N/A'}
-
-
-
- {request.claimDetails.requestDescription && (
-
-
-
- {request.claimDetails.requestDescription}
-
-
- )}
-
-
- )}
-
-
-
- {/* Workflow Tab */}
-
-
-
-
-
-
-
- Approval Workflow
-
-
- Track the approval progress through each step
-
-
- {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 (
-
-
-
- {getStepIcon(step.status, step.isSkipped)}
-
-
-
- {/* Header with Approver Label and Status */}
-
-
-
-
- Approver {index + 1}
-
-
- {step.isSkipped ? 'skipped' : step.status}
-
- {step.isSkipped && step.skipReason && (
-
-
-
-
-
-
- βοΈ Skip Reason:
- {step.skipReason}
-
-
-
- )}
- {isCompleted && actualHours && (
-
- {actualHours.toFixed(1)} hours
-
- )}
-
- {(() => {
- // Check if this approver is the current user
- const currentUserEmail = (user as any)?.email?.toLowerCase();
- const approverEmail = step.approverEmail?.toLowerCase();
- const isCurrentUser = currentUserEmail && approverEmail && currentUserEmail === approverEmail;
-
- return (
- <>
-
- {isCurrentUser ? (
- You
- ) : (
- step.approver
- )}
-
-
{step.role}
- >
- );
- })()}
-
-
-
Turnaround Time (TAT)
-
{tatHours} hours
-
-
-
- {/* 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 */}
-
-
-
-
- )}
-
- {/* 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}
-
- )}
-
- {/* Skipped Status */}
- {step.isSkipped && step.skipReason && (
-
-
βοΈ Skip Reason:
-
{step.skipReason}
- {step.timestamp && (
-
Skipped on {formatDateTime(step.timestamp)}
- )}
-
- )}
-
- {/* TAT Alerts/Reminders */}
- {step.tatAlerts && step.tatAlerts.length > 0 && (
-
- {step.tatAlerts.map((alert: any, alertIndex: number) => (
-
-
-
- {(alert.thresholdPercentage || 0) === 50 && 'β³'}
- {(alert.thresholdPercentage || 0) === 75 && 'β οΈ'}
- {(alert.thresholdPercentage || 0) === 100 && 'β°'}
-
-
-
-
- Reminder {alertIndex + 1} - {alert.thresholdPercentage || 0}% TAT
-
-
- {alert.isBreached ? 'BREACHED' : 'WARNING'}
-
-
-
-
- {alert.thresholdPercentage || 0}% of SLA breach reminder have been sent
-
-
- {/* Time Tracking Details */}
-
-
- Allocated:
-
- {Number(alert.tatHoursAllocated || 0).toFixed(2)}h
-
-
-
- Elapsed:
-
- {Number(alert.tatHoursElapsed || 0).toFixed(2)}h
- {alert.metadata?.tatTestMode && (
-
- ({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m)
-
- )}
-
-
-
- Remaining:
-
- {Number(alert.tatHoursRemaining || 0).toFixed(2)}h
- {alert.metadata?.tatTestMode && (
-
- ({(Number(alert.tatHoursRemaining || 0) * 60).toFixed(0)}m)
-
- )}
-
-
-
- Due by:
-
- {alert.expectedCompletionTime ? formatDateShort(alert.expectedCompletionTime) : 'N/A'}
-
-
-
-
-
-
-
- Reminder sent by system automatically
-
- {(alert.metadata?.testMode || alert.metadata?.tatTestMode) && (
-
- TEST MODE
-
- )}
-
-
- Sent at: {alert.alertSentAt ? formatDateTime(alert.alertSentAt) : 'N/A'}
-
- {(alert.metadata?.testMode || alert.metadata?.tatTestMode) && (
-
- Note: Test mode active (1 hour = 1 minute)
-
- )}
-
-
-
-
- ))}
-
- )}
-
- {step.timestamp && (
-
- {isCompleted ? 'Approved' : isRejected ? 'Rejected' : 'Actioned'} on {formatDateTime(step.timestamp)}
-
- )}
-
- {/* Skip Approver Button - Only show for initiator on pending/in-review levels */}
- {isInitiator && (isActive || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && (
-
-
-
- Skip if approver is unavailable and move to next level
-
-
- )}
-
-
-
- );
- })}
-
- ) : (
- No workflow steps defined
- )}
-
-
-
-
- {/* Documents Tab */}
-
-
- {/* Section 1: Request Documents */}
-
-
-
-
-
-
- Request Documents
-
- Documents attached while creating the request
-
-
-
-
-
- {request.documents && request.documents.length > 0 ? (
-
- {request.documents.map((doc: any, index: number) => (
-
-
-
-
-
-
-
{doc.name}
-
- {doc.size} β’ Uploaded by {doc.uploadedBy} on {formatDateTime(doc.uploadedAt)}
-
-
-
-
- {/* Preview button for images and PDFs */}
- {doc.documentId && (() => {
- const type = (doc.fileType || '').toLowerCase();
- return type.includes('image') || type.includes('pdf') ||
- type.includes('jpg') || type.includes('jpeg') ||
- type.includes('png') || type.includes('gif');
- })() && (
-
- )}
-
- {/* Download button */}
-
-
-
- ))}
-
- ) : (
- No documents uploaded yet
- )}
-
-
-
- {/* Section 2: Work Note Attachments */}
-
-
-
-
- Work Note Attachments
-
- Files shared in work notes discussions
-
-
- {workNoteAttachments && workNoteAttachments.length > 0 ? (
-
- {workNoteAttachments.map((file: any, index: number) => {
- return (
-
-
-
-
-
{file.name}
-
- {file.size ? `${(file.size / 1024).toFixed(1)} KB` : 'Unknown size'} β’ Shared by {file.uploadedBy} on {formatDateTime(file.uploadedAt)}
-
-
-
-
- {/* Preview button */}
- {file.attachmentId && (() => {
- const type = (file.type || '').toLowerCase();
- return type.includes('image') || type.includes('pdf') ||
- type.includes('jpg') || type.includes('jpeg') ||
- type.includes('png') || type.includes('gif');
- })() && (
-
- )}
-
- {/* Download button */}
-
-
-
- );
- })}
-
- ) : (
- No files shared in work notes yet
- )}
-
-
-
-
-
- {/* Activity Tab */}
-
-
-
-
-
- Activity Timeline
-
-
- Complete audit trail of all request activities
-
-
-
-
- {request.auditTrail && request.auditTrail.length > 0 ? request.auditTrail.map((entry: any, index: number) => (
-
- {/* Icon */}
-
-
- {getActionTypeIcon(entry.type)}
-
-
-
- {/* Activity Content */}
-
-
- {/* Header with action title and timestamp */}
-
-
{entry.action}
- {formatDateTime(entry.timestamp)}
-
-
- {/* Details (includes "by whom" at the end) */}
-
-
-
-
- )) : (
-
-
-
No activity recorded yet
-
Actions and updates will appear here
-
- )}
-
-
-
-
-
- {/* Work Notes Tab - Full Width (Last Tab) */}
-
-
- flow && typeof flow.step === 'number')
- .map((flow: any) => ({
- levelNumber: flow.step || 0,
- approverName: flow.approver || 'Unknown',
- status: flow.status || 'pending',
- tatHours: flow.tatHours || 24
- }))
- }
- onAddApprover={handleAddApprover}
- />
-
-
-
-
-
- {/* Right Column - Quick Actions Sidebar (1/3 width) - Hidden for Work Notes Tab */}
- {activeTab !== 'worknotes' && (
-
- {/* Quick Actions */}
-
-
- Quick Actions
-
-
- {/* Only initiator can add approvers (not for closed requests) */}
- {isInitiator && request.status !== 'closed' && (
-
- )}
- {/* Non-spectators can add spectators (not for closed requests) */}
- {!isSpectator && request.status !== 'closed' && (
-
- )}
-
-
- {!isSpectator && currentApprovalLevel && (
- <>
-
-
- >
- )}
-
-
-
-
- {/* Spectators */}
- {request.spectators && request.spectators.length > 0 && (
-
-
- Spectators
-
-
- {request.spectators.map((spectator: any, index: number) => (
-
-
-
- {spectator.avatar}
-
-
-
-
{spectator.name}
-
{spectator.role}
-
-
- ))}
-
-
- )}
-
- )}
-
-
- {/* Read-Only Conclusion Remark - Shows for closed requests */}
- {request.status === 'closed' && request.conclusionRemark && activeTab !== 'worknotes' && (
-
-
-
-
-
- Conclusion Remark
-
-
- Final summary of this closed request
-
-
-
-
-
- {request.conclusionRemark}
-
-
-
- {request.closureDate && (
-
- Request closed on {formatDateTime(request.closureDate)}
- By {request.initiator?.name || 'Initiator'}
-
- )}
-
-
-
- )}
-
- {/* Conclusion Remark Section - Shows below tabs when request is approved */}
- {needsClosure && activeTab !== 'worknotes' && (
-
-
-
-
-
-
-
- Conclusion Remark - Final Step
-
-
- All approvals are complete. Please review and finalize the conclusion to close this request.
-
-
-
-
-
-
- {conclusionLoading ? (
-
-
-
-
Preparing conclusion remark...
-
-
- ) : (
-
-
-
-
- {aiGenerated && (
- β System-generated suggestion (editable)
- )}
-
-
-
-
-
-
Finalizing this request will:
-
- - Change request status to "CLOSED"
- - Notify all participants of closure
- - Move request to Closed Requests
- - Save conclusion remark permanently
-
-
-
-
-
-
-
- )}
-
-
-
- )}
-
-
-
- setShowApproveModal(false)}
- onConfirm={handleApproveConfirm}
- requestIdDisplay={request.id}
- requestTitle={request.title}
- />
- setShowRejectModal(false)}
- onConfirm={handleRejectConfirm}
- requestIdDisplay={request.id}
- requestTitle={request.title}
- />
- setShowAddApproverModal(false)}
- onConfirm={handleAddApprover}
- requestIdDisplay={request.id}
- requestTitle={request.title}
- existingParticipants={existingParticipants}
- currentLevels={(request.approvalFlow || [])
- .filter((flow: any) => flow && typeof flow.step === 'number')
- .map((flow: any) => ({
- levelNumber: flow.step || 0,
- approverName: flow.approver || 'Unknown',
- status: flow.status || 'pending',
- tatHours: flow.tatHours || 24
- }))
- }
- />
- setShowAddSpectatorModal(false)}
- onConfirm={handleAddSpectator}
- requestIdDisplay={request.id}
- requestTitle={request.title}
- existingParticipants={existingParticipants}
- />
- {
- setShowSkipApproverModal(false);
- setSkipApproverData(null);
- }}
- onConfirm={handleSkipApprover}
- approverName={skipApproverData?.approverName}
- levelNumber={skipApproverData?.levelNumber}
- requestIdDisplay={request.id}
- requestTitle={request.title}
- />
- {previewDocument && (
- setPreviewDocument(null)}
- />
- )}
- {actionStatus && (
- {
- setShowActionStatusModal(false);
- setActionStatus(null);
- }}
- success={actionStatus.success}
- title={actionStatus.title}
- message={actionStatus.message}
- />
- )}
- >
- );
-}
-
-// Render modals near the root return (below existing JSX)
-// Note: Ensure this stays within the component scope
-
-// Export wrapped component with error boundary
-export function RequestDetail(props: RequestDetailProps) {
- return (
-
-
-
- );
-}
diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts
index f2565c3..b22545e 100644
--- a/src/services/dashboard.service.ts
+++ b/src/services/dashboard.service.ts
@@ -95,10 +95,31 @@ export interface CriticalRequest {
totalLevels: number;
submissionDate: string;
totalTATHours: number;
+ originalTATHours: number;
breachCount: number;
isCritical: boolean;
}
+export interface AIRemarkUtilization {
+ totalUsage: number;
+ totalEdits: number;
+ editRate: number;
+ monthlyTrends: Array<{
+ month: string;
+ aiUsage: number;
+ manualEdits: number;
+ }>;
+}
+
+export interface ApproverPerformance {
+ approverId: string;
+ approverName: string;
+ totalApproved: number;
+ tatCompliancePercent: number;
+ avgResponseHours: number;
+ pendingCount: number;
+}
+
export interface UpcomingDeadline {
levelId: string;
requestId: string;
@@ -227,14 +248,25 @@ class DashboardService {
}
/**
- * Get recent activity feed
+ * Get recent activity feed with pagination
*/
- async getRecentActivity(limit: number = 10): Promise {
+ async getRecentActivity(page: number = 1, limit: number = 10): Promise<{
+ activities: RecentActivity[],
+ pagination: {
+ currentPage: number,
+ totalPages: number,
+ totalRecords: number,
+ limit: number
+ }
+ }> {
try {
const response = await apiClient.get('/dashboard/activity/recent', {
- params: { limit }
+ params: { page, limit }
});
- return response.data.data;
+ return {
+ activities: response.data.data,
+ pagination: response.data.pagination
+ };
} catch (error) {
console.error('Failed to fetch recent activity:', error);
throw error;
@@ -242,12 +274,25 @@ class DashboardService {
}
/**
- * Get critical requests
+ * Get critical requests with pagination
*/
- async getCriticalRequests(): Promise {
+ async getCriticalRequests(page: number = 1, limit: number = 10): Promise<{
+ criticalRequests: CriticalRequest[],
+ pagination: {
+ currentPage: number,
+ totalPages: number,
+ totalRecords: number,
+ limit: number
+ }
+ }> {
try {
- const response = await apiClient.get('/dashboard/requests/critical');
- return response.data.data;
+ const response = await apiClient.get('/dashboard/requests/critical', {
+ params: { page, limit }
+ });
+ return {
+ criticalRequests: response.data.data,
+ pagination: response.data.pagination
+ };
} catch (error) {
console.error('Failed to fetch critical requests:', error);
throw error;
@@ -255,14 +300,25 @@ class DashboardService {
}
/**
- * Get upcoming deadlines
+ * Get upcoming deadlines with pagination
*/
- async getUpcomingDeadlines(limit: number = 5): Promise {
+ async getUpcomingDeadlines(page: number = 1, limit: number = 10): Promise<{
+ deadlines: UpcomingDeadline[],
+ pagination: {
+ currentPage: number,
+ totalPages: number,
+ totalRecords: number,
+ limit: number
+ }
+ }> {
try {
const response = await apiClient.get('/dashboard/deadlines/upcoming', {
- params: { limit }
+ params: { page, limit }
});
- return response.data.data;
+ return {
+ deadlines: response.data.data,
+ pagination: response.data.pagination
+ };
} catch (error) {
console.error('Failed to fetch upcoming deadlines:', error);
throw error;
@@ -298,6 +354,47 @@ class DashboardService {
throw error;
}
}
+
+ /**
+ * Get AI Remark Utilization with monthly trends
+ */
+ async getAIRemarkUtilization(dateRange?: DateRange): Promise {
+ try {
+ const response = await apiClient.get('/dashboard/stats/ai-remark-utilization', {
+ params: { dateRange }
+ });
+ return response.data.data;
+ } catch (error) {
+ console.error('Failed to fetch AI remark utilization:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Get Approver Performance metrics with pagination
+ */
+ async getApproverPerformance(dateRange?: DateRange, page: number = 1, limit: number = 10): Promise<{
+ performance: ApproverPerformance[],
+ pagination: {
+ currentPage: number,
+ totalPages: number,
+ totalRecords: number,
+ limit: number
+ }
+ }> {
+ try {
+ const response = await apiClient.get('/dashboard/stats/approver-performance', {
+ params: { dateRange, page, limit }
+ });
+ return {
+ performance: response.data.data,
+ pagination: response.data.pagination
+ };
+ } catch (error) {
+ console.error('Failed to fetch approver performance:', error);
+ throw error;
+ }
+ }
}
export const dashboardService = new DashboardService();
diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts
index cb6d6e0..71d529e 100644
--- a/src/services/workflowApi.ts
+++ b/src/services/workflowApi.ts
@@ -162,19 +162,31 @@ export async function listWorkflows(params: { page?: number; limit?: number } =
export async function listMyWorkflows(params: { page?: number; limit?: number } = {}) {
const { page = 1, limit = 20 } = params;
const res = await apiClient.get('/workflows/my', { params: { page, limit } });
- return res.data?.data || res.data;
+ // Response structure: { success, data: { data: [...], pagination: {...} } }
+ return {
+ data: res.data?.data?.data || res.data?.data || [],
+ pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 }
+ };
}
export async function listOpenForMe(params: { page?: number; limit?: number } = {}) {
const { page = 1, limit = 20 } = params;
const res = await apiClient.get('/workflows/open-for-me', { params: { page, limit } });
- return res.data?.data || res.data;
+ // Response structure: { success, data: { data: [...], pagination: {...} } }
+ return {
+ data: res.data?.data?.data || res.data?.data || [],
+ pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 }
+ };
}
export async function listClosedByMe(params: { page?: number; limit?: number } = {}) {
const { page = 1, limit = 20 } = params;
const res = await apiClient.get('/workflows/closed-by-me', { params: { page, limit } });
- return res.data?.data || res.data;
+ // Response structure: { success, data: { data: [...], pagination: {...} } }
+ return {
+ data: res.data?.data?.data || res.data?.data || [],
+ pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 }
+ };
}
export async function getWorkflowDetails(requestId: string) {