From bbae59e271c508d49078718d84cdfe5610a5828a Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Thu, 18 Dec 2025 21:24:37 +0530 Subject: [PATCH] credit note invoice mapped with webhooks --- .../components/request-detail/OverviewTab.tsx | 185 ++++++++++++++++++ .../components/request-detail/WorkflowTab.tsx | 28 ++- .../request-detail/claim-cards/CloserCard.tsx | 118 +++++++++++ .../request-detail/claim-cards/index.ts | 1 + .../modals/CreditNoteSAPModal.tsx | 138 +++++++------ src/dealer-claim/pages/RequestDetail.tsx | 40 ++++ src/services/dealerClaimApi.ts | 14 ++ 7 files changed, 463 insertions(+), 61 deletions(-) create mode 100644 src/dealer-claim/components/request-detail/claim-cards/CloserCard.tsx diff --git a/src/dealer-claim/components/request-detail/OverviewTab.tsx b/src/dealer-claim/components/request-detail/OverviewTab.tsx index 485a96a..e355b7d 100644 --- a/src/dealer-claim/components/request-detail/OverviewTab.tsx +++ b/src/dealer-claim/components/request-detail/OverviewTab.tsx @@ -18,6 +18,12 @@ import { getRoleBasedVisibility, type RequestRole, } from '@/utils/claimDataMapper'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { RichTextEditor } from '@/components/ui/rich-text-editor'; +import { FormattedDescription } from '@/components/common/FormattedDescription'; +import { CheckCircle, RefreshCw, Loader2 } from 'lucide-react'; +import { formatDateTime } from '@/utils/dateFormatter'; interface ClaimManagementOverviewTabProps { request: any; // Original request object @@ -26,6 +32,15 @@ interface ClaimManagementOverviewTabProps { isInitiator: boolean; onEditClaimAmount?: () => void; className?: string; + // Closure props + needsClosure?: boolean; + conclusionRemark?: string; + setConclusionRemark?: (value: string) => void; + conclusionLoading?: boolean; + conclusionSubmitting?: boolean; + aiGenerated?: boolean; + handleGenerateConclusion?: () => void; + handleFinalizeConclusion?: () => void; } export function ClaimManagementOverviewTab({ @@ -35,6 +50,14 @@ export function ClaimManagementOverviewTab({ isInitiator: _isInitiator, onEditClaimAmount: _onEditClaimAmount, className = '', + needsClosure = false, + conclusionRemark = '', + setConclusionRemark, + conclusionLoading = false, + conclusionSubmitting = false, + aiGenerated = false, + handleGenerateConclusion, + handleFinalizeConclusion, }: ClaimManagementOverviewTabProps) { // Check if this is a claim management request if (!isClaimManagementRequest(apiRequest)) { @@ -98,6 +121,21 @@ export function ClaimManagementOverviewTab({ phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile, }; + // Debug: Log closure props to help troubleshoot + console.debug('[ClaimManagementOverviewTab] Closure setup check:', { + needsClosure, + requestStatus: apiRequest?.status, + requestStatusLower: (apiRequest?.status || '').toLowerCase(), + hasConclusionRemark: !!conclusionRemark, + conclusionRemarkLength: conclusionRemark?.length || 0, + conclusionLoading, + conclusionSubmitting, + aiGenerated, + hasHandleGenerate: !!handleGenerateConclusion, + hasHandleFinalize: !!handleFinalizeConclusion, + hasSetConclusion: !!setConclusionRemark, + }); + return (
{/* Activity Information - Always visible */} @@ -118,6 +156,153 @@ export function ClaimManagementOverviewTab({ {/* Request Initiator */} + + {/* Closed Request Conclusion Remark Display */} + {apiRequest?.status === 'closed' && apiRequest?.conclusionRemark && ( + + + + + Conclusion Remark + + Final summary of this closed request + + +
+ +
+ + {apiRequest.closureDate && ( +
+ Request closed on {formatDateTime(apiRequest.closureDate)} + By {initiatorInfo.name} +
+ )} +
+
+ )} + + {/* Conclusion Remark Section - Closure Setup */} + {needsClosure && ( + + +
+
+ + + Conclusion Remark - Final Step + + + {(apiRequest?.status || '').toLowerCase() === 'rejected' + ? 'This request was rejected. Please review the AI-generated closure remark and finalize it to close this request.' + : 'All approvals are complete. Please review and finalize the conclusion to close this request.'} + +
+ {handleGenerateConclusion && ( + + )} +
+
+ + {conclusionLoading ? ( +
+
+ +

Preparing conclusion remark...

+
+
+ ) : ( +
+
+
+ + {aiGenerated && ( + + ✓ System-generated suggestion (editable) + + )} +
+ + {setConclusionRemark && ( + setConclusionRemark(html)} + placeholder="Enter a professional conclusion remark summarizing the request outcome, key decisions, and approvals..." + className="text-sm" + minHeight="160px" + data-testid="conclusion-remark-textarea" + /> + )} +

+ 💡 Tip: You can paste formatted content (lists, tables) and the formatting will be preserved. +

+ +
+

This will be the final summary for this request

+

+ {conclusionRemark ? conclusionRemark.replace(/<[^>]*>/g, '').length : 0} / 2000 characters +

+
+
+ +
+

Finalizing this request will:

+
    +
  • Change request status to "CLOSED"
  • +
  • Notify all participants of closure
  • +
  • Move request to Closed Requests
  • +
  • Save conclusion remark permanently
  • +
+
+ + {handleFinalizeConclusion && ( +
+ +
+ )} +
+ )} +
+
+ )}
); } diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx index d22f6d4..eef998a 100644 --- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx +++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx @@ -21,7 +21,7 @@ import { CreditNoteSAPModal } from './modals'; import { EmailNotificationTemplateModal } from './modals'; import { DMSPushModal } from './modals'; import { toast } from 'sonner'; -import { submitProposal, updateIODetails, submitCompletion, updateEInvoice } from '@/services/dealerClaimApi'; +import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, sendCreditNoteToDealer } from '@/services/dealerClaimApi'; import { getWorkflowDetails, approveLevel, rejectLevel } from '@/services/workflowApi'; import { uploadDocument } from '@/services/documentApi'; @@ -1271,8 +1271,26 @@ export function DealerClaimWorkflowTab({ toast.info('Download functionality will be implemented'); }} onSendToDealer={async () => { - // TODO: Implement send to dealer functionality - toast.info('Send to dealer functionality will be implemented'); + try { + const requestId = request?.requestId || request?.id; + if (!requestId) { + toast.error('Request ID not found'); + return; + } + + await sendCreditNoteToDealer(requestId); + + toast.success('Credit note sent to dealer successfully. Step 8 has been approved.'); + + // Refresh the request details to show updated status + if (onRefresh) { + onRefresh(); + } + } catch (error: any) { + console.error('Failed to send credit note to dealer:', error); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to send credit note to dealer'; + toast.error(errorMessage); + } }} creditNoteData={{ creditNoteNumber: (request as any)?.creditNote?.creditNoteNumber || @@ -1295,7 +1313,9 @@ export function DealerClaimWorkflowTab({ Number((request as any)?.claimDetails?.creditNoteAmount) : ((request as any)?.claimDetails?.credit_note_amount ? Number((request as any)?.claimDetails?.credit_note_amount) : undefined)))), - status: 'APPROVED', + status: (request as any)?.creditNote?.status || + (request as any)?.claimDetails?.creditNote?.status || + ((request as any)?.creditNote?.creditNoteNumber ? 'CONFIRMED' : 'PENDING'), }} dealerInfo={{ dealerName: (request as any)?.claimDetails?.dealerName || (request as any)?.claimDetails?.dealer_name, diff --git a/src/dealer-claim/components/request-detail/claim-cards/CloserCard.tsx b/src/dealer-claim/components/request-detail/claim-cards/CloserCard.tsx new file mode 100644 index 0000000..fb3cf80 --- /dev/null +++ b/src/dealer-claim/components/request-detail/claim-cards/CloserCard.tsx @@ -0,0 +1,118 @@ +/** + * CloserCard Component + * Displays who closed the request and closure details + */ + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Mail, Calendar, FileText } from 'lucide-react'; +import { formatDateTime } from '@/utils/dateFormatter'; + +interface CloserInfo { + name?: string; + role?: string; + department?: string; + email?: string; + closureDate?: string; + conclusionRemark?: string; +} + +interface CloserCardProps { + closerInfo: CloserInfo; + className?: string; +} + +export function CloserCard({ closerInfo, className }: CloserCardProps) { + // If no closure date, don't render + if (!closerInfo.closureDate) { + return null; + } + + // Generate initials from name + const getInitials = (name?: string) => { + if (!name) return 'CL'; + return name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2); + }; + + const closerName = closerInfo.name || 'System'; + const hasCloserInfo = closerInfo.name || closerInfo.email; + + return ( + + + Request Closer + + + {hasCloserInfo ? ( +
+ + + {getInitials(closerInfo.name)} + + +
+

{closerName}

+ {closerInfo.role && ( +

{closerInfo.role}

+ )} + {closerInfo.department && ( +

{closerInfo.department}

+ )} + +
+ {/* Email */} + {closerInfo.email && ( +
+ + {closerInfo.email} +
+ )} + + {/* Closure Date */} + {closerInfo.closureDate && ( +
+ + Closed on {formatDateTime(closerInfo.closureDate, { includeTime: true, format: 'short' })} +
+ )} +
+
+
+ ) : ( +
+

Request closed by system

+ {closerInfo.closureDate && ( +
+ + Closed on {formatDateTime(closerInfo.closureDate, { includeTime: true, format: 'short' })} +
+ )} +
+ )} + + {/* Conclusion Remark */} + {closerInfo.conclusionRemark && ( +
+
+ +
+

+ Conclusion Remark +

+

+ {closerInfo.conclusionRemark} +

+
+
+
+ )} +
+
+ ); +} + diff --git a/src/dealer-claim/components/request-detail/claim-cards/index.ts b/src/dealer-claim/components/request-detail/claim-cards/index.ts index 655c336..b5da555 100644 --- a/src/dealer-claim/components/request-detail/claim-cards/index.ts +++ b/src/dealer-claim/components/request-detail/claim-cards/index.ts @@ -10,3 +10,4 @@ export { DealerInformationCard } from './DealerInformationCard'; export { ProcessDetailsCard } from './ProcessDetailsCard'; export { ProposalDetailsCard } from './ProposalDetailsCard'; export { RequestInitiatorCard } from './RequestInitiatorCard'; +export { CloserCard } from './CloserCard'; diff --git a/src/dealer-claim/components/request-detail/modals/CreditNoteSAPModal.tsx b/src/dealer-claim/components/request-detail/modals/CreditNoteSAPModal.tsx index 579edca..aace86c 100644 --- a/src/dealer-claim/components/request-detail/modals/CreditNoteSAPModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/CreditNoteSAPModal.tsx @@ -57,12 +57,13 @@ export function CreditNoteSAPModal({ const [downloading, setDownloading] = useState(false); const [sending, setSending] = useState(false); - const creditNoteNumber = creditNoteData?.creditNoteNumber || 'CN-RE-REQ-2024-CM-101-312580'; + const hasCreditNote = creditNoteData?.creditNoteNumber && creditNoteData?.creditNoteNumber !== ''; + const creditNoteNumber = creditNoteData?.creditNoteNumber || ''; 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 creditNoteAmount = creditNoteData?.creditNoteAmount || 0; + const status = creditNoteData?.status || 'PENDING'; const dealerName = dealerInfo?.dealerName || 'Jaipur Royal Enfield'; const dealerCode = dealerInfo?.dealerCode || 'RE-JP-009'; @@ -127,44 +128,63 @@ export function CreditNoteSAPModal({
- {/* Credit Note Document Card */} -
-
-
-

Royal Enfield

-

Credit Note Document

+ {hasCreditNote ? ( + <> + {/* Credit Note Document Card */} +
+
+
+

Royal Enfield

+

Credit Note Document

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

{creditNoteNumber}

+
+
+ +

{creditNoteDate}

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

{creditNoteNumber}

-
-
- -

{creditNoteDate}

-
-
-
- {/* Credit Note Amount */} -
- -

{formatCurrency(creditNoteAmount)}

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

{formatCurrency(creditNoteAmount)}

+
+ + ) : ( + /* No Credit Note Available */ +
+
+
+ +
+
+

No Credit Note Available

+

+ Credit note has not been generated yet. Please wait for the credit note to be generated from DMS. +

+
+
+
+ )} {/* Dealer Information */}
@@ -244,23 +264,27 @@ export function CreditNoteSAPModal({ Close
- - + {hasCreditNote && ( + <> + + + + )}
diff --git a/src/dealer-claim/pages/RequestDetail.tsx b/src/dealer-claim/pages/RequestDetail.tsx index dfe3f35..29c4c26 100644 --- a/src/dealer-claim/pages/RequestDetail.tsx +++ b/src/dealer-claim/pages/RequestDetail.tsx @@ -36,6 +36,7 @@ import { useRequestDetails } from '@/hooks/useRequestDetails'; import { useRequestSocket } from '@/hooks/useRequestSocket'; import { useDocumentUpload } from '@/hooks/useDocumentUpload'; import { useModalManager } from '@/hooks/useModalManager'; +import { useConclusionRemark } from '@/hooks/useConclusionRemark'; import { downloadDocument } from '@/services/workflowApi'; // Dealer Claim Components (import from index to get properly aliased exports) @@ -218,6 +219,37 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam handleAddSpectator, } = useModalManager(requestIdentifier, currentApprovalLevel, refreshDetails); + // Closure functionality - only for initiator when request is approved/rejected + // Check both lowercase and uppercase status values + const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase(); + const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator; + + // Debug logging + console.debug('[DealerClaimRequestDetail] Closure check:', { + requestStatus, + requestStatusRaw: request?.status, + apiRequestStatusRaw: apiRequest?.status, + isInitiator, + needsClosure, + }); + const { + conclusionRemark, + setConclusionRemark, + conclusionLoading, + conclusionSubmitting, + aiGenerated, + handleGenerateConclusion, + handleFinalizeConclusion, + } = useConclusionRemark( + request, + requestIdentifier, + isInitiator, + refreshDetails, + onBack, + setActionStatus, + setShowActionStatusModal + ); + // Auto-switch tab when URL query parameter changes useEffect(() => { const urlParams = new URLSearchParams(window.location.search); @@ -500,6 +532,14 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam apiRequest={apiRequest} currentUserId={currentUserId} isInitiator={isInitiator} + needsClosure={needsClosure} + conclusionRemark={conclusionRemark} + setConclusionRemark={setConclusionRemark} + conclusionLoading={conclusionLoading} + conclusionSubmitting={conclusionSubmitting} + aiGenerated={aiGenerated} + handleGenerateConclusion={handleGenerateConclusion} + handleFinalizeConclusion={handleFinalizeConclusion} /> diff --git a/src/services/dealerClaimApi.ts b/src/services/dealerClaimApi.ts index c3e413b..cf93c85 100644 --- a/src/services/dealerClaimApi.ts +++ b/src/services/dealerClaimApi.ts @@ -290,3 +290,17 @@ export async function updateCreditNote( } } +/** + * Send credit note to dealer and auto-approve Step 8 + * POST /api/v1/dealer-claims/:requestId/credit-note/send + */ +export async function sendCreditNoteToDealer(requestId: string): Promise { + try { + const response = await apiClient.post(`/dealer-claims/${requestId}/credit-note/send`); + return response.data?.data || response.data; + } catch (error: any) { + console.error('[DealerClaimAPI] Error sending credit note to dealer:', error); + throw error; + } +} +