all modal ui added for dealerclaim wokflow step checked all 8 steps
This commit is contained in:
parent
0e9f8adbf6
commit
69c7e99d18
@ -279,15 +279,17 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
{dealers.length === 0 && !loadingDealers ? (
|
||||
<div className="p-2 text-sm text-gray-500">No dealers available</div>
|
||||
) : (
|
||||
dealers.map((dealer) => (
|
||||
<SelectItem key={dealer.dealerCode} value={dealer.dealerCode}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
|
||||
<span className="text-gray-400">•</span>
|
||||
<span>{dealer.dealerName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
dealers
|
||||
.filter((dealer) => dealer.dealerCode && dealer.dealerCode.trim() !== '')
|
||||
.map((dealer) => (
|
||||
<SelectItem key={dealer.dealerCode} value={dealer.dealerCode}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
|
||||
<span className="text-gray-400">•</span>
|
||||
<span>{dealer.dealerName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@ -236,6 +236,7 @@ export function useRequestDetails(
|
||||
let claimDetails = null;
|
||||
let proposalDetails = null;
|
||||
let completionDetails = null;
|
||||
let internalOrder = null;
|
||||
|
||||
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
||||
try {
|
||||
@ -255,12 +256,14 @@ export function useRequestDetails(
|
||||
hasClaimDetails: !!(claimData?.claimDetails || claimData?.claim_details),
|
||||
hasProposalDetails: !!(claimData?.proposalDetails || claimData?.proposal_details),
|
||||
hasCompletionDetails: !!(claimData?.completionDetails || claimData?.completion_details),
|
||||
hasInternalOrder: !!(claimData?.internalOrder || claimData?.internal_order),
|
||||
});
|
||||
|
||||
if (claimData) {
|
||||
claimDetails = claimData.claimDetails || claimData.claim_details;
|
||||
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
||||
completionDetails = claimData.completionDetails || claimData.completion_details;
|
||||
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
||||
|
||||
console.debug('[useRequestDetails] Extracted details:', {
|
||||
claimDetails: claimDetails ? {
|
||||
@ -274,6 +277,7 @@ export function useRequestDetails(
|
||||
} : null,
|
||||
hasProposalDetails: !!proposalDetails,
|
||||
hasCompletionDetails: !!completionDetails,
|
||||
hasInternalOrder: !!internalOrder,
|
||||
});
|
||||
} else {
|
||||
console.warn('[useRequestDetails] No claimData found in response');
|
||||
@ -332,6 +336,7 @@ export function useRequestDetails(
|
||||
claimDetails: claimDetails || null,
|
||||
proposalDetails: proposalDetails || null,
|
||||
completionDetails: completionDetails || null,
|
||||
internalOrder: internalOrder || null,
|
||||
};
|
||||
|
||||
setApiRequest(updatedRequest);
|
||||
@ -513,6 +518,7 @@ export function useRequestDetails(
|
||||
let claimDetails = null;
|
||||
let proposalDetails = null;
|
||||
let completionDetails = null;
|
||||
let internalOrder = null;
|
||||
|
||||
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
||||
try {
|
||||
@ -529,12 +535,14 @@ export function useRequestDetails(
|
||||
claimDetails = claimData.claimDetails || claimData.claim_details;
|
||||
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
||||
completionDetails = claimData.completionDetails || claimData.completion_details;
|
||||
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
||||
|
||||
console.debug('[useRequestDetails] Initial load - Extracted details:', {
|
||||
hasClaimDetails: !!claimDetails,
|
||||
claimDetailsKeys: claimDetails ? Object.keys(claimDetails) : [],
|
||||
hasProposalDetails: !!proposalDetails,
|
||||
hasCompletionDetails: !!completionDetails,
|
||||
hasInternalOrder: !!internalOrder,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
@ -583,6 +591,7 @@ export function useRequestDetails(
|
||||
claimDetails: claimDetails || null,
|
||||
proposalDetails: proposalDetails || null,
|
||||
completionDetails: completionDetails || null,
|
||||
internalOrder: internalOrder || null,
|
||||
};
|
||||
|
||||
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 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 gap-2">
|
||||
<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 { Badge } from '@/components/ui/badge';
|
||||
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 { DealerProposalSubmissionModal } from '../modals/DealerProposalSubmissionModal';
|
||||
import { InitiatorProposalApprovalModal } from '../modals/InitiatorProposalApprovalModal';
|
||||
import { DeptLeadIOApprovalModal } from '../modals/DeptLeadIOApprovalModal';
|
||||
import { DealerCompletionDocumentsModal } from '../modals/DealerCompletionDocumentsModal';
|
||||
import { CreditNoteSAPModal } from '../modals/CreditNoteSAPModal';
|
||||
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 { uploadDocument } from '@/services/documentApi';
|
||||
import { createWorkNoteMultipart } from '@/services/workflowApi';
|
||||
@ -38,7 +40,7 @@ interface WorkflowStep {
|
||||
approver: string;
|
||||
description: string;
|
||||
tatHours: number;
|
||||
status: 'pending' | 'approved' | 'waiting' | 'rejected';
|
||||
status: 'pending' | 'approved' | 'waiting' | 'rejected' | 'in_progress';
|
||||
comment?: string;
|
||||
approvedAt?: string;
|
||||
elapsedHours?: number;
|
||||
@ -89,7 +91,7 @@ const formatDateSafe = (dateString: string | undefined | null): string => {
|
||||
const getStepIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return <CheckCircle className="w-5 h-5 text-green-600" />;
|
||||
return <CircleCheckBig className="w-5 h-5 text-green-600" />;
|
||||
case 'pending':
|
||||
return <Clock className="w-5 h-5 text-blue-600" />;
|
||||
case 'rejected':
|
||||
@ -119,7 +121,7 @@ const getStepBadgeVariant = (status: string) => {
|
||||
* Get step card styling
|
||||
*/
|
||||
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';
|
||||
}
|
||||
if (status === 'approved') {
|
||||
@ -157,6 +159,8 @@ export function DealerClaimWorkflowTab({
|
||||
const [showProposalModal, setShowProposalModal] = useState(false);
|
||||
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
||||
const [showIOApprovalModal, setShowIOApprovalModal] = useState(false);
|
||||
const [showCompletionModal, setShowCompletionModal] = useState(false);
|
||||
const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
|
||||
|
||||
// Load approval flows from real API
|
||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||
@ -182,6 +186,7 @@ export function DealerClaimWorkflowTab({
|
||||
const flows = approvals.map((level: any) => ({
|
||||
step: level.levelNumber || level.level_number || 0,
|
||||
approver: level.approverName || level.approver_name || '',
|
||||
approverEmail: (level.approverEmail || level.approver_email || '').toLowerCase(),
|
||||
status: level.status?.toLowerCase() || 'waiting',
|
||||
tatHours: level.tatHours || level.tat_hours || 24,
|
||||
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
||||
@ -212,6 +217,7 @@ export function DealerClaimWorkflowTab({
|
||||
const flows = approvals.map((level: any) => ({
|
||||
step: level.levelNumber || level.level_number || 0,
|
||||
approver: level.approverName || level.approver_name || '',
|
||||
approverEmail: (level.approverEmail || level.approver_email || '').toLowerCase(),
|
||||
status: level.status?.toLowerCase() || 'waiting',
|
||||
tatHours: level.tatHours || level.tat_hours || 24,
|
||||
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
||||
@ -262,24 +268,37 @@ export function DealerClaimWorkflowTab({
|
||||
// Find approval data for this step
|
||||
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;
|
||||
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 = {
|
||||
ioNumber: approval.ioDetails.ioNumber || '',
|
||||
ioRemark: approval.ioDetails.ioRemark || '',
|
||||
organizedBy: approval.ioDetails.organizedBy || step.approver,
|
||||
organizedAt: approval.ioDetails.organizedAt || step.approvedAt || '',
|
||||
};
|
||||
} else if (request?.ioNumber) {
|
||||
// Fallback to request-level IO data
|
||||
ioDetails = {
|
||||
ioNumber: request.ioNumber || '',
|
||||
ioRemark: request.ioRemark || request.ioDetails?.ioRemark || '',
|
||||
organizedBy: step.approver,
|
||||
organizedAt: step.approvedAt || request.updatedAt || '',
|
||||
ioNumber: internalOrder.ioNumber || internalOrder.io_number || '',
|
||||
ioRemark: internalOrder.ioRemark || internalOrder.io_remark || 'N/A',
|
||||
organizedBy:
|
||||
internalOrder.organizer?.displayName ||
|
||||
internalOrder.organizer?.name ||
|
||||
internalOrder.organizedBy ||
|
||||
step.approver ||
|
||||
'N/A',
|
||||
organizedAt:
|
||||
internalOrder.organizedAt ||
|
||||
internalOrder.organized_at ||
|
||||
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 {
|
||||
step: step.step || index + 1,
|
||||
title: stepTitles[index] || `Step ${step.step || index + 1}`,
|
||||
approver: step.approver || 'Unknown',
|
||||
description: stepDescriptions[index] || step.description || '',
|
||||
tatHours: step.tatHours || 24,
|
||||
status: (step.status || 'waiting').toLowerCase() as any,
|
||||
status: normalizedStatus as any,
|
||||
comment: step.comment || approval?.comment,
|
||||
approvedAt: step.approvedAt || approval?.timestamp,
|
||||
elapsedHours: step.elapsedHours,
|
||||
@ -323,13 +348,58 @@ export function DealerClaimWorkflowTab({
|
||||
|
||||
const totalSteps = request?.totalSteps || 8;
|
||||
|
||||
// Calculate currentStep from approval flow - find the first pending step
|
||||
// If no pending step, use the request's currentStep
|
||||
const pendingStep = workflowSteps.find(s => s.status === 'pending');
|
||||
const currentStep = pendingStep ? pendingStep.step : (request?.currentStep || 1);
|
||||
// Calculate currentStep from approval flow - find the first pending or in_progress step
|
||||
// If no pending/in_progress step, use the request's currentStep
|
||||
// Note: Status normalization already handled in workflowSteps mapping above
|
||||
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;
|
||||
|
||||
// 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
|
||||
const handleProposalSubmit = async (data: {
|
||||
proposalDocument: File | null;
|
||||
@ -497,12 +567,14 @@ export function DealerClaimWorkflowTab({
|
||||
// The backend should handle SAP integration
|
||||
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
|
||||
});
|
||||
|
||||
// Approve Step 3 using real API
|
||||
// IO remark is stored in claimDetails, so we just pass the comments
|
||||
await approveLevel(requestId, levelId, data.comments);
|
||||
|
||||
// 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)
|
||||
const handleIORejection = async (comments: string) => {
|
||||
try {
|
||||
@ -686,10 +820,26 @@ export function DealerClaimWorkflowTab({
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{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 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
|
||||
if (step.step === 2) {
|
||||
console.log('[DealerClaimWorkflowTab] Step 2 Debug:', {
|
||||
@ -698,10 +848,42 @@ export function DealerClaimWorkflowTab({
|
||||
currentStep,
|
||||
isActive,
|
||||
isInitiator,
|
||||
isStep2Approver,
|
||||
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 (
|
||||
<div
|
||||
key={index}
|
||||
@ -722,7 +904,7 @@ export function DealerClaimWorkflowTab({
|
||||
Step {step.step}: {step.title}
|
||||
</h4>
|
||||
<Badge className={getStepBadgeVariant(step.status)}>
|
||||
{step.status}
|
||||
{step.status.toLowerCase()}
|
||||
</Badge>
|
||||
{/* Email Template Button (Step 4) */}
|
||||
{step.step === 4 && step.emailTemplateUrl && (
|
||||
@ -769,8 +951,8 @@ export function DealerClaimWorkflowTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* IO Organization Details (Step 3) */}
|
||||
{step.step === 3 && step.ioDetails && step.ioDetails.ioNumber && (
|
||||
{/* IO Organization Details (Step 3) - Show when step is approved and has IO details */}
|
||||
{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="flex items-center gap-2 mb-2">
|
||||
<Receipt className="w-4 h-4 text-blue-600" />
|
||||
@ -785,18 +967,19 @@ export function DealerClaimWorkflowTab({
|
||||
{step.ioDetails.ioNumber}
|
||||
</span>
|
||||
</div>
|
||||
{step.ioDetails.ioRemark && (
|
||||
<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-sm text-gray-900">{step.ioDetails.ioRemark}</p>
|
||||
</div>
|
||||
)}
|
||||
{step.ioDetails.organizedAt && (
|
||||
<div className="pt-1.5 border-t border-blue-100 text-xs text-gray-500">
|
||||
Organised by {step.ioDetails.organizedBy} on{' '}
|
||||
{formatDateSafe(step.ioDetails.organizedAt)}
|
||||
</div>
|
||||
)}
|
||||
<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-sm text-gray-900">
|
||||
{step.ioDetails.ioRemark || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-1.5 border-t border-blue-100 text-xs text-gray-500">
|
||||
Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '}
|
||||
{step.ioDetails.organizedAt
|
||||
? formatDateSafe(step.ioDetails.organizedAt)
|
||||
: (step.approvedAt ? formatDateSafe(step.approvedAt) : 'N/A')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -836,8 +1019,8 @@ export function DealerClaimWorkflowTab({
|
||||
{/* Action Buttons */}
|
||||
{isActive && (
|
||||
<div className="mt-4 flex gap-2">
|
||||
{/* Step 1: Submit Proposal Button */}
|
||||
{step.step === 1 && (
|
||||
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
|
||||
{step.step === 1 && (isDealer || isStep1Approver) && (
|
||||
<Button
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={() => {
|
||||
@ -850,8 +1033,8 @@ export function DealerClaimWorkflowTab({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Step 2: Review & Approve Proposal - Show for initiator when step is active */}
|
||||
{step.step === 2 && isInitiator && (
|
||||
{/* Step 2: Confirm Request - Only for initiator or step 2 approver */}
|
||||
{step.step === 2 && (isInitiator || isStep2Approver) && (
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
onClick={() => {
|
||||
@ -860,12 +1043,18 @@ export function DealerClaimWorkflowTab({
|
||||
}}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Review & Evaluate Proposal
|
||||
Confirm Request
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Step 3: Approve and Organise IO */}
|
||||
{step.step === 3 && (
|
||||
{/* Step 3: Approve and Organise IO - Only for department lead (step 3 approver) */}
|
||||
{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
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => {
|
||||
@ -878,18 +1067,74 @@ export function DealerClaimWorkflowTab({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Step 5: Upload Completion Documents */}
|
||||
{step.step === 5 && (
|
||||
{/* Step 5: Upload Completion Documents - Only for dealer */}
|
||||
{step.step === 5 && isDealer && (
|
||||
<Button
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={() => {
|
||||
console.log('[DealerClaimWorkflowTab] Upload Completion Documents clicked for Step 5');
|
||||
// TODO: Open document upload modal
|
||||
toast.info('Document upload feature coming soon');
|
||||
console.log('[DealerClaimWorkflowTab] Opening completion documents modal for Step 5');
|
||||
setShowCompletionModal(true);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
@ -944,6 +1189,45 @@ export function DealerClaimWorkflowTab({
|
||||
requestTitle={request?.title}
|
||||
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,
|
||||
ioData: {
|
||||
ioNumber: string;
|
||||
ioAvailableBalance: number;
|
||||
ioBlockedAmount: number;
|
||||
ioRemainingBalance: number;
|
||||
ioRemark?: string;
|
||||
ioAvailableBalance?: number;
|
||||
ioBlockedAmount?: number;
|
||||
ioRemainingBalance?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
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;
|
||||
} catch (error: any) {
|
||||
console.error('[DealerClaimAPI] Error updating IO details:', error);
|
||||
} catch (error) {
|
||||
console.error('Error updating IO details:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,12 +51,15 @@ export interface ClaimManagementRequest {
|
||||
submittedAt?: string;
|
||||
};
|
||||
|
||||
// IO Details (Step 3)
|
||||
// IO Details (Step 3) - from internal_orders table
|
||||
ioDetails?: {
|
||||
ioNumber?: string;
|
||||
ioRemark?: string;
|
||||
availableBalance?: number;
|
||||
blockedAmount?: number;
|
||||
remainingBalance?: number;
|
||||
organizedBy?: string;
|
||||
organizedAt?: string;
|
||||
};
|
||||
|
||||
// DMS Details (Step 7)
|
||||
@ -104,6 +107,7 @@ export function mapToClaimManagementRequest(
|
||||
const claimDetails = apiRequest.claimDetails || {};
|
||||
const proposalDetails = apiRequest.proposalDetails || {};
|
||||
const completionDetails = apiRequest.completionDetails || {};
|
||||
const internalOrder = apiRequest.internalOrder || apiRequest.internal_order || {};
|
||||
|
||||
// Debug: Log raw claim details to help troubleshoot
|
||||
console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
|
||||
@ -171,12 +175,15 @@ export function mapToClaimManagementRequest(
|
||||
submittedAt: proposalDetails.submittedAt || proposalDetails.submitted_at,
|
||||
} : undefined;
|
||||
|
||||
// Map IO details
|
||||
// Map IO details from dedicated internal_orders table
|
||||
const ioDetails = {
|
||||
ioNumber: claimDetails.ioNumber || claimDetails.io_number,
|
||||
availableBalance: claimDetails.ioAvailableBalance || claimDetails.io_available_balance,
|
||||
blockedAmount: claimDetails.ioBlockedAmount || claimDetails.io_blocked_amount,
|
||||
remainingBalance: claimDetails.ioRemainingBalance || claimDetails.io_remaining_balance,
|
||||
ioNumber: internalOrder.ioNumber || internalOrder.io_number || claimDetails.ioNumber || claimDetails.io_number,
|
||||
ioRemark: internalOrder.ioRemark || internalOrder.io_remark || '',
|
||||
availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || claimDetails.ioAvailableBalance || claimDetails.io_available_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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user