sap implementsion for internal order and budget block
This commit is contained in:
parent
22223fa00c
commit
ea6cd5151b
@ -546,14 +546,12 @@ export function DealerClaimWorkflowTab({
|
||||
const levelId = step3Level.levelId || step3Level.level_id;
|
||||
|
||||
// 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
|
||||
// The backend should handle SAP integration
|
||||
// Only pass ioNumber and ioRemark - don't override existing balance values
|
||||
// Balance values should already be stored when amount was blocked earlier
|
||||
await updateIODetails(requestId, {
|
||||
ioNumber: data.ioNumber,
|
||||
ioRemark: data.ioRemark,
|
||||
ioAvailableBalance: 0, // Should come from SAP integration
|
||||
ioBlockedAmount: 0, // Should come from SAP integration
|
||||
ioRemainingBalance: 0, // Should come from SAP integration
|
||||
// Don't pass balance fields - let backend preserve existing values
|
||||
});
|
||||
|
||||
// Approve Step 3 using real API
|
||||
@ -1002,7 +1000,6 @@ export function DealerClaimWorkflowTab({
|
||||
<Button
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => {
|
||||
console.log('[DealerClaimWorkflowTab] Opening IO approval modal for Step 3');
|
||||
setShowIOApprovalModal(true);
|
||||
}}
|
||||
>
|
||||
@ -1113,6 +1110,7 @@ export function DealerClaimWorkflowTab({
|
||||
requestId={request?.id || request?.requestId}
|
||||
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}
|
||||
preFilledRemainingBalance={request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance || undefined}
|
||||
/>
|
||||
|
||||
{/* Dealer Completion Documents Modal */}
|
||||
|
||||
@ -291,7 +291,7 @@ export function DMSPushModal({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{ioDetails.remainingBalance !== undefined && (
|
||||
{ioDetails.remainingBalance !== undefined && ioDetails.remainingBalance !== null && (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-gray-600">Remaining Balance:</span>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
|
||||
@ -18,7 +18,7 @@ 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 { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert, CheckCircle2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface ExpenseItem {
|
||||
@ -237,7 +237,7 @@ export function DealerCompletionDocumentsModal({
|
||||
|
||||
return (
|
||||
<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>
|
||||
<DialogTitle className="font-semibold flex items-center gap-2 text-2xl">
|
||||
<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">
|
||||
Upload documents proving activity completion (reports, certificates, etc.) - Can upload multiple files or ZIP folder
|
||||
</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
|
||||
ref={completionDocsInputRef}
|
||||
type="file"
|
||||
@ -375,28 +381,51 @@ export function DealerCompletionDocumentsModal({
|
||||
htmlFor="completionDocs"
|
||||
className="cursor-pointer flex flex-col items-center gap-2"
|
||||
>
|
||||
<Upload className="w-8 h-8 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload documents (PDF, DOC, ZIP - multiple files allowed)
|
||||
</span>
|
||||
{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" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload documents (PDF, DOC, ZIP - multiple files allowed)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{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) => (
|
||||
<div
|
||||
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
|
||||
type="button"
|
||||
variant="ghost"
|
||||
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)}
|
||||
title="Remove document"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@ -413,7 +442,13 @@ export function DealerCompletionDocumentsModal({
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Upload photos from the completed activity (event photos, installations, etc.)
|
||||
</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
|
||||
ref={photosInputRef}
|
||||
type="file"
|
||||
@ -427,28 +462,51 @@ export function DealerCompletionDocumentsModal({
|
||||
htmlFor="completionPhotos"
|
||||
className="cursor-pointer flex flex-col items-center gap-2"
|
||||
>
|
||||
<Image className="w-8 h-8 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload photos (JPG, PNG - multiple files allowed)
|
||||
</span>
|
||||
{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" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload photos (JPG, PNG - multiple files allowed)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{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) => (
|
||||
<div
|
||||
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
|
||||
type="button"
|
||||
variant="ghost"
|
||||
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)}
|
||||
title="Remove photo"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@ -473,7 +531,13 @@ export function DealerCompletionDocumentsModal({
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Upload invoices and receipts for expenses incurred
|
||||
</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
|
||||
ref={invoicesInputRef}
|
||||
type="file"
|
||||
@ -487,28 +551,51 @@ export function DealerCompletionDocumentsModal({
|
||||
htmlFor="invoiceReceipts"
|
||||
className="cursor-pointer flex flex-col items-center gap-2"
|
||||
>
|
||||
<Receipt className="w-8 h-8 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload invoices/receipts (PDF, JPG, PNG)
|
||||
</span>
|
||||
{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" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload invoices/receipts (PDF, JPG, PNG)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{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) => (
|
||||
<div
|
||||
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
|
||||
type="button"
|
||||
variant="ghost"
|
||||
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)}
|
||||
title="Remove document"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@ -524,7 +611,13 @@ export function DealerCompletionDocumentsModal({
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Upload attendance records or participant lists (if applicable)
|
||||
</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
|
||||
ref={attendanceInputRef}
|
||||
type="file"
|
||||
@ -537,27 +630,52 @@ export function DealerCompletionDocumentsModal({
|
||||
htmlFor="attendanceDoc"
|
||||
className="cursor-pointer flex flex-col items-center gap-2"
|
||||
>
|
||||
<Upload className="w-8 h-8 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload attendance sheet (Excel, PDF, CSV)
|
||||
</span>
|
||||
{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" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload attendance sheet (Excel, PDF, CSV)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{attendanceSheet && (
|
||||
<div className="mt-2 flex items-center justify-between bg-gray-50 p-2 rounded text-sm">
|
||||
<span className="truncate flex-1">{attendanceSheet.name}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 hover:bg-red-100 hover:text-red-700"
|
||||
onClick={() => {
|
||||
setAttendanceSheet(null);
|
||||
if (attendanceInputRef.current) attendanceInputRef.current.value = '';
|
||||
}}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
<div className="mt-3">
|
||||
<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
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-700 flex-shrink-0 ml-2"
|
||||
onClick={() => {
|
||||
setAttendanceSheet(null);
|
||||
if (attendanceInputRef.current) attendanceInputRef.current.value = '';
|
||||
}}
|
||||
title="Remove document"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,7 @@ 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 { Upload, Plus, X, Calendar, DollarSign, CircleAlert, CheckCircle2, FileText } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CostItem {
|
||||
@ -187,7 +187,7 @@ export function DealerProposalSubmissionModal({
|
||||
|
||||
return (
|
||||
<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>
|
||||
<DialogTitle className="flex items-center gap-2 text-2xl">
|
||||
<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">
|
||||
Detailed proposal with activity details and requested information
|
||||
</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
|
||||
ref={proposalDocInputRef}
|
||||
type="file"
|
||||
@ -236,12 +242,26 @@ export function DealerProposalSubmissionModal({
|
||||
htmlFor="proposalDoc"
|
||||
className="cursor-pointer flex flex-col items-center gap-2"
|
||||
>
|
||||
<Upload className="w-8 h-8 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">
|
||||
{proposalDocument
|
||||
? proposalDocument.name
|
||||
: 'Click to upload proposal (PDF, DOC, DOCX)'}
|
||||
</span>
|
||||
{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" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload proposal (PDF, DOC, DOCX)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -390,7 +410,13 @@ export function DealerProposalSubmissionModal({
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Any other supporting documents (invoices, receipts, photos, etc.)
|
||||
</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
|
||||
ref={otherDocsInputRef}
|
||||
type="file"
|
||||
@ -403,28 +429,53 @@ export function DealerProposalSubmissionModal({
|
||||
htmlFor="otherDocs"
|
||||
className="cursor-pointer flex flex-col items-center gap-2"
|
||||
>
|
||||
<Upload className="w-8 h-8 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload additional documents (multiple files allowed)
|
||||
</span>
|
||||
{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" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload additional documents (multiple files allowed)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{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) => (
|
||||
<div
|
||||
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
|
||||
type="button"
|
||||
variant="ghost"
|
||||
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)}
|
||||
title="Remove document"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -33,9 +33,10 @@ interface DeptLeadIOApprovalModalProps {
|
||||
onReject: (comments: string) => Promise<void>;
|
||||
requestTitle?: string;
|
||||
requestId?: string;
|
||||
// Pre-filled IO data from IO tab
|
||||
// Pre-filled IO data from IO table
|
||||
preFilledIONumber?: string;
|
||||
preFilledBlockedAmount?: number;
|
||||
preFilledRemainingBalance?: number;
|
||||
}
|
||||
|
||||
export function DeptLeadIOApprovalModal({
|
||||
@ -47,19 +48,25 @@ export function DeptLeadIOApprovalModal({
|
||||
requestId: _requestId,
|
||||
preFilledIONumber,
|
||||
preFilledBlockedAmount,
|
||||
preFilledRemainingBalance,
|
||||
}: DeptLeadIOApprovalModalProps) {
|
||||
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
|
||||
const [ioNumber, setIoNumber] = useState(preFilledIONumber || '');
|
||||
const [ioRemark, setIoRemark] = useState('');
|
||||
const [comments, setComments] = useState('');
|
||||
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(() => {
|
||||
if (isOpen && preFilledIONumber) {
|
||||
setIoNumber(preFilledIONumber);
|
||||
if (isOpen) {
|
||||
// Reset form when modal opens
|
||||
setIoRemark('');
|
||||
setComments('');
|
||||
setActionType('approve');
|
||||
}
|
||||
}, [isOpen, preFilledIONumber]);
|
||||
}, [isOpen]);
|
||||
|
||||
const ioRemarkChars = ioRemark.length;
|
||||
const commentsChars = comments.length;
|
||||
@ -71,9 +78,9 @@ export function DeptLeadIOApprovalModal({
|
||||
if (actionType === 'reject') {
|
||||
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 (
|
||||
ioNumber.trim().length > 0 &&
|
||||
ioNumber.trim().length > 0 && // IO number must exist from IO table
|
||||
ioRemark.trim().length > 0 &&
|
||||
comments.trim().length > 0
|
||||
);
|
||||
@ -83,7 +90,7 @@ export function DeptLeadIOApprovalModal({
|
||||
if (!isFormValid) {
|
||||
if (actionType === 'approve') {
|
||||
if (!ioNumber.trim()) {
|
||||
toast.error('Please enter IO number');
|
||||
toast.error('IO number is required. Please block amount from IO tab first.');
|
||||
return;
|
||||
}
|
||||
if (!ioRemark.trim()) {
|
||||
@ -123,7 +130,6 @@ export function DeptLeadIOApprovalModal({
|
||||
|
||||
const handleReset = () => {
|
||||
setActionType('approve');
|
||||
setIoNumber(preFilledIONumber || '');
|
||||
setIoRemark('');
|
||||
setComments('');
|
||||
};
|
||||
@ -148,7 +154,7 @@ export function DeptLeadIOApprovalModal({
|
||||
Approve and Organise IO
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm mt-1">
|
||||
Enter blocked IO details and provide your approval comments
|
||||
Review IO details and provide your approval comments
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
@ -206,52 +212,71 @@ export function DeptLeadIOApprovalModal({
|
||||
|
||||
{/* IO Organisation Details - Only shown when approving */}
|
||||
{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">
|
||||
<Receipt className="w-4 h-4 text-blue-600" />
|
||||
<h4 className="font-semibold text-blue-900">IO Organisation Details</h4>
|
||||
</div>
|
||||
|
||||
{/* IO Number */}
|
||||
{/* IO Number - Read-only from IO table */}
|
||||
<div className="space-y-1">
|
||||
<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>
|
||||
<Input
|
||||
id="ioNumber"
|
||||
placeholder="Enter IO number from SAP"
|
||||
value={ioNumber}
|
||||
onChange={(e) => setIoNumber(e.target.value)}
|
||||
className="bg-white h-8"
|
||||
value={ioNumber || '—'}
|
||||
disabled
|
||||
readOnly
|
||||
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">
|
||||
Pre-filled from IO tab
|
||||
✓ Loaded from IO table
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Blocked Amount Display (if available) */}
|
||||
{preFilledBlockedAmount !== undefined && preFilledBlockedAmount > 0 && (
|
||||
<div className="p-2 bg-green-50 border border-green-200 rounded">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-700">Blocked Amount:</span>
|
||||
<span className="text-sm font-bold text-green-700">
|
||||
₹{preFilledBlockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
{/* IO Balance Information - Read-only */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Blocked Amount Display */}
|
||||
{preFilledBlockedAmount !== undefined && preFilledBlockedAmount > 0 && (
|
||||
<div className="p-2 bg-green-50 border border-green-200 rounded">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-semibold text-gray-700">Blocked Amount:</span>
|
||||
<span className="text-sm font-bold text-green-700 mt-1">
|
||||
₹{preFilledBlockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Amount already blocked in SAP from IO tab</p>
|
||||
</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">
|
||||
<Label htmlFor="ioRemark" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
||||
IO Remark <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="ioRemark"
|
||||
placeholder="Enter remarks about IO blocking"
|
||||
placeholder="Enter remarks about IO organization"
|
||||
value={ioRemark}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
|
||||
@ -803,35 +803,26 @@ export function useRequestDetails(
|
||||
|
||||
const socket = getSocket();
|
||||
if (!socket) {
|
||||
console.warn('[useRequestDetails] Socket not available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[useRequestDetails] Setting up socket listener for request:', apiRequest.requestId);
|
||||
|
||||
/**
|
||||
* Handler: Request updated by another user
|
||||
* Silently refresh to show latest changes
|
||||
*/
|
||||
const handleRequestUpdated = (data: any) => {
|
||||
console.log('[useRequestDetails] 📡 Received request:updated event:', data);
|
||||
// Verify this update is for the current request
|
||||
if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) {
|
||||
console.log('[useRequestDetails] 🔄 Request updated remotely, refreshing silently...');
|
||||
// Silent refresh - no loading state, no user interruption
|
||||
refreshDetails();
|
||||
} else {
|
||||
console.log('[useRequestDetails] ⚠️ Event for different request, ignoring');
|
||||
}
|
||||
};
|
||||
|
||||
// Register listener
|
||||
socket.on('request:updated', handleRequestUpdated);
|
||||
console.log('[useRequestDetails] ✅ Registered listener for request:updated');
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
console.log('[useRequestDetails] 🧹 Cleaning up socket listener');
|
||||
socket.off('request:updated', handleRequestUpdated);
|
||||
};
|
||||
}, [requestIdentifier, apiRequest, refreshDetails]);
|
||||
|
||||
@ -222,14 +222,24 @@ export async function updateIODetails(
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 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,
|
||||
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);
|
||||
return response.data?.data || response.data;
|
||||
} catch (error) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user