/** * RequestDetail Component * * Purpose: Display and manage detailed view of a workflow request * * Architecture: * - Uses custom hooks for complex logic (data fetching, socket, document upload, etc.) * - Delegates UI rendering to specialized tab components * - 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'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ClipboardList, TrendingUp, FileText, Activity, MessageSquare, AlertTriangle, FileCheck, ShieldX, RefreshCw, ArrowLeft, } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; // 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'; import { downloadDocument } from '@/services/workflowApi'; // Components import { RequestDetailHeader } from './components/RequestDetailHeader'; import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal'; import { getSummaryDetails, getSummaryByRequestId, type SummaryDetails } from '@/services/summaryApi'; import { toast } from 'sonner'; import { OverviewTab } from './components/tabs/OverviewTab'; import { WorkflowTab } from './components/tabs/WorkflowTab'; import { DocumentsTab } from './components/tabs/DocumentsTab'; import { ActivityTab } from './components/tabs/ActivityTab'; import { WorkNotesTab } from './components/tabs/WorkNotesTab'; import { SummaryTab } from './components/tabs/SummaryTab'; import { QuickActionsSidebar } from './components/QuickActionsSidebar'; import { RequestDetailModals } from './components/RequestDetailModals'; import { RequestDetailProps } from './types/requestDetail.types'; import { PauseModal } from '@/components/workflow/PauseModal'; import { ResumeModal } from '@/components/workflow/ResumeModal'; import { RetriggerPauseModal } from '@/components/workflow/RetriggerPauseModal'; /** * Error Boundary Component */ 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; } } /** * RequestDetailInner Component */ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests = [] }: RequestDetailProps) { const params = useParams<{ requestId: string }>(); const requestIdentifier = params.requestId || propRequestId || ''; const urlParams = new URLSearchParams(window.location.search); const initialTab = urlParams.get('tab') || 'overview'; const [activeTab, setActiveTab] = useState(initialTab); const [showShareSummaryModal, setShowShareSummaryModal] = useState(false); const [summaryId, setSummaryId] = useState(null); const [summaryDetails, setSummaryDetails] = useState(null); const [loadingSummary, setLoadingSummary] = useState(false); const [sharedRecipientsRefreshTrigger, setSharedRecipientsRefreshTrigger] = useState(0); const [showPauseModal, setShowPauseModal] = useState(false); const [showResumeModal, setShowResumeModal] = useState(false); const [showRetriggerModal, setShowRetriggerModal] = useState(false); const { user } = useAuth(); // Custom hooks const { request, apiRequest, loading: requestLoading, refreshing, refreshDetails, currentApprovalLevel, isSpectator, isInitiator, existingParticipants, accessDenied, } = useRequestDetails(requestIdentifier, dynamicRequests, user); const { mergedMessages, unreadWorkNotes, workNoteAttachments, setWorkNoteAttachments, } = useRequestSocket(requestIdentifier, apiRequest, activeTab, user); const { uploadingDocument, triggerFileInput, previewDocument, setPreviewDocument, documentPolicy, documentError, setDocumentError, } = useDocumentUpload(apiRequest, refreshDetails); 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); const { conclusionRemark, setConclusionRemark, conclusionLoading, conclusionSubmitting, aiGenerated, handleGenerateConclusion, handleFinalizeConclusion, } = useConclusionRemark(request, requestIdentifier, isInitiator, refreshDetails, onBack, setActionStatus, setShowActionStatusModal); // Auto-switch tab when URL query parameter changes useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const tabParam = urlParams.get('tab'); if (tabParam) { setActiveTab(tabParam); } }, [requestIdentifier]); const handleRefresh = () => { refreshDetails(); }; // Pause handlers const handlePause = () => { setShowPauseModal(true); }; const handleResume = () => { setShowResumeModal(true); }; const handleResumeSuccess = async () => { // Wait for refresh to complete to show updated status await refreshDetails(); }; const handleRetrigger = () => { setShowRetriggerModal(true); }; const handlePauseSuccess = async () => { // Wait for refresh to complete to show updated pause status await refreshDetails(); }; const handleRetriggerSuccess = async () => { // Wait for refresh to complete await refreshDetails(); }; const handleShareSummary = async () => { if (!apiRequest?.requestId) { toast.error('Request ID not found'); return; } if (!summaryId) { toast.error('Summary not available. Please ensure the request is closed and the summary has been generated.'); return; } // Open share modal with the existing summary ID // Summary should already exist from closure (auto-created by backend) setShowShareSummaryModal(true); }; const needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator; // Check if request is closed (or needs closure for approved/rejected) const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator); // Fetch summary details if request is closed // Summary is automatically created by backend when request is closed (on final approval) useEffect(() => { const fetchSummaryDetails = async () => { if (!isClosed || !apiRequest?.requestId) { setSummaryDetails(null); setSummaryId(null); return; } try { setLoadingSummary(true); // Just fetch the summary by requestId - don't try to create it // Summary is auto-created by backend on final approval/rejection const summary = await getSummaryByRequestId(apiRequest.requestId); if (summary?.summaryId) { setSummaryId(summary.summaryId); // Fetch full summary details try { const details = await getSummaryDetails(summary.summaryId); setSummaryDetails(details); } catch (error: any) { console.error('Failed to fetch summary details:', error); setSummaryDetails(null); setSummaryId(null); } } else { // Summary doesn't exist yet - this is normal if request just closed setSummaryDetails(null); setSummaryId(null); } } catch (error: any) { // Summary not found - this is OK, summary may not exist yet setSummaryDetails(null); setSummaryId(null); } finally { setLoadingSummary(false); } }; fetchSummaryDetails(); }, [isClosed, apiRequest?.requestId]); // Get current levels for WorkNotesTab const 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, })); // Loading state if (requestLoading && !request && !apiRequest) { return (

Loading request details...

); } // Access Denied state if (accessDenied?.denied) { return (

Access Denied

{accessDenied.message}

Who can access this request?

  • • The person who created this request (Initiator)
  • • Designated approvers at any level
  • • Added spectators or participants
  • • Organization administrators
); } // Not Found state if (!request) { return (

Request Not Found

The request you're looking for doesn't exist or may have been deleted.

); } return ( <>
{/* Header Section */} window.history.back())} onRefresh={handleRefresh} onShareSummary={handleShareSummary} isInitiator={isInitiator} /> {/* Tabs */}
Overview {isClosed && summaryDetails && ( Summary )} Workflow Docs Activity Work Notes {unreadWorkNotes > 0 && ( {unreadWorkNotes > 9 ? '9+' : unreadWorkNotes} )}
{/* Main Layout */}
{/* Left Column: Tab content */}
{isClosed && ( )} { if (!data.levelId) { alert('Level ID not available'); return; } setSkipApproverData(data); setShowSkipApproverModal(true); }} onRefresh={refreshDetails} />
{/* Right Column: Quick Actions Sidebar */} {activeTab !== 'worknotes' && ( setShowAddApproverModal(true)} onAddSpectator={() => setShowAddSpectatorModal(true)} onApprove={() => setShowApproveModal(true)} onReject={() => setShowRejectModal(true)} onPause={handlePause} onResume={handleResume} onRetrigger={handleRetrigger} summaryId={summaryId} refreshTrigger={sharedRecipientsRefreshTrigger} pausedByUserId={request?.pauseInfo?.pausedBy?.userId} currentUserId={(user as any)?.userId} /> )}
{/* Share Summary Modal */} {showShareSummaryModal && summaryId && ( setShowShareSummaryModal(false)} summaryId={summaryId} requestTitle={request?.title || 'N/A'} onSuccess={() => { refreshDetails(); // Trigger refresh of shared recipients list setSharedRecipientsRefreshTrigger(prev => prev + 1); }} /> )} {/* Pause Modals */} {showPauseModal && apiRequest?.requestId && ( setShowPauseModal(false)} requestId={apiRequest.requestId} levelId={currentApprovalLevel?.levelId || null} onSuccess={handlePauseSuccess} /> )} {showResumeModal && apiRequest?.requestId && ( setShowResumeModal(false)} requestId={apiRequest.requestId} onSuccess={handleResumeSuccess} /> )} {showRetriggerModal && apiRequest?.requestId && ( setShowRetriggerModal(false)} requestId={apiRequest.requestId} approverName={request?.pauseInfo?.pausedBy?.name} onSuccess={handleRetriggerSuccess} /> )} {/* Modals */} ); } /** * RequestDetail Component (Exported) */ export function RequestDetail(props: RequestDetailProps) { return ( ); }