Dealer_Onboard_Frontend/src/features/fnf/pages/FinanceFnFDetailsPage.tsx

2247 lines
100 KiB
TypeScript

import { useState, useEffect } from 'react';
import { API } from '@/api/API';
import { settlementService } from '@/services/settlement.service';
import { Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Progress } from '@/components/ui/progress';
import {
ArrowLeft,
IndianRupee,
CheckCircle,
XCircle,
Upload,
FileText,
User,
AlertCircle,
Wallet,
Receipt,
TrendingUp,
TrendingDown,
Building,
CreditCard,
Send,
Users,
Plus,
Edit2,
Trash2,
Save,
Paperclip,
FileDown
} from 'lucide-react';
import { toast } from 'sonner';
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
import { formatDateTime } from '@/lib/dateUtils';
import { BankDetailsModal } from '@/features/onboarding/components/BankDetailsModal';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
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'
];
const DEPARTMENT_CLAIM_PREFIX = '[DEPARTMENT_CLAIM]';
const FINANCE_VALIDATED_PREFIX = '[FINANCE_VALIDATED]';
interface FinanceFnFDetailsPageProps {
fnfId: string;
onBack: () => void;
}
// Removing mock data functions as we use live API
const SETTLEMENT_CHECKLIST = [
{ id: 'calculations', label: 'Verified All Department Calculations' },
{ id: 'bank', label: 'Confirmed Bank Account Details' },
{ id: 'docs', label: 'Reviewed All Supporting Documents' },
{ id: 'sap', label: 'Synced Final Dues with SAP' },
{ id: 'noc', label: 'Received All Mandatory NOCs' }
];
interface FinancialLineItem {
id: string;
department: string;
description: string;
amount: number;
}
export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) {
const [fnfCase, setFnfCase] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
// Initialize editable line items
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
const [previewDocument, setPreviewDocument] = useState<any>(null);
const [bankDetails, setBankDetails] = useState<any[]>([]);
const [isBankModalOpen, setIsBankModalOpen] = useState(false);
const [editingBank, setEditingBank] = useState<any>(null);
const [checklist, setChecklist] = useState<string[]>([]);
useEffect(() => {
fetchDepartments();
fetchFnFDetails();
}, [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 && 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 isDepartmentClaimLine = (description?: string, sourceType?: string) =>
sourceType === 'DepartmentClaim' ||
(typeof description === 'string' &&
(description.startsWith(DEPARTMENT_CLAIM_PREFIX) || description.includes('Clearance:')));
const isAutoSeededDeptMirror = (li: any) =>
li?.sourceType === 'FinanceValidated' &&
typeof li?.description === 'string' &&
li.description.includes('Auto-seeded from department claim');
const isFinanceValidatedLine = (description?: string, sourceType?: string) =>
sourceType === 'FinanceValidated' ||
(typeof description === 'string' && description.startsWith(FINANCE_VALIDATED_PREFIX));
const cleanLineItemDescription = (description?: string) =>
(description || '')
.replace(DEPARTMENT_CLAIM_PREFIX, '')
.replace(FINANCE_VALIDATED_PREFIX, '')
.trim();
const fetchFnFDetails = async (showLoader: boolean = true) => {
try {
if (showLoader) setLoading(true);
const response = await API.getFnFSettlementById(fnfId);
const data = response.data as any;
if (data.success) {
const s = data.fnf;
const mappedCase = {
id: s.id,
caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || 'N/A',
dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination',
submittedDate: formatDateTime(s.createdAt),
createdAt: s.createdAt,
dueDate: s.settlementDate ? formatDateTime(s.settlementDate) : 'TBD',
status: s.status,
dealerId: s.outlet?.dealer?.id || s.dealerId,
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.requestId || s.terminationRequest?.id || "N/A",
salesCode: s.dealer?.dealerCode?.salesCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A',
serviceCode: s.dealer?.dealerCode?.serviceCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.serviceCode || 'N/A',
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',
allLineItems: (s.lineItems || []).filter((li: any) => li.isActive !== false),
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
const c = (s.clearances || []).find((clearance: any) => normalizeDepartment(clearance.department) === deptName);
const lines = (s.lineItems || []).filter((li: any) => li.isActive !== false);
const claimLines = lines.filter(
(li: any) =>
normalizeDepartment(li.department) === deptName && isDepartmentClaimLine(li.description, li.sourceType),
);
const seededMirrorLines = lines.filter(
(li: any) =>
normalizeDepartment(li.department) === deptName && isAutoSeededDeptMirror(li),
);
const relatedItems = claimLines.length > 0 ? claimLines : seededMirrorLines;
// 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;
const hasDuesAmount = Math.abs(netAmount) > 0;
const rawStatus = c?.status || 'Pending';
const normalizedStatus = hasDuesAmount
? 'Dues Pending'
: (rawStatus === 'Cleared' ? 'NOC Submitted' : rawStatus);
/** Net payable to dealer vs receivable from dealer — drives UI colors */
const duesFlow =
netAmount > 0 ? 'payable' as const :
netAmount < 0 ? 'recovery' as const :
null;
return {
id: c?.id || `dept-${deptName}`,
departmentName: deptName,
status: normalizedStatus,
remarks: c?.remarks || '-',
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : '-',
amount: Math.abs(netAmount),
duesFlow,
amountType: netAmount > 0
? 'Payable to dealer'
: netAmount < 0
? 'Receivable from dealer'
: null,
supportingDocument: c?.supportingDocument || null
};
}),
documents: [
{ name: 'Resignation Letter.pdf', size: 'N/A', uploadedOn: formatDateTime(s.createdAt), type: 'Resignation', url: '#' },
...(s.clearances || [])
.filter((c: any) => c.supportingDocument)
.map((c: any) => ({
name: c.supportingDocument.split('/').pop(),
size: 'N/A',
uploadedOn: formatDateTime(c.clearedAt),
type: `${c.department} Proof`,
url: c.supportingDocument
})),
...(s.clearanceDocuments || []).map((doc: any) => ({
name: doc.name || doc.supportingDocument?.split('/').pop() || 'Document',
size: 'N/A',
uploadedOn: formatDateTime(doc.clearedAt || s.createdAt),
type: 'Finance Upload',
url: doc.supportingDocument
}))
]
};
setFnfCase(mappedCase);
// Sync bank details from the pre-fetched data inside the settlement object
const preFetchedBankDetails = s.bankDetails || s.dealer?.bankDetails || s.outlet?.dealer?.dealerProfile?.bankDetails;
if (preFetchedBankDetails && preFetchedBankDetails.length > 0) {
setBankDetails(preFetchedBankDetails);
} else if (s.outlet?.dealer?.id || s.dealerId) {
fetchBankDetails(s.outlet?.dealer?.id || s.dealerId);
}
// Split line items into categories
const pItems: FinancialLineItem[] = [];
const rItems: FinancialLineItem[] = [];
const dItems: FinancialLineItem[] = [];
const allLineItems = (s.lineItems || []).filter((li: any) => li.isActive !== false);
const hasFinanceValidatedLines = allLineItems.some((li: any) => isFinanceValidatedLine(li.description, li.sourceType));
const calculationLineItems = hasFinanceValidatedLines
? allLineItems.filter((li: any) => isFinanceValidatedLine(li.description, li.sourceType))
: allLineItems.filter((li: any) => !isDepartmentClaimLine(li.description, li.sourceType));
calculationLineItems.forEach((li: any) => {
const item: FinancialLineItem = {
id: li.id,
department: normalizeDepartment(li.department),
description: cleanLineItemDescription(li.description || li.remarks || ''),
amount: Math.abs(li.amount)
};
if (li.itemType === 'Payable') {
pItems.push(item);
} else if (li.itemType === 'Deduction') {
dItems.push(item);
} else {
rItems.push(item);
}
});
setPayableItems(pItems);
setReceivableItems(rItems);
setDeductionItems(dItems);
// Populate settlement details from backend
setSettlementDetails({
verificationTransactionId: s.transactionReference || '',
settlementAmount: (s.settlementAmount || calculateDynamicSettlement().settlementAmount).toString(),
settlementDate: s.settlementDate ? new Date(s.settlementDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
paymentMode: s.paymentMode || '',
bankReference: '', // Optional field
verificationRemarks: s.remarks || '',
adjustments: '0'
});
}
} catch (error) {
console.error('Fetch F&F error:', error);
toast.error('Failed to fetch settlement details');
} finally {
if (showLoader) setLoading(false);
}
};
const fetchBankDetails = async (dealerId: string) => {
try {
const response = await API.getDealerBankDetails(dealerId);
const data = response.data as any;
if (data.success) {
setBankDetails(data.bankDetails || []);
}
} catch (error) {
console.error('Fetch bank details error:', error);
}
};
const handleUpsertBank = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData.entries());
try {
const dealerId = fnfCase?.dealerId;
const response = await API.saveBankDetail(dealerId, {
...data,
id: editingBank?.id,
isPrimary: formData.get('isPrimary') === 'on'
}) as any;
if (response.data.success) {
toast.success('Bank details saved');
fetchBankDetails(dealerId);
setIsBankModalOpen(false);
setEditingBank(null);
}
} catch (error) {
toast.error('Failed to save bank details');
}
};
const handleDeleteBank = async (id: string) => {
if (!confirm('Are you sure you want to delete this bank account?')) return;
try {
const response = await API.deleteBankDetail(id) as any;
if (response.data.success) {
toast.success('Bank detail deleted');
fetchBankDetails(fnfCase?.dealerId);
}
} catch (error) {
toast.error('Failed to delete bank details');
}
};
const toggleChecklist = (id: string) => {
setChecklist(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
// Form states for adding new items
const [newPayable, setNewPayable] = useState({ department: '', description: '', amount: '' });
const [newReceivable, setNewReceivable] = useState({ department: '', description: '', amount: '' });
const [newDeduction, setNewDeduction] = useState({ department: '', description: '', amount: '' });
// Edit mode states
const [editingPayableId, setEditingPayableId] = useState<string | null>(null);
const [editingReceivableId, setEditingReceivableId] = useState<string | null>(null);
const [editingDeductionId, setEditingDeductionId] = useState<string | null>(null);
const [editingPayableDrafts, setEditingPayableDrafts] = useState<Record<string, FinancialLineItem>>({});
const [editingReceivableDrafts, setEditingReceivableDrafts] = useState<Record<string, FinancialLineItem>>({});
const [editingDeductionDrafts, setEditingDeductionDrafts] = useState<Record<string, FinancialLineItem>>({});
// Calculate dynamic settlement
const calculateDynamicSettlement = () => {
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 {
payables,
receivables,
deductions,
netSettlement,
settlementAmount: Math.abs(netSettlement),
settlementType: netSettlement > 0 ? 'Payable to Dealer' : netSettlement < 0 ? 'Receivable from Dealer' : 'No Settlement Required'
};
};
const settlement = calculateDynamicSettlement();
const departmentReconciliation = ALL_DEPARTMENTS.map((dept) => {
const claim = (fnfCase?.departmentResponses || []).find((d: any) => d.departmentName === dept);
const claimAmount = Number(claim?.amount) || 0;
const claimType = claim?.amountType || '-';
const validatedPayable = payableItems
.filter((item) => normalizeDepartment(item.department) === dept)
.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
const validatedReceivable = receivableItems
.filter((item) => normalizeDepartment(item.department) === dept)
.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
const validatedDeduction = deductionItems
.filter((item) => normalizeDepartment(item.department) === dept)
.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
const validatedNet = validatedPayable - validatedReceivable - validatedDeduction;
const validatedAmount = Math.abs(validatedNet);
const validatedType = validatedNet > 0 ? 'Payable' : validatedNet < 0 ? 'Receivable' : '-';
const variance = validatedAmount - claimAmount;
return {
department: dept,
claimAmount,
claimType,
validatedAmount,
validatedType,
variance
};
});
const [settlementDetails, setSettlementDetails] = useState({
verificationTransactionId: '',
settlementAmount: settlement.settlementAmount.toString(),
settlementDate: new Date().toISOString().split('T')[0],
paymentMode: '',
bankReference: '',
verificationRemarks: '',
adjustments: '0'
});
// Handlers for Payables
const handleAddPayable = async () => {
if (!newPayable.department || !newPayable.description || !newPayable.amount) {
toast.error('Please fill in all fields');
return;
}
try {
const response = await API.addLineItem(fnfId, {
department: newPayable.department,
description: newPayable.description,
amount: Math.abs(parseFloat(newPayable.amount)),
itemType: 'Payable'
});
const data = response.data as any;
if (data.success) {
setPayableItems([...payableItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.description,
amount: Math.abs(data.lineItem.amount)
}]);
setNewPayable({ department: '', description: '', amount: '' });
toast.success('Payable item added');
fetchFnFDetails();
}
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to add payable item');
}
};
const handleUpdatePayable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
setEditingPayableDrafts((prev) => {
const base = prev[id] || payableItems.find((item) => item.id === id);
if (!base) return prev;
return {
...prev,
[id]: {
...base,
[field]: field === 'amount' ? Number(value) || 0 : value
} as FinancialLineItem
};
});
};
const handleSavePayableEdit = async (id: string) => {
const draft = editingPayableDrafts[id];
if (!draft) {
setEditingPayableId(null);
return;
}
setPayableItems((prev) => prev.map((item) => (item.id === id ? draft : item)));
try {
await API.updateLineItem(id, {
department: draft.department,
description: draft.description,
amount: -Math.abs(Number(draft.amount) || 0)
});
setEditingPayableId(null);
setEditingPayableDrafts((prev) => {
const next = { ...prev };
delete next[id];
return next;
});
toast.success('Changes saved');
fetchFnFDetails(false);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to update item');
fetchFnFDetails(false);
}
};
const handleDeletePayable = async (id: string) => {
try {
const response = await API.deleteLineItem(id);
const data = response.data as any;
if (data.success) {
setPayableItems(payableItems.filter(item => item.id !== id));
toast.info('Payable item removed');
fetchFnFDetails();
}
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to delete item');
}
};
// Handlers for Receivables
const handleAddReceivable = async () => {
if (!newReceivable.department || !newReceivable.description || !newReceivable.amount) {
toast.error('Please fill in all fields');
return;
}
try {
const response = await API.addLineItem(fnfId, {
department: newReceivable.department,
description: newReceivable.description,
amount: Math.abs(parseFloat(newReceivable.amount)),
itemType: 'Receivable'
});
const data = response.data as any;
if (data.success) {
setReceivableItems([...receivableItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.description,
amount: data.lineItem.amount
}]);
setNewReceivable({ department: '', description: '', amount: '' });
toast.success('Receivable item added');
fetchFnFDetails();
}
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to add receivable item');
}
};
const handleUpdateReceivable = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
setEditingReceivableDrafts((prev) => {
const base = prev[id] || receivableItems.find((item) => item.id === id);
if (!base) return prev;
return {
...prev,
[id]: {
...base,
[field]: field === 'amount' ? Number(value) || 0 : value
} as FinancialLineItem
};
});
};
const handleSaveReceivableEdit = async (id: string) => {
const draft = editingReceivableDrafts[id];
if (!draft) {
setEditingReceivableId(null);
return;
}
setReceivableItems((prev) => prev.map((item) => (item.id === id ? draft : item)));
try {
await API.updateLineItem(id, {
department: draft.department,
description: draft.description,
amount: Math.abs(Number(draft.amount) || 0)
});
setEditingReceivableId(null);
setEditingReceivableDrafts((prev) => {
const next = { ...prev };
delete next[id];
return next;
});
toast.success('Changes saved');
fetchFnFDetails(false);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to update item');
fetchFnFDetails(false);
}
};
const handleDeleteReceivable = async (id: string) => {
try {
await API.deleteLineItem(id);
setReceivableItems(receivableItems.filter(item => item.id !== id));
toast.info('Receivable item removed');
fetchFnFDetails();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to delete item');
}
};
// Handlers for Deductions
const handleAddDeduction = async () => {
if (!newDeduction.department || !newDeduction.description || !newDeduction.amount) {
toast.error('Please fill in all fields');
return;
}
try {
const response = await API.addLineItem(fnfId, {
department: newDeduction.department,
description: newDeduction.description,
amount: Math.abs(parseFloat(newDeduction.amount)),
itemType: 'Deduction'
});
const data = response.data as any;
if (data.success) {
setDeductionItems([...deductionItems, {
id: data.lineItem.id,
department: data.lineItem.department,
description: data.lineItem.description,
amount: data.lineItem.amount
}]);
setNewDeduction({ department: '', description: '', amount: '' });
toast.success('Deduction item added');
fetchFnFDetails();
}
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to add deduction item');
}
};
const handleUpdateDeduction = async (id: string, field: keyof FinancialLineItem, value: string | number) => {
setEditingDeductionDrafts((prev) => {
const base = prev[id] || deductionItems.find((item) => item.id === id);
if (!base) return prev;
return {
...prev,
[id]: {
...base,
[field]: field === 'amount' ? Number(value) || 0 : value
} as FinancialLineItem
};
});
};
const handleSaveDeductionEdit = async (id: string) => {
const draft = editingDeductionDrafts[id];
if (!draft) {
setEditingDeductionId(null);
return;
}
setDeductionItems((prev) => prev.map((item) => (item.id === id ? draft : item)));
try {
await API.updateLineItem(id, {
department: draft.department,
description: draft.description,
amount: Math.abs(Number(draft.amount) || 0)
});
setEditingDeductionId(null);
setEditingDeductionDrafts((prev) => {
const next = { ...prev };
delete next[id];
return next;
});
toast.success('Changes saved');
fetchFnFDetails(false);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to update item');
fetchFnFDetails(false);
}
};
const handleDeleteDeduction = async (id: string) => {
try {
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');
}
};
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
setLoading(true);
try {
let successCount = 0;
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('file', files[i]);
const response: any = await API.uploadFnFDocument(fnfId, formData);
if (response.data?.success) successCount++;
}
toast.success(`${successCount} document(s) uploaded successfully`);
fetchFnFDetails(false); // Fetch latest documents
} catch (error) {
toast.error('Failed to upload document(s)');
} finally {
setLoading(false);
}
}
};
const [submitting, setSubmitting] = useState(false);
const handleApproveSettlement = async () => {
if (!settlementDetails.verificationTransactionId || !settlementDetails.settlementDate || !settlementDetails.paymentMode) {
toast.error('Please fill in all required settlement details');
return;
}
try {
setSubmitting(true);
const adjustedAmount = (settlement.settlementAmount || 0) + parseFloat(settlementDetails.adjustments || '0');
await settlementService.updateFnF(fnfId, {
status: 'Completed',
finalSettlementAmount: adjustedAmount,
settlementDate: settlementDetails.settlementDate,
paymentMode: settlementDetails.paymentMode,
transactionReference: settlementDetails.verificationTransactionId,
remarks: settlementDetails.verificationRemarks || 'Approved by Finance'
});
toast.success(`F&F Settlement approved and completed for ${fnfCase.dealerName}`);
setTimeout(() => onBack(), 1500);
} catch (error: any) {
console.error('Approve settlement error:', error);
toast.error(error.message || 'Failed to approve settlement');
} finally {
setSubmitting(false);
}
};
const handleRejectSettlement = () => {
if (!settlementDetails.verificationRemarks) {
toast.error('Please provide remarks for rejection');
return;
}
toast.error(`F&F Settlement rejected for ${fnfCase.dealerName}`);
setTimeout(() => onBack(), 1500);
};
const handleRequestClarification = () => {
if (!settlementDetails.verificationRemarks) {
toast.error('Please provide details for clarification request');
return;
}
toast.info(`Clarification request sent for ${fnfCase.dealerName}`);
setTimeout(() => onBack(), 1500);
};
if (loading) {
return (
<div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div>
);
}
if (!fnfCase) {
return (
<div className="text-center py-12 text-slate-500">
<p>Settlement case not found</p>
<Button onClick={onBack} className="mt-4">Go Back</Button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="outline" size="icon" onClick={onBack}>
<ArrowLeft className="w-4 h-4" />
</Button>
<div>
<h1 className="text-3xl mb-1">F&F Settlement Review</h1>
<p className="text-slate-600">Full & Final Settlement for {fnfCase.dealerName}</p>
</div>
</div>
{/* Status Banner */}
<Card className="border-red-200 bg-red-50">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="size-12 shrink-0 aspect-square rounded-full bg-red-50 flex items-center justify-center">
<IndianRupee className="w-5 h-5" />
</div>
<div>
<p className="text-slate-900">Settlement Pending Finance Approval</p>
<p className="text-sm text-slate-600">Case: {fnfCase.caseNumber} Due: {fnfCase.dueDate}</p>
</div>
</div>
<div className="flex gap-2">
<Badge className="bg-re-red">
{fnfCase.status}
</Badge>
<Badge variant={fnfCase.terminationType === 'Resignation' ? 'default' : 'secondary'}>
{fnfCase.terminationType}
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* Settlement Summary Card */}
<Card className={`${
settlement.settlementType === 'Payable to Dealer'
? 'border-red-300 bg-red-50'
: settlement.settlementType === 'Receivable from Dealer'
? 'border-green-300 bg-green-50'
: 'border-slate-300 bg-slate-50'
}`}>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
{settlement.settlementType === 'Payable to Dealer' ? (
<TrendingDown className="w-12 h-12 text-red-600" />
) : settlement.settlementType === 'Receivable from Dealer' ? (
<TrendingUp className="w-12 h-12 text-green-600" />
) : (
<CheckCircle className="w-12 h-12 text-slate-600" />
)}
<div>
<p className={`text-sm ${
settlement.settlementType === 'Payable to Dealer'
? 'text-red-700'
: settlement.settlementType === 'Receivable from Dealer'
? 'text-green-700'
: 'text-slate-700'
}`}>
{settlement.settlementType}
</p>
<p className="text-3xl text-slate-900">
{settlement.settlementType === 'No Settlement Required'
? '₹0'
: `${settlement.settlementAmount.toLocaleString('en-IN')}`}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-slate-600">Net Settlement Amount</p>
<p className="text-xs text-slate-500 mt-1">
{settlement.settlementType === 'Payable to Dealer'
? 'Company will pay to dealer'
: settlement.settlementType === 'Receivable from Dealer'
? 'Dealer must pay to company'
: 'No payment required'}
</p>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Case Details & Financial Info */}
<div className="lg:col-span-2 space-y-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="financial">Financial</TabsTrigger>
<TabsTrigger value="departments">Departments</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger>
<TabsTrigger value="bank">Bank Details</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
{/* Case Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="w-5 h-5" />
Case Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-500">Case Number</Label>
<p className="text-slate-900">{fnfCase.caseNumber}</p>
</div>
<div>
<Label className="text-slate-500">Dealer Code</Label>
<p className="text-slate-900">{fnfCase.dealerCode}</p>
</div>
<div>
<Label className="text-slate-500">Dealer Name</Label>
<p className="text-slate-900">{fnfCase.dealerName}</p>
</div>
<div>
<Label className="text-slate-500">Location</Label>
<p className="text-slate-900">{fnfCase.location}</p>
</div>
<div>
<Label className="text-slate-500">Termination Type</Label>
<Badge variant={fnfCase.terminationType === 'Resignation' ? 'default' : 'secondary'}>
{fnfCase.terminationType}
</Badge>
</div>
<div>
<Label className="text-slate-500">Status</Label>
<Badge className="bg-re-red">
{fnfCase.status}
</Badge>
</div>
<div>
<Label className="text-slate-500">Submitted Date</Label>
<p className="text-slate-900">{fnfCase.submittedDate}</p>
</div>
<div>
<Label className="text-slate-500">Due Date</Label>
<p className="text-slate-900">{fnfCase.dueDate}</p>
</div>
<div>
<Label className="text-slate-500">Request Age</Label>
<p className="text-slate-900">
{(() => {
const submitted = new Date(fnfCase.createdAt);
const today = new Date();
const diffTime = Math.abs(today.getTime() - submitted.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return `${diffDays} day${diffDays !== 1 ? 's' : ''}`;
})()}
</p>
</div>
<div>
<Label className="text-slate-500">Sales Code</Label>
<p className="text-slate-900">{fnfCase.salesCode}</p>
</div>
<div>
<Label className="text-slate-500">Service Code</Label>
<p className="text-slate-900">{fnfCase.serviceCode}</p>
</div>
<div>
<Label className="text-slate-500">Gear Code</Label>
<p className="text-slate-900">{fnfCase.gearCode}</p>
</div>
<div>
<Label className="text-slate-500">GMA Code</Label>
<p className="text-slate-900">{fnfCase.gmaCode}</p>
</div>
</div>
</CardContent>
</Card>
{/* Settlement Calculation Overview */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<IndianRupee className="w-5 h-5" />
Settlement Calculation Summary
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-green-50 rounded-lg">
<span className="text-slate-900">Total Payables (to Dealer)</span>
<span className="text-green-700 text-lg">+ {settlement.payables.toLocaleString('en-IN')}</span>
</div>
<div className="flex justify-between items-center p-3 bg-red-50 rounded-lg">
<span className="text-slate-900">Total Receivables (from Dealer)</span>
<span className="text-red-700 text-lg">- {settlement.receivables.toLocaleString('en-IN')}</span>
</div>
<div className="flex justify-between items-center p-3 bg-red-50 rounded-lg">
<span className="text-slate-900">Total Deductions</span>
<span className="text-re-red-hover text-lg">- {settlement.deductions.toLocaleString('en-IN')}</span>
</div>
</div>
<div className="h-px bg-slate-300"></div>
<div className={`p-4 rounded-lg border-2 ${
settlement.settlementType === 'Payable to Dealer'
? 'bg-red-100 border-red-300'
: settlement.settlementType === 'Receivable from Dealer'
? 'bg-green-100 border-green-300'
: 'bg-slate-100 border-slate-300'
}`}>
<div className="flex items-center justify-between">
<div>
<span className="text-slate-900">Net Settlement</span>
<p className={`text-sm ${
settlement.settlementType === 'Payable to Dealer'
? 'text-red-700'
: settlement.settlementType === 'Receivable from Dealer'
? 'text-green-700'
: 'text-slate-700'
}`}>
{settlement.settlementType}
</p>
</div>
<span className="text-2xl text-slate-900">
{settlement.settlementType === 'No Settlement Required'
? '₹0'
: `${settlement.settlementAmount.toLocaleString('en-IN')}`}
</span>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-red-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600">
Net Settlement = Payables - Receivables - Deductions<br/>
<span className="text-xs">All amounts are editable in the Financial tab</span>
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="financial" className="space-y-4">
<Card className="border-blue-200 bg-blue-50">
<CardHeader>
<CardTitle className="text-base">Department Claim vs Finance Validation</CardTitle>
<CardDescription>
Finance validated values are used for final settlement totals.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Department</TableHead>
<TableHead>Department Claim</TableHead>
<TableHead>Finance Validated</TableHead>
<TableHead>Variance</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{departmentReconciliation.map((row) => (
<TableRow key={row.department}>
<TableCell>{row.department}</TableCell>
<TableCell>
{row.claimAmount > 0 ? `${row.claimType}${row.claimAmount.toLocaleString('en-IN')}` : '-'}
</TableCell>
<TableCell>
{row.validatedAmount > 0 ? `${row.validatedType}${row.validatedAmount.toLocaleString('en-IN')}` : '-'}
</TableCell>
<TableCell className={row.variance === 0 ? 'text-slate-600' : row.variance > 0 ? 'text-red-600' : 'text-green-600'}>
{row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `${row.variance.toLocaleString('en-IN')}`}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Payables - Editable */}
<Card className="border-green-200 bg-green-50">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base flex items-center gap-2">
<Wallet className="w-5 h-5 text-green-600" />
Payables to Dealer (Editable)
</CardTitle>
<CardDescription>Add or modify amounts company owes to dealer</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Existing Payables */}
<Table>
<TableHeader>
<TableRow>
<TableHead>Department</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right">Amount ()</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{payableItems.map((item) => (
<TableRow key={item.id}>
<TableCell>
{editingPayableId === item.id ? (
<Select
value={(editingPayableDrafts[item.id]?.department || item.department)}
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">{normalizeDepartment(item.department)}</span>
)}
</TableCell>
<TableCell>
{editingPayableId === item.id ? (
<Input
value={(editingPayableDrafts[item.id]?.description || item.description)}
onChange={(e) => handleUpdatePayable(item.id, 'description', e.target.value)}
className="h-8"
/>
) : (
<span className="text-slate-600">{item.description}</span>
)}
</TableCell>
<TableCell className="text-right">
{editingPayableId === item.id ? (
<Input
type="number"
value={(editingPayableDrafts[item.id]?.amount ?? item.amount)}
onChange={(e) => handleUpdatePayable(item.id, 'amount', e.target.value)}
className="h-8 text-right"
/>
) : (
<span className="text-slate-900">{item.amount.toLocaleString('en-IN')}</span>
)}
</TableCell>
<TableCell>
<div className="flex gap-1">
{editingPayableId === item.id ? (
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => handleSavePayableEdit(item.id)}
>
<Save className="w-4 h-4" />
</Button>
) : (
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => {
setEditingPayableId(item.id);
setEditingPayableDrafts((prev) => ({
...prev,
[item.id]: { ...item }
}));
}}
>
<Edit2 className="w-4 h-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-red-600 hover:text-red-700"
onClick={() => handleDeletePayable(item.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Add New Payable */}
<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">
<Select
value={newPayable.department}
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}
onChange={(e) => setNewPayable({ ...newPayable, description: e.target.value })}
className="col-span-5"
/>
<Input
type="number"
placeholder="Amount"
value={newPayable.amount}
onChange={(e) => setNewPayable({ ...newPayable, amount: e.target.value })}
className="col-span-3"
/>
<Button onClick={handleAddPayable} className="col-span-1 bg-green-600 hover:bg-green-700">
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
{/* Total */}
<div className="pt-3 border-t-2 border-green-400">
<div className="flex justify-between items-center">
<span className="text-slate-900">Total Payables</span>
<span className="text-green-700 text-xl">
{settlement.payables.toLocaleString('en-IN')}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Receivables - Editable */}
<Card className="border-red-200 bg-red-50">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base flex items-center gap-2">
<Receipt className="w-5 h-5 text-red-600" />
Receivables from Dealer (Editable)
</CardTitle>
<CardDescription>Add or modify amounts dealer owes to company</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Existing Receivables */}
<Table>
<TableHeader>
<TableRow>
<TableHead>Department</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right">Amount ()</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{receivableItems.map((item) => (
<TableRow key={item.id}>
<TableCell>
{editingReceivableId === item.id ? (
<Select
value={(editingReceivableDrafts[item.id]?.department || item.department)}
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">{normalizeDepartment(item.department)}</span>
)}
</TableCell>
<TableCell>
{editingReceivableId === item.id ? (
<Input
value={(editingReceivableDrafts[item.id]?.description || item.description)}
onChange={(e) => handleUpdateReceivable(item.id, 'description', e.target.value)}
className="h-8"
/>
) : (
<span className="text-slate-600">{item.description}</span>
)}
</TableCell>
<TableCell className="text-right">
{editingReceivableId === item.id ? (
<Input
type="number"
value={(editingReceivableDrafts[item.id]?.amount ?? item.amount)}
onChange={(e) => handleUpdateReceivable(item.id, 'amount', e.target.value)}
className="h-8 text-right"
/>
) : (
<span className="text-slate-900">{item.amount.toLocaleString('en-IN')}</span>
)}
</TableCell>
<TableCell>
<div className="flex gap-1">
{editingReceivableId === item.id ? (
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => handleSaveReceivableEdit(item.id)}
>
<Save className="w-4 h-4" />
</Button>
) : (
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => {
setEditingReceivableId(item.id);
setEditingReceivableDrafts((prev) => ({
...prev,
[item.id]: { ...item }
}));
}}
>
<Edit2 className="w-4 h-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-red-600 hover:text-red-700"
onClick={() => handleDeleteReceivable(item.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Add New Receivable */}
<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">
<Select
value={newReceivable.department}
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}
onChange={(e) => setNewReceivable({ ...newReceivable, description: e.target.value })}
className="col-span-5"
/>
<Input
type="number"
placeholder="Amount"
value={newReceivable.amount}
onChange={(e) => setNewReceivable({ ...newReceivable, amount: e.target.value })}
className="col-span-3"
/>
<Button onClick={handleAddReceivable} className="col-span-1 bg-red-600 hover:bg-red-700">
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
{/* Total */}
<div className="pt-3 border-t-2 border-red-400">
<div className="flex justify-between items-center">
<span className="text-slate-900">Total Receivables</span>
<span className="text-red-700 text-xl">
{settlement.receivables.toLocaleString('en-IN')}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Deductions - Editable */}
<Card className="border-red-200 bg-red-50">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-re-red" />
Deductions (Editable)
</CardTitle>
<CardDescription>Add or modify pending claims and deductions</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Existing Deductions */}
<Table>
<TableHeader>
<TableRow>
<TableHead>Department</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right">Amount ()</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{deductionItems.map((item) => (
<TableRow key={item.id}>
<TableCell>
{editingDeductionId === item.id ? (
<Select
value={(editingDeductionDrafts[item.id]?.department || item.department)}
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">{normalizeDepartment(item.department)}</span>
)}
</TableCell>
<TableCell>
{editingDeductionId === item.id ? (
<Input
value={(editingDeductionDrafts[item.id]?.description || item.description)}
onChange={(e) => handleUpdateDeduction(item.id, 'description', e.target.value)}
className="h-8"
/>
) : (
<span className="text-slate-600">{item.description}</span>
)}
</TableCell>
<TableCell className="text-right">
{editingDeductionId === item.id ? (
<Input
type="number"
value={(editingDeductionDrafts[item.id]?.amount ?? item.amount)}
onChange={(e) => handleUpdateDeduction(item.id, 'amount', e.target.value)}
className="h-8 text-right"
/>
) : (
<span className="text-slate-900">{item.amount.toLocaleString('en-IN')}</span>
)}
</TableCell>
<TableCell>
<div className="flex gap-1">
{editingDeductionId === item.id ? (
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => handleSaveDeductionEdit(item.id)}
>
<Save className="w-4 h-4" />
</Button>
) : (
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => {
setEditingDeductionId(item.id);
setEditingDeductionDrafts((prev) => ({
...prev,
[item.id]: { ...item }
}));
}}
>
<Edit2 className="w-4 h-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-red-600 hover:text-red-700"
onClick={() => handleDeleteDeduction(item.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Add New Deduction */}
<div className="border-t border-red-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">
<Select
value={newDeduction.department}
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}
onChange={(e) => setNewDeduction({ ...newDeduction, description: e.target.value })}
className="col-span-5"
/>
<Input
type="number"
placeholder="Amount"
value={newDeduction.amount}
onChange={(e) => setNewDeduction({ ...newDeduction, amount: e.target.value })}
className="col-span-3"
/>
<Button onClick={handleAddDeduction} className="col-span-1 bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
{/* Total */}
<div className="pt-3 border-t-2 border-red-300">
<div className="flex justify-between items-center">
<span className="text-slate-900">Total Deductions</span>
<span className="text-re-red-hover text-xl">
{settlement.deductions.toLocaleString('en-IN')}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Final Settlement Summary */}
<Card className="border-2 border-blue-300 bg-blue-50">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-re-red" />
Final Settlement Summary
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-white rounded-lg">
<span className="text-slate-900">Total Payables (to Dealer)</span>
<span className="text-green-700 text-lg">+ {settlement.payables.toLocaleString('en-IN')}</span>
</div>
<div className="flex justify-between items-center p-3 bg-white rounded-lg">
<span className="text-slate-900">Total Receivables (from Dealer)</span>
<span className="text-red-700 text-lg">- {settlement.receivables.toLocaleString('en-IN')}</span>
</div>
<div className="flex justify-between items-center p-3 bg-white rounded-lg">
<span className="text-slate-900">Total Deductions</span>
<span className="text-re-red-hover text-lg">- {settlement.deductions.toLocaleString('en-IN')}</span>
</div>
</div>
<div className="h-px bg-blue-300"></div>
<div className={`p-4 rounded-lg border-2 ${
settlement.settlementType === 'Payable to Dealer'
? 'bg-red-100 border-red-400'
: settlement.settlementType === 'Receivable from Dealer'
? 'bg-green-100 border-green-400'
: 'bg-slate-100 border-slate-400'
}`}>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-600 mb-1">Net Settlement</p>
<p className={`text-lg ${
settlement.settlementType === 'Payable to Dealer'
? 'text-red-700'
: settlement.settlementType === 'Receivable from Dealer'
? 'text-green-700'
: 'text-slate-700'
}`}>
{settlement.settlementType}
</p>
</div>
<span className="text-3xl text-slate-900">
{settlement.settlementType === 'No Settlement Required'
? '₹0'
: `${settlement.settlementAmount.toLocaleString('en-IN')}`}
</span>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-white border border-red-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600">
Net Settlement = Payables - Receivables - Deductions<br/>
{settlement.netSettlement > 0 && 'Positive value means company pays to dealer'}
{settlement.netSettlement < 0 && 'Negative value means dealer pays to company'}
{settlement.netSettlement === 0 && 'Zero means no payment required from either party'}
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="departments" className="space-y-4">
{/* Progress Summary */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5" />
Department Response Progress
</CardTitle>
<CardDescription>
{fnfCase.departmentResponses.filter((d: any) => d.status !== 'Pending').length} of {fnfCase.departmentResponses.length} departments have responded
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Progress
value={(fnfCase.departmentResponses.filter((d: any) => d.status !== 'Pending').length / fnfCase.departmentResponses.length) * 100}
className="h-3"
/>
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
<p className="text-sm text-green-700 mb-1">NOC Submitted</p>
<p className="text-2xl text-green-600">
{fnfCase.departmentResponses.filter((d: any) => d.status === 'NOC Submitted').length}
</p>
</div>
<div className="p-4 bg-red-50 rounded-lg border border-red-200">
<p className="text-sm text-red-700 mb-1">Dues Pending</p>
<p className="text-2xl text-red-600">
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues Pending').length}
</p>
</div>
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200">
<p className="text-sm text-slate-700 mb-1">Awaiting Response</p>
<p className="text-2xl text-slate-600">
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Pending').length}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Department Responses Table */}
<Card>
<CardHeader>
<CardTitle>All Department Responses</CardTitle>
<CardDescription>
Status of NOC and dues clearance from all 16 departments (read-only for Finance; updates are done by department stakeholders).
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Department</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount Type</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Submitted Date</TableHead>
<TableHead>Remarks</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fnfCase.departmentResponses.map((dept: any) => (
<TableRow
key={dept.id}
className={
dept.duesFlow === 'recovery'
? 'bg-red-50/40'
: dept.duesFlow === 'payable'
? 'bg-emerald-50/40'
: ''
}
>
<TableCell>{dept.departmentName}</TableCell>
<TableCell>
<Badge className={`border ${
dept.status === 'NOC Submitted' ? 'bg-green-100 text-green-700 border-green-300' :
dept.status === 'Dues Pending' ? 'bg-red-100 text-red-700 border-red-300' :
'bg-slate-100 text-slate-700 border-slate-300'
}`}>
{dept.status}
</Badge>
</TableCell>
<TableCell>
{dept.amountType ? (
<Badge
variant="outline"
className={
dept.duesFlow === 'recovery'
? 'bg-red-100 text-red-900 border-red-400 font-semibold'
: dept.duesFlow === 'payable'
? 'bg-emerald-100 text-emerald-900 border-emerald-400 font-semibold'
: 'bg-slate-50 text-slate-700 border-slate-200'
}
>
{dept.amountType}
</Badge>
) : (
'-'
)}
</TableCell>
<TableCell>
{dept.amount ? (
<span
className={`rounded-md px-2 py-0.5 font-semibold tabular-nums ${
dept.duesFlow === 'recovery'
? 'bg-red-100 text-red-800 ring-1 ring-red-300/70'
: dept.duesFlow === 'payable'
? 'bg-emerald-100 text-emerald-800 ring-1 ring-emerald-300/70'
: 'text-slate-700'
}`}
>
{dept.amount.toLocaleString('en-IN')}
</span>
) : (
'-'
)}
</TableCell>
<TableCell>{dept.submittedDate || '-'}</TableCell>
<TableCell className="max-w-xs truncate">
<div className="flex flex-col gap-1">
<span>{dept.remarks || '-'}</span>
{dept.supportingDocument && (
<button
onClick={() => setPreviewDocument({
fileName: `${dept.departmentName}_Proof`,
filePath: dept.supportingDocument,
documentType: 'Departmental Clearance Proof'
})}
className="flex items-center gap-1 text-[10px] text-re-red hover:underline"
>
<Paperclip className="w-3 h-3" />
View Proof
</button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Important Notes */}
<Card className="bg-blue-50 border-red-200">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div>
<p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p>
<ul className="text-sm text-slate-700 space-y-1">
<li> <strong>NOC Submitted:</strong> Department has no outstanding dues and provided clearance</li>
<li> <strong>Dues Pending:</strong> Department has identified amounts to be recovered or paid</li>
<li> <strong>Pending:</strong> Department has not yet responded to the F&F request</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="documents" className="space-y-4">
{/* Submitted Documents */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Submitted Documents
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{fnfCase.documents.map((doc: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border border-slate-200">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-slate-900">{doc.name}</p>
<p className="text-sm text-slate-500">{doc.size} {doc.type} Uploaded on {doc.uploadedOn}</p>
</div>
</div>
<div className="flex items-center gap-2">
{doc.url && doc.url !== '#' && (
<button
onClick={() => setPreviewDocument({
fileName: doc.name,
filePath: doc.url,
documentType: doc.type
})}
className="text-re-red hover:text-re-red-hover text-[10px] font-semibold flex items-center gap-1"
>
<Paperclip className="w-3 h-3" /> PREVIEW
</button>
)}
<Button variant="outline" size="sm" onClick={async () => {
if (doc.url && doc.url !== '#') {
try {
const response = await fetch(doc.url);
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = doc.name || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
} catch (e) {
// Fallback if CORS prevents blob fetch
const link = document.createElement('a');
link.href = doc.url;
link.download = doc.name || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
} else {
toast.error('Document URL not available');
}
}}>
Download
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Upload Additional Documents */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5" />
Upload Settlement Verification Documents
</CardTitle>
<CardDescription>
Upload bank receipts, settlement proofs, or any additional documents
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:border-red-300 hover:bg-red-50 transition-colors">
<Upload className="w-8 h-8 text-slate-400 mx-auto mb-2" />
<p className="text-slate-600 mb-2">Click to upload or drag and drop</p>
<p className="text-sm text-slate-500">PDF, DOC, DOCX, PNG, JPG, XLSX (max 10MB)</p>
<input
type="file"
multiple
className="hidden"
id="file-upload"
onChange={handleFileUpload}
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.xlsx,.xls"
/>
<label htmlFor="file-upload">
<Button variant="outline" className="mt-4" asChild>
<span>Choose Files</span>
</Button>
</label>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="bank" className="space-y-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Building className="w-5 h-5" />
Dealer Bank Account Details
</CardTitle>
<CardDescription>
Manage bank accounts for settlement transfer
</CardDescription>
</div>
<Button
size="sm"
className="bg-re-red"
onClick={() => {
setEditingBank(null);
setIsBankModalOpen(true);
}}
>
<Plus className="w-4 h-4 mr-2" />
Add Bank Account
</Button>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{bankDetails.length > 0 ? (
bankDetails.map((bank: any) => (
<Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-re-red bg-blue-50/30' : ''}`}>
{bank.isPrimary && (
<div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
Primary
</div>
)}
<CardContent className="p-4 pt-6">
<div className="space-y-3">
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Holder</Label>
<p className="text-sm font-semibold">{bank.accountHolderName}</p>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Bank</Label>
<p className="text-xs truncate">{bank.bankName}</p>
</div>
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">IFSC</Label>
<p className="text-xs">{bank.ifscCode}</p>
</div>
</div>
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Number</Label>
<p className="text-xs font-mono">{bank.accountNumber}</p>
</div>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<Button
variant="ghost"
size="sm"
className="h-7 text-[11px] text-re-red"
onClick={() => {
setEditingBank(bank);
setIsBankModalOpen(true);
}}
>
<Edit2 className="w-3 h-3 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 text-[11px] text-red-600"
onClick={() => handleDeleteBank(bank.id)}
>
<Trash2 className="w-3 h-3 mr-1" />
Delete
</Button>
</div>
</div>
</CardContent>
</Card>
))
) : (
<div className="col-span-full py-12 text-center border-2 border-dashed rounded-lg bg-slate-50">
<Building className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-600 text-sm">No bank details found</p>
</div>
)}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Right Column - Settlement Verification Form */}
<div className="space-y-6">
<Card className="sticky top-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CreditCard className="w-5 h-5" />
Settlement Verification
</CardTitle>
<CardDescription>
Enter settlement transaction details
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{fnfCase.status === 'Completed' ? (
<div className="space-y-6">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-3 text-green-700 mb-2">
<CheckCircle className="w-5 h-5" />
<span className="font-semibold">Settlement Completed</span>
</div>
<p className="text-sm text-green-600">
This settlement has been finalized and processed.
</p>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Settlement Date</span>
<span className="text-slate-900 font-medium">{formatDateTime(settlementDetails.settlementDate)}</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Payment Mode</span>
<span className="text-slate-900 font-medium">{settlementDetails.paymentMode}</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Transaction ID</span>
<span className="text-slate-900 font-medium truncate ml-4 max-w-[150px]" title={settlementDetails.verificationTransactionId}>
{settlementDetails.verificationTransactionId}
</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Final Amount</span>
<span className="text-slate-900 font-bold text-lg">{parseFloat(settlementDetails.settlementAmount).toLocaleString()}</span>
</div>
</div>
{settlementDetails.verificationRemarks && (
<div className="mt-4">
<Label className="text-slate-500 mb-1 block">Finance Remarks</Label>
<div className="p-3 bg-slate-50 rounded border text-sm text-slate-700">
{settlementDetails.verificationRemarks}
</div>
</div>
)}
<Button variant="outline" className="w-full mt-4" onClick={() => window.print()}>
<FileDown className="w-4 h-4 mr-2" />
Download Settlement Letter
</Button>
</div>
) : (
<>
{/* Settlement Checklist */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4">
<p className="text-sm font-bold text-slate-900 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-re-red" />
Compliance Checklist
</p>
<div className="space-y-3">
{SETTLEMENT_CHECKLIST.map(item => (
<div key={item.id} className="flex items-start gap-3">
<input
type="checkbox"
id={`check-${item.id}`}
checked={checklist.includes(item.id)}
onChange={() => toggleChecklist(item.id)}
className="w-4 h-4 mt-1 rounded border-slate-300 text-re-red focus:ring-re-red"
/>
<label htmlFor={`check-${item.id}`} className="text-sm text-slate-700 leading-tight">
{item.label}
</label>
</div>
))}
</div>
</div>
<div>
<Label htmlFor="paymentMode">
Payment Mode <span className="text-red-500">*</span>
</Label>
<Input
id="paymentMode"
placeholder="e.g., NEFT, RTGS, Cheque"
value={settlementDetails.paymentMode}
onChange={(e) => setSettlementDetails({ ...settlementDetails, paymentMode: e.target.value })}
/>
</div>
<div>
<Label htmlFor="verificationTxnId">
Transaction ID / Reference <span className="text-red-500">*</span>
</Label>
<Input
id="verificationTxnId"
placeholder="Enter transaction reference"
value={settlementDetails.verificationTransactionId}
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationTransactionId: e.target.value })}
/>
</div>
<div>
<Label htmlFor="bankReference">
Bank Reference Number
</Label>
<Input
id="bankReference"
placeholder="Enter bank reference"
value={settlementDetails.bankReference}
onChange={(e) => setSettlementDetails({ ...settlementDetails, bankReference: e.target.value })}
/>
</div>
<div>
<Label htmlFor="settlementAmount">
Settlement Amount () <span className="text-red-500">*</span>
</Label>
<Input
id="settlementAmount"
type="number"
placeholder="Enter settlement amount"
value={settlementDetails.settlementAmount}
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementAmount: e.target.value })}
/>
</div>
<div>
<Label htmlFor="adjustments">
Adjustments ()
</Label>
<Input
id="adjustments"
type="number"
placeholder="Enter any adjustments"
value={settlementDetails.adjustments}
onChange={(e) => {
const adjustments = e.target.value;
const adjustedAmount = settlement.settlementAmount + parseFloat(adjustments || '0');
setSettlementDetails({
...settlementDetails,
adjustments,
settlementAmount: adjustedAmount.toString()
});
}}
/>
{parseFloat(settlementDetails.adjustments) !== 0 && (
<p className="text-sm text-re-red mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Adjusted amount: {settlementDetails.settlementAmount}
</p>
)}
</div>
<div>
<Label htmlFor="settlementDate">
Settlement Date <span className="text-red-500">*</span>
</Label>
<Input
id="settlementDate"
type="date"
value={settlementDetails.settlementDate}
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementDate: e.target.value })}
/>
</div>
<div>
<Label htmlFor="verificationRemarks">Verification Remarks</Label>
<Textarea
id="verificationRemarks"
placeholder="Enter any remarks or notes..."
rows={4}
value={settlementDetails.verificationRemarks}
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationRemarks: e.target.value })}
/>
</div>
<div className="pt-4 space-y-3 border-t">
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={handleApproveSettlement}
disabled={submitting || checklist.length < SETTLEMENT_CHECKLIST.length}
>
{submitting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<CheckCircle className="w-4 h-4 mr-2" />
)}
Complete Settlement
</Button>
{checklist.length < SETTLEMENT_CHECKLIST.length && (
<p className="text-[10px] text-center text-red-500 mt-2 italic">
Check all compliance items to enable settlement
</p>
)}
<Button
variant="outline"
className="w-full border-blue-300 text-re-red hover:bg-blue-50"
onClick={handleRequestClarification}
disabled={submitting}
>
<Send className="w-4 h-4 mr-2" />
Request Clarification
</Button>
<Button
variant="outline"
className="w-full border-red-300 text-red-600 hover:bg-red-50"
onClick={handleRejectSettlement}
disabled={submitting}
>
<XCircle className="w-4 h-4 mr-2" />
Reject Settlement
</Button>
</div>
</>
)}
</CardContent>
</Card>
</div>
</div>
<BankDetailsModal
isOpen={isBankModalOpen}
onClose={() => {
setIsBankModalOpen(false);
setEditingBank(null);
}}
onSubmit={handleUpsertBank}
editingBank={editingBank}
isSubmitting={false}
/>
<DocumentPreviewModal
isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)}
document={previewDocument}
/>
</div>
);
}