From 0e9f8adbf67bca78f90b0b9e41889279f3c0c63f Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Tue, 9 Dec 2025 20:48:08 +0530 Subject: [PATCH 1/5] claim management related tabls added dealers seeded untilt real dealers available tdb droped multiple times to make fresh setup --- docs/FRONTEND_CLAIM_MANAGEMENT_UPDATES.md | 231 +++++ src/App.tsx | 47 +- .../ClaimManagementWizard.tsx | 80 +- src/hooks/useRequestDetails.ts | 111 ++ src/pages/RequestDetail/RequestDetail.tsx | 93 +- .../claim-cards/ActivityInformationCard.tsx | 177 ++++ .../claim-cards/DealerInformationCard.tsx | 100 ++ .../claim-cards/ProcessDetailsCard.tsx | 259 +++++ .../claim-cards/ProposalDetailsCard.tsx | 123 +++ .../claim-cards/RequestInitiatorCard.tsx | 77 ++ .../components/claim-cards/index.ts | 12 + .../modals/DealerProposalSubmissionModal.tsx | 485 +++++++++ .../modals/DeptLeadIOApprovalModal.tsx | 304 ++++++ .../modals/EditClaimAmountModal.tsx | 196 ++++ .../modals/InitiatorProposalApprovalModal.tsx | 443 ++++++++ .../tabs/ClaimManagementOverviewTab.tsx | 184 ++++ .../tabs/ClaimManagementWorkflowTab.tsx | 311 ++++++ .../tabs/DealerClaimWorkflowTab.tsx | 950 ++++++++++++++++++ .../RequestDetail/components/tabs/IOTab.tsx | 435 ++++++++ src/services/dealerApi.ts | 72 ++ src/services/dealerClaimApi.ts | 245 +++++ src/services/workflowApi.ts | 2 +- src/utils/claimDataMapper.ts | 318 ++++++ src/utils/claimRequestUtils.ts | 57 ++ 24 files changed, 5254 insertions(+), 58 deletions(-) create mode 100644 docs/FRONTEND_CLAIM_MANAGEMENT_UPDATES.md create mode 100644 src/pages/RequestDetail/components/claim-cards/ActivityInformationCard.tsx create mode 100644 src/pages/RequestDetail/components/claim-cards/DealerInformationCard.tsx create mode 100644 src/pages/RequestDetail/components/claim-cards/ProcessDetailsCard.tsx create mode 100644 src/pages/RequestDetail/components/claim-cards/ProposalDetailsCard.tsx create mode 100644 src/pages/RequestDetail/components/claim-cards/RequestInitiatorCard.tsx create mode 100644 src/pages/RequestDetail/components/claim-cards/index.ts create mode 100644 src/pages/RequestDetail/components/modals/DealerProposalSubmissionModal.tsx create mode 100644 src/pages/RequestDetail/components/modals/DeptLeadIOApprovalModal.tsx create mode 100644 src/pages/RequestDetail/components/modals/EditClaimAmountModal.tsx create mode 100644 src/pages/RequestDetail/components/modals/InitiatorProposalApprovalModal.tsx create mode 100644 src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx create mode 100644 src/pages/RequestDetail/components/tabs/ClaimManagementWorkflowTab.tsx create mode 100644 src/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab.tsx create mode 100644 src/pages/RequestDetail/components/tabs/IOTab.tsx create mode 100644 src/services/dealerApi.ts create mode 100644 src/services/dealerClaimApi.ts create mode 100644 src/utils/claimDataMapper.ts create mode 100644 src/utils/claimRequestUtils.ts diff --git a/docs/FRONTEND_CLAIM_MANAGEMENT_UPDATES.md b/docs/FRONTEND_CLAIM_MANAGEMENT_UPDATES.md new file mode 100644 index 0000000..df28da6 --- /dev/null +++ b/docs/FRONTEND_CLAIM_MANAGEMENT_UPDATES.md @@ -0,0 +1,231 @@ +# Frontend Updates for Dealer Claim Management + +## Overview + +This document outlines the frontend changes needed to support the new backend structure with `workflowType` field for dealer claim management. + +## ✅ Completed + +1. **Created utility function** (`src/utils/claimRequestUtils.ts`) + - `isClaimManagementRequest()` - Checks if request is claim management (supports both old and new formats) + - `getWorkflowType()` - Gets workflow type from request + - `shouldUseClaimManagementUI()` - Determines if claim-specific UI should be used + +## 🔧 Required Updates + +### 1. Update RequestDetail Component + +**File**: `src/pages/RequestDetail/RequestDetail.tsx` + +**Changes Needed**: +- Import `isClaimManagementRequest` from `@/utils/claimRequestUtils` +- Conditionally render `ClaimManagementOverviewTab` instead of `OverviewTab` when it's a claim management request +- Conditionally render `DealerClaimWorkflowTab` instead of `WorkflowTab` when it's a claim management request + +**Example**: +```typescript +import { isClaimManagementRequest } from '@/utils/claimRequestUtils'; +import { ClaimManagementOverviewTab } from './components/tabs/ClaimManagementOverviewTab'; +import { DealerClaimWorkflowTab } from './components/tabs/DealerClaimWorkflowTab'; + +// In the component: +const isClaimManagement = isClaimManagementRequest(apiRequest); + + + {isClaimManagement ? ( + + ) : ( + + )} + + + + {isClaimManagement ? ( + + ) : ( + + )} + +``` + +### 2. Create Missing Utility Functions + +**File**: `src/pages/RequestDetail/utils/claimDataMapper.ts` (NEW FILE) + +**Functions Needed**: +- `mapToClaimManagementRequest(apiRequest, userId)` - Maps backend API response to claim management structure +- `determineUserRole(apiRequest, userId)` - Determines user's role (INITIATOR, DEALER, APPROVER, SPECTATOR) +- Helper functions to extract claim-specific data from API response + +**Structure**: +```typescript +export interface ClaimManagementRequest { + activityInfo: { + activityName: string; + activityType: string; + activityDate: string; + location: string; + periodStart?: string; + periodEnd?: string; + estimatedBudget?: number; + closedExpensesBreakdown?: any[]; + }; + dealerInfo: { + dealerCode: string; + dealerName: string; + dealerEmail?: string; + dealerPhone?: string; + dealerAddress?: string; + }; + proposalDetails?: { + costBreakup: any[]; + totalEstimatedBudget: number; + timeline: string; + dealerComments?: string; + }; + ioDetails?: { + ioNumber?: string; + availableBalance?: number; + blockedAmount?: number; + remainingBalance?: number; + }; + dmsDetails?: { + dmsNumber?: string; + eInvoiceNumber?: string; + creditNoteNumber?: string; + }; + claimAmount?: number; +} + +export function mapToClaimManagementRequest( + apiRequest: any, + userId: string +): ClaimManagementRequest | null { + // Extract data from apiRequest.claimDetails (from backend) + // Map to ClaimManagementRequest structure +} + +export function determineUserRole( + apiRequest: any, + userId: string +): 'INITIATOR' | 'DEALER' | 'APPROVER' | 'SPECTATOR' { + // Check if user is initiator + // Check if user is dealer (from participants or claimDetails) + // Check if user is approver (from approval levels) + // Check if user is spectator (from participants) +} +``` + +### 3. Update ClaimManagementOverviewTab + +**File**: `src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx` + +**Changes Needed**: +- Import the new utility functions from `claimDataMapper.ts` +- Remove TODO comments +- Ensure it properly handles both old and new API response formats + +### 4. Update API Service + +**File**: `src/services/workflowApi.ts` + +**Changes Needed**: +- Add `workflowType` field to `CreateWorkflowFromFormPayload` interface +- Include `workflowType: 'CLAIM_MANAGEMENT'` when creating claim management requests +- Update response types to include `workflowType` field + +**Example**: +```typescript +export interface CreateWorkflowFromFormPayload { + templateId?: string | null; + templateType: 'CUSTOM' | 'TEMPLATE'; + workflowType?: string; // NEW: 'CLAIM_MANAGEMENT' | 'NON_TEMPLATIZED' | etc. + title: string; + // ... rest of fields +} +``` + +### 5. Update ClaimManagementWizard + +**File**: `src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx` + +**Changes Needed**: +- When submitting, include `workflowType: 'CLAIM_MANAGEMENT'` in the payload +- Update to call the new backend API endpoint for creating claim requests + +### 6. Update Request List Components + +**Files**: +- `src/pages/MyRequests/components/RequestCard.tsx` +- `src/pages/Requests/components/RequestCard.tsx` +- `src/pages/OpenRequests/components/RequestCard.tsx` + +**Changes Needed**: +- Display template/workflow type badge based on `workflowType` field +- Support both old (`templateType`) and new (`workflowType`) formats + +**Example**: +```typescript +const workflowType = request.workflowType || + (request.templateType === 'claim-management' ? 'CLAIM_MANAGEMENT' : 'NON_TEMPLATIZED'); + +{workflowType === 'CLAIM_MANAGEMENT' && ( + Claim Management +)} +``` + +### 7. Create API Service for Claim Management + +**File**: `src/services/dealerClaimApi.ts` (NEW FILE) + +**Endpoints Needed**: +- `createClaimRequest(data)` - POST `/api/v1/dealer-claims` +- `getClaimDetails(requestId)` - GET `/api/v1/dealer-claims/:requestId` +- `submitProposal(requestId, data)` - POST `/api/v1/dealer-claims/:requestId/proposal` +- `submitCompletion(requestId, data)` - POST `/api/v1/dealer-claims/:requestId/completion` +- `updateIODetails(requestId, data)` - PUT `/api/v1/dealer-claims/:requestId/io` + +### 8. Update Type Definitions + +**File**: `src/types/index.ts` + +**Changes Needed**: +- Add `workflowType?: string` to request interfaces +- Update `ClaimFormData` interface to match backend structure + +## 📋 Implementation Order + +1. ✅ Create `claimRequestUtils.ts` (DONE) +2. ⏳ Create `claimDataMapper.ts` with mapping functions +3. ⏳ Update `RequestDetail.tsx` to conditionally render claim components +4. ⏳ Update `workflowApi.ts` to include `workflowType` +5. ⏳ Update `ClaimManagementWizard.tsx` to send `workflowType` +6. ⏳ Create `dealerClaimApi.ts` for claim-specific endpoints +7. ⏳ Update request card components to show workflow type +8. ⏳ Test end-to-end flow + +## 🔄 Backward Compatibility + +The frontend should support both: +- **Old Format**: `templateType: 'claim-management'` +- **New Format**: `workflowType: 'CLAIM_MANAGEMENT'` + +The `isClaimManagementRequest()` utility function handles both formats automatically. + +## 📝 Notes + +- All existing claim management UI components are already created +- The main work is connecting them to the new backend API structure +- The `workflowType` field allows the system to support multiple template types in the future +- All requests (claim management, non-templatized, future templates) will appear in "My Requests" and "Open Requests" automatically + diff --git a/src/App.tsx b/src/App.tsx index 4bce3ae..f279a75 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import { toast } from 'sonner'; import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase'; import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; import { AuthCallback } from '@/pages/Auth/AuthCallback'; +import { createClaimRequest } from '@/services/dealerClaimApi'; // Combined Request Database for backward compatibility // This combines both custom and claim management requests @@ -265,7 +266,50 @@ function AppRoutes({ onLogout }: AppProps) { setApprovalAction(null); }; - const handleClaimManagementSubmit = (claimData: any) => { + const handleClaimManagementSubmit = async (claimData: any) => { + try { + // Prepare payload for API + const payload = { + activityName: claimData.activityName, + activityType: claimData.activityType, + dealerCode: claimData.dealerCode, + dealerName: claimData.dealerName, + dealerEmail: claimData.dealerEmail || undefined, + dealerPhone: claimData.dealerPhone || undefined, + dealerAddress: claimData.dealerAddress || undefined, + activityDate: claimData.activityDate ? new Date(claimData.activityDate).toISOString() : undefined, + location: claimData.location, + requestDescription: claimData.requestDescription, + periodStartDate: claimData.periodStartDate ? new Date(claimData.periodStartDate).toISOString() : undefined, + periodEndDate: claimData.periodEndDate ? new Date(claimData.periodEndDate).toISOString() : undefined, + estimatedBudget: claimData.estimatedBudget || undefined, + }; + + // Call API to create claim request + const response = await createClaimRequest(payload); + const createdRequest = response.request; + + toast.success('Claim Request Submitted', { + description: 'Your claim management request has been created successfully.', + }); + + // Navigate to the created request detail page + if (createdRequest?.requestId) { + navigate(`/request/${createdRequest.requestId}`); + } else { + navigate('/my-requests'); + } + } catch (error: any) { + console.error('[App] Error creating claim request:', error); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to create claim request'; + toast.error('Failed to Submit Claim Request', { + description: errorMessage, + }); + } + + // Keep the old code below for backward compatibility (local storage fallback) + // This can be removed once API integration is fully tested + /* // Generate unique ID for the new claim request const requestId = `RE-REQ-2024-CM-${String(dynamicRequests.length + 2).padStart(3, '0')}`; @@ -457,6 +501,7 @@ function AppRoutes({ onLogout }: AppProps) { description: 'Your claim management request has been created successfully.', }); navigate('/my-requests'); + */ }; return ( diff --git a/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx b/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx index a1a2432..9e89993 100644 --- a/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx +++ b/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -25,7 +25,7 @@ import { } from 'lucide-react'; import { format } from 'date-fns'; import { toast } from 'sonner'; -import { getAllDealers, getDealerInfo, formatDealerAddress } from '@/utils/dealerDatabase'; +import { getAllDealers as fetchDealersFromAPI, getDealerByCode, type DealerInfo } from '@/services/dealerApi'; interface ClaimManagementWizardProps { onBack?: () => void; @@ -41,9 +41,6 @@ const CLAIM_TYPES = [ 'Service Campaign' ]; -// Fetch dealers from database -const DEALERS = getAllDealers(); - const STEP_NAMES = [ 'Claim Details', 'Review & Submit' @@ -51,6 +48,8 @@ const STEP_NAMES = [ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizardProps) { const [currentStep, setCurrentStep] = useState(1); + const [dealers, setDealers] = useState([]); + const [loadingDealers, setLoadingDealers] = useState(true); const [formData, setFormData] = useState({ activityName: '', @@ -70,6 +69,23 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar const totalSteps = STEP_NAMES.length; + // Fetch dealers from API on component mount + useEffect(() => { + const fetchDealers = async () => { + setLoadingDealers(true); + try { + const fetchedDealers = await fetchDealersFromAPI(); + setDealers(fetchedDealers); + } catch (error) { + toast.error('Failed to load dealer list.'); + console.error('Error fetching dealers:', error); + } finally { + setLoadingDealers(false); + } + }; + fetchDealers(); + }, []); + const updateFormData = (field: string, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); }; @@ -103,14 +119,26 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar } }; - const handleDealerChange = (dealerCode: string) => { - const dealer = getDealerInfo(dealerCode); - if (dealer) { - updateFormData('dealerCode', dealer.code); - updateFormData('dealerName', dealer.name); - updateFormData('dealerEmail', dealer.email); - updateFormData('dealerPhone', dealer.phone); - updateFormData('dealerAddress', formatDealerAddress(dealer)); + const handleDealerChange = async (dealerCode: string) => { + const selectedDealer = dealers.find(d => d.dealerCode === dealerCode); + if (selectedDealer) { + updateFormData('dealerCode', dealerCode); + updateFormData('dealerName', selectedDealer.dealerName); + updateFormData('dealerEmail', selectedDealer.email || ''); + updateFormData('dealerPhone', selectedDealer.phone || ''); + updateFormData('dealerAddress', ''); // Address not available in API response + + // Try to fetch full dealer info from API if available + try { + const fullDealerInfo = await getDealerByCode(dealerCode); + if (fullDealerInfo) { + updateFormData('dealerEmail', fullDealerInfo.email || selectedDealer.email || ''); + updateFormData('dealerPhone', fullDealerInfo.phone || selectedDealer.phone || ''); + } + } catch (error) { + // Ignore error, use basic info from list + console.debug('Could not fetch full dealer info:', error); + } } }; @@ -235,9 +263,9 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar {/* Dealer Selection */}
- - + {formData.dealerCode && (
{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 */} +
+ + {dealerInfo.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 */} +
+ +
+ + + + + + + + + {proposalDetails.costBreakup.map((item, index) => ( + + + + + ))} + + + + + +
+ Item Description + + Amount +
+ {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 ( + + + + + + Dealer Proposal Submission + + + Step 1: Upload proposal and planning details + +
+
+ Dealer: {dealerName} +
+
+ Activity: {activityName} +
+
+ Please upload proposal document, provide cost breakdown, timeline, and detailed comments. +
+
+
+ +
+ {/* Proposal Document Section */} +
+
+

Proposal Document

+ Required +
+
+ +

+ Detailed proposal with activity details and requested information +

+
+ + +
+
+
+ + {/* Cost Breakup Section */} +
+
+
+

Cost Breakup

+ Required +
+ +
+
+ {costItems.map((item) => ( +
+
+ + handleCostItemChange(item.id, 'description', e.target.value) + } + /> +
+
+ + handleCostItemChange(item.id, 'amount', e.target.value) + } + /> +
+ +
+ ))} +
+
+
+
+ + Estimated Budget +
+
+ ₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+
+
+
+ + {/* Timeline for Closure Section */} +
+
+

Timeline for Closure

+ Required +
+
+
+ + +
+ {timelineMode === 'date' ? ( +
+ + setExpectedCompletionDate(e.target.value)} + /> +
+ ) : ( +
+ + setNumberOfDays(e.target.value)} + /> +
+ )} +
+
+ + {/* Other Supporting Documents Section */} +
+
+

Other Supporting Documents

+ Optional +
+
+ +

+ Any other supporting documents (invoices, receipts, photos, etc.) +

+
+ + +
+ {otherDocuments.length > 0 && ( +
+ {otherDocuments.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+ )} +
+
+ + {/* Dealer Comments Section */} +
+ +