2247 lines
100 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|
|
|