/**
* RequestDetail Component
*
* Purpose: Display and manage detailed view of a workflow request
*
* Features:
* - View request details and approval workflow
* - Approve/Reject requests (if user is assigned approver)
* - Add/Skip approvers (if user is initiator)
* - Upload and view documents
* - Real-time work notes chat
* - Activity timeline with audit trail
* - TAT/SLA tracking with alerts
* - Conclusion remark generation and finalization
*
* Architecture:
* - Custom hooks for complex logic (useRequestDetails, useRequestSocket, etc.)
* - Reusable components (ApprovalStepCard, DocumentCard, SLAProgressBar)
* - Error boundary for graceful error handling
* - Real-time WebSocket integration
*/
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Component, ErrorInfo, ReactNode } from 'react';
// UI Components
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
// Utility imports
import { formatDateTime, formatDateShort } from '@/utils/dateFormatter';
import { getPriorityConfig, getStatusConfig, getActionTypeIcon } from '@/utils/requestDetailHelpers.tsx';
// Service imports
import { downloadDocument, getDocumentPreviewUrl } from '@/services/workflowApi';
// Component imports
import { FilePreview } from '@/components/common/FilePreview';
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 { ApprovalStepCard } from '@/components/workflow/ApprovalWorkflow';
import { DocumentCard } from '@/components/workflow/DocumentUpload';
import { SLAProgressBar } from '@/components/sla/SLAProgressBar';
// Context and hooks
import { useAuth } from '@/contexts/AuthContext';
import { useRequestDetails } from '@/hooks/useRequestDetails';
import { useRequestSocket } from '@/hooks/useRequestSocket';
import { useDocumentUpload } from '@/hooks/useDocumentUpload';
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
import { useModalManager } from '@/hooks/useModalManager';
// Icon imports
import {
ArrowLeft,
User,
FileText,
MessageSquare,
CheckCircle,
XCircle,
Eye,
TrendingUp,
RefreshCw,
Activity,
Mail,
Phone,
Upload,
UserPlus,
ClipboardList,
AlertTriangle,
Loader2,
AlertCircle
} from 'lucide-react';
/**
* Error Boundary Component
*
* Purpose: Catch and handle React errors gracefully
*
* Benefits:
* - Prevents entire app crash if RequestDetail has an error
* - Shows user-friendly error message
* - Provides recovery options (reload or go back)
* - Logs errors for debugging
*
* Catches:
* - Runtime errors in component tree
* - Rendering errors
* - Lifecycle method errors
*/
class RequestDetailErrorBoundary extends Component<
{ children: ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props: { children: ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
// Static method: Update state when error is caught
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
// Lifecycle: Log error details for debugging
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('RequestDetail Error:', error, errorInfo);
}
// Render: Show error UI or normal children
override render() {
if (this.state.hasError) {
return (
Error Loading Request
{this.state.error?.message || 'An unexpected error occurred'}
window.location.reload()} className="mr-2">
Reload Page
window.history.back()}>
Go Back
);
}
return this.props.children;
}
}
/**
* RequestDetailProps Interface
*
* Props:
* @param requestId - Request number or UUID to load
* @param onBack - Optional callback for back button navigation
* @param dynamicRequests - Optional array of requests for fallback data
*/
interface RequestDetailProps {
requestId: string;
onBack?: () => void;
dynamicRequests?: any[];
}
/**
* RequestDetailInner Component
*
* Purpose: Main component logic for request detail view
*
* Architecture:
* - Uses custom hooks for complex logic (data fetching, socket, document upload, etc.)
* - Delegates state management to specialized hooks
* - Focuses on UI rendering and user interactions
* - All heavy lifting is done by hooks, keeping this component clean
*/
function RequestDetailInner({
requestId: propRequestId,
onBack,
dynamicRequests = []
}: RequestDetailProps) {
// Route params: Get request identifier from URL
const params = useParams<{ requestId: string }>();
const requestIdentifier = params.requestId || propRequestId || '';
// URL query params: Read initial tab from URL (e.g., ?tab=worknotes)
const urlParams = new URLSearchParams(window.location.search);
const initialTab = urlParams.get('tab') || 'overview';
// State: Currently active tab
const [activeTab, setActiveTab] = useState(initialTab);
// Auth: Get current logged-in user
const { user } = useAuth();
/**
* Custom Hook: useRequestDetails
*
* Handles:
* - Request data fetching and transformation
* - Approval flow mapping with TAT alerts
* - Spectator and participant management
* - Current user's role determination (initiator, approver, spectator)
* - Data refresh functionality
*/
const {
request,
apiRequest,
refreshing,
refreshDetails,
currentApprovalLevel,
isSpectator,
isInitiator,
existingParticipants
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
/**
* Custom Hook: useRequestSocket
*
* Handles:
* - WebSocket connection management
* - Real-time work notes updates
* - TAT alerts listening
* - Merged timeline of work notes and activities
* - Unread work notes badge
*/
const {
mergedMessages,
unreadWorkNotes,
workNoteAttachments,
setWorkNoteAttachments
} = useRequestSocket(requestIdentifier, apiRequest, activeTab, user);
/**
* Custom Hook: useDocumentUpload
*
* Handles:
* - Document upload functionality
* - File input triggering
* - Upload loading state
* - Document preview modal
*/
const {
uploadingDocument,
triggerFileInput,
previewDocument,
setPreviewDocument,
documentPolicy,
documentError,
setDocumentError
} = useDocumentUpload(apiRequest, refreshDetails);
/**
* Custom Hook: useModalManager
*
* Handles:
* - All modal visibility states
* - Approve/Reject/Skip actions
* - Add approver/spectator actions
* - Action status feedback
*/
const {
showApproveModal,
setShowApproveModal,
showRejectModal,
setShowRejectModal,
showAddApproverModal,
setShowAddApproverModal,
showAddSpectatorModal,
setShowAddSpectatorModal,
showSkipApproverModal,
setShowSkipApproverModal,
showActionStatusModal,
setShowActionStatusModal,
skipApproverData,
setSkipApproverData,
actionStatus,
setActionStatus,
handleApproveConfirm,
handleRejectConfirm,
handleAddApprover,
handleSkipApprover,
handleAddSpectator
} = useModalManager(requestIdentifier, currentApprovalLevel, refreshDetails);
/**
* Custom Hook: useConclusionRemark
*
* Handles:
* - AI-generated conclusion fetching
* - Conclusion generation
* - Conclusion finalization and request closure
* - Navigation after closure
*/
const {
conclusionRemark,
setConclusionRemark,
conclusionLoading,
conclusionSubmitting,
aiGenerated,
handleGenerateConclusion,
handleFinalizeConclusion
} = useConclusionRemark(
request,
requestIdentifier,
isInitiator,
refreshDetails,
onBack,
setActionStatus,
setShowActionStatusModal
);
/**
* Effect: Auto-switch tab when URL query parameter changes
*
* Use Case: When user clicks notification, URL includes ?tab=worknotes
* This effect automatically switches to the specified tab
*
* Trigger: When requestIdentifier changes (navigating to different request)
*/
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]);
/**
* Handler: handleRefresh
*
* Purpose: Trigger manual refresh of request data
* Delegates to useRequestDetails hook's refreshDetails function
*/
const handleRefresh = () => {
refreshDetails();
};
/**
* Computed: Get display configuration for priority and status badges
* Uses helper functions from requestDetailHelpers utility
*/
const priorityConfig = getPriorityConfig(request?.priority || 'standard');
const statusConfig = getStatusConfig(request?.status || 'pending');
/**
* Computed: Determine if request needs conclusion from initiator
* TRUE when: Request is approved AND current user is the initiator
* Purpose: Show conclusion remark form to close the request
*/
const needsClosure = request?.status === 'approved' && isInitiator;
/**
* Early return: Show loading state while request data is being fetched
*/
if (!request && !apiRequest) {
return (
Loading request details...
);
}
/**
* Early return: Show error state if request not found
*/
if (!request) {
return (
Request Not Found
The request you're looking for doesn't exist.
Go Back
);
}
/**
* Main Render: Request detail UI with tabs
*
* Layout structure:
* - Header: Request ID, title, priority, status, refresh button
* - SLA Progress Bar: Overall request SLA/TAT tracking
* - Tabs: Overview, Workflow, Documents, Activity, Work Notes
* - Sidebar: Quick actions (approve, reject, add participants)
* - Modals: All action modals rendered at bottom
*/
return (
<>
{/* Header Section: Request ID, title, badges, refresh button */}
{/* Top Header */}
{/* Back Button: Navigate to previous page */}
{/* File Icon */}
{/* Request ID/Number */}
{request.id || 'N/A'}
{/* Priority and Status Badges */}
{priorityConfig.label}
{statusConfig.label}
{/* Refresh Button: Manually reload request data */}
{refreshing ? 'Refreshing...' : 'Refresh'}
{/* Request Title */}
{request.title}
{/* SLA Progress Section: Shows overall request SLA/TAT from backend */}
{/* Tabs: Overview, Workflow, Documents, Activity, Work Notes */}
Overview
Workflow
Docs
Activity
Work Notes
{/* Unread Work Notes Badge */}
{unreadWorkNotes > 0 && (
{unreadWorkNotes > 9 ? '9+' : unreadWorkNotes}
)}
{/* Main Layout: Full width for Work Notes, Grid with sidebar for other tabs */}
{/* Left Column: Tab content (2/3 width normally, full width for work notes) */}
{/* Overview Tab: Request initiator, details, and conclusion */}
{/* Request Initiator Card */}
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 Card */}
Request Details
{/* Additional Details: Category, Subcategory (if applicable) */}
{(request.category || request.subcategory) && (
{request.category && (
Category
{request.category}
)}
{request.subcategory && (
Subcategory
{request.subcategory}
)}
)}
{/* Amount (if applicable) */}
{request.amount && (
)}
{/* Timestamps: Created and Last Updated */}
Created
{formatDateTime(request.createdAt)}
Last Updated
{formatDateTime(request.updatedAt)}
{/* Claim Management Details: Show only for claim management requests */}
{request.claimDetails && (
Claim Management Details
Activity Name
{request.claimDetails.activityName || 'N/A'}
Activity Type
{request.claimDetails.activityType || 'N/A'}
Location
{request.claimDetails.location || 'N/A'}
Activity Date
{request.claimDetails.activityDate ? formatDateShort(request.claimDetails.activityDate) : 'N/A'}
Dealer Code
{request.claimDetails.dealerCode || 'N/A'}
Dealer Name
{request.claimDetails.dealerName || 'N/A'}
{request.claimDetails.requestDescription && (
Request Description
{request.claimDetails.requestDescription}
)}
)}
{/* Read-Only Conclusion Remark: Shows for closed requests */}
{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 when request is approved (initiator finalizes) */}
{needsClosure && (
Conclusion Remark - Final Step
All approvals are complete. Please review and finalize the conclusion to close this request.
{/* AI Generation Button */}
{aiGenerated ? 'Regenerate' : 'Generate with AI'}
{conclusionLoading ? (
Preparing conclusion remark...
) : (
Conclusion Remark
{/* AI Generated Badge */}
{aiGenerated && (
✓ System-generated suggestion (editable)
)}
{/* Conclusion Textarea */}
{/* Info Box: What happens when finalizing */}
Finalizing this request will:
Change request status to "CLOSED"
Notify all participants of closure
Move request to Closed Requests
Save conclusion remark permanently
{/* Finalize Button */}
{conclusionSubmitting ? (
<>
Finalizing...
>
) : (
<>
Finalize & Close Request
>
)}
)}
)}
{/* Workflow Tab: Approval steps with TAT tracking */}
Approval Workflow
Track the approval progress through each step
{/* Progress Badge: Current step and completion count */}
{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 ? (
{/* Map each approval step to ApprovalStepCard component */}
{request.approvalFlow.map((step: any, index: number) => {
// Get approval details with backend-calculated SLA
const approval = request.approvals?.find((a: any) => a.levelId === step.levelId);
// 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 (
{
if (!data.levelId) {
alert('Level ID not available');
return;
}
setSkipApproverData(data);
setShowSkipApproverModal(true);
}}
onRefresh={refreshDetails}
testId="workflow-step"
/>
);
})}
) : (
No workflow steps defined
)}
{/* Documents Tab: Request documents and work note attachments */}
{/* Section 1: Request Documents */}
Request Documents
Documents attached while creating the request
{/* Upload Document Button */}
{uploadingDocument ? 'Uploading...' : request.status === 'closed' ? 'Closed' : 'Upload'}
{request.status === 'closed' ? '' : 'Document'}
Max {documentPolicy.maxFileSizeMB}MB
{request.documents && request.documents.length > 0 ? (
{/* Map documents to DocumentCard component */}
{request.documents.map((doc: any, index: number) => (
setPreviewDocument(previewDoc)}
onDownload={downloadDocument}
testId="request-document"
/>
))}
) : (
No documents uploaded yet
)}
{/* Section 2: Work Note Attachments */}
Work Note Attachments
Files shared in work notes discussions
{workNoteAttachments && workNoteAttachments.length > 0 ? (
{/* Map work note attachments to DocumentCard component */}
{workNoteAttachments.map((file: any, index: number) => (
setPreviewDocument(previewDoc)}
onDownload={async (attachmentId) => {
const { downloadWorkNoteAttachment } = require('@/services/workflowApi');
await downloadWorkNoteAttachment(attachmentId);
}}
testId="worknote-attachment"
/>
))}
) : (
No files shared in work notes yet
)}
{/* Activity Tab: Complete audit trail of all actions */}
Activity Timeline
Complete audit trail of all request activities
{/* Map activity entries to timeline items */}
{request.auditTrail && request.auditTrail.length > 0 ? request.auditTrail.map((entry: any, index: number) => (
{/* Icon based on activity type */}
{getActionTypeIcon(entry.type)}
{/* Activity Content */}
{/* Header with action title and timestamp */}
{entry.action}
{formatDateTime(entry.timestamp)}
{/* Details */}
)) : (
No activity recorded yet
Actions and updates will appear here
)}
{/* Work Notes Tab: Real-time chat (Full Width) */}
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) */}
{activeTab !== 'worknotes' && (
{/* Quick Actions Card */}
Quick Actions
{/* Add Approver: Only initiator can add (not for closed requests) */}
{isInitiator && request.status !== 'closed' && (
setShowAddApproverModal(true)}
data-testid="add-approver-button"
>
Add Approver
)}
{/* Add Spectator: Non-spectators can add (not for closed requests) */}
{!isSpectator && request.status !== 'closed' && (
setShowAddSpectatorModal(true)}
data-testid="add-spectator-button"
>
Add Spectator
)}
{/* Approve/Reject Buttons: Only show for assigned approvers */}
{!isSpectator && currentApprovalLevel && (
<>
setShowApproveModal(true)}
data-testid="approve-request-button"
>
Approve Request
setShowRejectModal(true)}
data-testid="reject-request-button"
>
Reject Request
>
)}
{/* Spectators Card: List of users with view-only access */}
{request.spectators && request.spectators.length > 0 && (
Spectators
{request.spectators.map((spectator: any, index: number) => (
{spectator.avatar}
{spectator.name}
{spectator.role}
))}
)}
)}
{/* Modals: All action modals rendered here */}
{/* Approval Modal */}
setShowApproveModal(false)}
onConfirm={handleApproveConfirm}
requestIdDisplay={request.id}
requestTitle={request.title}
/>
{/* Rejection Modal */}
setShowRejectModal(false)}
onConfirm={handleRejectConfirm}
requestIdDisplay={request.id}
requestTitle={request.title}
/>
{/* Add Approver Modal */}
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
}))
}
/>
{/* Add Spectator Modal */}
setShowAddSpectatorModal(false)}
onConfirm={handleAddSpectator}
requestIdDisplay={request.id}
requestTitle={request.title}
existingParticipants={existingParticipants}
/>
{/* Skip Approver Modal */}
{
setShowSkipApproverModal(false);
setSkipApproverData(null);
}}
onConfirm={handleSkipApprover}
approverName={skipApproverData?.approverName}
levelNumber={skipApproverData?.levelNumber}
requestIdDisplay={request.id}
requestTitle={request.title}
/>
{/* File Preview Modal */}
{previewDocument && (
setPreviewDocument(null)}
/>
)}
{/* Action Status Modal: Success/Error feedback */}
{actionStatus && (
{
setShowActionStatusModal(false);
setActionStatus(null);
}}
success={actionStatus.success}
title={actionStatus.title}
message={actionStatus.message}
/>
)}
{/* Document Validation Error Modal */}
setDocumentError(prev => ({ ...prev, show: open }))}>
Document Upload Policy Violation
The following file(s) could not be uploaded due to policy violations:
{documentError.errors.map((error, index) => (
{error.fileName}
{error.reason}
))}
Document Policy:
Maximum file size: {documentPolicy.maxFileSizeMB}MB
Allowed file types: {documentPolicy.allowedFileTypes.join(', ')}
setDocumentError({ show: false, errors: [] })}
className="w-full sm:w-auto"
>
OK
>
);
}
/**
* RequestDetail Component (Exported)
*
* Purpose: Wrap RequestDetailInner with Error Boundary
*
* Benefits:
* - Prevents app crash if errors occur
* - Shows graceful error UI
* - Provides error recovery options
*/
export function RequestDetail(props: RequestDetailProps) {
return (
);
}