Re_Figma_Code/src/dealer-claim/pages/RequestDetail.tsx

801 lines
32 KiB
TypeScript

/**
* Dealer Claim Request Detail Screen
*
* Standalone, dedicated request detail screen for Dealer Claim requests.
* This is a complete module that uses dealer claim specific components.
*
* LOCATION: src/dealer-claim/pages/RequestDetail.tsx
*
* IMPORTANT: This entire file and all its dependencies are in src/dealer-claim/ folder.
* Deleting src/dealer-claim/ folder removes ALL dealer claim related code.
*/
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,
DollarSign,
} 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 { useModalManager } from '@/hooks/useModalManager';
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
import { downloadDocument } from '@/services/workflowApi';
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
import { getSocket, joinUserRoom } from '@/utils/socket';
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
// Dealer Claim Components (import from index to get properly aliased exports)
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
// Shared Components
import { SharedComponents } from '@/shared/components';
const { DocumentsTab, ActivityTab, WorkNotesTab, SummaryTab, RequestDetailHeader, QuickActionsSidebar, RequestDetailModals } = SharedComponents;
// Other components
import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal';
import { getSummaryDetails, getSummaryByRequestId, type SummaryDetails } from '@/services/summaryApi';
import { toast } from 'sonner';
import { RequestDetailProps } from '@/pages/RequestDetail/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('Dealer Claim RequestDetail Error:', error, errorInfo);
}
override render() {
if (this.state.hasError) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-2">Error Loading Request</h2>
<p className="text-gray-600 mb-4">{this.state.error?.message || 'An unexpected error occurred'}</p>
<Button onClick={() => window.location.reload()} className="mr-2">
Reload Page
</Button>
<Button variant="outline" onClick={() => window.history.back()}>
Go Back
</Button>
</div>
</div>
);
}
return this.props.children;
}
}
/**
* Dealer Claim RequestDetailInner Component
*/
function DealerClaimRequestDetailInner({ 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<string | null>(null);
const [summaryDetails, setSummaryDetails] = useState<SummaryDetails | null>(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 [systemPolicy, setSystemPolicy] = useState<{
maxApprovalLevels: number;
maxParticipants: number;
allowSpectators: boolean;
maxSpectators: number;
}>({
maxApprovalLevels: 10,
maxParticipants: 50,
allowSpectators: true,
maxSpectators: 20
});
const [policyViolationModal, setPolicyViolationModal] = useState<{
open: boolean;
violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>;
}>({
open: false,
violations: []
});
const { user } = useAuth();
// Custom hooks
const {
request,
apiRequest,
loading: requestLoading,
refreshing,
refreshDetails,
currentApprovalLevel,
isSpectator,
isInitiator,
existingParticipants,
accessDenied,
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
// Determine if user is department lead (find dynamically by levelName, not hardcoded step number)
const currentUserId = (user as any)?.userId || '';
// IO tab visibility for dealer claims
// Show IO tab for initiator (Requestor Evaluation level) - initiator can now fetch and block IO
const showIOTab = isInitiator;
const {
mergedMessages,
unreadWorkNotes,
workNoteAttachments,
setWorkNoteAttachments,
} = useRequestSocket(requestIdentifier, apiRequest, activeTab, user);
const {
uploadingDocument,
triggerFileInput,
previewDocument,
setPreviewDocument,
documentPolicy,
documentError,
setDocumentError,
} = useDocumentUpload(apiRequest, refreshDetails);
// State to temporarily store approval level for modal (used for additional approvers)
const [temporaryApprovalLevel, setTemporaryApprovalLevel] = useState<any>(null);
// Use temporary level if set, otherwise use currentApprovalLevel
const effectiveApprovalLevel = temporaryApprovalLevel || currentApprovalLevel;
const {
showApproveModal,
setShowApproveModal,
showRejectModal,
setShowRejectModal,
showAddApproverModal,
setShowAddApproverModal,
showAddSpectatorModal,
setShowAddSpectatorModal,
showSkipApproverModal,
setShowSkipApproverModal,
showActionStatusModal,
setShowActionStatusModal,
skipApproverData,
setSkipApproverData,
actionStatus,
setActionStatus,
handleApproveConfirm: originalHandleApproveConfirm,
handleRejectConfirm: originalHandleRejectConfirm,
handleAddApprover,
handleSkipApprover,
handleAddSpectator,
} = useModalManager(requestIdentifier, effectiveApprovalLevel, refreshDetails);
// Wrapper handlers that clear temporary level after action
const handleApproveConfirm = async (description: string) => {
await originalHandleApproveConfirm(description);
setTemporaryApprovalLevel(null);
};
const handleRejectConfirm = async (description: string) => {
await originalHandleRejectConfirm(description);
setTemporaryApprovalLevel(null);
};
// Closure functionality - only for initiator when request is approved/rejected
// Check both lowercase and uppercase status values
const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator;
// Closure check completed
const {
conclusionRemark,
setConclusionRemark,
conclusionLoading,
conclusionSubmitting,
aiGenerated,
handleGenerateConclusion,
handleFinalizeConclusion,
generationAttempts,
generationFailed,
maxAttemptsReached,
} = useConclusionRemark(
request,
requestIdentifier,
isInitiator,
refreshDetails,
onBack,
setActionStatus,
setShowActionStatusModal
);
// Load system policy on mount
useEffect(() => {
const loadSystemPolicy = async () => {
try {
const systemSettingsConfigs = await getPublicConfigurations('SYSTEM_SETTINGS');
const workflowSharingConfigs = await getPublicConfigurations('WORKFLOW_SHARING');
const allConfigs = [...systemSettingsConfigs, ...workflowSharingConfigs];
const configMap: Record<string, string> = {};
allConfigs.forEach((c: AdminConfiguration) => {
configMap[c.configKey] = c.configValue;
});
setSystemPolicy({
maxApprovalLevels: parseInt(configMap['MAX_APPROVAL_LEVELS'] || '10'),
maxParticipants: parseInt(configMap['MAX_PARTICIPANTS_PER_REQUEST'] || '50'),
allowSpectators: configMap['ALLOW_ADD_SPECTATOR']?.toLowerCase() === 'true',
maxSpectators: parseInt(configMap['MAX_SPECTATORS_PER_REQUEST'] || '20')
});
} catch (error) {
console.error('Failed to load system policy:', error);
}
};
loadSystemPolicy();
}, []);
// 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 () => {
await refreshDetails();
};
const handleRetrigger = () => {
setShowRetriggerModal(true);
};
const handlePauseSuccess = async () => {
await refreshDetails();
};
const handleRetriggerSuccess = async () => {
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;
}
setShowShareSummaryModal(true);
};
const isClosed = request?.status === 'closed';
// Fetch summary details if request is closed
useEffect(() => {
const fetchSummaryDetails = async () => {
if (!isClosed || !apiRequest?.requestId) {
setSummaryDetails(null);
setSummaryId(null);
return;
}
try {
setLoadingSummary(true);
const summary = await getSummaryByRequestId(apiRequest.requestId);
if (summary?.summaryId) {
setSummaryId(summary.summaryId);
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 {
setSummaryDetails(null);
setSummaryId(null);
}
} catch (error: any) {
setSummaryDetails(null);
setSummaryId(null);
} finally {
setLoadingSummary(false);
}
};
fetchSummaryDetails();
}, [isClosed, apiRequest?.requestId]);
// Listen for credit note notifications and trigger silent refresh
useEffect(() => {
if (!currentUserId || !apiRequest?.requestId) return;
const socket = getSocket();
if (!socket) return;
joinUserRoom(socket, currentUserId);
const handleNewNotification = (data: { notification: any }) => {
const notif = data?.notification;
if (!notif) return;
const notifRequestId = notif.requestId || notif.request_id;
const notifRequestNumber = notif.metadata?.requestNumber || notif.metadata?.request_number;
if (notifRequestId !== apiRequest.requestId &&
notifRequestNumber !== requestIdentifier &&
notifRequestNumber !== apiRequest.requestNumber) return;
// Check for credit note metadata
if (notif.metadata?.creditNoteNumber || notif.metadata?.credit_note_number) {
refreshDetails();
}
};
socket.on('notification:new', handleNewNotification);
return () => {
socket.off('notification:new', handleNewNotification);
};
}, [currentUserId, apiRequest?.requestId, requestIdentifier, refreshDetails]);
// 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 (
<div className="flex items-center justify-center h-screen bg-gray-50" data-testid="loading-state">
<div className="text-center">
<RefreshCw className="w-12 h-12 text-blue-600 animate-spin mx-auto mb-4" />
<p className="text-gray-600">Loading dealer claim request details...</p>
</div>
</div>
);
}
// Access Denied state
if (accessDenied?.denied) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="access-denied-state">
<div className="max-w-lg w-full bg-white rounded-2xl shadow-xl p-8 text-center">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
<ShieldX className="w-10 h-10 text-red-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">Access Denied</h2>
<p className="text-gray-600 mb-6 leading-relaxed">
{accessDenied.message}
</p>
<div className="flex gap-3 justify-center">
<Button
variant="outline"
onClick={onBack || (() => window.history.back())}
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Go Back
</Button>
<Button
onClick={() => window.location.href = '/dashboard'}
className="bg-blue-600 hover:bg-blue-700"
>
Go to Dashboard
</Button>
</div>
</div>
</div>
);
}
// Not Found state
if (!request) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="not-found-state">
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 text-center">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
<FileText className="w-10 h-10 text-gray-400" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">Dealer Claim Request Not Found</h2>
<p className="text-gray-600 mb-6">
The dealer claim request you're looking for doesn't exist or may have been deleted.
</p>
<div className="flex gap-3 justify-center">
<Button
variant="outline"
onClick={onBack || (() => window.history.back())}
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Go Back
</Button>
<Button
onClick={() => window.location.href = '/dashboard'}
className="bg-blue-600 hover:bg-blue-700"
>
Go to Dashboard
</Button>
</div>
</div>
</div>
);
}
return (
<>
<div className="min-h-screen bg-gray-50" data-testid="dealer-claim-request-detail-page">
<div className="max-w-7xl mx-auto">
{/* Header Section */}
<RequestDetailHeader
request={request}
refreshing={refreshing}
onBack={onBack || (() => window.history.back())}
onRefresh={handleRefresh}
onShareSummary={handleShareSummary}
isInitiator={isInitiator}
// Dealer-claim module: Business logic for preparing SLA data
slaData={request?.summary?.sla || request?.sla || null}
isPaused={request?.pauseInfo?.isPaused || false}
/>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" data-testid="dealer-claim-request-detail-tabs">
<div className="mb-4 sm:mb-6">
<TabsList className="grid grid-cols-3 sm:grid-cols-6 lg:flex lg:flex-row h-auto bg-gray-100 p-1.5 sm:p-1 rounded-lg gap-1.5 sm:gap-1">
<TabsTrigger
value="overview"
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-overview"
>
<ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Overview</span>
</TabsTrigger>
{isClosed && summaryDetails && (
<TabsTrigger
value="summary"
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-summary"
>
<FileCheck className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Summary</span>
</TabsTrigger>
)}
<TabsTrigger
value="workflow"
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-workflow"
>
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Workflow</span>
</TabsTrigger>
{showIOTab && (
<TabsTrigger
value="io"
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-io"
>
<DollarSign className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">IO</span>
</TabsTrigger>
)}
<TabsTrigger
value="documents"
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-documents"
>
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Docs</span>
</TabsTrigger>
<TabsTrigger
value="activity"
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 col-span-1 sm:col-span-1"
data-testid="tab-activity"
>
<Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Activity</span>
</TabsTrigger>
<TabsTrigger
value="worknotes"
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 relative col-span-2 sm:col-span-1"
data-testid="tab-worknotes"
>
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Work Notes</span>
{unreadWorkNotes > 0 && (
<Badge
className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-[10px] flex items-center justify-center p-0"
data-testid="worknotes-unread-badge"
>
{unreadWorkNotes > 9 ? '9+' : unreadWorkNotes}
</Badge>
)}
</TabsTrigger>
</TabsList>
</div>
{/* Main Layout */}
<div className={activeTab === 'worknotes' ? '' : 'grid grid-cols-1 lg:grid-cols-3 gap-6'}>
{/* Left Column: Tab content */}
<div className={activeTab === 'worknotes' ? '' : 'lg:col-span-2'}>
<TabsContent value="overview" className="mt-0" data-testid="overview-tab-content">
<DealerClaimOverviewTab
request={request}
apiRequest={apiRequest}
currentUserId={currentUserId}
isInitiator={isInitiator}
needsClosure={needsClosure}
conclusionRemark={conclusionRemark}
setConclusionRemark={setConclusionRemark}
conclusionLoading={conclusionLoading}
conclusionSubmitting={conclusionSubmitting}
aiGenerated={aiGenerated}
handleGenerateConclusion={handleGenerateConclusion}
handleFinalizeConclusion={handleFinalizeConclusion}
generationAttempts={generationAttempts}
generationFailed={generationFailed}
maxAttemptsReached={maxAttemptsReached}
/>
</TabsContent>
{isClosed && (
<TabsContent value="summary" className="mt-0" data-testid="summary-tab-content">
<SummaryTab
summary={summaryDetails}
loading={loadingSummary}
onShare={handleShareSummary}
isInitiator={isInitiator}
/>
</TabsContent>
)}
<TabsContent value="workflow" className="mt-0">
<DealerClaimWorkflowTab
request={request}
user={user}
isInitiator={isInitiator}
onSkipApprover={(data) => {
if (!data.levelId) {
alert('Level ID not available');
return;
}
setSkipApproverData(data);
setShowSkipApproverModal(true);
}}
onRefresh={refreshDetails}
/>
</TabsContent>
{showIOTab && (
<TabsContent value="io" className="mt-0">
<IOTab
request={request}
apiRequest={apiRequest}
onRefresh={refreshDetails}
/>
</TabsContent>
)}
<TabsContent value="documents" className="mt-0">
<DocumentsTab
request={request}
workNoteAttachments={workNoteAttachments}
uploadingDocument={uploadingDocument}
documentPolicy={documentPolicy}
triggerFileInput={triggerFileInput}
setPreviewDocument={setPreviewDocument}
downloadDocument={downloadDocument}
/>
</TabsContent>
<TabsContent value="activity" className="mt-0">
<ActivityTab request={request} />
</TabsContent>
<TabsContent value="worknotes" className="mt-0" forceMount={true} hidden={activeTab !== 'worknotes'}>
<WorkNotesTab
requestId={requestIdentifier}
requestTitle={request.title}
mergedMessages={mergedMessages}
setWorkNoteAttachments={setWorkNoteAttachments}
isInitiator={isInitiator}
isSpectator={isSpectator}
currentLevels={currentLevels}
onAddApprover={handleAddApprover}
maxApprovalLevels={systemPolicy.maxApprovalLevels}
onPolicyViolation={(violations) => setPolicyViolationModal({ open: true, violations })}
/>
</TabsContent>
</div>
{/* Right Column: Quick Actions Sidebar */}
{activeTab !== 'worknotes' && (
<QuickActionsSidebar
request={request}
isInitiator={isInitiator}
isSpectator={isSpectator}
currentApprovalLevel={isClaimManagementRequest(apiRequest) ? null : currentApprovalLevel}
onAddApprover={() => 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={currentUserId}
apiRequest={apiRequest}
/>
)}
</div>
</Tabs>
</div>
</div>
{/* Share Summary Modal */}
{showShareSummaryModal && summaryId && (
<ShareSummaryModal
isOpen={showShareSummaryModal}
onClose={() => setShowShareSummaryModal(false)}
summaryId={summaryId}
requestTitle={request?.title || 'N/A'}
onSuccess={() => {
refreshDetails();
setSharedRecipientsRefreshTrigger(prev => prev + 1);
}}
/>
)}
{/* Pause Modals */}
{showPauseModal && apiRequest?.requestId && (
<PauseModal
isOpen={showPauseModal}
onClose={() => setShowPauseModal(false)}
requestId={apiRequest.requestId}
levelId={currentApprovalLevel?.levelId || null}
onSuccess={handlePauseSuccess}
/>
)}
{showResumeModal && apiRequest?.requestId && (
<ResumeModal
isOpen={showResumeModal}
onClose={() => setShowResumeModal(false)}
requestId={apiRequest.requestId}
onSuccess={handleResumeSuccess}
/>
)}
{showRetriggerModal && apiRequest?.requestId && (
<RetriggerPauseModal
isOpen={showRetriggerModal}
onClose={() => setShowRetriggerModal(false)}
requestId={apiRequest.requestId}
approverName={request?.pauseInfo?.pausedBy?.name}
onSuccess={handleRetriggerSuccess}
/>
)}
{/* Modals */}
<RequestDetailModals
showApproveModal={showApproveModal}
showRejectModal={showRejectModal}
showAddApproverModal={showAddApproverModal}
showAddSpectatorModal={showAddSpectatorModal}
showSkipApproverModal={showSkipApproverModal}
showActionStatusModal={showActionStatusModal}
previewDocument={previewDocument}
documentError={documentError}
request={request}
skipApproverData={skipApproverData}
actionStatus={actionStatus}
existingParticipants={existingParticipants}
currentLevels={currentLevels}
maxApprovalLevels={systemPolicy.maxApprovalLevels}
onPolicyViolation={(violations) => setPolicyViolationModal({ open: true, violations })}
setShowApproveModal={setShowApproveModal}
setShowRejectModal={setShowRejectModal}
setShowAddApproverModal={setShowAddApproverModal}
setShowAddSpectatorModal={setShowAddSpectatorModal}
setShowSkipApproverModal={setShowSkipApproverModal}
setShowActionStatusModal={setShowActionStatusModal}
setPreviewDocument={setPreviewDocument}
setDocumentError={setDocumentError}
setSkipApproverData={setSkipApproverData}
setActionStatus={setActionStatus}
handleApproveConfirm={handleApproveConfirm}
handleRejectConfirm={handleRejectConfirm}
handleAddApprover={handleAddApprover}
handleAddSpectator={handleAddSpectator}
handleSkipApprover={handleSkipApprover}
downloadDocument={downloadDocument}
documentPolicy={documentPolicy}
/>
{/* Policy Violation Modal */}
<PolicyViolationModal
open={policyViolationModal.open}
onClose={() => setPolicyViolationModal({ open: false, violations: [] })}
violations={policyViolationModal.violations}
policyDetails={{
maxApprovalLevels: systemPolicy.maxApprovalLevels,
maxParticipants: systemPolicy.maxParticipants,
allowSpectators: systemPolicy.allowSpectators,
maxSpectators: systemPolicy.maxSpectators,
}}
/>
</>
);
}
/**
* Dealer Claim RequestDetail Component (Exported)
*/
export function DealerClaimRequestDetail(props: RequestDetailProps) {
return (
<RequestDetailErrorBoundary>
<DealerClaimRequestDetailInner {...props} />
</RequestDetailErrorBoundary>
);
}