{formData.dealerCode}
@@ -248,15 +276,19 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
- {DEALERS.map((dealer) => (
-
-
- {dealer.code}
- •
- {dealer.name}
-
-
- ))}
+ {dealers.length === 0 && !loadingDealers ? (
+ No dealers available
+ ) : (
+ dealers.map((dealer) => (
+
+
+ {dealer.dealerCode}
+ •
+ {dealer.dealerName}
+
+
+ ))
+ )}
{formData.dealerCode && (
diff --git a/src/hooks/useRequestDetails.ts b/src/hooks/useRequestDetails.ts
index 391945d..062c7c1 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,66 @@ 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;
+
+ 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),
+ });
+
+ if (claimData) {
+ claimDetails = claimData.claimDetails || claimData.claim_details;
+ proposalDetails = claimData.proposalDetails || claimData.proposal_details;
+ completionDetails = claimData.completionDetails || claimData.completion_details;
+
+ 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,
+ });
+ } 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 +303,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 +328,10 @@ 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,
};
setApiRequest(updatedRequest);
@@ -441,6 +507,46 @@ 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;
+
+ 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;
+
+ console.debug('[useRequestDetails] Initial load - Extracted details:', {
+ hasClaimDetails: !!claimDetails,
+ claimDetailsKeys: claimDetails ? Object.keys(claimDetails) : [],
+ hasProposalDetails: !!proposalDetails,
+ hasCompletionDetails: !!completionDetails,
+ });
+ }
+ } 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 +555,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 +579,10 @@ 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,
};
setApiRequest(mapped);
diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx
index 70eb9ce..7c0e6c3 100644
--- a/src/pages/RequestDetail/RequestDetail.tsx
+++ b/src/pages/RequestDetail/RequestDetail.tsx
@@ -44,11 +44,14 @@ 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 { isClaimManagementRequest } from '@/utils/claimRequestUtils';
import { QuickActionsSidebar } from './components/QuickActionsSidebar';
import { RequestDetailModals } from './components/RequestDetailModals';
import { RequestDetailProps } from './types/requestDetail.types';
@@ -470,24 +473,33 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
{/* Left Column: Tab content */}
-
+ {isClaimManagementRequest(apiRequest) ? (
+
+ ) : (
+
+ )}
{isClosed && (
@@ -502,20 +514,37 @@ 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}
+ />
+ )}
diff --git a/src/pages/RequestDetail/components/claim-cards/ActivityInformationCard.tsx b/src/pages/RequestDetail/components/claim-cards/ActivityInformationCard.tsx
new file mode 100644
index 0000000..274f11e
--- /dev/null
+++ b/src/pages/RequestDetail/components/claim-cards/ActivityInformationCard.tsx
@@ -0,0 +1,177 @@
+/**
+ * ActivityInformationCard Component
+ * Displays activity details for Claim Management requests
+ */
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Calendar, MapPin, DollarSign, Receipt } from 'lucide-react';
+import { ClaimActivityInfo } from '../../types/claimManagement.types';
+import { format } from 'date-fns';
+
+interface ActivityInformationCardProps {
+ activityInfo: ClaimActivityInfo;
+ className?: string;
+}
+
+export function ActivityInformationCard({ activityInfo, className }: ActivityInformationCardProps) {
+ // Defensive check: Ensure activityInfo exists
+ if (!activityInfo) {
+ console.warn('[ActivityInformationCard] activityInfo is missing');
+ return (
+
+
+ Activity information not available
+
+
+ );
+ }
+
+ const formatCurrency = (amount: string | number) => {
+ const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
+ if (isNaN(numAmount)) return 'N/A';
+ return `₹${numAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ };
+
+ const formatDate = (dateString?: string) => {
+ if (!dateString) return 'N/A';
+ try {
+ return format(new Date(dateString), 'MMM d, yyyy');
+ } catch {
+ return dateString;
+ }
+ };
+
+ return (
+
+
+
+
+ Activity Information
+
+
+
+
+ {/* Activity Name */}
+
+
+
+ {activityInfo.activityName}
+
+
+
+ {/* Activity Type */}
+
+
+
+ {activityInfo.activityType}
+
+
+
+ {/* Location */}
+
+
+
+
+ {activityInfo.location}
+
+
+
+ {/* Requested Date */}
+
+
+
+ {formatDate(activityInfo.requestedDate)}
+
+
+
+ {/* Estimated Budget */}
+
+
+
+
+ {activityInfo.estimatedBudget
+ ? formatCurrency(activityInfo.estimatedBudget)
+ : 'TBD'}
+
+
+
+ {/* Closed Expenses */}
+ {activityInfo.closedExpenses !== undefined && (
+
+
+
+
+ {formatCurrency(activityInfo.closedExpenses)}
+
+
+ )}
+
+ {/* Period */}
+ {activityInfo.period && (
+
+
+
+ {formatDate(activityInfo.period.startDate)} - {formatDate(activityInfo.period.endDate)}
+
+
+ )}
+
+
+ {/* Closed Expenses Breakdown */}
+ {activityInfo.closedExpensesBreakdown && activityInfo.closedExpensesBreakdown.length > 0 && (
+
+
+
+ {activityInfo.closedExpensesBreakdown.map((item, index) => (
+
+ {item.description}
+
+ {formatCurrency(item.amount)}
+
+
+ ))}
+
+ Total
+
+ {formatCurrency(
+ activityInfo.closedExpensesBreakdown.reduce((sum, item) => sum + item.amount, 0)
+ )}
+
+
+
+
+ )}
+
+ {/* Description */}
+ {activityInfo.description && (
+
+
+
+ {activityInfo.description}
+
+
+ )}
+
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/claim-cards/DealerInformationCard.tsx b/src/pages/RequestDetail/components/claim-cards/DealerInformationCard.tsx
new file mode 100644
index 0000000..d6e5538
--- /dev/null
+++ b/src/pages/RequestDetail/components/claim-cards/DealerInformationCard.tsx
@@ -0,0 +1,100 @@
+/**
+ * DealerInformationCard Component
+ * Displays dealer details for Claim Management requests
+ */
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Building, Mail, Phone, MapPin } from 'lucide-react';
+import { DealerInfo } from '../../types/claimManagement.types';
+
+interface DealerInformationCardProps {
+ dealerInfo: DealerInfo;
+ className?: string;
+}
+
+export function DealerInformationCard({ dealerInfo, className }: DealerInformationCardProps) {
+ // Defensive check: Ensure dealerInfo exists
+ if (!dealerInfo) {
+ console.warn('[DealerInformationCard] dealerInfo is missing');
+ return (
+
+
+ Dealer information not available
+
+
+ );
+ }
+
+ // Check if essential fields are present
+ if (!dealerInfo.dealerCode && !dealerInfo.dealerName) {
+ console.warn('[DealerInformationCard] Dealer info missing essential fields:', dealerInfo);
+ return (
+
+
+ Dealer information incomplete
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Dealer Information
+
+
+
+ {/* Dealer Code and Name */}
+
+
+
+
+ {dealerInfo.dealerCode}
+
+
+
+
+
+
+ {dealerInfo.dealerName}
+
+
+
+
+ {/* Contact Information */}
+
+
+
+ {/* Email */}
+
+
+ {dealerInfo.email}
+
+
+ {/* Phone */}
+
+
+ {/* Address */}
+
+
+ {dealerInfo.address}
+
+
+
+
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/claim-cards/ProcessDetailsCard.tsx b/src/pages/RequestDetail/components/claim-cards/ProcessDetailsCard.tsx
new file mode 100644
index 0000000..4bd2885
--- /dev/null
+++ b/src/pages/RequestDetail/components/claim-cards/ProcessDetailsCard.tsx
@@ -0,0 +1,259 @@
+/**
+ * ProcessDetailsCard Component
+ * Displays process-related details: IO Number, DMS Number, Claim Amount, and Budget Breakdowns
+ * Visibility controlled by user role
+ */
+
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import { Activity, Receipt, DollarSign, Pen } from 'lucide-react';
+import {
+ IODetails,
+ DMSDetails,
+ ClaimAmountDetails,
+ CostBreakdownItem,
+ RoleBasedVisibility,
+} from '../../types/claimManagement.types';
+import { format } from 'date-fns';
+
+interface ProcessDetailsCardProps {
+ ioDetails?: IODetails;
+ dmsDetails?: DMSDetails;
+ claimAmount?: ClaimAmountDetails;
+ estimatedBudgetBreakdown?: CostBreakdownItem[];
+ closedExpensesBreakdown?: CostBreakdownItem[];
+ visibility: RoleBasedVisibility;
+ onEditClaimAmount?: () => void;
+ className?: string;
+}
+
+export function ProcessDetailsCard({
+ ioDetails,
+ dmsDetails,
+ claimAmount,
+ estimatedBudgetBreakdown,
+ closedExpensesBreakdown,
+ visibility,
+ onEditClaimAmount,
+ className,
+}: ProcessDetailsCardProps) {
+ const formatCurrency = (amount: number) => {
+ return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ };
+
+ const formatDate = (dateString: string) => {
+ try {
+ return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
+ } catch {
+ return dateString;
+ }
+ };
+
+ const calculateTotal = (items: CostBreakdownItem[]) => {
+ return items.reduce((sum, item) => sum + item.amount, 0);
+ };
+
+ // Don't render if nothing to show
+ const hasContent =
+ (visibility.showIODetails && ioDetails) ||
+ (visibility.showDMSDetails && dmsDetails) ||
+ (visibility.showClaimAmount && claimAmount) ||
+ estimatedBudgetBreakdown ||
+ closedExpensesBreakdown;
+
+ if (!hasContent) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ Process Details
+
+ Workflow reference numbers
+
+
+ {/* IO Details - Only visible to internal RE users */}
+ {visibility.showIODetails && ioDetails && (
+
+
+
+
+
+
{ioDetails.ioNumber}
+
+ {ioDetails.remarks && (
+
+
Remark:
+
{ioDetails.remarks}
+
+ )}
+
+ {/* Budget Details */}
+ {(ioDetails.availableBalance !== undefined || ioDetails.blockedAmount !== undefined) && (
+
+ {ioDetails.availableBalance !== undefined && (
+
+ Available Balance:
+
+ {formatCurrency(ioDetails.availableBalance)}
+
+
+ )}
+ {ioDetails.blockedAmount !== undefined && (
+
+ Blocked Amount:
+
+ {formatCurrency(ioDetails.blockedAmount)}
+
+
+ )}
+ {ioDetails.remainingBalance !== undefined && (
+
+ Remaining Balance:
+
+ {formatCurrency(ioDetails.remainingBalance)}
+
+
+ )}
+
+ )}
+
+
+
By {ioDetails.blockedByName}
+
{formatDate(ioDetails.blockedAt)}
+
+
+ )}
+
+ {/* DMS Details */}
+ {visibility.showDMSDetails && dmsDetails && (
+
+
+
{dmsDetails.dmsNumber}
+
+ {dmsDetails.remarks && (
+
+
Remarks:
+
{dmsDetails.remarks}
+
+ )}
+
+
+
By {dmsDetails.createdByName}
+
{formatDate(dmsDetails.createdAt)}
+
+
+ )}
+
+ {/* Claim Amount */}
+ {visibility.showClaimAmount && claimAmount && (
+
+
+
+
+
+
+ {visibility.canEditClaimAmount && onEditClaimAmount && (
+
+ )}
+
+
+ {formatCurrency(claimAmount.amount)}
+
+ {claimAmount.lastUpdatedBy && (
+
+
+ Last updated by {claimAmount.lastUpdatedBy}
+
+ {claimAmount.lastUpdatedAt && (
+
+ {formatDate(claimAmount.lastUpdatedAt)}
+
+ )}
+
+ )}
+
+ )}
+
+ {/* Estimated Budget Breakdown */}
+ {estimatedBudgetBreakdown && estimatedBudgetBreakdown.length > 0 && (
+
+
+
+
+
+
+ {estimatedBudgetBreakdown.map((item, index) => (
+
+ {item.description}
+
+ {formatCurrency(item.amount)}
+
+
+ ))}
+
+ Total
+
+ {formatCurrency(calculateTotal(estimatedBudgetBreakdown))}
+
+
+
+
+ )}
+
+ {/* Closed Expenses Breakdown */}
+ {closedExpensesBreakdown && closedExpensesBreakdown.length > 0 && (
+
+
+
+
+
+
+ {closedExpensesBreakdown.map((item, index) => (
+
+ {item.description}
+
+ {formatCurrency(item.amount)}
+
+
+ ))}
+
+ Total
+
+ {formatCurrency(calculateTotal(closedExpensesBreakdown))}
+
+
+
+
+ )}
+
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/claim-cards/ProposalDetailsCard.tsx b/src/pages/RequestDetail/components/claim-cards/ProposalDetailsCard.tsx
new file mode 100644
index 0000000..8c5a0d4
--- /dev/null
+++ b/src/pages/RequestDetail/components/claim-cards/ProposalDetailsCard.tsx
@@ -0,0 +1,123 @@
+/**
+ * ProposalDetailsCard Component
+ * Displays proposal details submitted by dealer for Claim Management requests
+ */
+
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Receipt, Calendar } from 'lucide-react';
+import { ProposalDetails } from '../../types/claimManagement.types';
+import { format } from 'date-fns';
+
+interface ProposalDetailsCardProps {
+ proposalDetails: ProposalDetails;
+ className?: string;
+}
+
+export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
+ const formatCurrency = (amount: number) => {
+ return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ };
+
+ const formatDate = (dateString: string) => {
+ try {
+ return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
+ } catch {
+ return dateString;
+ }
+ };
+
+ const formatTimelineDate = (dateString: string) => {
+ try {
+ return format(new Date(dateString), 'MMM d, yyyy');
+ } catch {
+ return dateString;
+ }
+ };
+
+ return (
+
+
+
+
+ Proposal Details
+
+ {proposalDetails.submittedOn && (
+
+ Submitted on {formatDate(proposalDetails.submittedOn)}
+
+ )}
+
+
+ {/* Cost Breakup */}
+
+
+
+
+
+
+ |
+ Item Description
+ |
+
+ Amount
+ |
+
+
+
+ {proposalDetails.costBreakup.map((item, index) => (
+
+ |
+ {item.description}
+ |
+
+ {formatCurrency(item.amount)}
+ |
+
+ ))}
+
+ |
+ Estimated Budget (Total)
+ |
+
+ {formatCurrency(proposalDetails.estimatedBudgetTotal)}
+ |
+
+
+
+
+
+
+ {/* Timeline for Closure */}
+
+
+
+
+
+
+ Expected completion by: {formatTimelineDate(proposalDetails.timelineForClosure)}
+
+
+
+
+
+ {/* Dealer Comments */}
+ {proposalDetails.dealerComments && (
+
+
+
+ {proposalDetails.dealerComments}
+
+
+ )}
+
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/claim-cards/RequestInitiatorCard.tsx b/src/pages/RequestDetail/components/claim-cards/RequestInitiatorCard.tsx
new file mode 100644
index 0000000..d31030e
--- /dev/null
+++ b/src/pages/RequestDetail/components/claim-cards/RequestInitiatorCard.tsx
@@ -0,0 +1,77 @@
+/**
+ * RequestInitiatorCard Component
+ * Displays initiator/requester details - can be used for both claim management and regular workflows
+ */
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+import { Mail, Phone } from 'lucide-react';
+
+interface InitiatorInfo {
+ name: string;
+ role?: string;
+ department?: string;
+ email: string;
+ phone?: string;
+}
+
+interface RequestInitiatorCardProps {
+ initiatorInfo: InitiatorInfo;
+ className?: string;
+}
+
+export function RequestInitiatorCard({ initiatorInfo, className }: RequestInitiatorCardProps) {
+ // Generate initials from name
+ const getInitials = (name: string) => {
+ return name
+ .split(' ')
+ .map((n) => n[0])
+ .join('')
+ .toUpperCase()
+ .slice(0, 2);
+ };
+
+ return (
+
+
+ Request Initiator
+
+
+
+
+
+ {getInitials(initiatorInfo.name)}
+
+
+
+
{initiatorInfo.name}
+ {initiatorInfo.role && (
+
{initiatorInfo.role}
+ )}
+ {initiatorInfo.department && (
+
{initiatorInfo.department}
+ )}
+
+
+ {/* Email */}
+
+
+ {initiatorInfo.email}
+
+
+ {/* Phone */}
+ {initiatorInfo.phone && (
+
+
+
{initiatorInfo.phone}
+
+ )}
+
+
+
+
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/claim-cards/index.ts b/src/pages/RequestDetail/components/claim-cards/index.ts
new file mode 100644
index 0000000..a13bef2
--- /dev/null
+++ b/src/pages/RequestDetail/components/claim-cards/index.ts
@@ -0,0 +1,12 @@
+/**
+ * Claim Management Card Components
+ * Re-export all claim-specific card components for easy imports
+ */
+
+export { ActivityInformationCard } from './ActivityInformationCard';
+export { DealerInformationCard } from './DealerInformationCard';
+export { ProposalDetailsCard } from './ProposalDetailsCard';
+export { ProcessDetailsCard } from './ProcessDetailsCard';
+export { RequestInitiatorCard } from './RequestInitiatorCard';
+
+
diff --git a/src/pages/RequestDetail/components/modals/DealerProposalSubmissionModal.tsx b/src/pages/RequestDetail/components/modals/DealerProposalSubmissionModal.tsx
new file mode 100644
index 0000000..9b994c5
--- /dev/null
+++ b/src/pages/RequestDetail/components/modals/DealerProposalSubmissionModal.tsx
@@ -0,0 +1,485 @@
+/**
+ * DealerProposalSubmissionModal Component
+ * Modal for Step 1: Dealer Proposal Submission
+ * Allows dealers to upload proposal documents, provide cost breakdown, timeline, and comments
+ */
+
+import { useState, useRef, useMemo } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import { Badge } from '@/components/ui/badge';
+import { Upload, Plus, X, Calendar, DollarSign, CircleAlert } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface CostItem {
+ id: string;
+ description: string;
+ amount: number;
+}
+
+interface DealerProposalSubmissionModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSubmit: (data: {
+ proposalDocument: File | null;
+ costBreakup: CostItem[];
+ expectedCompletionDate: string;
+ otherDocuments: File[];
+ dealerComments: string;
+ }) => Promise;
+ dealerName?: string;
+ activityName?: string;
+ requestId?: string;
+}
+
+export function DealerProposalSubmissionModal({
+ isOpen,
+ onClose,
+ onSubmit,
+ dealerName = 'Jaipur Royal Enfield',
+ activityName = 'Activity',
+ requestId,
+}: DealerProposalSubmissionModalProps) {
+ const [proposalDocument, setProposalDocument] = useState(null);
+ const [costItems, setCostItems] = useState([
+ { id: '1', description: '', amount: 0 },
+ ]);
+ const [timelineMode, setTimelineMode] = useState<'date' | 'days'>('date');
+ const [expectedCompletionDate, setExpectedCompletionDate] = useState('');
+ const [numberOfDays, setNumberOfDays] = useState('');
+ const [otherDocuments, setOtherDocuments] = useState([]);
+ const [dealerComments, setDealerComments] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+
+ const proposalDocInputRef = useRef(null);
+ const otherDocsInputRef = useRef(null);
+
+ // Calculate total estimated budget
+ const totalBudget = useMemo(() => {
+ return costItems.reduce((sum, item) => sum + (item.amount || 0), 0);
+ }, [costItems]);
+
+ // Check if all required fields are filled
+ const isFormValid = useMemo(() => {
+ const hasProposalDoc = proposalDocument !== null;
+ const hasValidCostItems = costItems.length > 0 &&
+ costItems.every(item => item.description.trim() !== '' && item.amount > 0);
+ const hasTimeline = timelineMode === 'date'
+ ? expectedCompletionDate !== ''
+ : numberOfDays !== '' && parseInt(numberOfDays) > 0;
+ const hasComments = dealerComments.trim().length > 0;
+
+ return hasProposalDoc && hasValidCostItems && hasTimeline && hasComments;
+ }, [proposalDocument, costItems, timelineMode, expectedCompletionDate, numberOfDays, dealerComments]);
+
+ const handleProposalDocChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate file type
+ const allowedTypes = ['.pdf', '.doc', '.docx'];
+ const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
+ if (!allowedTypes.includes(fileExtension)) {
+ toast.error('Please upload a PDF, DOC, or DOCX file');
+ return;
+ }
+ setProposalDocument(file);
+ }
+ };
+
+ const handleOtherDocsChange = (e: React.ChangeEvent) => {
+ const files = Array.from(e.target.files || []);
+ setOtherDocuments(prev => [...prev, ...files]);
+ };
+
+ const handleAddCostItem = () => {
+ setCostItems(prev => [
+ ...prev,
+ { id: Date.now().toString(), description: '', amount: 0 },
+ ]);
+ };
+
+ const handleRemoveCostItem = (id: string) => {
+ if (costItems.length > 1) {
+ setCostItems(prev => prev.filter(item => item.id !== id));
+ }
+ };
+
+ const handleCostItemChange = (id: string, field: 'description' | 'amount', value: string | number) => {
+ setCostItems(prev =>
+ prev.map(item =>
+ item.id === id
+ ? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) || 0 : value }
+ : item
+ )
+ );
+ };
+
+ const handleRemoveOtherDoc = (index: number) => {
+ setOtherDocuments(prev => prev.filter((_, i) => i !== index));
+ };
+
+ const handleSubmit = async () => {
+ if (!isFormValid) {
+ toast.error('Please fill all required fields');
+ return;
+ }
+
+ // Calculate final completion date if using days mode
+ let finalCompletionDate = expectedCompletionDate;
+ if (timelineMode === 'days' && numberOfDays) {
+ const days = parseInt(numberOfDays);
+ const date = new Date();
+ date.setDate(date.getDate() + days);
+ finalCompletionDate = date.toISOString().split('T')[0];
+ }
+
+ try {
+ setSubmitting(true);
+ await onSubmit({
+ proposalDocument,
+ costBreakup: costItems.filter(item => item.description.trim() !== '' && item.amount > 0),
+ expectedCompletionDate: finalCompletionDate,
+ otherDocuments,
+ dealerComments,
+ });
+ handleReset();
+ onClose();
+ } catch (error) {
+ console.error('Failed to submit proposal:', error);
+ toast.error('Failed to submit proposal. Please try again.');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleReset = () => {
+ setProposalDocument(null);
+ setCostItems([{ id: '1', description: '', amount: 0 }]);
+ setTimelineMode('date');
+ setExpectedCompletionDate('');
+ setNumberOfDays('');
+ setOtherDocuments([]);
+ setDealerComments('');
+ if (proposalDocInputRef.current) proposalDocInputRef.current.value = '';
+ if (otherDocsInputRef.current) otherDocsInputRef.current.value = '';
+ };
+
+ const handleClose = () => {
+ if (!submitting) {
+ handleReset();
+ onClose();
+ }
+ };
+
+ // Get minimum date (today)
+ const minDate = new Date().toISOString().split('T')[0];
+
+ return (
+
+ );
+}
+
diff --git a/src/pages/RequestDetail/components/modals/DeptLeadIOApprovalModal.tsx b/src/pages/RequestDetail/components/modals/DeptLeadIOApprovalModal.tsx
new file mode 100644
index 0000000..a6cbb42
--- /dev/null
+++ b/src/pages/RequestDetail/components/modals/DeptLeadIOApprovalModal.tsx
@@ -0,0 +1,304 @@
+/**
+ * DeptLeadIOApprovalModal Component
+ * Modal for Step 3: Dept Lead Approval and IO Organization
+ * Allows department lead to approve request and organize IO details
+ */
+
+import { useState, useMemo } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import { Badge } from '@/components/ui/badge';
+import { CircleCheckBig, CircleX, Receipt, TriangleAlert } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface DeptLeadIOApprovalModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onApprove: (data: {
+ ioNumber: string;
+ ioRemark: string;
+ comments: string;
+ }) => Promise;
+ onReject: (comments: string) => Promise;
+ requestTitle?: string;
+ requestId?: string;
+}
+
+export function DeptLeadIOApprovalModal({
+ isOpen,
+ onClose,
+ onApprove,
+ onReject,
+ requestTitle,
+ requestId,
+}: DeptLeadIOApprovalModalProps) {
+ const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
+ const [ioNumber, setIoNumber] = useState('');
+ const [ioRemark, setIoRemark] = useState('');
+ const [comments, setComments] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+
+ const ioRemarkChars = ioRemark.length;
+ const commentsChars = comments.length;
+ const maxIoRemarkChars = 300;
+ const maxCommentsChars = 500;
+
+ // Validate form
+ const isFormValid = useMemo(() => {
+ if (actionType === 'reject') {
+ return comments.trim().length > 0;
+ }
+ // For approve, need IO number, IO remark, and comments
+ return (
+ ioNumber.trim().length > 0 &&
+ ioRemark.trim().length > 0 &&
+ comments.trim().length > 0
+ );
+ }, [actionType, ioNumber, ioRemark, comments]);
+
+ const handleSubmit = async () => {
+ if (!isFormValid) {
+ if (actionType === 'approve') {
+ if (!ioNumber.trim()) {
+ toast.error('Please enter IO number');
+ return;
+ }
+ if (!ioRemark.trim()) {
+ toast.error('Please enter IO remark');
+ return;
+ }
+ }
+ if (!comments.trim()) {
+ toast.error('Please provide comments');
+ return;
+ }
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+
+ if (actionType === 'approve') {
+ await onApprove({
+ ioNumber: ioNumber.trim(),
+ ioRemark: ioRemark.trim(),
+ comments: comments.trim(),
+ });
+ } else {
+ await onReject(comments.trim());
+ }
+
+ handleReset();
+ onClose();
+ } catch (error) {
+ console.error(`Failed to ${actionType} request:`, error);
+ toast.error(`Failed to ${actionType} request. Please try again.`);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleReset = () => {
+ setActionType('approve');
+ setIoNumber('');
+ setIoRemark('');
+ setComments('');
+ };
+
+ const handleClose = () => {
+ if (!submitting) {
+ handleReset();
+ onClose();
+ }
+ };
+
+ return (
+
+ );
+}
+
diff --git a/src/pages/RequestDetail/components/modals/EditClaimAmountModal.tsx b/src/pages/RequestDetail/components/modals/EditClaimAmountModal.tsx
new file mode 100644
index 0000000..03dda18
--- /dev/null
+++ b/src/pages/RequestDetail/components/modals/EditClaimAmountModal.tsx
@@ -0,0 +1,196 @@
+/**
+ * EditClaimAmountModal Component
+ * Modal for editing claim amount (restricted by role)
+ */
+
+import { useState } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { DollarSign, AlertCircle } from 'lucide-react';
+import { toast } from 'sonner';
+
+interface EditClaimAmountModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ currentAmount: number;
+ onSubmit: (newAmount: number) => Promise;
+ currency?: string;
+}
+
+export function EditClaimAmountModal({
+ isOpen,
+ onClose,
+ currentAmount,
+ onSubmit,
+ currency = '₹',
+}: EditClaimAmountModalProps) {
+ const [amount, setAmount] = useState(currentAmount.toString());
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState('');
+
+ const formatCurrency = (value: number) => {
+ return `${currency}${value.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ };
+
+ const handleAmountChange = (value: string) => {
+ // Remove non-numeric characters except decimal point
+ const cleaned = value.replace(/[^\d.]/g, '');
+
+ // Ensure only one decimal point
+ const parts = cleaned.split('.');
+ if (parts.length > 2) {
+ return;
+ }
+
+ setAmount(cleaned);
+ setError('');
+ };
+
+ const handleSubmit = async () => {
+ // Validate amount
+ const numAmount = parseFloat(amount);
+
+ if (isNaN(numAmount) || numAmount <= 0) {
+ setError('Please enter a valid amount greater than 0');
+ return;
+ }
+
+ if (numAmount === currentAmount) {
+ toast.info('Amount is unchanged');
+ onClose();
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ await onSubmit(numAmount);
+ onClose();
+ } catch (error) {
+ console.error('Failed to update claim amount:', error);
+ setError('Failed to update claim amount. Please try again.');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleClose = () => {
+ if (!submitting) {
+ setAmount(currentAmount.toString());
+ setError('');
+ onClose();
+ }
+ };
+
+ return (
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/modals/InitiatorProposalApprovalModal.tsx b/src/pages/RequestDetail/components/modals/InitiatorProposalApprovalModal.tsx
new file mode 100644
index 0000000..9d99807
--- /dev/null
+++ b/src/pages/RequestDetail/components/modals/InitiatorProposalApprovalModal.tsx
@@ -0,0 +1,443 @@
+/**
+ * InitiatorProposalApprovalModal Component
+ * Modal for Step 2: Requestor Evaluation & Confirmation
+ * Allows initiator to review dealer's proposal and approve/reject
+ */
+
+import { useState, useMemo } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Textarea } from '@/components/ui/textarea';
+import { Badge } from '@/components/ui/badge';
+import {
+ CheckCircle,
+ XCircle,
+ FileText,
+ DollarSign,
+ Calendar,
+ MessageSquare,
+ Download,
+ Eye,
+} from 'lucide-react';
+import { toast } from 'sonner';
+
+interface CostItem {
+ id: string;
+ description: string;
+ amount: number;
+}
+
+interface ProposalData {
+ proposalDocument?: {
+ name: string;
+ url?: string;
+ id?: string;
+ };
+ costBreakup: CostItem[];
+ expectedCompletionDate: string;
+ otherDocuments?: Array<{
+ name: string;
+ url?: string;
+ id?: string;
+ }>;
+ dealerComments: string;
+ submittedAt?: string;
+}
+
+interface InitiatorProposalApprovalModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onApprove: (comments: string) => Promise;
+ onReject: (comments: string) => Promise;
+ proposalData: ProposalData | null;
+ dealerName?: string;
+ activityName?: string;
+ requestId?: string;
+}
+
+export function InitiatorProposalApprovalModal({
+ isOpen,
+ onClose,
+ onApprove,
+ onReject,
+ proposalData,
+ dealerName = 'Dealer',
+ activityName = 'Activity',
+ requestId: _requestId, // Prefix with _ to indicate intentionally unused
+}: InitiatorProposalApprovalModalProps) {
+ const [comments, setComments] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+ const [actionType, setActionType] = useState<'approve' | 'reject' | null>(null);
+
+ // Calculate total budget
+ const totalBudget = useMemo(() => {
+ if (!proposalData?.costBreakup) return 0;
+
+ // Ensure costBreakup is an array
+ const costBreakup = Array.isArray(proposalData.costBreakup)
+ ? proposalData.costBreakup
+ : (typeof proposalData.costBreakup === 'string'
+ ? JSON.parse(proposalData.costBreakup)
+ : []);
+
+ if (!Array.isArray(costBreakup)) return 0;
+
+ return costBreakup.reduce((sum: number, item: any) => {
+ const amount = typeof item === 'object' ? (item.amount || 0) : 0;
+ return sum + (Number(amount) || 0);
+ }, 0);
+ }, [proposalData]);
+
+ // Format date
+ const formatDate = (dateString: string) => {
+ if (!dateString) return '—';
+ try {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-IN', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ } catch {
+ return dateString;
+ }
+ };
+
+ const handleApprove = async () => {
+ if (!comments.trim()) {
+ toast.error('Please provide approval comments');
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ setActionType('approve');
+ await onApprove(comments);
+ handleReset();
+ onClose();
+ } catch (error) {
+ console.error('Failed to approve proposal:', error);
+ toast.error('Failed to approve proposal. Please try again.');
+ } finally {
+ setSubmitting(false);
+ setActionType(null);
+ }
+ };
+
+ const handleReject = async () => {
+ if (!comments.trim()) {
+ toast.error('Please provide rejection reason');
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ setActionType('reject');
+ await onReject(comments);
+ handleReset();
+ onClose();
+ } catch (error) {
+ console.error('Failed to reject proposal:', error);
+ toast.error('Failed to reject proposal. Please try again.');
+ } finally {
+ setSubmitting(false);
+ setActionType(null);
+ }
+ };
+
+ const handleReset = () => {
+ setComments('');
+ setActionType(null);
+ };
+
+ const handleClose = () => {
+ if (!submitting) {
+ handleReset();
+ onClose();
+ }
+ };
+
+ // Don't return null - show modal even if proposalData is not loaded yet
+ // This allows the modal to open and show a loading/empty state
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
diff --git a/src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx b/src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx
new file mode 100644
index 0000000..18d002d
--- /dev/null
+++ b/src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx
@@ -0,0 +1,184 @@
+/**
+ * ClaimManagementOverviewTab Component
+ * Specialized overview tab for Claim Management requests
+ * Uses modular card components for flexible rendering based on role and request state
+ */
+
+import { useState } from 'react';
+import {
+ ActivityInformationCard,
+ DealerInformationCard,
+ ProposalDetailsCard,
+ ProcessDetailsCard,
+ RequestInitiatorCard,
+} from '../claim-cards';
+import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
+import {
+ mapToClaimManagementRequest,
+ determineUserRole,
+ getRoleBasedVisibility,
+ type ClaimManagementRequest,
+ type RequestRole,
+} from '@/utils/claimDataMapper';
+
+interface ClaimManagementOverviewTabProps {
+ request: any; // Original request object
+ apiRequest: any; // API request data
+ currentUserId: string;
+ isInitiator: boolean;
+ onEditClaimAmount?: () => void;
+ className?: string;
+}
+
+export function ClaimManagementOverviewTab({
+ request,
+ apiRequest,
+ currentUserId,
+ isInitiator,
+ onEditClaimAmount,
+ className = '',
+}: ClaimManagementOverviewTabProps) {
+ // Check if this is a claim management request
+ if (!isClaimManagementRequest(apiRequest)) {
+ return (
+
+
This is not a claim management request.
+
+ );
+ }
+
+ // Map API data to claim management structure
+ const claimRequest = mapToClaimManagementRequest(apiRequest, currentUserId);
+
+ if (!claimRequest) {
+ console.warn('[ClaimManagementOverviewTab] Failed to map claim data:', {
+ apiRequest,
+ hasClaimDetails: !!apiRequest?.claimDetails,
+ hasProposalDetails: !!apiRequest?.proposalDetails,
+ hasCompletionDetails: !!apiRequest?.completionDetails,
+ });
+ return (
+
+
Unable to load claim management data.
+
Please ensure the request has been properly initialized.
+
+ );
+ }
+
+ // Debug: Log mapped data for troubleshooting
+ console.debug('[ClaimManagementOverviewTab] Mapped claim data:', {
+ activityInfo: claimRequest.activityInfo,
+ dealerInfo: claimRequest.dealerInfo,
+ hasProposalDetails: !!claimRequest.proposalDetails,
+ });
+
+ // Determine user's role
+ const userRole: RequestRole = determineUserRole(apiRequest, currentUserId);
+
+ // Get visibility settings based on role
+ const visibility = getRoleBasedVisibility(userRole);
+
+ console.debug('[ClaimManagementOverviewTab] User role and visibility:', {
+ userRole,
+ visibility,
+ currentUserId,
+ });
+
+ // Extract initiator info from request
+ const initiatorInfo = {
+ name: apiRequest.requestedBy?.name || apiRequest.createdByName || 'Unknown',
+ role: 'initiator',
+ department: apiRequest.requestedBy?.department || apiRequest.department || '',
+ email: apiRequest.requestedBy?.email || 'N/A',
+ phone: apiRequest.requestedBy?.phone || apiRequest.requestedBy?.mobile,
+ };
+
+ return (
+
+ {/* Left Column: Main Information Cards */}
+
+ {/* Activity Information - Always visible */}
+
+
+ {/* Dealer Information - Visible based on role */}
+ {visibility.showDealerInfo && (
+
+ )}
+
+ {/* Proposal Details - Only shown after dealer submits proposal */}
+ {visibility.showProposalDetails && claimRequest.proposalDetails && (
+
+ )}
+
+ {/* Request Initiator */}
+
+
+
+ {/* Right Column: Process Details Sidebar */}
+
+
+ );
+}
+
+/**
+ * Wrapper component that decides whether to show claim management or regular overview
+ */
+interface AdaptiveOverviewTabProps {
+ request: any;
+ apiRequest: any;
+ currentUserId: string;
+ isInitiator: boolean;
+ onEditClaimAmount?: () => void;
+ // Props for regular overview tab
+ regularOverviewComponent?: React.ComponentType;
+ regularOverviewProps?: any;
+}
+
+export function AdaptiveOverviewTab({
+ request,
+ apiRequest,
+ currentUserId,
+ isInitiator,
+ onEditClaimAmount,
+ regularOverviewComponent: RegularOverview,
+ regularOverviewProps,
+}: AdaptiveOverviewTabProps) {
+ // Determine if this is a claim management request
+ const isClaim = isClaimManagementRequest(apiRequest);
+
+ if (isClaim) {
+ return (
+
+ );
+ }
+
+ // Render regular overview if provided
+ if (RegularOverview) {
+ return ;
+ }
+
+ // Fallback
+ return (
+
+
No overview available for this request type.
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/tabs/ClaimManagementWorkflowTab.tsx b/src/pages/RequestDetail/components/tabs/ClaimManagementWorkflowTab.tsx
new file mode 100644
index 0000000..e07f2b8
--- /dev/null
+++ b/src/pages/RequestDetail/components/tabs/ClaimManagementWorkflowTab.tsx
@@ -0,0 +1,311 @@
+/**
+ * ClaimManagementWorkflowTab Component
+ * Displays the 8-step workflow process specific to Claim Management requests
+ */
+
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import {
+ TrendingUp,
+ CircleCheckBig,
+ Clock,
+ Mail,
+ Download,
+ Receipt,
+ Activity,
+ AlertCircle,
+} from 'lucide-react';
+import { format } from 'date-fns';
+
+interface WorkflowStep {
+ stepNumber: number;
+ stepName: string;
+ stepDescription: string;
+ assignedTo: string;
+ assignedToType: 'dealer' | 'requestor' | 'department_lead' | 'finance' | 'system';
+ status: 'pending' | 'in_progress' | 'approved' | 'rejected' | 'skipped';
+ tatHours: number;
+ elapsedHours?: number;
+ remarks?: string;
+ approvedAt?: string;
+ approvedBy?: string;
+ ioDetails?: {
+ ioNumber: string;
+ ioRemarks: string;
+ organisedBy: string;
+ organisedAt: string;
+ };
+ dmsDetails?: {
+ dmsNumber: string;
+ dmsRemarks: string;
+ pushedBy: string;
+ pushedAt: string;
+ };
+ hasEmailNotification?: boolean;
+ hasDownload?: boolean;
+ downloadUrl?: string;
+}
+
+interface ClaimManagementWorkflowTabProps {
+ steps: WorkflowStep[];
+ currentStep: number;
+ totalSteps?: number;
+ onViewEmailTemplate?: (stepNumber: number) => void;
+ onDownloadDocument?: (stepNumber: number, url: string) => void;
+ className?: string;
+}
+
+export function ClaimManagementWorkflowTab({
+ steps,
+ currentStep,
+ totalSteps = 8,
+ onViewEmailTemplate,
+ onDownloadDocument,
+ className = '',
+}: ClaimManagementWorkflowTabProps) {
+
+ const formatDate = (dateString?: string) => {
+ if (!dateString) return 'N/A';
+ try {
+ return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
+ } catch {
+ return dateString;
+ }
+ };
+
+ const getStepBorderColor = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return 'border-green-500 bg-green-50';
+ case 'in_progress':
+ return 'border-blue-500 bg-blue-50';
+ case 'rejected':
+ return 'border-red-500 bg-red-50';
+ case 'pending':
+ return 'border-gray-300 bg-white';
+ case 'skipped':
+ return 'border-gray-400 bg-gray-50';
+ default:
+ return 'border-gray-300 bg-white';
+ }
+ };
+
+ const getStepIconBg = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return 'bg-green-100';
+ case 'in_progress':
+ return 'bg-blue-100';
+ case 'rejected':
+ return 'bg-red-100';
+ case 'pending':
+ return 'bg-gray-100';
+ case 'skipped':
+ return 'bg-gray-200';
+ default:
+ return 'bg-gray-100';
+ }
+ };
+
+ const getStepIcon = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return ;
+ case 'in_progress':
+ return ;
+ case 'rejected':
+ return ;
+ case 'pending':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusBadgeColor = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return 'bg-green-100 text-green-800 border-green-200';
+ case 'in_progress':
+ return 'bg-blue-100 text-blue-800 border-blue-200';
+ case 'rejected':
+ return 'bg-red-100 text-red-800 border-red-200';
+ case 'pending':
+ return 'bg-gray-100 text-gray-600 border-gray-200';
+ case 'skipped':
+ return 'bg-gray-200 text-gray-700 border-gray-300';
+ default:
+ return 'bg-gray-100 text-gray-600 border-gray-200';
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Claim Management Workflow
+
+
+ 8-Step approval process for dealer claim management
+
+
+
+ Step {currentStep} of {totalSteps}
+
+
+
+
+
+ {steps.map((step, index) => (
+
+ {/* Step Content */}
+
+ {/* Icon */}
+
+ {getStepIcon(step.status)}
+
+
+ {/* Step Details */}
+
+ {/* Header Row */}
+
+
+
+
+ Step {step.stepNumber}: {step.stepName}
+
+
+ {step.status}
+
+
+ {/* Action Buttons */}
+ {step.hasEmailNotification && onViewEmailTemplate && (
+
+ )}
+
+ {step.hasDownload && onDownloadDocument && step.downloadUrl && (
+
+ )}
+
+
+
{step.assignedTo}
+
+ {step.stepDescription}
+
+
+
+ {/* TAT Info */}
+
+
TAT: {step.tatHours}h
+ {step.elapsedHours !== undefined && (
+
+ Elapsed: {step.elapsedHours}h
+
+ )}
+
+
+
+ {/* Remarks */}
+ {step.remarks && (
+
+ )}
+
+ {/* IO Details */}
+ {step.ioDetails && (
+
+
+
+
+ IO Organisation Details
+
+
+
+
+ IO Number:
+
+ {step.ioDetails.ioNumber}
+
+
+
+
IO Remark:
+
{step.ioDetails.ioRemarks}
+
+
+ Organised by {step.ioDetails.organisedBy} on{' '}
+ {formatDate(step.ioDetails.organisedAt)}
+
+
+
+ )}
+
+ {/* DMS Details */}
+ {step.dmsDetails && (
+
+
+
+
+ DMS Processing Details
+
+
+
+
+ DMS Number:
+
+ {step.dmsDetails.dmsNumber}
+
+
+
+
DMS Remarks:
+
{step.dmsDetails.dmsRemarks}
+
+
+ Pushed by {step.dmsDetails.pushedBy} on{' '}
+ {formatDate(step.dmsDetails.pushedAt)}
+
+
+
+ )}
+
+ {/* Approval Timestamp */}
+ {step.approvedAt && (
+
+ {step.status === 'approved' ? 'Approved' : 'Updated'} on{' '}
+ {formatDate(step.approvedAt)}
+
+ )}
+
+
+
+ ))}
+
+
+
+ );
+}
+
+
diff --git a/src/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab.tsx b/src/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab.tsx
new file mode 100644
index 0000000..1b94a1c
--- /dev/null
+++ b/src/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab.tsx
@@ -0,0 +1,950 @@
+/**
+ * Dealer Claim Workflow Tab Component
+ *
+ * Purpose: Specialized workflow view for dealer claim management
+ * Features:
+ * - 8-step approval process visualization
+ * - Action buttons for document uploads, IO organization, DMS processing
+ * - Special sections for IO details and DMS details
+ * - Dealer-specific workflow steps
+ */
+
+import { useState, useMemo, useEffect } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { TrendingUp, Clock, CheckCircle, Upload, Mail, Download, Receipt, Activity } from 'lucide-react';
+import { formatDateTime } from '@/utils/dateFormatter';
+import { DealerProposalSubmissionModal } from '../modals/DealerProposalSubmissionModal';
+import { InitiatorProposalApprovalModal } from '../modals/InitiatorProposalApprovalModal';
+import { DeptLeadIOApprovalModal } from '../modals/DeptLeadIOApprovalModal';
+import { toast } from 'sonner';
+import { submitProposal, updateIODetails } from '@/services/dealerClaimApi';
+import { getWorkflowDetails, approveLevel, rejectLevel, updateWorkflow } from '@/services/workflowApi';
+import { uploadDocument } from '@/services/documentApi';
+import { createWorkNoteMultipart } from '@/services/workflowApi';
+
+interface DealerClaimWorkflowTabProps {
+ request: any;
+ user: any;
+ isInitiator: boolean;
+ onSkipApprover?: (data: any) => void;
+ onRefresh?: () => void;
+}
+
+interface WorkflowStep {
+ step: number;
+ title: string;
+ approver: string;
+ description: string;
+ tatHours: number;
+ status: 'pending' | 'approved' | 'waiting' | 'rejected';
+ comment?: string;
+ approvedAt?: string;
+ elapsedHours?: number;
+ // Special fields for dealer claims
+ ioDetails?: {
+ ioNumber: string;
+ ioRemark: string;
+ organizedBy: string;
+ organizedAt: string;
+ };
+ dmsDetails?: {
+ dmsNumber: string;
+ dmsRemarks: string;
+ pushedBy: string;
+ pushedAt: string;
+ };
+ einvoiceUrl?: string;
+ emailTemplateUrl?: string;
+}
+
+/**
+ * Safe date formatter with fallback
+ */
+const formatDateSafe = (dateString: string | undefined | null): string => {
+ if (!dateString) return '';
+ try {
+ return formatDateTime(dateString);
+ } catch (error) {
+ // Fallback to simple date format
+ try {
+ return new Date(dateString).toLocaleString('en-IN', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: true,
+ });
+ } catch {
+ return dateString;
+ }
+ }
+};
+
+/**
+ * Get step icon based on status
+ */
+const getStepIcon = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return ;
+ case 'pending':
+ return ;
+ case 'rejected':
+ return ;
+ default:
+ return ;
+ }
+};
+
+/**
+ * Get step badge variant
+ */
+const getStepBadgeVariant = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return 'bg-green-100 text-green-800 border-green-200';
+ case 'pending':
+ return 'bg-purple-100 text-purple-800 border-purple-200';
+ case 'rejected':
+ return 'bg-red-100 text-red-800 border-red-200';
+ default:
+ return 'bg-gray-100 text-gray-800 border-gray-200';
+ }
+};
+
+/**
+ * Get step card styling
+ */
+const getStepCardStyle = (status: string, isActive: boolean) => {
+ if (isActive && status === 'pending') {
+ return 'border-purple-500 bg-purple-50 shadow-md';
+ }
+ if (status === 'approved') {
+ return 'border-green-500 bg-green-50';
+ }
+ if (status === 'rejected') {
+ return 'border-red-500 bg-red-50';
+ }
+ return 'border-gray-200 bg-white';
+};
+
+/**
+ * Get step icon background
+ */
+const getStepIconBg = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return 'bg-green-100';
+ case 'pending':
+ return 'bg-purple-100';
+ case 'rejected':
+ return 'bg-red-100';
+ default:
+ return 'bg-gray-100';
+ }
+};
+
+export function DealerClaimWorkflowTab({
+ request,
+ user,
+ isInitiator,
+ onSkipApprover,
+ onRefresh
+}: DealerClaimWorkflowTabProps) {
+ const [showProposalModal, setShowProposalModal] = useState(false);
+ const [showApprovalModal, setShowApprovalModal] = useState(false);
+ const [showIOApprovalModal, setShowIOApprovalModal] = useState(false);
+
+ // Load approval flows from real API
+ const [approvalFlow, setApprovalFlow] = useState([]);
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
+
+ // Reload approval flows whenever request changes or after refresh
+ useEffect(() => {
+ const loadApprovalFlows = async () => {
+ // First check if request has approvalFlow
+ if (request?.approvalFlow && request.approvalFlow.length > 0) {
+ setApprovalFlow(request.approvalFlow);
+ return;
+ }
+
+ // Load from real API
+ if (request?.id || request?.requestId) {
+ const requestId = request.id || request.requestId;
+ try {
+ const details = await getWorkflowDetails(requestId);
+ const approvals = details?.approvalLevels || details?.approvals || [];
+ if (approvals && approvals.length > 0) {
+ // Transform approval levels to match expected format
+ const flows = approvals.map((level: any) => ({
+ step: level.levelNumber || level.level_number || 0,
+ approver: level.approverName || level.approver_name || '',
+ status: level.status?.toLowerCase() || 'waiting',
+ tatHours: level.tatHours || level.tat_hours || 24,
+ elapsedHours: level.elapsedHours || level.elapsed_hours,
+ approvedAt: level.actionDate || level.action_date,
+ comment: level.comments || level.comment,
+ levelId: level.levelId || level.level_id,
+ }));
+ setApprovalFlow(flows);
+ }
+ } catch (error) {
+ console.warn('Failed to load approval flows from API:', error);
+ }
+ }
+ };
+
+ loadApprovalFlows();
+ }, [request, refreshTrigger]);
+
+ // Also reload when request.currentStep changes
+ useEffect(() => {
+ if (request?.id || request?.requestId) {
+ const requestId = request.id || request.requestId;
+ const loadApprovalFlows = async () => {
+ try {
+ const details = await getWorkflowDetails(requestId);
+ const approvals = details?.approvalLevels || details?.approvals || [];
+ if (approvals && approvals.length > 0) {
+ const flows = approvals.map((level: any) => ({
+ step: level.levelNumber || level.level_number || 0,
+ approver: level.approverName || level.approver_name || '',
+ status: level.status?.toLowerCase() || 'waiting',
+ tatHours: level.tatHours || level.tat_hours || 24,
+ elapsedHours: level.elapsedHours || level.elapsed_hours,
+ approvedAt: level.actionDate || level.action_date,
+ comment: level.comments || level.comment,
+ levelId: level.levelId || level.level_id,
+ }));
+ setApprovalFlow(flows);
+ }
+ } catch (error) {
+ console.warn('Failed to load approval flows from API:', error);
+ }
+ };
+ loadApprovalFlows();
+ }
+ }, [request?.currentStep]);
+
+ // Enhanced refresh handler that also reloads approval flows
+ const handleRefresh = () => {
+ setRefreshTrigger(prev => prev + 1);
+ onRefresh?.();
+ };
+
+ // Transform approval flow to dealer claim workflow steps
+ const workflowSteps: WorkflowStep[] = approvalFlow.map((step: any, index: number) => {
+ const stepTitles = [
+ 'Dealer - Proposal Submission',
+ 'Requestor Evaluation & Confirmation',
+ 'Dept Lead Approval',
+ 'Activity Creation',
+ 'Dealer - Completion Documents',
+ 'Requestor - Claim Approval',
+ 'E-Invoice Generation',
+ 'Credit Note from SAP',
+ ];
+
+ const stepDescriptions = [
+ 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
+ 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
+ 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
+ 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.',
+ 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
+ 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
+ 'E-invoice will be generated through DMS.',
+ 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
+ ];
+
+ // Find approval data for this step
+ const approval = request?.approvals?.find((a: any) => a.levelId === step.levelId);
+
+ // Extract IO details from approval data or request (Step 3)
+ let ioDetails = undefined;
+ if (step.step === 3) {
+ if (approval?.ioDetails) {
+ ioDetails = {
+ ioNumber: approval.ioDetails.ioNumber || '',
+ ioRemark: approval.ioDetails.ioRemark || '',
+ organizedBy: approval.ioDetails.organizedBy || step.approver,
+ organizedAt: approval.ioDetails.organizedAt || step.approvedAt || '',
+ };
+ } else if (request?.ioNumber) {
+ // Fallback to request-level IO data
+ ioDetails = {
+ ioNumber: request.ioNumber || '',
+ ioRemark: request.ioRemark || request.ioDetails?.ioRemark || '',
+ organizedBy: step.approver,
+ organizedAt: step.approvedAt || request.updatedAt || '',
+ };
+ }
+ }
+
+ // Extract DMS details from approval data (Step 6)
+ let dmsDetails = undefined;
+ if (step.step === 6) {
+ if (approval?.dmsDetails) {
+ dmsDetails = {
+ dmsNumber: approval.dmsDetails.dmsNumber || '',
+ dmsRemarks: approval.dmsDetails.dmsRemarks || '',
+ pushedBy: approval.dmsDetails.pushedBy || step.approver,
+ pushedAt: approval.dmsDetails.pushedAt || step.approvedAt || '',
+ };
+ } else if (request?.dmsNumber) {
+ // Fallback to request-level DMS data
+ dmsDetails = {
+ dmsNumber: request.dmsNumber || '',
+ dmsRemarks: request.dmsRemarks || request.dmsDetails?.dmsRemarks || '',
+ pushedBy: step.approver,
+ pushedAt: step.approvedAt || request.updatedAt || '',
+ };
+ }
+ }
+
+ return {
+ step: step.step || index + 1,
+ title: stepTitles[index] || `Step ${step.step || index + 1}`,
+ approver: step.approver || 'Unknown',
+ description: stepDescriptions[index] || step.description || '',
+ tatHours: step.tatHours || 24,
+ status: (step.status || 'waiting').toLowerCase() as any,
+ comment: step.comment || approval?.comment,
+ approvedAt: step.approvedAt || approval?.timestamp,
+ elapsedHours: step.elapsedHours,
+ ioDetails,
+ dmsDetails,
+ einvoiceUrl: step.step === 7 ? (approval as any)?.einvoiceUrl : undefined,
+ emailTemplateUrl: step.step === 4 ? (approval as any)?.emailTemplateUrl : undefined,
+ };
+ });
+
+ const totalSteps = request?.totalSteps || 8;
+
+ // Calculate currentStep from approval flow - find the first pending step
+ // If no pending step, use the request's currentStep
+ const pendingStep = workflowSteps.find(s => s.status === 'pending');
+ const currentStep = pendingStep ? pendingStep.step : (request?.currentStep || 1);
+
+ const completedCount = workflowSteps.filter(s => s.status === 'approved').length;
+
+ // Handle proposal submission
+ const handleProposalSubmit = async (data: {
+ proposalDocument: File | null;
+ costBreakup: Array<{ id: string; description: string; amount: number }>;
+ expectedCompletionDate: string;
+ otherDocuments: File[];
+ dealerComments: string;
+ }) => {
+ try {
+ if (!request?.id && !request?.requestId) {
+ throw new Error('Request ID not found');
+ }
+
+ const requestId = request.id || request.requestId;
+
+ // Upload proposal document if provided
+ if (data.proposalDocument) {
+ await uploadDocument(data.proposalDocument, requestId, 'APPROVAL');
+ }
+
+ // Upload other supporting documents
+ for (const file of data.otherDocuments) {
+ await uploadDocument(file, requestId, 'SUPPORTING');
+ }
+
+ // Submit proposal using dealer claim API
+ const totalBudget = data.costBreakup.reduce((sum, item) => sum + item.amount, 0);
+ await submitProposal(requestId, {
+ proposalDocument: data.proposalDocument || undefined,
+ costBreakup: data.costBreakup.map(item => ({
+ description: item.description,
+ amount: item.amount,
+ })),
+ totalEstimatedBudget: totalBudget,
+ expectedCompletionDate: data.expectedCompletionDate,
+ dealerComments: data.dealerComments,
+ });
+
+ // Create work note for activity log
+ await createWorkNoteMultipart(requestId, {
+ message: `Dealer submitted proposal with ${data.costBreakup.length} cost items and estimated budget of ₹${totalBudget.toLocaleString('en-IN')}. ${data.dealerComments ? `Comments: ${data.dealerComments}` : ''}`,
+ isPriority: false,
+ }, []);
+
+ toast.success('Proposal submitted successfully');
+ handleRefresh();
+ } catch (error: any) {
+ console.error('Failed to submit proposal:', error);
+ const errorMessage = error?.response?.data?.message || error?.message || 'Failed to submit proposal. Please try again.';
+ toast.error(errorMessage);
+ throw error;
+ }
+ };
+
+ // Handle proposal approval
+ const handleProposalApprove = async (comments: string) => {
+ try {
+ if (!request?.id && !request?.requestId) {
+ throw new Error('Request ID not found');
+ }
+
+ const requestId = request.id || request.requestId;
+
+ // Get approval levels to find Step 2 levelId
+ const details = await getWorkflowDetails(requestId);
+ const approvals = details?.approvalLevels || details?.approvals || [];
+ const step2Level = approvals.find((level: any) =>
+ (level.levelNumber || level.level_number) === 2
+ );
+
+ if (!step2Level?.levelId && !step2Level?.level_id) {
+ throw new Error('Step 2 approval level not found');
+ }
+
+ const levelId = step2Level.levelId || step2Level.level_id;
+
+ // Approve Step 2 using real API
+ await approveLevel(requestId, levelId, comments);
+
+ // Create work note for activity log
+ await createWorkNoteMultipart(requestId, {
+ message: `Requestor approved the dealer proposal. ${comments ? `Comments: ${comments}` : 'Proceeding to Dept Lead approval.'}`,
+ isPriority: false,
+ }, []);
+
+ toast.success('Proposal approved successfully');
+ handleRefresh();
+ } catch (error: any) {
+ console.error('Failed to approve proposal:', error);
+ const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve proposal. Please try again.';
+ toast.error(errorMessage);
+ throw error;
+ }
+ };
+
+ // Handle proposal rejection
+ const handleProposalReject = async (comments: string) => {
+ try {
+ if (!request?.id && !request?.requestId) {
+ throw new Error('Request ID not found');
+ }
+
+ const requestId = request.id || request.requestId;
+
+ // Get approval levels to find Step 2 levelId
+ const details = await getWorkflowDetails(requestId);
+ const approvals = details?.approvalLevels || details?.approvals || [];
+ const step2Level = approvals.find((level: any) =>
+ (level.levelNumber || level.level_number) === 2
+ );
+
+ if (!step2Level?.levelId && !step2Level?.level_id) {
+ throw new Error('Step 2 approval level not found');
+ }
+
+ const levelId = step2Level.levelId || step2Level.level_id;
+
+ // Reject Step 2 using real API
+ await rejectLevel(requestId, levelId, 'Proposal rejected by requestor', comments);
+
+ // Create work note for activity log
+ await createWorkNoteMultipart(requestId, {
+ message: `Requestor rejected the dealer proposal. Request has been cancelled. ${comments ? `Reason: ${comments}` : ''}`,
+ isPriority: false,
+ }, []);
+
+ toast.success('Proposal rejected. Request has been cancelled.');
+ handleRefresh();
+ } catch (error: any) {
+ console.error('Failed to reject proposal:', error);
+ const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject proposal. Please try again.';
+ toast.error(errorMessage);
+ throw error;
+ }
+ };
+
+ // Handle IO approval (Step 3)
+ const handleIOApproval = async (data: {
+ ioNumber: string;
+ ioRemark: string;
+ comments: string;
+ }) => {
+ try {
+ if (!request?.id && !request?.requestId) {
+ throw new Error('Request ID not found');
+ }
+
+ const requestId = request.id || request.requestId;
+
+ // Get approval levels to find Step 3 levelId
+ const details = await getWorkflowDetails(requestId);
+ const approvals = details?.approvalLevels || details?.approvals || [];
+ const step3Level = approvals.find((level: any) =>
+ (level.levelNumber || level.level_number) === 3
+ );
+
+ if (!step3Level?.levelId && !step3Level?.level_id) {
+ throw new Error('Step 3 approval level not found');
+ }
+
+ const levelId = step3Level.levelId || step3Level.level_id;
+
+ // First, update IO details using dealer claim API
+ // Note: We need to get IO balance from SAP integration, but for now we'll use placeholder values
+ // The backend should handle SAP integration
+ await updateIODetails(requestId, {
+ ioNumber: data.ioNumber,
+ ioAvailableBalance: 0, // Should come from SAP integration
+ ioBlockedAmount: 0, // Should come from SAP integration
+ ioRemainingBalance: 0, // Should come from SAP integration
+ });
+
+ // Approve Step 3 using real API
+ await approveLevel(requestId, levelId, data.comments);
+
+ // Create work note for activity log
+ await createWorkNoteMultipart(requestId, {
+ message: `Dept Lead approved request and organized IO ${data.ioNumber}. ${data.ioRemark ? `IO Remark: ${data.ioRemark}. ` : ''}${data.comments ? `Comments: ${data.comments}` : 'Budget will be blocked in SAP.'}`,
+ isPriority: false,
+ }, []);
+
+ toast.success('Request approved and IO organized successfully');
+ handleRefresh();
+ } catch (error: any) {
+ console.error('Failed to approve and organize IO:', error);
+ const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve request. Please try again.';
+ toast.error(errorMessage);
+ throw error;
+ }
+ };
+
+ // Handle IO rejection (Step 3)
+ const handleIORejection = async (comments: string) => {
+ try {
+ if (!request?.id && !request?.requestId) {
+ throw new Error('Request ID not found');
+ }
+
+ const requestId = request.id || request.requestId;
+
+ // Get approval levels to find Step 3 levelId
+ const details = await getWorkflowDetails(requestId);
+ const approvals = details?.approvalLevels || details?.approvals || [];
+ const step3Level = approvals.find((level: any) =>
+ (level.levelNumber || level.level_number) === 3
+ );
+
+ if (!step3Level?.levelId && !step3Level?.level_id) {
+ throw new Error('Step 3 approval level not found');
+ }
+
+ const levelId = step3Level.levelId || step3Level.level_id;
+
+ // Reject Step 3 using real API
+ await rejectLevel(requestId, levelId, 'Dept Lead rejected - More clarification required', comments);
+
+ // Create work note for activity log
+ await createWorkNoteMultipart(requestId, {
+ message: `Dept Lead rejected the request. More clarification required. Request has been cancelled. ${comments ? `Reason: ${comments}` : ''}`,
+ isPriority: false,
+ }, []);
+
+ toast.success('Request rejected. Request has been cancelled.');
+ handleRefresh();
+ } catch (error: any) {
+ console.error('Failed to reject request:', error);
+ const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject request. Please try again.';
+ toast.error(errorMessage);
+ throw error;
+ }
+ };
+
+ // Extract proposal data from request
+ const [proposalData, setProposalData] = useState(null);
+
+ useEffect(() => {
+ if (!request) {
+ setProposalData(null);
+ return;
+ }
+
+ const loadProposalData = async () => {
+ try {
+ const requestId = request.id || request.requestId;
+ if (!requestId) {
+ setProposalData(null);
+ return;
+ }
+
+ // Get workflow details which includes documents and proposal details
+ const details = await getWorkflowDetails(requestId);
+ const documents = details?.documents || [];
+ const proposalDetails = request.proposalDetails || details?.proposalDetails || {};
+
+ // Find proposal document (category APPROVAL or type proposal)
+ const proposalDoc = documents.find((d: any) =>
+ d.category === 'APPROVAL' || d.type === 'proposal' || d.documentCategory === 'APPROVAL'
+ );
+
+ // Find supporting documents
+ const otherDocs = documents.filter((d: any) =>
+ d.category === 'SUPPORTING' || d.type === 'supporting' || d.documentCategory === 'SUPPORTING'
+ );
+
+ // Ensure costBreakup is an array
+ let costBreakup = proposalDetails.costBreakup || [];
+ if (typeof costBreakup === 'string') {
+ try {
+ costBreakup = JSON.parse(costBreakup);
+ } catch (e) {
+ console.warn('Failed to parse costBreakup JSON:', e);
+ costBreakup = [];
+ }
+ }
+ if (!Array.isArray(costBreakup)) {
+ costBreakup = [];
+ }
+
+ setProposalData({
+ proposalDocument: proposalDoc ? {
+ name: proposalDoc.fileName || proposalDoc.file_name || proposalDoc.name,
+ id: proposalDoc.documentId || proposalDoc.document_id || proposalDoc.id,
+ } : undefined,
+ costBreakup: costBreakup,
+ expectedCompletionDate: proposalDetails.expectedCompletionDate || '',
+ otherDocuments: otherDocs.map((d: any) => ({
+ name: d.fileName || d.file_name || d.name,
+ id: d.documentId || d.document_id || d.id,
+ })),
+ dealerComments: proposalDetails.dealerComments || '',
+ submittedAt: proposalDetails.submittedAt,
+ });
+ } catch (error) {
+ console.warn('Failed to load proposal data:', error);
+ // Fallback to request data only
+ const proposalDetails = request.proposalDetails || {};
+
+ // Ensure costBreakup is an array
+ let costBreakup = proposalDetails.costBreakup || [];
+ if (typeof costBreakup === 'string') {
+ try {
+ costBreakup = JSON.parse(costBreakup);
+ } catch (e) {
+ console.warn('Failed to parse costBreakup JSON:', e);
+ costBreakup = [];
+ }
+ }
+ if (!Array.isArray(costBreakup)) {
+ costBreakup = [];
+ }
+
+ setProposalData({
+ proposalDocument: undefined,
+ costBreakup: costBreakup,
+ expectedCompletionDate: proposalDetails.expectedCompletionDate || '',
+ otherDocuments: [],
+ dealerComments: proposalDetails.dealerComments || '',
+ submittedAt: proposalDetails.submittedAt,
+ });
+ }
+ };
+
+ loadProposalData();
+ }, [request]);
+
+ // Get dealer and activity info
+ const dealerName = request?.claimDetails?.dealerName ||
+ request?.dealerInfo?.name ||
+ 'Dealer';
+ const activityName = request?.claimDetails?.activityName ||
+ request?.activityInfo?.activityName ||
+ request?.title ||
+ 'Activity';
+
+ return (
+ <>
+
+
+
+
+
+
+ Claim Management Workflow
+
+
+ 8-Step approval process for dealer claim management
+
+
+
+ Step {currentStep} of {totalSteps}
+
+
+
+
+
+ {workflowSteps.map((step, index) => {
+ const isActive = step.status === 'pending' && step.step === currentStep;
+ const isCompleted = step.status === 'approved';
+ const isWaiting = step.status === 'waiting';
+
+ // Debug logging for Step 2
+ if (step.step === 2) {
+ console.log('[DealerClaimWorkflowTab] Step 2 Debug:', {
+ step: step.step,
+ status: step.status,
+ currentStep,
+ isActive,
+ isInitiator,
+ showApprovalModal,
+ });
+ }
+
+ return (
+
+
+ {/* Step Icon */}
+
+ {getStepIcon(step.status)}
+
+
+ {/* Step Content */}
+
+
+
+
+
+ Step {step.step}: {step.title}
+
+
+ {step.status}
+
+ {/* Email Template Button (Step 4) */}
+ {step.step === 4 && step.emailTemplateUrl && (
+
+ )}
+ {/* E-Invoice Download Button (Step 7) */}
+ {step.step === 7 && step.einvoiceUrl && isCompleted && (
+
+ )}
+
+
{step.approver}
+
{step.description}
+
+
+
TAT: {step.tatHours}h
+ {step.elapsedHours && (
+
+ Elapsed: {step.elapsedHours}h
+
+ )}
+
+
+
+ {/* Comment Section */}
+ {step.comment && (
+
+ )}
+
+ {/* IO Organization Details (Step 3) */}
+ {step.step === 3 && step.ioDetails && step.ioDetails.ioNumber && (
+
+
+
+
+ IO Organisation Details
+
+
+
+
+ IO Number:
+
+ {step.ioDetails.ioNumber}
+
+
+ {step.ioDetails.ioRemark && (
+
+
IO Remark:
+
{step.ioDetails.ioRemark}
+
+ )}
+ {step.ioDetails.organizedAt && (
+
+ Organised by {step.ioDetails.organizedBy} on{' '}
+ {formatDateSafe(step.ioDetails.organizedAt)}
+
+ )}
+
+
+ )}
+
+ {/* DMS Processing Details (Step 6) */}
+ {step.step === 6 && step.dmsDetails && step.dmsDetails.dmsNumber && (
+
+
+
+
+ DMS Processing Details
+
+
+
+
+ DMS Number:
+
+ {step.dmsDetails.dmsNumber}
+
+
+ {step.dmsDetails.dmsRemarks && (
+
+
DMS Remarks:
+
{step.dmsDetails.dmsRemarks}
+
+ )}
+ {step.dmsDetails.pushedAt && (
+
+ Pushed by {step.dmsDetails.pushedBy} on{' '}
+ {formatDateSafe(step.dmsDetails.pushedAt)}
+
+ )}
+
+
+ )}
+
+ {/* Action Buttons */}
+ {isActive && (
+
+ {/* Step 1: Submit Proposal Button */}
+ {step.step === 1 && (
+
+ )}
+
+ {/* Step 2: Review & Approve Proposal - Show for initiator when step is active */}
+ {step.step === 2 && isInitiator && (
+
+ )}
+
+ {/* Step 3: Approve and Organise IO */}
+ {step.step === 3 && (
+
+ )}
+
+ {/* Step 5: Upload Completion Documents */}
+ {step.step === 5 && (
+
+ )}
+
+ )}
+
+ {/* Approved Date */}
+ {step.approvedAt && (
+
+ Approved on {formatDateSafe(step.approvedAt)}
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+
+ {/* Dealer Proposal Submission Modal */}
+ setShowProposalModal(false)}
+ onSubmit={handleProposalSubmit}
+ dealerName={dealerName}
+ activityName={activityName}
+ requestId={request?.id || request?.requestId}
+ />
+
+ {/* Initiator Proposal Approval Modal */}
+ {
+ console.log('[DealerClaimWorkflowTab] Closing approval modal');
+ setShowApprovalModal(false);
+ }}
+ onApprove={handleProposalApprove}
+ onReject={handleProposalReject}
+ proposalData={proposalData}
+ dealerName={dealerName}
+ activityName={activityName}
+ requestId={request?.id || request?.requestId}
+ />
+
+ {/* Dept Lead IO Approval Modal */}
+ setShowIOApprovalModal(false)}
+ onApprove={handleIOApproval}
+ onReject={handleIORejection}
+ requestTitle={request?.title}
+ requestId={request?.id || request?.requestId}
+ />
+ >
+ );
+}
+
diff --git a/src/pages/RequestDetail/components/tabs/IOTab.tsx b/src/pages/RequestDetail/components/tabs/IOTab.tsx
new file mode 100644
index 0000000..da2fe08
--- /dev/null
+++ b/src/pages/RequestDetail/components/tabs/IOTab.tsx
@@ -0,0 +1,435 @@
+/**
+ * IO Tab Component
+ *
+ * Purpose: Handle IO (Internal Order) budget management for dealer claims
+ * Features:
+ * - Fetch IO budget from SAP
+ * - Block IO amount in SAP
+ * - Display blocked IO details
+ */
+
+import { useState, useEffect } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { DollarSign, Download, CircleCheckBig, AlertCircle } from 'lucide-react';
+import { toast } from 'sonner';
+import { mockApi } from '@/services/mockApi';
+
+// Helper to extract data from API response and handle errors
+const handleApiResponse = (response: any): T => {
+ if (!response.success) {
+ const errorMsg = response.error?.message || 'Operation failed';
+ throw new Error(errorMsg);
+ }
+ return response.data;
+};
+
+interface IOTabProps {
+ request: any;
+ apiRequest?: any;
+ onRefresh?: () => void;
+}
+
+interface IOBlockedDetails {
+ ioNumber: string;
+ blockedAmount: number;
+ availableBalance: number;
+ blockedDate: string;
+ sapDocumentNumber: string;
+ status: 'blocked' | 'released' | 'failed';
+}
+
+export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
+ const requestId = apiRequest?.requestId || request?.requestId;
+ const [ioNumber, setIoNumber] = useState(request?.ioNumber || '');
+ const [fetchingAmount, setFetchingAmount] = useState(false);
+ const [fetchedAmount, setFetchedAmount] = useState(null);
+ const [blockedDetails, setBlockedDetails] = useState(null);
+ const [blockingBudget, setBlockingBudget] = useState(false);
+
+ // Load existing IO block from mock API
+ useEffect(() => {
+ if (requestId) {
+ mockApi.getIOBlock(requestId).then(response => {
+ try {
+ const ioBlock = handleApiResponse(response);
+ if (ioBlock) {
+ setBlockedDetails({
+ ioNumber: ioBlock.ioNumber,
+ blockedAmount: ioBlock.blockedAmount,
+ availableBalance: ioBlock.availableBalance,
+ blockedDate: ioBlock.blockedDate,
+ sapDocumentNumber: ioBlock.sapDocumentNumber,
+ status: ioBlock.status,
+ });
+ setIoNumber(ioBlock.ioNumber);
+ }
+ } catch (error) {
+ // IO block not found is expected for new requests
+ console.debug('No IO block found for request:', requestId);
+ }
+ }).catch(error => {
+ console.warn('Error loading IO block:', error);
+ });
+ }
+ }, [requestId]);
+
+ /**
+ * Fetch available budget from SAP
+ */
+ const handleFetchAmount = async () => {
+ if (!ioNumber.trim()) {
+ toast.error('Please enter an IO number');
+ return;
+ }
+
+ setFetchingAmount(true);
+ try {
+ // TODO: Replace with actual SAP API integration
+ // const response = await fetch(`/api/sap/io/${ioNumber}/budget`);
+ // const data = await response.json();
+
+ // Mock API call - simulate SAP integration
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ // Mock response
+ const mockAvailableAmount = 50000; // ₹50,000
+
+ setFetchedAmount(mockAvailableAmount);
+ toast.success('IO budget fetched successfully from SAP');
+ } catch (error: any) {
+ console.error('Failed to fetch IO budget:', error);
+ toast.error(error.message || 'Failed to fetch IO budget from SAP');
+ setFetchedAmount(null);
+ } finally {
+ setFetchingAmount(false);
+ }
+ };
+
+ /**
+ * Block budget in SAP system
+ */
+ const handleBlockBudget = async () => {
+ if (!ioNumber.trim() || !fetchedAmount) {
+ toast.error('Please fetch IO amount first');
+ return;
+ }
+
+ const claimAmount = request?.claimAmount || request?.amount || 0;
+
+ if (claimAmount > fetchedAmount) {
+ toast.error('Claim amount exceeds available IO budget');
+ return;
+ }
+
+ setBlockingBudget(true);
+ try {
+ // TODO: Replace with actual SAP API integration
+ // const response = await fetch(`/api/sap/io/block`, {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify({
+ // ioNumber,
+ // amount: claimAmount,
+ // requestId: apiRequest?.requestId,
+ // }),
+ // });
+ // const data = await response.json();
+
+ // Mock API call
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // Mock blocked details
+ const blocked: IOBlockedDetails = {
+ ioNumber,
+ blockedAmount: claimAmount,
+ availableBalance: fetchedAmount - claimAmount,
+ blockedDate: new Date().toISOString(),
+ sapDocumentNumber: `SAP-${Date.now()}`,
+ status: 'blocked',
+ };
+
+ // Save to mock API
+ if (requestId) {
+ try {
+ const ioBlockResponse = await mockApi.createIOBlock(requestId, {
+ id: `io-${Date.now()}`,
+ ioNumber: blocked.ioNumber,
+ blockedAmount: blocked.blockedAmount,
+ availableBalance: blocked.availableBalance,
+ blockedDate: blocked.blockedDate,
+ sapDocumentNumber: blocked.sapDocumentNumber,
+ status: blocked.status,
+ });
+ handleApiResponse(ioBlockResponse);
+
+ // Update request with IO number
+ const updateResponse = await mockApi.updateRequest(requestId, {
+ ioNumber: blocked.ioNumber,
+ ioBlockedAmount: blocked.blockedAmount,
+ sapDocumentNumber: blocked.sapDocumentNumber,
+ });
+ handleApiResponse(updateResponse);
+
+ // Create activity log
+ const activityResponse = await mockApi.createActivity(requestId, {
+ id: `act-${Date.now()}`,
+ type: 'io_blocked',
+ action: 'IO Budget Blocked',
+ details: `IO ${ioNumber} budget of ₹${claimAmount.toLocaleString('en-IN')} blocked in SAP`,
+ user: 'System',
+ message: `IO budget blocked: ${blocked.sapDocumentNumber}`,
+ });
+ handleApiResponse(activityResponse);
+ } catch (error) {
+ console.error('Failed to save IO block to database:', error);
+ }
+ }
+
+ setBlockedDetails(blocked);
+ toast.success('IO budget blocked successfully in SAP');
+
+ // Refresh request details
+ onRefresh?.();
+ } catch (error: any) {
+ console.error('Failed to block IO budget:', error);
+ toast.error(error.message || 'Failed to block IO budget in SAP');
+ } finally {
+ setBlockingBudget(false);
+ }
+ };
+
+ /**
+ * Release blocked budget
+ */
+ const handleReleaseBudget = async () => {
+ if (!blockedDetails || !requestId) return;
+
+ try {
+ // TODO: Replace with actual SAP API integration
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ // Update IO block in mock API
+ try {
+ const ioBlockResponse = await mockApi.getIOBlock(requestId);
+ const ioBlock = handleApiResponse(ioBlockResponse);
+ if (ioBlock) {
+ const updateIOResponse = await mockApi.updateIOBlock(requestId, {
+ status: 'released',
+ releasedDate: new Date().toISOString(),
+ });
+ handleApiResponse(updateIOResponse);
+
+ // Update request
+ const updateRequestResponse = await mockApi.updateRequest(requestId, {
+ ioBlockedAmount: null,
+ sapDocumentNumber: null,
+ });
+ handleApiResponse(updateRequestResponse);
+
+ // Create activity log
+ const activityResponse = await mockApi.createActivity(requestId, {
+ id: `act-${Date.now()}`,
+ type: 'io_released',
+ action: 'IO Budget Released',
+ details: `IO ${blockedDetails.ioNumber} budget released`,
+ user: 'System',
+ message: 'IO budget released',
+ });
+ handleApiResponse(activityResponse);
+ }
+ } catch (error) {
+ console.error('Failed to update IO block in database:', error);
+ }
+
+ setBlockedDetails(null);
+ setFetchedAmount(null);
+ setIoNumber('');
+ toast.success('IO budget released successfully');
+
+ onRefresh?.();
+ } catch (error: any) {
+ console.error('Failed to release IO budget:', error);
+ toast.error(error.message || 'Failed to release IO budget');
+ }
+ };
+
+ const claimAmount = request?.claimAmount || request?.amount || 0;
+
+ return (
+
+ {/* IO Budget Management Card */}
+
+
+
+
+ IO Budget Management
+
+
+ Enter IO number to fetch available budget from SAP
+
+
+
+ {/* IO Number Input */}
+
+
+
+ setIoNumber(e.target.value)}
+ disabled={fetchingAmount || !!blockedDetails}
+ className="flex-1"
+ />
+
+
+
+
+ {/* Fetched Amount Display */}
+ {fetchedAmount !== null && !blockedDetails && (
+
+
+
+
Available Budget
+
+ ₹{fetchedAmount.toLocaleString('en-IN')}
+
+
+
+
+
+
+
+ Claim Amount:
+
+ ₹{claimAmount.toLocaleString('en-IN')}
+
+
+
+ Balance After Block:
+
+ ₹{(fetchedAmount - claimAmount).toLocaleString('en-IN')}
+
+
+
+
+ {claimAmount > fetchedAmount ? (
+
+
+
+ Insufficient budget! Claim amount exceeds available balance.
+
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ {/* IO Blocked Details Card */}
+
+
+
+
+ IO Blocked Details
+
+
+ Details of IO blocked in SAP system
+
+
+
+ {blockedDetails ? (
+
+ {/* Success Banner */}
+
+
+
+
+
+
+
Budget Blocked Successfully!
+
SAP integration completed
+
+
+
+
+ {/* Blocked Details */}
+
+
+ IO Number:
+ {blockedDetails.ioNumber}
+
+
+ Blocked Amount:
+
+ ₹{blockedDetails.blockedAmount.toLocaleString('en-IN')}
+
+
+
+ Available Balance:
+
+ ₹{blockedDetails.availableBalance.toLocaleString('en-IN')}
+
+
+
+ Blocked Date:
+
+ {new Date(blockedDetails.blockedDate).toLocaleString('en-IN')}
+
+
+
+ SAP Document No:
+
+ {blockedDetails.sapDocumentNumber}
+
+
+
+ Status:
+
+ {blockedDetails.status.toUpperCase()}
+
+
+
+
+ {/* Release Button */}
+
+
+ ) : (
+
+
+
No IO blocked yet
+
+ Enter IO number and fetch amount to block budget
+
+
+ )}
+
+
+
+ );
+}
+
diff --git a/src/services/dealerApi.ts b/src/services/dealerApi.ts
new file mode 100644
index 0000000..5aa05ae
--- /dev/null
+++ b/src/services/dealerApi.ts
@@ -0,0 +1,72 @@
+/**
+ * Dealer API Service
+ * Handles API calls for dealer-related operations
+ */
+
+import apiClient from './authApi';
+
+export interface DealerInfo {
+ userId: string;
+ email: string;
+ dealerCode: string;
+ dealerName: string;
+ displayName: string;
+ phone?: string;
+ department?: string;
+ designation?: string;
+}
+
+/**
+ * Get all dealers
+ */
+export async function getAllDealers(): Promise {
+ try {
+ const res = await apiClient.get('/dealers');
+ return res.data?.data || res.data || [];
+ } catch (error) {
+ console.error('[DealerAPI] Error fetching dealers:', error);
+ throw error;
+ }
+}
+
+/**
+ * Get dealer by code
+ */
+export async function getDealerByCode(dealerCode: string): Promise {
+ try {
+ const res = await apiClient.get(`/dealers/code/${dealerCode}`);
+ return res.data?.data || res.data || null;
+ } catch (error) {
+ console.error('[DealerAPI] Error fetching dealer by code:', error);
+ return null;
+ }
+}
+
+/**
+ * Get dealer by email
+ */
+export async function getDealerByEmail(email: string): Promise {
+ try {
+ const res = await apiClient.get(`/dealers/email/${encodeURIComponent(email)}`);
+ return res.data?.data || res.data || null;
+ } catch (error) {
+ console.error('[DealerAPI] Error fetching dealer by email:', error);
+ return null;
+ }
+}
+
+/**
+ * Search dealers
+ */
+export async function searchDealers(searchTerm: string): Promise {
+ try {
+ const res = await apiClient.get('/dealers/search', {
+ params: { q: searchTerm },
+ });
+ return res.data?.data || res.data || [];
+ } catch (error) {
+ console.error('[DealerAPI] Error searching dealers:', error);
+ return [];
+ }
+}
+
diff --git a/src/services/dealerClaimApi.ts b/src/services/dealerClaimApi.ts
new file mode 100644
index 0000000..7159273
--- /dev/null
+++ b/src/services/dealerClaimApi.ts
@@ -0,0 +1,245 @@
+/**
+ * Dealer Claim API Service
+ * Handles API calls for dealer claim management operations
+ */
+
+import apiClient from './authApi';
+
+export interface CreateClaimRequestPayload {
+ activityName: string;
+ activityType: string;
+ dealerCode: string;
+ dealerName: string;
+ dealerEmail?: string;
+ dealerPhone?: string;
+ dealerAddress?: string;
+ activityDate?: string; // ISO date string
+ location: string;
+ requestDescription: string;
+ periodStartDate?: string; // ISO date string
+ periodEndDate?: string; // ISO date string
+ estimatedBudget?: string | number;
+}
+
+export interface ClaimRequestResponse {
+ request: {
+ requestId: string;
+ requestNumber: string;
+ title: string;
+ description: string;
+ status: string;
+ workflowType: string;
+ // ... other fields
+ };
+ message: string;
+}
+
+/**
+ * Create a new dealer claim request
+ * POST /api/v1/dealer-claims
+ */
+export async function createClaimRequest(payload: CreateClaimRequestPayload): Promise {
+ try {
+ const response = await apiClient.post('/dealer-claims', payload);
+ return response.data?.data || response.data;
+ } catch (error: any) {
+ console.error('[DealerClaimAPI] Error creating claim request:', error);
+ throw error;
+ }
+}
+
+/**
+ * Get claim details
+ * GET /api/v1/dealer-claims/:requestId
+ */
+export async function getClaimDetails(requestId: string): Promise {
+ try {
+ const response = await apiClient.get(`/dealer-claims/${requestId}`);
+ return response.data?.data || response.data;
+ } catch (error: any) {
+ console.error('[DealerClaimAPI] Error fetching claim details:', error);
+ throw error;
+ }
+}
+
+/**
+ * Submit dealer proposal (Step 1)
+ * POST /api/v1/dealer-claims/:requestId/proposal
+ */
+export async function submitProposal(
+ requestId: string,
+ proposalData: {
+ proposalDocument?: File;
+ costBreakup?: Array<{ description: string; amount: number }>;
+ totalEstimatedBudget?: number;
+ timelineMode?: 'date' | 'days';
+ expectedCompletionDate?: string;
+ expectedCompletionDays?: number;
+ dealerComments?: string;
+ }
+): Promise {
+ try {
+ const formData = new FormData();
+
+ if (proposalData.proposalDocument) {
+ formData.append('proposalDocument', proposalData.proposalDocument);
+ }
+
+ if (proposalData.costBreakup) {
+ formData.append('costBreakup', JSON.stringify(proposalData.costBreakup));
+ }
+
+ if (proposalData.totalEstimatedBudget !== undefined) {
+ formData.append('totalEstimatedBudget', proposalData.totalEstimatedBudget.toString());
+ }
+
+ if (proposalData.timelineMode) {
+ formData.append('timelineMode', proposalData.timelineMode);
+ }
+
+ if (proposalData.expectedCompletionDate) {
+ formData.append('expectedCompletionDate', proposalData.expectedCompletionDate);
+ }
+
+ if (proposalData.expectedCompletionDays !== undefined) {
+ formData.append('expectedCompletionDays', proposalData.expectedCompletionDays.toString());
+ }
+
+ if (proposalData.dealerComments) {
+ formData.append('dealerComments', proposalData.dealerComments);
+ }
+
+ const response = await apiClient.post(`/dealer-claims/${requestId}/proposal`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ return response.data?.data || response.data;
+ } catch (error: any) {
+ console.error('[DealerClaimAPI] Error submitting proposal:', error);
+ throw error;
+ }
+}
+
+/**
+ * Submit completion documents (Step 5)
+ * POST /api/v1/dealer-claims/:requestId/completion
+ */
+export async function submitCompletion(
+ requestId: string,
+ completionData: {
+ activityCompletionDate: string; // ISO date string
+ numberOfParticipants?: number;
+ closedExpenses?: Array<{ description: string; amount: number }>;
+ totalClosedExpenses?: number;
+ completionDocuments?: File[];
+ activityPhotos?: File[];
+ }
+): Promise {
+ try {
+ const formData = new FormData();
+
+ formData.append('activityCompletionDate', completionData.activityCompletionDate);
+
+ if (completionData.numberOfParticipants !== undefined) {
+ formData.append('numberOfParticipants', completionData.numberOfParticipants.toString());
+ }
+
+ if (completionData.closedExpenses) {
+ formData.append('closedExpenses', JSON.stringify(completionData.closedExpenses));
+ }
+
+ if (completionData.totalClosedExpenses !== undefined) {
+ formData.append('totalClosedExpenses', completionData.totalClosedExpenses.toString());
+ }
+
+ if (completionData.completionDocuments) {
+ completionData.completionDocuments.forEach((file) => {
+ formData.append('completionDocuments', file);
+ });
+ }
+
+ if (completionData.activityPhotos) {
+ completionData.activityPhotos.forEach((file) => {
+ formData.append('activityPhotos', file);
+ });
+ }
+
+ const response = await apiClient.post(`/dealer-claims/${requestId}/completion`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ return response.data?.data || response.data;
+ } catch (error: any) {
+ console.error('[DealerClaimAPI] Error submitting completion:', error);
+ throw error;
+ }
+}
+
+/**
+ * Update IO details (Step 3)
+ * PUT /api/v1/dealer-claims/:requestId/io
+ */
+export async function updateIODetails(
+ requestId: string,
+ ioData: {
+ ioNumber: string;
+ ioAvailableBalance: number;
+ ioBlockedAmount: number;
+ ioRemainingBalance: number;
+ }
+): Promise {
+ try {
+ const response = await apiClient.put(`/dealer-claims/${requestId}/io`, ioData);
+ return response.data?.data || response.data;
+ } catch (error: any) {
+ console.error('[DealerClaimAPI] Error updating IO details:', error);
+ throw error;
+ }
+}
+
+/**
+ * Update E-Invoice details (Step 7)
+ * PUT /api/v1/dealer-claims/:requestId/e-invoice
+ */
+export async function updateEInvoice(
+ requestId: string,
+ eInvoiceData: {
+ eInvoiceNumber?: string;
+ eInvoiceDate: string; // ISO date string
+ dmsNumber?: string;
+ }
+): Promise {
+ try {
+ const response = await apiClient.put(`/dealer-claims/${requestId}/e-invoice`, eInvoiceData);
+ return response.data?.data || response.data;
+ } catch (error: any) {
+ console.error('[DealerClaimAPI] Error updating e-invoice:', error);
+ throw error;
+ }
+}
+
+/**
+ * Update Credit Note details (Step 8)
+ * PUT /api/v1/dealer-claims/:requestId/credit-note
+ */
+export async function updateCreditNote(
+ requestId: string,
+ creditNoteData: {
+ creditNoteNumber?: string;
+ creditNoteDate: string; // ISO date string
+ creditNoteAmount: number;
+ }
+): Promise {
+ try {
+ const response = await apiClient.put(`/dealer-claims/${requestId}/credit-note`, creditNoteData);
+ return response.data?.data || response.data;
+ } catch (error: any) {
+ console.error('[DealerClaimAPI] Error updating credit note:', error);
+ throw error;
+ }
+}
+
diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts
index f414b5b..0ce6ca5 100644
--- a/src/services/workflowApi.ts
+++ b/src/services/workflowApi.ts
@@ -275,7 +275,7 @@ export async function listClosedByMe(params: { page?: number; limit?: number; se
};
}
-export async function getWorkflowDetails(requestId: string) {
+export async function getWorkflowDetails(requestId: string, workflowType?: string) {
const res = await apiClient.get(`/workflows/${requestId}/details`);
return res.data?.data || res.data;
}
diff --git a/src/utils/claimDataMapper.ts b/src/utils/claimDataMapper.ts
new file mode 100644
index 0000000..31d9c94
--- /dev/null
+++ b/src/utils/claimDataMapper.ts
@@ -0,0 +1,318 @@
+/**
+ * Claim Data Mapper Utilities
+ * Maps API response data to ClaimManagementRequest structure for frontend components
+ */
+
+import { isClaimManagementRequest } from './claimRequestUtils';
+
+/**
+ * User roles in a claim management request
+ */
+export type RequestRole = 'INITIATOR' | 'DEALER' | 'DEPARTMENT_LEAD' | 'APPROVER' | 'SPECTATOR';
+
+/**
+ * Claim Management Request structure for frontend
+ */
+export interface ClaimManagementRequest {
+ // Activity Information
+ activityInfo: {
+ activityName: string;
+ activityType: string;
+ requestedDate?: string;
+ location: string;
+ period?: {
+ startDate: string;
+ endDate: string;
+ };
+ estimatedBudget?: number;
+ closedExpenses?: number;
+ closedExpensesBreakdown?: Array<{ description: string; amount: number }>;
+ description?: string;
+ };
+
+ // Dealer Information
+ dealerInfo: {
+ dealerCode: string;
+ dealerName: string;
+ email?: string;
+ phone?: string;
+ address?: string;
+ };
+
+ // Proposal Details (Step 1)
+ proposalDetails?: {
+ proposalDocumentUrl?: string;
+ costBreakup: Array<{ description: string; amount: number }>;
+ totalEstimatedBudget: number;
+ timelineMode?: 'date' | 'days';
+ expectedCompletionDate?: string;
+ expectedCompletionDays?: number;
+ dealerComments?: string;
+ submittedAt?: string;
+ };
+
+ // IO Details (Step 3)
+ ioDetails?: {
+ ioNumber?: string;
+ availableBalance?: number;
+ blockedAmount?: number;
+ remainingBalance?: number;
+ };
+
+ // DMS Details (Step 7)
+ dmsDetails?: {
+ eInvoiceNumber?: string;
+ eInvoiceDate?: string;
+ dmsNumber?: string;
+ creditNoteNumber?: string;
+ creditNoteDate?: string;
+ creditNoteAmount?: number;
+ };
+
+ // Claim Amount
+ claimAmount: {
+ estimated: number;
+ closed: number;
+ };
+}
+
+/**
+ * Role-based visibility configuration
+ */
+export interface RoleVisibility {
+ showDealerInfo: boolean;
+ showProposalDetails: boolean;
+ showIODetails: boolean;
+ showDMSDetails: boolean;
+ showClaimAmount: boolean;
+ canEditClaimAmount: boolean;
+}
+
+/**
+ * Map API request data to ClaimManagementRequest structure
+ */
+export function mapToClaimManagementRequest(
+ apiRequest: any,
+ currentUserId: string
+): ClaimManagementRequest | null {
+ try {
+ if (!isClaimManagementRequest(apiRequest)) {
+ return null;
+ }
+
+ // Extract claim details from API response
+ const claimDetails = apiRequest.claimDetails || {};
+ const proposalDetails = apiRequest.proposalDetails || {};
+ const completionDetails = apiRequest.completionDetails || {};
+
+ // Debug: Log raw claim details to help troubleshoot
+ console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
+ console.debug('[claimDataMapper] Raw apiRequest:', {
+ hasClaimDetails: !!apiRequest.claimDetails,
+ hasProposalDetails: !!apiRequest.proposalDetails,
+ hasCompletionDetails: !!apiRequest.completionDetails,
+ workflowType: apiRequest.workflowType,
+ });
+
+ // Map activity information (matching ActivityInformationCard expectations)
+ // Handle both camelCase and snake_case field names from Sequelize
+ const periodStartDate = claimDetails.periodStartDate || claimDetails.period_start_date;
+ const periodEndDate = claimDetails.periodEndDate || claimDetails.period_end_date;
+
+ const activityName = claimDetails.activityName || claimDetails.activity_name || '';
+ const activityType = claimDetails.activityType || claimDetails.activity_type || '';
+ const location = claimDetails.location || '';
+
+ console.debug('[claimDataMapper] Mapped activity fields:', {
+ activityName,
+ activityType,
+ location,
+ hasActivityName: !!activityName,
+ hasActivityType: !!activityType,
+ hasLocation: !!location,
+ });
+
+ const activityInfo = {
+ activityName,
+ activityType,
+ requestedDate: claimDetails.activityDate || claimDetails.activity_date || apiRequest.createdAt, // Use activityDate as requestedDate, fallback to createdAt
+ location,
+ period: (periodStartDate && periodEndDate) ? {
+ startDate: periodStartDate,
+ endDate: periodEndDate,
+ } : undefined,
+ estimatedBudget: claimDetails.estimatedBudget || claimDetails.estimated_budget,
+ closedExpenses: claimDetails.closedExpenses || claimDetails.closed_expenses,
+ closedExpensesBreakdown: completionDetails.closedExpenses ||
+ completionDetails.closed_expenses ||
+ completionDetails.closedExpensesBreakdown ||
+ [],
+ description: apiRequest.description || '', // Get description from workflow request
+ };
+
+ // Map dealer information (matching DealerInformationCard expectations)
+ const dealerInfo = {
+ dealerCode: claimDetails.dealerCode || claimDetails.dealer_code || '',
+ dealerName: claimDetails.dealerName || claimDetails.dealer_name || '',
+ email: claimDetails.dealerEmail || claimDetails.dealer_email || '',
+ phone: claimDetails.dealerPhone || claimDetails.dealer_phone || '',
+ address: claimDetails.dealerAddress || claimDetails.dealer_address || '',
+ };
+
+ // Map proposal details
+ const proposal = proposalDetails ? {
+ proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
+ costBreakup: proposalDetails.costBreakup || proposalDetails.cost_breakup || [],
+ totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
+ timelineMode: proposalDetails.timelineMode || proposalDetails.timeline_mode,
+ expectedCompletionDate: proposalDetails.expectedCompletionDate || proposalDetails.expected_completion_date,
+ expectedCompletionDays: proposalDetails.expectedCompletionDays || proposalDetails.expected_completion_days,
+ dealerComments: proposalDetails.dealerComments || proposalDetails.dealer_comments,
+ submittedAt: proposalDetails.submittedAt || proposalDetails.submitted_at,
+ } : undefined;
+
+ // Map IO details
+ const ioDetails = {
+ ioNumber: claimDetails.ioNumber || claimDetails.io_number,
+ availableBalance: claimDetails.ioAvailableBalance || claimDetails.io_available_balance,
+ blockedAmount: claimDetails.ioBlockedAmount || claimDetails.io_blocked_amount,
+ remainingBalance: claimDetails.ioRemainingBalance || claimDetails.io_remaining_balance,
+ };
+
+ // Map DMS details
+ const dmsDetails = {
+ eInvoiceNumber: claimDetails.eInvoiceNumber || claimDetails.e_invoice_number,
+ eInvoiceDate: claimDetails.eInvoiceDate || claimDetails.e_invoice_date,
+ dmsNumber: claimDetails.dmsNumber || claimDetails.dms_number,
+ creditNoteNumber: claimDetails.creditNoteNumber || claimDetails.credit_note_number,
+ creditNoteDate: claimDetails.creditNoteDate || claimDetails.credit_note_date,
+ creditNoteAmount: claimDetails.creditNoteAmount || claimDetails.credit_note_amount,
+ };
+
+ // Map claim amounts
+ const claimAmount = {
+ estimated: activityInfo.estimatedBudget || 0,
+ closed: activityInfo.closedExpenses || 0,
+ };
+
+ return {
+ activityInfo,
+ dealerInfo,
+ proposalDetails: proposal,
+ ioDetails: Object.keys(ioDetails).some(k => ioDetails[k as keyof typeof ioDetails]) ? ioDetails : undefined,
+ dmsDetails: Object.keys(dmsDetails).some(k => dmsDetails[k as keyof typeof dmsDetails]) ? dmsDetails : undefined,
+ claimAmount,
+ };
+ } catch (error) {
+ console.error('[claimDataMapper] Error mapping claim data:', error);
+ return null;
+ }
+}
+
+/**
+ * Determine user's role in the request
+ */
+export function determineUserRole(apiRequest: any, currentUserId: string): RequestRole {
+ try {
+ // Check if user is the initiator
+ if (apiRequest.initiatorId === currentUserId ||
+ apiRequest.initiator?.userId === currentUserId ||
+ apiRequest.requestedBy?.userId === currentUserId) {
+ return 'INITIATOR';
+ }
+
+ // Check if user is a dealer (participant with DEALER type)
+ const participants = apiRequest.participants || [];
+ const dealerParticipant = participants.find((p: any) =>
+ (p.userId === currentUserId || p.user?.userId === currentUserId) &&
+ (p.participantType === 'DEALER' || p.type === 'DEALER')
+ );
+ if (dealerParticipant) {
+ return 'DEALER';
+ }
+
+ // Check if user is a department lead (approver at level 3)
+ const approvalLevels = apiRequest.approvalLevels || [];
+ const deptLeadLevel = approvalLevels.find((level: any) =>
+ level.levelNumber === 3 &&
+ (level.approverId === currentUserId || level.approver?.userId === currentUserId)
+ );
+ if (deptLeadLevel) {
+ return 'DEPARTMENT_LEAD';
+ }
+
+ // Check if user is an approver
+ const approverLevel = approvalLevels.find((level: any) =>
+ (level.approverId === currentUserId || level.approver?.userId === currentUserId) &&
+ level.status === 'PENDING'
+ );
+ if (approverLevel) {
+ return 'APPROVER';
+ }
+
+ // Default to spectator
+ return 'SPECTATOR';
+ } catch (error) {
+ console.error('[claimDataMapper] Error determining user role:', error);
+ return 'SPECTATOR';
+ }
+}
+
+/**
+ * Get role-based visibility settings
+ */
+export function getRoleBasedVisibility(role: RequestRole): RoleVisibility {
+ switch (role) {
+ case 'INITIATOR':
+ return {
+ showDealerInfo: true,
+ showProposalDetails: true,
+ showIODetails: true,
+ showDMSDetails: true,
+ showClaimAmount: true,
+ canEditClaimAmount: false, // Can only edit in specific scenarios
+ };
+
+ case 'DEALER':
+ return {
+ showDealerInfo: true,
+ showProposalDetails: true,
+ showIODetails: false,
+ showDMSDetails: false,
+ showClaimAmount: true,
+ canEditClaimAmount: false,
+ };
+
+ case 'DEPARTMENT_LEAD':
+ return {
+ showDealerInfo: true,
+ showProposalDetails: true,
+ showIODetails: true,
+ showDMSDetails: true,
+ showClaimAmount: true,
+ canEditClaimAmount: false,
+ };
+
+ case 'APPROVER':
+ return {
+ showDealerInfo: true,
+ showProposalDetails: true,
+ showIODetails: true,
+ showDMSDetails: true,
+ showClaimAmount: true,
+ canEditClaimAmount: false,
+ };
+
+ case 'SPECTATOR':
+ default:
+ return {
+ showDealerInfo: false,
+ showProposalDetails: false,
+ showIODetails: false,
+ showDMSDetails: false,
+ showClaimAmount: false,
+ canEditClaimAmount: false,
+ };
+ }
+}
+
diff --git a/src/utils/claimRequestUtils.ts b/src/utils/claimRequestUtils.ts
new file mode 100644
index 0000000..8deb17e
--- /dev/null
+++ b/src/utils/claimRequestUtils.ts
@@ -0,0 +1,57 @@
+/**
+ * Utility functions for identifying and handling Claim Management requests
+ * Works with both old format (templateType: 'claim-management') and new format (workflowType: 'CLAIM_MANAGEMENT')
+ */
+
+/**
+ * Check if a request is a Claim Management request
+ * Supports both old and new backend formats
+ */
+export function isClaimManagementRequest(request: any): boolean {
+ if (!request) return false;
+
+ // New format: Check workflowType
+ if (request.workflowType === 'CLAIM_MANAGEMENT') {
+ return true;
+ }
+
+ // Old format: Check templateType (for backward compatibility)
+ if (request.templateType === 'claim-management' || request.template === 'claim-management') {
+ return true;
+ }
+
+ // Check template name/code
+ if (request.templateName === 'Claim Management' || request.templateCode === 'CLAIM_MANAGEMENT') {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Get workflow type from request
+ * Returns 'CLAIM_MANAGEMENT' for claim requests, 'NON_TEMPLATIZED' for others
+ */
+export function getWorkflowType(request: any): string {
+ if (!request) return 'NON_TEMPLATIZED';
+
+ // New format
+ if (request.workflowType) {
+ return request.workflowType;
+ }
+
+ // Old format: Map templateType to workflowType
+ if (request.templateType === 'claim-management' || request.template === 'claim-management') {
+ return 'CLAIM_MANAGEMENT';
+ }
+
+ return 'NON_TEMPLATIZED';
+}
+
+/**
+ * Check if request needs claim-specific UI components
+ */
+export function shouldUseClaimManagementUI(request: any): boolean {
+ return isClaimManagementRequest(request);
+}
+