sap implementsion for internal order and budget block

This commit is contained in:
laxmanhalaki 2025-12-16 19:48:54 +05:30
parent 22223fa00c
commit ea6cd5151b
7 changed files with 316 additions and 123 deletions

View File

@ -546,14 +546,12 @@ export function DealerClaimWorkflowTab({
const levelId = step3Level.levelId || step3Level.level_id; const levelId = step3Level.levelId || step3Level.level_id;
// First, update IO details using dealer claim API // First, update IO details using dealer claim API
// Note: We need to get IO balance from SAP integration, but for now we'll use placeholder values // Only pass ioNumber and ioRemark - don't override existing balance values
// The backend should handle SAP integration // Balance values should already be stored when amount was blocked earlier
await updateIODetails(requestId, { await updateIODetails(requestId, {
ioNumber: data.ioNumber, ioNumber: data.ioNumber,
ioRemark: data.ioRemark, ioRemark: data.ioRemark,
ioAvailableBalance: 0, // Should come from SAP integration // Don't pass balance fields - let backend preserve existing values
ioBlockedAmount: 0, // Should come from SAP integration
ioRemainingBalance: 0, // Should come from SAP integration
}); });
// Approve Step 3 using real API // Approve Step 3 using real API
@ -1002,7 +1000,6 @@ export function DealerClaimWorkflowTab({
<Button <Button
className="bg-green-600 hover:bg-green-700" className="bg-green-600 hover:bg-green-700"
onClick={() => { onClick={() => {
console.log('[DealerClaimWorkflowTab] Opening IO approval modal for Step 3');
setShowIOApprovalModal(true); setShowIOApprovalModal(true);
}} }}
> >
@ -1113,6 +1110,7 @@ export function DealerClaimWorkflowTab({
requestId={request?.id || request?.requestId} requestId={request?.id || request?.requestId}
preFilledIONumber={request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number || undefined} preFilledIONumber={request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number || undefined}
preFilledBlockedAmount={request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount || undefined} preFilledBlockedAmount={request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount || undefined}
preFilledRemainingBalance={request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance || undefined}
/> />
{/* Dealer Completion Documents Modal */} {/* Dealer Completion Documents Modal */}

View File

@ -291,7 +291,7 @@ export function DMSPushModal({
</span> </span>
</div> </div>
)} )}
{ioDetails.remainingBalance !== undefined && ( {ioDetails.remainingBalance !== undefined && ioDetails.remainingBalance !== null && (
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600">Remaining Balance:</span> <span className="text-sm text-gray-600">Remaining Balance:</span>
<span className="text-sm font-semibold text-gray-900"> <span className="text-sm font-semibold text-gray-900">

View File

@ -18,7 +18,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert } from 'lucide-react'; import { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert, CheckCircle2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
interface ExpenseItem { interface ExpenseItem {
@ -237,7 +237,7 @@ export function DealerCompletionDocumentsModal({
return ( return (
<Dialog open={isOpen} onOpenChange={handleClose}> <Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-lg max-w-4xl max-h-[90vh] overflow-y-auto"> <DialogContent className="sm:max-w-lg max-w-4xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle className="font-semibold flex items-center gap-2 text-2xl"> <DialogTitle className="font-semibold flex items-center gap-2 text-2xl">
<Upload className="w-6 h-6 text-[--re-green]" /> <Upload className="w-6 h-6 text-[--re-green]" />
@ -361,7 +361,13 @@ export function DealerCompletionDocumentsModal({
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
Upload documents proving activity completion (reports, certificates, etc.) - Can upload multiple files or ZIP folder Upload documents proving activity completion (reports, certificates, etc.) - Can upload multiple files or ZIP folder
</p> </p>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 hover:border-blue-500 transition-colors"> <div
className={`border-2 border-dashed rounded-lg p-4 transition-all duration-200 ${
completionDocuments.length > 0
? 'border-green-500 bg-green-50 hover:border-green-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input <input
ref={completionDocsInputRef} ref={completionDocsInputRef}
type="file" type="file"
@ -375,28 +381,51 @@ export function DealerCompletionDocumentsModal({
htmlFor="completionDocs" htmlFor="completionDocs"
className="cursor-pointer flex flex-col items-center gap-2" className="cursor-pointer flex flex-col items-center gap-2"
> >
{completionDocuments.length > 0 ? (
<>
<CheckCircle2 className="w-8 h-8 text-green-600" />
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-semibold text-green-700">
{completionDocuments.length} document{completionDocuments.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-green-600">
Click to add more documents
</span>
</div>
</>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" /> <Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
Click to upload documents (PDF, DOC, ZIP - multiple files allowed) Click to upload documents (PDF, DOC, ZIP - multiple files allowed)
</span> </span>
</>
)}
</label> </label>
</div> </div>
{completionDocuments.length > 0 && ( {completionDocuments.length > 0 && (
<div className="mt-2 space-y-1"> <div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Documents ({completionDocuments.length}):
</p>
{completionDocuments.map((file, index) => ( {completionDocuments.map((file, index) => (
<div <div
key={index} key={index}
className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm" className="flex items-start justify-between bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 p-3 rounded-lg text-sm shadow-sm hover:shadow-md transition-shadow w-full"
> >
<span className="truncate flex-1">{file.name}</span> <div className="flex items-start gap-2 flex-1 min-w-0 pr-2">
<FileText className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-800 font-medium break-words break-all">{file.name}</span>
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 hover:bg-red-100 hover:text-red-700" className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-700 flex-shrink-0 ml-2"
onClick={() => handleRemoveCompletionDoc(index)} onClick={() => handleRemoveCompletionDoc(index)}
title="Remove document"
> >
<X className="w-3 h-3" /> <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
))} ))}
@ -413,7 +442,13 @@ export function DealerCompletionDocumentsModal({
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
Upload photos from the completed activity (event photos, installations, etc.) Upload photos from the completed activity (event photos, installations, etc.)
</p> </p>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 hover:border-blue-500 transition-colors"> <div
className={`border-2 border-dashed rounded-lg p-4 transition-all duration-200 ${
activityPhotos.length > 0
? 'border-green-500 bg-green-50 hover:border-green-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input <input
ref={photosInputRef} ref={photosInputRef}
type="file" type="file"
@ -427,28 +462,51 @@ export function DealerCompletionDocumentsModal({
htmlFor="completionPhotos" htmlFor="completionPhotos"
className="cursor-pointer flex flex-col items-center gap-2" className="cursor-pointer flex flex-col items-center gap-2"
> >
{activityPhotos.length > 0 ? (
<>
<CheckCircle2 className="w-8 h-8 text-green-600" />
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-semibold text-green-700">
{activityPhotos.length} photo{activityPhotos.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-green-600">
Click to add more photos
</span>
</div>
</>
) : (
<>
<Image className="w-8 h-8 text-gray-400" /> <Image className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
Click to upload photos (JPG, PNG - multiple files allowed) Click to upload photos (JPG, PNG - multiple files allowed)
</span> </span>
</>
)}
</label> </label>
</div> </div>
{activityPhotos.length > 0 && ( {activityPhotos.length > 0 && (
<div className="mt-2 space-y-1"> <div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Photos ({activityPhotos.length}):
</p>
{activityPhotos.map((file, index) => ( {activityPhotos.map((file, index) => (
<div <div
key={index} key={index}
className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm" className="flex items-start justify-between bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 p-3 rounded-lg text-sm shadow-sm hover:shadow-md transition-shadow w-full"
> >
<span className="truncate flex-1">{file.name}</span> <div className="flex items-start gap-2 flex-1 min-w-0 pr-2">
<Image className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-800 font-medium break-words break-all">{file.name}</span>
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 hover:bg-red-100 hover:text-red-700" className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-700 flex-shrink-0 ml-2"
onClick={() => handleRemovePhoto(index)} onClick={() => handleRemovePhoto(index)}
title="Remove photo"
> >
<X className="w-3 h-3" /> <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
))} ))}
@ -473,7 +531,13 @@ export function DealerCompletionDocumentsModal({
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
Upload invoices and receipts for expenses incurred Upload invoices and receipts for expenses incurred
</p> </p>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 hover:border-blue-500 transition-colors"> <div
className={`border-2 border-dashed rounded-lg p-4 transition-all duration-200 ${
invoicesReceipts.length > 0
? 'border-blue-500 bg-blue-50 hover:border-blue-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input <input
ref={invoicesInputRef} ref={invoicesInputRef}
type="file" type="file"
@ -487,28 +551,51 @@ export function DealerCompletionDocumentsModal({
htmlFor="invoiceReceipts" htmlFor="invoiceReceipts"
className="cursor-pointer flex flex-col items-center gap-2" className="cursor-pointer flex flex-col items-center gap-2"
> >
{invoicesReceipts.length > 0 ? (
<>
<CheckCircle2 className="w-8 h-8 text-blue-600" />
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-semibold text-blue-700">
{invoicesReceipts.length} document{invoicesReceipts.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-blue-600">
Click to add more documents
</span>
</div>
</>
) : (
<>
<Receipt className="w-8 h-8 text-gray-400" /> <Receipt className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
Click to upload invoices/receipts (PDF, JPG, PNG) Click to upload invoices/receipts (PDF, JPG, PNG)
</span> </span>
</>
)}
</label> </label>
</div> </div>
{invoicesReceipts.length > 0 && ( {invoicesReceipts.length > 0 && (
<div className="mt-2 space-y-1"> <div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Documents ({invoicesReceipts.length}):
</p>
{invoicesReceipts.map((file, index) => ( {invoicesReceipts.map((file, index) => (
<div <div
key={index} key={index}
className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm" className="flex items-start justify-between bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 p-3 rounded-lg text-sm shadow-sm hover:shadow-md transition-shadow w-full"
> >
<span className="truncate flex-1">{file.name}</span> <div className="flex items-start gap-2 flex-1 min-w-0 pr-2">
<Receipt className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-800 font-medium break-words break-all">{file.name}</span>
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 hover:bg-red-100 hover:text-red-700" className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-700 flex-shrink-0 ml-2"
onClick={() => handleRemoveInvoice(index)} onClick={() => handleRemoveInvoice(index)}
title="Remove document"
> >
<X className="w-3 h-3" /> <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
))} ))}
@ -524,7 +611,13 @@ export function DealerCompletionDocumentsModal({
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
Upload attendance records or participant lists (if applicable) Upload attendance records or participant lists (if applicable)
</p> </p>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 hover:border-blue-500 transition-colors"> <div
className={`border-2 border-dashed rounded-lg p-4 transition-all duration-200 ${
attendanceSheet
? 'border-blue-500 bg-blue-50 hover:border-blue-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input <input
ref={attendanceInputRef} ref={attendanceInputRef}
type="file" type="file"
@ -537,28 +630,53 @@ export function DealerCompletionDocumentsModal({
htmlFor="attendanceDoc" htmlFor="attendanceDoc"
className="cursor-pointer flex flex-col items-center gap-2" className="cursor-pointer flex flex-col items-center gap-2"
> >
{attendanceSheet ? (
<>
<CheckCircle2 className="w-8 h-8 text-blue-600" />
<div className="flex flex-col items-center gap-1 w-full max-w-full px-2">
<span className="text-sm font-semibold text-blue-700 break-words text-center w-full max-w-full">
{attendanceSheet.name}
</span>
<span className="text-xs text-blue-600">
Document selected
</span>
</div>
</>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" /> <Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
Click to upload attendance sheet (Excel, PDF, CSV) Click to upload attendance sheet (Excel, PDF, CSV)
</span> </span>
</>
)}
</label> </label>
</div> </div>
{attendanceSheet && ( {attendanceSheet && (
<div className="mt-2 flex items-center justify-between bg-gray-50 p-2 rounded text-sm"> <div className="mt-3">
<span className="truncate flex-1">{attendanceSheet.name}</span> <p className="text-xs font-medium text-gray-600 mb-2">
Selected Document:
</p>
<div className="flex items-start justify-between bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 p-3 rounded-lg text-sm shadow-sm hover:shadow-md transition-shadow w-full">
<div className="flex items-start gap-2 flex-1 min-w-0 pr-2">
<FileText className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-800 font-medium break-words break-all">{attendanceSheet.name}</span>
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 hover:bg-red-100 hover:text-red-700" className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-700 flex-shrink-0 ml-2"
onClick={() => { onClick={() => {
setAttendanceSheet(null); setAttendanceSheet(null);
if (attendanceInputRef.current) attendanceInputRef.current.value = ''; if (attendanceInputRef.current) attendanceInputRef.current.value = '';
}} }}
title="Remove document"
> >
<X className="w-3 h-3" /> <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -18,7 +18,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Upload, Plus, X, Calendar, DollarSign, CircleAlert } from 'lucide-react'; import { Upload, Plus, X, Calendar, DollarSign, CircleAlert, CheckCircle2, FileText } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
interface CostItem { interface CostItem {
@ -187,7 +187,7 @@ export function DealerProposalSubmissionModal({
return ( return (
<Dialog open={isOpen} onOpenChange={handleClose}> <Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-2xl"> <DialogTitle className="flex items-center gap-2 text-2xl">
<Upload className="w-6 h-6 text-[--re-green]" /> <Upload className="w-6 h-6 text-[--re-green]" />
@ -223,7 +223,13 @@ export function DealerProposalSubmissionModal({
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
Detailed proposal with activity details and requested information Detailed proposal with activity details and requested information
</p> </p>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 hover:border-blue-500 transition-colors"> <div
className={`border-2 border-dashed rounded-lg p-4 transition-all duration-200 ${
proposalDocument
? 'border-green-500 bg-green-50 hover:border-green-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input <input
ref={proposalDocInputRef} ref={proposalDocInputRef}
type="file" type="file"
@ -236,12 +242,26 @@ export function DealerProposalSubmissionModal({
htmlFor="proposalDoc" htmlFor="proposalDoc"
className="cursor-pointer flex flex-col items-center gap-2" className="cursor-pointer flex flex-col items-center gap-2"
> >
{proposalDocument ? (
<>
<CheckCircle2 className="w-8 h-8 text-green-600" />
<div className="flex flex-col items-center gap-1 w-full max-w-full px-2">
<span className="text-sm font-semibold text-green-700 break-words text-center w-full max-w-full">
{proposalDocument.name}
</span>
<span className="text-xs text-green-600">
Document selected
</span>
</div>
</>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" /> <Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
{proposalDocument Click to upload proposal (PDF, DOC, DOCX)
? proposalDocument.name
: 'Click to upload proposal (PDF, DOC, DOCX)'}
</span> </span>
</>
)}
</label> </label>
</div> </div>
</div> </div>
@ -390,7 +410,13 @@ export function DealerProposalSubmissionModal({
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
Any other supporting documents (invoices, receipts, photos, etc.) Any other supporting documents (invoices, receipts, photos, etc.)
</p> </p>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 hover:border-blue-500 transition-colors"> <div
className={`border-2 border-dashed rounded-lg p-4 transition-all duration-200 ${
otherDocuments.length > 0
? 'border-blue-500 bg-blue-50 hover:border-blue-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input <input
ref={otherDocsInputRef} ref={otherDocsInputRef}
type="file" type="file"
@ -403,28 +429,53 @@ export function DealerProposalSubmissionModal({
htmlFor="otherDocs" htmlFor="otherDocs"
className="cursor-pointer flex flex-col items-center gap-2" className="cursor-pointer flex flex-col items-center gap-2"
> >
{otherDocuments.length > 0 ? (
<>
<CheckCircle2 className="w-8 h-8 text-blue-600" />
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-semibold text-blue-700">
{otherDocuments.length} document{otherDocuments.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-blue-600">
Click to add more documents
</span>
</div>
</>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" /> <Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
Click to upload additional documents (multiple files allowed) Click to upload additional documents (multiple files allowed)
</span> </span>
</>
)}
</label> </label>
</div> </div>
{otherDocuments.length > 0 && ( {otherDocuments.length > 0 && (
<div className="mt-2 space-y-1"> <div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Documents ({otherDocuments.length}):
</p>
{otherDocuments.map((file, index) => ( {otherDocuments.map((file, index) => (
<div <div
key={index} key={index}
className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm" className="flex items-start justify-between bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 p-3 rounded-lg text-sm shadow-sm hover:shadow-md transition-shadow w-full"
> >
<span className="text-gray-700">{file.name}</span> <div className="flex items-start gap-2 flex-1 min-w-0 pr-2">
<FileText className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-800 font-medium break-words break-all">
{file.name}
</span>
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 hover:bg-red-100" className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-700 flex-shrink-0 ml-2"
onClick={() => handleRemoveOtherDoc(index)} onClick={() => handleRemoveOtherDoc(index)}
title="Remove document"
> >
<X className="w-3 h-3" /> <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
))} ))}

View File

@ -33,9 +33,10 @@ interface DeptLeadIOApprovalModalProps {
onReject: (comments: string) => Promise<void>; onReject: (comments: string) => Promise<void>;
requestTitle?: string; requestTitle?: string;
requestId?: string; requestId?: string;
// Pre-filled IO data from IO tab // Pre-filled IO data from IO table
preFilledIONumber?: string; preFilledIONumber?: string;
preFilledBlockedAmount?: number; preFilledBlockedAmount?: number;
preFilledRemainingBalance?: number;
} }
export function DeptLeadIOApprovalModal({ export function DeptLeadIOApprovalModal({
@ -47,19 +48,25 @@ export function DeptLeadIOApprovalModal({
requestId: _requestId, requestId: _requestId,
preFilledIONumber, preFilledIONumber,
preFilledBlockedAmount, preFilledBlockedAmount,
preFilledRemainingBalance,
}: DeptLeadIOApprovalModalProps) { }: DeptLeadIOApprovalModalProps) {
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve'); const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
const [ioNumber, setIoNumber] = useState(preFilledIONumber || '');
const [ioRemark, setIoRemark] = useState(''); const [ioRemark, setIoRemark] = useState('');
const [comments, setComments] = useState(''); const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
// Update ioNumber when preFilledIONumber changes (when modal opens with new data) // Get IO number from props (read-only, from IO table)
const ioNumber = preFilledIONumber || '';
// Reset form when modal opens/closes
React.useEffect(() => { React.useEffect(() => {
if (isOpen && preFilledIONumber) { if (isOpen) {
setIoNumber(preFilledIONumber); // Reset form when modal opens
setIoRemark('');
setComments('');
setActionType('approve');
} }
}, [isOpen, preFilledIONumber]); }, [isOpen]);
const ioRemarkChars = ioRemark.length; const ioRemarkChars = ioRemark.length;
const commentsChars = comments.length; const commentsChars = comments.length;
@ -71,9 +78,9 @@ export function DeptLeadIOApprovalModal({
if (actionType === 'reject') { if (actionType === 'reject') {
return comments.trim().length > 0; return comments.trim().length > 0;
} }
// For approve, need IO number, IO remark, and comments // For approve, need IO number (from table), IO remark, and comments
return ( return (
ioNumber.trim().length > 0 && ioNumber.trim().length > 0 && // IO number must exist from IO table
ioRemark.trim().length > 0 && ioRemark.trim().length > 0 &&
comments.trim().length > 0 comments.trim().length > 0
); );
@ -83,7 +90,7 @@ export function DeptLeadIOApprovalModal({
if (!isFormValid) { if (!isFormValid) {
if (actionType === 'approve') { if (actionType === 'approve') {
if (!ioNumber.trim()) { if (!ioNumber.trim()) {
toast.error('Please enter IO number'); toast.error('IO number is required. Please block amount from IO tab first.');
return; return;
} }
if (!ioRemark.trim()) { if (!ioRemark.trim()) {
@ -123,7 +130,6 @@ export function DeptLeadIOApprovalModal({
const handleReset = () => { const handleReset = () => {
setActionType('approve'); setActionType('approve');
setIoNumber(preFilledIONumber || '');
setIoRemark(''); setIoRemark('');
setComments(''); setComments('');
}; };
@ -148,7 +154,7 @@ export function DeptLeadIOApprovalModal({
Approve and Organise IO Approve and Organise IO
</DialogTitle> </DialogTitle>
<DialogDescription className="text-sm mt-1"> <DialogDescription className="text-sm mt-1">
Enter blocked IO details and provide your approval comments Review IO details and provide your approval comments
</DialogDescription> </DialogDescription>
</div> </div>
</div> </div>
@ -206,52 +212,71 @@ export function DeptLeadIOApprovalModal({
{/* IO Organisation Details - Only shown when approving */} {/* IO Organisation Details - Only shown when approving */}
{actionType === 'approve' && ( {actionType === 'approve' && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg space-y-2"> <div className="p-3 bg-blue-50 border border-blue-200 rounded-lg space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Receipt className="w-4 h-4 text-blue-600" /> <Receipt className="w-4 h-4 text-blue-600" />
<h4 className="font-semibold text-blue-900">IO Organisation Details</h4> <h4 className="font-semibold text-blue-900">IO Organisation Details</h4>
</div> </div>
{/* IO Number */} {/* IO Number - Read-only from IO table */}
<div className="space-y-1"> <div className="space-y-1">
<Label htmlFor="ioNumber" className="text-sm font-semibold text-gray-900 flex items-center gap-2"> <Label htmlFor="ioNumber" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
Blocked IO Number <span className="text-red-500">*</span> IO Number <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="ioNumber" id="ioNumber"
placeholder="Enter IO number from SAP" value={ioNumber || '—'}
value={ioNumber} disabled
onChange={(e) => setIoNumber(e.target.value)} readOnly
className="bg-white h-8" className="bg-gray-100 h-8 cursor-not-allowed"
/> />
{preFilledIONumber && ( {!ioNumber && (
<p className="text-xs text-red-600 mt-1">
IO number not found. Please block amount from IO tab first.
</p>
)}
{ioNumber && (
<p className="text-xs text-blue-600 mt-1"> <p className="text-xs text-blue-600 mt-1">
Pre-filled from IO tab Loaded from IO table
</p> </p>
)} )}
</div> </div>
{/* Blocked Amount Display (if available) */} {/* IO Balance Information - Read-only */}
<div className="grid grid-cols-2 gap-2">
{/* Blocked Amount Display */}
{preFilledBlockedAmount !== undefined && preFilledBlockedAmount > 0 && ( {preFilledBlockedAmount !== undefined && preFilledBlockedAmount > 0 && (
<div className="p-2 bg-green-50 border border-green-200 rounded"> <div className="p-2 bg-green-50 border border-green-200 rounded">
<div className="flex items-center justify-between"> <div className="flex flex-col">
<span className="text-xs font-semibold text-gray-700">Blocked Amount:</span> <span className="text-xs font-semibold text-gray-700">Blocked Amount:</span>
<span className="text-sm font-bold text-green-700"> <span className="text-sm font-bold text-green-700 mt-1">
{preFilledBlockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {preFilledBlockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span> </span>
</div> </div>
<p className="text-xs text-gray-600 mt-1">Amount already blocked in SAP from IO tab</p>
</div> </div>
)} )}
{/* IO Remark */} {/* Remaining Balance Display */}
{preFilledRemainingBalance !== undefined && preFilledRemainingBalance !== null && (
<div className="p-2 bg-blue-50 border border-blue-200 rounded">
<div className="flex flex-col">
<span className="text-xs font-semibold text-gray-700">Remaining Balance:</span>
<span className="text-sm font-bold text-blue-700 mt-1">
{preFilledRemainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
</div>
)}
</div>
{/* IO Remark - Only editable field */}
<div className="space-y-1"> <div className="space-y-1">
<Label htmlFor="ioRemark" className="text-sm font-semibold text-gray-900 flex items-center gap-2"> <Label htmlFor="ioRemark" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
IO Remark <span className="text-red-500">*</span> IO Remark <span className="text-red-500">*</span>
</Label> </Label>
<Textarea <Textarea
id="ioRemark" id="ioRemark"
placeholder="Enter remarks about IO blocking" placeholder="Enter remarks about IO organization"
value={ioRemark} value={ioRemark}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;

View File

@ -803,35 +803,26 @@ export function useRequestDetails(
const socket = getSocket(); const socket = getSocket();
if (!socket) { if (!socket) {
console.warn('[useRequestDetails] Socket not available');
return; return;
} }
console.log('[useRequestDetails] Setting up socket listener for request:', apiRequest.requestId);
/** /**
* Handler: Request updated by another user * Handler: Request updated by another user
* Silently refresh to show latest changes * Silently refresh to show latest changes
*/ */
const handleRequestUpdated = (data: any) => { const handleRequestUpdated = (data: any) => {
console.log('[useRequestDetails] 📡 Received request:updated event:', data);
// Verify this update is for the current request // Verify this update is for the current request
if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) { if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) {
console.log('[useRequestDetails] 🔄 Request updated remotely, refreshing silently...');
// Silent refresh - no loading state, no user interruption // Silent refresh - no loading state, no user interruption
refreshDetails(); refreshDetails();
} else {
console.log('[useRequestDetails] ⚠️ Event for different request, ignoring');
} }
}; };
// Register listener // Register listener
socket.on('request:updated', handleRequestUpdated); socket.on('request:updated', handleRequestUpdated);
console.log('[useRequestDetails] ✅ Registered listener for request:updated');
// Cleanup on unmount // Cleanup on unmount
return () => { return () => {
console.log('[useRequestDetails] 🧹 Cleaning up socket listener');
socket.off('request:updated', handleRequestUpdated); socket.off('request:updated', handleRequestUpdated);
}; };
}, [requestIdentifier, apiRequest, refreshDetails]); }, [requestIdentifier, apiRequest, refreshDetails]);

View File

@ -222,14 +222,24 @@ export async function updateIODetails(
): Promise<any> { ): Promise<any> {
try { try {
// Map frontend field names to backend expected field names // Map frontend field names to backend expected field names
const payload = { // Only include balance fields if they are explicitly provided (not undefined)
// This prevents overwriting existing balance values when only updating ioNumber/ioRemark
const payload: any = {
ioNumber: ioData.ioNumber, ioNumber: ioData.ioNumber,
ioRemark: ioData.ioRemark || '', ioRemark: ioData.ioRemark || '',
availableBalance: ioData.ioAvailableBalance ?? 0,
blockedAmount: ioData.ioBlockedAmount ?? 0,
remainingBalance: ioData.ioRemainingBalance ?? 0,
}; };
// Only include balance fields if explicitly provided
if (ioData.ioAvailableBalance !== undefined) {
payload.availableBalance = ioData.ioAvailableBalance;
}
if (ioData.ioBlockedAmount !== undefined) {
payload.blockedAmount = ioData.ioBlockedAmount;
}
if (ioData.ioRemainingBalance !== undefined) {
payload.remainingBalance = ioData.ioRemainingBalance;
}
const response = await apiClient.put(`/dealer-claims/${requestId}/io`, payload); const response = await apiClient.put(`/dealer-claims/${requestId}/io`, payload);
return response.data?.data || response.data; return response.data?.data || response.data;
} catch (error) { } catch (error) {