all modal ui added for dealerclaim wokflow step checked all 8 steps
This commit is contained in:
parent
0e9f8adbf6
commit
69c7e99d18
@ -279,7 +279,9 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
{dealers.length === 0 && !loadingDealers ? (
|
{dealers.length === 0 && !loadingDealers ? (
|
||||||
<div className="p-2 text-sm text-gray-500">No dealers available</div>
|
<div className="p-2 text-sm text-gray-500">No dealers available</div>
|
||||||
) : (
|
) : (
|
||||||
dealers.map((dealer) => (
|
dealers
|
||||||
|
.filter((dealer) => dealer.dealerCode && dealer.dealerCode.trim() !== '')
|
||||||
|
.map((dealer) => (
|
||||||
<SelectItem key={dealer.dealerCode} value={dealer.dealerCode}>
|
<SelectItem key={dealer.dealerCode} value={dealer.dealerCode}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
|
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
|
||||||
|
|||||||
@ -236,6 +236,7 @@ export function useRequestDetails(
|
|||||||
let claimDetails = null;
|
let claimDetails = null;
|
||||||
let proposalDetails = null;
|
let proposalDetails = null;
|
||||||
let completionDetails = null;
|
let completionDetails = null;
|
||||||
|
let internalOrder = null;
|
||||||
|
|
||||||
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
||||||
try {
|
try {
|
||||||
@ -255,12 +256,14 @@ export function useRequestDetails(
|
|||||||
hasClaimDetails: !!(claimData?.claimDetails || claimData?.claim_details),
|
hasClaimDetails: !!(claimData?.claimDetails || claimData?.claim_details),
|
||||||
hasProposalDetails: !!(claimData?.proposalDetails || claimData?.proposal_details),
|
hasProposalDetails: !!(claimData?.proposalDetails || claimData?.proposal_details),
|
||||||
hasCompletionDetails: !!(claimData?.completionDetails || claimData?.completion_details),
|
hasCompletionDetails: !!(claimData?.completionDetails || claimData?.completion_details),
|
||||||
|
hasInternalOrder: !!(claimData?.internalOrder || claimData?.internal_order),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (claimData) {
|
if (claimData) {
|
||||||
claimDetails = claimData.claimDetails || claimData.claim_details;
|
claimDetails = claimData.claimDetails || claimData.claim_details;
|
||||||
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
||||||
completionDetails = claimData.completionDetails || claimData.completion_details;
|
completionDetails = claimData.completionDetails || claimData.completion_details;
|
||||||
|
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
||||||
|
|
||||||
console.debug('[useRequestDetails] Extracted details:', {
|
console.debug('[useRequestDetails] Extracted details:', {
|
||||||
claimDetails: claimDetails ? {
|
claimDetails: claimDetails ? {
|
||||||
@ -274,6 +277,7 @@ export function useRequestDetails(
|
|||||||
} : null,
|
} : null,
|
||||||
hasProposalDetails: !!proposalDetails,
|
hasProposalDetails: !!proposalDetails,
|
||||||
hasCompletionDetails: !!completionDetails,
|
hasCompletionDetails: !!completionDetails,
|
||||||
|
hasInternalOrder: !!internalOrder,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('[useRequestDetails] No claimData found in response');
|
console.warn('[useRequestDetails] No claimData found in response');
|
||||||
@ -332,6 +336,7 @@ export function useRequestDetails(
|
|||||||
claimDetails: claimDetails || null,
|
claimDetails: claimDetails || null,
|
||||||
proposalDetails: proposalDetails || null,
|
proposalDetails: proposalDetails || null,
|
||||||
completionDetails: completionDetails || null,
|
completionDetails: completionDetails || null,
|
||||||
|
internalOrder: internalOrder || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setApiRequest(updatedRequest);
|
setApiRequest(updatedRequest);
|
||||||
@ -513,6 +518,7 @@ export function useRequestDetails(
|
|||||||
let claimDetails = null;
|
let claimDetails = null;
|
||||||
let proposalDetails = null;
|
let proposalDetails = null;
|
||||||
let completionDetails = null;
|
let completionDetails = null;
|
||||||
|
let internalOrder = null;
|
||||||
|
|
||||||
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
||||||
try {
|
try {
|
||||||
@ -529,12 +535,14 @@ export function useRequestDetails(
|
|||||||
claimDetails = claimData.claimDetails || claimData.claim_details;
|
claimDetails = claimData.claimDetails || claimData.claim_details;
|
||||||
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
||||||
completionDetails = claimData.completionDetails || claimData.completion_details;
|
completionDetails = claimData.completionDetails || claimData.completion_details;
|
||||||
|
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
||||||
|
|
||||||
console.debug('[useRequestDetails] Initial load - Extracted details:', {
|
console.debug('[useRequestDetails] Initial load - Extracted details:', {
|
||||||
hasClaimDetails: !!claimDetails,
|
hasClaimDetails: !!claimDetails,
|
||||||
claimDetailsKeys: claimDetails ? Object.keys(claimDetails) : [],
|
claimDetailsKeys: claimDetails ? Object.keys(claimDetails) : [],
|
||||||
hasProposalDetails: !!proposalDetails,
|
hasProposalDetails: !!proposalDetails,
|
||||||
hasCompletionDetails: !!completionDetails,
|
hasCompletionDetails: !!completionDetails,
|
||||||
|
hasInternalOrder: !!internalOrder,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -583,6 +591,7 @@ export function useRequestDetails(
|
|||||||
claimDetails: claimDetails || null,
|
claimDetails: claimDetails || null,
|
||||||
proposalDetails: proposalDetails || null,
|
proposalDetails: proposalDetails || null,
|
||||||
completionDetails: completionDetails || null,
|
completionDetails: completionDetails || null,
|
||||||
|
internalOrder: internalOrder || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setApiRequest(mapped);
|
setApiRequest(mapped);
|
||||||
|
|||||||
270
src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx
Normal file
270
src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* CreditNoteSAPModal Component
|
||||||
|
* Modal for Step 8: Credit Note from SAP
|
||||||
|
* Allows Finance team to review credit note details and send to dealer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Receipt, CircleCheckBig, Hash, Calendar, DollarSign, Building, FileText, Download, Send } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { formatDateTime } from '@/utils/dateFormatter';
|
||||||
|
|
||||||
|
interface CreditNoteSAPModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onDownload?: () => Promise<void>;
|
||||||
|
onSendToDealer?: () => Promise<void>;
|
||||||
|
creditNoteData?: {
|
||||||
|
creditNoteNumber?: string;
|
||||||
|
creditNoteDate?: string;
|
||||||
|
creditNoteAmount?: number;
|
||||||
|
status?: 'PENDING' | 'APPROVED' | 'ISSUED' | 'SENT';
|
||||||
|
};
|
||||||
|
dealerInfo?: {
|
||||||
|
dealerName?: string;
|
||||||
|
dealerCode?: string;
|
||||||
|
dealerEmail?: string;
|
||||||
|
};
|
||||||
|
activityName?: string;
|
||||||
|
requestNumber?: string;
|
||||||
|
requestId?: string;
|
||||||
|
dueDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreditNoteSAPModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onDownload,
|
||||||
|
onSendToDealer,
|
||||||
|
creditNoteData,
|
||||||
|
dealerInfo,
|
||||||
|
activityName,
|
||||||
|
requestNumber,
|
||||||
|
requestId,
|
||||||
|
dueDate,
|
||||||
|
}: CreditNoteSAPModalProps) {
|
||||||
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
|
|
||||||
|
const creditNoteNumber = creditNoteData?.creditNoteNumber || 'CN-RE-REQ-2024-CM-101-312580';
|
||||||
|
const creditNoteDate = creditNoteData?.creditNoteDate
|
||||||
|
? formatDateTime(creditNoteData.creditNoteDate, { includeTime: false, format: 'short' })
|
||||||
|
: 'Dec 5, 2025';
|
||||||
|
const creditNoteAmount = creditNoteData?.creditNoteAmount || 800;
|
||||||
|
const status = creditNoteData?.status || 'APPROVED';
|
||||||
|
|
||||||
|
const dealerName = dealerInfo?.dealerName || 'Jaipur Royal Enfield';
|
||||||
|
const dealerCode = dealerInfo?.dealerCode || 'RE-JP-009';
|
||||||
|
const activity = activityName || 'Activity';
|
||||||
|
const requestIdDisplay = requestNumber || 'RE-REQ-2024-CM-101';
|
||||||
|
const dueDateDisplay = dueDate
|
||||||
|
? formatDateTime(dueDate, { includeTime: false, format: 'short' })
|
||||||
|
: 'Jan 4, 2026';
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (onDownload) {
|
||||||
|
try {
|
||||||
|
setDownloading(true);
|
||||||
|
await onDownload();
|
||||||
|
toast.success('Credit note downloaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download credit note:', error);
|
||||||
|
toast.error('Failed to download credit note. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setDownloading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default behavior: show info message
|
||||||
|
toast.info('Credit note will be automatically saved to Documents tab');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendToDealer = async () => {
|
||||||
|
if (onSendToDealer) {
|
||||||
|
try {
|
||||||
|
setSending(true);
|
||||||
|
await onSendToDealer();
|
||||||
|
toast.success('Credit note sent to dealer successfully');
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send credit note to dealer:', error);
|
||||||
|
toast.error('Failed to send credit note. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default behavior: show info message
|
||||||
|
toast.info('Email notification will be sent to dealer with credit note attachment');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-lg max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="font-semibold flex items-center gap-2 text-2xl">
|
||||||
|
<Receipt className="w-6 h-6 text-[--re-green]" />
|
||||||
|
Credit Note from SAP
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-base">
|
||||||
|
Review and send credit note to dealer
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-5 py-4">
|
||||||
|
{/* 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="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-green-900 text-xl mb-1">Royal Enfield</h3>
|
||||||
|
<p className="text-sm text-green-700">Credit Note Document</p>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-600 text-white px-4 py-2 text-base">
|
||||||
|
<CircleCheckBig className="w-4 h-4 mr-2" />
|
||||||
|
{status === 'APPROVED' ? 'Approved' : status === 'ISSUED' ? 'Issued' : status === 'SENT' ? 'Sent' : 'Pending'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||||
|
<div className="bg-white rounded-lg p-3 border border-green-100">
|
||||||
|
<Label className="font-medium text-xs text-gray-600 uppercase tracking-wider flex items-center gap-1">
|
||||||
|
<Hash className="w-3 h-3" />
|
||||||
|
Credit Note Number
|
||||||
|
</Label>
|
||||||
|
<p className="font-bold text-gray-900 mt-1 text-lg">{creditNoteNumber}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg p-3 border border-green-100">
|
||||||
|
<Label className="font-medium text-xs text-gray-600 uppercase tracking-wider flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3" />
|
||||||
|
Issue Date
|
||||||
|
</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">{creditNoteDate}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Credit Note Amount */}
|
||||||
|
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-5">
|
||||||
|
<Label className="font-medium text-xs text-gray-600 uppercase tracking-wider flex items-center gap-1 mb-3">
|
||||||
|
<DollarSign className="w-4 h-4" />
|
||||||
|
Credit Note Amount
|
||||||
|
</Label>
|
||||||
|
<p className="text-4xl font-bold text-blue-700">{formatCurrency(creditNoteAmount)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dealer Information */}
|
||||||
|
<div className="bg-purple-50 border-2 border-purple-200 rounded-lg p-5">
|
||||||
|
<h3 className="font-semibold text-purple-900 mb-4 flex items-center gap-2">
|
||||||
|
<Building className="w-5 h-5" />
|
||||||
|
Dealer Information
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-white rounded-lg p-3 border border-purple-100">
|
||||||
|
<Label className="flex items-center gap-2 font-medium text-xs text-gray-600 uppercase tracking-wider">
|
||||||
|
Dealer Name
|
||||||
|
</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">{dealerName}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg p-3 border border-purple-100">
|
||||||
|
<Label className="flex items-center gap-2 font-medium text-xs text-gray-600 uppercase tracking-wider">
|
||||||
|
Dealer Code
|
||||||
|
</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">{dealerCode}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg p-3 border border-purple-100">
|
||||||
|
<Label className="flex items-center gap-2 font-medium text-xs text-gray-600 uppercase tracking-wider">
|
||||||
|
Activity
|
||||||
|
</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">{activity}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reference Details */}
|
||||||
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
Reference Details
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<Label className="flex items-center gap-2 font-medium text-xs text-gray-600">
|
||||||
|
Request ID
|
||||||
|
</Label>
|
||||||
|
<p className="font-medium text-gray-900 mt-1">{requestIdDisplay}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="flex items-center gap-2 font-medium text-xs text-gray-600">
|
||||||
|
Due Date
|
||||||
|
</Label>
|
||||||
|
<p className="font-medium text-gray-900 mt-1">{dueDateDisplay}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Available Actions Info */}
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-start gap-3">
|
||||||
|
<FileText className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800">
|
||||||
|
<p className="font-semibold mb-2">Available Actions</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-xs">
|
||||||
|
<li>
|
||||||
|
<strong>Download:</strong> Credit note will be automatically saved to Documents tab
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Send to Dealer:</strong> Email notification will be sent to dealer with credit note attachment
|
||||||
|
</li>
|
||||||
|
<li>All actions will be recorded in activity trail for audit purposes</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex-col-reverse gap-2 sm:flex-row flex items-center justify-between sm:justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={downloading || sending}
|
||||||
|
className="border-2"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={downloading || sending}
|
||||||
|
className="border-blue-600 text-blue-600 hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
{downloading ? 'Downloading...' : 'Download'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSendToDealer}
|
||||||
|
disabled={downloading || sending}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white shadow-md"
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
{sending ? 'Sending...' : 'Send to Dealer'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,616 @@
|
|||||||
|
/**
|
||||||
|
* DealerCompletionDocumentsModal Component
|
||||||
|
* Modal for Step 5: Activity Completion Documents
|
||||||
|
* Allows dealers to upload completion documents, photos, expenses, and provide completion details
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useRef, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface ExpenseItem {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DealerCompletionDocumentsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: {
|
||||||
|
activityCompletionDate: string;
|
||||||
|
numberOfParticipants?: number;
|
||||||
|
closedExpenses: ExpenseItem[];
|
||||||
|
totalClosedExpenses: number;
|
||||||
|
completionDocuments: File[];
|
||||||
|
activityPhotos: File[];
|
||||||
|
invoicesReceipts?: File[];
|
||||||
|
attendanceSheet?: File;
|
||||||
|
completionDescription: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
dealerName?: string;
|
||||||
|
activityName?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DealerCompletionDocumentsModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
dealerName = 'Jaipur Royal Enfield',
|
||||||
|
activityName = 'Activity',
|
||||||
|
requestId,
|
||||||
|
}: DealerCompletionDocumentsModalProps) {
|
||||||
|
const [activityCompletionDate, setActivityCompletionDate] = useState('');
|
||||||
|
const [numberOfParticipants, setNumberOfParticipants] = useState('');
|
||||||
|
const [expenseItems, setExpenseItems] = useState<ExpenseItem[]>([]);
|
||||||
|
const [completionDocuments, setCompletionDocuments] = useState<File[]>([]);
|
||||||
|
const [activityPhotos, setActivityPhotos] = useState<File[]>([]);
|
||||||
|
const [invoicesReceipts, setInvoicesReceipts] = useState<File[]>([]);
|
||||||
|
const [attendanceSheet, setAttendanceSheet] = useState<File | null>(null);
|
||||||
|
const [completionDescription, setCompletionDescription] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const completionDocsInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const photosInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const invoicesInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const attendanceInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Calculate total closed expenses
|
||||||
|
const totalClosedExpenses = useMemo(() => {
|
||||||
|
return expenseItems.reduce((sum, item) => sum + (item.amount || 0), 0);
|
||||||
|
}, [expenseItems]);
|
||||||
|
|
||||||
|
// Check if all required fields are filled
|
||||||
|
const isFormValid = useMemo(() => {
|
||||||
|
const hasCompletionDate = activityCompletionDate !== '';
|
||||||
|
const hasDocuments = completionDocuments.length > 0;
|
||||||
|
const hasPhotos = activityPhotos.length > 0;
|
||||||
|
const hasDescription = completionDescription.trim().length > 0;
|
||||||
|
|
||||||
|
return hasCompletionDate && hasDocuments && hasPhotos && hasDescription;
|
||||||
|
}, [activityCompletionDate, completionDocuments, activityPhotos, completionDescription]);
|
||||||
|
|
||||||
|
// Get today's date in YYYY-MM-DD format for max date
|
||||||
|
const maxDate = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const handleAddExpense = () => {
|
||||||
|
setExpenseItems([
|
||||||
|
...expenseItems,
|
||||||
|
{ id: Date.now().toString(), description: '', amount: 0 },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpenseChange = (id: string, field: 'description' | 'amount', value: string | number) => {
|
||||||
|
setExpenseItems(
|
||||||
|
expenseItems.map((item) =>
|
||||||
|
item.id === id ? { ...item, [field]: value } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveExpense = (id: string) => {
|
||||||
|
setExpenseItems(expenseItems.filter((item) => item.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompletionDocsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = Array.from(e.target.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
// Validate file types
|
||||||
|
const allowedTypes = ['.pdf', '.doc', '.docx', '.zip', '.rar'];
|
||||||
|
const invalidFiles = files.filter(
|
||||||
|
(file) => !allowedTypes.some((ext) => file.name.toLowerCase().endsWith(ext))
|
||||||
|
);
|
||||||
|
if (invalidFiles.length > 0) {
|
||||||
|
toast.error('Please upload PDF, DOC, DOCX, ZIP, or RAR files only');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCompletionDocuments([...completionDocuments, ...files]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveCompletionDoc = (index: number) => {
|
||||||
|
setCompletionDocuments(completionDocuments.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhotosChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = Array.from(e.target.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
// Validate image files
|
||||||
|
const invalidFiles = files.filter(
|
||||||
|
(file) => !file.type.startsWith('image/')
|
||||||
|
);
|
||||||
|
if (invalidFiles.length > 0) {
|
||||||
|
toast.error('Please upload image files only (JPG, PNG, etc.)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActivityPhotos([...activityPhotos, ...files]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemovePhoto = (index: number) => {
|
||||||
|
setActivityPhotos(activityPhotos.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInvoicesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = Array.from(e.target.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
// Validate file types
|
||||||
|
const allowedTypes = ['.pdf', '.jpg', '.jpeg', '.png'];
|
||||||
|
const invalidFiles = files.filter(
|
||||||
|
(file) => !allowedTypes.some((ext) => file.name.toLowerCase().endsWith(ext))
|
||||||
|
);
|
||||||
|
if (invalidFiles.length > 0) {
|
||||||
|
toast.error('Please upload PDF, JPG, or PNG files only');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInvoicesReceipts([...invoicesReceipts, ...files]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveInvoice = (index: number) => {
|
||||||
|
setInvoicesReceipts(invoicesReceipts.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAttendanceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// Validate file types
|
||||||
|
const allowedTypes = ['.pdf', '.xlsx', '.xls', '.csv'];
|
||||||
|
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||||
|
if (!allowedTypes.includes(fileExtension)) {
|
||||||
|
toast.error('Please upload PDF, Excel, or CSV files only');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAttendanceSheet(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!isFormValid) {
|
||||||
|
toast.error('Please fill all required fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter valid expense items
|
||||||
|
const validExpenses = expenseItems.filter(
|
||||||
|
(item) => item.description.trim() !== '' && item.amount > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
await onSubmit({
|
||||||
|
activityCompletionDate,
|
||||||
|
numberOfParticipants: numberOfParticipants ? parseInt(numberOfParticipants) : undefined,
|
||||||
|
closedExpenses: validExpenses,
|
||||||
|
totalClosedExpenses,
|
||||||
|
completionDocuments,
|
||||||
|
activityPhotos,
|
||||||
|
invoicesReceipts: invoicesReceipts.length > 0 ? invoicesReceipts : undefined,
|
||||||
|
attendanceSheet: attendanceSheet || undefined,
|
||||||
|
completionDescription,
|
||||||
|
});
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to submit completion documents:', error);
|
||||||
|
toast.error('Failed to submit completion documents. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setActivityCompletionDate('');
|
||||||
|
setNumberOfParticipants('');
|
||||||
|
setExpenseItems([]);
|
||||||
|
setCompletionDocuments([]);
|
||||||
|
setActivityPhotos([]);
|
||||||
|
setInvoicesReceipts([]);
|
||||||
|
setAttendanceSheet(null);
|
||||||
|
setCompletionDescription('');
|
||||||
|
if (completionDocsInputRef.current) completionDocsInputRef.current.value = '';
|
||||||
|
if (photosInputRef.current) photosInputRef.current.value = '';
|
||||||
|
if (invoicesInputRef.current) invoicesInputRef.current.value = '';
|
||||||
|
if (attendanceInputRef.current) attendanceInputRef.current.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!submitting) {
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="sm:max-w-lg max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="font-semibold flex items-center gap-2 text-2xl">
|
||||||
|
<Upload className="w-6 h-6 text-[--re-green]" />
|
||||||
|
Activity Completion Documents
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-base">
|
||||||
|
Step 5: Upload completion proof and final documents
|
||||||
|
</DialogDescription>
|
||||||
|
<div className="space-y-1 mt-2 text-sm text-gray-600">
|
||||||
|
<div>
|
||||||
|
<strong>Dealer:</strong> {dealerName}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Activity:</strong> {activityName}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
Please upload completion documents, photos, and provide details about the completed activity.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* Activity Completion Date */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-base font-semibold flex items-center gap-2" htmlFor="completionDate">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
Activity Completion Date *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
id="completionDate"
|
||||||
|
max={maxDate}
|
||||||
|
value={activityCompletionDate}
|
||||||
|
onChange={(e) => setActivityCompletionDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Closed Expenses Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-lg">Closed Expenses</h3>
|
||||||
|
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddExpense}
|
||||||
|
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
Add Expense
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{expenseItems.map((item) => (
|
||||||
|
<div key={item.id} className="flex gap-2 items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
placeholder="Item name (e.g., Venue rental, Refreshments, Printing)"
|
||||||
|
value={item.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleExpenseChange(item.id, 'description', e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-40">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Amount"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={item.amount || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleExpenseChange(item.id, 'amount', parseFloat(e.target.value) || 0)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="mt-0.5 hover:bg-red-100 hover:text-red-700"
|
||||||
|
onClick={() => handleRemoveExpense(item.id)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{expenseItems.length === 0 && (
|
||||||
|
<p className="text-sm text-gray-500 italic">
|
||||||
|
No expenses added. Click "Add Expense" to add expense items.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{expenseItems.length > 0 && totalClosedExpenses > 0 && (
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-semibold">Total Closed Expenses:</span>
|
||||||
|
<span className="font-semibold text-lg">
|
||||||
|
₹{totalClosedExpenses.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Completion Evidence Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-lg">Completion Evidence</h3>
|
||||||
|
<Badge className="bg-destructive text-white text-xs">Required</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Completion Documents */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
Completion Documents *
|
||||||
|
</Label>
|
||||||
|
<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">
|
||||||
|
<input
|
||||||
|
ref={completionDocsInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept=".pdf,.doc,.docx,.zip,.rar"
|
||||||
|
className="hidden"
|
||||||
|
id="completionDocs"
|
||||||
|
onChange={handleCompletionDocsChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{completionDocuments.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{completionDocuments.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm"
|
||||||
|
>
|
||||||
|
<span className="truncate flex-1">{file.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={() => handleRemoveCompletionDoc(index)}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity Photos */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold flex items-center gap-2">
|
||||||
|
<Image className="w-4 h-4" />
|
||||||
|
Activity Photos *
|
||||||
|
</Label>
|
||||||
|
<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">
|
||||||
|
<input
|
||||||
|
ref={photosInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
id="completionPhotos"
|
||||||
|
onChange={handlePhotosChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{activityPhotos.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{activityPhotos.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm"
|
||||||
|
>
|
||||||
|
<span className="truncate flex-1">{file.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={() => handleRemovePhoto(index)}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Supporting Documents Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-lg">Supporting Documents</h3>
|
||||||
|
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Invoices/Receipts */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold flex items-center gap-2">
|
||||||
|
<Receipt className="w-4 h-4" />
|
||||||
|
Invoices / Receipts
|
||||||
|
</Label>
|
||||||
|
<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">
|
||||||
|
<input
|
||||||
|
ref={invoicesInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
className="hidden"
|
||||||
|
id="invoiceReceipts"
|
||||||
|
onChange={handleInvoicesChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{invoicesReceipts.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{invoicesReceipts.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm"
|
||||||
|
>
|
||||||
|
<span className="truncate flex-1">{file.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={() => handleRemoveInvoice(index)}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attendance Sheet */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold">
|
||||||
|
Attendance Sheet / Participant List
|
||||||
|
</Label>
|
||||||
|
<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">
|
||||||
|
<input
|
||||||
|
ref={attendanceInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.xlsx,.xls,.csv"
|
||||||
|
className="hidden"
|
||||||
|
id="attendanceDoc"
|
||||||
|
onChange={handleAttendanceChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Completion Description */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-base font-semibold flex items-center gap-2" htmlFor="completionDescription">
|
||||||
|
Brief Description of Completion *
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="completionDescription"
|
||||||
|
placeholder="Provide a brief description of the completed activity, including key highlights, outcomes, challenges faced, and any relevant observations..."
|
||||||
|
value={completionDescription}
|
||||||
|
onChange={(e) => setCompletionDescription(e.target.value)}
|
||||||
|
className="min-h-[120px]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">{completionDescription.length} characters</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning Message */}
|
||||||
|
{!isFormValid && (
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 flex items-start gap-3">
|
||||||
|
<CircleAlert className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-amber-800">
|
||||||
|
<p className="font-semibold mb-1">Missing Required Information</p>
|
||||||
|
<p>
|
||||||
|
Please ensure completion date, at least one document/photo, and description are provided before submitting.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={submitting}
|
||||||
|
className="border-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={submitting || !isFormValid}
|
||||||
|
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white disabled:bg-gray-300 disabled:text-gray-500"
|
||||||
|
>
|
||||||
|
{submitting ? 'Submitting...' : 'Submit Documents'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -287,7 +287,7 @@ export function InitiatorProposalApprovalModal({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[--re-green] bg-opacity-10 border-2 border-[--re-green] rounded-lg p-4">
|
<div className="border-2 border-[--re-green] rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<DollarSign className="w-5 h-5 text-[--re-green]" />
|
<DollarSign className="w-5 h-5 text-[--re-green]" />
|
||||||
|
|||||||
@ -0,0 +1,219 @@
|
|||||||
|
# Credit Note from SAP Modal - Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `CreditNoteSAPModal` component is ready for implementation in Step 8 of the dealer claim workflow. This modal allows Finance team to review credit note details (generated from SAP) and send it to the dealer.
|
||||||
|
|
||||||
|
## Component Location
|
||||||
|
|
||||||
|
**File:** `Re_Figma_Code/src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx`
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Display Sections:
|
||||||
|
1. **Credit Note Document Card** (Green gradient)
|
||||||
|
- Royal Enfield branding
|
||||||
|
- Status badge (Approved/Issued/Sent/Pending)
|
||||||
|
- Credit Note Number
|
||||||
|
- Issue Date
|
||||||
|
|
||||||
|
2. **Credit Note Amount** (Blue box)
|
||||||
|
- Large display of credit note amount in ₹
|
||||||
|
|
||||||
|
3. **Dealer Information** (Purple box)
|
||||||
|
- Dealer Name
|
||||||
|
- Dealer Code
|
||||||
|
- Activity Name
|
||||||
|
|
||||||
|
4. **Reference Details** (Gray box)
|
||||||
|
- Request ID
|
||||||
|
- Due Date
|
||||||
|
|
||||||
|
5. **Available Actions Info** (Blue info box)
|
||||||
|
- Explains what Download and Send actions do
|
||||||
|
|
||||||
|
### Actions:
|
||||||
|
- **Download**: Downloads/saves credit note to Documents tab
|
||||||
|
- **Send to Dealer**: Sends email notification to dealer with credit note attachment
|
||||||
|
- **Close**: Closes the modal
|
||||||
|
|
||||||
|
## Props Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CreditNoteSAPModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onDownload?: () => Promise<void>; // Optional: Custom download handler
|
||||||
|
onSendToDealer?: () => Promise<void>; // Optional: Custom send handler
|
||||||
|
creditNoteData?: {
|
||||||
|
creditNoteNumber?: string;
|
||||||
|
creditNoteDate?: string;
|
||||||
|
creditNoteAmount?: number;
|
||||||
|
status?: 'PENDING' | 'APPROVED' | 'ISSUED' | 'SENT';
|
||||||
|
};
|
||||||
|
dealerInfo?: {
|
||||||
|
dealerName?: string;
|
||||||
|
dealerCode?: string;
|
||||||
|
dealerEmail?: string;
|
||||||
|
};
|
||||||
|
activityName?: string;
|
||||||
|
requestNumber?: string;
|
||||||
|
requestId?: string;
|
||||||
|
dueDate?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Steps
|
||||||
|
|
||||||
|
### 1. Import the Modal
|
||||||
|
|
||||||
|
In `DealerClaimWorkflowTab.tsx`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CreditNoteSAPModal } from '../modals/CreditNoteSAPModal';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add State
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add Button for Step 8
|
||||||
|
|
||||||
|
In the workflow steps rendering, add button for Step 8 (Finance):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{step.step === 8 && isFinanceUser && (
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={() => setShowCreditNoteModal(true)}
|
||||||
|
>
|
||||||
|
<Receipt className="w-4 h-4 mr-2" />
|
||||||
|
View Credit Note
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Add Modal Component
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<CreditNoteSAPModal
|
||||||
|
isOpen={showCreditNoteModal}
|
||||||
|
onClose={() => setShowCreditNoteModal(false)}
|
||||||
|
onDownload={handleCreditNoteDownload}
|
||||||
|
onSendToDealer={handleSendCreditNoteToDealer}
|
||||||
|
creditNoteData={{
|
||||||
|
creditNoteNumber: request?.claimDetails?.creditNoteNumber,
|
||||||
|
creditNoteDate: request?.claimDetails?.creditNoteDate,
|
||||||
|
creditNoteAmount: request?.claimDetails?.creditNoteAmount,
|
||||||
|
status: 'APPROVED', // or get from request status
|
||||||
|
}}
|
||||||
|
dealerInfo={{
|
||||||
|
dealerName: request?.claimDetails?.dealerName,
|
||||||
|
dealerCode: request?.claimDetails?.dealerCode,
|
||||||
|
dealerEmail: request?.claimDetails?.dealerEmail,
|
||||||
|
}}
|
||||||
|
activityName={request?.claimDetails?.activityName}
|
||||||
|
requestNumber={request?.requestNumber}
|
||||||
|
requestId={request?.requestId}
|
||||||
|
dueDate={/* Calculate due date based on business rules */}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Implement Handlers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleCreditNoteDownload = async () => {
|
||||||
|
try {
|
||||||
|
const requestId = request?.id || request?.requestId;
|
||||||
|
// TODO: Implement download logic
|
||||||
|
// - Generate/download credit note PDF from SAP
|
||||||
|
// - Save to Documents tab
|
||||||
|
// - Create activity log entry
|
||||||
|
toast.success('Credit note downloaded and saved to Documents');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download credit note:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCreditNoteToDealer = async () => {
|
||||||
|
try {
|
||||||
|
const requestId = request?.id || request?.requestId;
|
||||||
|
const dealerEmail = request?.claimDetails?.dealerEmail;
|
||||||
|
|
||||||
|
// TODO: Implement send logic
|
||||||
|
// - Send email to dealer with credit note attachment
|
||||||
|
// - Update credit note status to 'SENT'
|
||||||
|
// - Create activity log entry
|
||||||
|
// - Possibly approve Step 8
|
||||||
|
|
||||||
|
toast.success('Credit note sent to dealer successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send credit note:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Integration Points
|
||||||
|
|
||||||
|
### Backend Endpoints (to be implemented):
|
||||||
|
|
||||||
|
1. **Download Credit Note**
|
||||||
|
- `GET /api/v1/dealer-claims/:requestId/credit-note/download`
|
||||||
|
- Returns credit note PDF/document
|
||||||
|
|
||||||
|
2. **Send Credit Note to Dealer**
|
||||||
|
- `POST /api/v1/dealer-claims/:requestId/credit-note/send`
|
||||||
|
- Sends email notification to dealer
|
||||||
|
- Updates credit note status
|
||||||
|
|
||||||
|
3. **Get Credit Note Details**
|
||||||
|
- Already available via `getClaimDetails()` API
|
||||||
|
- Returns `creditNoteNumber`, `creditNoteDate`, `creditNoteAmount` from `claimDetails`
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. **Credit Note Generation** (Step 7 or Step 8):
|
||||||
|
- Credit note is generated from SAP/DMS
|
||||||
|
- Stored in `dealer_claim_details` table
|
||||||
|
- Fields: `credit_note_number`, `credit_note_date`, `credit_note_amount`
|
||||||
|
|
||||||
|
2. **Display in Modal**:
|
||||||
|
- Modal reads from `request.claimDetails`
|
||||||
|
- Displays credit note information
|
||||||
|
- Shows dealer and request details
|
||||||
|
|
||||||
|
3. **Actions**:
|
||||||
|
- Download: Saves credit note to Documents tab
|
||||||
|
- Send: Emails dealer and updates status
|
||||||
|
|
||||||
|
## UI Styling
|
||||||
|
|
||||||
|
The modal matches the provided HTML structure:
|
||||||
|
- ✅ Green gradient card for credit note document
|
||||||
|
- ✅ Blue box for amount display
|
||||||
|
- ✅ Purple box for dealer information
|
||||||
|
- ✅ Gray box for reference details
|
||||||
|
- ✅ Blue info box for available actions
|
||||||
|
- ✅ Proper icons (Receipt, Hash, Calendar, DollarSign, Building, FileText, Download, Send)
|
||||||
|
- ✅ Status badge with checkmark icon
|
||||||
|
- ✅ Responsive grid layouts
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
✅ **Component Created**: Ready for integration
|
||||||
|
⏳ **Integration**: Pending - needs to be added to `DealerClaimWorkflowTab.tsx`
|
||||||
|
⏳ **API Handlers**: Pending - download and send handlers need implementation
|
||||||
|
⏳ **Backend Endpoints**: Pending - download and send endpoints need to be created
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Add modal to `DealerClaimWorkflowTab.tsx` when Step 8 is ready
|
||||||
|
2. Implement download handler (integrate with SAP/DMS)
|
||||||
|
3. Implement send handler (email notification)
|
||||||
|
4. Add Step 8 button visibility logic (Finance team only)
|
||||||
|
5. Test credit note flow end-to-end
|
||||||
|
|
||||||
@ -13,13 +13,15 @@ import { useState, useMemo, useEffect } from 'react';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TrendingUp, Clock, CheckCircle, Upload, Mail, Download, Receipt, Activity } from 'lucide-react';
|
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity } from 'lucide-react';
|
||||||
import { formatDateTime } from '@/utils/dateFormatter';
|
import { formatDateTime } from '@/utils/dateFormatter';
|
||||||
import { DealerProposalSubmissionModal } from '../modals/DealerProposalSubmissionModal';
|
import { DealerProposalSubmissionModal } from '../modals/DealerProposalSubmissionModal';
|
||||||
import { InitiatorProposalApprovalModal } from '../modals/InitiatorProposalApprovalModal';
|
import { InitiatorProposalApprovalModal } from '../modals/InitiatorProposalApprovalModal';
|
||||||
import { DeptLeadIOApprovalModal } from '../modals/DeptLeadIOApprovalModal';
|
import { DeptLeadIOApprovalModal } from '../modals/DeptLeadIOApprovalModal';
|
||||||
|
import { DealerCompletionDocumentsModal } from '../modals/DealerCompletionDocumentsModal';
|
||||||
|
import { CreditNoteSAPModal } from '../modals/CreditNoteSAPModal';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { submitProposal, updateIODetails } from '@/services/dealerClaimApi';
|
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, updateCreditNote } from '@/services/dealerClaimApi';
|
||||||
import { getWorkflowDetails, approveLevel, rejectLevel, updateWorkflow } from '@/services/workflowApi';
|
import { getWorkflowDetails, approveLevel, rejectLevel, updateWorkflow } from '@/services/workflowApi';
|
||||||
import { uploadDocument } from '@/services/documentApi';
|
import { uploadDocument } from '@/services/documentApi';
|
||||||
import { createWorkNoteMultipart } from '@/services/workflowApi';
|
import { createWorkNoteMultipart } from '@/services/workflowApi';
|
||||||
@ -38,7 +40,7 @@ interface WorkflowStep {
|
|||||||
approver: string;
|
approver: string;
|
||||||
description: string;
|
description: string;
|
||||||
tatHours: number;
|
tatHours: number;
|
||||||
status: 'pending' | 'approved' | 'waiting' | 'rejected';
|
status: 'pending' | 'approved' | 'waiting' | 'rejected' | 'in_progress';
|
||||||
comment?: string;
|
comment?: string;
|
||||||
approvedAt?: string;
|
approvedAt?: string;
|
||||||
elapsedHours?: number;
|
elapsedHours?: number;
|
||||||
@ -89,7 +91,7 @@ const formatDateSafe = (dateString: string | undefined | null): string => {
|
|||||||
const getStepIcon = (status: string) => {
|
const getStepIcon = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'approved':
|
case 'approved':
|
||||||
return <CheckCircle className="w-5 h-5 text-green-600" />;
|
return <CircleCheckBig className="w-5 h-5 text-green-600" />;
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return <Clock className="w-5 h-5 text-blue-600" />;
|
return <Clock className="w-5 h-5 text-blue-600" />;
|
||||||
case 'rejected':
|
case 'rejected':
|
||||||
@ -119,7 +121,7 @@ const getStepBadgeVariant = (status: string) => {
|
|||||||
* Get step card styling
|
* Get step card styling
|
||||||
*/
|
*/
|
||||||
const getStepCardStyle = (status: string, isActive: boolean) => {
|
const getStepCardStyle = (status: string, isActive: boolean) => {
|
||||||
if (isActive && status === 'pending') {
|
if (isActive && (status === 'pending' || status === 'in_progress')) {
|
||||||
return 'border-purple-500 bg-purple-50 shadow-md';
|
return 'border-purple-500 bg-purple-50 shadow-md';
|
||||||
}
|
}
|
||||||
if (status === 'approved') {
|
if (status === 'approved') {
|
||||||
@ -157,6 +159,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
const [showProposalModal, setShowProposalModal] = useState(false);
|
const [showProposalModal, setShowProposalModal] = useState(false);
|
||||||
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
||||||
const [showIOApprovalModal, setShowIOApprovalModal] = useState(false);
|
const [showIOApprovalModal, setShowIOApprovalModal] = useState(false);
|
||||||
|
const [showCompletionModal, setShowCompletionModal] = useState(false);
|
||||||
|
const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
|
||||||
|
|
||||||
// Load approval flows from real API
|
// Load approval flows from real API
|
||||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||||
@ -182,6 +186,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
const flows = approvals.map((level: any) => ({
|
const flows = approvals.map((level: any) => ({
|
||||||
step: level.levelNumber || level.level_number || 0,
|
step: level.levelNumber || level.level_number || 0,
|
||||||
approver: level.approverName || level.approver_name || '',
|
approver: level.approverName || level.approver_name || '',
|
||||||
|
approverEmail: (level.approverEmail || level.approver_email || '').toLowerCase(),
|
||||||
status: level.status?.toLowerCase() || 'waiting',
|
status: level.status?.toLowerCase() || 'waiting',
|
||||||
tatHours: level.tatHours || level.tat_hours || 24,
|
tatHours: level.tatHours || level.tat_hours || 24,
|
||||||
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
||||||
@ -212,6 +217,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
const flows = approvals.map((level: any) => ({
|
const flows = approvals.map((level: any) => ({
|
||||||
step: level.levelNumber || level.level_number || 0,
|
step: level.levelNumber || level.level_number || 0,
|
||||||
approver: level.approverName || level.approver_name || '',
|
approver: level.approverName || level.approver_name || '',
|
||||||
|
approverEmail: (level.approverEmail || level.approver_email || '').toLowerCase(),
|
||||||
status: level.status?.toLowerCase() || 'waiting',
|
status: level.status?.toLowerCase() || 'waiting',
|
||||||
tatHours: level.tatHours || level.tat_hours || 24,
|
tatHours: level.tatHours || level.tat_hours || 24,
|
||||||
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
||||||
@ -262,24 +268,37 @@ export function DealerClaimWorkflowTab({
|
|||||||
// Find approval data for this step
|
// Find approval data for this step
|
||||||
const approval = request?.approvals?.find((a: any) => a.levelId === step.levelId);
|
const approval = request?.approvals?.find((a: any) => a.levelId === step.levelId);
|
||||||
|
|
||||||
// Extract IO details from approval data or request (Step 3)
|
// Extract IO details from internalOrder table (Step 3)
|
||||||
let ioDetails = undefined;
|
let ioDetails = undefined;
|
||||||
if (step.step === 3) {
|
if (step.step === 3) {
|
||||||
if (approval?.ioDetails) {
|
// Get IO details from dedicated internalOrder table
|
||||||
|
const internalOrder = request?.internalOrder || request?.internal_order;
|
||||||
|
|
||||||
|
if (internalOrder?.ioNumber || internalOrder?.io_number) {
|
||||||
ioDetails = {
|
ioDetails = {
|
||||||
ioNumber: approval.ioDetails.ioNumber || '',
|
ioNumber: internalOrder.ioNumber || internalOrder.io_number || '',
|
||||||
ioRemark: approval.ioDetails.ioRemark || '',
|
ioRemark: internalOrder.ioRemark || internalOrder.io_remark || 'N/A',
|
||||||
organizedBy: approval.ioDetails.organizedBy || step.approver,
|
organizedBy:
|
||||||
organizedAt: approval.ioDetails.organizedAt || step.approvedAt || '',
|
internalOrder.organizer?.displayName ||
|
||||||
};
|
internalOrder.organizer?.name ||
|
||||||
} else if (request?.ioNumber) {
|
internalOrder.organizedBy ||
|
||||||
// Fallback to request-level IO data
|
step.approver ||
|
||||||
ioDetails = {
|
'N/A',
|
||||||
ioNumber: request.ioNumber || '',
|
organizedAt:
|
||||||
ioRemark: request.ioRemark || request.ioDetails?.ioRemark || '',
|
internalOrder.organizedAt ||
|
||||||
organizedBy: step.approver,
|
internalOrder.organized_at ||
|
||||||
organizedAt: step.approvedAt || request.updatedAt || '',
|
step.approvedAt ||
|
||||||
|
request?.updatedAt ||
|
||||||
|
'',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Debug logging for Step 3 IO details
|
||||||
|
console.log('[DealerClaimWorkflowTab] Step 3 IO Details Debug:', {
|
||||||
|
step: step.step,
|
||||||
|
status: step.status,
|
||||||
|
internalOrder,
|
||||||
|
ioDetails,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,13 +323,19 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize status - handle "in-review" and other variations
|
||||||
|
let normalizedStatus = (step.status || 'waiting').toLowerCase();
|
||||||
|
if (normalizedStatus === 'in-review' || normalizedStatus === 'in_review' || normalizedStatus === 'in review') {
|
||||||
|
normalizedStatus = 'in_progress';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: step.step || index + 1,
|
step: step.step || index + 1,
|
||||||
title: stepTitles[index] || `Step ${step.step || index + 1}`,
|
title: stepTitles[index] || `Step ${step.step || index + 1}`,
|
||||||
approver: step.approver || 'Unknown',
|
approver: step.approver || 'Unknown',
|
||||||
description: stepDescriptions[index] || step.description || '',
|
description: stepDescriptions[index] || step.description || '',
|
||||||
tatHours: step.tatHours || 24,
|
tatHours: step.tatHours || 24,
|
||||||
status: (step.status || 'waiting').toLowerCase() as any,
|
status: normalizedStatus as any,
|
||||||
comment: step.comment || approval?.comment,
|
comment: step.comment || approval?.comment,
|
||||||
approvedAt: step.approvedAt || approval?.timestamp,
|
approvedAt: step.approvedAt || approval?.timestamp,
|
||||||
elapsedHours: step.elapsedHours,
|
elapsedHours: step.elapsedHours,
|
||||||
@ -323,13 +348,58 @@ export function DealerClaimWorkflowTab({
|
|||||||
|
|
||||||
const totalSteps = request?.totalSteps || 8;
|
const totalSteps = request?.totalSteps || 8;
|
||||||
|
|
||||||
// Calculate currentStep from approval flow - find the first pending step
|
// Calculate currentStep from approval flow - find the first pending or in_progress step
|
||||||
// If no pending step, use the request's currentStep
|
// If no pending/in_progress step, use the request's currentStep
|
||||||
const pendingStep = workflowSteps.find(s => s.status === 'pending');
|
// Note: Status normalization already handled in workflowSteps mapping above
|
||||||
const currentStep = pendingStep ? pendingStep.step : (request?.currentStep || 1);
|
const activeStep = workflowSteps.find(s => {
|
||||||
|
const status = s.status?.toLowerCase() || '';
|
||||||
|
return status === 'pending' || status === 'in_progress' || status === 'in-review' || status === 'in_review';
|
||||||
|
});
|
||||||
|
const currentStep = activeStep ? activeStep.step : (request?.currentStep || 1);
|
||||||
|
|
||||||
const completedCount = workflowSteps.filter(s => s.status === 'approved').length;
|
const completedCount = workflowSteps.filter(s => s.status === 'approved').length;
|
||||||
|
|
||||||
|
// Check if current user is the dealer (for steps 1 and 5)
|
||||||
|
const userEmail = (user as any)?.email?.toLowerCase() || '';
|
||||||
|
const dealerEmail = (
|
||||||
|
(request as any)?.dealerEmail?.toLowerCase() ||
|
||||||
|
(request as any)?.dealer?.email?.toLowerCase() ||
|
||||||
|
(request as any)?.claimDetails?.dealerEmail?.toLowerCase() ||
|
||||||
|
(request as any)?.claimDetails?.dealer_email?.toLowerCase() ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
const isDealer = dealerEmail && userEmail === dealerEmail;
|
||||||
|
|
||||||
|
// Check if current user is the approver for each step based on logged-in email
|
||||||
|
const getStepApproverEmail = (stepNumber: number) => {
|
||||||
|
const level = approvalFlow.find((l: any) =>
|
||||||
|
(l.step || l.levelNumber || l.level_number) === stepNumber
|
||||||
|
);
|
||||||
|
return (level?.approverEmail || '').toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if current user is the approver for the current step
|
||||||
|
const currentApprovalLevel = approvalFlow.find((level: any) =>
|
||||||
|
(level.step || level.levelNumber || level.level_number) === currentStep
|
||||||
|
);
|
||||||
|
const approverEmail = (currentApprovalLevel?.approverEmail || '').toLowerCase();
|
||||||
|
const isCurrentApprover = approverEmail && userEmail === approverEmail;
|
||||||
|
|
||||||
|
// Check if user is approver for step 2 (requestor evaluation) - match by email
|
||||||
|
const step2Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 2);
|
||||||
|
const step2ApproverEmail = (step2Level?.approverEmail || '').toLowerCase();
|
||||||
|
const isStep2Approver = step2ApproverEmail && userEmail === step2ApproverEmail;
|
||||||
|
|
||||||
|
// Check if user is approver for step 1 (dealer proposal submission) - match by email
|
||||||
|
const step1Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 1);
|
||||||
|
const step1ApproverEmail = (step1Level?.approverEmail || '').toLowerCase();
|
||||||
|
const isStep1Approver = step1ApproverEmail && userEmail === step1ApproverEmail;
|
||||||
|
|
||||||
|
// Check if user is approver for step 3 (department lead approval) - match by email
|
||||||
|
const step3Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 3);
|
||||||
|
const step3ApproverEmail = (step3Level?.approverEmail || '').toLowerCase();
|
||||||
|
const isStep3Approver = step3ApproverEmail && userEmail === step3ApproverEmail;
|
||||||
|
|
||||||
// Handle proposal submission
|
// Handle proposal submission
|
||||||
const handleProposalSubmit = async (data: {
|
const handleProposalSubmit = async (data: {
|
||||||
proposalDocument: File | null;
|
proposalDocument: File | null;
|
||||||
@ -497,12 +567,14 @@ export function DealerClaimWorkflowTab({
|
|||||||
// The backend should handle SAP integration
|
// The backend should handle SAP integration
|
||||||
await updateIODetails(requestId, {
|
await updateIODetails(requestId, {
|
||||||
ioNumber: data.ioNumber,
|
ioNumber: data.ioNumber,
|
||||||
|
ioRemark: data.ioRemark,
|
||||||
ioAvailableBalance: 0, // Should come from SAP integration
|
ioAvailableBalance: 0, // Should come from SAP integration
|
||||||
ioBlockedAmount: 0, // Should come from SAP integration
|
ioBlockedAmount: 0, // Should come from SAP integration
|
||||||
ioRemainingBalance: 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
|
||||||
|
// IO remark is stored in claimDetails, so we just pass the comments
|
||||||
await approveLevel(requestId, levelId, data.comments);
|
await approveLevel(requestId, levelId, data.comments);
|
||||||
|
|
||||||
// Create work note for activity log
|
// Create work note for activity log
|
||||||
@ -521,6 +593,68 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle completion documents submission (Step 5)
|
||||||
|
const handleCompletionSubmit = async (data: {
|
||||||
|
activityCompletionDate: string;
|
||||||
|
numberOfParticipants?: number;
|
||||||
|
closedExpenses: Array<{ id: string; description: string; amount: number }>;
|
||||||
|
totalClosedExpenses: number;
|
||||||
|
completionDocuments: File[];
|
||||||
|
activityPhotos: File[];
|
||||||
|
invoicesReceipts?: File[];
|
||||||
|
attendanceSheet?: File;
|
||||||
|
completionDescription: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
if (!request?.id && !request?.requestId) {
|
||||||
|
throw new Error('Request ID not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = request.id || request.requestId;
|
||||||
|
|
||||||
|
// Transform expense items to match API format
|
||||||
|
const closedExpenses = data.closedExpenses.map(item => ({
|
||||||
|
description: item.description,
|
||||||
|
amount: item.amount,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Submit completion documents using dealer claim API
|
||||||
|
await submitCompletion(requestId, {
|
||||||
|
activityCompletionDate: data.activityCompletionDate,
|
||||||
|
numberOfParticipants: data.numberOfParticipants,
|
||||||
|
closedExpenses,
|
||||||
|
totalClosedExpenses: data.totalClosedExpenses,
|
||||||
|
completionDocuments: data.completionDocuments,
|
||||||
|
activityPhotos: data.activityPhotos,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload supporting documents if provided
|
||||||
|
if (data.invoicesReceipts && data.invoicesReceipts.length > 0) {
|
||||||
|
for (const file of data.invoicesReceipts) {
|
||||||
|
await uploadDocument(file, requestId, 'SUPPORTING');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.attendanceSheet) {
|
||||||
|
await uploadDocument(data.attendanceSheet, requestId, 'SUPPORTING');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create work note with completion description
|
||||||
|
await createWorkNoteMultipart(requestId, {
|
||||||
|
message: `Dealer submitted completion documents. ${data.completionDescription ? `Description: ${data.completionDescription}` : ''}${data.totalClosedExpenses > 0 ? ` Total closed expenses: ₹${data.totalClosedExpenses.toLocaleString('en-IN')}` : ''}`,
|
||||||
|
isPriority: false,
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
toast.success('Completion documents submitted successfully');
|
||||||
|
handleRefresh();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to submit completion documents:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to submit completion documents. Please try again.';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle IO rejection (Step 3)
|
// Handle IO rejection (Step 3)
|
||||||
const handleIORejection = async (comments: string) => {
|
const handleIORejection = async (comments: string) => {
|
||||||
try {
|
try {
|
||||||
@ -686,10 +820,26 @@ export function DealerClaimWorkflowTab({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{workflowSteps.map((step, index) => {
|
{workflowSteps.map((step, index) => {
|
||||||
const isActive = step.status === 'pending' && step.step === currentStep;
|
// Step is active if it's pending or in_progress and matches currentStep
|
||||||
|
const isActive = (step.status === 'pending' || step.status === 'in_progress') && step.step === currentStep;
|
||||||
const isCompleted = step.status === 'approved';
|
const isCompleted = step.status === 'approved';
|
||||||
const isWaiting = step.status === 'waiting';
|
const isWaiting = step.status === 'waiting';
|
||||||
|
|
||||||
|
// Debug logging for Step 1
|
||||||
|
if (step.step === 1) {
|
||||||
|
console.log('[DealerClaimWorkflowTab] Step 1 Debug:', {
|
||||||
|
step: step.step,
|
||||||
|
status: step.status,
|
||||||
|
currentStep,
|
||||||
|
isActive,
|
||||||
|
isDealer,
|
||||||
|
isStep1Approver,
|
||||||
|
step1ApproverEmail,
|
||||||
|
userEmail,
|
||||||
|
dealerEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Debug logging for Step 2
|
// Debug logging for Step 2
|
||||||
if (step.step === 2) {
|
if (step.step === 2) {
|
||||||
console.log('[DealerClaimWorkflowTab] Step 2 Debug:', {
|
console.log('[DealerClaimWorkflowTab] Step 2 Debug:', {
|
||||||
@ -698,10 +848,42 @@ export function DealerClaimWorkflowTab({
|
|||||||
currentStep,
|
currentStep,
|
||||||
isActive,
|
isActive,
|
||||||
isInitiator,
|
isInitiator,
|
||||||
|
isStep2Approver,
|
||||||
showApprovalModal,
|
showApprovalModal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logging for Step 3
|
||||||
|
if (step.step === 3) {
|
||||||
|
console.log('[DealerClaimWorkflowTab] Step 3 Debug:', {
|
||||||
|
step: step.step,
|
||||||
|
status: step.status,
|
||||||
|
currentStep,
|
||||||
|
isActive,
|
||||||
|
isCurrentApprover,
|
||||||
|
isStep3Approver,
|
||||||
|
step3ApproverEmail,
|
||||||
|
userEmail,
|
||||||
|
approverEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging for Step 5
|
||||||
|
if (step.step === 5) {
|
||||||
|
console.log('[DealerClaimWorkflowTab] Step 5 Debug:', {
|
||||||
|
step: step.step,
|
||||||
|
status: step.status,
|
||||||
|
currentStep,
|
||||||
|
isActive,
|
||||||
|
isDealer,
|
||||||
|
dealerEmail,
|
||||||
|
userEmail,
|
||||||
|
requestDealerEmail: (request as any)?.dealerEmail,
|
||||||
|
requestClaimDetails: (request as any)?.claimDetails,
|
||||||
|
claimDetailsDealerEmail: (request as any)?.claimDetails?.dealerEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@ -722,7 +904,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
Step {step.step}: {step.title}
|
Step {step.step}: {step.title}
|
||||||
</h4>
|
</h4>
|
||||||
<Badge className={getStepBadgeVariant(step.status)}>
|
<Badge className={getStepBadgeVariant(step.status)}>
|
||||||
{step.status}
|
{step.status.toLowerCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
{/* Email Template Button (Step 4) */}
|
{/* Email Template Button (Step 4) */}
|
||||||
{step.step === 4 && step.emailTemplateUrl && (
|
{step.step === 4 && step.emailTemplateUrl && (
|
||||||
@ -769,8 +951,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* IO Organization Details (Step 3) */}
|
{/* IO Organization Details (Step 3) - Show when step is approved and has IO details */}
|
||||||
{step.step === 3 && step.ioDetails && step.ioDetails.ioNumber && (
|
{step.step === 3 && step.status === 'approved' && step.ioDetails && step.ioDetails.ioNumber && (
|
||||||
<div className="mt-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
<div className="mt-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Receipt className="w-4 h-4 text-blue-600" />
|
<Receipt className="w-4 h-4 text-blue-600" />
|
||||||
@ -785,18 +967,19 @@ export function DealerClaimWorkflowTab({
|
|||||||
{step.ioDetails.ioNumber}
|
{step.ioDetails.ioNumber}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{step.ioDetails.ioRemark && (
|
|
||||||
<div className="pt-1.5 border-t border-blue-100">
|
<div className="pt-1.5 border-t border-blue-100">
|
||||||
<p className="text-xs text-gray-600 mb-1">IO Remark:</p>
|
<p className="text-xs text-gray-600 mb-1">IO Remark:</p>
|
||||||
<p className="text-sm text-gray-900">{step.ioDetails.ioRemark}</p>
|
<p className="text-sm text-gray-900">
|
||||||
|
{step.ioDetails.ioRemark || 'N/A'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{step.ioDetails.organizedAt && (
|
|
||||||
<div className="pt-1.5 border-t border-blue-100 text-xs text-gray-500">
|
<div className="pt-1.5 border-t border-blue-100 text-xs text-gray-500">
|
||||||
Organised by {step.ioDetails.organizedBy} on{' '}
|
Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '}
|
||||||
{formatDateSafe(step.ioDetails.organizedAt)}
|
{step.ioDetails.organizedAt
|
||||||
|
? formatDateSafe(step.ioDetails.organizedAt)
|
||||||
|
: (step.approvedAt ? formatDateSafe(step.approvedAt) : 'N/A')
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -836,8 +1019,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<div className="mt-4 flex gap-2">
|
<div className="mt-4 flex gap-2">
|
||||||
{/* Step 1: Submit Proposal Button */}
|
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
|
||||||
{step.step === 1 && (
|
{step.step === 1 && (isDealer || isStep1Approver) && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -850,8 +1033,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 2: Review & Approve Proposal - Show for initiator when step is active */}
|
{/* Step 2: Confirm Request - Only for initiator or step 2 approver */}
|
||||||
{step.step === 2 && isInitiator && (
|
{step.step === 2 && (isInitiator || isStep2Approver) && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -860,12 +1043,18 @@ export function DealerClaimWorkflowTab({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
Review & Evaluate Proposal
|
Confirm Request
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 3: Approve and Organise IO */}
|
{/* Step 3: Approve and Organise IO - Only for department lead (step 3 approver) */}
|
||||||
{step.step === 3 && (
|
{step.step === 3 && (() => {
|
||||||
|
// Find step 3 from approvalFlow to get approverEmail
|
||||||
|
const step3Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 3);
|
||||||
|
const step3ApproverEmail = (step3Level?.approverEmail || '').toLowerCase();
|
||||||
|
const isStep3ApproverByEmail = step3ApproverEmail && userEmail === step3ApproverEmail;
|
||||||
|
return isStep3ApproverByEmail || isStep3Approver || isCurrentApprover;
|
||||||
|
})() && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-green-600 hover:bg-green-700"
|
className="bg-green-600 hover:bg-green-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -878,18 +1067,74 @@ export function DealerClaimWorkflowTab({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 5: Upload Completion Documents */}
|
{/* Step 5: Upload Completion Documents - Only for dealer */}
|
||||||
{step.step === 5 && (
|
{step.step === 5 && isDealer && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('[DealerClaimWorkflowTab] Upload Completion Documents clicked for Step 5');
|
console.log('[DealerClaimWorkflowTab] Opening completion documents modal for Step 5');
|
||||||
// TODO: Open document upload modal
|
setShowCompletionModal(true);
|
||||||
toast.info('Document upload feature coming soon');
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
Upload Documents
|
Upload Completion Docs
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 6: Push to DMS - Only for initiator or step 6 approver */}
|
||||||
|
{step.step === 6 && (isInitiator || (() => {
|
||||||
|
const step6Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 6);
|
||||||
|
const step6ApproverEmail = (step6Level?.approverEmail || '').toLowerCase();
|
||||||
|
return step6ApproverEmail && userEmail === step6ApproverEmail;
|
||||||
|
})()) && (
|
||||||
|
<Button
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700"
|
||||||
|
onClick={async () => {
|
||||||
|
console.log('[DealerClaimWorkflowTab] Pushing to DMS for Step 6');
|
||||||
|
try {
|
||||||
|
const requestId = request?.requestId || request?.id;
|
||||||
|
if (!requestId) {
|
||||||
|
toast.error('Request ID not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Call API to push to DMS (this will auto-generate e-invoice)
|
||||||
|
// eInvoiceDate is required, so we pass current date
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
await updateEInvoice(requestId as string, {
|
||||||
|
eInvoiceDate: today,
|
||||||
|
});
|
||||||
|
toast.success('Pushed to DMS successfully');
|
||||||
|
handleRefresh();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[DealerClaimWorkflowTab] Error pushing to DMS:', error);
|
||||||
|
toast.error(error?.response?.data?.message || 'Failed to push to DMS');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Activity className="w-4 h-4 mr-2" />
|
||||||
|
Push to DMS
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 8: View & Send Credit Note - Only for finance approver or step 8 approver */}
|
||||||
|
{step.step === 8 && (() => {
|
||||||
|
const step8Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 8);
|
||||||
|
const step8ApproverEmail = (step8Level?.approverEmail || '').toLowerCase();
|
||||||
|
const isStep8Approver = step8ApproverEmail && userEmail === step8ApproverEmail;
|
||||||
|
// Also check if user has finance role
|
||||||
|
const userRole = (user as any)?.role?.toUpperCase() || '';
|
||||||
|
const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN';
|
||||||
|
return isStep8Approver || isFinanceUser;
|
||||||
|
})() && (
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={() => {
|
||||||
|
console.log('[DealerClaimWorkflowTab] Opening credit note modal for Step 8');
|
||||||
|
setShowCreditNoteModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Receipt className="w-4 h-4 mr-2" />
|
||||||
|
View & Send Credit Note
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -944,6 +1189,45 @@ export function DealerClaimWorkflowTab({
|
|||||||
requestTitle={request?.title}
|
requestTitle={request?.title}
|
||||||
requestId={request?.id || request?.requestId}
|
requestId={request?.id || request?.requestId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Dealer Completion Documents Modal */}
|
||||||
|
<DealerCompletionDocumentsModal
|
||||||
|
isOpen={showCompletionModal}
|
||||||
|
onClose={() => setShowCompletionModal(false)}
|
||||||
|
onSubmit={handleCompletionSubmit}
|
||||||
|
dealerName={dealerName}
|
||||||
|
activityName={activityName}
|
||||||
|
requestId={request?.id || request?.requestId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Credit Note from SAP Modal (Step 8) */}
|
||||||
|
<CreditNoteSAPModal
|
||||||
|
isOpen={showCreditNoteModal}
|
||||||
|
onClose={() => setShowCreditNoteModal(false)}
|
||||||
|
onDownload={async () => {
|
||||||
|
// TODO: Implement download functionality
|
||||||
|
toast.info('Download functionality will be implemented');
|
||||||
|
}}
|
||||||
|
onSendToDealer={async () => {
|
||||||
|
// TODO: Implement send to dealer functionality
|
||||||
|
toast.info('Send to dealer functionality will be implemented');
|
||||||
|
}}
|
||||||
|
creditNoteData={{
|
||||||
|
creditNoteNumber: (request as any)?.claimDetails?.creditNoteNumber || (request as any)?.claimDetails?.credit_note_number,
|
||||||
|
creditNoteDate: (request as any)?.claimDetails?.creditNoteDate || (request as any)?.claimDetails?.credit_note_date,
|
||||||
|
creditNoteAmount: (request as any)?.claimDetails?.creditNoteAmount || (request as any)?.claimDetails?.credit_note_amount,
|
||||||
|
status: 'APPROVED',
|
||||||
|
}}
|
||||||
|
dealerInfo={{
|
||||||
|
dealerName: (request as any)?.claimDetails?.dealerName || (request as any)?.claimDetails?.dealer_name,
|
||||||
|
dealerCode: (request as any)?.claimDetails?.dealerCode || (request as any)?.claimDetails?.dealer_code,
|
||||||
|
dealerEmail: (request as any)?.claimDetails?.dealerEmail || (request as any)?.claimDetails?.dealer_email,
|
||||||
|
}}
|
||||||
|
activityName={(request as any)?.claimDetails?.activityName || (request as any)?.claimDetails?.activity_name}
|
||||||
|
requestNumber={request?.requestNumber || request?.id}
|
||||||
|
requestId={request?.requestId || request?.id}
|
||||||
|
dueDate={request?.dueDate}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -187,16 +187,26 @@ export async function updateIODetails(
|
|||||||
requestId: string,
|
requestId: string,
|
||||||
ioData: {
|
ioData: {
|
||||||
ioNumber: string;
|
ioNumber: string;
|
||||||
ioAvailableBalance: number;
|
ioRemark?: string;
|
||||||
ioBlockedAmount: number;
|
ioAvailableBalance?: number;
|
||||||
ioRemainingBalance: number;
|
ioBlockedAmount?: number;
|
||||||
|
ioRemainingBalance?: number;
|
||||||
}
|
}
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.put(`/dealer-claims/${requestId}/io`, ioData);
|
// Map frontend field names to backend expected field names
|
||||||
|
const payload = {
|
||||||
|
ioNumber: ioData.ioNumber,
|
||||||
|
ioRemark: ioData.ioRemark || '',
|
||||||
|
availableBalance: ioData.ioAvailableBalance ?? 0,
|
||||||
|
blockedAmount: ioData.ioBlockedAmount ?? 0,
|
||||||
|
remainingBalance: ioData.ioRemainingBalance ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await apiClient.put(`/dealer-claims/${requestId}/io`, payload);
|
||||||
return response.data?.data || response.data;
|
return response.data?.data || response.data;
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error('[DealerClaimAPI] Error updating IO details:', error);
|
console.error('Error updating IO details:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,12 +51,15 @@ export interface ClaimManagementRequest {
|
|||||||
submittedAt?: string;
|
submittedAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// IO Details (Step 3)
|
// IO Details (Step 3) - from internal_orders table
|
||||||
ioDetails?: {
|
ioDetails?: {
|
||||||
ioNumber?: string;
|
ioNumber?: string;
|
||||||
|
ioRemark?: string;
|
||||||
availableBalance?: number;
|
availableBalance?: number;
|
||||||
blockedAmount?: number;
|
blockedAmount?: number;
|
||||||
remainingBalance?: number;
|
remainingBalance?: number;
|
||||||
|
organizedBy?: string;
|
||||||
|
organizedAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// DMS Details (Step 7)
|
// DMS Details (Step 7)
|
||||||
@ -104,6 +107,7 @@ export function mapToClaimManagementRequest(
|
|||||||
const claimDetails = apiRequest.claimDetails || {};
|
const claimDetails = apiRequest.claimDetails || {};
|
||||||
const proposalDetails = apiRequest.proposalDetails || {};
|
const proposalDetails = apiRequest.proposalDetails || {};
|
||||||
const completionDetails = apiRequest.completionDetails || {};
|
const completionDetails = apiRequest.completionDetails || {};
|
||||||
|
const internalOrder = apiRequest.internalOrder || apiRequest.internal_order || {};
|
||||||
|
|
||||||
// Debug: Log raw claim details to help troubleshoot
|
// Debug: Log raw claim details to help troubleshoot
|
||||||
console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
|
console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
|
||||||
@ -171,12 +175,15 @@ export function mapToClaimManagementRequest(
|
|||||||
submittedAt: proposalDetails.submittedAt || proposalDetails.submitted_at,
|
submittedAt: proposalDetails.submittedAt || proposalDetails.submitted_at,
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
// Map IO details
|
// Map IO details from dedicated internal_orders table
|
||||||
const ioDetails = {
|
const ioDetails = {
|
||||||
ioNumber: claimDetails.ioNumber || claimDetails.io_number,
|
ioNumber: internalOrder.ioNumber || internalOrder.io_number || claimDetails.ioNumber || claimDetails.io_number,
|
||||||
availableBalance: claimDetails.ioAvailableBalance || claimDetails.io_available_balance,
|
ioRemark: internalOrder.ioRemark || internalOrder.io_remark || '',
|
||||||
blockedAmount: claimDetails.ioBlockedAmount || claimDetails.io_blocked_amount,
|
availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || claimDetails.ioAvailableBalance || claimDetails.io_available_balance,
|
||||||
remainingBalance: claimDetails.ioRemainingBalance || claimDetails.io_remaining_balance,
|
blockedAmount: internalOrder.ioBlockedAmount || internalOrder.io_blocked_amount || claimDetails.ioBlockedAmount || claimDetails.io_blocked_amount,
|
||||||
|
remainingBalance: internalOrder.ioRemainingBalance || internalOrder.io_remaining_balance || claimDetails.ioRemainingBalance || claimDetails.io_remaining_balance,
|
||||||
|
organizedBy: internalOrder.organizer?.displayName || internalOrder.organizer?.name || internalOrder.organizedBy || '',
|
||||||
|
organizedAt: internalOrder.organizedAt || internalOrder.organized_at || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map DMS details
|
// Map DMS details
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user