all modal ui added for dealerclaim wokflow step checked all 8 steps

This commit is contained in:
laxmanhalaki 2025-12-10 20:45:26 +05:30
parent 0e9f8adbf6
commit 69c7e99d18
9 changed files with 1492 additions and 75 deletions

View File

@ -279,15 +279,17 @@ 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
<SelectItem key={dealer.dealerCode} value={dealer.dealerCode}> .filter((dealer) => dealer.dealerCode && dealer.dealerCode.trim() !== '')
<div className="flex items-center gap-2"> .map((dealer) => (
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span> <SelectItem key={dealer.dealerCode} value={dealer.dealerCode}>
<span className="text-gray-400"></span> <div className="flex items-center gap-2">
<span>{dealer.dealerName}</span> <span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
</div> <span className="text-gray-400"></span>
</SelectItem> <span>{dealer.dealerName}</span>
)) </div>
</SelectItem>
))
)} )}
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -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);

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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]" />

View File

@ -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

View File

@ -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">
<p className="text-sm text-gray-900">{step.ioDetails.ioRemark}</p> {step.ioDetails.ioRemark || 'N/A'}
</div> </p>
)} </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 || step.approver || 'N/A'} on{' '}
Organised by {step.ioDetails.organizedBy} on{' '} {step.ioDetails.organizedAt
{formatDateSafe(step.ioDetails.organizedAt)} ? formatDateSafe(step.ioDetails.organizedAt)
</div> : (step.approvedAt ? formatDateSafe(step.approvedAt) : 'N/A')
)} }
</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}
/>
</> </>
); );
} }

View File

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

View File

@ -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