credit note invoice mapped with webhooks

This commit is contained in:
laxmanhalaki 2025-12-18 21:24:37 +05:30
parent ecf2556c64
commit bbae59e271
7 changed files with 463 additions and 61 deletions

View File

@ -18,6 +18,12 @@ import {
getRoleBasedVisibility, getRoleBasedVisibility,
type RequestRole, type RequestRole,
} from '@/utils/claimDataMapper'; } 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 { interface ClaimManagementOverviewTabProps {
request: any; // Original request object request: any; // Original request object
@ -26,6 +32,15 @@ interface ClaimManagementOverviewTabProps {
isInitiator: boolean; isInitiator: boolean;
onEditClaimAmount?: () => void; onEditClaimAmount?: () => void;
className?: string; 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({ export function ClaimManagementOverviewTab({
@ -35,6 +50,14 @@ export function ClaimManagementOverviewTab({
isInitiator: _isInitiator, isInitiator: _isInitiator,
onEditClaimAmount: _onEditClaimAmount, onEditClaimAmount: _onEditClaimAmount,
className = '', className = '',
needsClosure = false,
conclusionRemark = '',
setConclusionRemark,
conclusionLoading = false,
conclusionSubmitting = false,
aiGenerated = false,
handleGenerateConclusion,
handleFinalizeConclusion,
}: ClaimManagementOverviewTabProps) { }: ClaimManagementOverviewTabProps) {
// Check if this is a claim management request // Check if this is a claim management request
if (!isClaimManagementRequest(apiRequest)) { if (!isClaimManagementRequest(apiRequest)) {
@ -98,6 +121,21 @@ export function ClaimManagementOverviewTab({
phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile, 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 ( return (
<div className={`space-y-6 ${className}`}> <div className={`space-y-6 ${className}`}>
{/* Activity Information - Always visible */} {/* Activity Information - Always visible */}
@ -118,6 +156,153 @@ export function ClaimManagementOverviewTab({
{/* Request Initiator */} {/* Request Initiator */}
<RequestInitiatorCard initiatorInfo={initiatorInfo} /> <RequestInitiatorCard initiatorInfo={initiatorInfo} />
{/* Closed Request Conclusion Remark Display */}
{apiRequest?.status === 'closed' && apiRequest?.conclusionRemark && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<CheckCircle className="w-5 h-5 text-gray-600" />
Conclusion Remark
</CardTitle>
<CardDescription className="mt-1 text-xs sm:text-sm">Final summary of this closed request</CardDescription>
</CardHeader>
<CardContent className="pt-4">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<FormattedDescription
content={apiRequest.conclusionRemark || ''}
className="text-sm"
/>
</div>
{apiRequest.closureDate && (
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 border-t border-gray-200 pt-3">
<span>Request closed on {formatDateTime(apiRequest.closureDate)}</span>
<span>By {initiatorInfo.name}</span>
</div>
)}
</CardContent>
</Card>
)}
{/* Conclusion Remark Section - Closure Setup */}
{needsClosure && (
<Card data-testid="conclusion-remark-card">
<CardHeader className={`bg-gradient-to-r border-b ${
(apiRequest?.status || '').toLowerCase() === 'rejected'
? 'from-red-50 to-rose-50 border-red-200'
: 'from-green-50 to-emerald-50 border-green-200'
}`}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<CardTitle className={`flex items-center gap-2 text-base sm:text-lg ${
(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-700' : 'text-green-700'
}`}>
<CheckCircle className={`w-5 h-5 ${
(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-600' : 'text-green-600'
}`} />
Conclusion Remark - Final Step
</CardTitle>
<CardDescription className="mt-1 text-xs sm:text-sm">
{(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.'}
</CardDescription>
</div>
{handleGenerateConclusion && (
<Button
variant="outline"
size="sm"
onClick={handleGenerateConclusion}
disabled={conclusionLoading}
className="gap-2 shrink-0"
data-testid="generate-ai-conclusion-button"
>
<RefreshCw className={`w-3.5 h-3.5 ${conclusionLoading ? 'animate-spin' : ''}`} />
{aiGenerated ? 'Regenerate' : 'Generate with AI'}
</Button>
)}
</div>
</CardHeader>
<CardContent className="pt-4">
{conclusionLoading ? (
<div className="flex items-center justify-center py-8" data-testid="conclusion-loading">
<div className="text-center">
<Loader2 className="w-8 h-8 text-blue-600 animate-spin mx-auto mb-2" />
<p className="text-sm text-gray-600">Preparing conclusion remark...</p>
</div>
</div>
) : (
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700">Conclusion Remark</label>
{aiGenerated && (
<span className="text-xs text-blue-600" data-testid="ai-generated-label">
System-generated suggestion (editable)
</span>
)}
</div>
{setConclusionRemark && (
<RichTextEditor
value={conclusionRemark}
onChange={(html) => 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"
/>
)}
<p className="text-xs text-blue-600 mt-1">
💡 Tip: You can paste formatted content (lists, tables) and the formatting will be preserved.
</p>
<div className="flex items-center justify-between mt-2">
<p className="text-xs text-gray-500">This will be the final summary for this request</p>
<p className="text-xs text-gray-500" data-testid="character-count">
{conclusionRemark ? conclusionRemark.replace(/<[^>]*>/g, '').length : 0} / 2000 characters
</p>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-xs sm:text-sm font-semibold text-blue-900 mb-1.5">Finalizing this request will:</p>
<ul className="text-xs sm:text-sm text-blue-800 space-y-0.5 pl-4">
<li className="list-disc">Change request status to "CLOSED"</li>
<li className="list-disc">Notify all participants of closure</li>
<li className="list-disc">Move request to Closed Requests</li>
<li className="list-disc">Save conclusion remark permanently</li>
</ul>
</div>
{handleFinalizeConclusion && (
<div className="flex gap-3 justify-end pt-3 border-t">
<Button
onClick={handleFinalizeConclusion}
disabled={conclusionSubmitting || !conclusionRemark.trim()}
className="bg-green-600 hover:bg-green-700 text-white"
data-testid="finalize-close-button"
>
{conclusionSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Finalizing...
</>
) : (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Finalize & Close Request
</>
)}
</Button>
</div>
)}
</div>
)}
</CardContent>
</Card>
)}
</div> </div>
); );
} }

View File

@ -21,7 +21,7 @@ import { CreditNoteSAPModal } from './modals';
import { EmailNotificationTemplateModal } from './modals'; import { EmailNotificationTemplateModal } from './modals';
import { DMSPushModal } from './modals'; import { DMSPushModal } from './modals';
import { toast } from 'sonner'; 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 { getWorkflowDetails, approveLevel, rejectLevel } from '@/services/workflowApi';
import { uploadDocument } from '@/services/documentApi'; import { uploadDocument } from '@/services/documentApi';
@ -1271,8 +1271,26 @@ export function DealerClaimWorkflowTab({
toast.info('Download functionality will be implemented'); toast.info('Download functionality will be implemented');
}} }}
onSendToDealer={async () => { onSendToDealer={async () => {
// TODO: Implement send to dealer functionality try {
toast.info('Send to dealer functionality will be implemented'); 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={{ creditNoteData={{
creditNoteNumber: (request as any)?.creditNote?.creditNoteNumber || creditNoteNumber: (request as any)?.creditNote?.creditNoteNumber ||
@ -1295,7 +1313,9 @@ export function DealerClaimWorkflowTab({
Number((request as any)?.claimDetails?.creditNoteAmount) : Number((request as any)?.claimDetails?.creditNoteAmount) :
((request as any)?.claimDetails?.credit_note_amount ? ((request as any)?.claimDetails?.credit_note_amount ?
Number((request as any)?.claimDetails?.credit_note_amount) : undefined)))), 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={{ dealerInfo={{
dealerName: (request as any)?.claimDetails?.dealerName || (request as any)?.claimDetails?.dealer_name, dealerName: (request as any)?.claimDetails?.dealerName || (request as any)?.claimDetails?.dealer_name,

View File

@ -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 (
<Card className={className}>
<CardHeader>
<CardTitle className="text-base">Request Closer</CardTitle>
</CardHeader>
<CardContent>
{hasCloserInfo ? (
<div className="flex items-start gap-4">
<Avatar className="h-14 w-14 ring-2 ring-white shadow-md">
<AvatarFallback className="bg-green-700 text-white font-semibold text-lg">
{getInitials(closerInfo.name)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{closerName}</h3>
{closerInfo.role && (
<p className="text-sm text-gray-600">{closerInfo.role}</p>
)}
{closerInfo.department && (
<p className="text-sm text-gray-500">{closerInfo.department}</p>
)}
<div className="mt-3 space-y-2">
{/* Email */}
{closerInfo.email && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Mail className="w-4 h-4" />
<span>{closerInfo.email}</span>
</div>
)}
{/* Closure Date */}
{closerInfo.closureDate && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Closed on {formatDateTime(closerInfo.closureDate, { includeTime: true, format: 'short' })}</span>
</div>
)}
</div>
</div>
</div>
) : (
<div className="space-y-2">
<p className="text-sm text-gray-600">Request closed by system</p>
{closerInfo.closureDate && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Closed on {formatDateTime(closerInfo.closureDate, { includeTime: true, format: 'short' })}</span>
</div>
)}
</div>
)}
{/* Conclusion Remark */}
{closerInfo.conclusionRemark && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex items-start gap-2">
<FileText className="w-4 h-4 text-gray-500 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs font-medium text-gray-600 uppercase tracking-wider mb-1">
Conclusion Remark
</p>
<p className="text-sm text-gray-700 whitespace-pre-wrap">
{closerInfo.conclusionRemark}
</p>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -10,3 +10,4 @@ export { DealerInformationCard } from './DealerInformationCard';
export { ProcessDetailsCard } from './ProcessDetailsCard'; export { ProcessDetailsCard } from './ProcessDetailsCard';
export { ProposalDetailsCard } from './ProposalDetailsCard'; export { ProposalDetailsCard } from './ProposalDetailsCard';
export { RequestInitiatorCard } from './RequestInitiatorCard'; export { RequestInitiatorCard } from './RequestInitiatorCard';
export { CloserCard } from './CloserCard';

View File

@ -57,12 +57,13 @@ export function CreditNoteSAPModal({
const [downloading, setDownloading] = useState(false); const [downloading, setDownloading] = useState(false);
const [sending, setSending] = 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 const creditNoteDate = creditNoteData?.creditNoteDate
? formatDateTime(creditNoteData.creditNoteDate, { includeTime: false, format: 'short' }) ? formatDateTime(creditNoteData.creditNoteDate, { includeTime: false, format: 'short' })
: 'Dec 5, 2025'; : '';
const creditNoteAmount = creditNoteData?.creditNoteAmount || 800; const creditNoteAmount = creditNoteData?.creditNoteAmount || 0;
const status = creditNoteData?.status || 'APPROVED'; const status = creditNoteData?.status || 'PENDING';
const dealerName = dealerInfo?.dealerName || 'Jaipur Royal Enfield'; const dealerName = dealerInfo?.dealerName || 'Jaipur Royal Enfield';
const dealerCode = dealerInfo?.dealerCode || 'RE-JP-009'; const dealerCode = dealerInfo?.dealerCode || 'RE-JP-009';
@ -127,6 +128,8 @@ export function CreditNoteSAPModal({
</DialogHeader> </DialogHeader>
<div className="space-y-5 py-4"> <div className="space-y-5 py-4">
{hasCreditNote ? (
<>
{/* Credit Note Document Card */} {/* Credit Note Document Card */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 rounded-lg p-6"> <div className="bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@ -136,7 +139,7 @@ export function CreditNoteSAPModal({
</div> </div>
<Badge className="bg-green-600 text-white px-4 py-2 text-base"> <Badge className="bg-green-600 text-white px-4 py-2 text-base">
<CircleCheckBig className="w-4 h-4 mr-2" /> <CircleCheckBig className="w-4 h-4 mr-2" />
{status === 'APPROVED' ? 'Approved' : status === 'ISSUED' ? 'Issued' : status === 'SENT' ? 'Sent' : 'Pending'} {status === 'APPROVED' || status === 'CONFIRMED' ? 'Approved' : status === 'ISSUED' ? 'Issued' : status === 'SENT' ? 'Sent' : 'Pending'}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 gap-4 mt-4"> <div className="grid grid-cols-2 gap-4 mt-4">
@ -165,6 +168,23 @@ export function CreditNoteSAPModal({
</Label> </Label>
<p className="text-4xl font-bold text-blue-700">{formatCurrency(creditNoteAmount)}</p> <p className="text-4xl font-bold text-blue-700">{formatCurrency(creditNoteAmount)}</p>
</div> </div>
</>
) : (
/* No Credit Note Available */
<div className="bg-gray-50 border-2 border-gray-300 rounded-lg p-8 text-center">
<div className="flex flex-col items-center justify-center space-y-4">
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center">
<Receipt className="w-8 h-8 text-gray-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-700 mb-2">No Credit Note Available</h3>
<p className="text-sm text-gray-500">
Credit note has not been generated yet. Please wait for the credit note to be generated from DMS.
</p>
</div>
</div>
</div>
)}
{/* Dealer Information */} {/* Dealer Information */}
<div className="bg-purple-50 border-2 border-purple-200 rounded-lg p-5"> <div className="bg-purple-50 border-2 border-purple-200 rounded-lg p-5">
@ -244,6 +264,8 @@ export function CreditNoteSAPModal({
Close Close
</Button> </Button>
<div className="flex gap-2"> <div className="flex gap-2">
{hasCreditNote && (
<>
<Button <Button
variant="outline" variant="outline"
onClick={handleDownload} onClick={handleDownload}
@ -261,6 +283,8 @@ export function CreditNoteSAPModal({
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
{sending ? 'Sending...' : 'Send to Dealer'} {sending ? 'Sending...' : 'Send to Dealer'}
</Button> </Button>
</>
)}
</div> </div>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@ -36,6 +36,7 @@ import { useRequestDetails } from '@/hooks/useRequestDetails';
import { useRequestSocket } from '@/hooks/useRequestSocket'; import { useRequestSocket } from '@/hooks/useRequestSocket';
import { useDocumentUpload } from '@/hooks/useDocumentUpload'; import { useDocumentUpload } from '@/hooks/useDocumentUpload';
import { useModalManager } from '@/hooks/useModalManager'; import { useModalManager } from '@/hooks/useModalManager';
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
import { downloadDocument } from '@/services/workflowApi'; import { downloadDocument } from '@/services/workflowApi';
// Dealer Claim Components (import from index to get properly aliased exports) // Dealer Claim Components (import from index to get properly aliased exports)
@ -218,6 +219,37 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
handleAddSpectator, handleAddSpectator,
} = useModalManager(requestIdentifier, currentApprovalLevel, refreshDetails); } = 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 // Auto-switch tab when URL query parameter changes
useEffect(() => { useEffect(() => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@ -500,6 +532,14 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
apiRequest={apiRequest} apiRequest={apiRequest}
currentUserId={currentUserId} currentUserId={currentUserId}
isInitiator={isInitiator} isInitiator={isInitiator}
needsClosure={needsClosure}
conclusionRemark={conclusionRemark}
setConclusionRemark={setConclusionRemark}
conclusionLoading={conclusionLoading}
conclusionSubmitting={conclusionSubmitting}
aiGenerated={aiGenerated}
handleGenerateConclusion={handleGenerateConclusion}
handleFinalizeConclusion={handleFinalizeConclusion}
/> />
</TabsContent> </TabsContent>

View File

@ -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<any> {
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;
}
}