module wise audit tables added

This commit is contained in:
laxman h 2026-04-13 20:42:25 +05:30
parent 7126d4b6bf
commit 71e6c10c16
10 changed files with 816 additions and 357 deletions

View File

@ -159,6 +159,9 @@ export const API = {
// Line items
addLineItem: (fnfId: string, data: any) => client.post(`/settlement/fnf/${fnfId}/line-items`, data),
updateFnFClearance: (fnfId: string, clearanceId: string, data: any) => client.put(`/settlement/fnf/${fnfId}/clearances/${clearanceId}`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
}),
updateLineItem: (itemId: string, data: any) => client.put(`/settlement/fnf/line-items/${itemId}`, data),
deleteLineItem: (itemId: string) => client.delete(`/settlement/fnf/line-items/${itemId}`),

View File

@ -88,13 +88,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const [comments, setComments] = useState('');
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [request, setRequest] = useState<any>(null);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isActionLoading, setIsActionLoading] = useState(false);
useEffect(() => {
fetchRequestDetails();
fetchAuditLogs();
}, [requestId]);
const fetchAuditLogs = async () => {
try {
const response: any = await API.getAuditLogs('constitutional_change', requestId);
if (response.data && response.data.success) {
setAuditLogs(response.data.data || []);
}
} catch (error) {
console.error('Error fetching audit logs:', error);
}
};
const fetchRequestDetails = async () => {
try {
setIsLoading(true);
@ -155,7 +168,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
// Centralized Permissions Utility (Fixes security gap where buttons showed for everyone)
const getConstitutionalPermissions = () => {
if (!request || !currentUser) {
return { canApprove: false, canReject: false, canHold: false };
return { canApprove: false, canReject: false, canHold: false, isFinalState: false };
}
const currentStage = request.currentStage;
@ -170,18 +183,19 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
// Role matching logic (Handles Role names from constants vs workflow mapping)
const isCurrentlyAssigned = currentUser.roleCode === 'SUPER_ADMIN' || (
(stageDef?.role === 'ASM' && userRole === 'ASM') ||
(stageDef?.role === 'ZM/RBM' && (userRole === 'ZM' || userRole === 'RBM')) ||
(stageDef?.role === 'ZM/RBM' && (userRole === 'DD-ZM' || userRole === 'RBM')) ||
(stageDef?.role === 'ZBH' && userRole === 'ZBH') ||
(stageDef?.role === 'DD Lead' && userRole === 'DD Lead') ||
(stageDef?.role === 'DD Head' && userRole === 'DD Head') ||
(stageDef?.role === 'NBH' && userRole === 'NBH') ||
(stageDef?.role === 'Legal Team' && (userRole === 'Legal' || userRole === 'Legal Admin'))
(stageDef?.role === 'Legal Team' && (userRole === 'Legal Admin'))
);
return {
canApprove: isCurrentlyAssigned && !isFinalState,
canReject: isCurrentlyAssigned && !isFinalState,
canHold: isCurrentlyAssigned && !isFinalState
canHold: isCurrentlyAssigned && !isFinalState,
isFinalState
};
};
@ -579,14 +593,14 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{/* History Tab */}
<TabsContent value="history" className="mt-0">
<div className="space-y-4">
{(request.timeline || request.history || []).map((entry: any, index: number) => (
{auditLogs.map((entry: any, index: number) => (
<div key={index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
(entry.status || entry.action)?.toLowerCase().includes('approve') || (entry.status || entry.action)?.toLowerCase().includes('complete') ? 'bg-green-100' :
(entry.status || entry.action)?.toLowerCase().includes('pending') || (entry.status || entry.action)?.toLowerCase().includes('progress') ? 'bg-amber-100' :
(entry.action)?.toLowerCase().includes('approve') || (entry.action)?.toLowerCase().includes('complete') ? 'bg-green-100' :
(entry.action)?.toLowerCase().includes('pending') || (entry.action)?.toLowerCase().includes('progress') || (entry.action)?.toLowerCase().includes('update') ? 'bg-amber-100' :
'bg-slate-100'
}`}>
{(entry.status || entry.action)?.toLowerCase().includes('approve') || (entry.status || entry.action)?.toLowerCase().includes('complete') ? (
{(entry.action)?.toLowerCase().includes('approve') || (entry.action)?.toLowerCase().includes('complete') ? (
<CheckCircle2 className="w-5 h-5 text-green-600" />
) : (
<Clock className="w-5 h-5 text-amber-600" />
@ -595,19 +609,19 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="text-slate-900">{entry.stage || entry.entityType || 'Update'}</h4>
<p className="text-slate-600 text-sm">{entry.actor || entry.user?.fullName || entry.user}</p>
<h4 className="text-slate-900">{entry.stage || entry.action}</h4>
<p className="text-slate-600 text-sm">{entry.userName || 'System'}</p>
</div>
<Badge className={getStatusColor(entry.status || entry.action)}>
{entry.action || entry.status}
<Badge className={getStatusColor(entry.action)}>
{entry.action}
</Badge>
</div>
<p className="text-slate-600 text-sm mt-2">{entry.comments || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-500 text-sm mt-1">{formatDateTime(entry.date || entry.createdAt || entry.timestamp)}</p>
<p className="text-slate-600 text-sm mt-2">{entry.description || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-500 text-sm mt-1">{formatDateTime(entry.timestamp)}</p>
</div>
</div>
))}
{(request.timeline || request.history || []).length === 0 && (
{auditLogs.length === 0 && (
<div className="text-center py-8 text-slate-500">
No history found
</div>

View File

@ -37,8 +37,15 @@ import {
} from 'lucide-react';
import { toast } from 'sonner';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime, formatDateOnly } from '../../lib/dateUtils';
import { formatDateTime } from '../../lib/dateUtils';
import { BankDetailsModal } from './BankDetailsModal';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select';
import {
Dialog,
DialogContent,
@ -46,13 +53,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
} from "../ui/dialog";
// Will be updated from API
let ALL_DEPARTMENTS = [
'Sales', 'Service', 'Spares / Parts', 'Finance', 'Accounts', 'Warranty',
'Marketing', 'HR', 'IT', 'Legal', 'Logistics', 'Quality', 'FDD', 'Apparel',
'DMS', 'Admin / DD-Admin'
const ALL_DEPARTMENTS = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
];
interface FinanceFnFDetailsPageProps {
@ -91,6 +98,16 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const [isBankModalOpen, setIsBankModalOpen] = useState(false);
const [editingBank, setEditingBank] = useState<any>(null);
const [checklist, setChecklist] = useState<string[]>([]);
const [showClearanceDialog, setShowClearanceDialog] = useState(false);
const [selectedDept, setSelectedDept] = useState<any>(null);
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
const [clearanceForm, setClearanceForm] = useState({
status: 'Pending',
remarks: '',
amount: 0,
type: 'Recovery'
});
const [clearanceFile, setClearanceFile] = useState<File | null>(null);
useEffect(() => {
fetchDepartments();
@ -98,17 +115,58 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
}, [fnfId]);
const fetchDepartments = async () => {
// We use the local ALL_DEPARTMENTS constant as the source of truth
// But we check if the server has a different list
try {
const response = await API.getSettlementDepartments();
const data = response.data as any;
if (data && data.success) {
ALL_DEPARTMENTS = data.departments;
if (data && data.success && data.departments?.length > 0) {
// If needed, we could set a state here, but for now we stick to the standardized 16
}
} catch (error) {
console.error("Fetch departments error:", error);
}
};
const normalizeDepartment = (name: string) => {
if (!name) return name;
let inputName = name.trim();
// Exact match first
const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase());
if (exactMatch) return exactMatch;
// Smart mapping for shorthands
const mapping: Record<string, string> = {
'sales': 'Sales Department',
'service': 'Service Department',
'spares': 'Parts Department',
'parts': 'Parts Department',
'spares / parts': 'Parts Department',
'finance': 'Finance Department',
'accounts': 'Finance Department',
'warranty': 'Warranty Department',
'marketing': 'Marketing Department',
'hr': 'HR Department',
'it': 'IT Department',
'legal': 'Legal Department',
'logistics': 'Logistics Department',
'quality': 'Quality Department',
'fdd': 'Finance Department',
'apparel': 'Accessories Department',
'accessories': 'Accessories Department',
'dms': 'IT Department',
'rto': 'Admin Department',
'admin': 'Admin Department',
'admin / dd-admin': 'Admin Department'
};
const mapped = mapping[inputName.toLowerCase().replace(' department', '')];
if (mapped) return mapped;
return name;
};
const fetchFnFDetails = async () => {
try {
setLoading(true);
@ -134,10 +192,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
gearCode: s.dealer?.dealerCode?.gearCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gearCode || 'N/A',
gmaCode: s.dealer?.dealerCode?.gmaCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gmaCode || 'N/A',
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
const totalAmount = relatedItems.reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount)), 0);
const hasPayable = relatedItems.some((li: any) => li.amount < 0);
const c = (s.clearances || []).find((clearance: any) => normalizeDepartment(clearance.department) === deptName);
const relatedItems = (s.lineItems || []).filter((li: any) => normalizeDepartment(li.department) === deptName);
// Calculate departmental net
let deptPayables = 0;
let deptRecoveries = 0;
relatedItems.forEach((li: any) => {
const amt = Math.abs(parseFloat(li.amount) || 0);
if (li.itemType === 'Payable') deptPayables += amt;
else deptRecoveries += amt; // Receivables & Deductions
});
const netAmount = deptPayables - deptRecoveries;
return {
id: c?.id || `dept-${deptName}`,
@ -145,8 +212,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
status: c?.status || 'Pending',
remarks: c?.remarks || '-',
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : '-',
amount: totalAmount,
amountType: hasPayable ? 'Payable Amount' : totalAmount > 0 ? 'Recovery Amount' : null,
amount: Math.abs(netAmount),
amountType: netAmount > 0 ? 'Payable Amount' : netAmount < 0 ? 'Recovery Amount' : null,
supportingDocument: c?.supportingDocument || null
};
}),
@ -182,8 +249,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
(s.lineItems || []).forEach((li: any) => {
const item: FinancialLineItem = {
id: li.id,
department: li.department,
description: li.remarks || '',
department: normalizeDepartment(li.department),
description: li.description || li.remarks || '',
amount: Math.abs(li.amount)
};
@ -286,9 +353,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
// Calculate dynamic settlement
const calculateDynamicSettlement = () => {
const payables = payableItems.reduce((sum, item) => sum + item.amount, 0);
const receivables = receivableItems.reduce((sum, item) => sum + item.amount, 0);
const deductions = deductionItems.reduce((sum, item) => sum + item.amount, 0);
const payables = payableItems.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
const receivables = receivableItems.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
const deductions = deductionItems.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
const netSettlement = payables - receivables - deductions;
return {
@ -303,9 +370,6 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const settlement = calculateDynamicSettlement();
// Get primary bank for display
const primaryBank = bankDetails.find(b => b.isPrimary) || bankDetails[0];
const [settlementDetails, setSettlementDetails] = useState({
verificationTransactionId: '',
settlementAmount: settlement.settlementAmount.toString(),
@ -325,10 +389,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
return;
}
try {
const amount = -Math.abs(parseFloat(newPayable.amount)); // Payable is negative
const response = await API.addLineItem(fnfId, {
department: newPayable.department,
remarks: newPayable.description,
description: newPayable.description,
amount: Math.abs(parseFloat(newPayable.amount)),
itemType: 'Payable'
});
@ -337,11 +400,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
setPayableItems([...payableItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.remarks,
description: data.lineItem.description,
amount: Math.abs(data.lineItem.amount)
}]);
setNewPayable({ department: '', description: '', amount: '' });
toast.success('Payable item added');
fetchFnFDetails();
}
} catch (error) {
toast.error('Failed to add payable item');
@ -361,9 +425,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
if (item) {
await API.updateLineItem(id, {
department: item.department,
remarks: item.description,
description: item.description,
amount: -Math.abs(item.amount)
});
fetchFnFDetails();
}
} catch (error) {
toast.error('Failed to update item');
@ -378,12 +443,38 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
if (data.success) {
setPayableItems(payableItems.filter(item => item.id !== id));
toast.info('Payable item removed');
fetchFnFDetails();
}
} catch (error) {
toast.error('Failed to delete item');
}
};
const handleUpdateClearance = async () => {
if (!selectedDept || !fnfId) return;
try {
setIsUpdatingClearance(true);
const formData = new FormData();
formData.append('status', clearanceForm.status);
formData.append('remarks', clearanceForm.remarks);
formData.append('amount', String(clearanceForm.amount));
formData.append('type', clearanceForm.type);
if (clearanceFile) formData.append('file', clearanceFile);
await API.updateFnFClearance(fnfId, selectedDept.id, formData);
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
setShowClearanceDialog(false);
fetchFnFDetails();
} catch (error) {
console.error("Update clearance error:", error);
toast.error("Failed to update department clearance");
} finally {
setIsUpdatingClearance(false);
}
};
// Handlers for Receivables
const handleAddReceivable = async () => {
if (!newReceivable.department || !newReceivable.description || !newReceivable.amount) {
@ -393,7 +484,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
try {
const response = await API.addLineItem(fnfId, {
department: newReceivable.department,
remarks: newReceivable.description,
description: newReceivable.description,
amount: Math.abs(parseFloat(newReceivable.amount)),
itemType: 'Receivable'
});
@ -402,11 +493,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
setReceivableItems([...receivableItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.remarks,
description: data.lineItem.description,
amount: data.lineItem.amount
}]);
setNewReceivable({ department: '', description: '', amount: '' });
toast.success('Receivable item added');
fetchFnFDetails();
}
} catch (error) {
toast.error('Failed to add receivable item');
@ -424,9 +516,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
if (item) {
await API.updateLineItem(id, {
department: item.department,
remarks: item.description,
description: item.description,
amount: Math.abs(item.amount)
});
fetchFnFDetails();
}
} catch (error) {
toast.error('Failed to update item');
@ -439,6 +532,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
await API.deleteLineItem(id);
setReceivableItems(receivableItems.filter(item => item.id !== id));
toast.info('Receivable item removed');
fetchFnFDetails();
} catch (error) {
toast.error('Failed to delete item');
}
@ -453,7 +547,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
try {
const response = await API.addLineItem(fnfId, {
department: newDeduction.department,
remarks: newDeduction.description,
description: newDeduction.description,
amount: Math.abs(parseFloat(newDeduction.amount)),
itemType: 'Deduction'
});
@ -462,11 +556,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
setDeductionItems([...deductionItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.remarks,
description: data.lineItem.description,
amount: data.lineItem.amount
}]);
setNewDeduction({ department: '', description: '', amount: '' });
toast.success('Deduction item added');
fetchFnFDetails();
}
} catch (error) {
toast.error('Failed to add deduction item');
@ -484,9 +579,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
if (item) {
await API.updateLineItem(id, {
department: item.department,
remarks: item.description,
description: item.description,
amount: Math.abs(item.amount)
});
fetchFnFDetails();
}
} catch (error) {
toast.error('Failed to update item');
@ -499,6 +595,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
await API.deleteLineItem(id);
setDeductionItems(deductionItems.filter(item => item.id !== id));
toast.info('Deduction item removed');
fetchFnFDetails();
} catch (error) {
toast.error('Failed to delete item');
}
@ -862,13 +959,21 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<TableRow key={item.id}>
<TableCell>
{editingPayableId === item.id ? (
<Input
<Select
value={item.department}
onChange={(e) => handleUpdatePayable(item.id, 'department', e.target.value)}
className="h-8"
/>
onValueChange={(val) => handleUpdatePayable(item.id, 'department', val)}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
{ALL_DEPARTMENTS.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span className="text-slate-900">{item.department}</span>
<span className="text-slate-900">{normalizeDepartment(item.department)}</span>
)}
</TableCell>
<TableCell>
@ -937,12 +1042,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<div className="border-t border-green-300 pt-4 space-y-3">
<p className="text-sm text-slate-700">Add New Payable Item:</p>
<div className="grid grid-cols-12 gap-2">
<Input
placeholder="Department"
<Select
value={newPayable.department}
onChange={(e) => setNewPayable({ ...newPayable, department: e.target.value })}
className="col-span-3"
/>
onValueChange={(val) => setNewPayable({ ...newPayable, department: val })}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
{ALL_DEPARTMENTS.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
<Input
placeholder="Description"
value={newPayable.description}
@ -1003,13 +1115,21 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<TableRow key={item.id}>
<TableCell>
{editingReceivableId === item.id ? (
<Input
<Select
value={item.department}
onChange={(e) => handleUpdateReceivable(item.id, 'department', e.target.value)}
className="h-8"
/>
onValueChange={(val) => handleUpdateReceivable(item.id, 'department', val)}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
{ALL_DEPARTMENTS.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span className="text-slate-900">{item.department}</span>
<span className="text-slate-900">{normalizeDepartment(item.department)}</span>
)}
</TableCell>
<TableCell>
@ -1078,12 +1198,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<div className="border-t border-red-300 pt-4 space-y-3">
<p className="text-sm text-slate-700">Add New Receivable Item:</p>
<div className="grid grid-cols-12 gap-2">
<Input
placeholder="Department"
<Select
value={newReceivable.department}
onChange={(e) => setNewReceivable({ ...newReceivable, department: e.target.value })}
className="col-span-3"
/>
onValueChange={(val) => setNewReceivable({ ...newReceivable, department: val })}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
{ALL_DEPARTMENTS.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
<Input
placeholder="Description"
value={newReceivable.description}
@ -1144,13 +1271,21 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<TableRow key={item.id}>
<TableCell>
{editingDeductionId === item.id ? (
<Input
<Select
value={item.department}
onChange={(e) => handleUpdateDeduction(item.id, 'department', e.target.value)}
className="h-8"
/>
onValueChange={(val) => handleUpdateDeduction(item.id, 'department', val)}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
{ALL_DEPARTMENTS.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span className="text-slate-900">{item.department}</span>
<span className="text-slate-900">{normalizeDepartment(item.department)}</span>
)}
</TableCell>
<TableCell>
@ -1219,12 +1354,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<div className="border-t border-amber-300 pt-4 space-y-3">
<p className="text-sm text-slate-700">Add New Deduction Item:</p>
<div className="grid grid-cols-12 gap-2">
<Input
placeholder="Department"
<Select
value={newDeduction.department}
onChange={(e) => setNewDeduction({ ...newDeduction, department: e.target.value })}
className="col-span-3"
/>
onValueChange={(val) => setNewDeduction({ ...newDeduction, department: val })}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
{ALL_DEPARTMENTS.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
<Input
placeholder="Description"
value={newDeduction.description}
@ -1385,6 +1527,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<TableHead>Amount</TableHead>
<TableHead>Submitted Date</TableHead>
<TableHead>Remarks</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -1442,6 +1585,25 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
)}
</div>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
className="text-amber-600 hover:text-blue-700 font-medium"
onClick={() => {
setSelectedDept(dept);
setClearanceForm({
status: dept.status,
remarks: dept.remarks === '-' ? '' : dept.remarks,
amount: dept.amount || 0,
type: dept.amountType || (dept.amount > 0 ? 'Recovery' : 'Payable')
});
setShowClearanceDialog(true);
}}
>
Update Status
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
@ -1877,6 +2039,94 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div>
</div>
{/* Clearance Update Dialog */}
<Dialog open={showClearanceDialog} onOpenChange={setShowClearanceDialog}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Update {selectedDept?.departmentName} Status</DialogTitle>
<DialogDescription>
Mark the department as cleared or report pending dues with amount.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="status" className="text-right">Status</Label>
<select
id="status"
className="col-span-3 flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
value={clearanceForm.status}
onChange={(e) => setClearanceForm({ ...clearanceForm, status: e.target.value })}
>
<option value="Pending">Pending</option>
<option value="NOC Submitted">NOC Submitted (Cleared)</option>
<option value="Dues Pending">Dues Pending (Hold)</option>
</select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="type" className="text-right">Type</Label>
<select
id="type"
className="col-span-3 flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
value={clearanceForm.type}
onChange={(e) => setClearanceForm({ ...clearanceForm, type: e.target.value })}
>
<option value="Recovery">Recovery (from Dealer)</option>
<option value="Payable">Payable (to Dealer)</option>
<option value="Deduction">Deduction (Penalties)</option>
</select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="amount" className="text-right">Amount</Label>
<div className="col-span-3 relative">
<span className="absolute left-3 top-2.5 text-slate-500 font-medium"></span>
<input
id="amount"
type="number"
className="flex h-10 w-full rounded-md border border-slate-200 bg-white pl-7 pr-3 py-2 text-sm"
value={clearanceForm.amount}
onChange={(e) => setClearanceForm({ ...clearanceForm, amount: Number(e.target.value) })}
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="remarks" className="text-right">Remarks</Label>
<textarea
id="remarks"
className="col-span-3 flex min-h-[80px] w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
placeholder="Enter description or dues details..."
value={clearanceForm.remarks}
onChange={(e) => setClearanceForm({ ...clearanceForm, remarks: e.target.value })}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="file" className="text-right">Proof</Label>
<input
id="file"
type="file"
className="col-span-3 text-sm"
onChange={(e) => setClearanceFile(e.target.files?.[0] || null)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowClearanceDialog(false)}>Cancel</Button>
<Button
className="bg-amber-600 hover:bg-blue-700"
onClick={handleUpdateClearance}
disabled={isUpdatingClearance}
>
{isUpdatingClearance ? "Updating..." : "Save Changes"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<BankDetailsModal
isOpen={isBankModalOpen}
onClose={() => {

View File

@ -79,7 +79,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
id: s.id,
caseId: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || 'N/A',
dealerName: s.outlet?.dealer?.fullName || s.dealer?.legalName || s.dealer?.businessName || s.dealer?.fullName || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination',
submittedDate: formatDateTime(s.createdAt),

View File

@ -41,7 +41,6 @@ import {
} from "../ui/table";
import { Progress } from "../ui/progress";
import { useState, useEffect } from "react";
import { User } from "../../lib/mock-data";
import { toast } from "sonner";
import { settlementService } from "../../services/settlement.service";
import { API } from "../../api/API";
@ -53,9 +52,16 @@ import { BankDetailsModal } from "./BankDetailsModal";
interface FnFDetailsProps {
fnfId: string;
onBack: () => void;
currentUser: User | null;
currentUser: any;
}
const ALL_DEPARTMENTS = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
];
export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const navigate = useNavigate();
const [fnfCase, setFnfCase] = useState<any>(null);
@ -69,6 +75,16 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const [isSubmittingBank, setIsSubmittingBank] = useState(false);
const [departments, setDepartments] = useState<string[]>([]);
const [showClearanceDialog, setShowClearanceDialog] = useState(false);
const [selectedDept, setSelectedDept] = useState<any>(null);
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
const [clearanceForm, setClearanceForm] = useState({
status: 'Pending',
remarks: '',
amount: 0,
type: 'Recovery'
});
const [clearanceFile, setClearanceFile] = useState<File | null>(null);
useEffect(() => {
fetchDepartments();
@ -88,6 +104,58 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
}
};
const normalizeDepartment = (name: string) => {
if (!name) return name;
let inputName = name.trim();
// Exact match first
const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase());
if (exactMatch) return exactMatch;
// Smart mapping for shorthands
const mapping: Record<string, string> = {
'sales': 'Sales Department',
'service': 'Service Department',
'spares': 'Parts Department',
'parts': 'Parts Department',
'spares / parts': 'Parts Department',
'finance': 'Finance Department',
'accounts': 'Finance Department',
'warranty': 'Warranty Department',
'marketing': 'Marketing Department',
'hr': 'HR Department',
'it': 'IT Department',
'legal': 'Legal Department',
'logistics': 'Logistics Department',
'quality': 'Quality Department',
'fdd': 'Finance Department',
'apparel': 'Accessories Department',
'accessories': 'Accessories Department',
'dms': 'IT Department',
'rto': 'Admin Department',
'admin': 'Admin Department',
'admin / dd-admin': 'Admin Department'
};
const mapped = mapping[inputName.toLowerCase().replace(' department', '')];
if (mapped) return mapped;
return name;
};
const getFriendlyActionName = (action: string) => {
if (!action) return 'Action';
const mapping: Record<string, string> = {
'ADD_LINE_ITEM': 'Line Item Added',
'UPDATE_LINE_ITEM': 'Line Item Updated',
'REMOVE_LINE_ITEM': 'Line Item Removed',
'UPDATE_CLEARANCE': 'Clearance Status Updated',
'SETTLE_CASE': 'Settlement Finalized',
'FNF_UPDATED': 'F&F Case Updated'
};
return mapping[action] || action.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
};
const fetchFnFDetails = async () => {
try {
setLoading(true);
@ -96,7 +164,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
if (data.success) {
const s = data.fnf;
// Map backend data to UI format
const mappedCase = {
const mappedCase: any = {
id: s.id,
caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
status: s.status,
@ -127,37 +195,54 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
: s.status === "Completed"
? "Completed"
: "Pending",
totalPayableAmount: parseFloat(s.totalPayables) || 0,
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
totalDeductions: parseFloat(s.totalDeductions) || 0,
netAmount: parseFloat(s.netAmount) || 0,
departmentResponses: (departments.length > 0 ? departments : [
"Sales", "Service", "Spares / Parts", "Finance", "Accounts", "Warranty",
"Marketing", "HR", "IT", "Legal", "Logistics", "Quality", "FDD", "Apparel",
"DMS", "Admin / DD-Admin"
]).map((deptName: string) => {
totalPayableAmount: (s.lineItems || []).filter((li: any) => li.itemType === 'Payable').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
totalRecoveryAmount: (s.lineItems || []).filter((li: any) => li.itemType === 'Receivable' || li.itemType === 'Recovery').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
totalDeductions: (s.lineItems || []).filter((li: any) => li.itemType === 'Deduction').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0),
netAmount: 0,
departmentResponses: [] as any[]
};
mappedCase.netAmount = mappedCase.totalPayableAmount - mappedCase.totalRecoveryAmount - mappedCase.totalDeductions;
mappedCase.departmentResponses = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
].map((deptName: string) => {
const c = (s.clearances || []).find(
(clearance: any) => clearance.department === deptName,
(clearance: any) => normalizeDepartment(clearance.department) === deptName,
);
const lineItem = (s.lineItems || []).find(
(li: any) => li.department === deptName,
const relatedItems = (s.lineItems || []).filter(
(li: any) => normalizeDepartment(li.department) === deptName,
);
// Calculate departmental net
let deptPayables = 0;
let deptRecoveries = 0;
relatedItems.forEach((li: any) => {
const amt = Math.abs(parseFloat(li.amount) || 0);
if (li.itemType === 'Payable') deptPayables += amt;
else deptRecoveries += amt; // Receivables & Deductions
});
const netAmount = deptPayables - deptRecoveries;
return {
id: c?.id || `dept-${deptName}`,
departmentName: deptName,
status: c?.status || "Pending",
amountType: lineItem
? parseFloat(lineItem.amount) > 0
? "Recovery"
: "Payable"
: null,
amount: lineItem ? Math.abs(parseFloat(lineItem.amount)) : 0,
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : null,
amountType: netAmount > 0 ? "Payable" : netAmount < 0 ? "Recovery" : null,
amount: Math.abs(netAmount),
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : "-",
remarks: c?.remarks || "-",
supportingDocument: c?.supportingDocument || null,
};
}),
});
// Add documents and other fields
const finalMapped = {
...mappedCase,
documents: [
...(s.resignation?.uploadedDocuments || []).map((doc: any) => ({
id: `res-${doc.id}`,
@ -187,7 +272,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
})),
],
};
setFnfCase(mappedCase);
setFnfCase(finalMapped);
// Sync bank details from the pre-fetched data
const preFetchedBankDetails = s.bankDetails || s.dealer?.bankDetails || s.outlet?.dealer?.dealerProfile?.bankDetails;
@ -299,7 +384,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
// Calculate age in days from F&F submission date
const calculateAge = (startDate: string) => {
const start = new Date(startDate);
const today = new Date("2025-10-15"); // Using current date from context
const today = new Date(); // Use real current date
const diffTime = Math.abs(today.getTime() - start.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
@ -313,6 +398,32 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
currentUser.role,
);
const handleUpdateClearance = async () => {
if (!selectedDept || !fnfId) return;
try {
setIsUpdatingClearance(true);
const formData = new FormData();
const derivedStatus = Number(clearanceForm.amount) > 0 ? 'Dues Pending' : 'NOC Submitted';
formData.append('status', derivedStatus);
formData.append('remarks', clearanceForm.remarks);
formData.append('amount', String(clearanceForm.amount));
formData.append('type', clearanceForm.type);
if (clearanceFile) formData.append('file', clearanceFile);
await API.updateFnFClearance(fnfId, selectedDept.id, formData);
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
setShowClearanceDialog(false);
fetchFnFDetails();
} catch (error) {
console.error("Update clearance error:", error);
toast.error("Failed to update department clearance");
} finally {
setIsUpdatingClearance(false);
}
};
const handleSendToStakeholders = () => {
toast.success("Notifications sent to all 16 departments");
setSendStakeholdersDialog(false);
@ -1093,7 +1204,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<p>{fnfCase.submittedOn}</p>
</div>
<div>
<Label className="text-slate-600">Age (Days)</Label>
<Label className="text-slate-600">Days Elapsed since Submission</Label>
<div className="flex items-center gap-2">
<p>{fnfAge} days</p>
<Badge
@ -1183,6 +1294,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TableHead>Amount</TableHead>
<TableHead>Submitted Date</TableHead>
<TableHead>Remarks</TableHead>
{(currentUser?.role === 'Super Admin' || currentUser?.role === 'Finance Admin' || currentUser?.role === 'DD Admin' || departments.some(d => currentUser?.role?.includes(d.replace(' Department', '')))) && (
<TableHead>Actions</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
@ -1227,6 +1341,27 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TableCell className="max-w-xs truncate">
{dept.remarks || "-"}
</TableCell>
{(currentUser?.role === 'Super Admin' || currentUser?.role === 'Finance Admin' || currentUser?.role === 'DD Admin' || (currentUser?.role && currentUser.role.includes(dept.departmentName.replace(' Department', '')))) && (
<TableCell>
<Button
variant="ghost"
size="sm"
className="text-amber-600 hover:text-blue-700"
onClick={() => {
setSelectedDept(dept);
setClearanceForm({
status: dept.status,
remarks: dept.remarks === '-' ? '' : dept.remarks,
amount: dept.amount || 0,
type: dept.amountType || 'Recovery'
});
setShowClearanceDialog(true);
}}
>
Update
</Button>
</TableCell>
)}
</TableRow>
))}
</TableBody>
@ -1270,13 +1405,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</p>
</div>
<div className="p-6 bg-amber-50 rounded-lg border border-amber-200">
<p className="text-sm text-blue-700 mb-2">
<p className="text-sm text-amber-700 mb-2">
Total Deductions
</p>
<p className="text-3xl text-blue-600 font-bold">
<p className="text-3xl text-amber-600 font-bold">
{fnfCase.totalDeductions?.toLocaleString() || "0"}
</p>
<p className="text-xs text-blue-600 mt-1">
<p className="text-xs text-amber-600 mt-1">
Warranty holdbacks / Policy penalties
</p>
</div>
@ -1540,7 +1675,16 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex items-center justify-between mb-1">
<p className="font-semibold text-slate-900 flex items-center gap-2">
{log.action === 'FNF_CREATED' && <Badge className="bg-amber-600 h-2 w-2 p-0 rounded-full" />}
{log.description || log.action?.split('_').join(' ')}
{(log.description && !log.newData?.action) ? log.description : (
<>
{getFriendlyActionName(log.newData?.action || log.action)}
{log.newData?.department && (
<span className="text-amber-600 ml-1 font-bold">
- {log.newData.department}
</span>
)}
</>
)}
</p>
<span className="text-sm text-slate-600 font-mono">
{formatDateTime(log.createdAt || log.timestamp)}
@ -1556,11 +1700,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</div>
)}
{log.newData && Object.keys(log.newData).filter(k => k !== 'remarks' && k !== 'status').length > 0 && (
{log.newData && Object.keys(log.newData).filter(k => k !== 'remarks' && k !== 'status' && k !== 'action' && k !== 'department').length > 0 && (
<div className="mt-2 space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold px-1">Changes:</p>
<div className="flex flex-wrap gap-2">
{Object.entries(log.newData).filter(([k]) => k !== 'remarks').map(([key, val]: any) => (
{Object.entries(log.newData).filter(([k]) => k !== 'remarks' && k !== 'action' && k !== 'department').map(([key, val]: any) => (
<div key={key} className="text-[11px] bg-slate-100 border rounded px-2 py-0.5 flex items-center gap-1">
<span className="text-slate-500">{key}:</span>
<span className="text-slate-900 font-medium">{String(val)}</span>
@ -1629,6 +1773,82 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</DialogContent>
</Dialog>
{/* Clearance Update Dialog */}
<Dialog open={showClearanceDialog} onOpenChange={setShowClearanceDialog}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Update {selectedDept?.departmentName} Clearance</DialogTitle>
<DialogDescription>
Mark the department as cleared or report pending dues with amount.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="type" className="text-right">Type</Label>
<select
id="type"
className="col-span-3 flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
value={clearanceForm.type}
onChange={(e) => setClearanceForm({ ...clearanceForm, type: e.target.value })}
>
<option value="Recovery">Recovery (from Dealer)</option>
<option value="Payable">Payable (to Dealer)</option>
<option value="Deduction">Deduction (Penalties)</option>
</select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="amount" className="text-right">Amount</Label>
<div className="col-span-3 relative">
<span className="absolute left-3 top-2.5 text-slate-500 font-medium"></span>
<input
id="amount"
type="number"
className="flex h-10 w-full rounded-md border border-slate-200 bg-white pl-7 pr-3 py-2 text-sm"
value={clearanceForm.amount}
onChange={(e) => setClearanceForm({ ...clearanceForm, amount: Number(e.target.value) })}
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="remarks" className="text-right">Remarks</Label>
<textarea
id="remarks"
className="col-span-3 flex min-h-[80px] w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
placeholder="Enter description or dues details..."
value={clearanceForm.remarks}
onChange={(e) => setClearanceForm({ ...clearanceForm, remarks: e.target.value })}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="file" className="text-right">Proof</Label>
<input
id="file"
type="file"
className="col-span-3 text-sm"
onChange={(e) => setClearanceFile(e.target.files?.[0] || null)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowClearanceDialog(false)}>Cancel</Button>
<Button
className="bg-amber-600 hover:bg-blue-700"
onClick={handleUpdateClearance}
disabled={isUpdatingClearance}
>
{isUpdatingClearance ? "Updating..." : "Save Changes"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Bank Details Modal */}
<BankDetailsModal
isOpen={isBankModalOpen}

View File

@ -78,14 +78,14 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
// Helper to map backend data to UI-friendly format
const getMappedData = (s: any) => ({
id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(),
caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8).toUpperCase(),
status: s.status,
requestType: s.resignationId ? 'Resignation' : 'Termination',
dealerName: s.outlet?.dealer?.name || 'N/A',
dealerCode: s.outlet?.code || 'N/A',
dealerName: s.outlet?.dealer?.fullName || s.dealer?.legalName || s.dealer?.businessName || s.dealer?.fullName || 'N/A',
dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
dealershipName: s.outlet?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.id || 'N/A',
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.requestId || s.terminationRequest?.id || 'N/A',
submittedOn: formatDateTime(s.createdAt),
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,

View File

@ -63,6 +63,7 @@ const getStatusColor = (status: string) => {
export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) {
const navigate = useNavigate();
const [request, setRequest] = useState<any>(null);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
@ -81,8 +82,20 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
useEffect(() => {
fetchRequestDetails();
fetchAuditLogs();
}, [requestId]);
const fetchAuditLogs = async () => {
try {
const response: any = await API.getAuditLogs('relocation', requestId);
if (response.data && response.data.success) {
setAuditLogs(response.data.data || []);
}
} catch (error) {
console.error('Error fetching audit logs:', error);
}
};
const fetchEorChecklist = async () => {
try {
setIsEorLoading(true);
@ -816,14 +829,15 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{/* History Tab */}
<TabsContent value="history" className="mt-0">
<div className="space-y-4">
{request.timeline && request.timeline.length > 0 ? request.timeline.map((entry: any, index: number) => (
{auditLogs.length > 0 ? (
auditLogs.map((entry: any, index: number) => (
<div key={index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${entry.action.toLowerCase().includes('approve') || entry.action.toLowerCase().includes('submit') ? 'bg-green-100' :
'bg-amber-100'
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
(entry.action)?.toLowerCase().includes('approve') || (entry.action)?.toLowerCase().includes('complete') ? 'bg-green-100' :
(entry.action)?.toLowerCase().includes('pending') || (entry.action)?.toLowerCase().includes('progress') || (entry.action)?.toLowerCase().includes('update') ? 'bg-amber-100' :
'bg-slate-100'
}`}>
{entry.action.toLowerCase().includes('approve') ? (
<CheckCircle2 className="w-5 h-5 text-green-600" />
) : entry.action.toLowerCase().includes('submit') ? (
{(entry.action)?.toLowerCase().includes('approve') || (entry.action)?.toLowerCase().includes('complete') ? (
<CheckCircle2 className="w-5 h-5 text-green-600" />
) : (
<Clock className="w-5 h-5 text-amber-600" />
@ -832,20 +846,21 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="text-slate-900">{entry.stage || 'Update'}</h4>
<p className="text-slate-600 text-sm">{entry.user}</p>
<h4 className="text-slate-900">{entry.stage || entry.action}</h4>
<p className="text-slate-600 text-sm">{entry.userName || 'System'}</p>
</div>
<Badge className={getStatusColor(entry.action)}>
{entry.action}
</Badge>
</div>
<p className="text-slate-600 text-sm mt-2">{entry.remarks || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-600 text-sm mt-2">{entry.description || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-500 text-sm mt-1">{formatDateTime(entry.timestamp)}</p>
</div>
</div>
)) : (
))
) : (
<div className="text-center py-8 text-slate-500">
No history records available
No history found
</div>
)}
</div>

View File

@ -19,11 +19,52 @@ import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
const ALL_DEPARTMENTS = [
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
'Legal', 'Quality', 'Logistics', 'Customer Relations'
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
];
const normalizeDepartment = (name: string) => {
if (!name) return name;
let inputName = name.trim();
// Exact match first
const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase());
if (exactMatch) return exactMatch;
// Smart mapping for shorthands
const mapping: Record<string, string> = {
'sales': 'Sales Department',
'service': 'Service Department',
'spares': 'Parts Department',
'parts': 'Parts Department',
'spares / parts': 'Parts Department',
'finance': 'Finance Department',
'accounts': 'Finance Department',
'warranty': 'Warranty Department',
'marketing': 'Marketing Department',
'hr': 'HR Department',
'it': 'IT Department',
'legal': 'Legal Department',
'logistics': 'Logistics Department',
'quality': 'Quality Department',
'fdd': 'Finance Department',
'apparel': 'Accessories Department',
'accessories': 'Accessories Department',
'dms': 'IT Department',
'rto': 'Admin Department',
'admin': 'Admin Department',
'admin / dd-admin': 'Admin Department'
};
const mapped = mapping[inputName.toLowerCase().replace(' department', '')];
if (mapped) return mapped;
return name;
};
interface ResignationDetailsProps {
resignationId: string;
onBack: () => void;
@ -47,24 +88,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [assignToUser, setAssignToUser] = useState('');
const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
const [showClearanceDialog, setShowClearanceDialog] = useState(false);
const [selectedDept, setSelectedDept] = useState('');
const [clearanceStatus, setClearanceStatus] = useState<any>('Pending');
const [clearanceRemarks, setClearanceRemarks] = useState('');
const [clearanceAmount, setClearanceAmount] = useState<number>(0);
const [clearanceType, setClearanceType] = useState<'Payable' | 'Recovery'>('Recovery');
const [clearanceFile, setClearanceFile] = useState<File | null>(null);
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
const [isLoading, setIsLoading] = useState(false);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [previewDocument, setPreviewDocument] = useState<any>(null);
const fetchResignation = async () => {
try {
setIsLoading(true);
const data = await resignationService.getResignationById(resignationId);
setResignationData(data);
fetchAuditLogs();
} catch (error) {
console.error('Error fetching resignation:', error);
} finally {
@ -72,19 +106,21 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}
};
const fetchAuditLogs = async () => {
try {
const response: any = await API.getAuditLogs('resignation', resignationId);
if (response.data && response.data.success) {
setAuditLogs(response.data.data || []);
}
} catch (error) {
console.error('Error fetching audit logs:', error);
}
};
useEffect(() => {
fetchResignation();
}, [resignationId]);
// Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
// Check if user is assigned to the current stage
const isCurrentlyAssigned = currentUser && (
currentUser.role === 'Super Admin' ||
currentUser.role === STAGE_TO_ROLE_MAP[resignationData?.currentStage]
);
// Progress stages logic based on live data
const progressStages = [
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
@ -140,6 +176,11 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const currentIndex = stagesOrdered.indexOf(currentStage);
const stageIndex = stagesOrdered.indexOf(stageKey);
// Final state override: if the whole request is completed, mark current stage as completed too
if (resignationData.status === 'Completed' || resignationData.status === 'Settled') {
if (stageIndex <= currentIndex) return 'completed';
}
if (currentIndex === -1) return 'pending';
if (stageIndex < currentIndex) return 'completed';
if (stageIndex === currentIndex) return 'active';
@ -190,38 +231,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}
};
const handleClearanceUpdate = async () => {
if (!selectedDept) return;
try {
setIsUpdatingClearance(true);
const formData = new FormData();
formData.append('department', selectedDept);
formData.append('status', clearanceStatus);
formData.append('remarks', clearanceRemarks);
formData.append('amount', String(clearanceAmount));
formData.append('type', clearanceType);
if (clearanceFile) {
formData.append('file', clearanceFile);
}
const response: any = await resignationService.updateClearance(resignationId, formData);
if (response?.success) {
toast.success(`Successfully updated clearance for ${selectedDept}`);
setShowClearanceDialog(false);
setClearanceFile(null);
fetchResignation();
} else {
toast.error(response?.message || 'Failed to update clearance status');
}
} catch (error) {
toast.error('Failed to update clearance status');
} finally {
setIsUpdatingClearance(false);
}
};
if (isLoading && !resignationData) {
return (
@ -243,8 +252,14 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<h1 className="text-2xl">{resignationData?.resignationId || resignationId}</h1>
<p className="text-slate-600">{resignationData?.outlet?.name}</p>
</div>
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
{resignationData?.status}
<Badge className={
resignationData?.status === 'Completed' || resignationData?.status === 'Settled'
? 'bg-green-100 text-green-700 border-green-300'
: resignationData?.status === 'Rejected' || resignationData?.status === 'Withdrawn'
? 'bg-red-100 text-red-700 border-red-300'
: 'bg-yellow-100 text-yellow-700 border-yellow-300'
}>
{resignationData?.status === 'Settled' ? 'Completed' : (resignationData?.status || 'Pending')}
</Badge>
</div>
</div>
@ -563,22 +578,36 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{ALL_DEPARTMENTS.map((dept) => {
const settlement = resignationData?.settlement;
const fffClearance = (settlement?.clearances || []).find((c: any) => c.department === dept);
const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => li.department === dept);
const lineItemAmount = relatedLineItems.reduce((sum: number, li: any) => sum + parseFloat(li.amount || 0), 0);
const fffClearance = (settlement?.clearances || []).find((c: any) => normalizeDepartment(c.department) === dept);
const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => normalizeDepartment(li.department) === dept);
// Use standardized JSON field but override with live F&F data if available
// Calculate cumulative net for this department
let deptPayables = 0;
let deptRecoveries = 0;
relatedLineItems.forEach((li: any) => {
const amt = Math.abs(parseFloat(li.amount) || 0);
if (li.itemType === 'Payable') deptPayables += amt;
else deptRecoveries += amt; // Receivables & Deductions
});
const netAmount = deptPayables - deptRecoveries;
// Use standardized JSON field from initial clearance phase
const jsonClearance = (resignationData?.departmentalClearances || {})[dept] || { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' };
const displayStatus = fffClearance ? (fffClearance.status === 'NOC Submitted' ? 'Cleared' : fffClearance.status === 'Pending' ? 'Pending' : 'Dues') : jsonClearance.status;
// Logic: If FFF has items or a specific clearance object, it overrides the initial JSON clearance
const displayStatus = fffClearance
? (fffClearance.status === 'NOC Submitted' || fffClearance.status === 'Cleared' ? (netAmount < 0 ? 'Dues' : 'Cleared') : fffClearance.status)
: jsonClearance.status;
const displayRemarks = fffClearance ? fffClearance.remarks : jsonClearance.remarks;
const displayAmount = fffClearance ? Math.abs(lineItemAmount) : jsonClearance.amount;
const displayType = fffClearance ? (lineItemAmount < 0 ? 'Payable' : 'Recovery') : jsonClearance.type;
const displayAmount = Math.abs(netAmount) || jsonClearance.amount;
const displayType = netAmount > 0 ? 'Payable' : 'Recovery';
return (
<Card key={dept} className="border border-slate-200">
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium capitalize">{dept}</CardTitle>
<CardTitle className="text-base font-medium capitalize">{dept.replace(' Department', '')}</CardTitle>
<Badge className={
displayStatus === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
displayStatus === 'Dues' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
@ -634,24 +663,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
)}
</div>
</div>
{currentUser && (currentUser.role === 'Super Admin' || currentUser.role === 'DD Admin' || (currentUser.role.includes(dept) && resignationData?.currentStage === 'Clearance' || resignationData?.currentStage === 'F&F Initiated')) && (
<Button
variant="ghost"
size="sm"
className="mt-2 text-amber-600 hover:text-blue-700 p-0"
onClick={() => {
setSelectedDept(dept);
setClearanceStatus(displayStatus || 'Pending');
setClearanceRemarks(displayRemarks || '');
setClearanceAmount(displayAmount || 0);
setClearanceType(displayType || 'Recovery');
setClearanceFile(null);
setShowClearanceDialog(true);
}}
>
Update Status
</Button>
)}
</CardContent>
</Card>
);
@ -774,23 +785,37 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</CardHeader>
<CardContent>
<div className="space-y-4">
{(resignationData?.timeline || []).length > 0 ? (
(resignationData.timeline || []).map((log: any, index: number) => (
<div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
{auditLogs.length > 0 ? (
auditLogs.map((log: any, index: number) => (
<div
key={index}
className="flex gap-4 pb-4 border-b border-slate-200 last:border-0"
>
<div className="w-2 h-2 rounded-full bg-amber-600 mt-2" />
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<p className="font-medium text-slate-900">{log.action || log.status}</p>
<span className="text-sm text-slate-600">{formatDateTime(log.timestamp || log.createdAt)}</span>
<p className="font-semibold text-slate-900 flex items-center gap-2">
{log.description || log.action}
</p>
<span className="text-sm text-slate-600 font-mono">
{formatDateTime(log.timestamp || log.createdAt)}
</span>
</div>
<p className="text-sm text-slate-600">{log.user || log.actor}</p>
{log.comments && <p className="text-sm text-slate-500 mt-1">{log.comments}</p>}
<div className="flex items-center gap-2 text-sm text-slate-600 mb-2">
<Badge variant="outline" className="text-[10px] uppercase">{log.userName || 'System'}</Badge>
</div>
{(log.remarks || log.newData?.remarks) && (
<div className="mt-2 p-3 bg-blue-50 border-l-4 border-blue-400 rounded-r text-sm italic text-blue-800">
" {log.remarks || log.newData?.remarks} "
</div>
)}
</div>
</div>
))
) : (
<div className="text-center py-8 text-slate-500">
No audit logs found
<p>No activity logs found for this case.</p>
</div>
)}
</div>
@ -972,111 +997,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</DialogContent>
</Dialog>
{/* Clearance Update Modal */}
<Dialog open={showClearanceDialog} onOpenChange={setShowClearanceDialog}>
<DialogContent className="bg-white">
<DialogHeader>
<DialogTitle>Update {selectedDept} Clearance</DialogTitle>
<DialogDescription>
Provide clearance status and observations for this resignation.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Clearance Status</Label>
<Select value={clearanceStatus} onValueChange={(val: any) => setClearanceStatus(val)}>
<SelectTrigger className="mt-2 text-slate-900 border-slate-300">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]">
<SelectItem value="Cleared" className="text-green-600 focus:bg-green-50">Cleared</SelectItem>
<SelectItem value="Pending" className="text-yellow-600 focus:bg-yellow-50">Pending / In-Review</SelectItem>
<SelectItem value="Dues" className="text-red-600 focus:bg-red-50">Dues / Outstanding</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Amount ()</Label>
<input
type="number"
className="w-full mt-2 p-2 border border-slate-300 rounded-md"
value={clearanceAmount}
onChange={(e) => setClearanceAmount(Number(e.target.value))}
/>
</div>
<div>
<Label>Type</Label>
<Select value={clearanceType} onValueChange={(val: any) => setClearanceType(val)}>
<SelectTrigger className="mt-2 border-slate-300">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent className="bg-white">
<SelectItem value="Recovery">Recovery (Dealer owes RE)</SelectItem>
<SelectItem value="Payable">Payable (RE owes Dealer)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label>Remarks/Details</Label>
<Textarea
placeholder="List any dues, remaining tasks, or observations..."
value={clearanceRemarks}
onChange={(e) => setClearanceRemarks(e.target.value)}
className="mt-2 border-slate-300"
rows={4}
/>
</div>
<div>
<Label>Supporting Document (Optional)</Label>
<div className="mt-2 text-sm text-slate-500 mb-2">
Upload NOC, ledger statement, or evidence of dues.
</div>
<div className="relative group border-2 border-dashed border-slate-200 rounded-lg p-4 transition-colors hover:border-blue-300">
<input
type="file"
onChange={(e) => setClearanceFile(e.target.files?.[0] || null)}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
accept=".pdf,.jpg,.jpeg,.png"
/>
<div className="flex flex-col items-center justify-center gap-2">
{clearanceFile ? (
<div className="flex items-center gap-2 text-amber-600 font-medium">
<FileText className="w-5 h-5" />
<span className="truncate max-w-[200px]">{clearanceFile.name}</span>
<X className="w-4 h-4 cursor-pointer text-slate-400 hover:text-red-500" onClick={(e) => { e.stopPropagation(); setClearanceFile(null); }} />
</div>
) : (
<>
<Upload className="w-8 h-8 text-slate-300 group-hover:text-blue-400" />
<span className="text-slate-400">Click or drag to upload (PDF, JPG, PNG)</span>
</>
)}
</div>
</div>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1 border-slate-300"
onClick={() => setShowClearanceDialog(false)}
disabled={isUpdatingClearance}
>
Cancel
</Button>
<Button
className="flex-1 bg-slate-900 hover:bg-slate-800 text-white"
onClick={handleClearanceUpdate}
disabled={isUpdatingClearance}
>
{isUpdatingClearance ? 'Updating...' : 'Update Clearance'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<DocumentPreviewModal
isOpen={!!previewDocument}

View File

@ -14,8 +14,8 @@ import { User } from '../../lib/mock-data';
import { toast } from 'sonner';
import { terminationService } from '../../services/termination.service';
import { useNavigate } from 'react-router-dom';
import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils';
interface TerminationDetailsProps {
terminationId: string;
onBack: () => void;
@ -30,6 +30,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
const [isLoading, setIsLoading] = useState(true);
const [terminationData, setTerminationData] = useState<any>(null);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [showSCNDialog, setShowSCNDialog] = useState(false);
const [scnFile, setScnFile] = useState<File | null>(null);
const [scnRemarks, setScnRemarks] = useState('');
@ -43,6 +44,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
setIsLoading(true);
const data = await terminationService.getTerminationById(terminationId);
setTerminationData(data);
fetchAuditLogs();
} catch (error) {
console.error('Error fetching termination:', error);
} finally {
@ -50,6 +52,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
}
};
const fetchAuditLogs = async () => {
try {
const response: any = await API.getAuditLogs('termination', terminationId);
if (response.data && response.data.success) {
setAuditLogs(response.data.data || []);
}
} catch (error) {
console.error('Error fetching audit logs:', error);
}
};
useEffect(() => {
fetchTermination();
}, [terminationId]);
@ -370,8 +383,14 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<Badge className={getSeverityColor(request.severity)}>
{request.severity}
</Badge>
<Badge className="bg-red-100 text-red-700 border-red-300">
{request.status}
<Badge className={
request.status === 'Completed' || request.status === 'Terminated' || request.status === 'Settled'
? 'bg-green-100 text-green-700 border-green-300'
: request.status === 'Rejected' || request.status === 'Withdrawn'
? 'bg-red-100 text-red-700 border-red-300'
: 'bg-yellow-100 text-yellow-700 border-yellow-300'
}>
{request.status === 'Settled' ? 'Completed' : (request.status || 'Pending')}
</Badge>
</div>
</div>
@ -840,23 +859,37 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</CardHeader>
<CardContent>
<div className="space-y-4">
{(request.timeline || []).length > 0 ? (
(request.timeline || []).map((log: any, index: number) => (
<div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className="w-2 h-2 rounded-full bg-red-600 mt-2" />
{auditLogs.length > 0 ? (
auditLogs.map((log: any, index: number) => (
<div
key={index}
className="flex gap-4 pb-4 border-b border-slate-200 last:border-0"
>
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" />
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<p className="font-medium text-slate-900">{log.action || log.status}</p>
<span className="text-sm text-slate-600">{formatDateTime(log.timestamp || log.createdAt)}</span>
<p className="font-semibold text-slate-900 flex items-center gap-2">
{log.description || log.action}
</p>
<span className="text-sm text-slate-600 font-mono">
{formatDateTime(log.timestamp || log.createdAt)}
</span>
</div>
<p className="text-sm text-slate-600">{log.user || log.actor}</p>
{log.remarks || log.comments ? <p className="text-sm text-slate-500 mt-1">{log.remarks || log.comments}</p> : null}
<div className="flex items-center gap-2 text-sm text-slate-600 mb-2">
<Badge variant="outline" className="text-[10px] uppercase">{log.userName || 'System'}</Badge>
</div>
{(log.remarks || log.newData?.remarks) && (
<div className="mt-2 p-3 bg-blue-50 border-l-4 border-blue-400 rounded-r text-sm italic text-blue-800">
" {log.remarks || log.newData?.remarks} "
</div>
)}
</div>
</div>
))
) : (
<div className="text-center py-8 text-slate-500">
No history found
<p>No activity logs found for this case.</p>
</div>
)}
</div>

View File

@ -17,6 +17,9 @@ export type UserRole =
| 'Finance'
| 'Finance Admin'
| 'Dealer'
| 'ASM'
| 'CCO'
| 'CEO'
| 'Prospective Dealer';
export type ApplicationStatus =