added end to end testing files for all modules all midules coverd partially f&F resignation coverd majorly
This commit is contained in:
parent
c78d97fd31
commit
6542f0fb30
@ -122,7 +122,9 @@ export const API = {
|
||||
|
||||
// Resignation
|
||||
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),
|
||||
|
||||
// Termination
|
||||
@ -144,6 +146,8 @@ export const API = {
|
||||
createTermination: (data: any) => client.post('/termination', 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'),
|
||||
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
|
||||
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
|
||||
|
||||
@ -1183,18 +1183,17 @@ export const ApplicationDetails = () => {
|
||||
name: 'Statutory Documents',
|
||||
color: 'green',
|
||||
stages: [
|
||||
{ id: '11b-1', name: 'GST Certificate', status: isDocumentUploaded('Statutory GST') || isDocumentUploaded('GST Certificate') ? 'completed' : 'active', description: 'GST details' },
|
||||
{ id: '11b-2', name: 'PAN Card', status: isDocumentUploaded('Statutory PAN') || isDocumentUploaded('PAN Card') ? 'completed' : 'active', description: 'PAN details' },
|
||||
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Statutory Nodal') || isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal details' },
|
||||
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Statutory Check') || isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Bank verification' },
|
||||
{ id: '11b-5', name: 'Partnership Deed', status: isDocumentUploaded('Statutory Partnership') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Legal constitution' },
|
||||
{ id: '11b-6', name: 'Firm Registration', status: isDocumentUploaded('Statutory Firm Reg') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'RoC/Firm reg' },
|
||||
{ id: '11b-7', name: 'Virtual Code', status: isDocumentUploaded('Statutory Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Oracle setup' },
|
||||
{ id: '11b-8', name: 'Domain ID', status: isDocumentUploaded('Statutory Domain') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Email setup' },
|
||||
{ id: '11b-9', name: 'MSD Configuration', status: isDocumentUploaded('Statutory MSD') || isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Multiple Security Deposit' },
|
||||
{ id: '11b-10', name: 'LOI Acknowledgement', status: isDocumentUploaded('Statutory LOI Ack') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI Signed copy' },
|
||||
{ id: '11b-11', name: 'Board Resolution', status: isDocumentUploaded('Board Resolution') || isDocumentUploaded('Authorization Proof') ? 'completed' : 'active', description: 'Legal authorization' },
|
||||
{ id: '11b-12', name: 'Consolidated Approval', status: application.statutoryStatus === 'COMPLETED' ? 'completed' : 'active', description: 'Managerial sign-off' }
|
||||
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
|
||||
{ id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
|
||||
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
|
||||
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
|
||||
{ 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 Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
|
||||
{ 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: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
|
||||
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
|
||||
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
|
||||
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -13,6 +13,7 @@ import { User as UserType } from '../../lib/mock-data';
|
||||
import { toast } from 'sonner';
|
||||
import { API } from '../../api/API';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
|
||||
interface ConstitutionalChangeDetailsProps {
|
||||
requestId: string;
|
||||
@ -20,34 +21,30 @@ interface ConstitutionalChangeDetailsProps {
|
||||
currentUser: UserType | null;
|
||||
}
|
||||
|
||||
// Workflow stages as per the process flow
|
||||
// Workflow stages as per the process flow (SRS 12.2.4)
|
||||
const workflowStages = [
|
||||
{ id: 1, name: 'Request Created', key: 'created', role: 'Dealer' },
|
||||
{ id: 2, name: 'ASM Review', key: 'asm', role: 'ASM' },
|
||||
{ id: 3, name: 'RBM Review', key: 'rbm', role: 'RBM' },
|
||||
{ id: 4, name: 'DD ZM Review', key: 'dd-zm', role: 'DD-ZM' },
|
||||
{ id: 5, name: 'ZBH Review', key: 'zbh', role: 'ZBH' },
|
||||
{ id: 6, name: 'DD Lead Review', key: 'dd-lead', role: 'DD Lead' },
|
||||
{ id: 7, name: 'FDD Review', key: 'fdd', role: 'FDD' },
|
||||
{ id: 8, name: 'DD Head Review', key: 'dd-head', role: 'DD Head' },
|
||||
{ id: 9, name: 'NBH Review', key: 'nbh', role: 'NBH' },
|
||||
{ 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' }
|
||||
{ id: 1, name: 'Submitted', key: 'submitted', role: 'Dealer' },
|
||||
{ id: 2, name: 'ASM Review', key: 'asm-review', role: 'ASM' },
|
||||
{ id: 3, name: 'ZM/RBM Review', key: 'zm-rbm-review', role: 'ZM/RBM' },
|
||||
{ id: 4, name: 'ZBH Review', key: 'zbh-review', role: 'ZBH' },
|
||||
{ id: 5, name: 'DD Lead Review', key: 'lead-review', role: 'DD Lead' },
|
||||
{ id: 6, name: 'DD Head Review', key: 'head-review', role: 'DD Head' },
|
||||
{ id: 7, name: 'NBH Approval', key: 'nbh-approval', role: 'NBH' },
|
||||
{ id: 8, name: 'Legal Review', key: 'legal-review', role: 'Legal Team' },
|
||||
{ id: 9, name: 'Completed', key: 'completed', role: 'System' }
|
||||
];
|
||||
|
||||
// Document requirements mapping (same as in ConstitutionalChangePage)
|
||||
const documentRequirements: Record<string, number[]> = {
|
||||
'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],
|
||||
'Proprietorship': [1, 2, 3, 10, 16]
|
||||
};
|
||||
|
||||
const documentNames: Record<number, string> = {
|
||||
1: 'GST',
|
||||
2: 'Firm Pan Copy',
|
||||
1: 'GST Certificate',
|
||||
2: 'Firm PAN Copy',
|
||||
3: 'Self attested KYC\'s',
|
||||
4: 'Partnership Agreement (Notarised)',
|
||||
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 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-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';
|
||||
default: return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||
}
|
||||
@ -78,7 +76,7 @@ const getTypeColor = (type: string) => {
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
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';
|
||||
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
|
||||
const requiredDocs = documentRequirements[request.changeType] || [];
|
||||
|
||||
// Calculate current stage index
|
||||
// Calculate current stage index mapping to backend stages
|
||||
const getCurrentStageIndex = () => {
|
||||
const stageMap: Record<string, number> = {
|
||||
'Dealer': 1,
|
||||
'ASM': 2,
|
||||
'RBM': 3,
|
||||
'DD-ZM': 4,
|
||||
'ZBH': 5,
|
||||
'DD Lead': 6,
|
||||
'FDD': 7,
|
||||
'DD Head': 8,
|
||||
'NBH': 9,
|
||||
'DD H.O': 10,
|
||||
'Closed': 13
|
||||
'Submitted': 1,
|
||||
'ASM Review': 2,
|
||||
'ZM/RBM Review': 3,
|
||||
'ZBH Review': 4,
|
||||
'DD Lead Review': 5,
|
||||
'DD Head Review': 6,
|
||||
'NBH Approval': 7,
|
||||
'Legal Review': 8,
|
||||
'Completed': 9
|
||||
};
|
||||
return stageMap[request.currentStage] || 1;
|
||||
};
|
||||
@ -243,7 +239,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
||||
</div>
|
||||
<div>
|
||||
<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-900 text-sm mt-2">Current Stage: {request.currentStage}</p>
|
||||
</div>
|
||||
@ -469,7 +465,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
||||
{doc.fileName || doc.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-600">
|
||||
{new Date(doc.uploadedOn || doc.createdAt).toLocaleDateString()}
|
||||
{formatDateTime(doc.uploadedOn || doc.createdAt)}
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-600">
|
||||
{doc.uploadedBy || 'Dealer'}
|
||||
@ -533,7 +529,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm mt-2">{entry.comments || entry.remarks || 'No remarks provided'}</p>
|
||||
<p className="text-slate-500 text-sm mt-1">{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>
|
||||
))}
|
||||
|
||||
@ -13,6 +13,7 @@ import { useState, useEffect } from 'react';
|
||||
import { User as UserType } from '../../lib/mock-data';
|
||||
import { toast } from 'sonner';
|
||||
import { API } from '../../api/API';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
|
||||
interface ConstitutionalChangePageProps {
|
||||
currentUser: UserType | null;
|
||||
@ -465,7 +466,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</div>
|
||||
</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>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@ -677,7 +678,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
|
||||
<div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
|
||||
@ -30,9 +30,17 @@ import {
|
||||
Plus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Save
|
||||
Save,
|
||||
Paperclip
|
||||
} from 'lucide-react';
|
||||
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 {
|
||||
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 {
|
||||
id: string;
|
||||
department: string;
|
||||
@ -72,6 +98,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
|
||||
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
|
||||
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
|
||||
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFnFDetails();
|
||||
@ -83,11 +110,11 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
const response = await API.getFnFSettlementById(fnfId);
|
||||
const data = response.data as any;
|
||||
if (data.success) {
|
||||
const s = data.settlement;
|
||||
const s = data.fnf;
|
||||
setFnfCase({
|
||||
id: s.id,
|
||||
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',
|
||||
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
||||
terminationType: s.resignationId ? 'Resignation' : 'Termination',
|
||||
@ -95,23 +122,40 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD',
|
||||
status: s.status,
|
||||
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
|
||||
ifscCode: 'N/A',
|
||||
bankName: 'N/A',
|
||||
branch: 'N/A'
|
||||
},
|
||||
departmentResponses: (s.lineItems || []).map((li: any) => ({
|
||||
id: li.id,
|
||||
departmentName: li.department,
|
||||
status: 'Submitted',
|
||||
remarks: li.remarks,
|
||||
amount: Math.abs(li.amount),
|
||||
amountType: li.amount < 0 ? 'Payable Amount' : 'Recovery Amount'
|
||||
})),
|
||||
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
|
||||
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
|
||||
const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
|
||||
const totalAmount = relatedItems.reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount)), 0);
|
||||
const hasPayable = relatedItems.some((li: any) => li.amount < 0);
|
||||
|
||||
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: [
|
||||
{ name: 'Resignation Letter.pdf', size: '245 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Resignation' },
|
||||
{ name: 'Inventory Report.xlsx', size: '856 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Inventory' }
|
||||
{ name: 'Resignation Letter.pdf', size: 'N/A', uploadedOn: formatDateTime(s.createdAt), type: 'Resignation', url: '#' },
|
||||
...(s.clearances || [])
|
||||
.filter((c: any) => c.supportingDocument)
|
||||
.map((c: any) => ({
|
||||
name: c.supportingDocument.split('/').pop(),
|
||||
size: 'N/A',
|
||||
uploadedOn: formatDateTime(c.clearedAt),
|
||||
type: `${c.department} Proof`,
|
||||
url: c.supportingDocument
|
||||
}))
|
||||
]
|
||||
});
|
||||
|
||||
@ -1254,7 +1298,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{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}
|
||||
</Badge>
|
||||
) : (
|
||||
@ -1263,7 +1312,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{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')}
|
||||
</span>
|
||||
) : (
|
||||
@ -1271,7 +1320,24 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
)}
|
||||
</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>
|
||||
))}
|
||||
</TableBody>
|
||||
@ -1606,6 +1672,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentPreviewModal
|
||||
isOpen={!!previewDocument}
|
||||
onClose={() => setPreviewDocument(null)}
|
||||
document={previewDocument}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -76,6 +76,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
||||
|
||||
const getMappedData = (s: any) => ({
|
||||
id: s.id,
|
||||
caseId: s.resignation?.resignationId || s.id,
|
||||
dealerCode: s.outlet?.code || 'N/A',
|
||||
dealerName: s.outlet?.dealer?.name || 'N/A',
|
||||
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
||||
@ -201,13 +202,18 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
||||
|
||||
<Card>
|
||||
<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>
|
||||
<CardContent>
|
||||
<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" />
|
||||
</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>
|
||||
</Card>
|
||||
</div>
|
||||
@ -264,7 +270,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
||||
<TableRow key={fnfCase.id}>
|
||||
<TableCell>
|
||||
<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>
|
||||
</TableCell>
|
||||
|
||||
@ -8,10 +8,11 @@ import { Label } from '../ui/label';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||
import { Progress } from '../ui/progress';
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||
|
||||
interface FnFDetailsProps {
|
||||
fnfId: string;
|
||||
@ -24,6 +25,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
||||
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFnFDetails();
|
||||
@ -35,14 +37,14 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
const response = await API.getFnFSettlementById(fnfId);
|
||||
const data = response.data as any;
|
||||
if (data.success) {
|
||||
const s = data.settlement;
|
||||
const s = data.fnf;
|
||||
// Map backend data to UI format
|
||||
const mappedCase = {
|
||||
id: s.id,
|
||||
caseNumber: s.id.substring(0, 8).toUpperCase(),
|
||||
caseNumber: s.resignation?.resignationId || s.id,
|
||||
status: s.status,
|
||||
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',
|
||||
dealershipName: s.outlet?.name || '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',
|
||||
lastOperationalDateServices: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
|
||||
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',
|
||||
totalPayableAmount: parseFloat(s.totalPayables) || 0,
|
||||
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
|
||||
departmentResponses: (s.lineItems || []).map((li: any) => ({
|
||||
id: li.id,
|
||||
departmentName: li.department,
|
||||
status: li.remarks && li.remarks.toLowerCase().includes('no dues') ? 'No Dues' : (li.amount > 0 ? 'Dues' : 'Pending'),
|
||||
amountType: li.amount > 0 ? 'Recovery Amount' : null,
|
||||
amount: Math.abs(parseFloat(li.amount)) || 0,
|
||||
submittedDate: li.updatedAt ? new Date(li.updatedAt).toLocaleDateString() : null,
|
||||
remarks: li.remarks
|
||||
departmentResponses: [
|
||||
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
|
||||
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
|
||||
'Legal', 'Quality', 'Logistics', 'Customer Relations'
|
||||
].map((deptName: string) => {
|
||||
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
|
||||
const lineItem = (s.lineItems || []).find((li: any) => li.department === deptName);
|
||||
|
||||
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);
|
||||
}
|
||||
@ -128,9 +154,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
|
||||
const getDepartmentStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'No Dues':
|
||||
case 'NOC Submitted':
|
||||
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';
|
||||
case 'Pending':
|
||||
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}`, {
|
||||
state: {
|
||||
applicationName: fnfCase.dealerName || 'F&F Settlement',
|
||||
registrationNumber: fnfId || '',
|
||||
registrationNumber: fnfCase.caseNumber || '',
|
||||
participants: fnfCase.participants || []
|
||||
}
|
||||
})}
|
||||
@ -200,15 +226,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
<Progress value={progressPercentage} className="h-3" />
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
||||
<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">
|
||||
{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length}
|
||||
{fnfCase.departmentResponses.filter((d: any) => d.status === 'NOC Submitted').length}
|
||||
</p>
|
||||
</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">
|
||||
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length}
|
||||
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues Pending').length}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@ -351,12 +377,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
<Progress value={progressPercentage} className="h-2" />
|
||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||
<div className="text-center p-2 bg-green-100 rounded">
|
||||
<p className="text-green-700">No Dues</p>
|
||||
<p className="text-green-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length}</p>
|
||||
<p className="text-green-700">NOC Submitted</p>
|
||||
<p className="text-green-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'NOC Submitted').length}</p>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-red-100 rounded">
|
||||
<p className="text-red-700">Dues</p>
|
||||
<p className="text-red-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length}</p>
|
||||
<p className="text-red-700">Dues Pending</p>
|
||||
<p className="text-red-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues Pending').length}</p>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-slate-100 rounded">
|
||||
<p className="text-slate-700">Pending</p>
|
||||
@ -767,7 +793,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{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}
|
||||
</Badge>
|
||||
) : (
|
||||
@ -776,7 +807,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{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()}
|
||||
</span>
|
||||
) : (
|
||||
@ -890,7 +921,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mockDocuments.map((doc: any) => (
|
||||
{fnfCase.documents.map((doc: any) => (
|
||||
<TableRow key={doc.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -906,7 +937,23 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</Badge>
|
||||
</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>
|
||||
</TableRow>
|
||||
))}
|
||||
@ -980,6 +1027,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DocumentPreviewModal
|
||||
isOpen={!!previewDocument}
|
||||
onClose={() => setPreviewDocument(null)}
|
||||
document={previewDocument}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
@ -15,6 +15,14 @@ import { toast } from 'sonner';
|
||||
import { resignationService } from '../../services/resignation.service';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
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 {
|
||||
resignationId: string;
|
||||
@ -29,13 +37,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
const [assignToUser, setAssignToUser] = useState('');
|
||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||
const [showClearanceDialog, setShowClearanceDialog] = useState(false);
|
||||
const [selectedDept, setSelectedDept] = useState<string | null>(null);
|
||||
const [clearanceStatus, setClearanceStatus] = useState<'Cleared' | 'Pending' | 'Rejected'>('Cleared');
|
||||
const [selectedDept, setSelectedDept] = useState('');
|
||||
const [clearanceStatus, setClearanceStatus] = useState<any>('Pending');
|
||||
const [clearanceRemarks, setClearanceRemarks] = useState('');
|
||||
const [clearanceAmount, setClearanceAmount] = useState<number>(0);
|
||||
const [clearanceType, setClearanceType] = useState<'Payable' | 'Recovery'>('Recovery');
|
||||
const [clearanceFile, setClearanceFile] = useState<File | null>(null);
|
||||
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
|
||||
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||
|
||||
const fetchResignation = async () => {
|
||||
try {
|
||||
@ -58,25 +70,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
|
||||
// Progress stages logic based on live data
|
||||
const progressStages = [
|
||||
{ id: 1, name: 'Request Submitted', key: 'Submitted', description: 'Resignation request created' },
|
||||
{ id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
||||
{ id: 3, name: 'Departmental Clearances', key: 'Clearance', description: 'Clearance from departments' },
|
||||
{ id: 4, name: 'RBM + DD ZM Review', key: 'RBM', description: 'Regional Business Manager and DD ZM evaluation' },
|
||||
{ id: 5, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
|
||||
{ id: 6, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
|
||||
{ id: 7, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
||||
{ id: 8, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' }
|
||||
{ id: 1, 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: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
|
||||
{ id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
|
||||
{ id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
||||
{ id: 6, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification' },
|
||||
{ id: 7, 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) => {
|
||||
if (!resignationData) return 'pending';
|
||||
const currentStage = resignationData.currentStage;
|
||||
|
||||
// Simple logic for simulation - in real app, this would be more complex
|
||||
const stagesOrdered = ['Submitted', 'ASM', 'Clearance', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal'];
|
||||
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
|
||||
const currentIndex = stagesOrdered.indexOf(currentStage);
|
||||
const stageIndex = stagesOrdered.indexOf(stageKey);
|
||||
|
||||
if (currentIndex === -1) return 'pending'; // Fallback for rejected/other states
|
||||
if (stageIndex < currentIndex) return 'completed';
|
||||
if (stageIndex === currentIndex) return 'active';
|
||||
return 'pending';
|
||||
@ -129,16 +142,25 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
if (!selectedDept) return;
|
||||
try {
|
||||
setIsUpdatingClearance(true);
|
||||
await resignationService.updateClearance(resignationId, {
|
||||
department: selectedDept,
|
||||
status: clearanceStatus,
|
||||
remarks: clearanceRemarks
|
||||
});
|
||||
toast.success(`${selectedDept} clearance updated`);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('department', selectedDept);
|
||||
formData.append('status', clearanceStatus);
|
||||
formData.append('remarks', clearanceRemarks);
|
||||
formData.append('amount', String(clearanceAmount));
|
||||
formData.append('type', clearanceType);
|
||||
|
||||
if (clearanceFile) {
|
||||
formData.append('file', clearanceFile);
|
||||
}
|
||||
|
||||
await resignationService.updateClearance(resignationId, formData);
|
||||
toast.success(`Successfully updated clearance for ${selectedDept}`);
|
||||
setShowClearanceDialog(false);
|
||||
setClearanceFile(null);
|
||||
fetchResignation();
|
||||
} catch (error) {
|
||||
toast.error('Failed to update clearance');
|
||||
toast.error('Failed to update clearance status');
|
||||
} finally {
|
||||
setIsUpdatingClearance(false);
|
||||
}
|
||||
@ -161,7 +183,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl">{resignationId}</h1>
|
||||
<h1 className="text-2xl">{resignationData?.resignationId || resignationId}</h1>
|
||||
<p className="text-slate-600">{resignationData?.outlet?.name}</p>
|
||||
</div>
|
||||
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
|
||||
@ -216,7 +238,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{/* Secondary Actions */}
|
||||
{currentUser?.role !== 'Dealer' && (
|
||||
<div className="flex items-center gap-2">
|
||||
{canPushToFnF && (
|
||||
{canPushToFnF && resignationData?.status !== 'FNF_INITIATED' && resignationData?.status !== 'Settled' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@ -365,11 +387,11 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
@ -379,7 +401,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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>
|
||||
<Label className="text-slate-600">Current Stage</Label>
|
||||
@ -434,7 +456,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{timelineEntry && (
|
||||
<div className="flex items-center gap-1 text-sm text-slate-600">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{new Date(timelineEntry.timestamp || timelineEntry.createdAt).toLocaleDateString()}</span>
|
||||
<span>{formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -472,31 +494,91 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{(resignationData?.clearances || []).map((clearance: any) => (
|
||||
<Card key={clearance.department} className="border border-slate-200">
|
||||
{ALL_DEPARTMENTS.map((dept) => {
|
||||
const settlement = resignationData?.settlement;
|
||||
const fffClearance = (settlement?.clearances || []).find((c: any) => c.department === dept);
|
||||
const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => li.department === dept);
|
||||
const lineItemAmount = relatedLineItems.reduce((sum: number, li: any) => sum + parseFloat(li.amount || 0), 0);
|
||||
|
||||
// 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">
|
||||
<CardTitle className="text-base font-medium">{clearance.department}</CardTitle>
|
||||
<CardTitle className="text-base font-medium capitalize">{dept}</CardTitle>
|
||||
<Badge className={
|
||||
clearance.status === 'Cleared' || clearance.status === 'Approved' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
|
||||
clearance.status === 'Rejected' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
|
||||
displayStatus === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
|
||||
displayStatus === 'Dues' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
|
||||
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
|
||||
}>
|
||||
{clearance.status}
|
||||
{displayStatus || 'Pending'}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-slate-600 line-clamp-2 min-h-[2.5rem]">
|
||||
{clearance.remarks || 'No remarks provided'}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs text-slate-500">
|
||||
<span>Amount: ₹{(displayAmount || 0).toLocaleString()}</span>
|
||||
<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>
|
||||
{currentUser && (currentUser.role === 'Super Admin' || currentUser.role === 'DD Admin' || (currentUser.role.includes(clearance.department) && resignationData?.currentStage === 'Clearance')) && (
|
||||
|
||||
{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
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mt-2 text-blue-600 hover:text-blue-700 p-0"
|
||||
onClick={() => {
|
||||
setSelectedDept(clearance.department);
|
||||
setClearanceStatus(clearance.status);
|
||||
setClearanceRemarks(clearance.remarks);
|
||||
setSelectedDept(dept);
|
||||
setClearanceStatus(displayStatus || 'Pending');
|
||||
setClearanceRemarks(displayRemarks || '');
|
||||
setClearanceAmount(displayAmount || 0);
|
||||
setClearanceType(displayType || 'Recovery');
|
||||
setClearanceFile(null);
|
||||
setShowClearanceDialog(true);
|
||||
}}
|
||||
>
|
||||
@ -505,7 +587,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -530,8 +613,36 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<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}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -539,21 +650,33 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<span>{doc.name || doc.fileName}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{doc.type || 'Document'}</TableCell>
|
||||
<TableCell>{doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : 'N/A'}</TableCell>
|
||||
<TableCell>{doc.uploadedBy || 'Dealer'}</TableCell>
|
||||
<TableCell>{doc.documentType || doc.type || 'Document'}</TableCell>
|
||||
<TableCell>{doc.createdAt ? formatDateTime(doc.createdAt) : 'N/A'}</TableCell>
|
||||
<TableCell>{doc.uploader?.fullName || doc.uploadedBy || 'Dealer'}</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>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
|
||||
No documents found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
})()
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
@ -576,7 +699,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<p className="font-medium text-slate-900">{log.action || log.status}</p>
|
||||
<span className="text-sm text-slate-600">{new Date(log.timestamp || log.createdAt).toLocaleString()}</span>
|
||||
<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.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]">
|
||||
<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="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>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Amount (₹)</Label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full mt-2 p-2 border border-slate-300 rounded-md"
|
||||
value={clearanceAmount}
|
||||
onChange={(e) => setClearanceAmount(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Type</Label>
|
||||
<Select value={clearanceType} onValueChange={(val: any) => setClearanceType(val)}>
|
||||
<SelectTrigger className="mt-2 border-slate-300">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-white">
|
||||
<SelectItem value="Recovery">Recovery (Dealer owes RE)</SelectItem>
|
||||
<SelectItem value="Payable">Payable (RE owes Dealer)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Remarks/Details</Label>
|
||||
<Textarea
|
||||
@ -779,6 +926,34 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Supporting Document (Optional)</Label>
|
||||
<div className="mt-2 text-sm text-slate-500 mb-2">
|
||||
Upload NOC, ledger statement, or evidence of dues.
|
||||
</div>
|
||||
<div className="relative group border-2 border-dashed border-slate-200 rounded-lg p-4 transition-colors hover:border-blue-300">
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => setClearanceFile(e.target.files?.[0] || null)}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
{clearanceFile ? (
|
||||
<div className="flex items-center gap-2 text-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">
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -799,6 +974,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DocumentPreviewModal
|
||||
isOpen={!!previewDocument}
|
||||
onClose={() => setPreviewDocument(null)}
|
||||
document={previewDocument}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -57,9 +57,9 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
||||
'DD AM': ['ASM'],
|
||||
'ZBH': ['ZBH'],
|
||||
'NBH': ['NBH'],
|
||||
'Legal Admin': ['Legal'],
|
||||
'DD Admin': ['DD Admin'],
|
||||
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'ASM', 'DD Lead']
|
||||
'Legal Admin': ['Legal', 'FNF Initiate'],
|
||||
'DD Admin': ['DD Admin', 'FNF Initiate'],
|
||||
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'ASM', 'DD Lead', 'FNF Initiate']
|
||||
};
|
||||
|
||||
const userStages = roleToStageMapping[currentUser.role] || [];
|
||||
|
||||
@ -10,10 +10,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||
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 { terminationService } from '../../services/termination.service';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
|
||||
interface TerminationDetailsProps {
|
||||
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)
|
||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
||||
|
||||
// Mock data - would come from API
|
||||
const request = {
|
||||
id: terminationId,
|
||||
dealerCode: 'DL-MH-025',
|
||||
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'
|
||||
};
|
||||
// Use actual data from backend
|
||||
const request = terminationData || {};
|
||||
|
||||
// Define internal names for mapping if needed, but backend strings are preferred
|
||||
|
||||
|
||||
// Mock documents by stage
|
||||
const stageDocuments: Record<string, any[]> = {
|
||||
@ -186,96 +157,91 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
const progressStages = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Request Initiated',
|
||||
name: 'Submitted',
|
||||
status: 'completed',
|
||||
date: '2025-10-15',
|
||||
description: 'Termination request created by ASM/Initiator',
|
||||
actionType: 'approved',
|
||||
actionBy: 'ASM - Mumbai Region',
|
||||
remarks: 'Termination request initiated due to severe breach of agreement. Multiple violations documented.',
|
||||
feedback: 'All evidence and documentation attached. Case requires urgent attention due to severity of violations.'
|
||||
description: 'Termination request initiated',
|
||||
date: '',
|
||||
actionType: '',
|
||||
actionBy: '',
|
||||
remarks: '',
|
||||
feedback: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
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',
|
||||
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
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'DD Lead & Legal Review',
|
||||
status: request.currentStage === 'DD Lead Legal' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Evaluation of SCN response'
|
||||
name: 'Personal Hearing',
|
||||
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 & Hearing'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'NBH Termination Approval',
|
||||
status: request.currentStage === 'NBH Final' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'NBH approves termination'
|
||||
name: 'NBH Final Approval',
|
||||
status: request.currentStage === 'NBH Final Approval' ? 'active' : ['CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'NBH final termination decision'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Legal - Termination Letter',
|
||||
status: request.currentStage === 'Legal Letter' ? 'active' : ['DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||
description: 'Legal team shares termination letter to DD-Lead and DD Admin'
|
||||
status: request.currentStage === 'Legal - Termination Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||
description: 'Legal team issues final termination letter'
|
||||
},
|
||||
{
|
||||
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',
|
||||
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 });
|
||||
};
|
||||
|
||||
const handleSubmitAction = () => {
|
||||
const handleSubmitAction = async () => {
|
||||
if (!remarks && actionDialog.type !== 'assign' && actionDialog.type !== 'pushfnf') {
|
||||
toast.error('Please provide remarks');
|
||||
return;
|
||||
@ -298,6 +264,14 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
if (actionDialog.type === 'approve') {
|
||||
await terminationService.updateTerminationStatus(terminationId, 'approve', remarks);
|
||||
} else {
|
||||
// Handle other actions
|
||||
}
|
||||
|
||||
const actionMessages = {
|
||||
approve: 'Request approved and forwarded',
|
||||
withdrawal: 'Request withdrawn successfully',
|
||||
@ -310,6 +284,12 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
setActionDialog({ open: false, type: null });
|
||||
setRemarks('');
|
||||
setAssignToUser('');
|
||||
fetchTermination();
|
||||
} catch (error) {
|
||||
toast.error('Failed to perform action');
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
@ -345,8 +325,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl">{terminationId}</h1>
|
||||
<p className="text-slate-600">{request.dealerName}</p>
|
||||
<h1 className="text-2xl">{request.requestId || terminationId}</h1>
|
||||
<p className="text-slate-600">{request.dealer?.businessName || request.dealerName}</p>
|
||||
</div>
|
||||
<Badge className={getSeverityColor(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>
|
||||
<Label className="text-slate-600">Dealer Code</Label>
|
||||
<p>{request.dealerCode}</p>
|
||||
<p>{request.dealer?.dealerCode?.code || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Dealer Name</Label>
|
||||
<p>{request.dealerName}</p>
|
||||
<p>{request.dealer?.businessName || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">GST</Label>
|
||||
<p>{request.gst}</p>
|
||||
<p>{request.dealer?.gstNumber || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-600">Address</Label>
|
||||
<p>{request.address}</p>
|
||||
<p>{request.dealer?.registeredAddress || request.dealer?.application?.address || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">City Category</Label>
|
||||
<p>{request.cityCategory}</p>
|
||||
<Label className="text-slate-600">City</Label>
|
||||
<p>{request.dealer?.application?.city || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Domain Name</Label>
|
||||
<p>{request.domainName}</p>
|
||||
<Label className="text-slate-600">District</Label>
|
||||
<p>{request.dealer?.application?.district?.name || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Dealership Name</Label>
|
||||
<p>{request.dealershipName}</p>
|
||||
<p>{request.dealer?.businessName || request.dealershipName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Sales Code</Label>
|
||||
<p>{request.salesCode}</p>
|
||||
<p>{request.dealer?.dealerCode?.salesCode || request.salesCode || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Service Code</Label>
|
||||
<p>{request.serviceCode}</p>
|
||||
<p>{request.dealer?.dealerCode?.serviceCode || request.serviceCode || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Accessories Code</Label>
|
||||
<p>{request.accessoriesCode}</p>
|
||||
<p>{request.dealer?.dealerCode?.accessoriesCode || request.accessoriesCode || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">GMA Code</Label>
|
||||
<p>{request.gmaCode}</p>
|
||||
<p>{request.dealer?.dealerCode?.gmaCode || request.gmaCode || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
<Label className="text-slate-600">LOA</Label>
|
||||
<p>{request.loa}</p>
|
||||
<p>{request.loa || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">LOI</Label>
|
||||
<p>{request.loi}</p>
|
||||
<p>{request.loi || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Last 6 Months Sales</Label>
|
||||
@ -574,23 +554,23 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Constitution</Label>
|
||||
<p>{request.constitution}</p>
|
||||
<p>{request.dealer?.constitutionType || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Dealership Type</Label>
|
||||
<p>{request.dealershipType}</p>
|
||||
<p>{request.dealer?.application?.businessType || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Type of Closure</Label>
|
||||
<p>{request.typeOfClosure}</p>
|
||||
<p>{request.typeOfClosure || 'Complete'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Format Category</Label>
|
||||
<p>{request.formatCategory}</p>
|
||||
<p>{request.formatCategory || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Dealer Score Card Band</Label>
|
||||
<p>{request.dealerScoreCardBand}</p>
|
||||
<p>{request.dealerScoreCardBand || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -607,15 +587,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<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>
|
||||
<Label className="text-slate-600">Sub Category</Label>
|
||||
<p>{request.subCategory}</p>
|
||||
<p>{request.subCategory || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Description</Label>
|
||||
<p>{request.description}</p>
|
||||
<p>{request.reason}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
@ -628,11 +608,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Submitted By</Label>
|
||||
<p>{request.submittedBy}</p>
|
||||
<p>{request.initiator?.fullName || 'System'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-600">Submitted On</Label>
|
||||
<p>{request.submittedOn}</p>
|
||||
<p>{formatDateTime(request.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -761,22 +741,37 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mockDocuments.map((doc) => (
|
||||
<TableRow key={doc.id}>
|
||||
{(() => {
|
||||
const allDocs = [
|
||||
...(request.documents || []),
|
||||
...(request.uploadedDocuments || [])
|
||||
];
|
||||
|
||||
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={doc.id || index}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-slate-500" />
|
||||
<span>{doc.name}</span>
|
||||
<span>{doc.name || doc.fileName}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{doc.type}</TableCell>
|
||||
<TableCell>{doc.uploadDate}</TableCell>
|
||||
<TableCell>{doc.uploader || '-'}</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>
|
||||
</Table>
|
||||
</CardContent>
|
||||
@ -792,19 +787,25 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{mockAuditLogs.map((log) => (
|
||||
<div key={log.id} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
|
||||
{(request.timeline || []).length > 0 ? (
|
||||
(request.timeline || []).map((log: any, index: number) => (
|
||||
<div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
|
||||
<div className="w-2 h-2 rounded-full bg-red-600 mt-2" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<p>{log.action}</p>
|
||||
<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}</p>
|
||||
{log.details && <p className="text-sm text-slate-500 mt-1">{log.details}</p>}
|
||||
<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>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
No history found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -11,6 +11,7 @@ import { Textarea } from '../ui/textarea';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { API } from '../../api/API';
|
||||
import { formatDateTime } from '../ui/utils';
|
||||
import { User } from '../../lib/mock-data';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@ -108,7 +109,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
// Outlet model has associate dealer? Let's check.
|
||||
// In my outlet.controller.ts, I included 'dealer'.
|
||||
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,
|
||||
reason: formData.reason,
|
||||
proposedLwd: formData.proposedLwd,
|
||||
@ -150,13 +151,16 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
if (!currentUser) return false;
|
||||
|
||||
const roleToStageMapping: Record<string, string[]> = {
|
||||
'DD Lead': ['DD Lead'],
|
||||
'RBM': ['RBM'],
|
||||
'ZBH': ['ZBH'],
|
||||
'NBH': ['NBH'],
|
||||
'Legal Admin': ['Legal'],
|
||||
'DD Admin': ['DD Admin'],
|
||||
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'DD Lead', 'CCO', 'CEO']
|
||||
'DD Lead': ['DD Lead Review'],
|
||||
'RBM': ['RBM Review'],
|
||||
'ZBH': ['ZBH Review'],
|
||||
'NBH': ['NBH Evaluation', 'NBH Final Approval'],
|
||||
'Legal Admin': ['Legal Verification', 'Legal - Termination Letter'],
|
||||
'Legal': ['Legal Verification'],
|
||||
'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] || [];
|
||||
@ -397,7 +401,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<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')}>
|
||||
{request.severity || 'Normal'}
|
||||
</Badge>
|
||||
@ -431,7 +436,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Submitted On</p>
|
||||
<p>{new Date(request.createdAt).toLocaleDateString()}</p>
|
||||
<p>{formatDateTime(request.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -471,7 +476,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<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)}>
|
||||
{request.status}
|
||||
</Badge>
|
||||
@ -491,7 +497,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Submitted On</p>
|
||||
<p>{new Date(request.createdAt).toLocaleDateString()}</p>
|
||||
<p>{formatDateTime(request.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -532,7 +538,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<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)}>
|
||||
{request.status}
|
||||
</Badge>
|
||||
@ -544,7 +551,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Closed On</p>
|
||||
<p>{new Date(request.updatedAt).toLocaleDateString()}</p>
|
||||
<p>{formatDateTime(request.updatedAt)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Termination Category</p>
|
||||
|
||||
@ -13,6 +13,7 @@ import { useState, useEffect } from 'react';
|
||||
import { User } from '../../lib/mock-data';
|
||||
import { toast } from 'sonner';
|
||||
import { onboardingService } from '../../services/onboarding.service';
|
||||
import { settlementService } from '../../services/settlement.service';
|
||||
|
||||
interface FinanceDashboardProps {
|
||||
currentUser: User | null;
|
||||
@ -22,36 +23,6 @@ interface FinanceDashboardProps {
|
||||
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 {
|
||||
id: string;
|
||||
department: string;
|
||||
@ -61,7 +32,9 @@ interface FinanceLineItem {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@ -71,20 +44,24 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await onboardingService.getApplications();
|
||||
// Filter for applications relevant to finance
|
||||
const financeApps = data.filter((app: any) => {
|
||||
const s = app.overallStatus || app.status;
|
||||
const stage = app.currentStage;
|
||||
return [
|
||||
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
||||
'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT',
|
||||
'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
|
||||
].includes(s) || stage === 'Finance';
|
||||
});
|
||||
setApplications(financeApps);
|
||||
const [payments, settlements, apps] = await Promise.all([
|
||||
settlementService.getOnboardingPayments(),
|
||||
settlementService.getFnFSettlements(),
|
||||
onboardingService.getApplications()
|
||||
]);
|
||||
|
||||
setOnboardingPayments(payments);
|
||||
setFnfSettlements(settlements);
|
||||
|
||||
// Filter for applications needing FDD review
|
||||
const fddApps = apps.filter((app: any) =>
|
||||
app.status === 'FDD_VERIFICATION' || app.overallStatus === 'FDD Verification' || app.currentStage === 'FDD'
|
||||
);
|
||||
setPendingAudits(fddApps);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
toast.error('Failed to load dashboard data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -102,6 +79,50 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
});
|
||||
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 = () => {
|
||||
if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) {
|
||||
@ -127,54 +148,6 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
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();
|
||||
|
||||
if (loading) {
|
||||
@ -326,29 +299,29 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<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">
|
||||
{app.status}
|
||||
{app.paymentStatus}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-slate-600">Location</p>
|
||||
<p>{app.city || app.preferredLocation}, {app.state}</p>
|
||||
<p className="text-slate-600">Type</p>
|
||||
<p>{app.paymentType}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Stage</p>
|
||||
<p className="text-slate-600">Amount</p>
|
||||
<p className="text-amber-700 font-bold">
|
||||
{app.status === 'PAYMENT_VERIFICATION' ? 'Security Deposit' : 'First Fill'}
|
||||
₹{parseFloat(app.amount).toLocaleString('en-IN')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Email</p>
|
||||
<p className="truncate">{app.email}</p>
|
||||
<p className="text-slate-600">Created On</p>
|
||||
<p>{new Date(app.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -399,27 +372,29 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<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">
|
||||
{getRelevantPaymentStatus(app)}
|
||||
{app.paymentStatus}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-slate-600">Location</p>
|
||||
<p>{app.city || app.preferredLocation}, {app.state}</p>
|
||||
<p className="text-slate-600">Type</p>
|
||||
<p>{app.paymentType}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Status</p>
|
||||
<p className="text-green-700 font-bold">Payment Verified</p>
|
||||
<p className="text-slate-600">Amount</p>
|
||||
<p className="text-green-700 font-bold">
|
||||
₹{parseFloat(app.amount).toLocaleString('en-IN')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Email</p>
|
||||
<p className="truncate">{app.email}</p>
|
||||
<p className="text-slate-600">Verified On</p>
|
||||
<p>{app.verificationDate ? new Date(app.verificationDate).toLocaleDateString() : 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -476,29 +451,33 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h3 className="text-lg font-bold">{fnf.id}</h3>
|
||||
<Badge variant="outline">{fnf.type}</Badge>
|
||||
<h3 className="text-lg font-bold">{fnf.resignation?.resignationId || fnf.id}</h3>
|
||||
<Badge variant="outline">{fnf.resignation ? 'Resignation' : fnf.terminationRequest ? 'Termination' : 'General'}</Badge>
|
||||
<Badge className="bg-orange-100 text-orange-700 border-orange-300">
|
||||
{fnf.status}
|
||||
</Badge>
|
||||
</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>
|
||||
<p className="text-slate-600">Dealer Name</p>
|
||||
<p className="font-medium">{fnf.dealerName}</p>
|
||||
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Name</p>
|
||||
<p className="font-semibold text-slate-900">{fnf.outlet?.dealer?.fullName || fnf.outlet?.name || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Dealer Code</p>
|
||||
<p>{fnf.dealerCode}</p>
|
||||
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Code</p>
|
||||
<p className="font-mono text-xs font-bold text-blue-600">{fnf.outlet?.code || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-600">Location</p>
|
||||
<p>{fnf.location}</p>
|
||||
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Location</p>
|
||||
<p className="text-slate-900">{fnf.outlet?.city || 'N/A'}, {fnf.outlet?.state || ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-green-700">{fnf.departmentsResponded}/{fnf.totalDepartments} Departments Responded</span>
|
||||
<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-emerald-500" />
|
||||
<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 className="ml-4 flex flex-col gap-2">
|
||||
@ -610,7 +589,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Financial Settlement Summary</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedFnF?.id} - {selectedFnF?.dealerName} ({selectedFnF?.dealerCode})
|
||||
{selectedFnF?.resignation?.resignationId || selectedFnF?.id} - {selectedFnF?.dealerName} ({selectedFnF?.dealerCode})
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -770,7 +749,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
<DialogHeader>
|
||||
<DialogTitle>Full Settlement Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedFnF?.id} - {selectedFnF?.dealerName}
|
||||
{selectedFnF?.resignation?.resignationId || selectedFnF?.id} - {selectedFnF?.dealerName}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -779,15 +758,15 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
<Card className="bg-slate-50 border-none">
|
||||
<CardContent className="pt-6 grid grid-cols-3 gap-6">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase">Dealer Code</p>
|
||||
<p className="text-lg font-bold">{selectedFnF.dealerCode}</p>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Dealer Code</p>
|
||||
<p className="text-lg font-bold">{selectedFnF.outlet?.code || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase">Location</p>
|
||||
<p className="text-lg font-bold">{selectedFnF.location}</p>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Location</p>
|
||||
<p className="text-lg font-bold">{selectedFnF.outlet?.city || 'N/A'}</p>
|
||||
</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">
|
||||
{selectedFnF.status}
|
||||
</Badge>
|
||||
@ -805,51 +784,63 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-500">Inventory Value</span>
|
||||
<span className="font-bold">₹12,45,000</span>
|
||||
<span className="text-slate-500">Security Deposit Credits</span>
|
||||
<span className="font-bold text-slate-900">₹{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-500">Security Deposit</span>
|
||||
<span className="font-bold">₹2,00,000</span>
|
||||
<span className="text-slate-500">Other Payable Credits</span>
|
||||
<span className="font-bold text-slate-900">₹0</span>
|
||||
</div>
|
||||
<div className="border-t pt-2 flex justify-between font-bold text-blue-700">
|
||||
<span>Total Payables</span>
|
||||
<span>₹14,45,000</span>
|
||||
<span>₹{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-red-100">
|
||||
<CardHeader className="bg-red-50/50 pb-2">
|
||||
<CardTitle className="text-sm font-bold flex items-center gap-2">
|
||||
<TrendingDown className="w-4 h-4 text-red-600" />
|
||||
Recoveries Check
|
||||
<Card className="border-red-100 shadow-sm">
|
||||
<CardHeader className="bg-red-50/30 pb-2 border-b border-red-50">
|
||||
<CardTitle className="text-sm font-bold flex items-center gap-2 text-red-700">
|
||||
<TrendingDown className="w-4 h-4" />
|
||||
Receivables Check
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-500">Outstanding Invoices</span>
|
||||
<span className="font-bold">₹8,50,000</span>
|
||||
<span className="text-slate-500">Departmental Dues (Recoverable)</span>
|
||||
<span className="font-bold text-red-600">₹{parseFloat(selectedFnF.totalReceivables || 0).toLocaleString('en-IN')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-500">Spares & Oil Dues</span>
|
||||
<span className="font-bold">₹1,20,000</span>
|
||||
<span className="text-slate-500">Outstanding Invoices</span>
|
||||
<span className="font-bold text-slate-900">₹0</span>
|
||||
</div>
|
||||
<div className="border-t pt-2 flex justify-between font-bold text-red-700">
|
||||
<span>Total Recoveries</span>
|
||||
<span>₹9,70,000</span>
|
||||
<span>₹{parseFloat(selectedFnF.totalReceivables || 0).toLocaleString('en-IN')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-900 text-white rounded-lg flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-green-300 uppercase font-bold">Estimated Net Settlement</p>
|
||||
<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>
|
||||
<div className="p-5 bg-slate-900 text-white rounded-xl flex items-center justify-between shadow-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`p-3 rounded-lg ${parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'bg-emerald-500/20' : 'bg-rose-500/20'}`}>
|
||||
<IndianRupee className={`w-6 h-6 ${parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'text-emerald-400' : 'text-rose-400'}`} />
|
||||
</div>
|
||||
<Button className="bg-green-600 hover:bg-green-500">
|
||||
Generate PDF
|
||||
<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>
|
||||
<Button className="bg-blue-600 hover:bg-blue-500 px-6 font-bold shadow-md transition-all active:scale-95">
|
||||
Generate PDF Summary
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
if (format === 'full') {
|
||||
return d.toLocaleString('en-IN', options);
|
||||
}
|
||||
|
||||
return d.toLocaleDateString('en-IN', options);
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export const resignationService = {
|
||||
getResignationById: async (id: string) => {
|
||||
try {
|
||||
const response: any = await API.getResignationById(id);
|
||||
return response.data?.data || response.data;
|
||||
return response.data?.resignation || response.data?.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Get resignation error:', error);
|
||||
throw error;
|
||||
|
||||
34
src/services/settlement.service.ts
Normal file
34
src/services/settlement.service.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@ -3,7 +3,7 @@ import { API } from '../api/API';
|
||||
export const terminationService = {
|
||||
getTerminationById: async (id: string) => {
|
||||
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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user