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
|
// Resignation
|
||||||
getResignationById: (id: string) => client.get(`/resignation/${id}`),
|
getResignationById: (id: string) => client.get(`/resignation/${id}`),
|
||||||
updateClearance: (id: string, data: any) => client.post(`/resignation/${id}/clearance`, data),
|
updateClearance: (id: string, data: any) => client.put(`/resignation/${id}/clearance`, data, {
|
||||||
|
headers: data instanceof FormData ? { 'Content-Type': 'multipart/form-data' } : {}
|
||||||
|
}),
|
||||||
updateResignationStatus: (id: string, data: any) => client.post(`/resignation/${id}/status`, data),
|
updateResignationStatus: (id: string, data: any) => client.post(`/resignation/${id}/status`, data),
|
||||||
|
|
||||||
// Termination
|
// Termination
|
||||||
@ -144,6 +146,8 @@ export const API = {
|
|||||||
createTermination: (data: any) => client.post('/termination', data),
|
createTermination: (data: any) => client.post('/termination', data),
|
||||||
updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
|
updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
|
||||||
|
|
||||||
|
getOnboardingPayments: () => client.get('/settlement/onboarding'),
|
||||||
|
updatePayment: (id: string, data: any) => client.put(`/settlement/payments/${id}`, data),
|
||||||
getFnFSettlements: () => client.get('/settlement/fnf'),
|
getFnFSettlements: () => client.get('/settlement/fnf'),
|
||||||
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
|
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
|
||||||
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
|
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
|
||||||
|
|||||||
@ -1183,18 +1183,17 @@ export const ApplicationDetails = () => {
|
|||||||
name: 'Statutory Documents',
|
name: 'Statutory Documents',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
stages: [
|
stages: [
|
||||||
{ id: '11b-1', name: 'GST Certificate', status: isDocumentUploaded('Statutory GST') || isDocumentUploaded('GST Certificate') ? 'completed' : 'active', description: 'GST details' },
|
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
|
||||||
{ id: '11b-2', name: 'PAN Card', status: isDocumentUploaded('Statutory PAN') || isDocumentUploaded('PAN Card') ? 'completed' : 'active', description: 'PAN details' },
|
{ id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
|
||||||
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Statutory Nodal') || isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal details' },
|
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
|
||||||
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Statutory Check') || isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Bank verification' },
|
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
|
||||||
{ id: '11b-5', name: 'Partnership Deed', status: isDocumentUploaded('Statutory Partnership') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Legal constitution' },
|
{ id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
|
||||||
{ id: '11b-6', name: 'Firm Registration', status: isDocumentUploaded('Statutory Firm Reg') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'RoC/Firm reg' },
|
{ id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
|
||||||
{ id: '11b-7', name: 'Virtual Code', status: isDocumentUploaded('Statutory Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Oracle setup' },
|
{ id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
|
||||||
{ id: '11b-8', name: 'Domain ID', status: isDocumentUploaded('Statutory Domain') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Email setup' },
|
{ id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
|
||||||
{ id: '11b-9', name: 'MSD Configuration', status: isDocumentUploaded('Statutory MSD') || isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Multiple Security Deposit' },
|
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
|
||||||
{ id: '11b-10', name: 'LOI Acknowledgement', status: isDocumentUploaded('Statutory LOI Ack') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI Signed copy' },
|
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
|
||||||
{ id: '11b-11', name: 'Board Resolution', status: isDocumentUploaded('Board Resolution') || isDocumentUploaded('Authorization Proof') ? 'completed' : 'active', description: 'Legal authorization' },
|
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' }
|
||||||
{ id: '11b-12', name: 'Consolidated Approval', status: application.statutoryStatus === 'COMPLETED' ? 'completed' : 'active', description: 'Managerial sign-off' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { User as UserType } from '../../lib/mock-data';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface ConstitutionalChangeDetailsProps {
|
interface ConstitutionalChangeDetailsProps {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
@ -20,34 +21,30 @@ interface ConstitutionalChangeDetailsProps {
|
|||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow stages as per the process flow
|
// Workflow stages as per the process flow (SRS 12.2.4)
|
||||||
const workflowStages = [
|
const workflowStages = [
|
||||||
{ id: 1, name: 'Request Created', key: 'created', role: 'Dealer' },
|
{ id: 1, name: 'Submitted', key: 'submitted', role: 'Dealer' },
|
||||||
{ id: 2, name: 'ASM Review', key: 'asm', role: 'ASM' },
|
{ id: 2, name: 'ASM Review', key: 'asm-review', role: 'ASM' },
|
||||||
{ id: 3, name: 'RBM Review', key: 'rbm', role: 'RBM' },
|
{ id: 3, name: 'ZM/RBM Review', key: 'zm-rbm-review', role: 'ZM/RBM' },
|
||||||
{ id: 4, name: 'DD ZM Review', key: 'dd-zm', role: 'DD-ZM' },
|
{ id: 4, name: 'ZBH Review', key: 'zbh-review', role: 'ZBH' },
|
||||||
{ id: 5, name: 'ZBH Review', key: 'zbh', role: 'ZBH' },
|
{ id: 5, name: 'DD Lead Review', key: 'lead-review', role: 'DD Lead' },
|
||||||
{ id: 6, name: 'DD Lead Review', key: 'dd-lead', role: 'DD Lead' },
|
{ id: 6, name: 'DD Head Review', key: 'head-review', role: 'DD Head' },
|
||||||
{ id: 7, name: 'FDD Review', key: 'fdd', role: 'FDD' },
|
{ id: 7, name: 'NBH Approval', key: 'nbh-approval', role: 'NBH' },
|
||||||
{ id: 8, name: 'DD Head Review', key: 'dd-head', role: 'DD Head' },
|
{ id: 8, name: 'Legal Review', key: 'legal-review', role: 'Legal Team' },
|
||||||
{ id: 9, name: 'NBH Review', key: 'nbh', role: 'NBH' },
|
{ id: 9, name: 'Completed', key: 'completed', role: 'System' }
|
||||||
{ id: 10, name: 'Docs Collection by DD H.O', key: 'docs-collection', role: 'DD H.O' },
|
|
||||||
{ id: 11, name: 'New Code Creation', key: 'code-creation', role: 'DD Admin' },
|
|
||||||
{ id: 12, name: 'New LOA Issuance', key: 'loa-issuance', role: 'DD Admin' },
|
|
||||||
{ id: 13, name: 'Closure of Request', key: 'closure', role: 'System' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Document requirements mapping (same as in ConstitutionalChangePage)
|
// Document requirements mapping (same as in ConstitutionalChangePage)
|
||||||
const documentRequirements: Record<string, number[]> = {
|
const documentRequirements: Record<string, number[]> = {
|
||||||
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
|
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
|
||||||
'LLP': [1, 2, 3, 7, 8, 9, 10, 16],
|
'LLP': [1, 2, 3, 7, 8, 9, 10, 11, 16],
|
||||||
'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16],
|
'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16],
|
||||||
'Proprietorship': [1, 2, 3, 10, 16]
|
'Proprietorship': [1, 2, 3, 10, 16]
|
||||||
};
|
};
|
||||||
|
|
||||||
const documentNames: Record<number, string> = {
|
const documentNames: Record<number, string> = {
|
||||||
1: 'GST',
|
1: 'GST Certificate',
|
||||||
2: 'Firm Pan Copy',
|
2: 'Firm PAN Copy',
|
||||||
3: 'Self attested KYC\'s',
|
3: 'Self attested KYC\'s',
|
||||||
4: 'Partnership Agreement (Notarised)',
|
4: 'Partnership Agreement (Notarised)',
|
||||||
5: 'MOA (Applicable for Only Pvt.Ltd)',
|
5: 'MOA (Applicable for Only Pvt.Ltd)',
|
||||||
@ -71,6 +68,7 @@ const getTypeColor = (type: string) => {
|
|||||||
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
|
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
|
||||||
case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300';
|
case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300';
|
||||||
case 'LLP': return 'bg-indigo-100 text-indigo-700 border-indigo-300';
|
case 'LLP': return 'bg-indigo-100 text-indigo-700 border-indigo-300';
|
||||||
|
case 'Private Limited':
|
||||||
case 'Pvt Ltd': return 'bg-cyan-100 text-cyan-700 border-cyan-300';
|
case 'Pvt Ltd': return 'bg-cyan-100 text-cyan-700 border-cyan-300';
|
||||||
default: return 'bg-slate-100 text-slate-700 border-slate-300';
|
default: return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||||
}
|
}
|
||||||
@ -78,7 +76,7 @@ const getTypeColor = (type: string) => {
|
|||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
if (status === 'Completed' || status === 'Verified') return 'bg-green-100 text-green-700 border-green-300';
|
if (status === 'Completed' || status === 'Verified') return 'bg-green-100 text-green-700 border-green-300';
|
||||||
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress' || status === 'Submitted') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
||||||
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
|
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
|
||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||||
};
|
};
|
||||||
@ -136,20 +134,18 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
// Get required documents for this request
|
// Get required documents for this request
|
||||||
const requiredDocs = documentRequirements[request.changeType] || [];
|
const requiredDocs = documentRequirements[request.changeType] || [];
|
||||||
|
|
||||||
// Calculate current stage index
|
// Calculate current stage index mapping to backend stages
|
||||||
const getCurrentStageIndex = () => {
|
const getCurrentStageIndex = () => {
|
||||||
const stageMap: Record<string, number> = {
|
const stageMap: Record<string, number> = {
|
||||||
'Dealer': 1,
|
'Submitted': 1,
|
||||||
'ASM': 2,
|
'ASM Review': 2,
|
||||||
'RBM': 3,
|
'ZM/RBM Review': 3,
|
||||||
'DD-ZM': 4,
|
'ZBH Review': 4,
|
||||||
'ZBH': 5,
|
'DD Lead Review': 5,
|
||||||
'DD Lead': 6,
|
'DD Head Review': 6,
|
||||||
'FDD': 7,
|
'NBH Approval': 7,
|
||||||
'DD Head': 8,
|
'Legal Review': 8,
|
||||||
'NBH': 9,
|
'Completed': 9
|
||||||
'DD H.O': 10,
|
|
||||||
'Closed': 13
|
|
||||||
};
|
};
|
||||||
return stageMap[request.currentStage] || 1;
|
return stageMap[request.currentStage] || 1;
|
||||||
};
|
};
|
||||||
@ -243,7 +239,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 text-sm mb-1">Request Information</p>
|
<p className="text-slate-600 text-sm mb-1">Request Information</p>
|
||||||
<p className="text-slate-900 text-sm">Submitted: {new Date(request.createdAt).toLocaleDateString()}</p>
|
<p className="text-slate-900 text-sm">Submitted: {formatDateTime(request.createdAt)}</p>
|
||||||
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName || 'Dealer'}</p>
|
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName || 'Dealer'}</p>
|
||||||
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage}</p>
|
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -469,7 +465,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
{doc.fileName || doc.name}
|
{doc.fileName || doc.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-600">
|
<TableCell className="text-slate-600">
|
||||||
{new Date(doc.uploadedOn || doc.createdAt).toLocaleDateString()}
|
{formatDateTime(doc.uploadedOn || doc.createdAt)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-600">
|
<TableCell className="text-slate-600">
|
||||||
{doc.uploadedBy || 'Dealer'}
|
{doc.uploadedBy || 'Dealer'}
|
||||||
@ -533,7 +529,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-600 text-sm mt-2">{entry.comments || entry.remarks || 'No remarks provided'}</p>
|
<p className="text-slate-600 text-sm mt-2">{entry.comments || entry.remarks || 'No remarks provided'}</p>
|
||||||
<p className="text-slate-500 text-sm mt-1">{new Date(entry.date || entry.createdAt || entry.timestamp).toLocaleString()}</p>
|
<p className="text-slate-500 text-sm mt-1">{formatDateTime(entry.date || entry.createdAt || entry.timestamp)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { User as UserType } from '../../lib/mock-data';
|
import { User as UserType } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface ConstitutionalChangePageProps {
|
interface ConstitutionalChangePageProps {
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
@ -465,7 +466,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
|
<div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
|
||||||
<div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div>
|
<div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -677,7 +678,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
|
<div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -30,9 +30,17 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Edit2,
|
Edit2,
|
||||||
Trash2,
|
Trash2,
|
||||||
Save
|
Save,
|
||||||
|
Paperclip
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
|
|
||||||
|
const ALL_DEPARTMENTS = [
|
||||||
|
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
|
||||||
|
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
|
||||||
|
'Legal', 'Quality', 'Logistics', 'Customer Relations'
|
||||||
|
];
|
||||||
|
|
||||||
interface FinanceFnFDetailsPageProps {
|
interface FinanceFnFDetailsPageProps {
|
||||||
fnfId: string;
|
fnfId: string;
|
||||||
@ -57,6 +65,24 @@ const getDepartmentStatusColor = (status: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateString: any) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return '-';
|
||||||
|
return date.toLocaleString('en-IN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface FinancialLineItem {
|
interface FinancialLineItem {
|
||||||
id: string;
|
id: string;
|
||||||
department: string;
|
department: string;
|
||||||
@ -72,6 +98,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
|
const [payableItems, setPayableItems] = useState<FinancialLineItem[]>([]);
|
||||||
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
|
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
|
||||||
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
|
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
|
||||||
|
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
@ -83,11 +110,11 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const response = await API.getFnFSettlementById(fnfId);
|
const response = await API.getFnFSettlementById(fnfId);
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const s = data.settlement;
|
const s = data.fnf;
|
||||||
setFnfCase({
|
setFnfCase({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
caseNumber: s.id.substring(0, 8).toUpperCase(),
|
caseNumber: s.id.substring(0, 8).toUpperCase(),
|
||||||
dealerName: s.outlet?.dealer?.name || 'N/A',
|
dealerName: s.outlet?.dealer?.fullName || s.outlet?.name || 'N/A',
|
||||||
dealerCode: s.outlet?.code || 'N/A',
|
dealerCode: s.outlet?.code || 'N/A',
|
||||||
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
||||||
terminationType: s.resignationId ? 'Resignation' : 'Termination',
|
terminationType: s.resignationId ? 'Resignation' : 'Termination',
|
||||||
@ -95,23 +122,40 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD',
|
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD',
|
||||||
status: s.status,
|
status: s.status,
|
||||||
bankDetails: {
|
bankDetails: {
|
||||||
accountName: s.outlet?.dealer?.name || 'N/A',
|
accountName: s.outlet?.dealer?.fullName || 'N/A',
|
||||||
accountNumber: 'N/A', // These should come from dealer model in a real app
|
accountNumber: 'N/A', // These should come from dealer model in a real app
|
||||||
ifscCode: 'N/A',
|
ifscCode: 'N/A',
|
||||||
bankName: 'N/A',
|
bankName: 'N/A',
|
||||||
branch: 'N/A'
|
branch: 'N/A'
|
||||||
},
|
},
|
||||||
departmentResponses: (s.lineItems || []).map((li: any) => ({
|
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
|
||||||
id: li.id,
|
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
|
||||||
departmentName: li.department,
|
const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
|
||||||
status: 'Submitted',
|
const totalAmount = relatedItems.reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount)), 0);
|
||||||
remarks: li.remarks,
|
const hasPayable = relatedItems.some((li: any) => li.amount < 0);
|
||||||
amount: Math.abs(li.amount),
|
|
||||||
amountType: li.amount < 0 ? 'Payable Amount' : 'Recovery Amount'
|
return {
|
||||||
})),
|
id: c?.id || `dept-${deptName}`,
|
||||||
|
departmentName: deptName,
|
||||||
|
status: c?.status || 'Pending',
|
||||||
|
remarks: c?.remarks || '-',
|
||||||
|
submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : '-',
|
||||||
|
amount: totalAmount,
|
||||||
|
amountType: hasPayable ? 'Payable Amount' : totalAmount > 0 ? 'Recovery Amount' : null,
|
||||||
|
supportingDocument: c?.supportingDocument || null
|
||||||
|
};
|
||||||
|
}),
|
||||||
documents: [
|
documents: [
|
||||||
{ name: 'Resignation Letter.pdf', size: '245 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Resignation' },
|
{ name: 'Resignation Letter.pdf', size: 'N/A', uploadedOn: formatDateTime(s.createdAt), type: 'Resignation', url: '#' },
|
||||||
{ name: 'Inventory Report.xlsx', size: '856 KB', uploadedOn: new Date(s.createdAt).toLocaleDateString(), type: 'Inventory' }
|
...(s.clearances || [])
|
||||||
|
.filter((c: any) => c.supportingDocument)
|
||||||
|
.map((c: any) => ({
|
||||||
|
name: c.supportingDocument.split('/').pop(),
|
||||||
|
size: 'N/A',
|
||||||
|
uploadedOn: formatDateTime(c.clearedAt),
|
||||||
|
type: `${c.department} Proof`,
|
||||||
|
url: c.supportingDocument
|
||||||
|
}))
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1254,7 +1298,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{dept.amountType ? (
|
{dept.amountType ? (
|
||||||
<Badge variant={dept.amountType === 'Recovery Amount' ? 'destructive' : 'default'}>
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={dept.amountType === 'Recovery'
|
||||||
|
? 'bg-red-50 text-red-700 border-red-200'
|
||||||
|
: 'bg-green-50 text-green-700 border-green-200'}
|
||||||
|
>
|
||||||
{dept.amountType}
|
{dept.amountType}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
@ -1263,7 +1312,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{dept.amount ? (
|
{dept.amount ? (
|
||||||
<span className={dept.amountType === 'Recovery Amount' ? 'text-red-600' : 'text-green-600'}>
|
<span className={`${dept.amountType === 'Recovery' ? 'text-red-600 font-bold' : 'text-green-600 font-bold'}`}>
|
||||||
₹{dept.amount.toLocaleString('en-IN')}
|
₹{dept.amount.toLocaleString('en-IN')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@ -1271,7 +1320,24 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{dept.submittedDate || '-'}</TableCell>
|
<TableCell>{dept.submittedDate || '-'}</TableCell>
|
||||||
<TableCell className="max-w-xs truncate">{dept.remarks || '-'}</TableCell>
|
<TableCell className="max-w-xs truncate">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span>{dept.remarks || '-'}</span>
|
||||||
|
{dept.supportingDocument && (
|
||||||
|
<button
|
||||||
|
onClick={() => setPreviewDocument({
|
||||||
|
fileName: `${dept.departmentName}_Proof`,
|
||||||
|
filePath: dept.supportingDocument,
|
||||||
|
documentType: 'Departmental Clearance Proof'
|
||||||
|
})}
|
||||||
|
className="flex items-center gap-1 text-[10px] text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
<Paperclip className="w-3 h-3" />
|
||||||
|
View Proof
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@ -1606,6 +1672,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DocumentPreviewModal
|
||||||
|
isOpen={!!previewDocument}
|
||||||
|
onClose={() => setPreviewDocument(null)}
|
||||||
|
document={previewDocument}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
|
|
||||||
const getMappedData = (s: any) => ({
|
const getMappedData = (s: any) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
|
caseId: s.resignation?.resignationId || s.id,
|
||||||
dealerCode: s.outlet?.code || 'N/A',
|
dealerCode: s.outlet?.code || 'N/A',
|
||||||
dealerName: s.outlet?.dealer?.name || 'N/A',
|
dealerName: s.outlet?.dealer?.name || 'N/A',
|
||||||
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
||||||
@ -201,13 +202,18 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-sm text-slate-600">Net Receivable</CardTitle>
|
<CardTitle className="text-sm text-slate-600">Net Financial Position</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-slate-900 text-2xl">₹2.5L</div>
|
<div className={`text-2xl ${displaySettlements.reduce((sum, s) => sum + (s.financialData.netAmount || 0), 0) < 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
₹{Math.abs(displaySettlements.reduce((sum, s) => sum + (s.financialData.netAmount || 0), 0)).toLocaleString('en-IN')}
|
||||||
|
</div>
|
||||||
<TrendingUp className="w-8 h-8 text-purple-600" />
|
<TrendingUp className="w-8 h-8 text-purple-600" />
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-[10px] text-slate-500 mt-1">
|
||||||
|
{displaySettlements.reduce((sum, s) => sum + (s.financialData.netAmount || 0), 0) < 0 ? 'Net Recovery' : 'Net Payable'}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -264,7 +270,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
<TableRow key={fnfCase.id}>
|
<TableRow key={fnfCase.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-slate-900">{fnfCase.id}</div>
|
<div className="text-slate-900">{fnfCase.caseId}</div>
|
||||||
<div className="text-sm text-slate-500">{fnfCase.dealerCode}</div>
|
<div className="text-sm text-slate-500">{fnfCase.dealerCode}</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@ -8,10 +8,11 @@ import { Label } from '../ui/label';
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||||
import { Progress } from '../ui/progress';
|
import { Progress } from '../ui/progress';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { User, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
import { User, mockAuditLogs } from '../../lib/mock-data';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
|
|
||||||
interface FnFDetailsProps {
|
interface FnFDetailsProps {
|
||||||
fnfId: string;
|
fnfId: string;
|
||||||
@ -24,6 +25,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false);
|
||||||
|
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
@ -35,14 +37,14 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
const response = await API.getFnFSettlementById(fnfId);
|
const response = await API.getFnFSettlementById(fnfId);
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const s = data.settlement;
|
const s = data.fnf;
|
||||||
// Map backend data to UI format
|
// Map backend data to UI format
|
||||||
const mappedCase = {
|
const mappedCase = {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
caseNumber: s.id.substring(0, 8).toUpperCase(),
|
caseNumber: s.resignation?.resignationId || s.id,
|
||||||
status: s.status,
|
status: s.status,
|
||||||
requestType: s.resignationId ? 'Resignation' : 'Termination',
|
requestType: s.resignationId ? 'Resignation' : 'Termination',
|
||||||
dealerName: s.outlet?.dealer?.name || 'N/A',
|
dealerName: s.outlet?.dealer?.fullName || 'N/A',
|
||||||
dealerCode: s.outlet?.code || 'N/A',
|
dealerCode: s.outlet?.code || 'N/A',
|
||||||
dealershipName: s.outlet?.name || 'N/A',
|
dealershipName: s.outlet?.name || 'N/A',
|
||||||
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
location: s.outlet?.city || s.outlet?.location || 'N/A',
|
||||||
@ -51,19 +53,43 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
lastOperationalDateSales: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
|
lastOperationalDateSales: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
|
||||||
lastOperationalDateServices: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
|
lastOperationalDateServices: s.resignation?.lastWorkingDay || s.terminationRequest?.effectiveDate || 'N/A',
|
||||||
typeOfClosure: s.resignationId ? 'Voluntary' : 'Involuntary',
|
typeOfClosure: s.resignationId ? 'Voluntary' : 'Involuntary',
|
||||||
gst: s.outlet?.dealer?.pan || 'N/A', // Using PAN as placeholder if GST not available
|
gst: s.outlet?.dealer?.dealerProfile?.gstNumber || 'N/A',
|
||||||
|
pan: s.outlet?.dealer?.dealerProfile?.panNumber || 'N/A',
|
||||||
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
|
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
|
||||||
totalPayableAmount: parseFloat(s.totalPayables) || 0,
|
totalPayableAmount: parseFloat(s.totalPayables) || 0,
|
||||||
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
|
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
|
||||||
departmentResponses: (s.lineItems || []).map((li: any) => ({
|
departmentResponses: [
|
||||||
id: li.id,
|
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
|
||||||
departmentName: li.department,
|
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
|
||||||
status: li.remarks && li.remarks.toLowerCase().includes('no dues') ? 'No Dues' : (li.amount > 0 ? 'Dues' : 'Pending'),
|
'Legal', 'Quality', 'Logistics', 'Customer Relations'
|
||||||
amountType: li.amount > 0 ? 'Recovery Amount' : null,
|
].map((deptName: string) => {
|
||||||
amount: Math.abs(parseFloat(li.amount)) || 0,
|
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
|
||||||
submittedDate: li.updatedAt ? new Date(li.updatedAt).toLocaleDateString() : null,
|
const lineItem = (s.lineItems || []).find((li: any) => li.department === deptName);
|
||||||
remarks: li.remarks
|
|
||||||
|
return {
|
||||||
|
id: c?.id || `dept-${deptName}`,
|
||||||
|
departmentName: deptName,
|
||||||
|
status: c?.status || 'Pending',
|
||||||
|
amountType: lineItem ? (parseFloat(lineItem.amount) > 0 ? 'Recovery' : 'Payable') : null,
|
||||||
|
amount: lineItem ? Math.abs(parseFloat(lineItem.amount)) : 0,
|
||||||
|
submittedDate: c?.clearedAt ? new Date(c.clearedAt).toLocaleString() : null,
|
||||||
|
remarks: c?.remarks || '-',
|
||||||
|
supportingDocument: c?.supportingDocument || null
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
documents: [
|
||||||
|
{ id: 'res-letter', name: 'Resignation Letter.pdf', type: 'Resignation', uploadDate: new Date(s.createdAt).toLocaleDateString(), status: 'Verified', url: '#' },
|
||||||
|
...(s.clearances || [])
|
||||||
|
.filter((c: any) => c.supportingDocument)
|
||||||
|
.map((c: any) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.supportingDocument.split('/').pop(),
|
||||||
|
type: `${c.department} Proof`,
|
||||||
|
uploadDate: new Date(c.clearedAt).toLocaleDateString(),
|
||||||
|
status: 'Attached',
|
||||||
|
url: c.supportingDocument
|
||||||
}))
|
}))
|
||||||
|
]
|
||||||
};
|
};
|
||||||
setFnfCase(mappedCase);
|
setFnfCase(mappedCase);
|
||||||
}
|
}
|
||||||
@ -128,9 +154,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
|
|
||||||
const getDepartmentStatusColor = (status: string) => {
|
const getDepartmentStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'No Dues':
|
case 'NOC Submitted':
|
||||||
return 'bg-green-100 text-green-700 border-green-300';
|
return 'bg-green-100 text-green-700 border-green-300';
|
||||||
case 'Dues':
|
case 'Dues Pending':
|
||||||
return 'bg-red-100 text-red-700 border-red-300';
|
return 'bg-red-100 text-red-700 border-red-300';
|
||||||
case 'Pending':
|
case 'Pending':
|
||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||||
@ -169,7 +195,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
onClick={() => navigate(`/worknotes/fnf/${fnfId}`, {
|
onClick={() => navigate(`/worknotes/fnf/${fnfId}`, {
|
||||||
state: {
|
state: {
|
||||||
applicationName: fnfCase.dealerName || 'F&F Settlement',
|
applicationName: fnfCase.dealerName || 'F&F Settlement',
|
||||||
registrationNumber: fnfId || '',
|
registrationNumber: fnfCase.caseNumber || '',
|
||||||
participants: fnfCase.participants || []
|
participants: fnfCase.participants || []
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@ -200,15 +226,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<Progress value={progressPercentage} className="h-3" />
|
<Progress value={progressPercentage} className="h-3" />
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 text-sm">No Dues</p>
|
<p className="text-slate-600 text-sm">NOC Submitted</p>
|
||||||
<p className="text-2xl text-green-600">
|
<p className="text-2xl text-green-600">
|
||||||
{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length}
|
{fnfCase.departmentResponses.filter((d: any) => d.status === 'NOC Submitted').length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 text-sm">Dues</p>
|
<p className="text-slate-600 text-sm">Dues Pending</p>
|
||||||
<p className="text-2xl text-red-600">
|
<p className="text-2xl text-red-600">
|
||||||
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length}
|
{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues Pending').length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -351,12 +377,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<Progress value={progressPercentage} className="h-2" />
|
<Progress value={progressPercentage} className="h-2" />
|
||||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||||
<div className="text-center p-2 bg-green-100 rounded">
|
<div className="text-center p-2 bg-green-100 rounded">
|
||||||
<p className="text-green-700">No Dues</p>
|
<p className="text-green-700">NOC Submitted</p>
|
||||||
<p className="text-green-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'No Dues').length}</p>
|
<p className="text-green-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'NOC Submitted').length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-2 bg-red-100 rounded">
|
<div className="text-center p-2 bg-red-100 rounded">
|
||||||
<p className="text-red-700">Dues</p>
|
<p className="text-red-700">Dues Pending</p>
|
||||||
<p className="text-red-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues').length}</p>
|
<p className="text-red-900">{fnfCase.departmentResponses.filter((d: any) => d.status === 'Dues Pending').length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-2 bg-slate-100 rounded">
|
<div className="text-center p-2 bg-slate-100 rounded">
|
||||||
<p className="text-slate-700">Pending</p>
|
<p className="text-slate-700">Pending</p>
|
||||||
@ -767,7 +793,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{dept.amountType ? (
|
{dept.amountType ? (
|
||||||
<Badge variant={dept.amountType === 'Recovery Amount' ? 'destructive' : 'default'}>
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={dept.amountType === 'Recovery'
|
||||||
|
? 'bg-red-50 text-red-700 border-red-200'
|
||||||
|
: 'bg-green-50 text-green-700 border-green-200'}
|
||||||
|
>
|
||||||
{dept.amountType}
|
{dept.amountType}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
@ -776,7 +807,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{dept.amount ? (
|
{dept.amount ? (
|
||||||
<span className={dept.amountType === 'Recovery Amount' ? 'text-red-600' : 'text-green-600'}>
|
<span className={`${dept.amountType === 'Recovery' ? 'text-red-600 font-bold' : 'text-green-600 font-bold'}`}>
|
||||||
₹{dept.amount.toLocaleString()}
|
₹{dept.amount.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@ -890,7 +921,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{mockDocuments.map((doc: any) => (
|
{fnfCase.documents.map((doc: any) => (
|
||||||
<TableRow key={doc.id}>
|
<TableRow key={doc.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -906,7 +937,23 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button size="sm" variant="outline">View</Button>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const path = doc.url;
|
||||||
|
const fullPath = path.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
|
||||||
|
? path.replace('/uploads/', '/uploads/documents/')
|
||||||
|
: path;
|
||||||
|
setPreviewDocument({
|
||||||
|
fileName: doc.name,
|
||||||
|
filePath: fullPath,
|
||||||
|
documentType: doc.type
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@ -980,6 +1027,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<DocumentPreviewModal
|
||||||
|
isOpen={!!previewDocument}
|
||||||
|
onClose={() => setPreviewDocument(null)}
|
||||||
|
document={previewDocument}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send } from 'lucide-react';
|
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, Upload, Eye } from 'lucide-react';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
@ -15,6 +15,14 @@ import { toast } from 'sonner';
|
|||||||
import { resignationService } from '../../services/resignation.service';
|
import { resignationService } from '../../services/resignation.service';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
|
const ALL_DEPARTMENTS = [
|
||||||
|
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
|
||||||
|
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
|
||||||
|
'Legal', 'Quality', 'Logistics', 'Customer Relations'
|
||||||
|
];
|
||||||
|
|
||||||
interface ResignationDetailsProps {
|
interface ResignationDetailsProps {
|
||||||
resignationId: string;
|
resignationId: string;
|
||||||
@ -29,13 +37,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
const [assignToUser, setAssignToUser] = useState('');
|
const [assignToUser, setAssignToUser] = useState('');
|
||||||
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
|
||||||
const [showClearanceDialog, setShowClearanceDialog] = useState(false);
|
const [showClearanceDialog, setShowClearanceDialog] = useState(false);
|
||||||
const [selectedDept, setSelectedDept] = useState<string | null>(null);
|
const [selectedDept, setSelectedDept] = useState('');
|
||||||
const [clearanceStatus, setClearanceStatus] = useState<'Cleared' | 'Pending' | 'Rejected'>('Cleared');
|
const [clearanceStatus, setClearanceStatus] = useState<any>('Pending');
|
||||||
const [clearanceRemarks, setClearanceRemarks] = useState('');
|
const [clearanceRemarks, setClearanceRemarks] = useState('');
|
||||||
|
const [clearanceAmount, setClearanceAmount] = useState<number>(0);
|
||||||
|
const [clearanceType, setClearanceType] = useState<'Payable' | 'Recovery'>('Recovery');
|
||||||
|
const [clearanceFile, setClearanceFile] = useState<File | null>(null);
|
||||||
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
|
const [isUpdatingClearance, setIsUpdatingClearance] = useState(false);
|
||||||
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
|
const [resignationData, setResignationData] = useState<any>(null); // Real data from API
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||||
|
|
||||||
const fetchResignation = async () => {
|
const fetchResignation = async () => {
|
||||||
try {
|
try {
|
||||||
@ -58,25 +70,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
|
|
||||||
// Progress stages logic based on live data
|
// Progress stages logic based on live data
|
||||||
const progressStages = [
|
const progressStages = [
|
||||||
{ id: 1, name: 'Request Submitted', key: 'Submitted', description: 'Resignation request created' },
|
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
||||||
{ id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
{ id: 2, name: 'RBM Review', key: 'RBM', description: 'Regional Business Manager evaluation' },
|
||||||
{ id: 3, name: 'Departmental Clearances', key: 'Clearance', description: 'Clearance from departments' },
|
{ id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
|
||||||
{ id: 4, name: 'RBM + DD ZM Review', key: 'RBM', description: 'Regional Business Manager and DD ZM evaluation' },
|
{ id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
|
||||||
{ id: 5, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
|
{ id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
||||||
{ id: 6, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
|
{ id: 6, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification' },
|
||||||
{ id: 7, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
{ id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
|
||||||
{ id: 8, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' }
|
{ id: 8, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
|
||||||
|
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const getStageStatus = (stageKey: string) => {
|
const getStageStatus = (stageKey: string) => {
|
||||||
if (!resignationData) return 'pending';
|
if (!resignationData) return 'pending';
|
||||||
const currentStage = resignationData.currentStage;
|
const currentStage = resignationData.currentStage;
|
||||||
|
|
||||||
// Simple logic for simulation - in real app, this would be more complex
|
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
|
||||||
const stagesOrdered = ['Submitted', 'ASM', 'Clearance', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal'];
|
|
||||||
const currentIndex = stagesOrdered.indexOf(currentStage);
|
const currentIndex = stagesOrdered.indexOf(currentStage);
|
||||||
const stageIndex = stagesOrdered.indexOf(stageKey);
|
const stageIndex = stagesOrdered.indexOf(stageKey);
|
||||||
|
|
||||||
|
if (currentIndex === -1) return 'pending'; // Fallback for rejected/other states
|
||||||
if (stageIndex < currentIndex) return 'completed';
|
if (stageIndex < currentIndex) return 'completed';
|
||||||
if (stageIndex === currentIndex) return 'active';
|
if (stageIndex === currentIndex) return 'active';
|
||||||
return 'pending';
|
return 'pending';
|
||||||
@ -129,16 +142,25 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
if (!selectedDept) return;
|
if (!selectedDept) return;
|
||||||
try {
|
try {
|
||||||
setIsUpdatingClearance(true);
|
setIsUpdatingClearance(true);
|
||||||
await resignationService.updateClearance(resignationId, {
|
|
||||||
department: selectedDept,
|
const formData = new FormData();
|
||||||
status: clearanceStatus,
|
formData.append('department', selectedDept);
|
||||||
remarks: clearanceRemarks
|
formData.append('status', clearanceStatus);
|
||||||
});
|
formData.append('remarks', clearanceRemarks);
|
||||||
toast.success(`${selectedDept} clearance updated`);
|
formData.append('amount', String(clearanceAmount));
|
||||||
|
formData.append('type', clearanceType);
|
||||||
|
|
||||||
|
if (clearanceFile) {
|
||||||
|
formData.append('file', clearanceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
await resignationService.updateClearance(resignationId, formData);
|
||||||
|
toast.success(`Successfully updated clearance for ${selectedDept}`);
|
||||||
setShowClearanceDialog(false);
|
setShowClearanceDialog(false);
|
||||||
|
setClearanceFile(null);
|
||||||
fetchResignation();
|
fetchResignation();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update clearance');
|
toast.error('Failed to update clearance status');
|
||||||
} finally {
|
} finally {
|
||||||
setIsUpdatingClearance(false);
|
setIsUpdatingClearance(false);
|
||||||
}
|
}
|
||||||
@ -161,7 +183,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl">{resignationId}</h1>
|
<h1 className="text-2xl">{resignationData?.resignationId || resignationId}</h1>
|
||||||
<p className="text-slate-600">{resignationData?.outlet?.name}</p>
|
<p className="text-slate-600">{resignationData?.outlet?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
|
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
|
||||||
@ -216,7 +238,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
{/* Secondary Actions */}
|
{/* Secondary Actions */}
|
||||||
{currentUser?.role !== 'Dealer' && (
|
{currentUser?.role !== 'Dealer' && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{canPushToFnF && (
|
{canPushToFnF && resignationData?.status !== 'FNF_INITIATED' && resignationData?.status !== 'Settled' && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -365,11 +387,11 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Last Operational Date (Sales)</Label>
|
<Label className="text-slate-600">Last Operational Date (Sales)</Label>
|
||||||
<p>{resignationData?.lastOperationalDateSales ? new Date(resignationData.lastOperationalDateSales).toLocaleDateString() : 'N/A'}</p>
|
<p>{resignationData?.lastOperationalDateSales ? formatDateTime(resignationData.lastOperationalDateSales, 'date') : 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Last Operational Date (Services)</Label>
|
<Label className="text-slate-600">Last Operational Date (Services)</Label>
|
||||||
<p>{resignationData?.lastOperationalDateServices ? new Date(resignationData.lastOperationalDateServices).toLocaleDateString() : 'N/A'}</p>
|
<p>{resignationData?.lastOperationalDateServices ? formatDateTime(resignationData.lastOperationalDateServices, 'date') : 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -379,7 +401,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Submitted On</Label>
|
<Label className="text-slate-600">Submitted On</Label>
|
||||||
<p>{resignationData?.submittedOn ? new Date(resignationData.submittedOn).toLocaleDateString() : 'N/A'}</p>
|
<p>{resignationData?.submittedOn ? formatDateTime(resignationData.submittedOn) : 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Current Stage</Label>
|
<Label className="text-slate-600">Current Stage</Label>
|
||||||
@ -434,7 +456,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
{timelineEntry && (
|
{timelineEntry && (
|
||||||
<div className="flex items-center gap-1 text-sm text-slate-600">
|
<div className="flex items-center gap-1 text-sm text-slate-600">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span>{new Date(timelineEntry.timestamp || timelineEntry.createdAt).toLocaleDateString()}</span>
|
<span>{formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -472,31 +494,91 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{(resignationData?.clearances || []).map((clearance: any) => (
|
{ALL_DEPARTMENTS.map((dept) => {
|
||||||
<Card key={clearance.department} className="border border-slate-200">
|
const settlement = resignationData?.settlement;
|
||||||
|
const fffClearance = (settlement?.clearances || []).find((c: any) => c.department === dept);
|
||||||
|
const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => li.department === dept);
|
||||||
|
const lineItemAmount = relatedLineItems.reduce((sum: number, li: any) => sum + parseFloat(li.amount || 0), 0);
|
||||||
|
|
||||||
|
// Use standardized JSON field but override with live F&F data if available
|
||||||
|
const jsonClearance = (resignationData?.departmentalClearances || {})[dept] || { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' };
|
||||||
|
|
||||||
|
const displayStatus = fffClearance ? (fffClearance.status === 'NOC Submitted' ? 'Cleared' : fffClearance.status === 'Pending' ? 'Pending' : 'Dues') : jsonClearance.status;
|
||||||
|
const displayRemarks = fffClearance ? fffClearance.remarks : jsonClearance.remarks;
|
||||||
|
const displayAmount = fffClearance ? Math.abs(lineItemAmount) : jsonClearance.amount;
|
||||||
|
const displayType = fffClearance ? (lineItemAmount < 0 ? 'Payable' : 'Recovery') : jsonClearance.type;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={dept} className="border border-slate-200">
|
||||||
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
||||||
<CardTitle className="text-base font-medium">{clearance.department}</CardTitle>
|
<CardTitle className="text-base font-medium capitalize">{dept}</CardTitle>
|
||||||
<Badge className={
|
<Badge className={
|
||||||
clearance.status === 'Cleared' || clearance.status === 'Approved' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
|
displayStatus === 'Cleared' ? 'bg-green-100 text-green-700 hover:bg-green-100' :
|
||||||
clearance.status === 'Rejected' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
|
displayStatus === 'Dues' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
|
||||||
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
|
'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'
|
||||||
}>
|
}>
|
||||||
{clearance.status}
|
{displayStatus || 'Pending'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-slate-600 line-clamp-2 min-h-[2.5rem]">
|
<div className="space-y-2">
|
||||||
{clearance.remarks || 'No remarks provided'}
|
<div className="flex justify-between text-xs text-slate-500">
|
||||||
|
<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>
|
</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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-2 text-blue-600 hover:text-blue-700 p-0"
|
className="mt-2 text-blue-600 hover:text-blue-700 p-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDept(clearance.department);
|
setSelectedDept(dept);
|
||||||
setClearanceStatus(clearance.status);
|
setClearanceStatus(displayStatus || 'Pending');
|
||||||
setClearanceRemarks(clearance.remarks);
|
setClearanceRemarks(displayRemarks || '');
|
||||||
|
setClearanceAmount(displayAmount || 0);
|
||||||
|
setClearanceType(displayType || 'Recovery');
|
||||||
|
setClearanceFile(null);
|
||||||
setShowClearanceDialog(true);
|
setShowClearanceDialog(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -505,7 +587,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -530,8 +613,36 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(resignationData?.documents || []).length > 0 ? (
|
{(() => {
|
||||||
(resignationData.documents || []).map((doc: any, index: number) => (
|
const allDocs = [
|
||||||
|
...(resignationData?.documents || []),
|
||||||
|
...(resignationData?.uploadedDocuments || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add clearance documents
|
||||||
|
if (resignationData?.departmentalClearances) {
|
||||||
|
Object.entries(resignationData.departmentalClearances).forEach(([dept, data]: [string, any]) => {
|
||||||
|
if (data.supportingDocument) {
|
||||||
|
allDocs.push({
|
||||||
|
name: `${dept} Clearance Proof`,
|
||||||
|
type: 'Clearance NOC',
|
||||||
|
path: data.supportingDocument,
|
||||||
|
createdAt: data.updatedAt,
|
||||||
|
uploadedBy: data.updatedBy || 'Department Admin'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allDocs.length === 0) return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
|
||||||
|
No documents found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
return allDocs.map((doc: any, index: number) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -539,21 +650,33 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<span>{doc.name || doc.fileName}</span>
|
<span>{doc.name || doc.fileName}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{doc.type || 'Document'}</TableCell>
|
<TableCell>{doc.documentType || doc.type || 'Document'}</TableCell>
|
||||||
<TableCell>{doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : 'N/A'}</TableCell>
|
<TableCell>{doc.createdAt ? formatDateTime(doc.createdAt) : 'N/A'}</TableCell>
|
||||||
<TableCell>{doc.uploadedBy || 'Dealer'}</TableCell>
|
<TableCell>{doc.uploader?.fullName || doc.uploadedBy || 'Dealer'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button size="sm" variant="outline">View</Button>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const path = doc.filePath || doc.path;
|
||||||
|
const fullPath = path?.startsWith('/uploads/') && !path.startsWith('/uploads/documents/')
|
||||||
|
? path.replace('/uploads/', '/uploads/documents/')
|
||||||
|
: path;
|
||||||
|
|
||||||
|
setPreviewDocument({
|
||||||
|
fileName: doc.fileName || doc.name,
|
||||||
|
filePath: fullPath,
|
||||||
|
documentType: doc.documentType || doc.type
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : (
|
})()
|
||||||
<TableRow>
|
}
|
||||||
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
|
|
||||||
No documents found
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -576,7 +699,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<p className="font-medium text-slate-900">{log.action || log.status}</p>
|
<p className="font-medium text-slate-900">{log.action || log.status}</p>
|
||||||
<span className="text-sm text-slate-600">{new Date(log.timestamp || log.createdAt).toLocaleString()}</span>
|
<span className="text-sm text-slate-600">{formatDateTime(log.timestamp || log.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-600">{log.user || log.actor}</p>
|
<p className="text-sm text-slate-600">{log.user || log.actor}</p>
|
||||||
{log.comments && <p className="text-sm text-slate-500 mt-1">{log.comments}</p>}
|
{log.comments && <p className="text-sm text-slate-500 mt-1">{log.comments}</p>}
|
||||||
@ -765,10 +888,34 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]">
|
<SelectContent className="bg-white border-slate-200 shadow-xl overflow-visible z-[9999]">
|
||||||
<SelectItem value="Cleared" className="text-green-600 focus:bg-green-50">Cleared</SelectItem>
|
<SelectItem value="Cleared" className="text-green-600 focus:bg-green-50">Cleared</SelectItem>
|
||||||
<SelectItem value="Pending" className="text-yellow-600 focus:bg-yellow-50">Pending / In-Review</SelectItem>
|
<SelectItem value="Pending" className="text-yellow-600 focus:bg-yellow-50">Pending / In-Review</SelectItem>
|
||||||
<SelectItem value="Rejected" className="text-red-600 focus:bg-red-50">Rejected / Dues Owed</SelectItem>
|
<SelectItem value="Dues" className="text-red-600 focus:bg-red-50">Dues / Outstanding</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Amount (₹)</Label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="w-full mt-2 p-2 border border-slate-300 rounded-md"
|
||||||
|
value={clearanceAmount}
|
||||||
|
onChange={(e) => setClearanceAmount(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Type</Label>
|
||||||
|
<Select value={clearanceType} onValueChange={(val: any) => setClearanceType(val)}>
|
||||||
|
<SelectTrigger className="mt-2 border-slate-300">
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-white">
|
||||||
|
<SelectItem value="Recovery">Recovery (Dealer owes RE)</SelectItem>
|
||||||
|
<SelectItem value="Payable">Payable (RE owes Dealer)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Remarks/Details</Label>
|
<Label>Remarks/Details</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@ -779,6 +926,34 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Supporting Document (Optional)</Label>
|
||||||
|
<div className="mt-2 text-sm text-slate-500 mb-2">
|
||||||
|
Upload NOC, ledger statement, or evidence of dues.
|
||||||
|
</div>
|
||||||
|
<div className="relative group border-2 border-dashed border-slate-200 rounded-lg p-4 transition-colors hover:border-blue-300">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={(e) => setClearanceFile(e.target.files?.[0] || null)}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2">
|
||||||
|
{clearanceFile ? (
|
||||||
|
<div className="flex items-center gap-2 text-blue-600 font-medium">
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
<span className="truncate max-w-[200px]">{clearanceFile.name}</span>
|
||||||
|
<X className="w-4 h-4 cursor-pointer text-slate-400 hover:text-red-500" onClick={(e) => { e.stopPropagation(); setClearanceFile(null); }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Upload className="w-8 h-8 text-slate-300 group-hover:text-blue-400" />
|
||||||
|
<span className="text-slate-400">Click or drag to upload (PDF, JPG, PNG)</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -799,6 +974,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<DocumentPreviewModal
|
||||||
|
isOpen={!!previewDocument}
|
||||||
|
onClose={() => setPreviewDocument(null)}
|
||||||
|
document={previewDocument}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,9 +57,9 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
'DD AM': ['ASM'],
|
'DD AM': ['ASM'],
|
||||||
'ZBH': ['ZBH'],
|
'ZBH': ['ZBH'],
|
||||||
'NBH': ['NBH'],
|
'NBH': ['NBH'],
|
||||||
'Legal Admin': ['Legal'],
|
'Legal Admin': ['Legal', 'FNF Initiate'],
|
||||||
'DD Admin': ['DD Admin'],
|
'DD Admin': ['DD Admin', 'FNF Initiate'],
|
||||||
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'ASM', 'DD Lead']
|
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'ASM', 'DD Lead', 'FNF Initiate']
|
||||||
};
|
};
|
||||||
|
|
||||||
const userStages = roleToStageMapping[currentUser.role] || [];
|
const userStages = roleToStageMapping[currentUser.role] || [];
|
||||||
|
|||||||
@ -10,10 +10,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { User, mockWorkNotes, mockDocuments, mockAuditLogs } from '../../lib/mock-data';
|
import { User, mockWorkNotes } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { terminationService } from '../../services/termination.service';
|
import { terminationService } from '../../services/termination.service';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface TerminationDetailsProps {
|
interface TerminationDetailsProps {
|
||||||
terminationId: string;
|
terminationId: string;
|
||||||
@ -108,41 +109,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
// Check if user can push to F&F (DD Lead and above)
|
// Check if user can push to F&F (DD Lead and above)
|
||||||
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
|
||||||
|
|
||||||
// Mock data - would come from API
|
// Use actual data from backend
|
||||||
const request = {
|
const request = terminationData || {};
|
||||||
id: terminationId,
|
|
||||||
dealerCode: 'DL-MH-025',
|
// Define internal names for mapping if needed, but backend strings are preferred
|
||||||
dealerName: 'Vikram Patil Motors',
|
|
||||||
address: '789, FC Road, Shivaji Nagar, Pune',
|
|
||||||
cityCategory: 'Tier 2',
|
|
||||||
domainName: 'Pune West',
|
|
||||||
dealershipName: 'Royal Enfield Pune',
|
|
||||||
gst: '27AABCU9604R1ZX',
|
|
||||||
salesCode: 'SAL-MH-025',
|
|
||||||
serviceCode: 'SRV-MH-025',
|
|
||||||
accessoriesCode: 'ACC-MH-025',
|
|
||||||
gmaCode: 'GMA-MH-025',
|
|
||||||
location: 'Pune, Maharashtra',
|
|
||||||
inauguration: 'June 2019',
|
|
||||||
loa: 'May 2019',
|
|
||||||
loi: 'April 2019',
|
|
||||||
lastSixMonthsSales: '₹45,00,000',
|
|
||||||
numberOfDealerships: '1',
|
|
||||||
numberOfStudios: '0',
|
|
||||||
constitution: 'Partnership',
|
|
||||||
dealershipType: 'Main Dealer',
|
|
||||||
typeOfClosure: 'Complete',
|
|
||||||
formatCategory: 'B',
|
|
||||||
dealerScoreCardBand: 'Bronze',
|
|
||||||
terminationCategory: 'Breach of Agreement',
|
|
||||||
subCategory: 'Violation of exclusivity clause, unauthorized sub-dealership',
|
|
||||||
description: 'Multiple instances of contract violations including unauthorized sale of competing brands and creation of sub-dealerships without company approval. Despite warnings, dealer has continued non-compliant practices.',
|
|
||||||
severity: 'High',
|
|
||||||
status: 'RBM Review',
|
|
||||||
currentStage: 'RBM',
|
|
||||||
submittedOn: '2025-10-15',
|
|
||||||
submittedBy: 'ASM - Mumbai Region'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock documents by stage
|
// Mock documents by stage
|
||||||
const stageDocuments: Record<string, any[]> = {
|
const stageDocuments: Record<string, any[]> = {
|
||||||
@ -186,96 +157,91 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
const progressStages = [
|
const progressStages = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Request Initiated',
|
name: 'Submitted',
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
date: '2025-10-15',
|
description: 'Termination request initiated',
|
||||||
description: 'Termination request created by ASM/Initiator',
|
date: '',
|
||||||
actionType: 'approved',
|
actionType: '',
|
||||||
actionBy: 'ASM - Mumbai Region',
|
actionBy: '',
|
||||||
remarks: 'Termination request initiated due to severe breach of agreement. Multiple violations documented.',
|
remarks: '',
|
||||||
feedback: 'All evidence and documentation attached. Case requires urgent attention due to severity of violations.'
|
feedback: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'RBM Review',
|
name: 'RBM Review',
|
||||||
status: request.currentStage === 'RBM' ? 'active' : ['ZBH', 'DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'RBM Review' ? 'active' : ['ZBH Review', 'DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'Regional Business Manager review',
|
description: 'Regional Business Manager review'
|
||||||
actionType: request.currentStage === 'RBM' ? undefined : undefined,
|
|
||||||
actionBy: request.currentStage === 'RBM' ? undefined : undefined,
|
|
||||||
remarks: request.currentStage === 'RBM' ? undefined : undefined,
|
|
||||||
feedback: request.currentStage === 'RBM' ? undefined : undefined
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'ZBH Review',
|
name: 'ZBH Review',
|
||||||
status: request.currentStage === 'ZBH' ? 'active' : ['DD Lead', 'Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'ZBH Review' ? 'active' : ['DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'Zonal Business Head evaluation'
|
description: 'Zonal Business Head evaluation'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
name: 'DD Lead Review',
|
name: 'DD Lead Review',
|
||||||
status: request.currentStage === 'DD Lead' ? 'active' : ['Legal', 'NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'DD Lead Review' ? 'active' : ['Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'DD Lead validation'
|
description: 'DD Lead validation'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'Legal Verification',
|
name: 'Legal Verification',
|
||||||
status: request.currentStage === 'Legal' ? 'active' : ['NBH', 'SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'Legal Verification' ? 'active' : ['NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'Legal team validates termination grounds'
|
description: 'Legal team validates termination grounds'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
name: 'NBH Evaluation',
|
name: 'NBH Evaluation',
|
||||||
status: request.currentStage === 'NBH' ? 'active' : ['SCN', 'CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'NBH Evaluation' ? 'active' : ['Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'National Business Head decision'
|
description: 'National Business Head decision'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
name: 'Show Cause Notice (SCN)',
|
name: 'Show Cause Notice (SCN)',
|
||||||
status: request.currentStage === 'SCN' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'Show Cause Notice' ? 'active' : ['Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'SCN sent to dealer, awaiting response'
|
description: 'SCN sent to dealer, awaiting response'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
name: 'DD Lead & Legal Review',
|
name: 'Personal Hearing',
|
||||||
status: request.currentStage === 'DD Lead Legal' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'Personal Hearing' ? 'active' : ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'Evaluation of SCN response'
|
description: 'Evaluation of SCN response & Hearing'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
name: 'NBH Termination Approval',
|
name: 'NBH Final Approval',
|
||||||
status: request.currentStage === 'NBH Final' ? 'active' : ['CCO', 'CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'NBH Final Approval' ? 'active' : ['CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'NBH approves termination'
|
description: 'NBH final termination decision'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
name: 'CCO Approval',
|
name: 'CCO Approval',
|
||||||
status: request.currentStage === 'CCO' ? 'active' : ['CEO', 'Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'CCO Approval' ? 'active' : ['CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'Chief Commercial Officer approval'
|
description: 'Chief Commercial Officer approval'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
name: 'CEO Final Approval',
|
name: 'CEO Final Approval',
|
||||||
status: request.currentStage === 'CEO' ? 'active' : ['Legal Letter', 'DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'CEO Final Approval' ? 'active' : ['Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
||||||
description: 'CEO final authorization'
|
description: 'CEO final authorization'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
name: 'Legal - Termination Letter',
|
name: 'Legal - Termination Letter',
|
||||||
status: request.currentStage === 'Legal Letter' ? 'active' : ['DD Admin Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending',
|
status: request.currentStage === 'Legal - Termination Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||||
description: 'Legal team shares termination letter to DD-Lead and DD Admin'
|
description: 'Legal team issues final termination letter'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 13,
|
||||||
name: 'DD Admin - Share with Dealer',
|
|
||||||
status: request.currentStage === 'DD Admin Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
|
||||||
description: 'DD Admin shares termination letter with dealer (Proceed to F&F)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
name: 'Dealer Terminated',
|
name: 'Dealer Terminated',
|
||||||
status: request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
status: request.currentStage === 'Terminated' ? 'completed' : 'pending',
|
||||||
description: 'Dealership termination effective'
|
description: 'Dealership termination effective',
|
||||||
|
date: '',
|
||||||
|
actionType: '',
|
||||||
|
actionBy: '',
|
||||||
|
remarks: '',
|
||||||
|
feedback: ''
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -288,7 +254,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
setActionDialog({ open: true, type });
|
setActionDialog({ open: true, type });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitAction = () => {
|
const handleSubmitAction = async () => {
|
||||||
if (!remarks && actionDialog.type !== 'assign' && actionDialog.type !== 'pushfnf') {
|
if (!remarks && actionDialog.type !== 'assign' && actionDialog.type !== 'pushfnf') {
|
||||||
toast.error('Please provide remarks');
|
toast.error('Please provide remarks');
|
||||||
return;
|
return;
|
||||||
@ -298,6 +264,14 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
try {
|
||||||
|
if (actionDialog.type === 'approve') {
|
||||||
|
await terminationService.updateTerminationStatus(terminationId, 'approve', remarks);
|
||||||
|
} else {
|
||||||
|
// Handle other actions
|
||||||
|
}
|
||||||
|
|
||||||
const actionMessages = {
|
const actionMessages = {
|
||||||
approve: 'Request approved and forwarded',
|
approve: 'Request approved and forwarded',
|
||||||
withdrawal: 'Request withdrawn successfully',
|
withdrawal: 'Request withdrawn successfully',
|
||||||
@ -310,6 +284,12 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
setActionDialog({ open: false, type: null });
|
setActionDialog({ open: false, type: null });
|
||||||
setRemarks('');
|
setRemarks('');
|
||||||
setAssignToUser('');
|
setAssignToUser('');
|
||||||
|
fetchTermination();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to perform action');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSeverityColor = (severity: string) => {
|
const getSeverityColor = (severity: string) => {
|
||||||
@ -345,8 +325,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl">{terminationId}</h1>
|
<h1 className="text-2xl">{request.requestId || terminationId}</h1>
|
||||||
<p className="text-slate-600">{request.dealerName}</p>
|
<p className="text-slate-600">{request.dealer?.businessName || request.dealerName}</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={getSeverityColor(request.severity)}>
|
<Badge className={getSeverityColor(request.severity)}>
|
||||||
{request.severity}
|
{request.severity}
|
||||||
@ -496,47 +476,47 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Dealer Code</Label>
|
<Label className="text-slate-600">Dealer Code</Label>
|
||||||
<p>{request.dealerCode}</p>
|
<p>{request.dealer?.dealerCode?.code || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Dealer Name</Label>
|
<Label className="text-slate-600">Dealer Name</Label>
|
||||||
<p>{request.dealerName}</p>
|
<p>{request.dealer?.businessName || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">GST</Label>
|
<Label className="text-slate-600">GST</Label>
|
||||||
<p>{request.gst}</p>
|
<p>{request.dealer?.gstNumber || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label className="text-slate-600">Address</Label>
|
<Label className="text-slate-600">Address</Label>
|
||||||
<p>{request.address}</p>
|
<p>{request.dealer?.registeredAddress || request.dealer?.application?.address || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">City Category</Label>
|
<Label className="text-slate-600">City</Label>
|
||||||
<p>{request.cityCategory}</p>
|
<p>{request.dealer?.application?.city || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Domain Name</Label>
|
<Label className="text-slate-600">District</Label>
|
||||||
<p>{request.domainName}</p>
|
<p>{request.dealer?.application?.district?.name || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Dealership Name</Label>
|
<Label className="text-slate-600">Dealership Name</Label>
|
||||||
<p>{request.dealershipName}</p>
|
<p>{request.dealer?.businessName || request.dealershipName}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Sales Code</Label>
|
<Label className="text-slate-600">Sales Code</Label>
|
||||||
<p>{request.salesCode}</p>
|
<p>{request.dealer?.dealerCode?.salesCode || request.salesCode || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Service Code</Label>
|
<Label className="text-slate-600">Service Code</Label>
|
||||||
<p>{request.serviceCode}</p>
|
<p>{request.dealer?.dealerCode?.serviceCode || request.serviceCode || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Accessories Code</Label>
|
<Label className="text-slate-600">Accessories Code</Label>
|
||||||
<p>{request.accessoriesCode}</p>
|
<p>{request.dealer?.dealerCode?.accessoriesCode || request.accessoriesCode || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">GMA Code</Label>
|
<Label className="text-slate-600">GMA Code</Label>
|
||||||
<p>{request.gmaCode}</p>
|
<p>{request.dealer?.dealerCode?.gmaCode || request.gmaCode || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -550,15 +530,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Inauguration</Label>
|
<Label className="text-slate-600">Inauguration</Label>
|
||||||
<p>{request.inauguration}</p>
|
<p>{request.dealer?.onboardedAt ? formatDateTime(request.dealer.onboardedAt, 'date') : (request.inauguration || 'N/A')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">LOA</Label>
|
<Label className="text-slate-600">LOA</Label>
|
||||||
<p>{request.loa}</p>
|
<p>{request.loa || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">LOI</Label>
|
<Label className="text-slate-600">LOI</Label>
|
||||||
<p>{request.loi}</p>
|
<p>{request.loi || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Last 6 Months Sales</Label>
|
<Label className="text-slate-600">Last 6 Months Sales</Label>
|
||||||
@ -574,23 +554,23 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Constitution</Label>
|
<Label className="text-slate-600">Constitution</Label>
|
||||||
<p>{request.constitution}</p>
|
<p>{request.dealer?.constitutionType || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Dealership Type</Label>
|
<Label className="text-slate-600">Dealership Type</Label>
|
||||||
<p>{request.dealershipType}</p>
|
<p>{request.dealer?.application?.businessType || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Type of Closure</Label>
|
<Label className="text-slate-600">Type of Closure</Label>
|
||||||
<p>{request.typeOfClosure}</p>
|
<p>{request.typeOfClosure || 'Complete'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Format Category</Label>
|
<Label className="text-slate-600">Format Category</Label>
|
||||||
<p>{request.formatCategory}</p>
|
<p>{request.formatCategory || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Dealer Score Card Band</Label>
|
<Label className="text-slate-600">Dealer Score Card Band</Label>
|
||||||
<p>{request.dealerScoreCardBand}</p>
|
<p>{request.dealerScoreCardBand || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -607,15 +587,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Termination Category</Label>
|
<Label className="text-slate-600">Termination Category</Label>
|
||||||
<p className="text-red-900">{request.terminationCategory}</p>
|
<p className="text-red-900">{request.category}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Sub Category</Label>
|
<Label className="text-slate-600">Sub Category</Label>
|
||||||
<p>{request.subCategory}</p>
|
<p>{request.subCategory || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Description</Label>
|
<Label className="text-slate-600">Description</Label>
|
||||||
<p>{request.description}</p>
|
<p>{request.reason}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
@ -628,11 +608,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Submitted By</Label>
|
<Label className="text-slate-600">Submitted By</Label>
|
||||||
<p>{request.submittedBy}</p>
|
<p>{request.initiator?.fullName || 'System'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Submitted On</Label>
|
<Label className="text-slate-600">Submitted On</Label>
|
||||||
<p>{request.submittedOn}</p>
|
<p>{formatDateTime(request.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -761,22 +741,37 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{mockDocuments.map((doc) => (
|
{(() => {
|
||||||
<TableRow key={doc.id}>
|
const allDocs = [
|
||||||
|
...(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>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FileText className="w-4 h-4 text-slate-500" />
|
<FileText className="w-4 h-4 text-slate-500" />
|
||||||
<span>{doc.name}</span>
|
<span>{doc.name || doc.fileName}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{doc.type}</TableCell>
|
<TableCell>{doc.documentType || doc.type || 'Document'}</TableCell>
|
||||||
<TableCell>{doc.uploadDate}</TableCell>
|
<TableCell>{formatDateTime(doc.uploadDate || doc.createdAt)}</TableCell>
|
||||||
<TableCell>{doc.uploader || '-'}</TableCell>
|
<TableCell>{doc.uploader?.fullName || doc.uploader || '-'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button size="sm" variant="outline">View</Button>
|
<Button size="sm" variant="outline">View</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
));
|
||||||
|
})()}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -792,19 +787,25 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{mockAuditLogs.map((log) => (
|
{(request.timeline || []).length > 0 ? (
|
||||||
<div key={log.id} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
|
(request.timeline || []).map((log: any, index: number) => (
|
||||||
|
<div 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="w-2 h-2 rounded-full bg-red-600 mt-2" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<p>{log.action}</p>
|
<p className="font-medium text-slate-900">{log.action || log.status}</p>
|
||||||
<span className="text-sm text-slate-600">{log.timestamp}</span>
|
<span className="text-sm text-slate-600">{formatDateTime(log.timestamp || log.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-600">{log.user}</p>
|
<p className="text-sm text-slate-600">{log.user || log.actor}</p>
|
||||||
{log.details && <p className="text-sm text-slate-500 mt-1">{log.details}</p>}
|
{log.remarks || log.comments ? <p className="text-sm text-slate-500 mt-1">{log.remarks || log.comments}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-slate-500">
|
||||||
|
No history found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { Textarea } from '../ui/textarea';
|
|||||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
import { User } from '../../lib/mock-data';
|
import { User } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
// Outlet model has associate dealer? Let's check.
|
// Outlet model has associate dealer? Let's check.
|
||||||
// In my outlet.controller.ts, I included 'dealer'.
|
// In my outlet.controller.ts, I included 'dealer'.
|
||||||
const payload = {
|
const payload = {
|
||||||
dealerId: autoFilledData.Dealer?.id || autoFilledData.dealerId, // Map from outlet's dealer association
|
dealerId: autoFilledData.Dealer?.id || autoFilledData.id, // outlet.id might be used if dealerId is missing, but backend expects dealerId (Dealer model)
|
||||||
category: formData.terminationCategory,
|
category: formData.terminationCategory,
|
||||||
reason: formData.reason,
|
reason: formData.reason,
|
||||||
proposedLwd: formData.proposedLwd,
|
proposedLwd: formData.proposedLwd,
|
||||||
@ -150,13 +151,16 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
if (!currentUser) return false;
|
if (!currentUser) return false;
|
||||||
|
|
||||||
const roleToStageMapping: Record<string, string[]> = {
|
const roleToStageMapping: Record<string, string[]> = {
|
||||||
'DD Lead': ['DD Lead'],
|
'DD Lead': ['DD Lead Review'],
|
||||||
'RBM': ['RBM'],
|
'RBM': ['RBM Review'],
|
||||||
'ZBH': ['ZBH'],
|
'ZBH': ['ZBH Review'],
|
||||||
'NBH': ['NBH'],
|
'NBH': ['NBH Evaluation', 'NBH Final Approval'],
|
||||||
'Legal Admin': ['Legal'],
|
'Legal Admin': ['Legal Verification', 'Legal - Termination Letter'],
|
||||||
'DD Admin': ['DD Admin'],
|
'Legal': ['Legal Verification'],
|
||||||
'Super Admin': ['DD Admin', 'NBH', 'Legal', 'ZBH', 'RBM', 'DD Lead', 'CCO', 'CEO']
|
'DD Admin': ['Show Cause Notice', 'Terminated'],
|
||||||
|
'CCO': ['CCO Approval'],
|
||||||
|
'CEO': ['CEO Final Approval'],
|
||||||
|
'Super Admin': ['DD Lead Review', 'RBM Review', 'ZBH Review', 'NBH Evaluation', 'Legal Verification', 'Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated']
|
||||||
};
|
};
|
||||||
|
|
||||||
const userStages = roleToStageMapping[currentUser.role] || [];
|
const userStages = roleToStageMapping[currentUser.role] || [];
|
||||||
@ -397,7 +401,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-lg">{request.id.substring(0, 8)}</h3>
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
||||||
|
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
|
||||||
<Badge className={getSeverityColor(request.severity || 'Medium')}>
|
<Badge className={getSeverityColor(request.severity || 'Medium')}>
|
||||||
{request.severity || 'Normal'}
|
{request.severity || 'Normal'}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -431,7 +436,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Submitted On</p>
|
<p className="text-slate-600">Submitted On</p>
|
||||||
<p>{new Date(request.createdAt).toLocaleDateString()}</p>
|
<p>{formatDateTime(request.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -471,7 +476,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-lg">{request.id.substring(0, 8)}</h3>
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
||||||
|
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
|
||||||
<Badge className={getStatusColor(request.status)}>
|
<Badge className={getStatusColor(request.status)}>
|
||||||
{request.status}
|
{request.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -491,7 +497,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Submitted On</p>
|
<p className="text-slate-600">Submitted On</p>
|
||||||
<p>{new Date(request.createdAt).toLocaleDateString()}</p>
|
<p>{formatDateTime(request.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -532,7 +538,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-lg">{request.id.substring(0, 8)}</h3>
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
||||||
|
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
|
||||||
<Badge className={getStatusColor(request.status)}>
|
<Badge className={getStatusColor(request.status)}>
|
||||||
{request.status}
|
{request.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -544,7 +551,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Closed On</p>
|
<p className="text-slate-600">Closed On</p>
|
||||||
<p>{new Date(request.updatedAt).toLocaleDateString()}</p>
|
<p>{formatDateTime(request.updatedAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Termination Category</p>
|
<p className="text-slate-600">Termination Category</p>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { User } from '../../lib/mock-data';
|
import { User } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { onboardingService } from '../../services/onboarding.service';
|
import { onboardingService } from '../../services/onboarding.service';
|
||||||
|
import { settlementService } from '../../services/settlement.service';
|
||||||
|
|
||||||
interface FinanceDashboardProps {
|
interface FinanceDashboardProps {
|
||||||
currentUser: User | null;
|
currentUser: User | null;
|
||||||
@ -22,36 +23,6 @@ interface FinanceDashboardProps {
|
|||||||
onViewFnFDetails?: (fnfId: string) => void;
|
onViewFnFDetails?: (fnfId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock data for F&F cases (Keeping as static for now as per original)
|
|
||||||
const mockFnFCases = [
|
|
||||||
{
|
|
||||||
id: 'RES-001',
|
|
||||||
dealerName: 'Amit Sharma Motors',
|
|
||||||
dealerCode: 'DL-MH-001',
|
|
||||||
type: 'Resignation',
|
|
||||||
location: 'Mumbai, Maharashtra',
|
|
||||||
status: 'Pending Finance Summary',
|
|
||||||
submittedOn: '2025-10-08',
|
|
||||||
departmentsResponded: 16,
|
|
||||||
totalDepartments: 16,
|
|
||||||
hasFinanceSummary: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'TERM-002',
|
|
||||||
dealerName: 'Sanjay Enterprises',
|
|
||||||
dealerCode: 'DL-TG-033',
|
|
||||||
type: 'Termination',
|
|
||||||
location: 'Hyderabad, Telangana',
|
|
||||||
status: 'Finance Summary Completed',
|
|
||||||
submittedOn: '2025-09-20',
|
|
||||||
departmentsResponded: 16,
|
|
||||||
totalDepartments: 16,
|
|
||||||
hasFinanceSummary: true,
|
|
||||||
netAmount: -270000,
|
|
||||||
completedOn: '2025-10-10'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
interface FinanceLineItem {
|
interface FinanceLineItem {
|
||||||
id: string;
|
id: string;
|
||||||
department: string;
|
department: string;
|
||||||
@ -61,7 +32,9 @@ interface FinanceLineItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAuditDetails, onViewFnFDetails }: FinanceDashboardProps) {
|
export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAuditDetails, onViewFnFDetails }: FinanceDashboardProps) {
|
||||||
const [applications, setApplications] = useState<any[]>([]);
|
const [onboardingPayments, setOnboardingPayments] = useState<any[]>([]);
|
||||||
|
const [fnfSettlements, setFnfSettlements] = useState<any[]>([]);
|
||||||
|
const [pendingAudits, setPendingAudits] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -71,20 +44,24 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await onboardingService.getApplications();
|
const [payments, settlements, apps] = await Promise.all([
|
||||||
// Filter for applications relevant to finance
|
settlementService.getOnboardingPayments(),
|
||||||
const financeApps = data.filter((app: any) => {
|
settlementService.getFnFSettlements(),
|
||||||
const s = app.overallStatus || app.status;
|
onboardingService.getApplications()
|
||||||
const stage = app.currentStage;
|
]);
|
||||||
return [
|
|
||||||
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
|
setOnboardingPayments(payments);
|
||||||
'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT',
|
setFnfSettlements(settlements);
|
||||||
'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
|
|
||||||
].includes(s) || stage === 'Finance';
|
// Filter for applications needing FDD review
|
||||||
});
|
const fddApps = apps.filter((app: any) =>
|
||||||
setApplications(financeApps);
|
app.status === 'FDD_VERIFICATION' || app.overallStatus === 'FDD Verification' || app.currentStage === 'FDD'
|
||||||
|
);
|
||||||
|
setPendingAudits(fddApps);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error);
|
console.error('Fetch error:', error);
|
||||||
|
toast.error('Failed to load dashboard data');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -102,6 +79,50 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
});
|
});
|
||||||
const [finalRemarks, setFinalRemarks] = useState('');
|
const [finalRemarks, setFinalRemarks] = useState('');
|
||||||
|
|
||||||
|
const calculateTotals = () => {
|
||||||
|
const totalRecovery = lineItems
|
||||||
|
.filter(item => item.type === 'recovery')
|
||||||
|
.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
|
||||||
|
const totalPayable = lineItems
|
||||||
|
.filter(item => item.type === 'payable')
|
||||||
|
.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
|
||||||
|
const netAmount = totalPayable - totalRecovery;
|
||||||
|
|
||||||
|
return { totalRecovery, totalPayable, netAmount };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitFinanceSummary = async () => {
|
||||||
|
if (lineItems.length === 0) {
|
||||||
|
toast.error('Please add at least one line item');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!finalRemarks) {
|
||||||
|
toast.error('Please add final remarks');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
// Implement submission to backend settlement update
|
||||||
|
toast.success('Finance summary submitted successfully');
|
||||||
|
setFnfDialog(false);
|
||||||
|
setSelectedFnF(null);
|
||||||
|
setLineItems([]);
|
||||||
|
setFinalRemarks('');
|
||||||
|
fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to submit summary');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pendingOnboarding = onboardingPayments.filter(p => p.paymentStatus !== 'Paid' && p.paymentStatus !== 'Verified');
|
||||||
|
const verifiedOnboarding = onboardingPayments.filter(p => p.paymentStatus === 'Paid' || p.paymentStatus === 'Verified');
|
||||||
|
const pendingFnF = fnfSettlements.filter(f => f.status === 'Initiated' || f.status === 'Calculated');
|
||||||
|
const completedFnF = fnfSettlements.filter(f => f.status === 'Completed' || f.status === 'Cleared');
|
||||||
|
|
||||||
const handleAddLineItem = () => {
|
const handleAddLineItem = () => {
|
||||||
if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) {
|
if (!newLineItem.department || !newLineItem.description || !newLineItem.amount) {
|
||||||
@ -127,54 +148,6 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
toast.info('Line item removed');
|
toast.info('Line item removed');
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTotals = () => {
|
|
||||||
const totalRecovery = lineItems
|
|
||||||
.filter(item => item.type === 'recovery')
|
|
||||||
.reduce((sum, item) => sum + item.amount, 0);
|
|
||||||
|
|
||||||
const totalPayable = lineItems
|
|
||||||
.filter(item => item.type === 'payable')
|
|
||||||
.reduce((sum, item) => sum + item.amount, 0);
|
|
||||||
|
|
||||||
const netAmount = totalPayable - totalRecovery;
|
|
||||||
|
|
||||||
return { totalRecovery, totalPayable, netAmount };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmitFinanceSummary = () => {
|
|
||||||
if (lineItems.length === 0) {
|
|
||||||
toast.error('Please add at least one line item');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!finalRemarks) {
|
|
||||||
toast.error('Please add final remarks');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('Finance summary submitted successfully');
|
|
||||||
setFnfDialog(false);
|
|
||||||
setSelectedFnF(null);
|
|
||||||
setLineItems([]);
|
|
||||||
setFinalRemarks('');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const getRelevantPaymentStatus = (app: any) => {
|
|
||||||
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
|
|
||||||
|
|
||||||
const s = app.overallStatus || app.status || '';
|
|
||||||
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION') ? 'INITIAL' : 'FINAL';
|
|
||||||
const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
|
|
||||||
|
|
||||||
return deposit ? deposit.status : 'Awaiting Payment';
|
|
||||||
};
|
|
||||||
|
|
||||||
const pendingOnboarding = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified');
|
|
||||||
const verifiedOnboarding = applications.filter(app => getRelevantPaymentStatus(app) === 'Verified');
|
|
||||||
const pendingAudits = applications.filter(app => app.status === 'FDD_VERIFICATION' || app.overallStatus === 'FDD Verification');
|
|
||||||
const pendingFnF = mockFnFCases.filter(f => !f.hasFinanceSummary);
|
|
||||||
const completedFnF = mockFnFCases.filter(f => f.hasFinanceSummary);
|
|
||||||
|
|
||||||
const { totalRecovery, totalPayable, netAmount } = calculateTotals();
|
const { totalRecovery, totalPayable, netAmount } = calculateTotals();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -326,29 +299,29 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<h3 className="text-lg font-bold">{app.applicationId || app.id}</h3>
|
<h3 className="text-lg font-bold">{app.application?.applicationId || 'N/A'}</h3>
|
||||||
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
|
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
|
||||||
{app.status}
|
{app.paymentStatus}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Applicant Name</p>
|
<p className="text-slate-600">Applicant Name</p>
|
||||||
<p className="font-medium">{app.applicantName}</p>
|
<p className="font-medium">{app.application?.applicantName}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Location</p>
|
<p className="text-slate-600">Type</p>
|
||||||
<p>{app.city || app.preferredLocation}, {app.state}</p>
|
<p>{app.paymentType}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Stage</p>
|
<p className="text-slate-600">Amount</p>
|
||||||
<p className="text-amber-700 font-bold">
|
<p className="text-amber-700 font-bold">
|
||||||
{app.status === 'PAYMENT_VERIFICATION' ? 'Security Deposit' : 'First Fill'}
|
₹{parseFloat(app.amount).toLocaleString('en-IN')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Email</p>
|
<p className="text-slate-600">Created On</p>
|
||||||
<p className="truncate">{app.email}</p>
|
<p>{new Date(app.createdAt).toLocaleDateString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -399,27 +372,29 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<h3 className="text-lg font-bold">{app.applicationId || app.id}</h3>
|
<h3 className="text-lg font-bold">{app.application?.applicationId || 'N/A'}</h3>
|
||||||
<Badge className="bg-green-100 text-green-700 border-green-300">
|
<Badge className="bg-green-100 text-green-700 border-green-300">
|
||||||
{getRelevantPaymentStatus(app)}
|
{app.paymentStatus}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Applicant Name</p>
|
<p className="text-slate-600">Applicant Name</p>
|
||||||
<p className="font-medium">{app.applicantName}</p>
|
<p className="font-medium">{app.application?.applicantName}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Location</p>
|
<p className="text-slate-600">Type</p>
|
||||||
<p>{app.city || app.preferredLocation}, {app.state}</p>
|
<p>{app.paymentType}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Status</p>
|
<p className="text-slate-600">Amount</p>
|
||||||
<p className="text-green-700 font-bold">Payment Verified</p>
|
<p className="text-green-700 font-bold">
|
||||||
|
₹{parseFloat(app.amount).toLocaleString('en-IN')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Email</p>
|
<p className="text-slate-600">Verified On</p>
|
||||||
<p className="truncate">{app.email}</p>
|
<p>{app.verificationDate ? new Date(app.verificationDate).toLocaleDateString() : 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -476,29 +451,33 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<h3 className="text-lg font-bold">{fnf.id}</h3>
|
<h3 className="text-lg font-bold">{fnf.resignation?.resignationId || fnf.id}</h3>
|
||||||
<Badge variant="outline">{fnf.type}</Badge>
|
<Badge variant="outline">{fnf.resignation ? 'Resignation' : fnf.terminationRequest ? 'Termination' : 'General'}</Badge>
|
||||||
<Badge className="bg-orange-100 text-orange-700 border-orange-300">
|
<Badge className="bg-orange-100 text-orange-700 border-orange-300">
|
||||||
{fnf.status}
|
{fnf.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm mb-3">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm mb-3">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm mb-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Dealer Name</p>
|
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Name</p>
|
||||||
<p className="font-medium">{fnf.dealerName}</p>
|
<p className="font-semibold text-slate-900">{fnf.outlet?.dealer?.fullName || fnf.outlet?.name || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Dealer Code</p>
|
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Code</p>
|
||||||
<p>{fnf.dealerCode}</p>
|
<p className="font-mono text-xs font-bold text-blue-600">{fnf.outlet?.code || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Location</p>
|
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Location</p>
|
||||||
<p>{fnf.location}</p>
|
<p className="text-slate-900">{fnf.outlet?.city || 'N/A'}, {fnf.outlet?.state || ''}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm bg-white/50 p-2 rounded-lg border border-slate-100">
|
||||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
||||||
<span className="text-green-700">{fnf.departmentsResponded}/{fnf.totalDepartments} Departments Responded</span>
|
<span className="text-slate-600 font-medium">
|
||||||
|
<span className="text-emerald-600 font-bold">{(fnf.clearances || []).filter((c: any) => c.status === 'NOC Submitted' || c.status === 'Dues Pending').length}</span>/16 Departments Cleared
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 flex flex-col gap-2">
|
<div className="ml-4 flex flex-col gap-2">
|
||||||
@ -610,7 +589,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Financial Settlement Summary</DialogTitle>
|
<DialogTitle>Create Financial Settlement Summary</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{selectedFnF?.id} - {selectedFnF?.dealerName} ({selectedFnF?.dealerCode})
|
{selectedFnF?.resignation?.resignationId || selectedFnF?.id} - {selectedFnF?.dealerName} ({selectedFnF?.dealerCode})
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@ -770,7 +749,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Full Settlement Details</DialogTitle>
|
<DialogTitle>Full Settlement Details</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{selectedFnF?.id} - {selectedFnF?.dealerName}
|
{selectedFnF?.resignation?.resignationId || selectedFnF?.id} - {selectedFnF?.dealerName}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@ -779,15 +758,15 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
<Card className="bg-slate-50 border-none">
|
<Card className="bg-slate-50 border-none">
|
||||||
<CardContent className="pt-6 grid grid-cols-3 gap-6">
|
<CardContent className="pt-6 grid grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 uppercase">Dealer Code</p>
|
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Dealer Code</p>
|
||||||
<p className="text-lg font-bold">{selectedFnF.dealerCode}</p>
|
<p className="text-lg font-bold">{selectedFnF.outlet?.code || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 uppercase">Location</p>
|
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Location</p>
|
||||||
<p className="text-lg font-bold">{selectedFnF.location}</p>
|
<p className="text-lg font-bold">{selectedFnF.outlet?.city || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 uppercase">Status</p>
|
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Current Status</p>
|
||||||
<Badge variant="outline" className="bg-amber-100 text-amber-700 border-amber-200">
|
<Badge variant="outline" className="bg-amber-100 text-amber-700 border-amber-200">
|
||||||
{selectedFnF.status}
|
{selectedFnF.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -805,51 +784,63 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4 space-y-3">
|
<CardContent className="pt-4 space-y-3">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-slate-500">Inventory Value</span>
|
<span className="text-slate-500">Security Deposit Credits</span>
|
||||||
<span className="font-bold">₹12,45,000</span>
|
<span className="font-bold text-slate-900">₹{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-slate-500">Security Deposit</span>
|
<span className="text-slate-500">Other Payable Credits</span>
|
||||||
<span className="font-bold">₹2,00,000</span>
|
<span className="font-bold text-slate-900">₹0</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-2 flex justify-between font-bold text-blue-700">
|
<div className="border-t pt-2 flex justify-between font-bold text-blue-700">
|
||||||
<span>Total Payables</span>
|
<span>Total Payables</span>
|
||||||
<span>₹14,45,000</span>
|
<span>₹{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="border-red-100">
|
<Card className="border-red-100 shadow-sm">
|
||||||
<CardHeader className="bg-red-50/50 pb-2">
|
<CardHeader className="bg-red-50/30 pb-2 border-b border-red-50">
|
||||||
<CardTitle className="text-sm font-bold flex items-center gap-2">
|
<CardTitle className="text-sm font-bold flex items-center gap-2 text-red-700">
|
||||||
<TrendingDown className="w-4 h-4 text-red-600" />
|
<TrendingDown className="w-4 h-4" />
|
||||||
Recoveries Check
|
Receivables Check
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4 space-y-3">
|
<CardContent className="pt-4 space-y-3">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-slate-500">Outstanding Invoices</span>
|
<span className="text-slate-500">Departmental Dues (Recoverable)</span>
|
||||||
<span className="font-bold">₹8,50,000</span>
|
<span className="font-bold text-red-600">₹{parseFloat(selectedFnF.totalReceivables || 0).toLocaleString('en-IN')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-slate-500">Spares & Oil Dues</span>
|
<span className="text-slate-500">Outstanding Invoices</span>
|
||||||
<span className="font-bold">₹1,20,000</span>
|
<span className="font-bold text-slate-900">₹0</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-2 flex justify-between font-bold text-red-700">
|
<div className="border-t pt-2 flex justify-between font-bold text-red-700">
|
||||||
<span>Total Recoveries</span>
|
<span>Total Recoveries</span>
|
||||||
<span>₹9,70,000</span>
|
<span>₹{parseFloat(selectedFnF.totalReceivables || 0).toLocaleString('en-IN')}</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 bg-green-900 text-white rounded-lg flex items-center justify-between">
|
<div className="p-5 bg-slate-900 text-white rounded-xl flex items-center justify-between shadow-lg">
|
||||||
<div>
|
<div className="flex items-center gap-4">
|
||||||
<p className="text-xs text-green-300 uppercase font-bold">Estimated Net Settlement</p>
|
<div className={`p-3 rounded-lg ${parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'bg-emerald-500/20' : 'bg-rose-500/20'}`}>
|
||||||
<p className="text-2xl font-bold">₹4,75,000 <span className="text-sm font-normal text-green-200 ml-2">(Payable to Dealer)</span></p>
|
<IndianRupee className={`w-6 h-6 ${parseFloat(selectedFnF.netAmount || 0) >= 0 ? 'text-emerald-400' : 'text-rose-400'}`} />
|
||||||
</div>
|
</div>
|
||||||
<Button className="bg-green-600 hover:bg-green-500">
|
<div>
|
||||||
Generate PDF
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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 });
|
return d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (format === 'full') {
|
||||||
|
return d.toLocaleString('en-IN', options);
|
||||||
|
}
|
||||||
|
|
||||||
return d.toLocaleDateString('en-IN', options);
|
return d.toLocaleDateString('en-IN', options);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const resignationService = {
|
|||||||
getResignationById: async (id: string) => {
|
getResignationById: async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const response: any = await API.getResignationById(id);
|
const response: any = await API.getResignationById(id);
|
||||||
return response.data?.data || response.data;
|
return response.data?.resignation || response.data?.data || response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get resignation error:', error);
|
console.error('Get resignation error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
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 = {
|
export const terminationService = {
|
||||||
getTerminationById: async (id: string) => {
|
getTerminationById: async (id: string) => {
|
||||||
const response = await API.getTerminationById(id);
|
const response = await API.getTerminationById(id);
|
||||||
return response.data;
|
return response.data?.termination || response.data?.data || response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTerminationStatus: async (id: string, status: string, remarks: string) => {
|
updateTerminationStatus: async (id: string, status: string, remarks: string) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user