From 71e6c10c16ec6f1ecf80bed0a4fe7da9766e8ee0 Mon Sep 17 00:00:00 2001 From: laxman h Date: Mon, 13 Apr 2026 20:42:25 +0530 Subject: [PATCH] module wise audit tables added --- src/api/API.ts | 3 + .../ConstitutionalChangeDetails.tsx | 44 +- .../applications/FinanceFnFDetailsPage.tsx | 376 +++++++++++++++--- .../applications/FinanceFnFPage.tsx | 2 +- src/components/applications/FnFDetails.tsx | 304 ++++++++++++-- src/components/applications/FnFPage.tsx | 8 +- .../applications/RelocationRequestDetails.tsx | 67 ++-- .../applications/ResignationDetails.tsx | 309 ++++++-------- .../applications/TerminationDetails.tsx | 57 ++- src/lib/mock-data.ts | 3 + 10 files changed, 816 insertions(+), 357 deletions(-) diff --git a/src/api/API.ts b/src/api/API.ts index d35dc9a..8bd71ac 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -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}`), diff --git a/src/components/applications/ConstitutionalChangeDetails.tsx b/src/components/applications/ConstitutionalChangeDetails.tsx index 0bf608a..827bab9 100644 --- a/src/components/applications/ConstitutionalChangeDetails.tsx +++ b/src/components/applications/ConstitutionalChangeDetails.tsx @@ -88,13 +88,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: const [comments, setComments] = useState(''); const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [request, setRequest] = useState(null); + const [auditLogs, setAuditLogs] = useState([]); 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 */}
- {(request.timeline || request.history || []).map((entry: any, index: number) => ( + {auditLogs.map((entry: any, index: number) => (
- {(entry.status || entry.action)?.toLowerCase().includes('approve') || (entry.status || entry.action)?.toLowerCase().includes('complete') ? ( + {(entry.action)?.toLowerCase().includes('approve') || (entry.action)?.toLowerCase().includes('complete') ? ( ) : ( @@ -595,19 +609,19 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
-

{entry.stage || entry.entityType || 'Update'}

-

{entry.actor || entry.user?.fullName || entry.user}

+

{entry.stage || entry.action}

+

{entry.userName || 'System'}

- - {entry.action || entry.status} + + {entry.action}
-

{entry.comments || entry.remarks || 'No remarks provided'}

-

{formatDateTime(entry.date || entry.createdAt || entry.timestamp)}

+

{entry.description || entry.remarks || 'No remarks provided'}

+

{formatDateTime(entry.timestamp)}

))} - {(request.timeline || request.history || []).length === 0 && ( + {auditLogs.length === 0 && (
No history found
diff --git a/src/components/applications/FinanceFnFDetailsPage.tsx b/src/components/applications/FinanceFnFDetailsPage.tsx index fd5b797..24e4524 100644 --- a/src/components/applications/FinanceFnFDetailsPage.tsx +++ b/src/components/applications/FinanceFnFDetailsPage.tsx @@ -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(null); const [checklist, setChecklist] = useState([]); + const [showClearanceDialog, setShowClearanceDialog] = useState(false); + const [selectedDept, setSelectedDept] = useState(null); + const [isUpdatingClearance, setIsUpdatingClearance] = useState(false); + const [clearanceForm, setClearanceForm] = useState({ + status: 'Pending', + remarks: '', + amount: 0, + type: 'Recovery' + }); + const [clearanceFile, setClearanceFile] = useState(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 = { + '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 {editingPayableId === item.id ? ( - handleUpdatePayable(item.id, 'department', e.target.value)} - className="h-8" - /> + onValueChange={(val) => handleUpdatePayable(item.id, 'department', val)} + > + + + + + {ALL_DEPARTMENTS.map(dept => ( + {dept} + ))} + + ) : ( - {item.department} + {normalizeDepartment(item.department)} )} @@ -937,12 +1042,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr

Add New Payable Item:

- setNewPayable({ ...newPayable, department: e.target.value })} - className="col-span-3" - /> + onValueChange={(val) => setNewPayable({ ...newPayable, department: val })} + > + + + + + {ALL_DEPARTMENTS.map(dept => ( + {dept} + ))} + + {editingReceivableId === item.id ? ( - handleUpdateReceivable(item.id, 'department', e.target.value)} - className="h-8" - /> + onValueChange={(val) => handleUpdateReceivable(item.id, 'department', val)} + > + + + + + {ALL_DEPARTMENTS.map(dept => ( + {dept} + ))} + + ) : ( - {item.department} + {normalizeDepartment(item.department)} )} @@ -1078,12 +1198,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr

Add New Receivable Item:

- setNewReceivable({ ...newReceivable, department: e.target.value })} - className="col-span-3" - /> + onValueChange={(val) => setNewReceivable({ ...newReceivable, department: val })} + > + + + + + {ALL_DEPARTMENTS.map(dept => ( + {dept} + ))} + + {editingDeductionId === item.id ? ( - handleUpdateDeduction(item.id, 'department', e.target.value)} - className="h-8" - /> + onValueChange={(val) => handleUpdateDeduction(item.id, 'department', val)} + > + + + + + {ALL_DEPARTMENTS.map(dept => ( + {dept} + ))} + + ) : ( - {item.department} + {normalizeDepartment(item.department)} )} @@ -1219,12 +1354,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr

Add New Deduction Item:

- setNewDeduction({ ...newDeduction, department: e.target.value })} - className="col-span-3" - /> + onValueChange={(val) => setNewDeduction({ ...newDeduction, department: val })} + > + + + + + {ALL_DEPARTMENTS.map(dept => ( + {dept} + ))} + + Amount Submitted Date Remarks + Actions @@ -1442,6 +1585,25 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr )}
+ + + ))} @@ -1877,6 +2039,94 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
+ {/* Clearance Update Dialog */} + + + + Update {selectedDept?.departmentName} Status + + Mark the department as cleared or report pending dues with amount. + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ + setClearanceForm({ ...clearanceForm, amount: Number(e.target.value) })} + /> +
+
+ +
+ +