{formData.dealerCode}
@@ -248,15 +276,21 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
- {DEALERS.map((dealer) => (
-
-
- {dealer.code}
- •
- {dealer.name}
-
-
- ))}
+ {dealers.length === 0 && !loadingDealers ? (
+ No dealers available
+ ) : (
+ dealers
+ .filter((dealer) => dealer.dealerCode && dealer.dealerCode.trim() !== '')
+ .map((dealer) => (
+
+
+ {dealer.dealerCode}
+ •
+ {dealer.dealerName}
+
+
+ ))
+ )}
{formData.dealerCode && (
diff --git a/src/hooks/useRequestDetails.ts b/src/hooks/useRequestDetails.ts
index 391945d..460200d 100644
--- a/src/hooks/useRequestDetails.ts
+++ b/src/hooks/useRequestDetails.ts
@@ -1,5 +1,6 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import workflowApi, { getPauseDetails } from '@/services/workflowApi';
+import apiClient from '@/services/authApi';
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
import { getSocket } from '@/utils/socket';
@@ -229,6 +230,87 @@ export function useRequestDetails(
console.debug('Pause details not available:', error);
}
+ /**
+ * Fetch: Get claim details if this is a claim management request
+ */
+ let claimDetails = null;
+ let proposalDetails = null;
+ let completionDetails = null;
+ let internalOrder = null;
+
+ if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
+ try {
+ console.debug('[useRequestDetails] Fetching claim details for requestId:', wf.requestId);
+ const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
+ console.debug('[useRequestDetails] Claim API response:', {
+ status: claimResponse.status,
+ hasData: !!claimResponse.data,
+ dataKeys: claimResponse.data ? Object.keys(claimResponse.data) : [],
+ fullResponse: claimResponse.data,
+ });
+
+ const claimData = claimResponse.data?.data || claimResponse.data;
+ console.debug('[useRequestDetails] Extracted claimData:', {
+ hasClaimData: !!claimData,
+ claimDataKeys: claimData ? Object.keys(claimData) : [],
+ hasClaimDetails: !!(claimData?.claimDetails || claimData?.claim_details),
+ hasProposalDetails: !!(claimData?.proposalDetails || claimData?.proposal_details),
+ hasCompletionDetails: !!(claimData?.completionDetails || claimData?.completion_details),
+ hasInternalOrder: !!(claimData?.internalOrder || claimData?.internal_order),
+ });
+
+ if (claimData) {
+ claimDetails = claimData.claimDetails || claimData.claim_details;
+ proposalDetails = claimData.proposalDetails || claimData.proposal_details;
+ completionDetails = claimData.completionDetails || claimData.completion_details;
+ internalOrder = claimData.internalOrder || claimData.internal_order || null;
+ // New normalized tables
+ const budgetTracking = claimData.budgetTracking || claimData.budget_tracking || null;
+ const invoice = claimData.invoice || null;
+ const creditNote = claimData.creditNote || claimData.credit_note || null;
+ const completionExpenses = claimData.completionExpenses || claimData.completion_expenses || null;
+
+ // Store new fields in claimDetails for backward compatibility and easy access
+ if (claimDetails) {
+ (claimDetails as any).budgetTracking = budgetTracking;
+ (claimDetails as any).invoice = invoice;
+ (claimDetails as any).creditNote = creditNote;
+ (claimDetails as any).completionExpenses = completionExpenses;
+ }
+
+ console.debug('[useRequestDetails] Extracted details:', {
+ claimDetails: claimDetails ? {
+ hasActivityName: !!(claimDetails.activityName || claimDetails.activity_name),
+ hasActivityType: !!(claimDetails.activityType || claimDetails.activity_type),
+ hasLocation: !!(claimDetails.location),
+ activityName: claimDetails.activityName || claimDetails.activity_name,
+ activityType: claimDetails.activityType || claimDetails.activity_type,
+ location: claimDetails.location,
+ allKeys: Object.keys(claimDetails),
+ } : null,
+ hasProposalDetails: !!proposalDetails,
+ hasCompletionDetails: !!completionDetails,
+ hasInternalOrder: !!internalOrder,
+ hasBudgetTracking: !!budgetTracking,
+ hasInvoice: !!invoice,
+ hasCreditNote: !!creditNote,
+ hasCompletionExpenses: Array.isArray(completionExpenses) && completionExpenses.length > 0,
+ });
+ } else {
+ console.warn('[useRequestDetails] No claimData found in response');
+ }
+ } catch (error: any) {
+ // Claim details not available - request might not be fully initialized yet
+ console.error('[useRequestDetails] Error fetching claim details:', {
+ error: error?.message || error,
+ status: error?.response?.status,
+ statusText: error?.response?.statusText,
+ responseData: error?.response?.data,
+ requestId: wf.requestId,
+ });
+ }
+ }
+
/**
* Build: Complete request object with all transformed data
* This object is used throughout the UI
@@ -242,6 +324,7 @@ export function useRequestDetails(
description: wf.description,
status: statusMap(wf.status),
priority: (wf.priority || '').toString().toLowerCase(),
+ workflowType: wf.workflowType || (wf.templateType === 'claim-management' ? 'CLAIM_MANAGEMENT' : 'NON_TEMPLATIZED'),
approvalFlow,
approvals, // Raw approvals for SLA calculations
participants,
@@ -266,6 +349,16 @@ export function useRequestDetails(
conclusionRemark: wf.conclusionRemark || null,
closureDate: wf.closureDate || null,
pauseInfo: pauseInfo || null, // Include pause info for resume/retrigger buttons
+ // Claim management specific data
+ claimDetails: claimDetails || null,
+ proposalDetails: proposalDetails || null,
+ completionDetails: completionDetails || null,
+ internalOrder: internalOrder || null,
+ // New normalized tables (also available via claimDetails for backward compatibility)
+ budgetTracking: (claimDetails as any)?.budgetTracking || null,
+ invoice: (claimDetails as any)?.invoice || null,
+ creditNote: (claimDetails as any)?.creditNote || null,
+ completionExpenses: (claimDetails as any)?.completionExpenses || null,
};
setApiRequest(updatedRequest);
@@ -441,6 +534,49 @@ export function useRequestDetails(
console.debug('Pause details not available:', error);
}
+ /**
+ * Fetch: Get claim details if this is a claim management request
+ */
+ let claimDetails = null;
+ let proposalDetails = null;
+ let completionDetails = null;
+ let internalOrder = null;
+
+ if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
+ try {
+ console.debug('[useRequestDetails] Initial load - Fetching claim details for requestId:', wf.requestId);
+ const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
+ console.debug('[useRequestDetails] Initial load - Claim API response:', {
+ status: claimResponse.status,
+ hasData: !!claimResponse.data,
+ dataKeys: claimResponse.data ? Object.keys(claimResponse.data) : [],
+ });
+
+ const claimData = claimResponse.data?.data || claimResponse.data;
+ if (claimData) {
+ claimDetails = claimData.claimDetails || claimData.claim_details;
+ proposalDetails = claimData.proposalDetails || claimData.proposal_details;
+ completionDetails = claimData.completionDetails || claimData.completion_details;
+ internalOrder = claimData.internalOrder || claimData.internal_order || null;
+
+ console.debug('[useRequestDetails] Initial load - Extracted details:', {
+ hasClaimDetails: !!claimDetails,
+ claimDetailsKeys: claimDetails ? Object.keys(claimDetails) : [],
+ hasProposalDetails: !!proposalDetails,
+ hasCompletionDetails: !!completionDetails,
+ hasInternalOrder: !!internalOrder,
+ });
+ }
+ } catch (error: any) {
+ // Claim details not available - request might not be fully initialized yet
+ console.error('[useRequestDetails] Initial load - Error fetching claim details:', {
+ error: error?.message || error,
+ status: error?.response?.status,
+ requestId: wf.requestId,
+ });
+ }
+ }
+
// Build complete request object
const mapped = {
id: wf.requestNumber || wf.requestId,
@@ -449,6 +585,7 @@ export function useRequestDetails(
description: wf.description,
priority,
status: statusMap(wf.status),
+ workflowType: wf.workflowType || (wf.templateType === 'claim-management' ? 'CLAIM_MANAGEMENT' : 'NON_TEMPLATIZED'),
summary,
initiator: {
name: wf.initiator?.displayName || wf.initiator?.email,
@@ -472,6 +609,11 @@ export function useRequestDetails(
conclusionRemark: wf.conclusionRemark || null,
closureDate: wf.closureDate || null,
pauseInfo: pauseInfo || null,
+ // Claim management specific data
+ claimDetails: claimDetails || null,
+ proposalDetails: proposalDetails || null,
+ completionDetails: completionDetails || null,
+ internalOrder: internalOrder || null,
};
setApiRequest(mapped);
diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx
index 70eb9ce..1066f9f 100644
--- a/src/pages/RequestDetail/RequestDetail.tsx
+++ b/src/pages/RequestDetail/RequestDetail.tsx
@@ -26,6 +26,7 @@ import {
ShieldX,
RefreshCw,
ArrowLeft,
+ DollarSign,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
@@ -44,11 +45,15 @@ 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 { ClaimManagementOverviewTab } from './components/tabs/ClaimManagementOverviewTab';
import { WorkflowTab } from './components/tabs/WorkflowTab';
+import { DealerClaimWorkflowTab } from './components/tabs/DealerClaimWorkflowTab';
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 { IOTab } from './components/tabs/IOTab';
+import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
import { QuickActionsSidebar } from './components/QuickActionsSidebar';
import { RequestDetailModals } from './components/RequestDetailModals';
import { RequestDetailProps } from './types/requestDetail.types';
@@ -130,6 +135,74 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
accessDenied,
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
+ // Determine if user is initiator (from overview tab initiator info)
+ const currentUserId = (user as any)?.userId || '';
+ const currentUserEmail = (user as any)?.email?.toLowerCase() || '';
+ const initiatorUserId = apiRequest?.initiator?.userId;
+ const initiatorEmail = apiRequest?.initiator?.email?.toLowerCase();
+ const isUserInitiator = apiRequest?.initiator && (
+ (initiatorUserId && initiatorUserId === currentUserId) ||
+ (initiatorEmail && initiatorEmail === currentUserEmail)
+ );
+
+ // Determine if user is department lead (whoever is in step 3 / approval level 3)
+ const approvalLevels = apiRequest?.approvalLevels || [];
+ const step3Level = approvalLevels.find((level: any) =>
+ (level.levelNumber || level.level_number) === 3
+ );
+ const deptLeadUserId = step3Level?.approverId || step3Level?.approver?.userId;
+ const deptLeadEmail = (step3Level?.approverEmail || step3Level?.approver?.email || step3Level?.approverEmail || '').toLowerCase().trim();
+
+ // Check if user is department lead by userId or email (case-insensitive)
+ const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
+ (deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
+
+ // Get step 3 status (case-insensitive check)
+ const step3Status = step3Level?.status ? String(step3Level.status).toUpperCase() : '';
+ const isStep3PendingOrInProgress = step3Status === 'PENDING' ||
+ step3Status === 'IN_PROGRESS';
+
+ // Check if user is current approver for step 3 (can access IO tab when step is pending/in-progress)
+ // Also check if currentLevel is 3 (workflow is at step 3)
+ const currentLevel = apiRequest?.currentLevel || apiRequest?.current_level || 0;
+ const isStep3CurrentLevel = currentLevel === 3;
+
+ const isStep3CurrentApprover = step3Level && isStep3PendingOrInProgress && isStep3CurrentLevel && (
+ (deptLeadUserId && deptLeadUserId === currentUserId) ||
+ (deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail)
+ );
+
+ // Check if IO tab should be visible (for initiator and department lead in claim management requests)
+ // Department lead can access IO tab when they are the current approver for step 3 (to fetch and block IO)
+ const showIOTab = isClaimManagementRequest(apiRequest) &&
+ (isUserInitiator || isDeptLead || isStep3CurrentApprover);
+
+ // Debug logging for troubleshooting
+ console.debug('[RequestDetail] IO Tab visibility:', {
+ isClaimManagement: isClaimManagementRequest(apiRequest),
+ isUserInitiator,
+ isDeptLead,
+ isStep3CurrentApprover,
+ currentUserId,
+ currentUserEmail,
+ initiatorUserId,
+ initiatorEmail,
+ currentLevel,
+ isStep3CurrentLevel,
+ step3Level: step3Level ? {
+ levelNumber: step3Level.levelNumber || step3Level.level_number,
+ approverId: step3Level.approverId || step3Level.approver?.userId,
+ approverEmail: step3Level.approverEmail || step3Level.approver?.email,
+ status: step3Level.status,
+ statusUpper: step3Status,
+ isPendingOrInProgress: isStep3PendingOrInProgress
+ } : null,
+ deptLeadUserId,
+ deptLeadEmail,
+ emailMatch: deptLeadEmail && currentUserEmail ? deptLeadEmail === currentUserEmail : false,
+ showIOTab,
+ });
+
const {
mergedMessages,
unreadWorkNotes,
@@ -430,6 +503,16 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
Workflow
+ {showIOTab && (
+
+
+ IO
+
+ )}
-
+ {isClaimManagementRequest(apiRequest) ? (
+
+ ) : (
+
+ )}
{isClosed && (
@@ -502,22 +594,49 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
)}
- {
- if (!data.levelId) {
- alert('Level ID not available');
- return;
- }
- setSkipApproverData(data);
- setShowSkipApproverModal(true);
- }}
- onRefresh={refreshDetails}
- />
+ {isClaimManagementRequest(apiRequest) ? (
+ {
+ if (!data.levelId) {
+ alert('Level ID not available');
+ return;
+ }
+ setSkipApproverData(data);
+ setShowSkipApproverModal(true);
+ }}
+ onRefresh={refreshDetails}
+ />
+ ) : (
+ {
+ if (!data.levelId) {
+ alert('Level ID not available');
+ return;
+ }
+ setSkipApproverData(data);
+ setShowSkipApproverModal(true);
+ }}
+ onRefresh={refreshDetails}
+ />
+ )}
+ {showIOTab && (
+
+
+
+ )}
+
)}
diff --git a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
index 03804aa..c24cddf 100644
--- a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
+++ b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
@@ -2,7 +2,7 @@
* Quick Actions Sidebar Component
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
@@ -10,6 +10,9 @@ import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle }
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
import { useAuth } from '@/contexts/AuthContext';
import notificationApi, { type Notification } from '@/services/notificationApi';
+import { ProcessDetailsCard } from './claim-cards';
+import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
+import { determineUserRole, getRoleBasedVisibility, mapToClaimManagementRequest } from '@/utils/claimDataMapper';
interface QuickActionsSidebarProps {
request: any;
@@ -27,6 +30,8 @@ interface QuickActionsSidebarProps {
refreshTrigger?: number; // Trigger to refresh shared recipients list
pausedByUserId?: string; // User ID of the approver who paused (kept for backwards compatibility)
currentUserId?: string; // Current user's ID (kept for backwards compatibility)
+ apiRequest?: any;
+ onEditClaimAmount?: () => void;
}
export function QuickActionsSidebar({
@@ -43,6 +48,10 @@ export function QuickActionsSidebar({
onRetrigger,
summaryId,
refreshTrigger,
+ pausedByUserId: pausedByUserIdProp,
+ currentUserId: currentUserIdProp,
+ apiRequest,
+ onEditClaimAmount,
}: QuickActionsSidebarProps) {
const { user } = useAuth();
const [sharedRecipients, setSharedRecipients] = useState