759 lines
31 KiB
TypeScript
759 lines
31 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';
|
||
import { TokenManager } from '@/utils/tokenManager';
|
||
|
||
// Custom Request Components (import from index to get properly aliased exports)
|
||
import { CustomOverviewTab, CustomWorkflowTab } from '../index';
|
||
import { Form16OverviewTab } from '../components/request-detail/Form16OverviewTab';
|
||
import { Form16WorkflowTab } from '../components/request-detail/Form16WorkflowTab';
|
||
import { Form16QuickActions } from '../components/request-detail/Form16QuickActions';
|
||
|
||
// 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,
|
||
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 needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator;
|
||
const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
|
||
const isForm16Request = (request?.templateType || request?.template_type || '').toString().toUpperCase() === 'FORM_16';
|
||
|
||
useEffect(() => {
|
||
if (isForm16Request && activeTab === 'worknotes') setActiveTab('overview');
|
||
}, [isForm16Request, activeTab]);
|
||
|
||
// 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>
|
||
);
|
||
}
|
||
|
||
const isReUser = (user as any)?.role === 'MANAGEMENT' || (user as any)?.role === 'ADMIN';
|
||
// Quick Actions and Spectators are for RE users / employees / admins only; hide for dealers
|
||
const isDealer = (TokenManager.getUserData() as any)?.jobTitle === 'Dealer';
|
||
|
||
return (
|
||
<>
|
||
<div className="min-h-screen bg-gray-50" data-testid="custom-request-detail-page">
|
||
<div className="max-w-7xl mx-auto">
|
||
{isForm16Request && (
|
||
<p className="text-sm text-emerald-700 font-medium mb-2" data-testid="form16-details-heading">Form 16 Details</p>
|
||
)}
|
||
{/* 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">{isForm16Request ? 'Form 16' : '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>
|
||
{!isForm16Request && (
|
||
<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' && !isForm16Request ? '' : 'grid grid-cols-1 lg:grid-cols-3 gap-6'}>
|
||
{/* Left Column: Tab content */}
|
||
<div className={activeTab === 'worknotes' && !isForm16Request ? '' : 'lg:col-span-2'}>
|
||
<TabsContent value="overview" className="mt-0" data-testid="overview-tab-content">
|
||
{isForm16Request ? (
|
||
// Form 16: dedicated overview (prevents duplicate sections below the nav bar)
|
||
<Form16OverviewTab request={request} />
|
||
) : (
|
||
<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}
|
||
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">
|
||
{isForm16Request ? (
|
||
<Form16WorkflowTab
|
||
request={request}
|
||
requestId={apiRequest?.requestId || requestIdentifier}
|
||
isReUser={isReUser}
|
||
onRefresh={refreshDetails}
|
||
/>
|
||
) : (
|
||
<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>
|
||
|
||
{!isForm16Request && (
|
||
<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 – RE users / employees / admins only; hidden for dealers */}
|
||
{!isDealer && activeTab !== 'worknotes' && (
|
||
<div className="space-y-4 sm:space-y-6">
|
||
{/* Form 16 RE actions – Quick Actions section (Form 16 only, no change to shared workflow) */}
|
||
{isForm16Request && isReUser && (
|
||
<Form16QuickActions
|
||
requestId={apiRequest?.requestId || requestIdentifier}
|
||
request={request}
|
||
onRefresh={refreshDetails}
|
||
/>
|
||
)}
|
||
<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}
|
||
hideApproveReject={isDealer}
|
||
/>
|
||
</div>
|
||
)}
|
||
</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>
|
||
);
|
||
}
|