module wise audit tables added
This commit is contained in:
parent
7126d4b6bf
commit
71e6c10c16
@ -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}`),
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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={() => {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -17,6 +17,9 @@ export type UserRole =
|
||||
| 'Finance'
|
||||
| 'Finance Admin'
|
||||
| 'Dealer'
|
||||
| 'ASM'
|
||||
| 'CCO'
|
||||
| 'CEO'
|
||||
| 'Prospective Dealer';
|
||||
|
||||
export type ApplicationStatus =
|
||||
|
||||
Loading…
Reference in New Issue
Block a user