708 lines
28 KiB
TypeScript
708 lines
28 KiB
TypeScript
/**
|
|
* Custom Request Detail Screen
|
|
*
|
|
* Standalone, dedicated request detail screen for Custom requests.
|
|
* This is a complete module that uses custom request specific components.
|
|
*
|
|
* LOCATION: src/custom/pages/RequestDetail.tsx
|
|
*
|
|
* IMPORTANT: This entire file and all its dependencies are in src/custom/ folder.
|
|
* Deleting src/custom/ folder removes ALL custom request 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,
|
|
} 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';
|
|
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
|
|
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
|
|
|
|
// Custom Request Components (import from index to get properly aliased exports)
|
|
import { CustomOverviewTab, CustomWorkflowTab } from '../index';
|
|
|
|
// Shared Components (from src/shared/)
|
|
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('Custom 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Custom RequestDetailInner Component
|
|
*/
|
|
function CustomRequestDetailInner({ 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);
|
|
|
|
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);
|
|
|
|
// 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 needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator;
|
|
const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
|
|
|
|
// 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]);
|
|
|
|
// 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 custom 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">Custom Request Not Found</h2>
|
|
<p className="text-gray-600 mb-6">
|
|
The custom 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="custom-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}
|
|
// Custom 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="custom-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>
|
|
<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">
|
|
<CustomOverviewTab
|
|
request={request}
|
|
isInitiator={isInitiator}
|
|
needsClosure={needsClosure}
|
|
conclusionRemark={conclusionRemark}
|
|
setConclusionRemark={setConclusionRemark}
|
|
conclusionLoading={conclusionLoading}
|
|
conclusionSubmitting={conclusionSubmitting}
|
|
aiGenerated={aiGenerated}
|
|
handleGenerateConclusion={handleGenerateConclusion}
|
|
handleFinalizeConclusion={handleFinalizeConclusion}
|
|
onPause={handlePause}
|
|
onResume={handleResume}
|
|
onRetrigger={handleRetrigger}
|
|
currentUserIsApprover={!!currentApprovalLevel}
|
|
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
|
currentUserId={(user as any)?.userId}
|
|
/>
|
|
</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">
|
|
<CustomWorkflowTab
|
|
request={request}
|
|
user={user}
|
|
isInitiator={isInitiator}
|
|
onSkipApprover={(data) => {
|
|
if (!data.levelId) {
|
|
alert('Level ID not available');
|
|
return;
|
|
}
|
|
setSkipApproverData(data);
|
|
setShowSkipApproverModal(true);
|
|
}}
|
|
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={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={(user as any)?.userId}
|
|
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,
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Custom RequestDetail Component (Exported)
|
|
*/
|
|
export function CustomRequestDetail(props: RequestDetailProps) {
|
|
return (
|
|
<RequestDetailErrorBoundary>
|
|
<CustomRequestDetailInner {...props} />
|
|
</RequestDetailErrorBoundary>
|
|
);
|
|
}
|