added end to end testing files for all modules all midules coverd partially f&F resignation coverd majorly

This commit is contained in:
laxman h 2026-04-09 20:09:00 +05:30
parent c78d97fd31
commit 6542f0fb30
16 changed files with 805 additions and 456 deletions

View File

@ -122,7 +122,9 @@ export const API = {
// Resignation // Resignation
getResignationById: (id: string) => client.get(`/resignation/${id}`), getResignationById: (id: string) => client.get(`/resignation/${id}`),
updateClearance: (id: string, data: any) => client.post(`/resignation/${id}/clearance`, data), updateClearance: (id: string, data: any) => client.put(`/resignation/${id}/clearance`, data, {
headers: data instanceof FormData ? { 'Content-Type': 'multipart/form-data' } : {}
}),
updateResignationStatus: (id: string, data: any) => client.post(`/resignation/${id}/status`, data), updateResignationStatus: (id: string, data: any) => client.post(`/resignation/${id}/status`, data),
// Termination // Termination
@ -144,6 +146,8 @@ export const API = {
createTermination: (data: any) => client.post('/termination', data), createTermination: (data: any) => client.post('/termination', data),
updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data), updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
getOnboardingPayments: () => client.get('/settlement/onboarding'),
updatePayment: (id: string, data: any) => client.put(`/settlement/payments/${id}`, data),
getFnFSettlements: () => client.get('/settlement/fnf'), getFnFSettlements: () => client.get('/settlement/fnf'),
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`), getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`), calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),

View File

@ -1183,18 +1183,17 @@ export const ApplicationDetails = () => {
name: 'Statutory Documents', name: 'Statutory Documents',
color: 'green', color: 'green',
stages: [ stages: [
{ id: '11b-1', name: 'GST Certificate', status: isDocumentUploaded('Statutory GST') || isDocumentUploaded('GST Certificate') ? 'completed' : 'active', description: 'GST details' }, { id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
{ id: '11b-2', name: 'PAN Card', status: isDocumentUploaded('Statutory PAN') || isDocumentUploaded('PAN Card') ? 'completed' : 'active', description: 'PAN details' }, { id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Statutory Nodal') || isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal details' }, { id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Statutory Check') || isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Bank verification' }, { id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
{ id: '11b-5', name: 'Partnership Deed', status: isDocumentUploaded('Statutory Partnership') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Legal constitution' }, { id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
{ id: '11b-6', name: 'Firm Registration', status: isDocumentUploaded('Statutory Firm Reg') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'RoC/Firm reg' }, { id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
{ id: '11b-7', name: 'Virtual Code', status: isDocumentUploaded('Statutory Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Oracle setup' }, { id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
{ id: '11b-8', name: 'Domain ID', status: isDocumentUploaded('Statutory Domain') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Email setup' }, { id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
{ id: '11b-9', name: 'MSD Configuration', status: isDocumentUploaded('Statutory MSD') || isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Multiple Security Deposit' }, { id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
{ id: '11b-10', name: 'LOI Acknowledgement', status: isDocumentUploaded('Statutory LOI Ack') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI Signed copy' }, { id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
{ id: '11b-11', name: 'Board Resolution', status: isDocumentUploaded('Board Resolution') || isDocumentUploaded('Authorization Proof') ? 'completed' : 'active', description: 'Legal authorization' }, { id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' }
{ id: '11b-12', name: 'Consolidated Approval', status: application.statutoryStatus === 'COMPLETED' ? 'completed' : 'active', description: 'Managerial sign-off' }
] ]
} }
] ]

View File

@ -13,6 +13,7 @@ import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { formatDateTime } from '../ui/utils';
interface ConstitutionalChangeDetailsProps { interface ConstitutionalChangeDetailsProps {
requestId: string; requestId: string;
@ -20,34 +21,30 @@ interface ConstitutionalChangeDetailsProps {
currentUser: UserType | null; currentUser: UserType | null;
} }
// Workflow stages as per the process flow // Workflow stages as per the process flow (SRS 12.2.4)
const workflowStages = [ const workflowStages = [
{ id: 1, name: 'Request Created', key: 'created', role: 'Dealer' }, { id: 1, name: 'Submitted', key: 'submitted', role: 'Dealer' },
{ id: 2, name: 'ASM Review', key: 'asm', role: 'ASM' }, { id: 2, name: 'ASM Review', key: 'asm-review', role: 'ASM' },
{ id: 3, name: 'RBM Review', key: 'rbm', role: 'RBM' }, { id: 3, name: 'ZM/RBM Review', key: 'zm-rbm-review', role: 'ZM/RBM' },
{ id: 4, name: 'DD ZM Review', key: 'dd-zm', role: 'DD-ZM' }, { id: 4, name: 'ZBH Review', key: 'zbh-review', role: 'ZBH' },
{ id: 5, name: 'ZBH Review', key: 'zbh', role: 'ZBH' }, { id: 5, name: 'DD Lead Review', key: 'lead-review', role: 'DD Lead' },
{ id: 6, name: 'DD Lead Review', key: 'dd-lead', role: 'DD Lead' }, { id: 6, name: 'DD Head Review', key: 'head-review', role: 'DD Head' },
{ id: 7, name: 'FDD Review', key: 'fdd', role: 'FDD' }, { id: 7, name: 'NBH Approval', key: 'nbh-approval', role: 'NBH' },
{ id: 8, name: 'DD Head Review', key: 'dd-head', role: 'DD Head' }, { id: 8, name: 'Legal Review', key: 'legal-review', role: 'Legal Team' },
{ id: 9, name: 'NBH Review', key: 'nbh', role: 'NBH' }, { id: 9, name: 'Completed', key: 'completed', role: 'System' }
{ id: 10, name: 'Docs Collection by DD H.O', key: 'docs-collection', role: 'DD H.O' },
{ id: 11, name: 'New Code Creation', key: 'code-creation', role: 'DD Admin' },
{ id: 12, name: 'New LOA Issuance', key: 'loa-issuance', role: 'DD Admin' },
{ id: 13, name: 'Closure of Request', key: 'closure', role: 'System' }
]; ];
// Document requirements mapping (same as in ConstitutionalChangePage) // Document requirements mapping (same as in ConstitutionalChangePage)
const documentRequirements: Record<string, number[]> = { const documentRequirements: Record<string, number[]> = {
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16], 'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
'LLP': [1, 2, 3, 7, 8, 9, 10, 16], 'LLP': [1, 2, 3, 7, 8, 9, 10, 11, 16],
'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16],
'Proprietorship': [1, 2, 3, 10, 16] 'Proprietorship': [1, 2, 3, 10, 16]
}; };
const documentNames: Record<number, string> = { const documentNames: Record<number, string> = {
1: 'GST', 1: 'GST Certificate',
2: 'Firm Pan Copy', 2: 'Firm PAN Copy',
3: 'Self attested KYC\'s', 3: 'Self attested KYC\'s',
4: 'Partnership Agreement (Notarised)', 4: 'Partnership Agreement (Notarised)',
5: 'MOA (Applicable for Only Pvt.Ltd)', 5: 'MOA (Applicable for Only Pvt.Ltd)',
@ -71,6 +68,7 @@ const getTypeColor = (type: string) => {
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300'; case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300'; case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300';
case 'LLP': return 'bg-indigo-100 text-indigo-700 border-indigo-300'; case 'LLP': return 'bg-indigo-100 text-indigo-700 border-indigo-300';
case 'Private Limited':
case 'Pvt Ltd': return 'bg-cyan-100 text-cyan-700 border-cyan-300'; case 'Pvt Ltd': return 'bg-cyan-100 text-cyan-700 border-cyan-300';
default: return 'bg-slate-100 text-slate-700 border-slate-300'; default: return 'bg-slate-100 text-slate-700 border-slate-300';
} }
@ -78,7 +76,7 @@ const getTypeColor = (type: string) => {
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
if (status === 'Completed' || status === 'Verified') return 'bg-green-100 text-green-700 border-green-300'; if (status === 'Completed' || status === 'Verified') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300'; if (status.includes('Review') || status.includes('Pending') || status === 'In Progress' || status === 'Submitted') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300'; if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
return 'bg-slate-100 text-slate-700 border-slate-300'; return 'bg-slate-100 text-slate-700 border-slate-300';
}; };
@ -136,20 +134,18 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
// Get required documents for this request // Get required documents for this request
const requiredDocs = documentRequirements[request.changeType] || []; const requiredDocs = documentRequirements[request.changeType] || [];
// Calculate current stage index // Calculate current stage index mapping to backend stages
const getCurrentStageIndex = () => { const getCurrentStageIndex = () => {
const stageMap: Record<string, number> = { const stageMap: Record<string, number> = {
'Dealer': 1, 'Submitted': 1,
'ASM': 2, 'ASM Review': 2,
'RBM': 3, 'ZM/RBM Review': 3,
'DD-ZM': 4, 'ZBH Review': 4,
'ZBH': 5, 'DD Lead Review': 5,
'DD Lead': 6, 'DD Head Review': 6,
'FDD': 7, 'NBH Approval': 7,
'DD Head': 8, 'Legal Review': 8,
'NBH': 9, 'Completed': 9
'DD H.O': 10,
'Closed': 13
}; };
return stageMap[request.currentStage] || 1; return stageMap[request.currentStage] || 1;
}; };
@ -243,7 +239,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
</div> </div>
<div> <div>
<p className="text-slate-600 text-sm mb-1">Request Information</p> <p className="text-slate-600 text-sm mb-1">Request Information</p>
<p className="text-slate-900 text-sm">Submitted: {new Date(request.createdAt).toLocaleDateString()}</p> <p className="text-slate-900 text-sm">Submitted: {formatDateTime(request.createdAt)}</p>
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName || 'Dealer'}</p> <p className="text-slate-600 text-sm">By: {request.dealer?.fullName || 'Dealer'}</p>
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage}</p> <p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage}</p>
</div> </div>
@ -469,7 +465,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
{doc.fileName || doc.name} {doc.fileName || doc.name}
</TableCell> </TableCell>
<TableCell className="text-slate-600"> <TableCell className="text-slate-600">
{new Date(doc.uploadedOn || doc.createdAt).toLocaleDateString()} {formatDateTime(doc.uploadedOn || doc.createdAt)}
</TableCell> </TableCell>
<TableCell className="text-slate-600"> <TableCell className="text-slate-600">
{doc.uploadedBy || 'Dealer'} {doc.uploadedBy || 'Dealer'}
@ -533,7 +529,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
</Badge> </Badge>
</div> </div>
<p className="text-slate-600 text-sm mt-2">{entry.comments || entry.remarks || 'No remarks provided'}</p> <p className="text-slate-600 text-sm mt-2">{entry.comments || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-500 text-sm mt-1">{new Date(entry.date || entry.createdAt || entry.timestamp).toLocaleString()}</p> <p className="text-slate-500 text-sm mt-1">{formatDateTime(entry.date || entry.createdAt || entry.timestamp)}</p>
</div> </div>
</div> </div>
))} ))}

View File

@ -13,6 +13,7 @@ import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data'; import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils';
interface ConstitutionalChangePageProps { interface ConstitutionalChangePageProps {
currentUser: UserType | null; currentUser: UserType | null;
@ -465,7 +466,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div> <div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
<div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div> <div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
@ -677,7 +678,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div> <div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <Button

View File

@ -30,9 +30,17 @@ import {
Plus, Plus,
Edit2, Edit2,
Trash2, Trash2,
Save Save,
Paperclip
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
const ALL_DEPARTMENTS = [
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
'Legal', 'Quality', 'Logistics', 'Customer Relations'
];
interface FinanceFnFDetailsPageProps { interface FinanceFnFDetailsPageProps {
fnfId: string; fnfId: string;
@ -57,6 +65,24 @@ const getDepartmentStatusColor = (status: string) => {
} }
}; };
const formatDateTime = (dateString: any) => {
if (!dateString) return '-';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return '-';
return date.toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
} catch (e) {
return '-';
}
};
interface FinancialLineItem { interface FinancialLineItem {
id: string; id: string;
department: string; department: string;
@ -72,6 +98,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]); const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]); const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]); const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
const [previewDocument, setPreviewDocument] = useState<any>(null);
useEffect(() => { useEffect(() => {
fetchFnFDetails(); fetchFnFDetails();
@ -83,11 +110,11 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const response = await API.getFnFSettlementById(fnfId); const response = await API.getFnFSettlementById(fnfId);
const data = response.data as any; const data = response.data as any;
if (data.success) { if (data.success) {
const s = data.settlement; const s = data.fnf;
setFnfCase({ setFnfCase({
id: s.id, id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(), caseNumber: s.id.substring(0, 8).toUpperCase(),
dealerName: s.outlet?.dealer?.name || 'N/A', dealerName: s.outlet?.dealer?.fullName || s.outlet?.name || 'N/A',
dealerCode: s.outlet?.code || 'N/A', dealerCode: s.outlet?.code || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A', location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination', terminationType: s.resignationId ? 'Resignation' : 'Termination',
@ -95,23 +122,40 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD', dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD',
status: s.status, status: s.status,
bankDetails: { bankDetails: {
accountName: s.outlet?.dealer?.name || 'N/A', accountName: s.outlet?.dealer?.fullName || 'N/A',
accountNumber: 'N/A', // These should come from dealer model in a real app accountNumber: 'N/A', // These should come from dealer model in a real app
ifscCode: 'N/A', ifscCode: 'N/A',
bankName: 'N/A', bankName: 'N/A',
branch: 'N/A' branch: 'N/A'
}, },
departmentResponses: (s.lineItems || []).map((li: any) => ({ departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
id: li.id, const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
departmentName: li.department, const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
status: 'Submitted', const totalAmount = relatedItems.reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount)), 0);
remarks: li.remarks, const hasPayable = relatedItems.some((li: any) => li.amount < 0);
amount: Math.abs(li.amount),
amountType: li.amount < 0 ? 'Payable Amount' : 'Recovery Amount' return {
})), id: c?.id || `dept-${deptName}`,
departmentName: deptName,
status: c?.status || 'Pending',
remarks: c?.remarks || '-',
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : '-',
amount: totalAmount,
amountType: hasPayable ? 'Payable Amount' : totalAmount > 0 ? 'Recovery Amount' : null,
supportingDocument: c?.supportingDocument || null
};
}),
documents: [ documents: [
{ name: 'Resignation Letter.pdf', size: '245 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Resignation' }, { name: 'Resignation Letter.pdf', size: 'N/A', uploadedOn: formatDateTime(s.createdAt), type: 'Resignation', url: '#' },
{ name: 'Inventory Report.xlsx', size: '856 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Inventory' } ...(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
}))
] ]
}); });
@ -1254,7 +1298,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</TableCell> </TableCell>
<TableCell> <TableCell>
{dept.amountType ? ( {dept.amountType ? (
<Badge variant={dept.amountType === 'Recovery Amount' ? 'destructive' : 'default'}> <Badge
variant="outline"
className={dept.amountType === 'Recovery'
? 'bg-red-50 text-red-700 border-red-200'
: 'bg-green-50 text-green-700 border-green-200'}
>
{dept.amountType} {dept.amountType}
</Badge> </Badge>
) : ( ) : (
@ -1263,7 +1312,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</TableCell> </TableCell>
<TableCell> <TableCell>
{dept.amount ? ( {dept.amount ? (
<span className={dept.amountType === 'Recovery Amount' ? 'text-red-600' : 'text-green-600'}> <span className={`${dept.amountType === 'Recovery' ? 'text-red-600 font-bold' : 'text-green-600 font-bold'}`}>
{dept.amount.toLocaleString('en-IN')} {dept.amount.toLocaleString('en-IN')}
</span> </span>
) : ( ) : (
@ -1271,7 +1320,24 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
)} )}
</TableCell> </TableCell>
<TableCell>{dept.submittedDate || '-'}</TableCell> <TableCell>{dept.submittedDate || '-'}</TableCell>
<TableCell className="max-w-xs truncate">{dept.remarks || '-'}</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-blue-600 hover:underline"
>
<Paperclip className="w-3 h-3" />
View Proof
</button>
)}
</div>
</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@ -1606,6 +1672,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card> </Card>
</div> </div>
</div> </div>
<DocumentPreviewModal
isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)}
document={previewDocument}
/>
</div> </div>
); );
} }

View File

@ -76,6 +76,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
const getMappedData = (s: any) => ({ const getMappedData = (s: any) => ({
id: s.id, id: s.id,
caseId: s.resignation?.resignationId || s.id,
dealerCode: s.outlet?.code || 'N/A', dealerCode: s.outlet?.code || 'N/A',
dealerName: s.outlet?.dealer?.name || 'N/A', dealerName: s.outlet?.dealer?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A', location: s.outlet?.city || s.outlet?.location || 'N/A',
@ -201,13 +202,18 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-600">Net Receivable</CardTitle> <CardTitle className="text-sm text-slate-600">Net Financial Position</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">2.5L</div> <div className={`text-2xl ${displaySettlements.reduce((sum, s) => sum + (s.financialData.netAmount || 0), 0) < 0 ? 'text-green-600' : 'text-red-600'}`}>
{Math.abs(displaySettlements.reduce((sum, s) => sum + (s.financialData.netAmount || 0), 0)).toLocaleString('en-IN')}
</div>
<TrendingUp className="w-8 h-8 text-purple-600" /> <TrendingUp className="w-8 h-8 text-purple-600" />
</div> </div>
<p className="text-[10px] text-slate-500 mt-1">
{displaySettlements.reduce((sum, s) => sum + (s.financialData.netAmount || 0), 0) < 0 ? 'Net Recovery' : 'Net Payable'}
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -264,7 +270,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<TableRow key={fnfCase.id}> <TableRow key={fnfCase.id}>
<TableCell> <TableCell>
<div> <div>
<div className="text-slate-900">{fnfCase.id}</div> <div className="text-slate-900">{fnfCase.caseId}</div>
<div className="text-sm text-slate-500">{fnfCase.dealerCode}</div> <div className="text-sm text-slate-500">{fnfCase.dealerCode}</div>
</div> </div>
</TableCell> </TableCell>

View File

@ -8,10 +8,11 @@ import { Label } from '../ui/label';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { Progress } from '../ui/progress'; import { Progress } from '../ui/progress';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { User, mockDocuments, mockAuditLogs } from '../../lib/mock-data'; import { User, mockAuditLogs } from '../../lib/mock-data';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
interface FnFDetailsProps { interface FnFDetailsProps {
fnfId: string; fnfId: string;
@ -24,6 +25,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const [fnfCase, setFnfCase] = useState<any>(null); const [fnfCase, setFnfCase] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false); const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
const [previewDocument, setPreviewDocument] = useState<any>(null);
useEffect(() => { useEffect(() => {
fetchFnFDetails(); fetchFnFDetails();
@ -35,14 +37,14 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const response = await API.getFnFSettlementById(fnfId); const response = await API.getFnFSettlementById(fnfId);
const data = response.data as any; const data = response.data as any;
if (data.success) { if (data.success) {
const s = data.settlement; const s = data.fnf;
// Map backend data to UI format // Map backend data to UI format
const mappedCase = { const mappedCase = {
id: s.id, id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(), caseNumber: s.resignation?.resignationId || s.id,
status: s.status, status: s.status,
requestType: s.resignationId ? 'Resignation' : 'Termination', requestType: s.resignationId ? 'Resignation' : 'Termination',
dealerName: s.outlet?.dealer?.name || 'N/A', dealerName: s.outlet?.dealer?.fullName || 'N/A',
dealerCode: s.outlet?.code || 'N/A', dealerCode: s.outlet?.code || 'N/A',
dealershipName: s.outlet?.name || 'N/A', dealershipName: s.outlet?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A', location: s.outlet?.city || s.outlet?.location || 'N/A',
@ -51,19 +53,43 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
lastOperationalDateSales: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A', lastOperationalDateSales: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
lastOperationalDateServices: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A', lastOperationalDateServices: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
typeOfClosure: s.resignationId ? 'Voluntary' : 'Involuntary', typeOfClosure: s.resignationId ? 'Voluntary' : 'Involuntary',
gst: s.outlet?.dealer?.pan || 'N/A', // Using PAN as placeholder if GST not available gst: s.outlet?.dealer?.dealerProfile?.gstNumber || 'N/A',
pan: s.outlet?.dealer?.dealerProfile?.panNumber || 'N/A',
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending', financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
totalPayableAmount: parseFloat(s.totalPayables) || 0, totalPayableAmount: parseFloat(s.totalPayables) || 0,
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0, totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
departmentResponses: (s.lineItems || []).map((li: any) => ({ departmentResponses: [
id: li.id, 'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
departmentName: li.department, 'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
status: li.remarks && li.remarks.toLowerCase().includes('no dues') ? 'No Dues' : (li.amount > 0 ? 'Dues' : 'Pending'), 'Legal', 'Quality', 'Logistics', 'Customer Relations'
amountType: li.amount > 0 ? 'Recovery Amount' : null, ].map((deptName: string) => {
amount: Math.abs(parseFloat(li.amount)) || 0, const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
submittedDate: li.updatedAt ? new Date(li.updatedAt).toLocaleDateString() : null, const lineItem = (s.lineItems || []).find((li: any) => li.department === deptName);
remarks: li.remarks
})) return {
id: c?.id || `dept-${deptName}`,
departmentName: deptName,
status: c?.status || 'Pending',
amountType: lineItem ? (parseFloat(lineItem.amount) > 0 ? 'Recovery' : 'Payable') : null,
amount: lineItem ? Math.abs(parseFloat(lineItem.amount)) : 0,
submittedDate: c?.clearedAt ? new Date(c.clearedAt).toLocaleString() : null,
remarks: c?.remarks || '-',
supportingDocument: c?.supportingDocument || null
};
}),
documents: [
{ id: 'res-letter', name: 'Resignation Letter.pdf', type: 'Resignation', uploadDate: new Date(s.createdAt).toLocaleDateString(), status: 'Verified', url: '#' },
...(s.clearances || [])
.filter((c: any) => c.supportingDocument)
.map((c: any) => ({
id: c.id,
name: c.supportingDocument.split('/').pop(),
type: `${c.department} Proof`,
uploadDate: new Date(c.clearedAt).toLocaleDateString(),
status: 'Attached',
url: c.supportingDocument
}))
]
}; };
setFnfCase(mappedCase); setFnfCase(mappedCase);
} }
@ -128,9 +154,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const getDepartmentStatusColor = (status: string) => { const getDepartmentStatusColor = (status: string) => {
switch (status) { switch (status) {
case 'No Dues': case 'NOC Submitted':
return 'bg-green-100 text-green-700 border-green-300'; return 'bg-green-100 text-green-700 border-green-300';
case 'Dues': case 'Dues Pending':
return 'bg-red-100 text-red-700 border-red-300'; return 'bg-red-100 text-red-700 border-red-300';
case 'Pending': case 'Pending':
return 'bg-slate-100 text-slate-700 border-slate-300'; return 'bg-slate-100 text-slate-700 border-slate-300';
@ -169,7 +195,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
onClick={() => navigate(`/worknotes/fnf/${fnfId}`, { onClick={() => navigate(`/worknotes/fnf/${fnfId}`, {
state: { state: {
applicationName: fnfCase.dealerName || 'F&F Settlement', applicationName: fnfCase.dealerName || 'F&F Settlement',
registrationNumber: fnfId || '', registrationNumber: fnfCase.caseNumber || '',
participants: fnfCase.participants || [] participants: fnfCase.participants || []
} }
})} })}
@ -200,15 +226,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Progress value={progressPercentage} className="h-3" /> <Progress value={progressPercentage} className="h-3" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
<div> <div>
<p className="text-slate-600 text-sm">No Dues</p> <p className="text-slate-600 text-sm">NOC Submitted</p>
<p className="text-2xl text-green-600"> <p className="text-2xl text-green-600">
{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length} {fnfCase.departmentResponses.filter((d: any) => d.status === 'NOC Submitted').length}
</p> </p>
</div> </div>
<div> <div>
<p className="text-slate-600 text-sm">Dues</p> <p className="text-slate-600 text-sm">Dues Pending</p>
<p className="text-2xl text-red-600"> <p className="text-2xl text-red-600">
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length} {fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues Pending').length}
</p> </p>
</div> </div>
<div> <div>
@ -351,12 +377,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Progress value={progressPercentage} className="h-2" /> <Progress value={progressPercentage} className="h-2" />
<div className="grid grid-cols-3 gap-3 text-sm"> <div className="grid grid-cols-3 gap-3 text-sm">
<div className="text-center p-2 bg-green-100 rounded"> <div className="text-center p-2 bg-green-100 rounded">
<p className="text-green-700">No Dues</p> <p className="text-green-700">NOC Submitted</p>
<p className="text-green-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length}</p> <p className="text-green-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'NOC Submitted').length}</p>
</div> </div>
<div className="text-center p-2 bg-red-100 rounded"> <div className="text-center p-2 bg-red-100 rounded">
<p className="text-red-700">Dues</p> <p className="text-red-700">Dues Pending</p>
<p className="text-red-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length}</p> <p className="text-red-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues Pending').length}</p>
</div> </div>
<div className="text-center p-2 bg-slate-100 rounded"> <div className="text-center p-2 bg-slate-100 rounded">
<p className="text-slate-700">Pending</p> <p className="text-slate-700">Pending</p>
@ -767,7 +793,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableCell> </TableCell>
<TableCell> <TableCell>
{dept.amountType ? ( {dept.amountType ? (
<Badge variant={dept.amountType === 'Recovery Amount' ? 'destructive' : 'default'}> <Badge
variant="outline"
className={dept.amountType === 'Recovery'
? 'bg-red-50 text-red-700 border-red-200'
: 'bg-green-50 text-green-700 border-green-200'}
>
{dept.amountType} {dept.amountType}
</Badge> </Badge>
) : ( ) : (
@ -776,7 +807,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableCell> </TableCell>
<TableCell> <TableCell>
{dept.amount ? ( {dept.amount ? (
<span className={dept.amountType === 'Recovery Amount' ? 'text-red-600' : 'text-green-600'}> <span className={`${dept.amountType === 'Recovery' ? 'text-red-600 font-bold' : 'text-green-600 font-bold'}`}>
{dept.amount.toLocaleString()} {dept.amount.toLocaleString()}
</span> </span>
) : ( ) : (
@ -890,7 +921,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{mockDocuments.map((doc: any) => ( {fnfCase.documents.map((doc: any) => (
<TableRow key={doc.id}> <TableRow key={doc.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -906,7 +937,23 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button size="sm" variant="outline">View</Button> <Button
size="sm"
variant="outline"
onClick={() => {
const path = doc.url;
const fullPath = path.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
? path.replace('/uploads/', '/uploads/documents/')
: path;
setPreviewDocument({
fileName: doc.name,
filePath: fullPath,
documentType: doc.type
});
}}
>
View
</Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@ -980,6 +1027,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<DocumentPreviewModal
isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)}
document={previewDocument}
/>
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send } from 'lucide-react'; import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, Upload, Eye } from 'lucide-react';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
@ -15,6 +15,14 @@ import { toast } from 'sonner';
import { resignationService } from '../../services/resignation.service'; import { resignationService } from '../../services/resignation.service';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
const ALL_DEPARTMENTS = [
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
'Legal', 'Quality', 'Logistics', 'Customer Relations'
];
interface ResignationDetailsProps { interface ResignationDetailsProps {
resignationId: string; resignationId: string;
@ -29,13 +37,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [assignToUser, setAssignToUser] = useState(''); const [assignToUser, setAssignToUser] = useState('');
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] }); const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
const [showClearanceDialog, setShowClearanceDialog] = useState(false); const [showClearanceDialog, setShowClearanceDialog] = useState(false);
const [selectedDept, setSelectedDept] = useState<string | null>(null); const [selectedDept, setSelectedDept] = useState('');
const [clearanceStatus, setClearanceStatus] = useState<'Cleared' | 'Pending' | 'Rejected'>('Cleared'); const [clearanceStatus, setClearanceStatus] = useState<any>('Pending');
const [clearanceRemarks, setClearanceRemarks] = useState(''); const [clearanceRemarks, setClearanceRemarks] = useState('');
const [clearanceAmount, setClearanceAmount] = useState<number>(0);
const [clearanceType, setClearanceType] = useState<'Payable' | 'Recovery'>('Recovery');
const [clearanceFile, setClearanceFile] = useState<File | null>(null);
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false); const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
const [resignationData, setResignationData] = useState<any>(null); // Real data from API const [resignationData, setResignationData] = useState<any>(null); // Real data from API
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [previewDocument, setPreviewDocument] = useState<any>(null);
const fetchResignation = async () => { const fetchResignation = async () => {
try { try {
@ -58,25 +70,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
// Progress stages logic based on live data // Progress stages logic based on live data
const progressStages = [ const progressStages = [
{ id: 1, name: 'Request Submitted', key: 'Submitted', description: 'Resignation request created' }, { id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
{ id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' }, { id: 2, name: 'RBM Review', key: 'RBM', description: 'Regional Business Manager evaluation' },
{ id: 3, name: 'Departmental Clearances', key: 'Clearance', description: 'Clearance from departments' }, { id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
{ id: 4, name: 'RBM + DD ZM Review', key: 'RBM', description: 'Regional Business Manager and DD ZM evaluation' }, { id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
{ id: 5, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' }, { id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
{ id: 6, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' }, { id: 6, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification' },
{ id: 7, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' }, { id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
{ id: 8, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' } { id: 8, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
]; ];
const getStageStatus = (stageKey: string) => { const getStageStatus = (stageKey: string) => {
if (!resignationData) return 'pending'; if (!resignationData) return 'pending';
const currentStage = resignationData.currentStage; const currentStage = resignationData.currentStage;
// Simple logic for simulation - in real app, this would be more complex const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
const stagesOrdered = ['Submitted', 'ASM', 'Clearance', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal'];
const currentIndex = stagesOrdered.indexOf(currentStage); const currentIndex = stagesOrdered.indexOf(currentStage);
const stageIndex = stagesOrdered.indexOf(stageKey); const stageIndex = stagesOrdered.indexOf(stageKey);
if (currentIndex === -1) return 'pending'; // Fallback for rejected/other states
if (stageIndex < currentIndex) return 'completed'; if (stageIndex < currentIndex) return 'completed';
if (stageIndex === currentIndex) return 'active'; if (stageIndex === currentIndex) return 'active';
return 'pending'; return 'pending';
@ -129,16 +142,25 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
if (!selectedDept) return; if (!selectedDept) return;
try { try {
setIsUpdatingClearance(true); setIsUpdatingClearance(true);
await resignationService.updateClearance(resignationId, {
department: selectedDept, const formData = new FormData();
status: clearanceStatus, formData.append('department', selectedDept);
remarks: clearanceRemarks formData.append('status', clearanceStatus);
}); formData.append('remarks', clearanceRemarks);
toast.success(`${selectedDept} clearance updated`); formData.append('amount', String(clearanceAmount));
formData.append('type', clearanceType);
if (clearanceFile) {
formData.append('file', clearanceFile);
}
await resignationService.updateClearance(resignationId, formData);
toast.success(`Successfully updated clearance for ${selectedDept}`);
setShowClearanceDialog(false); setShowClearanceDialog(false);
setClearanceFile(null);
fetchResignation(); fetchResignation();
} catch (error) { } catch (error) {
toast.error('Failed to update clearance'); toast.error('Failed to update clearance status');
} finally { } finally {
setIsUpdatingClearance(false); setIsUpdatingClearance(false);
} }
@ -161,7 +183,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
</Button> </Button>
<div> <div>
<h1 className="text-2xl">{resignationId}</h1> <h1 className="text-2xl">{resignationData?.resignationId || resignationId}</h1>
<p className="text-slate-600">{resignationData?.outlet?.name}</p> <p className="text-slate-600">{resignationData?.outlet?.name}</p>
</div> </div>
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300"> <Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
@ -216,7 +238,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{/* Secondary Actions */} {/* Secondary Actions */}
{currentUser?.role !== 'Dealer' && ( {currentUser?.role !== 'Dealer' && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canPushToFnF && ( {canPushToFnF && resignationData?.status !== 'FNF_INITIATED' && resignationData?.status !== 'Settled' && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -365,11 +387,11 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<Label className="text-slate-600">Last Operational Date (Sales)</Label> <Label className="text-slate-600">Last Operational Date (Sales)</Label>
<p>{resignationData?.lastOperationalDateSales ? new Date(resignationData.lastOperationalDateSales).toLocaleDateString() : 'N/A'}</p> <p>{resignationData?.lastOperationalDateSales ? formatDateTime(resignationData.lastOperationalDateSales, 'date') : 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Last Operational Date (Services)</Label> <Label className="text-slate-600">Last Operational Date (Services)</Label>
<p>{resignationData?.lastOperationalDateServices ? new Date(resignationData.lastOperationalDateServices).toLocaleDateString() : 'N/A'}</p> <p>{resignationData?.lastOperationalDateServices ? formatDateTime(resignationData.lastOperationalDateServices, 'date') : 'N/A'}</p>
</div> </div>
</div> </div>
<div> <div>
@ -379,7 +401,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<Label className="text-slate-600">Submitted On</Label> <Label className="text-slate-600">Submitted On</Label>
<p>{resignationData?.submittedOn ? new Date(resignationData.submittedOn).toLocaleDateString() : 'N/A'}</p> <p>{resignationData?.submittedOn ? formatDateTime(resignationData.submittedOn) : 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Current Stage</Label> <Label className="text-slate-600">Current Stage</Label>
@ -434,7 +456,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{timelineEntry && ( {timelineEntry && (
<div className="flex items-center gap-1 text-sm text-slate-600"> <div className="flex items-center gap-1 text-sm text-slate-600">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
<span>{new Date(timelineEntry.timestamp || timelineEntry.createdAt).toLocaleDateString()}</span> <span>{formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}</span>
</div> </div>
)} )}
</div> </div>
@ -472,31 +494,91 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{(resignationData?.clearances || []).map((clearance: any) => ( {ALL_DEPARTMENTS.map((dept) => {
<Card key={clearance.department} className="border border-slate-200"> const settlement = resignationData?.settlement;
const fffClearance = (settlement?.clearances || []).find((c: any) => c.department === dept);
const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => li.department === dept);
const lineItemAmount = relatedLineItems.reduce((sum: number, li: any) => sum + parseFloat(li.amount || 0), 0);
// Use standardized JSON field but override with live F&F data if available
const jsonClearance = (resignationData?.departmentalClearances || {})[dept] || { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' };
const displayStatus = fffClearance ? (fffClearance.status === 'NOC Submitted' ? 'Cleared' : fffClearance.status === 'Pending' ? 'Pending' : 'Dues') : jsonClearance.status;
const displayRemarks = fffClearance ? fffClearance.remarks : jsonClearance.remarks;
const displayAmount = fffClearance ? Math.abs(lineItemAmount) : jsonClearance.amount;
const displayType = fffClearance ? (lineItemAmount < 0 ? 'Payable' : 'Recovery') : jsonClearance.type;
return (
<Card key={dept} className="border border-slate-200">
<CardHeader className="pb-2 flex flex-row items-center justify-between"> <CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium">{clearance.department}</CardTitle> <CardTitle className="text-base font-medium capitalize">{dept}</CardTitle>
<Badge className={ <Badge className={
clearance.status === 'Cleared' || clearance.status === 'Approved' ? 'bg-green-100 text-green-700 hover:bg-green-100' : displayStatus === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
clearance.status === 'Rejected' ? 'bg-red-100 text-red-700 hover:bg-red-100' : displayStatus === 'Dues' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100' 'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
}> }>
{clearance.status} {displayStatus || 'Pending'}
</Badge> </Badge>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-sm text-slate-600 line-clamp-2 min-h-[2.5rem]"> <div className="space-y-2">
{clearance.remarks || 'No remarks provided'} <div className="flex justify-between text-xs text-slate-500">
</p> <span>Amount: {(displayAmount || 0).toLocaleString()}</span>
{currentUser && (currentUser.role === 'Super Admin' || currentUser.role === 'DD Admin' || (currentUser.role.includes(clearance.department) && resignationData?.currentStage === 'Clearance')) && ( <span className={displayType === 'Recovery' ? 'text-red-600' : 'text-green-600'}>
{displayType || 'Recovery'}
</span>
</div>
<div className="flex flex-col gap-2">
<p className="text-sm text-slate-600 line-clamp-3 min-h-[3.5rem]">
{displayRemarks || 'Awaiting departmental verification.'}
</p>
{fffClearance?.supportingDocument && (
<div className="pt-2 border-t border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center border border-blue-100">
<FileText className="w-4 h-4 text-blue-500" />
</div>
<div>
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-tight leading-none mb-1">Evidence Attached</p>
<span className="text-[10px] text-slate-500 truncate max-w-[100px] block">
{fffClearance.supportingDocument.split('/').pop()?.substring(0, 12)}...
</span>
</div>
</div>
<button
onClick={() => {
const path = fffClearance.supportingDocument;
const fullPath = path.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
? path.replace('/uploads/', '/uploads/documents/')
: path;
setPreviewDocument({
fileName: `${dept}_Proof`,
filePath: fullPath,
documentType: 'Clearance Proof'
});
}}
className="flex items-center gap-1.5 text-xs text-blue-600 hover:text-blue-700 hover:underline font-bold bg-blue-50/50 px-2.5 py-1.5 rounded-md border border-blue-100/50 transition-colors"
>
<Eye className="w-3.5 h-3.5" />
Preview
</button>
</div>
)}
</div>
</div>
{currentUser && (currentUser.role === 'Super Admin' || currentUser.role === 'DD Admin' || (currentUser.role.includes(dept) && resignationData?.currentStage === 'Clearance' || resignationData?.currentStage === 'F&F Initiated')) && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="mt-2 text-blue-600 hover:text-blue-700 p-0" className="mt-2 text-blue-600 hover:text-blue-700 p-0"
onClick={() => { onClick={() => {
setSelectedDept(clearance.department); setSelectedDept(dept);
setClearanceStatus(clearance.status); setClearanceStatus(displayStatus || 'Pending');
setClearanceRemarks(clearance.remarks); setClearanceRemarks(displayRemarks || '');
setClearanceAmount(displayAmount || 0);
setClearanceType(displayType || 'Recovery');
setClearanceFile(null);
setShowClearanceDialog(true); setShowClearanceDialog(true);
}} }}
> >
@ -505,7 +587,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
)} )}
</CardContent> </CardContent>
</Card> </Card>
))} );
})}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -530,8 +613,36 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{(resignationData?.documents || []).length > 0 ? ( {(() => {
(resignationData.documents || []).map((doc: any, index: number) => ( const allDocs = [
...(resignationData?.documents || []),
...(resignationData?.uploadedDocuments || [])
];
// Add clearance documents
if (resignationData?.departmentalClearances) {
Object.entries(resignationData.departmentalClearances).forEach(([dept, data]: [string, any]) => {
if (data.supportingDocument) {
allDocs.push({
name: `${dept} Clearance Proof`,
type: 'Clearance NOC',
path: data.supportingDocument,
createdAt: data.updatedAt,
uploadedBy: data.updatedBy || 'Department Admin'
});
}
});
}
if (allDocs.length === 0) return (
<TableRow>
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
No documents found
</TableCell>
</TableRow>
);
return allDocs.map((doc: any, index: number) => (
<TableRow key={index}> <TableRow key={index}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -539,21 +650,33 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<span>{doc.name || doc.fileName}</span> <span>{doc.name || doc.fileName}</span>
</div> </div>
</TableCell> </TableCell>
<TableCell>{doc.type || 'Document'}</TableCell> <TableCell>{doc.documentType || doc.type || 'Document'}</TableCell>
<TableCell>{doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : 'N/A'}</TableCell> <TableCell>{doc.createdAt ? formatDateTime(doc.createdAt) : 'N/A'}</TableCell>
<TableCell>{doc.uploadedBy || 'Dealer'}</TableCell> <TableCell>{doc.uploader?.fullName || doc.uploadedBy || 'Dealer'}</TableCell>
<TableCell> <TableCell>
<Button size="sm" variant="outline">View</Button> <Button
size="sm"
variant="outline"
onClick={() => {
const path = doc.filePath || doc.path;
const fullPath = path?.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
? path.replace('/uploads/', '/uploads/documents/')
: path;
setPreviewDocument({
fileName: doc.fileName || doc.name,
filePath: fullPath,
documentType: doc.documentType || doc.type
});
}}
>
View
</Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))
) : ( })()
<TableRow> }
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
No documents found
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</CardContent> </CardContent>
@ -576,7 +699,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="font-medium text-slate-900">{log.action || log.status}</p> <p className="font-medium text-slate-900">{log.action || log.status}</p>
<span className="text-sm text-slate-600">{new Date(log.timestamp || log.createdAt).toLocaleString()}</span> <span className="text-sm text-slate-600">{formatDateTime(log.timestamp || log.createdAt)}</span>
</div> </div>
<p className="text-sm text-slate-600">{log.user || log.actor}</p> <p className="text-sm text-slate-600">{log.user || log.actor}</p>
{log.comments && <p className="text-sm text-slate-500 mt-1">{log.comments}</p>} {log.comments && <p className="text-sm text-slate-500 mt-1">{log.comments}</p>}
@ -765,10 +888,34 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]"> <SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]">
<SelectItem value="Cleared" className="text-green-600 focus:bg-green-50">Cleared</SelectItem> <SelectItem value="Cleared" className="text-green-600 focus:bg-green-50">Cleared</SelectItem>
<SelectItem value="Pending" className="text-yellow-600 focus:bg-yellow-50">Pending / In-Review</SelectItem> <SelectItem value="Pending" className="text-yellow-600 focus:bg-yellow-50">Pending / In-Review</SelectItem>
<SelectItem value="Rejected" className="text-red-600 focus:bg-red-50">Rejected / Dues Owed</SelectItem> <SelectItem value="Dues" className="text-red-600 focus:bg-red-50">Dues / Outstanding</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Amount ()</Label>
<input
type="number"
className="w-full mt-2 p-2 border border-slate-300 rounded-md"
value={clearanceAmount}
onChange={(e) => setClearanceAmount(Number(e.target.value))}
/>
</div>
<div>
<Label>Type</Label>
<Select value={clearanceType} onValueChange={(val: any) => setClearanceType(val)}>
<SelectTrigger className="mt-2 border-slate-300">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent className="bg-white">
<SelectItem value="Recovery">Recovery (Dealer owes RE)</SelectItem>
<SelectItem value="Payable">Payable (RE owes Dealer)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div> <div>
<Label>Remarks/Details</Label> <Label>Remarks/Details</Label>
<Textarea <Textarea
@ -779,6 +926,34 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
rows={4} rows={4}
/> />
</div> </div>
<div>
<Label>Supporting Document (Optional)</Label>
<div className="mt-2 text-sm text-slate-500 mb-2">
Upload NOC, ledger statement, or evidence of dues.
</div>
<div className="relative group border-2 border-dashed border-slate-200 rounded-lg p-4 transition-colors hover:border-blue-300">
<input
type="file"
onChange={(e) => setClearanceFile(e.target.files?.[0] || null)}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
accept=".pdf,.jpg,.jpeg,.png"
/>
<div className="flex flex-col items-center justify-center gap-2">
{clearanceFile ? (
<div className="flex items-center gap-2 text-blue-600 font-medium">
<FileText className="w-5 h-5" />
<span className="truncate max-w-[200px]">{clearanceFile.name}</span>
<X className="w-4 h-4 cursor-pointer text-slate-400 hover:text-red-500" onClick={(e) => { e.stopPropagation(); setClearanceFile(null); }} />
</div>
) : (
<>
<Upload className="w-8 h-8 text-slate-300 group-hover:text-blue-400" />
<span className="text-slate-400">Click or drag to upload (PDF, JPG, PNG)</span>
</>
)}
</div>
</div>
</div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
variant="outline" variant="outline"
@ -799,6 +974,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<DocumentPreviewModal
isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)}
document={previewDocument}
/>
</div> </div>
); );
} }

View File

@ -57,9 +57,9 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
'DD AM': ['ASM'], 'DD AM': ['ASM'],
'ZBH': ['ZBH'], 'ZBH': ['ZBH'],
'NBH': ['NBH'], 'NBH': ['NBH'],
'Legal Admin': ['Legal'], 'Legal Admin': ['Legal', 'FNF Initiate'],
'DD Admin': ['DD Admin'], 'DD Admin': ['DD Admin', 'FNF Initiate'],
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'ASM', 'DD Lead'] 'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'ASM', 'DD Lead', 'FNF Initiate']
}; };
const userStages = roleToStageMapping[currentUser.role] || []; const userStages = roleToStageMapping[currentUser.role] || [];

View File

@ -10,10 +10,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert'; import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data'; import { User, mockWorkNotes } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { terminationService } from '../../services/termination.service'; import { terminationService } from '../../services/termination.service';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { formatDateTime } from '../ui/utils';
interface TerminationDetailsProps { interface TerminationDetailsProps {
terminationId: string; terminationId: string;
@ -108,41 +109,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
// Check if user can push to F&F (DD Lead and above) // Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role); const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
// Mock data - would come from API // Use actual data from backend
const request = { const request = terminationData || {};
id: terminationId,
dealerCode: 'DL-MH-025', // Define internal names for mapping if needed, but backend strings are preferred
dealerName: 'Vikram Patil Motors',
address: '789, FC Road, Shivaji Nagar, Pune',
cityCategory: 'Tier 2',
domainName: 'Pune West',
dealershipName: 'Royal Enfield Pune',
gst: '27AABCU9604R1ZX',
salesCode: 'SAL-MH-025',
serviceCode: 'SRV-MH-025',
accessoriesCode: 'ACC-MH-025',
gmaCode: 'GMA-MH-025',
location: 'Pune, Maharashtra',
inauguration: 'June 2019',
loa: 'May 2019',
loi: 'April 2019',
lastSixMonthsSales: '₹45,00,000',
numberOfDealerships: '1',
numberOfStudios: '0',
constitution: 'Partnership',
dealershipType: 'Main Dealer',
typeOfClosure: 'Complete',
formatCategory: 'B',
dealerScoreCardBand: 'Bronze',
terminationCategory: 'Breach of Agreement',
subCategory: 'Violation of exclusivity clause, unauthorized sub-dealership',
description: 'Multiple instances of contract violations including unauthorized sale of competing brands and creation of sub-dealerships without company approval. Despite warnings, dealer has continued non-compliant practices.',
severity: 'High',
status: 'RBM Review',
currentStage: 'RBM',
submittedOn: '2025-10-15',
submittedBy: 'ASM - Mumbai Region'
};
// Mock documents by stage // Mock documents by stage
const stageDocuments: Record<string, any[]> = { const stageDocuments: Record<string, any[]> = {
@ -186,96 +157,91 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const progressStages = [ const progressStages = [
{ {
id: 1, id: 1,
name: 'Request Initiated', name: 'Submitted',
status: 'completed', status: 'completed',
date: '2025-10-15', description: 'Termination request initiated',
description: 'Termination request created by ASM/Initiator', date: '',
actionType: 'approved', actionType: '',
actionBy: 'ASM - Mumbai Region', actionBy: '',
remarks: 'Termination request initiated due to severe breach of agreement. Multiple violations documented.', remarks: '',
feedback: 'All evidence and documentation attached. Case requires urgent attention due to severity of violations.' feedback: ''
}, },
{ {
id: 2, id: 2,
name: 'RBM Review', name: 'RBM Review',
status: request.currentStage === 'RBM' ? 'active' : ['ZBH', 'DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'RBM Review' ? 'active' : ['ZBH Review', 'DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'Regional Business Manager review', description: 'Regional Business Manager review'
actionType: request.currentStage === 'RBM' ? undefined : undefined,
actionBy: request.currentStage === 'RBM' ? undefined : undefined,
remarks: request.currentStage === 'RBM' ? undefined : undefined,
feedback: request.currentStage === 'RBM' ? undefined : undefined
}, },
{ {
id: 3, id: 3,
name: 'ZBH Review', name: 'ZBH Review',
status: request.currentStage === 'ZBH' ? 'active' : ['DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'ZBH Review' ? 'active' : ['DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'Zonal Business Head evaluation' description: 'Zonal Business Head evaluation'
}, },
{ {
id: 4, id: 4,
name: 'DD Lead Review', name: 'DD Lead Review',
status: request.currentStage === 'DD Lead' ? 'active' : ['Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'DD Lead Review' ? 'active' : ['Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'DD Lead validation' description: 'DD Lead validation'
}, },
{ {
id: 5, id: 5,
name: 'Legal Verification', name: 'Legal Verification',
status: request.currentStage === 'Legal' ? 'active' : ['NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'Legal Verification' ? 'active' : ['NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'Legal team validates termination grounds' description: 'Legal team validates termination grounds'
}, },
{ {
id: 6, id: 6,
name: 'NBH Evaluation', name: 'NBH Evaluation',
status: request.currentStage === 'NBH' ? 'active' : ['SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'NBH Evaluation' ? 'active' : ['Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'National Business Head decision' description: 'National Business Head decision'
}, },
{ {
id: 7, id: 7,
name: 'Show Cause Notice (SCN)', name: 'Show Cause Notice (SCN)',
status: request.currentStage === 'SCN' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'Show Cause Notice' ? 'active' : ['Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'SCN sent to dealer, awaiting response' description: 'SCN sent to dealer, awaiting response'
}, },
{ {
id: 8, id: 8,
name: 'DD Lead & Legal Review', name: 'Personal Hearing',
status: request.currentStage === 'DD Lead Legal' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'Personal Hearing' ? 'active' : ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'Evaluation of SCN response' description: 'Evaluation of SCN response & Hearing'
}, },
{ {
id: 9, id: 9,
name: 'NBH Termination Approval', name: 'NBH Final Approval',
status: request.currentStage === 'NBH Final' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'NBH Final Approval' ? 'active' : ['CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'NBH approves termination' description: 'NBH final termination decision'
}, },
{ {
id: 10, id: 10,
name: 'CCO Approval', name: 'CCO Approval',
status: request.currentStage === 'CCO' ? 'active' : ['CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'CCO Approval' ? 'active' : ['CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'Chief Commercial Officer approval' description: 'Chief Commercial Officer approval'
}, },
{ {
id: 11, id: 11,
name: 'CEO Final Approval', name: 'CEO Final Approval',
status: request.currentStage === 'CEO' ? 'active' : ['Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'CEO Final Approval' ? 'active' : ['Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
description: 'CEO final authorization' description: 'CEO final authorization'
}, },
{ {
id: 12, id: 12,
name: 'Legal - Termination Letter', name: 'Legal - Termination Letter',
status: request.currentStage === 'Legal Letter' ? 'active' : ['DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', status: request.currentStage === 'Legal - Termination Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
description: 'Legal team shares termination letter to DD-Lead and DD Admin' description: 'Legal team issues final termination letter'
}, },
{ {
id: 13, id: 13,
name: 'DD Admin - Share with Dealer',
status: request.currentStage === 'DD Admin Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
description: 'DD Admin shares termination letter with dealer (Proceed to F&F)'
},
{
id: 14,
name: 'Dealer Terminated', name: 'Dealer Terminated',
status: request.currentStage === 'Terminated' ? 'completed' : 'pending', status: request.currentStage === 'Terminated' ? 'completed' : 'pending',
description: 'Dealership termination effective' description: 'Dealership termination effective',
date: '',
actionType: '',
actionBy: '',
remarks: '',
feedback: ''
} }
]; ];
@ -288,7 +254,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
setActionDialog({ open: true, type }); setActionDialog({ open: true, type });
}; };
const handleSubmitAction = () => { const handleSubmitAction = async () => {
if (!remarks && actionDialog.type !== 'assign' && actionDialog.type !== 'pushfnf') { if (!remarks && actionDialog.type !== 'assign' && actionDialog.type !== 'pushfnf') {
toast.error('Please provide remarks'); toast.error('Please provide remarks');
return; return;
@ -298,18 +264,32 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
return; return;
} }
const actionMessages = { setIsProcessing(true);
approve: 'Request approved and forwarded', try {
withdrawal: 'Request withdrawn successfully', if (actionDialog.type === 'approve') {
sendback: 'Request sent back for clarification', await terminationService.updateTerminationStatus(terminationId, 'approve', remarks);
assign: `Request assigned to ${assignToUser}`, } else {
pushfnf: 'Request pushed to F&F successfully' // Handle other actions
}; }
toast.success(actionMessages[actionDialog.type!]); const actionMessages = {
setActionDialog({ open: false, type: null }); approve: 'Request approved and forwarded',
setRemarks(''); withdrawal: 'Request withdrawn successfully',
setAssignToUser(''); sendback: 'Request sent back for clarification',
assign: `Request assigned to ${assignToUser}`,
pushfnf: 'Request pushed to F&F successfully'
};
toast.success(actionMessages[actionDialog.type!]);
setActionDialog({ open: false, type: null });
setRemarks('');
setAssignToUser('');
fetchTermination();
} catch (error) {
toast.error('Failed to perform action');
} finally {
setIsProcessing(false);
}
}; };
const getSeverityColor = (severity: string) => { const getSeverityColor = (severity: string) => {
@ -345,8 +325,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
</Button> </Button>
<div> <div>
<h1 className="text-2xl">{terminationId}</h1> <h1 className="text-2xl">{request.requestId || terminationId}</h1>
<p className="text-slate-600">{request.dealerName}</p> <p className="text-slate-600">{request.dealer?.businessName || request.dealerName}</p>
</div> </div>
<Badge className={getSeverityColor(request.severity)}> <Badge className={getSeverityColor(request.severity)}>
{request.severity} {request.severity}
@ -496,47 +476,47 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="grid grid-cols-2 md:grid-cols-3 gap-6"> <div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div> <div>
<Label className="text-slate-600">Dealer Code</Label> <Label className="text-slate-600">Dealer Code</Label>
<p>{request.dealerCode}</p> <p>{request.dealer?.dealerCode?.code || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Dealer Name</Label> <Label className="text-slate-600">Dealer Name</Label>
<p>{request.dealerName}</p> <p>{request.dealer?.businessName || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">GST</Label> <Label className="text-slate-600">GST</Label>
<p>{request.gst}</p> <p>{request.dealer?.gstNumber || 'N/A'}</p>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label className="text-slate-600">Address</Label> <Label className="text-slate-600">Address</Label>
<p>{request.address}</p> <p>{request.dealer?.registeredAddress || request.dealer?.application?.address || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">City Category</Label> <Label className="text-slate-600">City</Label>
<p>{request.cityCategory}</p> <p>{request.dealer?.application?.city || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Domain Name</Label> <Label className="text-slate-600">District</Label>
<p>{request.domainName}</p> <p>{request.dealer?.application?.district?.name || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Dealership Name</Label> <Label className="text-slate-600">Dealership Name</Label>
<p>{request.dealershipName}</p> <p>{request.dealer?.businessName || request.dealershipName}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Sales Code</Label> <Label className="text-slate-600">Sales Code</Label>
<p>{request.salesCode}</p> <p>{request.dealer?.dealerCode?.salesCode || request.salesCode || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Service Code</Label> <Label className="text-slate-600">Service Code</Label>
<p>{request.serviceCode}</p> <p>{request.dealer?.dealerCode?.serviceCode || request.serviceCode || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Accessories Code</Label> <Label className="text-slate-600">Accessories Code</Label>
<p>{request.accessoriesCode}</p> <p>{request.dealer?.dealerCode?.accessoriesCode || request.accessoriesCode || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">GMA Code</Label> <Label className="text-slate-600">GMA Code</Label>
<p>{request.gmaCode}</p> <p>{request.dealer?.dealerCode?.gmaCode || request.gmaCode || 'N/A'}</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -550,15 +530,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="grid grid-cols-2 md:grid-cols-3 gap-6"> <div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div> <div>
<Label className="text-slate-600">Inauguration</Label> <Label className="text-slate-600">Inauguration</Label>
<p>{request.inauguration}</p> <p>{request.dealer?.onboardedAt ? formatDateTime(request.dealer.onboardedAt, 'date') : (request.inauguration || 'N/A')}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">LOA</Label> <Label className="text-slate-600">LOA</Label>
<p>{request.loa}</p> <p>{request.loa || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">LOI</Label> <Label className="text-slate-600">LOI</Label>
<p>{request.loi}</p> <p>{request.loi || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Last 6 Months Sales</Label> <Label className="text-slate-600">Last 6 Months Sales</Label>
@ -574,23 +554,23 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</div> </div>
<div> <div>
<Label className="text-slate-600">Constitution</Label> <Label className="text-slate-600">Constitution</Label>
<p>{request.constitution}</p> <p>{request.dealer?.constitutionType || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Dealership Type</Label> <Label className="text-slate-600">Dealership Type</Label>
<p>{request.dealershipType}</p> <p>{request.dealer?.application?.businessType || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Type of Closure</Label> <Label className="text-slate-600">Type of Closure</Label>
<p>{request.typeOfClosure}</p> <p>{request.typeOfClosure || 'Complete'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Format Category</Label> <Label className="text-slate-600">Format Category</Label>
<p>{request.formatCategory}</p> <p>{request.formatCategory || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Dealer Score Card Band</Label> <Label className="text-slate-600">Dealer Score Card Band</Label>
<p>{request.dealerScoreCardBand}</p> <p>{request.dealerScoreCardBand || 'N/A'}</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -607,15 +587,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label className="text-slate-600">Termination Category</Label> <Label className="text-slate-600">Termination Category</Label>
<p className="text-red-900">{request.terminationCategory}</p> <p className="text-red-900">{request.category}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Sub Category</Label> <Label className="text-slate-600">Sub Category</Label>
<p>{request.subCategory}</p> <p>{request.subCategory || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Description</Label> <Label className="text-slate-600">Description</Label>
<p>{request.description}</p> <p>{request.reason}</p>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
@ -628,11 +608,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</div> </div>
<div> <div>
<Label className="text-slate-600">Submitted By</Label> <Label className="text-slate-600">Submitted By</Label>
<p>{request.submittedBy}</p> <p>{request.initiator?.fullName || 'System'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Submitted On</Label> <Label className="text-slate-600">Submitted On</Label>
<p>{request.submittedOn}</p> <p>{formatDateTime(request.createdAt)}</p>
</div> </div>
</div> </div>
</div> </div>
@ -761,22 +741,37 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{mockDocuments.map((doc) => ( {(() => {
<TableRow key={doc.id}> const allDocs = [
<TableCell> ...(request.documents || []),
<div className="flex items-center gap-2"> ...(request.uploadedDocuments || [])
<FileText className="w-4 h-4 text-slate-500" /> ];
<span>{doc.name}</span>
</div> if (allDocs.length === 0) return (
</TableCell> <TableRow>
<TableCell>{doc.type}</TableCell> <TableCell colSpan={5} className="text-center py-4 text-slate-500">
<TableCell>{doc.uploadDate}</TableCell> No documents found
<TableCell>{doc.uploader || '-'}</TableCell> </TableCell>
<TableCell> </TableRow>
<Button size="sm" variant="outline">View</Button> );
</TableCell>
</TableRow> return allDocs.map((doc: any, index: number) => (
))} <TableRow key={doc.id || index}>
<TableCell>
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-500" />
<span>{doc.name || doc.fileName}</span>
</div>
</TableCell>
<TableCell>{doc.documentType || doc.type || 'Document'}</TableCell>
<TableCell>{formatDateTime(doc.uploadDate || doc.createdAt)}</TableCell>
<TableCell>{doc.uploader?.fullName || doc.uploader || '-'}</TableCell>
<TableCell>
<Button size="sm" variant="outline">View</Button>
</TableCell>
</TableRow>
));
})()}
</TableBody> </TableBody>
</Table> </Table>
</CardContent> </CardContent>
@ -792,19 +787,25 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{mockAuditLogs.map((log) => ( {(request.timeline || []).length > 0 ? (
<div key={log.id} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0"> (request.timeline || []).map((log: any, index: number) => (
<div className="w-2 h-2 rounded-full bg-red-600 mt-2" /> <div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className="flex-1"> <div className="w-2 h-2 rounded-full bg-red-600 mt-2" />
<div className="flex items-center justify-between mb-1"> <div className="flex-1">
<p>{log.action}</p> <div className="flex items-center justify-between mb-1">
<span className="text-sm text-slate-600">{log.timestamp}</span> <p className="font-medium text-slate-900">{log.action || log.status}</p>
<span className="text-sm text-slate-600">{formatDateTime(log.timestamp || log.createdAt)}</span>
</div>
<p className="text-sm text-slate-600">{log.user || log.actor}</p>
{log.remarks || log.comments ? <p className="text-sm text-slate-500 mt-1">{log.remarks || log.comments}</p> : null}
</div> </div>
<p className="text-sm text-slate-600">{log.user}</p>
{log.details && <p className="text-sm text-slate-500 mt-1">{log.details}</p>}
</div> </div>
))
) : (
<div className="text-center py-8 text-slate-500">
No history found
</div> </div>
))} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -11,6 +11,7 @@ import { Textarea } from '../ui/textarea';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert'; import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils';
import { User } from '../../lib/mock-data'; import { User } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -108,7 +109,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
// Outlet model has associate dealer? Let's check. // Outlet model has associate dealer? Let's check.
// In my outlet.controller.ts, I included 'dealer'. // In my outlet.controller.ts, I included 'dealer'.
const payload = { const payload = {
dealerId: autoFilledData.Dealer?.id || autoFilledData.dealerId, // Map from outlet's dealer association dealerId: autoFilledData.Dealer?.id || autoFilledData.id, // outlet.id might be used if dealerId is missing, but backend expects dealerId (Dealer model)
category: formData.terminationCategory, category: formData.terminationCategory,
reason: formData.reason, reason: formData.reason,
proposedLwd: formData.proposedLwd, proposedLwd: formData.proposedLwd,
@ -150,13 +151,16 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
if (!currentUser) return false; if (!currentUser) return false;
const roleToStageMapping: Record<string, string[]> = { const roleToStageMapping: Record<string, string[]> = {
'DD Lead': ['DD Lead'], 'DD Lead': ['DD Lead Review'],
'RBM': ['RBM'], 'RBM': ['RBM Review'],
'ZBH': ['ZBH'], 'ZBH': ['ZBH Review'],
'NBH': ['NBH'], 'NBH': ['NBH Evaluation', 'NBH Final Approval'],
'Legal Admin': ['Legal'], 'Legal Admin': ['Legal Verification', 'Legal - Termination Letter'],
'DD Admin': ['DD Admin'], 'Legal': ['Legal Verification'],
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'DD Lead', 'CCO', 'CEO'] 'DD Admin': ['Show Cause Notice', 'Terminated'],
'CCO': ['CCO Approval'],
'CEO': ['CEO Final Approval'],
'Super Admin': ['DD Lead Review', 'RBM Review', 'ZBH Review', 'NBH Evaluation', 'Legal Verification', 'Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated']
}; };
const userStages = roleToStageMapping[currentUser.role] || []; const userStages = roleToStageMapping[currentUser.role] || [];
@ -397,7 +401,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id.substring(0, 8)}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getSeverityColor(request.severity || 'Medium')}> <Badge className={getSeverityColor(request.severity || 'Medium')}>
{request.severity || 'Normal'} {request.severity || 'Normal'}
</Badge> </Badge>
@ -431,7 +436,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Submitted On</p> <p className="text-slate-600">Submitted On</p>
<p>{new Date(request.createdAt).toLocaleDateString()}</p> <p>{formatDateTime(request.createdAt)}</p>
</div> </div>
</div> </div>
</div> </div>
@ -471,7 +476,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id.substring(0, 8)}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {request.status}
</Badge> </Badge>
@ -491,7 +497,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Submitted On</p> <p className="text-slate-600">Submitted On</p>
<p>{new Date(request.createdAt).toLocaleDateString()}</p> <p>{formatDateTime(request.createdAt)}</p>
</div> </div>
</div> </div>
</div> </div>
@ -532,7 +538,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg">{request.id.substring(0, 8)}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {request.status}
</Badge> </Badge>
@ -544,7 +551,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Closed On</p> <p className="text-slate-600">Closed On</p>
<p>{new Date(request.updatedAt).toLocaleDateString()}</p> <p>{formatDateTime(request.updatedAt)}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Termination Category</p> <p className="text-slate-600">Termination Category</p>

View File

@ -13,6 +13,7 @@ import { useState, useEffect } from 'react';
import { User } from '../../lib/mock-data'; import { User } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { onboardingService } from '../../services/onboarding.service'; import { onboardingService } from '../../services/onboarding.service';
import { settlementService } from '../../services/settlement.service';
interface FinanceDashboardProps { interface FinanceDashboardProps {
currentUser: User | null; currentUser: User | null;
@ -22,36 +23,6 @@ interface FinanceDashboardProps {
onViewFnFDetails?: (fnfId: string) => void; onViewFnFDetails?: (fnfId: string) => void;
} }
// Mock data for F&F cases (Keeping as static for now as per original)
const mockFnFCases = [
{
id: 'RES-001',
dealerName: 'Amit Sharma Motors',
dealerCode: 'DL-MH-001',
type: 'Resignation',
location: 'Mumbai, Maharashtra',
status: 'Pending Finance Summary',
submittedOn: '2025-10-08',
departmentsResponded: 16,
totalDepartments: 16,
hasFinanceSummary: false
},
{
id: 'TERM-002',
dealerName: 'Sanjay Enterprises',
dealerCode: 'DL-TG-033',
type: 'Termination',
location: 'Hyderabad, Telangana',
status: 'Finance Summary Completed',
submittedOn: '2025-09-20',
departmentsResponded: 16,
totalDepartments: 16,
hasFinanceSummary: true,
netAmount: -270000,
completedOn: '2025-10-10'
}
];
interface FinanceLineItem { interface FinanceLineItem {
id: string; id: string;
department: string; department: string;
@ -61,7 +32,9 @@ interface FinanceLineItem {
} }
export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAuditDetails, onViewFnFDetails }: FinanceDashboardProps) { export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAuditDetails, onViewFnFDetails }: FinanceDashboardProps) {
const [applications, setApplications] = useState<any[]>([]); const [onboardingPayments, setOnboardingPayments] = useState<any[]>([]);
const [fnfSettlements, setFnfSettlements] = useState<any[]>([]);
const [pendingAudits, setPendingAudits] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@ -71,20 +44,24 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
const fetchData = async () => { const fetchData = async () => {
try { try {
setLoading(true); setLoading(true);
const data = await onboardingService.getApplications(); const [payments, settlements, apps] = await Promise.all([
// Filter for applications relevant to finance settlementService.getOnboardingPayments(),
const financeApps = data.filter((app: any) => { settlementService.getFnFSettlements(),
const s = app.overallStatus || app.status; onboardingService.getApplications()
const stage = app.currentStage; ]);
return [
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation', setOnboardingPayments(payments);
'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT', setFnfSettlements(settlements);
'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
].includes(s) || stage === 'Finance'; // Filter for applications needing FDD review
}); const fddApps = apps.filter((app: any) =>
setApplications(financeApps); app.status === 'FDD_VERIFICATION' || app.overallStatus === 'FDD Verification' || app.currentStage === 'FDD'
);
setPendingAudits(fddApps);
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Fetch error:', error);
toast.error('Failed to load dashboard data');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -102,6 +79,50 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
}); });
const [finalRemarks, setFinalRemarks] = useState(''); const [finalRemarks, setFinalRemarks] = useState('');
const calculateTotals = () => {
const totalRecovery = lineItems
.filter(item => item.type === 'recovery')
.reduce((sum, item) => sum + item.amount, 0);
const totalPayable = lineItems
.filter(item => item.type === 'payable')
.reduce((sum, item) => sum + item.amount, 0);
const netAmount = totalPayable - totalRecovery;
return { totalRecovery, totalPayable, netAmount };
};
const handleSubmitFinanceSummary = async () => {
if (lineItems.length === 0) {
toast.error('Please add at least one line item');
return;
}
if (!finalRemarks) {
toast.error('Please add final remarks');
return;
}
try {
setLoading(true);
// Implement submission to backend settlement update
toast.success('Finance summary submitted successfully');
setFnfDialog(false);
setSelectedFnF(null);
setLineItems([]);
setFinalRemarks('');
fetchData();
} catch (err) {
toast.error('Failed to submit summary');
} finally {
setLoading(false);
}
};
const pendingOnboarding = onboardingPayments.filter(p => p.paymentStatus !== 'Paid' && p.paymentStatus !== 'Verified');
const verifiedOnboarding = onboardingPayments.filter(p => p.paymentStatus === 'Paid' || p.paymentStatus === 'Verified');
const pendingFnF = fnfSettlements.filter(f => f.status === 'Initiated' || f.status === 'Calculated');
const completedFnF = fnfSettlements.filter(f => f.status === 'Completed' || f.status === 'Cleared');
const handleAddLineItem = () => { const handleAddLineItem = () => {
if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) { if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) {
@ -127,54 +148,6 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
toast.info('Line item removed'); toast.info('Line item removed');
}; };
const calculateTotals = () => {
const totalRecovery = lineItems
.filter(item => item.type === 'recovery')
.reduce((sum, item) => sum + item.amount, 0);
const totalPayable = lineItems
.filter(item => item.type === 'payable')
.reduce((sum, item) => sum + item.amount, 0);
const netAmount = totalPayable - totalRecovery;
return { totalRecovery, totalPayable, netAmount };
};
const handleSubmitFinanceSummary = () => {
if (lineItems.length === 0) {
toast.error('Please add at least one line item');
return;
}
if (!finalRemarks) {
toast.error('Please add final remarks');
return;
}
toast.success('Finance summary submitted successfully');
setFnfDialog(false);
setSelectedFnF(null);
setLineItems([]);
setFinalRemarks('');
};
const getRelevantPaymentStatus = (app: any) => {
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
const s = app.overallStatus || app.status || '';
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION') ? 'INITIAL' : 'FINAL';
const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
return deposit ? deposit.status : 'Awaiting Payment';
};
const pendingOnboarding = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified');
const verifiedOnboarding = applications.filter(app => getRelevantPaymentStatus(app) === 'Verified');
const pendingAudits = applications.filter(app => app.status === 'FDD_VERIFICATION' || app.overallStatus === 'FDD Verification');
const pendingFnF = mockFnFCases.filter(f => !f.hasFinanceSummary);
const completedFnF = mockFnFCases.filter(f => f.hasFinanceSummary);
const { totalRecovery, totalPayable, netAmount } = calculateTotals(); const { totalRecovery, totalPayable, netAmount } = calculateTotals();
if (loading) { if (loading) {
@ -326,29 +299,29 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<h3 className="text-lg font-bold">{app.applicationId || app.id}</h3> <h3 className="text-lg font-bold">{app.application?.applicationId || 'N/A'}</h3>
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300"> <Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
{app.status} {app.paymentStatus}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
<p className="text-slate-600">Applicant Name</p> <p className="text-slate-600">Applicant Name</p>
<p className="font-medium">{app.applicantName}</p> <p className="font-medium">{app.application?.applicantName}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Location</p> <p className="text-slate-600">Type</p>
<p>{app.city || app.preferredLocation}, {app.state}</p> <p>{app.paymentType}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Stage</p> <p className="text-slate-600">Amount</p>
<p className="text-amber-700 font-bold"> <p className="text-amber-700 font-bold">
{app.status === 'PAYMENT_VERIFICATION' ? 'Security Deposit' : 'First Fill'} {parseFloat(app.amount).toLocaleString('en-IN')}
</p> </p>
</div> </div>
<div> <div>
<p className="text-slate-600">Email</p> <p className="text-slate-600">Created On</p>
<p className="truncate">{app.email}</p> <p>{new Date(app.createdAt).toLocaleDateString()}</p>
</div> </div>
</div> </div>
</div> </div>
@ -399,27 +372,29 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<h3 className="text-lg font-bold">{app.applicationId || app.id}</h3> <h3 className="text-lg font-bold">{app.application?.applicationId || 'N/A'}</h3>
<Badge className="bg-green-100 text-green-700 border-green-300"> <Badge className="bg-green-100 text-green-700 border-green-300">
{getRelevantPaymentStatus(app)} {app.paymentStatus}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
<p className="text-slate-600">Applicant Name</p> <p className="text-slate-600">Applicant Name</p>
<p className="font-medium">{app.applicantName}</p> <p className="font-medium">{app.application?.applicantName}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Location</p> <p className="text-slate-600">Type</p>
<p>{app.city || app.preferredLocation}, {app.state}</p> <p>{app.paymentType}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Status</p> <p className="text-slate-600">Amount</p>
<p className="text-green-700 font-bold">Payment Verified</p> <p className="text-green-700 font-bold">
{parseFloat(app.amount).toLocaleString('en-IN')}
</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Email</p> <p className="text-slate-600">Verified On</p>
<p className="truncate">{app.email}</p> <p>{app.verificationDate ? new Date(app.verificationDate).toLocaleDateString() : 'N/A'}</p>
</div> </div>
</div> </div>
</div> </div>
@ -476,31 +451,35 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<h3 className="text-lg font-bold">{fnf.id}</h3> <h3 className="text-lg font-bold">{fnf.resignation?.resignationId || fnf.id}</h3>
<Badge variant="outline">{fnf.type}</Badge> <Badge variant="outline">{fnf.resignation ? 'Resignation' : fnf.terminationRequest ? 'Termination' : 'General'}</Badge>
<Badge className="bg-orange-100 text-orange-700 border-orange-300"> <Badge className="bg-orange-100 text-orange-700 border-orange-300">
{fnf.status} {fnf.status}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm mb-3">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm mb-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm mb-3">
<div> <div>
<p className="text-slate-600">Dealer Name</p> <p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Name</p>
<p className="font-medium">{fnf.dealerName}</p> <p className="font-semibold text-slate-900">{fnf.outlet?.dealer?.fullName || fnf.outlet?.name || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Dealer Code</p> <p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Code</p>
<p>{fnf.dealerCode}</p> <p className="font-mono text-xs font-bold text-blue-600">{fnf.outlet?.code || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Location</p> <p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Location</p>
<p>{fnf.location}</p> <p className="text-slate-900">{fnf.outlet?.city || 'N/A'}, {fnf.outlet?.state || ''}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm bg-white/50 p-2 rounded-lg border border-slate-100">
<CheckCircle className="w-4 h-4 text-green-600" /> <CheckCircle className="w-4 h-4 text-emerald-500" />
<span className="text-green-700">{fnf.departmentsResponded}/{fnf.totalDepartments} Departments Responded</span> <span className="text-slate-600 font-medium">
<span className="text-emerald-600 font-bold">{(fnf.clearances || []).filter((c: any) => c.status === 'NOC Submitted' || c.status === 'Dues Pending').length}</span>/16 Departments Cleared
</span>
</div> </div>
</div> </div>
</div>
<div className="ml-4 flex flex-col gap-2"> <div className="ml-4 flex flex-col gap-2">
<Button <Button
size="sm" size="sm"
@ -610,7 +589,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<DialogHeader> <DialogHeader>
<DialogTitle>Create Financial Settlement Summary</DialogTitle> <DialogTitle>Create Financial Settlement Summary</DialogTitle>
<DialogDescription> <DialogDescription>
{selectedFnF?.id} - {selectedFnF?.dealerName} ({selectedFnF?.dealerCode}) {selectedFnF?.resignation?.resignationId || selectedFnF?.id} - {selectedFnF?.dealerName} ({selectedFnF?.dealerCode})
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -770,7 +749,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<DialogHeader> <DialogHeader>
<DialogTitle>Full Settlement Details</DialogTitle> <DialogTitle>Full Settlement Details</DialogTitle>
<DialogDescription> <DialogDescription>
{selectedFnF?.id} - {selectedFnF?.dealerName} {selectedFnF?.resignation?.resignationId || selectedFnF?.id} - {selectedFnF?.dealerName}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -779,15 +758,15 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<Card className="bg-slate-50 border-none"> <Card className="bg-slate-50 border-none">
<CardContent className="pt-6 grid grid-cols-3 gap-6"> <CardContent className="pt-6 grid grid-cols-3 gap-6">
<div> <div>
<p className="text-xs text-slate-500 uppercase">Dealer Code</p> <p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Dealer Code</p>
<p className="text-lg font-bold">{selectedFnF.dealerCode}</p> <p className="text-lg font-bold">{selectedFnF.outlet?.code || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-xs text-slate-500 uppercase">Location</p> <p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Location</p>
<p className="text-lg font-bold">{selectedFnF.location}</p> <p className="text-lg font-bold">{selectedFnF.outlet?.city || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-xs text-slate-500 uppercase">Status</p> <p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Current Status</p>
<Badge variant="outline" className="bg-amber-100 text-amber-700 border-amber-200"> <Badge variant="outline" className="bg-amber-100 text-amber-700 border-amber-200">
{selectedFnF.status} {selectedFnF.status}
</Badge> </Badge>
@ -805,51 +784,63 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</CardHeader> </CardHeader>
<CardContent className="pt-4 space-y-3"> <CardContent className="pt-4 space-y-3">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-slate-500">Inventory Value</span> <span className="text-slate-500">Security Deposit Credits</span>
<span className="font-bold">12,45,000</span> <span className="font-bold text-slate-900">{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-slate-500">Security Deposit</span> <span className="text-slate-500">Other Payable Credits</span>
<span className="font-bold">2,00,000</span> <span className="font-bold text-slate-900">0</span>
</div> </div>
<div className="border-t pt-2 flex justify-between font-bold text-blue-700"> <div className="border-t pt-2 flex justify-between font-bold text-blue-700">
<span>Total Payables</span> <span>Total Payables</span>
<span>14,45,000</span> <span>{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-red-100"> <Card className="border-red-100 shadow-sm">
<CardHeader className="bg-red-50/50 pb-2"> <CardHeader className="bg-red-50/30 pb-2 border-b border-red-50">
<CardTitle className="text-sm font-bold flex items-center gap-2"> <CardTitle className="text-sm font-bold flex items-center gap-2 text-red-700">
<TrendingDown className="w-4 h-4 text-red-600" /> <TrendingDown className="w-4 h-4" />
Recoveries Check Receivables Check
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-4 space-y-3"> <CardContent className="pt-4 space-y-3">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-slate-500">Outstanding Invoices</span> <span className="text-slate-500">Departmental Dues (Recoverable)</span>
<span className="font-bold">8,50,000</span> <span className="font-bold text-red-600">{parseFloat(selectedFnF.totalReceivables || 0).toLocaleString('en-IN')}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-slate-500">Spares & Oil Dues</span> <span className="text-slate-500">Outstanding Invoices</span>
<span className="font-bold">1,20,000</span> <span className="font-bold text-slate-900">0</span>
</div> </div>
<div className="border-t pt-2 flex justify-between font-bold text-red-700"> <div className="border-t pt-2 flex justify-between font-bold text-red-700">
<span>Total Recoveries</span> <span>Total Recoveries</span>
<span>9,70,000</span> <span>{parseFloat(selectedFnF.totalReceivables || 0).toLocaleString('en-IN')}</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<div className="p-4 bg-green-900 text-white rounded-lg flex items-center justify-between"> <div className="p-5 bg-slate-900 text-white rounded-xl flex items-center justify-between shadow-lg">
<div> <div className="flex items-center gap-4">
<p className="text-xs text-green-300 uppercase font-bold">Estimated Net Settlement</p> <div className={`p-3 rounded-lg ${parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'bg-emerald-500/20' : 'bg-rose-500/20'}`}>
<p className="text-2xl font-bold">4,75,000 <span className="text-sm font-normal text-green-200 ml-2">(Payable to Dealer)</span></p> <IndianRupee className={`w-6 h-6 ${parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'text-emerald-400' : 'text-rose-400'}`} />
</div>
<div>
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-widest mb-1">Final Net Financial Position</p>
<div className="flex items-baseline gap-2">
<p className={`text-3xl font-bold tracking-tight ${parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{Math.abs(parseFloat(selectedFnF.netAmount || 0)).toLocaleString('en-IN')}
</p>
<span className="text-xs font-medium text-slate-400">
({parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'Payable to Dealer' : 'Recoverable from Dealer'})
</span>
</div>
</div>
</div> </div>
<Button className="bg-green-600 hover:bg-green-500"> <Button className="bg-blue-600 hover:bg-blue-500 px-6 font-bold shadow-md transition-all active:scale-95">
Generate PDF Generate PDF Summary
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -24,5 +24,9 @@ export function formatDateTime(date: string | Date | number, format: 'full' | 'd
return d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true }); return d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true });
} }
if (format === 'full') {
return d.toLocaleString('en-IN', options);
}
return d.toLocaleDateString('en-IN', options); return d.toLocaleDateString('en-IN', options);
} }

View File

@ -22,7 +22,7 @@ export const resignationService = {
getResignationById: async (id: string) => { getResignationById: async (id: string) => {
try { try {
const response: any = await API.getResignationById(id); const response: any = await API.getResignationById(id);
return response.data?.data || response.data; return response.data?.resignation || response.data?.data || response.data;
} catch (error) { } catch (error) {
console.error('Get resignation error:', error); console.error('Get resignation error:', error);
throw error; throw error;

View File

@ -0,0 +1,34 @@
import { API } from '../api/API';
export const settlementService = {
getOnboardingPayments: async () => {
const response: any = await API.getOnboardingPayments();
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch onboarding payments');
return response.data?.payments || [];
},
getFnFSettlements: async () => {
const response: any = await API.getFnFSettlements();
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch F&F settlements');
return response.data?.settlements || [];
},
getFnFSettlementById: async (id: string) => {
const response: any = await API.getFnFSettlementById(id);
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch F&F details');
return response.data?.fnf;
},
updatePayment: async (id: string, data: any) => {
const response: any = await API.updatePayment(id, data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to update payment');
return response.data;
},
calculateFnF: async (id: string) => {
const response: any = await API.calculateFnF(id);
if (!response.ok) throw new Error(response.data?.message || 'Failed to calculate F&F');
return response.data;
},
addLineItem: async (fnfId: string, data: any) => {
const response: any = await API.addLineItem(fnfId, data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to add line item');
return response.data;
}
};

View File

@ -3,7 +3,7 @@ import { API } from '../api/API';
export const terminationService = { export const terminationService = {
getTerminationById: async (id: string) => { getTerminationById: async (id: string) => {
const response = await API.getTerminationById(id); const response = await API.getTerminationById(id);
return response.data; return response.data?.termination || response.data?.data || response.data;
}, },
updateTerminationStatus: async (id: string, status: string, remarks: string) => { updateTerminationStatus: async (id: string, status: string, remarks: string) => {