diff --git a/src/components/common/Pagination/Pagination.tsx b/src/components/common/Pagination/Pagination.tsx
new file mode 100644
index 0000000..2a155b1
--- /dev/null
+++ b/src/components/common/Pagination/Pagination.tsx
@@ -0,0 +1,141 @@
+import { Button } from '@/components/ui/button';
+import { Card, CardContent } from '@/components/ui/card';
+import { ArrowRight } from 'lucide-react';
+
+interface PaginationProps {
+ currentPage: number;
+ totalPages: number;
+ totalRecords: number;
+ itemsPerPage: number;
+ onPageChange: (page: number) => void;
+ loading?: boolean;
+ itemLabel?: string; // e.g., "requests", "activities", "approvers"
+ testIdPrefix?: string;
+}
+
+export function Pagination({
+ currentPage,
+ totalPages,
+ itemsPerPage,
+ totalRecords,
+ onPageChange,
+ loading = false,
+ itemLabel = 'items',
+ testIdPrefix = 'pagination'
+}: PaginationProps) {
+ const getPageNumbers = () => {
+ const pages = [];
+ const maxPagesToShow = 5;
+ let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
+ let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
+
+ if (endPage - startPage < maxPagesToShow - 1) {
+ startPage = Math.max(1, endPage - maxPagesToShow + 1);
+ }
+
+ for (let i = startPage; i <= endPage; i++) {
+ pages.push(i);
+ }
+
+ return pages;
+ };
+
+ // Don't show pagination if only 1 page or loading
+ if (totalPages <= 1 || loading) {
+ return null;
+ }
+
+ const startItem = ((currentPage - 1) * itemsPerPage) + 1;
+ const endItem = Math.min(currentPage * itemsPerPage, totalRecords);
+
+ return (
+
+
+
+
+ Showing {startItem} to {endItem} of {totalRecords} {itemLabel}
+
+
+
+ {/* Previous Button */}
+
+
+ {/* First page + ellipsis */}
+ {currentPage > 3 && totalPages > 5 && (
+ <>
+
+
...
+ >
+ )}
+
+ {/* Page Numbers */}
+ {getPageNumbers().map((pageNum) => (
+
+ ))}
+
+ {/* Last page + ellipsis */}
+ {currentPage < totalPages - 2 && totalPages > 5 && (
+ <>
+
...
+
+ >
+ )}
+
+ {/* Next Button */}
+
+
+
+
+
+ );
+}
+
diff --git a/src/components/common/Pagination/index.ts b/src/components/common/Pagination/index.ts
new file mode 100644
index 0000000..5af4a89
--- /dev/null
+++ b/src/components/common/Pagination/index.ts
@@ -0,0 +1,2 @@
+export { Pagination } from './Pagination';
+
diff --git a/src/components/dashboard/ActivityFeedItem/ActivityFeedItem.tsx b/src/components/dashboard/ActivityFeedItem/ActivityFeedItem.tsx
new file mode 100644
index 0000000..1b8b344
--- /dev/null
+++ b/src/components/dashboard/ActivityFeedItem/ActivityFeedItem.tsx
@@ -0,0 +1,215 @@
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Badge } from '@/components/ui/badge';
+import {
+ CheckCircle,
+ AlertTriangle,
+ MessageSquare,
+ Flame,
+ FileText,
+ Paperclip,
+ Activity,
+ ArrowRight
+} from 'lucide-react';
+import { differenceInMinutes, differenceInHours, differenceInDays } from 'date-fns';
+
+export interface ActivityData {
+ activityId: string;
+ requestNumber: string;
+ requestTitle: string;
+ action: string;
+ userId: string;
+ userName: string;
+ timestamp: string;
+ priority: string;
+}
+
+interface ActivityFeedItemProps {
+ activity: ActivityData;
+ currentUserId?: string;
+ currentUserDisplayName?: string;
+ currentUserEmail?: string;
+ onNavigate?: (requestNumber: string) => void;
+ testId?: string;
+}
+
+// Utility functions
+const getPriorityColor = (priority: string) => {
+ const p = priority.toLowerCase();
+ switch (p) {
+ case 'express': return 'bg-orange-100 text-orange-800 border-orange-200';
+ case 'standard': return 'bg-blue-100 text-blue-800 border-blue-200';
+ case 'high': return 'bg-red-100 text-red-800 border-red-200';
+ case 'medium': return 'bg-orange-100 text-orange-800 border-orange-200';
+ case 'low': return 'bg-green-100 text-green-800 border-green-200';
+ default: return 'bg-gray-100 text-gray-800 border-gray-200';
+ }
+};
+
+const getRelativeTime = (timestamp: string) => {
+ const now = new Date();
+ const time = new Date(timestamp);
+ const diffMin = differenceInMinutes(now, time);
+
+ if (diffMin < 1) return 'just now';
+ if (diffMin < 60) return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`;
+
+ const diffHrs = differenceInHours(now, time);
+ if (diffHrs < 24) return `${diffHrs} hour${diffHrs > 1 ? 's' : ''} ago`;
+
+ const diffDay = differenceInDays(now, time);
+ return `${diffDay} day${diffDay > 1 ? 's' : ''} ago`;
+};
+
+const cleanActivityDescription = (desc: string) => {
+ if (!desc) return desc;
+
+ // Remove email addresses in parentheses
+ let cleaned = desc.replace(/\s*\([^)]*@[^)]*\)/g, '');
+
+ // Remove "by [user]" at the end - we show user separately
+ cleaned = cleaned.replace(/\s+by\s+.+$/i, '');
+
+ // Shorten common phrases
+ cleaned = cleaned.replace(/has been added as approver/gi, 'added as approver');
+ cleaned = cleaned.replace(/has been added as spectator/gi, 'added as spectator');
+ cleaned = cleaned.replace(/has been/gi, '');
+
+ // Make TAT format more compact
+ cleaned = cleaned.replace(/with TAT of (\d+) hours?/gi, '(TAT: $1h)');
+ cleaned = cleaned.replace(/with TAT of (\d+) days?/gi, '(TAT: $1d)');
+
+ // Replace multiple spaces with single space
+ cleaned = cleaned.replace(/\s+/g, ' ');
+
+ return cleaned.trim();
+};
+
+const getActionIcon = (action: string) => {
+ const actionLower = action.toLowerCase();
+ if (actionLower.includes('approv')) return ;
+ if (actionLower.includes('reject')) return ;
+ if (actionLower.includes('comment')) return ;
+ if (actionLower.includes('escalat')) return ;
+ if (actionLower.includes('submit')) return ;
+ if (actionLower.includes('document')) return ;
+ return ;
+};
+
+export function ActivityFeedItem({
+ activity,
+ currentUserId,
+ currentUserDisplayName,
+ currentUserEmail,
+ onNavigate,
+ testId = 'activity-feed-item'
+}: ActivityFeedItemProps) {
+ const isCurrentUser = activity.userId === currentUserId;
+ const displayName = isCurrentUser ? 'You' : (activity.userName || 'System');
+
+ const userInitials = isCurrentUser
+ ? ((currentUserDisplayName || currentUserEmail || 'ME')
+ .split(' ')
+ .map((n: string) => n[0])
+ .join('')
+ .toUpperCase()
+ .substring(0, 2))
+ : activity.userName
+ ? activity.userName
+ .split(' ')
+ .map(n => n[0])
+ .join('')
+ .toUpperCase()
+ .substring(0, 2)
+ : 'SY'; // System default
+
+ return (
+
onNavigate?.(activity.requestNumber)}
+ data-testid={`${testId}-${activity.activityId}`}
+ >
+
+
+
+
+ {userInitials}
+
+
+
+ {getActionIcon(activity.action)}
+
+
+
+
+ {/* Header with Request Number and Priority Badge */}
+
+
+
+ {activity.requestNumber}
+
+
+ {activity.priority}
+
+
+
+
+ {/* Action Description as Text */}
+
+
+ {cleanActivityDescription(activity.action)}
+
+
+
+ {/* Request Title */}
+
+ {activity.requestTitle}
+
+
+ {/* User and Time */}
+
+
+ {displayName}
+
+ β’
+
+ {getRelativeTime(activity.timestamp)}
+
+
+
+
+
+
+ );
+}
+
diff --git a/src/components/dashboard/ActivityFeedItem/index.ts b/src/components/dashboard/ActivityFeedItem/index.ts
new file mode 100644
index 0000000..2e46e45
--- /dev/null
+++ b/src/components/dashboard/ActivityFeedItem/index.ts
@@ -0,0 +1,3 @@
+export { ActivityFeedItem } from './ActivityFeedItem';
+export type { ActivityData } from './ActivityFeedItem';
+
diff --git a/src/components/dashboard/CriticalAlertCard/CriticalAlertCard.tsx b/src/components/dashboard/CriticalAlertCard/CriticalAlertCard.tsx
new file mode 100644
index 0000000..bdc6c4f
--- /dev/null
+++ b/src/components/dashboard/CriticalAlertCard/CriticalAlertCard.tsx
@@ -0,0 +1,138 @@
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { Star } from 'lucide-react';
+
+export interface CriticalAlertData {
+ requestId: string;
+ requestNumber: string;
+ title: string;
+ priority: string;
+ totalTATHours: number;
+ originalTATHours: number;
+ breachCount: number;
+ currentLevel: number;
+ totalLevels: number;
+}
+
+interface CriticalAlertCardProps {
+ alert: CriticalAlertData;
+ onNavigate?: (requestNumber: string) => void;
+ testId?: string;
+}
+
+// Utility functions
+const calculateProgress = (alert: CriticalAlertData) => {
+ if (!alert.originalTATHours || alert.originalTATHours === 0) return 0;
+
+ const originalTAT = alert.originalTATHours;
+ const remainingTAT = alert.totalTATHours;
+
+ // If breached (negative remaining), show 100%
+ if (remainingTAT <= 0) return 100;
+
+ // Calculate elapsed time
+ const elapsedTAT = originalTAT - remainingTAT;
+
+ // Calculate percentage used
+ const percentageUsed = (elapsedTAT / originalTAT) * 100;
+
+ // Ensure it's between 0 and 100
+ return Math.min(100, Math.max(0, Math.round(percentageUsed)));
+};
+
+const formatRemainingTime = (alert: CriticalAlertData) => {
+ if (alert.totalTATHours === undefined || alert.totalTATHours === null) return 'N/A';
+
+ const hours = alert.totalTATHours;
+
+ // If TAT is breached (negative or zero)
+ if (hours <= 0) {
+ const overdue = Math.abs(hours);
+ if (overdue < 1) return `Breached`;
+ if (overdue < 24) return `${Math.round(overdue)}h overdue`;
+ return `${Math.round(overdue / 24)}d overdue`;
+ }
+
+ // If TAT is still remaining
+ if (hours < 1) return `${Math.round(hours * 60)}min left`;
+ if (hours < 24) return `${Math.round(hours)}h left`;
+ return `${Math.round(hours / 24)}d left`;
+};
+
+export function CriticalAlertCard({
+ alert,
+ onNavigate,
+ testId = 'critical-alert-card'
+}: CriticalAlertCardProps) {
+ const progress = calculateProgress(alert);
+
+ return (
+ onNavigate?.(alert.requestNumber)}
+ data-testid={`${testId}-${alert.requestId}`}
+ >
+
+
+
+
+ {alert.requestNumber}
+
+ {alert.priority === 'express' && (
+
+ )}
+ {alert.breachCount > 0 && (
+
+ {alert.breachCount}
+
+ )}
+
+
+ {alert.title}
+
+
+
+ {formatRemainingTime(alert)}
+
+
+
+
+ TAT Used
+
+ {progress}%
+
+
+
+
+ );
+}
+
diff --git a/src/components/dashboard/CriticalAlertCard/index.ts b/src/components/dashboard/CriticalAlertCard/index.ts
new file mode 100644
index 0000000..e7984a2
--- /dev/null
+++ b/src/components/dashboard/CriticalAlertCard/index.ts
@@ -0,0 +1,3 @@
+export { CriticalAlertCard } from './CriticalAlertCard';
+export type { CriticalAlertData } from './CriticalAlertCard';
+
diff --git a/src/components/dashboard/KPICard/KPICard.tsx b/src/components/dashboard/KPICard/KPICard.tsx
new file mode 100644
index 0000000..efa74e4
--- /dev/null
+++ b/src/components/dashboard/KPICard/KPICard.tsx
@@ -0,0 +1,72 @@
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { LucideIcon } from 'lucide-react';
+import { ReactNode } from 'react';
+
+interface KPICardProps {
+ title: string;
+ value: string | number;
+ icon: LucideIcon;
+ iconBgColor: string;
+ iconColor: string;
+ subtitle?: string;
+ children?: ReactNode;
+ testId?: string;
+ onClick?: () => void;
+}
+
+export function KPICard({
+ title,
+ value,
+ icon: Icon,
+ iconBgColor,
+ iconColor,
+ subtitle,
+ children,
+ testId = 'kpi-card',
+ onClick
+}: KPICardProps) {
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+ {value}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ {children && (
+
+ {children}
+
+ )}
+
+
+ );
+}
+
diff --git a/src/components/dashboard/KPICard/index.ts b/src/components/dashboard/KPICard/index.ts
new file mode 100644
index 0000000..b418b34
--- /dev/null
+++ b/src/components/dashboard/KPICard/index.ts
@@ -0,0 +1,2 @@
+export { KPICard } from './KPICard';
+
diff --git a/src/components/dashboard/StatCard/StatCard.tsx b/src/components/dashboard/StatCard/StatCard.tsx
new file mode 100644
index 0000000..baf6f7d
--- /dev/null
+++ b/src/components/dashboard/StatCard/StatCard.tsx
@@ -0,0 +1,41 @@
+import { ReactNode } from 'react';
+
+interface StatCardProps {
+ label: string;
+ value: string | number;
+ bgColor: string;
+ textColor: string;
+ testId?: string;
+ children?: ReactNode;
+}
+
+export function StatCard({
+ label,
+ value,
+ bgColor,
+ textColor,
+ testId = 'stat-card',
+ children
+}: StatCardProps) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+ {children}
+
+ );
+}
+
diff --git a/src/components/dashboard/StatCard/index.ts b/src/components/dashboard/StatCard/index.ts
new file mode 100644
index 0000000..b25baf4
--- /dev/null
+++ b/src/components/dashboard/StatCard/index.ts
@@ -0,0 +1,2 @@
+export { StatCard } from './StatCard';
+
diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx
new file mode 100644
index 0000000..27ba6ba
--- /dev/null
+++ b/src/pages/RequestDetail/RequestDetail.tsx
@@ -0,0 +1,2518 @@
+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}
+
+
+ )}
+
+
+ )}
+
+ {/* Read-Only Conclusion Remark - Shows for closed requests in Overview tab only */}
+ {request.status === 'closed' && request.conclusionRemark && (
+
+
+
+
+ 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 in Overview tab when request is approved */}
+ {needsClosure && (
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+ )}
+
+
+ )}
+
+
+
+ {/* 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}
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+
+
+ 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 (
+
+
+
+ );
+}