From 69c7e99d18ff5300add77be8a53fdce2a34dce59 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Wed, 10 Dec 2025 20:45:26 +0530 Subject: [PATCH] all modal ui added for dealerclaim wokflow step checked all 8 steps --- .../ClaimManagementWizard.tsx | 20 +- src/hooks/useRequestDetails.ts | 9 + .../components/modals/CreditNoteSAPModal.tsx | 270 ++++++++ .../modals/DealerCompletionDocumentsModal.tsx | 616 ++++++++++++++++++ .../modals/InitiatorProposalApprovalModal.tsx | 2 +- .../modals/README_CREDIT_NOTE_MODAL.md | 219 +++++++ .../tabs/DealerClaimWorkflowTab.tsx | 390 +++++++++-- src/services/dealerClaimApi.ts | 22 +- src/utils/claimDataMapper.ts | 19 +- 9 files changed, 1492 insertions(+), 75 deletions(-) create mode 100644 src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx create mode 100644 src/pages/RequestDetail/components/modals/DealerCompletionDocumentsModal.tsx create mode 100644 src/pages/RequestDetail/components/modals/README_CREDIT_NOTE_MODAL.md diff --git a/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx b/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx index 9e89993..d505fdf 100644 --- a/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx +++ b/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx @@ -279,15 +279,17 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar {dealers.length === 0 && !loadingDealers ? (
No dealers available
) : ( - dealers.map((dealer) => ( - -
- {dealer.dealerCode} - - {dealer.dealerName} -
-
- )) + dealers + .filter((dealer) => dealer.dealerCode && dealer.dealerCode.trim() !== '') + .map((dealer) => ( + +
+ {dealer.dealerCode} + + {dealer.dealerName} +
+
+ )) )} diff --git a/src/hooks/useRequestDetails.ts b/src/hooks/useRequestDetails.ts index 062c7c1..95ef983 100644 --- a/src/hooks/useRequestDetails.ts +++ b/src/hooks/useRequestDetails.ts @@ -236,6 +236,7 @@ export function useRequestDetails( let claimDetails = null; let proposalDetails = null; let completionDetails = null; + let internalOrder = null; if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') { try { @@ -255,12 +256,14 @@ export function useRequestDetails( hasClaimDetails: !!(claimData?.claimDetails || claimData?.claim_details), hasProposalDetails: !!(claimData?.proposalDetails || claimData?.proposal_details), hasCompletionDetails: !!(claimData?.completionDetails || claimData?.completion_details), + hasInternalOrder: !!(claimData?.internalOrder || claimData?.internal_order), }); if (claimData) { claimDetails = claimData.claimDetails || claimData.claim_details; proposalDetails = claimData.proposalDetails || claimData.proposal_details; completionDetails = claimData.completionDetails || claimData.completion_details; + internalOrder = claimData.internalOrder || claimData.internal_order || null; console.debug('[useRequestDetails] Extracted details:', { claimDetails: claimDetails ? { @@ -274,6 +277,7 @@ export function useRequestDetails( } : null, hasProposalDetails: !!proposalDetails, hasCompletionDetails: !!completionDetails, + hasInternalOrder: !!internalOrder, }); } else { console.warn('[useRequestDetails] No claimData found in response'); @@ -332,6 +336,7 @@ export function useRequestDetails( claimDetails: claimDetails || null, proposalDetails: proposalDetails || null, completionDetails: completionDetails || null, + internalOrder: internalOrder || null, }; setApiRequest(updatedRequest); @@ -513,6 +518,7 @@ export function useRequestDetails( let claimDetails = null; let proposalDetails = null; let completionDetails = null; + let internalOrder = null; if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') { try { @@ -529,12 +535,14 @@ export function useRequestDetails( claimDetails = claimData.claimDetails || claimData.claim_details; proposalDetails = claimData.proposalDetails || claimData.proposal_details; completionDetails = claimData.completionDetails || claimData.completion_details; + internalOrder = claimData.internalOrder || claimData.internal_order || null; console.debug('[useRequestDetails] Initial load - Extracted details:', { hasClaimDetails: !!claimDetails, claimDetailsKeys: claimDetails ? Object.keys(claimDetails) : [], hasProposalDetails: !!proposalDetails, hasCompletionDetails: !!completionDetails, + hasInternalOrder: !!internalOrder, }); } } catch (error: any) { @@ -583,6 +591,7 @@ export function useRequestDetails( claimDetails: claimDetails || null, proposalDetails: proposalDetails || null, completionDetails: completionDetails || null, + internalOrder: internalOrder || null, }; setApiRequest(mapped); diff --git a/src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx b/src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx new file mode 100644 index 0000000..e7ad310 --- /dev/null +++ b/src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx @@ -0,0 +1,270 @@ +/** + * CreditNoteSAPModal Component + * Modal for Step 8: Credit Note from SAP + * Allows Finance team to review credit note details and send to dealer + */ + +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Label } from '@/components/ui/label'; +import { Receipt, CircleCheckBig, Hash, Calendar, DollarSign, Building, FileText, Download, Send } from 'lucide-react'; +import { toast } from 'sonner'; +import { formatDateTime } from '@/utils/dateFormatter'; + +interface CreditNoteSAPModalProps { + isOpen: boolean; + onClose: () => void; + onDownload?: () => Promise; + onSendToDealer?: () => Promise; + creditNoteData?: { + creditNoteNumber?: string; + creditNoteDate?: string; + creditNoteAmount?: number; + status?: 'PENDING' | 'APPROVED' | 'ISSUED' | 'SENT'; + }; + dealerInfo?: { + dealerName?: string; + dealerCode?: string; + dealerEmail?: string; + }; + activityName?: string; + requestNumber?: string; + requestId?: string; + dueDate?: string; +} + +export function CreditNoteSAPModal({ + isOpen, + onClose, + onDownload, + onSendToDealer, + creditNoteData, + dealerInfo, + activityName, + requestNumber, + requestId, + dueDate, +}: CreditNoteSAPModalProps) { + const [downloading, setDownloading] = useState(false); + const [sending, setSending] = useState(false); + + const creditNoteNumber = creditNoteData?.creditNoteNumber || 'CN-RE-REQ-2024-CM-101-312580'; + const creditNoteDate = creditNoteData?.creditNoteDate + ? formatDateTime(creditNoteData.creditNoteDate, { includeTime: false, format: 'short' }) + : 'Dec 5, 2025'; + const creditNoteAmount = creditNoteData?.creditNoteAmount || 800; + const status = creditNoteData?.status || 'APPROVED'; + + const dealerName = dealerInfo?.dealerName || 'Jaipur Royal Enfield'; + const dealerCode = dealerInfo?.dealerCode || 'RE-JP-009'; + const activity = activityName || 'Activity'; + const requestIdDisplay = requestNumber || 'RE-REQ-2024-CM-101'; + const dueDateDisplay = dueDate + ? formatDateTime(dueDate, { includeTime: false, format: 'short' }) + : 'Jan 4, 2026'; + + const handleDownload = async () => { + if (onDownload) { + try { + setDownloading(true); + await onDownload(); + toast.success('Credit note downloaded successfully'); + } catch (error) { + console.error('Failed to download credit note:', error); + toast.error('Failed to download credit note. Please try again.'); + } finally { + setDownloading(false); + } + } else { + // Default behavior: show info message + toast.info('Credit note will be automatically saved to Documents tab'); + } + }; + + const handleSendToDealer = async () => { + if (onSendToDealer) { + try { + setSending(true); + await onSendToDealer(); + toast.success('Credit note sent to dealer successfully'); + onClose(); + } catch (error) { + console.error('Failed to send credit note to dealer:', error); + toast.error('Failed to send credit note. Please try again.'); + } finally { + setSending(false); + } + } else { + // Default behavior: show info message + toast.info('Email notification will be sent to dealer with credit note attachment'); + } + }; + + const formatCurrency = (amount: number) => { + return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; + }; + + return ( + + + + + + Credit Note from SAP + + + Review and send credit note to dealer + + + +
+ {/* Credit Note Document Card */} +
+
+
+

Royal Enfield

+

Credit Note Document

+
+ + + {status === 'APPROVED' ? 'Approved' : status === 'ISSUED' ? 'Issued' : status === 'SENT' ? 'Sent' : 'Pending'} + +
+
+
+ +

{creditNoteNumber}

+
+
+ +

{creditNoteDate}

+
+
+
+ + {/* Credit Note Amount */} +
+ +

{formatCurrency(creditNoteAmount)}

+
+ + {/* Dealer Information */} +
+

+ + Dealer Information +

+
+
+ +

{dealerName}

+
+
+ +

{dealerCode}

+
+
+ +

{activity}

+
+
+
+ + {/* Reference Details */} +
+

+ + Reference Details +

+
+
+ +

{requestIdDisplay}

+
+
+ +

{dueDateDisplay}

+
+
+
+ + {/* Available Actions Info */} +
+ +
+

Available Actions

+
    +
  • + Download: Credit note will be automatically saved to Documents tab +
  • +
  • + Send to Dealer: Email notification will be sent to dealer with credit note attachment +
  • +
  • All actions will be recorded in activity trail for audit purposes
  • +
+
+
+
+ + + +
+ + +
+
+
+
+ ); +} + diff --git a/src/pages/RequestDetail/components/modals/DealerCompletionDocumentsModal.tsx b/src/pages/RequestDetail/components/modals/DealerCompletionDocumentsModal.tsx new file mode 100644 index 0000000..8742235 --- /dev/null +++ b/src/pages/RequestDetail/components/modals/DealerCompletionDocumentsModal.tsx @@ -0,0 +1,616 @@ +/** + * DealerCompletionDocumentsModal Component + * Modal for Step 5: Activity Completion Documents + * Allows dealers to upload completion documents, photos, expenses, and provide completion details + */ + +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, FileText, Image, Receipt, CircleAlert } from 'lucide-react'; +import { toast } from 'sonner'; + +interface ExpenseItem { + id: string; + description: string; + amount: number; +} + +interface DealerCompletionDocumentsModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (data: { + activityCompletionDate: string; + numberOfParticipants?: number; + closedExpenses: ExpenseItem[]; + totalClosedExpenses: number; + completionDocuments: File[]; + activityPhotos: File[]; + invoicesReceipts?: File[]; + attendanceSheet?: File; + completionDescription: string; + }) => Promise; + dealerName?: string; + activityName?: string; + requestId?: string; +} + +export function DealerCompletionDocumentsModal({ + isOpen, + onClose, + onSubmit, + dealerName = 'Jaipur Royal Enfield', + activityName = 'Activity', + requestId, +}: DealerCompletionDocumentsModalProps) { + const [activityCompletionDate, setActivityCompletionDate] = useState(''); + const [numberOfParticipants, setNumberOfParticipants] = useState(''); + const [expenseItems, setExpenseItems] = useState([]); + const [completionDocuments, setCompletionDocuments] = useState([]); + const [activityPhotos, setActivityPhotos] = useState([]); + const [invoicesReceipts, setInvoicesReceipts] = useState([]); + const [attendanceSheet, setAttendanceSheet] = useState(null); + const [completionDescription, setCompletionDescription] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const completionDocsInputRef = useRef(null); + const photosInputRef = useRef(null); + const invoicesInputRef = useRef(null); + const attendanceInputRef = useRef(null); + + // Calculate total closed expenses + const totalClosedExpenses = useMemo(() => { + return expenseItems.reduce((sum, item) => sum + (item.amount || 0), 0); + }, [expenseItems]); + + // Check if all required fields are filled + const isFormValid = useMemo(() => { + const hasCompletionDate = activityCompletionDate !== ''; + const hasDocuments = completionDocuments.length > 0; + const hasPhotos = activityPhotos.length > 0; + const hasDescription = completionDescription.trim().length > 0; + + return hasCompletionDate && hasDocuments && hasPhotos && hasDescription; + }, [activityCompletionDate, completionDocuments, activityPhotos, completionDescription]); + + // Get today's date in YYYY-MM-DD format for max date + const maxDate = new Date().toISOString().split('T')[0]; + + const handleAddExpense = () => { + setExpenseItems([ + ...expenseItems, + { id: Date.now().toString(), description: '', amount: 0 }, + ]); + }; + + const handleExpenseChange = (id: string, field: 'description' | 'amount', value: string | number) => { + setExpenseItems( + expenseItems.map((item) => + item.id === id ? { ...item, [field]: value } : item + ) + ); + }; + + const handleRemoveExpense = (id: string) => { + setExpenseItems(expenseItems.filter((item) => item.id !== id)); + }; + + const handleCompletionDocsChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + // Validate file types + const allowedTypes = ['.pdf', '.doc', '.docx', '.zip', '.rar']; + const invalidFiles = files.filter( + (file) => !allowedTypes.some((ext) => file.name.toLowerCase().endsWith(ext)) + ); + if (invalidFiles.length > 0) { + toast.error('Please upload PDF, DOC, DOCX, ZIP, or RAR files only'); + return; + } + setCompletionDocuments([...completionDocuments, ...files]); + } + }; + + const handleRemoveCompletionDoc = (index: number) => { + setCompletionDocuments(completionDocuments.filter((_, i) => i !== index)); + }; + + const handlePhotosChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + // Validate image files + const invalidFiles = files.filter( + (file) => !file.type.startsWith('image/') + ); + if (invalidFiles.length > 0) { + toast.error('Please upload image files only (JPG, PNG, etc.)'); + return; + } + setActivityPhotos([...activityPhotos, ...files]); + } + }; + + const handleRemovePhoto = (index: number) => { + setActivityPhotos(activityPhotos.filter((_, i) => i !== index)); + }; + + const handleInvoicesChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + // Validate file types + const allowedTypes = ['.pdf', '.jpg', '.jpeg', '.png']; + const invalidFiles = files.filter( + (file) => !allowedTypes.some((ext) => file.name.toLowerCase().endsWith(ext)) + ); + if (invalidFiles.length > 0) { + toast.error('Please upload PDF, JPG, or PNG files only'); + return; + } + setInvoicesReceipts([...invoicesReceipts, ...files]); + } + }; + + const handleRemoveInvoice = (index: number) => { + setInvoicesReceipts(invoicesReceipts.filter((_, i) => i !== index)); + }; + + const handleAttendanceChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Validate file types + const allowedTypes = ['.pdf', '.xlsx', '.xls', '.csv']; + const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); + if (!allowedTypes.includes(fileExtension)) { + toast.error('Please upload PDF, Excel, or CSV files only'); + return; + } + setAttendanceSheet(file); + } + }; + + const handleSubmit = async () => { + if (!isFormValid) { + toast.error('Please fill all required fields'); + return; + } + + // Filter valid expense items + const validExpenses = expenseItems.filter( + (item) => item.description.trim() !== '' && item.amount > 0 + ); + + try { + setSubmitting(true); + await onSubmit({ + activityCompletionDate, + numberOfParticipants: numberOfParticipants ? parseInt(numberOfParticipants) : undefined, + closedExpenses: validExpenses, + totalClosedExpenses, + completionDocuments, + activityPhotos, + invoicesReceipts: invoicesReceipts.length > 0 ? invoicesReceipts : undefined, + attendanceSheet: attendanceSheet || undefined, + completionDescription, + }); + handleReset(); + onClose(); + } catch (error) { + console.error('Failed to submit completion documents:', error); + toast.error('Failed to submit completion documents. Please try again.'); + } finally { + setSubmitting(false); + } + }; + + const handleReset = () => { + setActivityCompletionDate(''); + setNumberOfParticipants(''); + setExpenseItems([]); + setCompletionDocuments([]); + setActivityPhotos([]); + setInvoicesReceipts([]); + setAttendanceSheet(null); + setCompletionDescription(''); + if (completionDocsInputRef.current) completionDocsInputRef.current.value = ''; + if (photosInputRef.current) photosInputRef.current.value = ''; + if (invoicesInputRef.current) invoicesInputRef.current.value = ''; + if (attendanceInputRef.current) attendanceInputRef.current.value = ''; + }; + + const handleClose = () => { + if (!submitting) { + handleReset(); + onClose(); + } + }; + + return ( + + + + + + Activity Completion Documents + + + Step 5: Upload completion proof and final documents + +
+
+ Dealer: {dealerName} +
+
+ Activity: {activityName} +
+
+ Please upload completion documents, photos, and provide details about the completed activity. +
+
+
+ +
+ {/* Activity Completion Date */} +
+ + setActivityCompletionDate(e.target.value)} + /> +
+ + {/* Closed Expenses Section */} +
+
+
+

Closed Expenses

+ Optional +
+ +
+
+ {expenseItems.map((item) => ( +
+
+ + handleExpenseChange(item.id, 'description', e.target.value) + } + /> +
+
+ + handleExpenseChange(item.id, 'amount', parseFloat(e.target.value) || 0) + } + /> +
+ +
+ ))} + {expenseItems.length === 0 && ( +

+ No expenses added. Click "Add Expense" to add expense items. +

+ )} + {expenseItems.length > 0 && totalClosedExpenses > 0 && ( +
+
+ Total Closed Expenses: + + ₹{totalClosedExpenses.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + +
+
+ )} +
+
+ + {/* Completion Evidence Section */} +
+
+

Completion Evidence

+ Required +
+ + {/* Completion Documents */} +
+ +

+ Upload documents proving activity completion (reports, certificates, etc.) - Can upload multiple files or ZIP folder +

+
+ + +
+ {completionDocuments.length > 0 && ( +
+ {completionDocuments.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+ )} +
+ + {/* Activity Photos */} +
+ +

+ Upload photos from the completed activity (event photos, installations, etc.) +

+
+ + +
+ {activityPhotos.length > 0 && ( +
+ {activityPhotos.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+ )} +
+
+ + {/* Supporting Documents Section */} +
+
+

Supporting Documents

+ Optional +
+ + {/* Invoices/Receipts */} +
+ +

+ Upload invoices and receipts for expenses incurred +

+
+ + +
+ {invoicesReceipts.length > 0 && ( +
+ {invoicesReceipts.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+ )} +
+ + {/* Attendance Sheet */} +
+ +

+ Upload attendance records or participant lists (if applicable) +

+
+ + +
+ {attendanceSheet && ( +
+ {attendanceSheet.name} + +
+ )} +
+
+ + {/* Completion Description */} +
+ +