new mail templates added dealer table enhanced add bank details added
This commit is contained in:
parent
dad534c169
commit
7126d4b6bf
@ -101,6 +101,9 @@ export const API = {
|
|||||||
getDealerById: (id: string) => client.get(`/dealer/${id}`),
|
getDealerById: (id: string) => client.get(`/dealer/${id}`),
|
||||||
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
|
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
|
||||||
getDealerDashboard: () => client.get('/dealer/dashboard'),
|
getDealerDashboard: () => client.get('/dealer/dashboard'),
|
||||||
|
getDealerBankDetails: (dealerId: string) => client.get(`/dealer/${dealerId}/bank-details`),
|
||||||
|
saveBankDetail: (dealerId: string, data: any) => client.post(`/dealer/${dealerId}/bank-details`, data),
|
||||||
|
deleteBankDetail: (id: string) => client.delete(`/dealer/bank-details/${id}`),
|
||||||
|
|
||||||
// Email Templates
|
// Email Templates
|
||||||
getEmailTemplates: () => client.get('/admin/email-templates'),
|
getEmailTemplates: () => client.get('/admin/email-templates'),
|
||||||
@ -152,6 +155,7 @@ export const API = {
|
|||||||
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`),
|
||||||
updateFnF: (id: string, data: any) => client.put(`/settlement/fnf/${id}`, data),
|
updateFnF: (id: string, data: any) => client.put(`/settlement/fnf/${id}`, data),
|
||||||
|
getSettlementDepartments: () => client.get('/settlement/departments'),
|
||||||
|
|
||||||
// Line items
|
// Line items
|
||||||
addLineItem: (fnfId: string, data: any) => client.post(`/settlement/fnf/${fnfId}/line-items`, data),
|
addLineItem: (fnfId: string, data: any) => client.post(`/settlement/fnf/${fnfId}/line-items`, data),
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
CreditCard,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Progress } from '../ui/progress';
|
import { Progress } from '../ui/progress';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
@ -356,6 +357,14 @@ export const ApplicationDetails = () => {
|
|||||||
constitutionType: data.constitutionType,
|
constitutionType: data.constitutionType,
|
||||||
architectureStatus: data.architectureStatus,
|
architectureStatus: data.architectureStatus,
|
||||||
statutoryStatus: data.statutoryStatus,
|
statutoryStatus: data.statutoryStatus,
|
||||||
|
panNumber: data.panNumber,
|
||||||
|
gstNumber: data.gstNumber,
|
||||||
|
bankName: data.bankName,
|
||||||
|
accountNumber: data.accountNumber,
|
||||||
|
ifscCode: data.ifscCode,
|
||||||
|
branchName: data.branchName,
|
||||||
|
accountHolderName: data.accountHolderName,
|
||||||
|
registeredAddress: data.registeredAddress,
|
||||||
};
|
};
|
||||||
setApplication(mappedApp);
|
setApplication(mappedApp);
|
||||||
if (data.uploadedDocuments) {
|
if (data.uploadedDocuments) {
|
||||||
@ -477,6 +486,50 @@ export const ApplicationDetails = () => {
|
|||||||
const [previewDoc, setPreviewDoc] = useState<any>(null);
|
const [previewDoc, setPreviewDoc] = useState<any>(null);
|
||||||
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
||||||
const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>('');
|
const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>('');
|
||||||
|
const [isEditingStatutory, setIsEditingStatutory] = useState(false);
|
||||||
|
const [statutoryForm, setStatutoryForm] = useState({
|
||||||
|
accountHolderName: '',
|
||||||
|
panNumber: '',
|
||||||
|
gstNumber: '',
|
||||||
|
bankName: '',
|
||||||
|
accountNumber: '',
|
||||||
|
ifscCode: '',
|
||||||
|
registeredAddress: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEditStatutory = () => {
|
||||||
|
if (!application) return;
|
||||||
|
setStatutoryForm({
|
||||||
|
accountHolderName: application.accountHolderName || '',
|
||||||
|
panNumber: application.panNumber || '',
|
||||||
|
gstNumber: application.gstNumber || '',
|
||||||
|
bankName: application.bankName || '',
|
||||||
|
accountNumber: application.accountNumber || '',
|
||||||
|
ifscCode: application.ifscCode || '',
|
||||||
|
registeredAddress: application.registeredAddress || ''
|
||||||
|
});
|
||||||
|
setIsEditingStatutory(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isSavingStatutory, setIsSavingStatutory] = useState(false);
|
||||||
|
const handleSaveStatutory = async () => {
|
||||||
|
try {
|
||||||
|
setIsSavingStatutory(true);
|
||||||
|
await onboardingService.updateApplication(applicationId!, statutoryForm);
|
||||||
|
toast.success('Statutory & Bank details updated successfully');
|
||||||
|
setIsEditingStatutory(false);
|
||||||
|
fetchApplication(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update statutory details', error);
|
||||||
|
toast.error('Failed to update details');
|
||||||
|
} finally {
|
||||||
|
setIsSavingStatutory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canEditStatutory = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin';
|
||||||
|
const isAdmin = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin';
|
||||||
|
|
||||||
const [interviews, setInterviews] = useState<any[]>([]);
|
const [interviews, setInterviews] = useState<any[]>([]);
|
||||||
const [isScheduling, setIsScheduling] = useState(false);
|
const [isScheduling, setIsScheduling] = useState(false);
|
||||||
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
|
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
|
||||||
@ -1672,75 +1725,72 @@ export const ApplicationDetails = () => {
|
|||||||
currentUserEvaluation?.decision === 'Rejected' ||
|
currentUserEvaluation?.decision === 'Rejected' ||
|
||||||
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
|
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
|
||||||
|
|
||||||
// Final visibility flags
|
// Centralized Permissions Utility (Consolidates 500 lines of fragmented logic)
|
||||||
const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
|
const getApplicationPermissions = () => {
|
||||||
const isAdministrativeStage = [
|
if (!application || !currentUser) {
|
||||||
'Level 3 Approved', 'FDD Verification',
|
return { canApprove: false, canReject: false, canSchedule: false, canAssign: false, isLoaLocked: false, showDecisionMessage: false };
|
||||||
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
|
|
||||||
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
|
|
||||||
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
|
|
||||||
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
|
|
||||||
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
|
|
||||||
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration'
|
|
||||||
].includes(application.status);
|
|
||||||
|
|
||||||
const finalDepositVerified = getDeposit('FIRST_FILL')?.status === 'Verified';
|
|
||||||
const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified;
|
|
||||||
|
|
||||||
// Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5)
|
|
||||||
// Approval Chain: DD-Head -> NBH
|
|
||||||
// Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5)
|
|
||||||
// Approval Chain: DD-Head -> NBH
|
|
||||||
const ddHeadApproved = application.stageApprovals?.some(
|
|
||||||
(a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sequential Enforcement for LOA APPROVAL (SRS 6.18.3.1)
|
|
||||||
// Approval Chain: DD-Head -> NBH
|
|
||||||
const ddHeadLoaApproved = application.stageApprovals?.some(
|
|
||||||
(a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'
|
|
||||||
);
|
|
||||||
|
|
||||||
let sequenceMet = true;
|
|
||||||
if (!['Super Admin', 'DD Admin'].includes(currentUser?.role || '')) {
|
|
||||||
// FDD Stage Restriction: Only DD Admin or Super Admin can approve
|
|
||||||
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') {
|
|
||||||
sequenceMet = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOI Sequence Enforcement
|
// 1. Core Flags
|
||||||
if (application.status === 'LOI In Progress') {
|
const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
|
||||||
if (currentUser?.role === 'NBH') {
|
const isAdministrativeStage = [
|
||||||
sequenceMet = !!ddHeadApproved; // NBH can only approve after DD Head
|
'Level 3 Approved', 'FDD Verification',
|
||||||
}
|
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
|
||||||
// Roles not in the sequence (like Admin or Finance) should not see the buttons for LOI issuance decision
|
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
|
||||||
if (!['DD Head', 'NBH'].includes(currentUser?.role || '')) {
|
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
|
||||||
sequenceMet = false;
|
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
|
||||||
}
|
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
|
||||||
}
|
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration'
|
||||||
|
].includes(application.status);
|
||||||
|
|
||||||
// LOA Sequence Enforcement
|
const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
|
||||||
if (application.status === 'LOA Pending') {
|
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected' || application.status === 'Approved';
|
||||||
if (currentUser?.role === 'NBH') {
|
|
||||||
sequenceMet = !!ddHeadLoaApproved; // NBH can only approve after DD Head
|
|
||||||
}
|
|
||||||
// Roles not in the sequence (like Finance or FDD) should not see the buttons for LOA decision
|
|
||||||
if (!['DD Head', 'NBH'].includes(currentUser?.role || '')) {
|
|
||||||
sequenceMet = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show Approve/Reject if:
|
// 2. Interview Specific Logic
|
||||||
// 1. It's an interview and feedback is submitted AND no decision made yet
|
const activeInterviewForUser = (interviews || []).find(i =>
|
||||||
// 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet AND sequence is valid
|
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
|
||||||
const shouldShowApproveReject =
|
i.participants?.some((p: any) => p.userId === currentUser?.id)
|
||||||
!isLoaLocked && (
|
);
|
||||||
(!hasMadeDecisionForUser && !!hasSubmittedFeedbackForActive) ||
|
const hasSubmittedFeedback = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
|
||||||
(!!isAdmin && !!isAdministrativeStage && !hasMadeStageDecision && !!sequenceMet)
|
(e: any) => e.evaluatorId === currentUser?.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldShowDecisionMessage = !!hasMadeDecisionForUser && (!isAdministrativeStage || !!hasMadeStageDecision);
|
// 3. Sequential Sequence Check
|
||||||
|
const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
|
||||||
|
const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
|
||||||
|
|
||||||
|
let sequenceMet = true;
|
||||||
|
if (!['Super Admin', 'DD Admin'].includes(currentUser.role)) {
|
||||||
|
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') sequenceMet = false;
|
||||||
|
if (application.status === 'LOI In Progress') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadApproved : (currentUser.role === 'DD Head');
|
||||||
|
if (application.status === 'LOA Pending') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadLoaApproved : (currentUser.role === 'DD Head');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Decision Tracking
|
||||||
|
const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id));
|
||||||
|
const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || '');
|
||||||
|
const hasMadeDecisionTotal = hasMadeStageDecision || hasMadeInterviewDecision;
|
||||||
|
|
||||||
|
// 5. Final Permission Bits
|
||||||
|
const isDecisionMade = hasMadeDecisionTotal || hasMadeStageDecision;
|
||||||
|
const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && (
|
||||||
|
(!!activeInterviewForUser && !!hasSubmittedFeedback) ||
|
||||||
|
(isAdminRole && isAdministrativeStage && sequenceMet)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canApprove: canApproveReject,
|
||||||
|
canReject: canApproveReject,
|
||||||
|
isLoaLocked,
|
||||||
|
showDecisionMessage: isDecisionMade && (!isAdministrativeStage || hasMadeStageDecision),
|
||||||
|
canSchedule: ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
|
||||||
|
!isFinalState &&
|
||||||
|
!([1, 2, 3].every(level => (interviews || []).some(i => i.level === level))),
|
||||||
|
canAssign: ['DD Admin', 'Super Admin', 'DD AM'].includes(currentUser.role)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissions = getApplicationPermissions();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -2045,8 +2095,7 @@ export const ApplicationDetails = () => {
|
|||||||
<FileText className="w-6 h-6 text-red-500" />
|
<FileText className="w-6 h-6 text-red-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<p className="text-slate-900 font-bold text-sm truncate max-w-[180px]">{report.reportDocument.fileName}</p>
|
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {formatDateTime(report.createdAt)}</p>
|
||||||
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {new Date(report.createdAt).toLocaleDateString()}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@ -2387,6 +2436,141 @@ export const ApplicationDetails = () => {
|
|||||||
<p className="text-slate-600 mb-2">Past Experience</p>
|
<p className="text-slate-600 mb-2">Past Experience</p>
|
||||||
<p className="text-slate-900">{application.pastExperience || 'N/A'}</p>
|
<p className="text-slate-900">{application.pastExperience || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-6 border-t mt-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-black text-slate-900 uppercase tracking-widest flex items-center gap-2">
|
||||||
|
<CreditCard className="w-4 h-4 text-amber-600" /> Statutory & Bank Information
|
||||||
|
</h3>
|
||||||
|
{canEditStatutory && !isEditingStatutory && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleEditStatutory}
|
||||||
|
className="h-8 text-amber-600 hover:text-amber-700 hover:bg-amber-50 gap-1.5"
|
||||||
|
>
|
||||||
|
<Pencil className="w-3.5 h-3.5" />
|
||||||
|
Edit Details
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditingStatutory ? (
|
||||||
|
<div className="bg-slate-50/50 p-6 rounded-xl border-2 border-amber-100 space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Legal Entity Name</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.accountHolderName}
|
||||||
|
onChange={(e) => setStatutoryForm({...statutoryForm, accountHolderName: e.target.value})}
|
||||||
|
placeholder="Enter Legal Entity Name"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">PAN Number</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.panNumber}
|
||||||
|
onChange={(e) => setStatutoryForm({...statutoryForm, panNumber: e.target.value.toUpperCase()})}
|
||||||
|
placeholder="10-digit PAN"
|
||||||
|
maxLength={10}
|
||||||
|
className="bg-white border-slate-200 uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">GST Number</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.gstNumber}
|
||||||
|
onChange={(e) => setStatutoryForm({...statutoryForm, gstNumber: e.target.value.toUpperCase()})}
|
||||||
|
placeholder="15-digit GSTIN"
|
||||||
|
maxLength={15}
|
||||||
|
className="bg-white border-slate-200 uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Registered Address</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.registeredAddress}
|
||||||
|
onChange={(e) => setStatutoryForm({...statutoryForm, registeredAddress: e.target.value})}
|
||||||
|
placeholder="Enter Registered Office Address"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Bank Name</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.bankName}
|
||||||
|
onChange={(e) => setStatutoryForm({...statutoryForm, bankName: e.target.value})}
|
||||||
|
placeholder="Enter Bank Name"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Account Number</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.accountNumber}
|
||||||
|
onChange={(e) => setStatutoryForm({...statutoryForm, accountNumber: e.target.value})}
|
||||||
|
placeholder="Enter Account Number"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">IFSC Code</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.ifscCode}
|
||||||
|
onChange={(e) => setStatutoryForm({...statutoryForm, ifscCode: e.target.value.toUpperCase()})}
|
||||||
|
placeholder="11-digit IFSC"
|
||||||
|
maxLength={11}
|
||||||
|
className="bg-white border-slate-200 uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsEditingStatutory(false)}
|
||||||
|
disabled={isSavingStatutory}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSaveStatutory}
|
||||||
|
disabled={isSavingStatutory}
|
||||||
|
className="bg-amber-600 hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
{isSavingStatutory ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Save Details'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 bg-slate-50/50 p-4 rounded-xl border border-slate-100">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Legal Entity Name</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900">{application.accountHolderName || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">PAN Number</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900 uppercase">{application.panNumber || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">GST Number</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900 uppercase">{application.gstNumber || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Registered Address</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900">{application.registeredAddress || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Bank Details</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900">{application.bankName || 'N/A'}</p>
|
||||||
|
<p className="text-[10px] text-slate-600">A/C: {application.accountNumber || 'N/A'}</p>
|
||||||
|
<p className="text-[10px] text-slate-600">IFSC: {application.ifscCode || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -2679,38 +2863,7 @@ export const ApplicationDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Branch Approval Button */}
|
|
||||||
{(() => {
|
|
||||||
const branchConfig = branchColor === 'blue'
|
|
||||||
? { status: application.architectureStatus, role: 'ARCHITECTURE', stageCode: 'ARCHITECTURE_WORK' }
|
|
||||||
: { status: application.statutoryStatus, role: 'Legal Admin', stageCode: 'STATUTORY_WORK' };
|
|
||||||
|
|
||||||
if (branchConfig.status !== 'COMPLETED' && (currentUser?.roleCode === branchConfig.role || currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin')) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={cn(
|
|
||||||
"h-14 px-6 font-bold shadow-lg border-b-4 active:border-b-0 active:translate-y-1 transition-all",
|
|
||||||
branchColor === 'blue' ? "bg-blue-600 hover:bg-blue-700 border-blue-800" : "bg-green-600 hover:bg-green-700 border-green-800"
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
onboardingService.submitStageDecision({
|
|
||||||
applicationId: application.id,
|
|
||||||
stageCode: branchConfig.stageCode,
|
|
||||||
decision: 'Approved',
|
|
||||||
remarks: `Consolidated approval for ${branch.name} track`
|
|
||||||
}).then(() => {
|
|
||||||
toast.success(`${branch.name} approved successfully`);
|
|
||||||
fetchApplication();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Approve Track
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
@ -2826,7 +2979,7 @@ export const ApplicationDetails = () => {
|
|||||||
<span className="truncate max-w-[150px] md:max-w-[300px]">{doc.fileName}</span>
|
<span className="truncate max-w-[150px] md:max-w-[300px]">{doc.fileName}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{doc.documentType}</TableCell>
|
<TableCell>{doc.documentType}</TableCell>
|
||||||
<TableCell>{new Date(doc.createdAt).toLocaleDateString()}</TableCell>
|
<TableCell>{formatDateTime(doc.createdAt)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')}
|
{doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -2919,7 +3072,7 @@ export const ApplicationDetails = () => {
|
|||||||
<h4 className="font-semibold text-slate-800 mb-2">
|
<h4 className="font-semibold text-slate-800 mb-2">
|
||||||
Level {interview.level} Interview
|
Level {interview.level} Interview
|
||||||
<span className="font-normal text-slate-500 text-sm ml-2">
|
<span className="font-normal text-slate-500 text-sm ml-2">
|
||||||
({new Date(interview.scheduleDate).toLocaleDateString()} - {interview.interviewType})
|
({formatDateTime(interview.scheduleDate)} - {interview.interviewType})
|
||||||
</span>
|
</span>
|
||||||
</h4>
|
</h4>
|
||||||
{interview.evaluations && interview.evaluations.length > 0 ? (
|
{interview.evaluations && interview.evaluations.length > 0 ? (
|
||||||
@ -3204,7 +3357,7 @@ export const ApplicationDetails = () => {
|
|||||||
{deposit?.paymentReference && (
|
{deposit?.paymentReference && (
|
||||||
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
|
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
|
||||||
<span>Ref: {deposit.paymentReference}</span>
|
<span>Ref: {deposit.paymentReference}</span>
|
||||||
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>}
|
{deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -3287,7 +3440,7 @@ export const ApplicationDetails = () => {
|
|||||||
{deposit?.paymentReference && (
|
{deposit?.paymentReference && (
|
||||||
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
|
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
|
||||||
<span>Ref: {deposit.paymentReference}</span>
|
<span>Ref: {deposit.paymentReference}</span>
|
||||||
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>}
|
{deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -3352,7 +3505,7 @@ export const ApplicationDetails = () => {
|
|||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<p className="text-slate-900 font-medium">{log.description || log.action}</p>
|
<p className="text-slate-900 font-medium">{log.description || log.action}</p>
|
||||||
<span className="text-slate-500 text-sm whitespace-nowrap ml-4">
|
<span className="text-slate-500 text-sm whitespace-nowrap ml-4">
|
||||||
{new Date(log.timestamp).toLocaleString()}
|
{formatDateTime(log.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-600 mt-1">by {log.userName || 'System'}</p>
|
<p className="text-slate-600 mt-1">by {log.userName || 'System'}</p>
|
||||||
@ -3418,7 +3571,7 @@ export const ApplicationDetails = () => {
|
|||||||
{application.deadline && (
|
{application.deadline && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Questionnaire Deadline</p>
|
<p className="text-slate-600">Questionnaire Deadline</p>
|
||||||
<p className="text-slate-900">{new Date(application.deadline).toLocaleDateString()}</p>
|
<p className="text-slate-900">{formatDateTime(application.deadline)}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -3434,7 +3587,7 @@ export const ApplicationDetails = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{/* Show Approve/Reject block */}
|
{/* Show Approve/Reject block */}
|
||||||
{isLoaLocked && (
|
{permissions.isLoaLocked && (
|
||||||
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
|
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
|
||||||
<Lock className="w-4 h-4 text-amber-600" />
|
<Lock className="w-4 h-4 text-amber-600" />
|
||||||
<AlertTitle className="text-amber-900 font-semibold">Stage Locked</AlertTitle>
|
<AlertTitle className="text-amber-900 font-semibold">Stage Locked</AlertTitle>
|
||||||
@ -3444,10 +3597,10 @@ export const ApplicationDetails = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowApproveReject && (
|
{permissions.canApprove && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-green-600 hover:bg-green-700"
|
className="w-full bg-green-600 hover:bg-green-700 font-bold"
|
||||||
onClick={() => setShowApproveModal(true)}
|
onClick={() => setShowApproveModal(true)}
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
@ -3456,7 +3609,7 @@ export const ApplicationDetails = () => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="w-full"
|
className="w-full font-bold"
|
||||||
onClick={() => setShowRejectModal(true)}
|
onClick={() => setShowRejectModal(true)}
|
||||||
>
|
>
|
||||||
<XCircle className="w-4 h-4 mr-2" />
|
<XCircle className="w-4 h-4 mr-2" />
|
||||||
@ -3465,7 +3618,7 @@ export const ApplicationDetails = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(shouldShowDecisionMessage) && (
|
{permissions.showDecisionMessage && (
|
||||||
<div className={`w-full p-2 text-center rounded border ${(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'}`}>
|
<div className={`w-full p-2 text-center rounded border ${(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'}`}>
|
||||||
You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
|
You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
|
||||||
</div>
|
</div>
|
||||||
@ -3488,17 +3641,16 @@ export const ApplicationDetails = () => {
|
|||||||
Work Note
|
Work Note
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
|
{permissions.canSchedule && (
|
||||||
!([1, 2, 3].every(level => interviews.some(i => i.level === level))) && (
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
className="w-full"
|
||||||
className="w-full"
|
onClick={() => setShowScheduleModal(true)}
|
||||||
onClick={() => setShowScheduleModal(true)}
|
>
|
||||||
>
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
<Calendar className="w-4 h-4 mr-2" />
|
Schedule Interview
|
||||||
Schedule Interview
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && (
|
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && (
|
||||||
<>
|
<>
|
||||||
@ -4639,7 +4791,7 @@ export const ApplicationDetails = () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="py-3 whitespace-nowrap text-slate-600">
|
<TableCell className="py-3 whitespace-nowrap text-slate-600">
|
||||||
{new Date(doc.createdAt).toLocaleDateString('en-GB')}
|
{formatDateTime(doc.createdAt)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="py-3 text-slate-600">
|
<TableCell className="py-3 text-slate-600">
|
||||||
{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}
|
{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}
|
||||||
|
|||||||
130
src/components/applications/BankDetailsModal.tsx
Normal file
130
src/components/applications/BankDetailsModal.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface BankDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
editingBank: any;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BankDetailsModal: React.FC<BankDetailsModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
editingBank,
|
||||||
|
isSubmitting
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingBank ? 'Edit Bank Details' : 'Add Bank Account'}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter the dealer's bank information for settlement transfers.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="bankName" className="text-right text-xs">Bank Name</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Input
|
||||||
|
id="bankName"
|
||||||
|
name="bankName"
|
||||||
|
defaultValue={editingBank?.bankName}
|
||||||
|
required
|
||||||
|
placeholder="e.g. HDFC Bank, ICICI Bank"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="accountHolderName" className="text-right text-xs">Holder Name</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Input
|
||||||
|
id="accountHolderName"
|
||||||
|
name="accountHolderName"
|
||||||
|
defaultValue={editingBank?.accountHolderName}
|
||||||
|
required
|
||||||
|
placeholder="Full name as per bank records"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="accountNumber" className="text-right text-xs">A/C Number</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Input
|
||||||
|
id="accountNumber"
|
||||||
|
name="accountNumber"
|
||||||
|
defaultValue={editingBank?.accountNumber}
|
||||||
|
required
|
||||||
|
placeholder="Enter account number"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="ifscCode" className="text-right text-xs">IFSC Code</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Input
|
||||||
|
id="ifscCode"
|
||||||
|
name="ifscCode"
|
||||||
|
defaultValue={editingBank?.ifscCode}
|
||||||
|
required
|
||||||
|
placeholder="11-character code"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="branchName" className="text-right text-xs">Branch</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Input
|
||||||
|
id="branchName"
|
||||||
|
name="branchName"
|
||||||
|
defaultValue={editingBank?.branchName}
|
||||||
|
required
|
||||||
|
placeholder="Branch location"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2 col-start-2 col-span-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="isPrimaryModal"
|
||||||
|
name="isPrimary"
|
||||||
|
defaultChecked={editingBank?.isPrimary}
|
||||||
|
className="w-4 h-4 rounded border-slate-300 text-amber-600 focus:ring-amber-500"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="isPrimaryModal" className="text-xs font-medium cursor-pointer">Set as primary account</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={onClose}>Cancel</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting} size="sm" className="bg-amber-600">
|
||||||
|
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
|
||||||
|
{editingBank ? 'Update Account' : 'Save Bank Details'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -81,7 +81,7 @@ const getStatusColor = (status: string) => {
|
|||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ConstitutionalChangeDetails({ requestId, onBack }: ConstitutionalChangeDetailsProps) {
|
export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ConstitutionalChangeDetailsProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
|
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
|
||||||
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
|
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
|
||||||
@ -152,6 +152,41 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
|
|
||||||
const currentStageIndex = getCurrentStageIndex();
|
const currentStageIndex = getCurrentStageIndex();
|
||||||
|
|
||||||
|
// Centralized Permissions Utility (Fixes security gap where buttons showed for everyone)
|
||||||
|
const getConstitutionalPermissions = () => {
|
||||||
|
if (!request || !currentUser) {
|
||||||
|
return { canApprove: false, canReject: false, canHold: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStage = request.currentStage;
|
||||||
|
const status = request.status;
|
||||||
|
const userRole = currentUser.role;
|
||||||
|
|
||||||
|
const isFinalState = ['Completed', 'Rejected', 'Hold'].includes(status);
|
||||||
|
|
||||||
|
// Find stage definition
|
||||||
|
const stageDef = workflowStages.find(s => s.name === currentStage || s.key === currentStage);
|
||||||
|
|
||||||
|
// Role matching logic (Handles Role names from constants vs workflow mapping)
|
||||||
|
const isCurrentlyAssigned = currentUser.roleCode === 'SUPER_ADMIN' || (
|
||||||
|
(stageDef?.role === 'ASM' && userRole === 'ASM') ||
|
||||||
|
(stageDef?.role === 'ZM/RBM' && (userRole === 'ZM' || userRole === 'RBM')) ||
|
||||||
|
(stageDef?.role === 'ZBH' && userRole === 'ZBH') ||
|
||||||
|
(stageDef?.role === 'DD Lead' && userRole === 'DD Lead') ||
|
||||||
|
(stageDef?.role === 'DD Head' && userRole === 'DD Head') ||
|
||||||
|
(stageDef?.role === 'NBH' && userRole === 'NBH') ||
|
||||||
|
(stageDef?.role === 'Legal Team' && (userRole === 'Legal' || userRole === 'Legal Admin'))
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canApprove: isCurrentlyAssigned && !isFinalState,
|
||||||
|
canReject: isCurrentlyAssigned && !isFinalState,
|
||||||
|
canHold: isCurrentlyAssigned && !isFinalState
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissions = getConstitutionalPermissions();
|
||||||
|
|
||||||
const handleAction = (type: 'approve' | 'reject' | 'hold') => {
|
const handleAction = (type: 'approve' | 'reject' | 'hold') => {
|
||||||
setActionType(type);
|
setActionType(type);
|
||||||
setIsActionDialogOpen(true);
|
setIsActionDialogOpen(true);
|
||||||
@ -203,7 +238,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-slate-900">{request.requestId} - Constitutional Change Details</h1>
|
<h1 className="text-slate-900">{request.requestId} - Constitutional Change Details</h1>
|
||||||
<p className="text-slate-600">
|
<p className="text-slate-600">
|
||||||
{request.outlet?.name || 'N/A'} ({request.outlet?.code || 'N/A'})
|
{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'} ({request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'})
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -221,9 +256,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 text-sm mb-1">Dealer Details</p>
|
<p className="text-slate-600 text-sm mb-1">Dealer Details</p>
|
||||||
<p className="text-slate-900">{request.outlet?.name || 'N/A'}</p>
|
<p className="text-slate-900">{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
|
||||||
<p className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</p>
|
<p className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</p>
|
||||||
<p className="text-slate-600 text-sm">{request.outlet?.city || request.outlet?.address || 'N/A'}</p>
|
<p className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.registeredAddress || request.outlet?.city || request.outlet?.address || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
|
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
|
||||||
@ -605,32 +640,44 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
|
|||||||
<CardTitle>Actions</CardTitle>
|
<CardTitle>Actions</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<Button
|
{permissions.canApprove && (
|
||||||
className="w-full bg-green-600 hover:bg-green-700"
|
<Button
|
||||||
onClick={() => handleAction('approve')}
|
className="w-full bg-green-600 hover:bg-green-700"
|
||||||
disabled={isActionLoading}
|
onClick={() => handleAction('approve')}
|
||||||
>
|
disabled={isActionLoading}
|
||||||
{isActionLoading && actionType === 'approve' ? (
|
>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
{isActionLoading && actionType === 'approve' ? (
|
||||||
) : (
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
) : (
|
||||||
)}
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||||
Approve Request
|
)}
|
||||||
</Button>
|
Approve Request
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
{permissions.canReject && (
|
||||||
variant="destructive"
|
<Button
|
||||||
className="w-full"
|
variant="destructive"
|
||||||
onClick={() => handleAction('reject')}
|
className="w-full"
|
||||||
disabled={isActionLoading}
|
onClick={() => handleAction('reject')}
|
||||||
>
|
disabled={isActionLoading}
|
||||||
{isActionLoading && actionType === 'reject' ? (
|
>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
{isActionLoading && actionType === 'reject' ? (
|
||||||
) : (
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
<AlertCircle className="w-4 h-4 mr-2" />
|
) : (
|
||||||
)}
|
<AlertCircle className="w-4 h-4 mr-2" />
|
||||||
Reject Request
|
)}
|
||||||
</Button>
|
Reject Request
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!permissions.canApprove && !permissions.canReject && (
|
||||||
|
<div className="text-center py-4 bg-slate-50 rounded-lg border border-dashed border-slate-200">
|
||||||
|
<p className="text-slate-500 text-xs px-4">
|
||||||
|
{permissions.isFinalState ? 'This request is finalized.' : 'No actions available for your role at this stage.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="border-t border-slate-200 pt-3 mt-3">
|
<div className="border-t border-slate-200 pt-3 mt-3">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -432,11 +432,15 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
|||||||
<TableRow key={request.requestId}>
|
<TableRow key={request.requestId}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.requestId}</div>
|
<div className="font-medium text-slate-900">{request.requestId}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
|
<div className="font-medium text-slate-900">{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.city || request.outlet?.address || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">
|
||||||
|
{request.dealer?.dealerProfile?.registeredAddress ||
|
||||||
|
(request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` :
|
||||||
|
(request.dealer?.dealerProfile?.application?.city || request.outlet?.address || 'N/A'))}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -507,11 +511,14 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
|||||||
<TableRow key={request.requestId}>
|
<TableRow key={request.requestId}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.requestId}</div>
|
<div className="font-medium text-slate-900">{request.requestId}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
|
<div className="font-medium text-slate-900">{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.city || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">
|
||||||
|
{request.dealer?.dealerProfile?.registeredAddress ||
|
||||||
|
(request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : (request.outlet?.address || 'N/A'))}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -578,11 +585,15 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
|||||||
<TableRow key={request.requestId}>
|
<TableRow key={request.requestId}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.requestId}</div>
|
<div className="font-medium text-slate-900">{request.requestId}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
|
<div className="font-medium text-slate-900">{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.city || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">
|
||||||
|
{request.dealer?.dealerProfile?.registeredAddress ||
|
||||||
|
(request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` :
|
||||||
|
(request.dealer?.dealerProfile?.application?.city || request.outlet?.address || 'N/A'))}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -655,11 +666,14 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
|
|||||||
<TableRow key={request.requestId}>
|
<TableRow key={request.requestId}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.requestId}</div>
|
<div className="font-medium text-slate-900">{request.requestId}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
|
<div className="font-medium text-slate-900">{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</div>
|
||||||
<div className="text-slate-600 text-sm">{request.outlet?.city || 'N/A'}</div>
|
<div className="text-slate-600 text-sm">
|
||||||
|
{request.dealer?.dealerProfile?.registeredAddress ||
|
||||||
|
(request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : (request.outlet?.address || 'N/A'))}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { toast } from 'sonner';
|
|||||||
import { onboardingService } from '../../services/onboarding.service';
|
import { onboardingService } from '../../services/onboarding.service';
|
||||||
import { worknoteService } from '../../services/worknote.service';
|
import { worknoteService } from '../../services/worknote.service';
|
||||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
// Simple helper for class merging
|
// Simple helper for class merging
|
||||||
const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
|
const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
|
||||||
@ -264,7 +265,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-slate-800">{docType.label}</p>
|
<p className="text-sm font-bold text-slate-800">{docType.label}</p>
|
||||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">
|
||||||
{doc ? `Uploaded: ${new Date(doc.createdAt).toLocaleDateString()}` : 'Missing in Documentation'}
|
{doc ? `Uploaded: ${formatDateTime(doc.createdAt)}` : 'Missing in Documentation'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -371,7 +372,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<p className="text-slate-900 font-black text-sm truncate max-w-[140px] uppercase">{report.reportDocument.fileName}</p>
|
<p className="text-slate-900 font-black text-sm truncate max-w-[140px] uppercase">{report.reportDocument.fileName}</p>
|
||||||
<p className="text-slate-500 text-[10px] font-bold">SUBMITTED {new Date(report.createdAt).toLocaleDateString()}</p>
|
<p className="text-slate-500 text-[10px] font-bold">SUBMITTED {formatDateTime(report.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@ -556,7 +557,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
<div className="flex-1 space-y-1">
|
<div className="flex-1 space-y-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-sm font-black text-slate-900">{note.author?.fullName || 'System'}</h4>
|
<h4 className="text-sm font-black text-slate-900">{note.author?.fullName || 'System'}</h4>
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase">{new Date(note.createdAt).toLocaleString()}</span>
|
<span className="text-[10px] font-bold text-slate-400 uppercase">{formatDateTime(note.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">{note.author?.roleCode || 'RE Stakeholder'}</p>
|
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">{note.author?.roleCode || 'RE Stakeholder'}</p>
|
||||||
<div className="mt-4 p-4 bg-white rounded-xl border border-slate-100 text-sm text-slate-700 leading-relaxed shadow-sm">
|
<div className="mt-4 p-4 bg-white rounded-xl border border-slate-100 text-sm text-slate-700 leading-relaxed shadow-sm">
|
||||||
@ -614,7 +615,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-slate-900">{step.stageName}</p>
|
<p className="text-sm font-bold text-slate-900">{step.stageName}</p>
|
||||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">
|
||||||
{step.stageCompletedAt ? `Completed ${new Date(step.stageCompletedAt).toLocaleDateString()}` : 'Pending'}
|
{step.stageCompletedAt ? `Completed ${formatDateTime(step.stageCompletedAt)}` : 'Pending'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { settlementService } from '../../services/settlement.service';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
@ -31,15 +32,27 @@ import {
|
|||||||
Edit2,
|
Edit2,
|
||||||
Trash2,
|
Trash2,
|
||||||
Save,
|
Save,
|
||||||
Paperclip
|
Paperclip,
|
||||||
|
FileDown
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
|
import { formatDateTime, formatDateOnly } from '../../lib/dateUtils';
|
||||||
|
import { BankDetailsModal } from './BankDetailsModal';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '../ui/dialog';
|
||||||
|
|
||||||
const ALL_DEPARTMENTS = [
|
// Will be updated from API
|
||||||
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
|
let ALL_DEPARTMENTS = [
|
||||||
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
|
'Sales', 'Service', 'Spares / Parts', 'Finance', 'Accounts', 'Warranty',
|
||||||
'Legal', 'Quality', 'Logistics', 'Customer Relations'
|
'Marketing', 'HR', 'IT', 'Legal', 'Logistics', 'Quality', 'FDD', 'Apparel',
|
||||||
|
'DMS', 'Admin / DD-Admin'
|
||||||
];
|
];
|
||||||
|
|
||||||
interface FinanceFnFDetailsPageProps {
|
interface FinanceFnFDetailsPageProps {
|
||||||
@ -50,38 +63,13 @@ interface FinanceFnFDetailsPageProps {
|
|||||||
// Removing mock data functions as we use live API
|
// Removing mock data functions as we use live API
|
||||||
|
|
||||||
|
|
||||||
const getDepartmentStatusColor = (status: string) => {
|
const SETTLEMENT_CHECKLIST = [
|
||||||
switch (status) {
|
{ id: 'calculations', label: 'Verified All Department Calculations' },
|
||||||
case 'NOC Submitted':
|
{ id: 'bank', label: 'Confirmed Bank Account Details' },
|
||||||
return 'bg-green-100 text-green-700 border-green-300';
|
{ id: 'docs', label: 'Reviewed All Supporting Documents' },
|
||||||
case 'Dues Pending':
|
{ id: 'sap', label: 'Synced Final Dues with SAP' },
|
||||||
return 'bg-red-100 text-red-700 border-red-300';
|
{ id: 'noc', label: 'Received All Mandatory NOCs' }
|
||||||
case 'Completed':
|
];
|
||||||
return 'bg-green-100 text-green-700 border-green-300';
|
|
||||||
case 'Pending':
|
|
||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
|
||||||
default:
|
|
||||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
||||||
@ -99,11 +87,28 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
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);
|
const [previewDocument, setPreviewDocument] = useState<any>(null);
|
||||||
|
const [bankDetails, setBankDetails] = useState<any[]>([]);
|
||||||
|
const [isBankModalOpen, setIsBankModalOpen] = useState(false);
|
||||||
|
const [editingBank, setEditingBank] = useState<any>(null);
|
||||||
|
const [checklist, setChecklist] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
fetchDepartments();
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
}, [fnfId]);
|
}, [fnfId]);
|
||||||
|
|
||||||
|
const fetchDepartments = async () => {
|
||||||
|
try {
|
||||||
|
const response = await API.getSettlementDepartments();
|
||||||
|
const data = response.data as any;
|
||||||
|
if (data && data.success) {
|
||||||
|
ALL_DEPARTMENTS = data.departments;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetch departments error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchFnFDetails = async () => {
|
const fetchFnFDetails = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -111,23 +116,23 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const s = data.fnf;
|
const s = data.fnf;
|
||||||
setFnfCase({
|
const mappedCase = {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
caseNumber: s.id.substring(0, 8).toUpperCase(),
|
caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
|
||||||
dealerName: s.outlet?.dealer?.fullName || s.outlet?.name || 'N/A',
|
dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || 'N/A',
|
||||||
dealerCode: s.outlet?.code || 'N/A',
|
dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || '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',
|
||||||
submittedDate: new Date(s.createdAt).toLocaleDateString(),
|
submittedDate: formatDateTime(s.createdAt),
|
||||||
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD',
|
createdAt: s.createdAt,
|
||||||
status: s.status,
|
dueDate: s.settlementDate ? formatDateTime(s.settlementDate) : 'TBD',
|
||||||
bankDetails: {
|
status: s.status,
|
||||||
accountName: s.outlet?.dealer?.fullName || 'N/A',
|
dealerId: s.outlet?.dealer?.id || s.dealerId,
|
||||||
accountNumber: 'N/A', // These should come from dealer model in a real app
|
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.requestId || s.terminationRequest?.id || "N/A",
|
||||||
ifscCode: 'N/A',
|
salesCode: s.dealer?.dealerCode?.salesCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A',
|
||||||
bankName: 'N/A',
|
serviceCode: s.dealer?.dealerCode?.serviceCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.serviceCode || 'N/A',
|
||||||
branch: 'N/A'
|
gearCode: s.dealer?.dealerCode?.gearCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gearCode || 'N/A',
|
||||||
},
|
gmaCode: s.dealer?.dealerCode?.gmaCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gmaCode || 'N/A',
|
||||||
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
|
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
|
||||||
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
|
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
|
||||||
const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
|
const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
|
||||||
@ -157,7 +162,17 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
url: c.supportingDocument
|
url: c.supportingDocument
|
||||||
}))
|
}))
|
||||||
]
|
]
|
||||||
});
|
};
|
||||||
|
|
||||||
|
setFnfCase(mappedCase);
|
||||||
|
|
||||||
|
// Sync bank details from the pre-fetched data inside the settlement object
|
||||||
|
const preFetchedBankDetails = s.bankDetails || s.dealer?.bankDetails || s.outlet?.dealer?.dealerProfile?.bankDetails;
|
||||||
|
if (preFetchedBankDetails && preFetchedBankDetails.length > 0) {
|
||||||
|
setBankDetails(preFetchedBankDetails);
|
||||||
|
} else if (s.outlet?.dealer?.id || s.dealerId) {
|
||||||
|
fetchBankDetails(s.outlet?.dealer?.id || s.dealerId);
|
||||||
|
}
|
||||||
|
|
||||||
// Split line items into categories
|
// Split line items into categories
|
||||||
const pItems: FinancialLineItem[] = [];
|
const pItems: FinancialLineItem[] = [];
|
||||||
@ -172,21 +187,29 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
amount: Math.abs(li.amount)
|
amount: Math.abs(li.amount)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (li.amount < 0) {
|
if (li.itemType === 'Payable') {
|
||||||
pItems.push(item);
|
pItems.push(item);
|
||||||
|
} else if (li.itemType === 'Deduction') {
|
||||||
|
dItems.push(item);
|
||||||
} else {
|
} else {
|
||||||
// Check if it's a deduction (usually Warranty related in this UI)
|
rItems.push(item);
|
||||||
if (li.department.toLowerCase().includes('warranty')) {
|
|
||||||
dItems.push(item);
|
|
||||||
} else {
|
|
||||||
rItems.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setPayableItems(pItems);
|
setPayableItems(pItems);
|
||||||
setReceivableItems(rItems);
|
setReceivableItems(rItems);
|
||||||
setDeductionItems(dItems);
|
setDeductionItems(dItems);
|
||||||
|
|
||||||
|
// Populate settlement details from backend
|
||||||
|
setSettlementDetails({
|
||||||
|
verificationTransactionId: s.transactionReference || '',
|
||||||
|
settlementAmount: (s.settlementAmount || calculateDynamicSettlement().settlementAmount).toString(),
|
||||||
|
settlementDate: s.settlementDate ? new Date(s.settlementDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
||||||
|
paymentMode: s.paymentMode || '',
|
||||||
|
bankReference: '', // Optional field
|
||||||
|
verificationRemarks: s.remarks || '',
|
||||||
|
adjustments: '0'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch F&F error:', error);
|
console.error('Fetch F&F error:', error);
|
||||||
@ -196,6 +219,61 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchBankDetails = async (dealerId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await API.getDealerBankDetails(dealerId);
|
||||||
|
const data = response.data as any;
|
||||||
|
if (data.success) {
|
||||||
|
setBankDetails(data.bankDetails || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch bank details error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpsertBank = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dealerId = fnfCase?.dealerId;
|
||||||
|
const response = await API.saveBankDetail(dealerId, {
|
||||||
|
...data,
|
||||||
|
id: editingBank?.id,
|
||||||
|
isPrimary: formData.get('isPrimary') === 'on'
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
toast.success('Bank details saved');
|
||||||
|
fetchBankDetails(dealerId);
|
||||||
|
setIsBankModalOpen(false);
|
||||||
|
setEditingBank(null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to save bank details');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteBank = async (id: string) => {
|
||||||
|
if (!confirm('Are you sure you want to delete this bank account?')) return;
|
||||||
|
try {
|
||||||
|
const response = await API.deleteBankDetail(id) as any;
|
||||||
|
if (response.data.success) {
|
||||||
|
toast.success('Bank detail deleted');
|
||||||
|
fetchBankDetails(fnfCase?.dealerId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to delete bank details');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleChecklist = (id: string) => {
|
||||||
|
setChecklist(prev =>
|
||||||
|
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Form states for adding new items
|
// Form states for adding new items
|
||||||
const [newPayable, setNewPayable] = useState({ department: '', description: '', amount: '' });
|
const [newPayable, setNewPayable] = useState({ department: '', description: '', amount: '' });
|
||||||
const [newReceivable, setNewReceivable] = useState({ department: '', description: '', amount: '' });
|
const [newReceivable, setNewReceivable] = useState({ department: '', description: '', amount: '' });
|
||||||
@ -225,6 +303,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
|
|
||||||
const settlement = calculateDynamicSettlement();
|
const settlement = calculateDynamicSettlement();
|
||||||
|
|
||||||
|
// Get primary bank for display
|
||||||
|
const primaryBank = bankDetails.find(b => b.isPrimary) || bankDetails[0];
|
||||||
|
|
||||||
const [settlementDetails, setSettlementDetails] = useState({
|
const [settlementDetails, setSettlementDetails] = useState({
|
||||||
verificationTransactionId: '',
|
verificationTransactionId: '',
|
||||||
settlementAmount: settlement.settlementAmount.toString(),
|
settlementAmount: settlement.settlementAmount.toString(),
|
||||||
@ -248,7 +329,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const response = await API.addLineItem(fnfId, {
|
const response = await API.addLineItem(fnfId, {
|
||||||
department: newPayable.department,
|
department: newPayable.department,
|
||||||
remarks: newPayable.description,
|
remarks: newPayable.description,
|
||||||
amount: amount
|
amount: Math.abs(parseFloat(newPayable.amount)),
|
||||||
|
itemType: 'Payable'
|
||||||
});
|
});
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@ -312,7 +394,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const response = await API.addLineItem(fnfId, {
|
const response = await API.addLineItem(fnfId, {
|
||||||
department: newReceivable.department,
|
department: newReceivable.department,
|
||||||
remarks: newReceivable.description,
|
remarks: newReceivable.description,
|
||||||
amount: Math.abs(parseFloat(newReceivable.amount)) // Receivable is positive
|
amount: Math.abs(parseFloat(newReceivable.amount)),
|
||||||
|
itemType: 'Receivable'
|
||||||
});
|
});
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@ -371,7 +454,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const response = await API.addLineItem(fnfId, {
|
const response = await API.addLineItem(fnfId, {
|
||||||
department: newDeduction.department,
|
department: newDeduction.department,
|
||||||
remarks: newDeduction.description,
|
remarks: newDeduction.description,
|
||||||
amount: Math.abs(parseFloat(newDeduction.amount)) // Deductions are positive (act as receivables)
|
amount: Math.abs(parseFloat(newDeduction.amount)),
|
||||||
|
itemType: 'Deduction'
|
||||||
});
|
});
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@ -426,7 +510,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
const newDocs = Array.from(files).map(file => ({
|
const newDocs = Array.from(files).map(file => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: `${(file.size / 1024).toFixed(0)} KB`,
|
size: `${(file.size / 1024).toFixed(0)} KB`,
|
||||||
uploadedOn: new Date().toISOString().split('T')[0],
|
uploadedOn: new Date().toISOString(),
|
||||||
type: 'Settlement Verification'
|
type: 'Settlement Verification'
|
||||||
}));
|
}));
|
||||||
setUploadedDocuments([...uploadedDocuments, ...newDocs]);
|
setUploadedDocuments([...uploadedDocuments, ...newDocs]);
|
||||||
@ -434,19 +518,35 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApproveSettlement = () => {
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleApproveSettlement = async () => {
|
||||||
if (!settlementDetails.verificationTransactionId || !settlementDetails.settlementDate || !settlementDetails.paymentMode) {
|
if (!settlementDetails.verificationTransactionId || !settlementDetails.settlementDate || !settlementDetails.paymentMode) {
|
||||||
toast.error('Please fill in all required settlement details');
|
toast.error('Please fill in all required settlement details');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjustedAmount = settlement.settlementAmount + parseFloat(settlementDetails.adjustments || '0');
|
try {
|
||||||
if (adjustedAmount.toString() !== settlementDetails.settlementAmount) {
|
setSubmitting(true);
|
||||||
toast.warning('Settlement amount has been adjusted');
|
const adjustedAmount = (settlement.settlementAmount || 0) + parseFloat(settlementDetails.adjustments || '0');
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(`F&F Settlement approved for ${fnfCase.dealerName}`);
|
await settlementService.updateFnF(fnfId, {
|
||||||
setTimeout(() => onBack(), 1500);
|
status: 'Completed',
|
||||||
|
finalSettlementAmount: adjustedAmount,
|
||||||
|
settlementDate: settlementDetails.settlementDate,
|
||||||
|
paymentMode: settlementDetails.paymentMode,
|
||||||
|
transactionReference: settlementDetails.verificationTransactionId,
|
||||||
|
remarks: settlementDetails.verificationRemarks || 'Approved by Finance'
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`F&F Settlement approved and completed for ${fnfCase.dealerName}`);
|
||||||
|
setTimeout(() => onBack(), 1500);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Approve settlement error:', error);
|
||||||
|
toast.error(error.message || 'Failed to approve settlement');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRejectSettlement = () => {
|
const handleRejectSettlement = () => {
|
||||||
@ -636,7 +736,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<Label className="text-slate-500">Request Age</Label>
|
<Label className="text-slate-500">Request Age</Label>
|
||||||
<p className="text-slate-900">
|
<p className="text-slate-900">
|
||||||
{(() => {
|
{(() => {
|
||||||
const submitted = new Date(fnfCase.submittedDate);
|
const submitted = new Date(fnfCase.createdAt);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const diffTime = Math.abs(today.getTime() - submitted.getTime());
|
const diffTime = Math.abs(today.getTime() - submitted.getTime());
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
@ -646,19 +746,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-500">Sales Code</Label>
|
<Label className="text-slate-500">Sales Code</Label>
|
||||||
<p className="text-slate-900">SAL-{fnfCase.dealerCode}</p>
|
<p className="text-slate-900">{fnfCase.salesCode}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-500">Service Code</Label>
|
<Label className="text-slate-500">Service Code</Label>
|
||||||
<p className="text-slate-900">SRV-{fnfCase.dealerCode}</p>
|
<p className="text-slate-900">{fnfCase.serviceCode}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-500">Gear Code</Label>
|
<Label className="text-slate-500">Gear Code</Label>
|
||||||
<p className="text-slate-900">GER-{fnfCase.dealerCode}</p>
|
<p className="text-slate-900">{fnfCase.gearCode}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-500">GMA Code</Label>
|
<Label className="text-slate-500">GMA Code</Label>
|
||||||
<p className="text-slate-900">GMA-{fnfCase.dealerCode}</p>
|
<p className="text-slate-900">{fnfCase.gmaCode}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -718,8 +818,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-amber-200 rounded-lg">
|
||||||
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-600">
|
||||||
@ -1160,7 +1260,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<Card className="border-2 border-blue-300 bg-blue-50">
|
<Card className="border-2 border-blue-300 bg-blue-50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-5 h-5 text-blue-600" />
|
<CheckCircle className="w-5 h-5 text-amber-600" />
|
||||||
Final Settlement Summary
|
Final Settlement Summary
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -1210,8 +1310,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-white border border-blue-200 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-white border border-amber-200 rounded-lg">
|
||||||
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-600">
|
||||||
@ -1292,7 +1392,11 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<TableRow key={dept.id}>
|
<TableRow key={dept.id}>
|
||||||
<TableCell>{dept.departmentName}</TableCell>
|
<TableCell>{dept.departmentName}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className={getDepartmentStatusColor(dept.status)}>
|
<Badge className={`border ${
|
||||||
|
dept.status === 'NOC Submitted' ? 'bg-green-100 text-green-700 border-green-300' :
|
||||||
|
dept.status === 'Dues Pending' ? 'bg-red-100 text-red-700 border-red-300' :
|
||||||
|
'bg-slate-100 text-slate-700 border-slate-300'
|
||||||
|
}`}>
|
||||||
{dept.status}
|
{dept.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -1330,7 +1434,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
filePath: dept.supportingDocument,
|
filePath: dept.supportingDocument,
|
||||||
documentType: 'Departmental Clearance Proof'
|
documentType: 'Departmental Clearance Proof'
|
||||||
})}
|
})}
|
||||||
className="flex items-center gap-1 text-[10px] text-blue-600 hover:underline"
|
className="flex items-center gap-1 text-[10px] text-amber-600 hover:underline"
|
||||||
>
|
>
|
||||||
<Paperclip className="w-3 h-3" />
|
<Paperclip className="w-3 h-3" />
|
||||||
View Proof
|
View Proof
|
||||||
@ -1346,10 +1450,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Important Notes */}
|
{/* Important Notes */}
|
||||||
<Card className="bg-blue-50 border-blue-200">
|
<Card className="bg-blue-50 border-amber-200">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p>
|
<p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p>
|
||||||
<ul className="text-sm text-slate-700 space-y-1">
|
<ul className="text-sm text-slate-700 space-y-1">
|
||||||
@ -1363,6 +1467,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|
||||||
<TabsContent value="documents" className="space-y-4">
|
<TabsContent value="documents" className="space-y-4">
|
||||||
{/* Submitted Documents */}
|
{/* Submitted Documents */}
|
||||||
<Card>
|
<Card>
|
||||||
@ -1446,51 +1551,93 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="bank" className="space-y-4">
|
<TabsContent value="bank" className="space-y-4">
|
||||||
{/* Bank Account Details */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<div>
|
||||||
<Building className="w-5 h-5" />
|
<CardTitle className="flex items-center gap-2">
|
||||||
Dealer Bank Account Details
|
<Building className="w-5 h-5" />
|
||||||
</CardTitle>
|
Dealer Bank Account Details
|
||||||
<CardDescription>
|
</CardTitle>
|
||||||
Bank account for settlement transfer (if payable to dealer)
|
<CardDescription>
|
||||||
</CardDescription>
|
Manage bank accounts for settlement transfer
|
||||||
</CardHeader>
|
</CardDescription>
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-slate-500">Account Holder Name</Label>
|
|
||||||
<p className="text-slate-900">{fnfCase.bankDetails.accountName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-slate-500">Account Number</Label>
|
|
||||||
<p className="text-slate-900">{fnfCase.bankDetails.accountNumber}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-slate-500">IFSC Code</Label>
|
|
||||||
<p className="text-slate-900">{fnfCase.bankDetails.ifscCode}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-slate-500">Bank Name</Label>
|
|
||||||
<p className="text-slate-900">{fnfCase.bankDetails.bankName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-slate-500">Branch</Label>
|
|
||||||
<p className="text-slate-900">{fnfCase.bankDetails.branch}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-amber-600"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingBank(null);
|
||||||
|
setIsBankModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Add Bank Account
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{bankDetails.length > 0 ? (
|
||||||
|
bankDetails.map((bank: any) => (
|
||||||
|
<Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-amber-500 bg-blue-50/30' : ''}`}>
|
||||||
|
{bank.isPrimary && (
|
||||||
|
<div className="absolute top-0 right-0 p-1 bg-amber-600 text-white text-[10px] uppercase font-bold px-2 rounded-bl">
|
||||||
|
Primary
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<CardContent className="p-4 pt-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Holder</Label>
|
||||||
|
<p className="text-sm font-semibold">{bank.accountHolderName}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-slate-500 uppercase font-bold">Bank</Label>
|
||||||
|
<p className="text-xs truncate">{bank.bankName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-slate-500 uppercase font-bold">IFSC</Label>
|
||||||
|
<p className="text-xs">{bank.ifscCode}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Number</Label>
|
||||||
|
<p className="text-xs font-mono">{bank.accountNumber}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
|
||||||
<div className="flex items-start gap-3">
|
<Button
|
||||||
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
|
variant="ghost"
|
||||||
<div>
|
size="sm"
|
||||||
<p className="text-sm text-slate-900 mb-1">Bank Verification Required</p>
|
className="h-7 text-[11px] text-amber-600"
|
||||||
<p className="text-sm text-slate-600">
|
onClick={() => {
|
||||||
Please verify bank account details before processing settlement payment
|
setEditingBank(bank);
|
||||||
</p>
|
setIsBankModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit2 className="w-3 h-3 mr-1" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-[11px] text-red-600"
|
||||||
|
onClick={() => handleDeleteBank(bank.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 mr-1" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="col-span-full py-12 text-center border-2 border-dashed rounded-lg bg-slate-50">
|
||||||
|
<Building className="w-12 h-12 text-slate-300 mx-auto mb-3" />
|
||||||
|
<p className="text-slate-600 text-sm">No bank details found</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -1511,168 +1658,236 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
{fnfCase.status === 'Completed' ? (
|
||||||
<Label htmlFor="paymentMode">
|
<div className="space-y-6">
|
||||||
Payment Mode <span className="text-red-500">*</span>
|
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
</Label>
|
<div className="flex items-center gap-3 text-green-700 mb-2">
|
||||||
<Input
|
<CheckCircle className="w-5 h-5" />
|
||||||
id="paymentMode"
|
<span className="font-semibold">Settlement Completed</span>
|
||||||
placeholder="e.g., NEFT, RTGS, Cheque"
|
</div>
|
||||||
value={settlementDetails.paymentMode}
|
<p className="text-sm text-green-600">
|
||||||
onChange={(e) => setSettlementDetails({ ...settlementDetails, paymentMode: e.target.value })}
|
This settlement has been finalized and processed.
|
||||||
/>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-3">
|
||||||
<Label htmlFor="verificationTxnId">
|
<div className="flex justify-between items-center py-2 border-b">
|
||||||
Transaction ID / Reference <span className="text-red-500">*</span>
|
<span className="text-slate-500 text-sm">Settlement Date</span>
|
||||||
</Label>
|
<span className="text-slate-900 font-medium">{formatDateTime(settlementDetails.settlementDate)}</span>
|
||||||
<Input
|
</div>
|
||||||
id="verificationTxnId"
|
<div className="flex justify-between items-center py-2 border-b">
|
||||||
placeholder="Enter transaction reference"
|
<span className="text-slate-500 text-sm">Payment Mode</span>
|
||||||
value={settlementDetails.verificationTransactionId}
|
<span className="text-slate-900 font-medium">{settlementDetails.paymentMode}</span>
|
||||||
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationTransactionId: e.target.value })}
|
</div>
|
||||||
/>
|
<div className="flex justify-between items-center py-2 border-b">
|
||||||
</div>
|
<span className="text-slate-500 text-sm">Transaction ID</span>
|
||||||
|
<span className="text-slate-900 font-medium truncate ml-4 max-w-[150px]" title={settlementDetails.verificationTransactionId}>
|
||||||
|
{settlementDetails.verificationTransactionId}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-2 border-b">
|
||||||
|
<span className="text-slate-500 text-sm">Final Amount</span>
|
||||||
|
<span className="text-slate-900 font-bold text-lg">₹{parseFloat(settlementDetails.settlementAmount).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
{settlementDetails.verificationRemarks && (
|
||||||
<Label htmlFor="bankReference">
|
<div className="mt-4">
|
||||||
Bank Reference Number
|
<Label className="text-slate-500 mb-1 block">Finance Remarks</Label>
|
||||||
</Label>
|
<div className="p-3 bg-slate-50 rounded border text-sm text-slate-700">
|
||||||
<Input
|
{settlementDetails.verificationRemarks}
|
||||||
id="bankReference"
|
</div>
|
||||||
placeholder="Enter bank reference"
|
</div>
|
||||||
value={settlementDetails.bankReference}
|
)}
|
||||||
onChange={(e) => setSettlementDetails({ ...settlementDetails, bankReference: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Button variant="outline" className="w-full mt-4" onClick={() => window.print()}>
|
||||||
<Label htmlFor="settlementAmount">
|
<FileDown className="w-4 h-4 mr-2" />
|
||||||
Settlement Amount (₹) <span className="text-red-500">*</span>
|
Download Settlement Letter
|
||||||
</Label>
|
</Button>
|
||||||
<Input
|
</div>
|
||||||
id="settlementAmount"
|
) : (
|
||||||
type="number"
|
<>
|
||||||
placeholder="Enter settlement amount"
|
{/* Settlement Checklist */}
|
||||||
value={settlementDetails.settlementAmount}
|
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4">
|
||||||
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementAmount: e.target.value })}
|
<p className="text-sm font-bold text-slate-900 mb-3 flex items-center gap-2">
|
||||||
/>
|
<CheckCircle className="w-4 h-4 text-amber-600" />
|
||||||
</div>
|
Compliance Checklist
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{SETTLEMENT_CHECKLIST.map(item => (
|
||||||
|
<div key={item.id} className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`check-${item.id}`}
|
||||||
|
checked={checklist.includes(item.id)}
|
||||||
|
onChange={() => toggleChecklist(item.id)}
|
||||||
|
className="w-4 h-4 mt-1 rounded border-slate-300 text-amber-600 focus:ring-amber-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor={`check-${item.id}`} className="text-sm text-slate-700 leading-tight">
|
||||||
|
{item.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="adjustments">
|
<Label htmlFor="paymentMode">
|
||||||
Adjustments (₹)
|
Payment Mode <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="adjustments"
|
id="paymentMode"
|
||||||
type="number"
|
placeholder="e.g., NEFT, RTGS, Cheque"
|
||||||
placeholder="Enter any adjustments"
|
value={settlementDetails.paymentMode}
|
||||||
value={settlementDetails.adjustments}
|
onChange={(e) => setSettlementDetails({ ...settlementDetails, paymentMode: e.target.value })}
|
||||||
onChange={(e) => {
|
/>
|
||||||
const adjustments = e.target.value;
|
</div>
|
||||||
const adjustedAmount = settlement.settlementAmount + parseFloat(adjustments || '0');
|
|
||||||
setSettlementDetails({
|
|
||||||
...settlementDetails,
|
|
||||||
adjustments,
|
|
||||||
settlementAmount: adjustedAmount.toString()
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{parseFloat(settlementDetails.adjustments) !== 0 && (
|
|
||||||
<p className="text-sm text-amber-600 mt-1 flex items-center gap-1">
|
|
||||||
<AlertCircle className="w-3 h-3" />
|
|
||||||
Adjusted amount: ₹{settlementDetails.settlementAmount}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="settlementDate">
|
<Label htmlFor="verificationTxnId">
|
||||||
Settlement Date <span className="text-red-500">*</span>
|
Transaction ID / Reference <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="settlementDate"
|
id="verificationTxnId"
|
||||||
type="date"
|
placeholder="Enter transaction reference"
|
||||||
value={settlementDetails.settlementDate}
|
value={settlementDetails.verificationTransactionId}
|
||||||
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementDate: e.target.value })}
|
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationTransactionId: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="verificationRemarks">Verification Remarks</Label>
|
<Label htmlFor="bankReference">
|
||||||
<Textarea
|
Bank Reference Number
|
||||||
id="verificationRemarks"
|
</Label>
|
||||||
placeholder="Enter any remarks or notes..."
|
<Input
|
||||||
rows={4}
|
id="bankReference"
|
||||||
value={settlementDetails.verificationRemarks}
|
placeholder="Enter bank reference"
|
||||||
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationRemarks: e.target.value })}
|
value={settlementDetails.bankReference}
|
||||||
/>
|
onChange={(e) => setSettlementDetails({ ...settlementDetails, bankReference: e.target.value })}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 space-y-3 border-t">
|
<div>
|
||||||
<Button
|
<Label htmlFor="settlementAmount">
|
||||||
className="w-full bg-green-600 hover:bg-green-700"
|
Settlement Amount (₹) <span className="text-red-500">*</span>
|
||||||
onClick={handleApproveSettlement}
|
</Label>
|
||||||
>
|
<Input
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
id="settlementAmount"
|
||||||
Approve Settlement
|
type="number"
|
||||||
</Button>
|
placeholder="Enter settlement amount"
|
||||||
|
value={settlementDetails.settlementAmount}
|
||||||
|
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementAmount: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<div>
|
||||||
variant="outline"
|
<Label htmlFor="adjustments">
|
||||||
className="w-full border-blue-300 text-blue-600 hover:bg-blue-50"
|
Adjustments (₹)
|
||||||
onClick={handleRequestClarification}
|
</Label>
|
||||||
>
|
<Input
|
||||||
<Send className="w-4 h-4 mr-2" />
|
id="adjustments"
|
||||||
Request Clarification
|
type="number"
|
||||||
</Button>
|
placeholder="Enter any adjustments"
|
||||||
|
value={settlementDetails.adjustments}
|
||||||
|
onChange={(e) => {
|
||||||
|
const adjustments = e.target.value;
|
||||||
|
const adjustedAmount = settlement.settlementAmount + parseFloat(adjustments || '0');
|
||||||
|
setSettlementDetails({
|
||||||
|
...settlementDetails,
|
||||||
|
adjustments,
|
||||||
|
settlementAmount: adjustedAmount.toString()
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{parseFloat(settlementDetails.adjustments) !== 0 && (
|
||||||
|
<p className="text-sm text-amber-600 mt-1 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3 h-3" />
|
||||||
|
Adjusted amount: ₹{settlementDetails.settlementAmount}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<div>
|
||||||
variant="outline"
|
<Label htmlFor="settlementDate">
|
||||||
className="w-full border-red-300 text-red-600 hover:bg-red-50"
|
Settlement Date <span className="text-red-500">*</span>
|
||||||
onClick={handleRejectSettlement}
|
</Label>
|
||||||
>
|
<Input
|
||||||
<XCircle className="w-4 h-4 mr-2" />
|
id="settlementDate"
|
||||||
Reject Settlement
|
type="date"
|
||||||
</Button>
|
value={settlementDetails.settlementDate}
|
||||||
</div>
|
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementDate: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="verificationRemarks">Verification Remarks</Label>
|
||||||
|
<Textarea
|
||||||
|
id="verificationRemarks"
|
||||||
|
placeholder="Enter any remarks or notes..."
|
||||||
|
rows={4}
|
||||||
|
value={settlementDetails.verificationRemarks}
|
||||||
|
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationRemarks: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 space-y-3 border-t">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={handleApproveSettlement}
|
||||||
|
disabled={submitting || checklist.length < SETTLEMENT_CHECKLIST.length}
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Complete Settlement
|
||||||
|
</Button>
|
||||||
|
{checklist.length < SETTLEMENT_CHECKLIST.length && (
|
||||||
|
<p className="text-[10px] text-center text-red-500 mt-2 italic">
|
||||||
|
Check all compliance items to enable settlement
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-blue-300 text-amber-600 hover:bg-blue-50"
|
||||||
|
onClick={handleRequestClarification}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
Request Clarification
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-red-300 text-red-600 hover:bg-red-50"
|
||||||
|
onClick={handleRejectSettlement}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
<XCircle className="w-4 h-4 mr-2" />
|
||||||
|
Reject Settlement
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Quick Info Card */}
|
|
||||||
<Card className="bg-blue-50 border-blue-200">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">Settlement Checklist</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ul className="space-y-2 text-sm text-slate-700">
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
|
|
||||||
<span>Verify all financial calculations</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
|
|
||||||
<span>Confirm bank account details</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
|
|
||||||
<span>Review all submitted documents</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
|
|
||||||
<span>Upload settlement proof documents</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
|
|
||||||
<span>Enter transaction details accurately</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<BankDetailsModal
|
||||||
|
isOpen={isBankModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsBankModalOpen(false);
|
||||||
|
setEditingBank(null);
|
||||||
|
}}
|
||||||
|
onSubmit={handleUpsertBank}
|
||||||
|
editingBank={editingBank}
|
||||||
|
isSubmitting={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<DocumentPreviewModal
|
<DocumentPreviewModal
|
||||||
isOpen={!!previewDocument}
|
isOpen={!!previewDocument}
|
||||||
onClose={() => setPreviewDocument(null)}
|
onClose={() => setPreviewDocument(null)}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import {
|
|||||||
TrendingDown,
|
TrendingDown,
|
||||||
MapPin
|
MapPin
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
// Using live data from API instead of mockFnFCases
|
// Using live data from API instead of mockFnFCases
|
||||||
@ -76,12 +77,12 @@ 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,
|
caseId: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
|
||||||
dealerCode: s.outlet?.code || 'N/A',
|
dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
|
||||||
dealerName: s.outlet?.dealer?.name || 'N/A',
|
dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || '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',
|
||||||
submittedDate: new Date(s.createdAt).toLocaleDateString(),
|
submittedDate: formatDateTime(s.createdAt),
|
||||||
status: s.status === 'Calculated' ? 'Pending Finance Review' : (s.status === 'Settled' ? 'Settled' : s.status),
|
status: s.status === 'Calculated' ? 'Pending Finance Review' : (s.status === 'Settled' ? 'Settled' : s.status),
|
||||||
financialData: {
|
financialData: {
|
||||||
totalPayables: parseFloat(s.totalPayables) || 0,
|
totalPayables: parseFloat(s.totalPayables) || 0,
|
||||||
@ -90,7 +91,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
},
|
},
|
||||||
settlementAmount: Math.abs(parseFloat(s.netAmount) || 0),
|
settlementAmount: Math.abs(parseFloat(s.netAmount) || 0),
|
||||||
settlementType: parseFloat(s.netAmount) > 0 ? 'Payable to Dealer' : 'Receivable from Dealer',
|
settlementType: parseFloat(s.netAmount) > 0 ? 'Payable to Dealer' : 'Receivable from Dealer',
|
||||||
approvedDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : null
|
approvedDate: s.settlementDate ? formatDateTime(s.settlementDate) : null
|
||||||
});
|
});
|
||||||
|
|
||||||
const displaySettlements = settlements.map(getMappedData);
|
const displaySettlements = settlements.map(getMappedData);
|
||||||
|
|||||||
@ -336,7 +336,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-900 font-medium">{doc.fileName || doc.name}</p>
|
<p className="text-slate-900 font-medium">{doc.fileName || doc.name}</p>
|
||||||
<p className="text-xs text-slate-500 uppercase">{doc.documentType} • {new Date(doc.createdAt).toLocaleDateString()}</p>
|
<p className="text-xs text-slate-500 uppercase">{doc.documentType} • {formatDateTime(doc.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
|||||||
import { User } from '../../lib/mock-data';
|
import { User } from '../../lib/mock-data';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface FnFPageProps {
|
interface FnFPageProps {
|
||||||
currentUser: User | null;
|
currentUser: User | null;
|
||||||
@ -85,11 +86,11 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
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',
|
||||||
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.id || 'N/A',
|
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.id || 'N/A',
|
||||||
submittedOn: new Date(s.createdAt).toLocaleDateString(),
|
submittedOn: formatDateTime(s.createdAt),
|
||||||
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
|
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
|
||||||
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
|
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
|
||||||
totalPayableAmount: parseFloat(s.totalPayables) || 0,
|
totalPayableAmount: parseFloat(s.totalPayables) || 0,
|
||||||
completedOn: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : null,
|
completedOn: s.settlementDate ? formatDateTime(s.settlementDate) : null,
|
||||||
departmentResponses: s.lineItems || []
|
departmentResponses: s.lineItems || []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Badge } from '../../ui/badge';
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
|
||||||
import { Mail, Plus, Edit2, Trash2, Calendar } from 'lucide-react';
|
import { Mail, Plus, Edit2, Trash2, Calendar } from 'lucide-react';
|
||||||
import { RootState } from '../../../store';
|
import { RootState } from '../../../store';
|
||||||
|
import { formatDateTime } from '../../ui/utils';
|
||||||
|
|
||||||
interface EmailTemplatesProps {
|
interface EmailTemplatesProps {
|
||||||
onAddTemplate: () => void;
|
onAddTemplate: () => void;
|
||||||
@ -63,7 +64,7 @@ export const EmailTemplates: React.FC<EmailTemplatesProps> = ({
|
|||||||
<TableCell className="text-slate-500 text-sm">
|
<TableCell className="text-slate-500 text-sm">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Calendar className="w-3.5 h-3.5" />
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
{template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : '-'}
|
{template.updatedAt ? formatDateTime(template.updatedAt) : '-'}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Badge } from '../../ui/badge';
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
|
||||||
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
|
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
|
||||||
import { RootState } from '../../../store';
|
import { RootState } from '../../../store';
|
||||||
|
import { formatDateTime } from '../../ui/utils';
|
||||||
|
|
||||||
interface LocationManagementProps {
|
interface LocationManagementProps {
|
||||||
onAddLocation: () => void;
|
onAddLocation: () => void;
|
||||||
@ -85,13 +86,13 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
|
|||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span className="text-slate-600">From:</span>
|
<span className="text-slate-600">From:</span>
|
||||||
<Badge variant="outline" className="text-xs font-medium">
|
<Badge variant="outline" className="text-xs font-medium">
|
||||||
{new Date(district.openFrom).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
|
{formatDateTime(district.openFrom)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span className="text-slate-600">To:</span>
|
<span className="text-slate-600">To:</span>
|
||||||
<Badge variant="outline" className="text-xs font-medium">
|
<Badge variant="outline" className="text-xs font-medium">
|
||||||
{new Date(district.openTo).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
|
{formatDateTime(district.openTo)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '../ui/table';
|
} from '../ui/table';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface NonOpportunitiesPageProps {
|
interface NonOpportunitiesPageProps {
|
||||||
onViewDetails: (id: string) => void;
|
onViewDetails: (id: string) => void;
|
||||||
@ -230,7 +231,7 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
|
|||||||
<TableCell className="text-slate-600">{lead.pastExperience}</TableCell>
|
<TableCell className="text-slate-600">{lead.pastExperience}</TableCell>
|
||||||
<TableCell className="text-slate-900">{lead.education}</TableCell>
|
<TableCell className="text-slate-900">{lead.education}</TableCell>
|
||||||
<TableCell className="text-slate-600">
|
<TableCell className="text-slate-600">
|
||||||
{new Date(lead.submissionDate).toLocaleDateString()}
|
{formatDateTime(lead.submissionDate)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -4,7 +4,14 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
Clock,
|
Clock,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
File
|
File,
|
||||||
|
CreditCard,
|
||||||
|
Building,
|
||||||
|
Landmark,
|
||||||
|
CheckCircle2,
|
||||||
|
Info,
|
||||||
|
User,
|
||||||
|
MapPin
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
@ -22,6 +29,19 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
const [selectedDocType, setSelectedDocType] = useState('');
|
const [selectedDocType, setSelectedDocType] = useState('');
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// Statutory & Bank State
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
panNumber: '',
|
||||||
|
gstNumber: '',
|
||||||
|
registeredAddress: '',
|
||||||
|
bankName: '',
|
||||||
|
accountNumber: '',
|
||||||
|
ifscCode: '',
|
||||||
|
branchName: '',
|
||||||
|
accountHolderName: ''
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@ -36,7 +56,18 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (detailsRes.data?.success) {
|
if (detailsRes.data?.success) {
|
||||||
setDetails(detailsRes.data.data);
|
const data = detailsRes.data.data;
|
||||||
|
setDetails(data);
|
||||||
|
setForm({
|
||||||
|
panNumber: data.panNumber || '',
|
||||||
|
gstNumber: data.gstNumber || '',
|
||||||
|
registeredAddress: data.registeredAddress || data.address || '',
|
||||||
|
bankName: data.bankName || '',
|
||||||
|
accountNumber: data.accountNumber || '',
|
||||||
|
ifscCode: data.ifscCode || '',
|
||||||
|
branchName: data.branchName || '',
|
||||||
|
accountHolderName: data.accountHolderName || data.applicantName || ''
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (docsRes.data?.success || docsRes.ok) {
|
if (docsRes.data?.success || docsRes.ok) {
|
||||||
setDocuments(docsRes.data.data || []);
|
setDocuments(docsRes.data.data || []);
|
||||||
@ -49,6 +80,24 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveDetails = async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const response: any = await API.updateApplication(id, form);
|
||||||
|
if (response.data?.success || response.ok) {
|
||||||
|
toast.success('Business details updated successfully');
|
||||||
|
fetchData();
|
||||||
|
} else {
|
||||||
|
toast.error(response.data?.message || 'Update failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save details error:', error);
|
||||||
|
toast.error('Failed to save details');
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files && e.target.files[0]) {
|
if (e.target.files && e.target.files[0]) {
|
||||||
setFile(e.target.files[0]);
|
setFile(e.target.files[0]);
|
||||||
@ -109,84 +158,236 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<button
|
<div className="flex items-center">
|
||||||
onClick={onBack}
|
<button
|
||||||
className="mr-3 p-1.5 rounded-full hover:bg-slate-200 text-slate-600 transition-colors"
|
onClick={onBack}
|
||||||
>
|
className="mr-3 p-1.5 rounded-full hover:bg-slate-200 text-slate-600 transition-colors"
|
||||||
<ChevronLeft className="w-5 h-5" />
|
>
|
||||||
</button>
|
<ChevronLeft className="w-5 h-5" />
|
||||||
<div>
|
</button>
|
||||||
<h1 className="text-slate-900 text-2xl font-bold mb-1">Application Details</h1>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<h1 className="text-slate-900 text-2xl font-bold mb-1">Application Details</h1>
|
||||||
<p className="text-slate-600 font-medium">
|
<div className="flex items-center gap-2">
|
||||||
{details.applicationId || 'Loading...'}
|
<p className="text-slate-600 font-medium">
|
||||||
</p>
|
{details.applicationId || 'Loading...'}
|
||||||
{details.districtId ? (
|
</p>
|
||||||
<span className="text-[10px] bg-green-100 text-green-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Opportunity</span>
|
{details.districtId ? (
|
||||||
) : (
|
<span className="text-[10px] bg-green-100 text-green-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Opportunity</span>
|
||||||
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Future Reference</span>
|
) : (
|
||||||
)}
|
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Future Reference</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="animate-in fade-in duration-500">
|
<div className="animate-in fade-in duration-500 space-y-6">
|
||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6 mb-6">
|
{/* Status & Tracking Summary Card */}
|
||||||
<h4 className="text-lg font-semibold text-slate-900 mb-4 border-b pb-2">Status & Tracking</h4>
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="flex items-center justify-between mb-4 border-b pb-2">
|
||||||
<div>
|
<h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||||||
<p className="text-sm text-slate-500 mb-1">Overall Status</p>
|
<Info className="w-5 h-5 text-amber-600" /> Application Summary
|
||||||
<p className="font-medium text-slate-900">{details.overallStatus || '-'}</p>
|
</h4>
|
||||||
</div>
|
<div className="text-right">
|
||||||
<div>
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Current Stage</p>
|
||||||
<p className="text-sm text-slate-500 mb-1">Current Stage</p>
|
<span className="bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide">
|
||||||
<p className="font-medium text-slate-900">{details.currentStage || '-'}</p>
|
{details.currentStage || details.overallStatus}
|
||||||
</div>
|
</span>
|
||||||
<div>
|
|
||||||
<p className="text-sm text-slate-500 mb-1">Location</p>
|
|
||||||
<p className="font-medium text-slate-900">{details.city}, {details.state}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-slate-500 mb-1">Applied Date</p>
|
|
||||||
<p className="font-medium text-slate-900">
|
|
||||||
{details.createdAt ? formatDateTime(details.createdAt) : '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{details.statusHistory?.[0]?.changeReason && (
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="mt-4 p-3 bg-amber-50 border border-amber-100 rounded-lg">
|
<div className="space-y-4">
|
||||||
<p className="text-xs font-semibold text-amber-800 uppercase tracking-wider mb-1">Latest Feedback</p>
|
<div className="flex items-start gap-3">
|
||||||
<p className="text-sm text-amber-900 italic">"{details.statusHistory[0].changeReason}"</p>
|
<div className="p-2 bg-blue-50 rounded-lg">
|
||||||
|
<User className="w-4 h-4 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold">Applicant</p>
|
||||||
|
<p className="text-sm font-semibold text-slate-900">{details.applicantName}</p>
|
||||||
|
<p className="text-xs text-slate-600">{details.email} | {details.phone}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="p-2 bg-amber-50 rounded-lg">
|
||||||
|
<MapPin className="w-4 h-4 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold">Proposed Location</p>
|
||||||
|
<p className="text-sm font-semibold text-slate-900">{details.city}, {details.state}</p>
|
||||||
|
<p className="text-xs text-slate-600">{details.preferredLocation || 'Standard Area'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex items-start gap-3">
|
||||||
<p className="text-sm font-medium text-slate-700">Application Progress</p>
|
<div className="p-2 bg-green-50 rounded-lg">
|
||||||
<p className="text-sm font-medium text-amber-600">{details.progressPercentage || 0}%</p>
|
<Building className="w-4 h-4 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold">Business Concept</p>
|
||||||
|
<p className="text-sm font-semibold text-slate-900">{details.businessType} - {details.constitutionType}</p>
|
||||||
|
<p className="text-xs text-slate-600">Investment: {details.investmentCapacity}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="p-2 bg-slate-50 rounded-lg">
|
||||||
|
<Clock className="w-4 h-4 text-slate-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold">Applied On</p>
|
||||||
|
<p className="text-sm font-semibold text-slate-900">
|
||||||
|
{details.createdAt ? new Date(details.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' }) : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-slate-100 rounded-full h-2.5">
|
|
||||||
<div className="bg-amber-500 h-2.5 rounded-full transition-all" style={{ width: `${details.progressPercentage || 0}%` }}></div>
|
<div className="bg-slate-50 rounded-xl p-4 flex flex-col justify-center border border-slate-100">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<p className="text-xs font-bold text-slate-700 uppercase tracking-tight">Onboarding Progress</p>
|
||||||
|
<p className="text-xs font-black text-amber-600">{details.progressPercentage || 0}%</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-200 rounded-full h-2 shadow-inner">
|
||||||
|
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000 ease-out shadow-sm" style={{ width: `${details.progressPercentage || 0}%` }}></div>
|
||||||
|
</div>
|
||||||
|
{details.statusHistory?.[0]?.changeReason && (
|
||||||
|
<p className="mt-3 text-[11px] text-slate-600 italic leading-relaxed border-l-2 border-amber-300 pl-2">
|
||||||
|
"{details.statusHistory[0].changeReason}"
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
|
{/* Statutory & Bank Details Form */}
|
||||||
<div className="p-6 border-b border-slate-200">
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
<h4 className="flex items-center gap-2 text-lg font-semibold text-slate-900">
|
<div className="p-4 bg-slate-900 text-white flex justify-between items-center">
|
||||||
<Upload className="w-5 h-5 text-blue-600" /> Document Upload
|
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest">
|
||||||
|
<CreditCard className="w-4 h-4 text-amber-400" /> Statutory & Bank Details
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveDetails}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="text-xs bg-amber-600 hover:bg-amber-700 text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />}
|
||||||
|
Save Business Info
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Business Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
||||||
|
value={form.accountHolderName}
|
||||||
|
onChange={(e) => setForm({ ...form, accountHolderName: e.target.value })}
|
||||||
|
placeholder="As per legal documents"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Permanent Account Number (PAN)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
|
||||||
|
value={form.panNumber}
|
||||||
|
onChange={(e) => setForm({ ...form, panNumber: e.target.value.toUpperCase() })}
|
||||||
|
placeholder="ABCDE1234F"
|
||||||
|
maxLength={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">GST Identification Number (GSTIN)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
|
||||||
|
value={form.gstNumber}
|
||||||
|
onChange={(e) => setForm({ ...form, gstNumber: e.target.value.toUpperCase() })}
|
||||||
|
placeholder="27ABCDE1234F1Z5"
|
||||||
|
maxLength={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Office Address</label>
|
||||||
|
<textarea
|
||||||
|
rows={1}
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
||||||
|
value={form.registeredAddress}
|
||||||
|
onChange={(e) => setForm({ ...form, registeredAddress: e.target.value })}
|
||||||
|
placeholder="Full legal address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2 border-t pt-2 mt-2">
|
||||||
|
<h5 className="text-xs font-black text-slate-900 uppercase flex items-center gap-2 mb-3">
|
||||||
|
<Landmark className="w-3.5 h-3.5 text-blue-600" /> Primary Bank Information
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Bank Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
||||||
|
value={form.bankName}
|
||||||
|
onChange={(e) => setForm({ ...form, bankName: e.target.value })}
|
||||||
|
placeholder="e.g. HDFC Bank"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Account Number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
||||||
|
value={form.accountNumber}
|
||||||
|
onChange={(e) => setForm({ ...form, accountNumber: e.target.value })}
|
||||||
|
placeholder="Bank account number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">IFSC Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
|
||||||
|
value={form.ifscCode}
|
||||||
|
onChange={(e) => setForm({ ...form, ifscCode: e.target.value.toUpperCase() })}
|
||||||
|
placeholder="HDFC0001234"
|
||||||
|
maxLength={11}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Branch Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
|
||||||
|
value={form.branchName}
|
||||||
|
onChange={(e) => setForm({ ...form, branchName: e.target.value })}
|
||||||
|
placeholder="e.g. South Mumbai"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Document Upload Card */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
|
<div className="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
|
||||||
|
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-slate-900">
|
||||||
|
<Upload className="w-4 h-4 text-blue-600" /> Required Documents
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg border border-slate-100">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1">
|
||||||
<label className="text-sm font-medium text-slate-900">Document Type</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
|
||||||
<select
|
<select
|
||||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm"
|
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
value={selectedDocType}
|
value={selectedDocType}
|
||||||
onChange={(e) => setSelectedDocType(e.target.value)}
|
onChange={(e) => setSelectedDocType(e.target.value)}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
@ -213,46 +414,54 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
<option value="Other">Other</option>
|
<option value="Other">Other</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-1">
|
||||||
<label className="text-sm font-medium text-slate-900">File</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase">Select File</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
className="w-full text-sm"
|
className="w-full text-xs text-slate-600 file:mr-4 file:py-1 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2 flex justify-end mt-2">
|
<div className="md:col-span-2 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!file || !selectedDocType || isUploading}
|
disabled={!file || !selectedDocType || isUploading}
|
||||||
className="bg-amber-600 text-white px-4 py-2 rounded-md hover:bg-amber-700 disabled:opacity-50 flex items-center gap-2"
|
className="bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 text-xs font-bold transition-all shadow-sm flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{isUploading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
|
{isUploading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
|
||||||
Upload
|
Upload Document
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<h3 className="font-medium text-slate-900">Uploaded Documents ({documents.length})</h3>
|
<div className="flex items-center gap-2 text-slate-900 border-b pb-1">
|
||||||
<div className="space-y-2">
|
<File className="w-4 h-4 text-blue-600" />
|
||||||
|
<h3 className="text-xs font-black uppercase tracking-tighter">Uploaded Library ({documents.length})</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{documents.length > 0 ? documents.map((doc) => (
|
{documents.length > 0 ? documents.map((doc) => (
|
||||||
<div key={doc.id} className="flex justify-between items-center p-3 border border-slate-200 rounded-lg bg-white">
|
<div key={doc.id} className="flex justify-between items-center p-3 border border-slate-100 rounded-xl bg-slate-50 group hover:border-amber-200 transition-all">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<File className="w-5 h-5 text-blue-600" />
|
<div className="w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200 group-hover:bg-blue-50">
|
||||||
|
<File className="w-4 h-4 text-slate-400 group-hover:text-blue-600" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{doc.documentType}</p>
|
<p className="text-[11px] font-bold text-slate-900">{doc.documentType}</p>
|
||||||
<p className="text-xs text-slate-500">{doc.fileName}</p>
|
<p className="text-[10px] text-slate-400 truncate w-32">{doc.fileName}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold uppercase ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
|
<span className={`text-[9px] px-2 py-0.5 rounded-full font-black uppercase tracking-tighter ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
|
||||||
{doc.status || 'Pending'}
|
{doc.status || 'Pending'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)) : (
|
)) : (
|
||||||
<p className="text-sm text-slate-500 italic text-center py-4 bg-slate-50 rounded-lg">No documents uploaded yet.</p>
|
<div className="col-span-2 py-8 text-center bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
||||||
|
<Info className="w-6 h-6 text-slate-300 mx-auto mb-2" />
|
||||||
|
<p className="text-xs text-slate-500 font-medium">No documents in your library yet</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -260,31 +469,36 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1 space-y-6">
|
||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
<div className="p-4 bg-slate-50 border-b border-slate-200">
|
<div className="p-4 bg-slate-50 border-b border-slate-200">
|
||||||
<h3 className="text-sm font-bold text-slate-900 flex items-center gap-2 uppercase tracking-wide">
|
<h3 className="text-xs font-bold text-slate-900 flex items-center gap-2 uppercase tracking-widest">
|
||||||
<Clock className="w-4 h-4 text-amber-600" /> Timeline
|
<Clock className="w-4 h-4 text-amber-600" /> Recent Updates
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{details.statusHistory?.length > 0 ? (
|
{details.statusHistory?.length > 0 ? (
|
||||||
<div className="relative space-y-6">
|
<div className="relative space-y-6">
|
||||||
<div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-200"></div>
|
<div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-100"></div>
|
||||||
{[...details.statusHistory].reverse().map((item: any) => (
|
{[...details.statusHistory].reverse().map((item: any, idx: number) => (
|
||||||
<div key={item.id} className="relative pl-8">
|
<div key={item.id} className="relative pl-8 animate-in slide-in-from-left duration-300" style={{ animationDelay: `${idx * 100}ms` }}>
|
||||||
<div className="absolute left-0 top-1 w-[24px] h-[24px] rounded-full border-2 bg-white flex items-center justify-center border-green-500">
|
<div className="absolute left-0 top-1 w-[24px] h-[24px] rounded-full border-2 bg-white flex items-center justify-center border-amber-500 shadow-sm">
|
||||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-slate-900">{item.newStatus}</p>
|
<p className="text-xs font-bold text-slate-900 uppercase tracking-tight">{item.newStatus}</p>
|
||||||
<p className="text-[11px] text-slate-500">{new Date(item.createdAt).toLocaleString()}</p>
|
<p className="text-[10px] text-slate-400 font-medium">{new Date(item.createdAt).toLocaleString('en-IN')}</p>
|
||||||
|
{item.changeReason && (
|
||||||
|
<p className="text-[10px] text-slate-500 mt-1 italic leading-tight">"{item.changeReason}"</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-slate-500 italic text-center">No history available</p>
|
<div className="text-center py-6">
|
||||||
|
<p className="text-xs text-slate-500 italic">No history available yet</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,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 { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface RelocationRequestPageProps {
|
interface RelocationRequestPageProps {
|
||||||
currentUser: User | null;
|
currentUser: User | null;
|
||||||
@ -205,7 +206,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
</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>
|
||||||
@ -396,7 +397,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
</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>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, Upload, Eye } from 'lucide-react';
|
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, Upload, Eye, AlertCircle, Loader2 } 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';
|
||||||
@ -13,7 +13,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
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 { resignationService } from '../../services/resignation.service';
|
import { resignationService } from '../../services/resignation.service';
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
import { formatDateTime } from '../ui/utils';
|
import { formatDateTime } from '../ui/utils';
|
||||||
@ -30,11 +30,22 @@ interface ResignationDetailsProps {
|
|||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STAGE_TO_ROLE_MAP: Record<string, string> = {
|
||||||
|
'ASM': 'ASM',
|
||||||
|
'RBM': 'RBM',
|
||||||
|
'ZBH': 'ZBH',
|
||||||
|
'DD Lead': 'DD Lead',
|
||||||
|
'NBH': 'NBH',
|
||||||
|
'DD Admin': 'DD Admin',
|
||||||
|
'Legal': 'Legal Admin'
|
||||||
|
};
|
||||||
|
|
||||||
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
|
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
|
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
|
||||||
const [remarks, setRemarks] = useState('');
|
const [remarks, setRemarks] = useState('');
|
||||||
const [assignToUser, setAssignToUser] = useState('');
|
const [assignToUser, setAssignToUser] = useState('');
|
||||||
|
const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
|
||||||
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('');
|
const [selectedDept, setSelectedDept] = useState('');
|
||||||
@ -68,6 +79,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
// 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);
|
||||||
|
|
||||||
|
// Check if user is assigned to the current stage
|
||||||
|
const isCurrentlyAssigned = currentUser && (
|
||||||
|
currentUser.role === 'Super Admin' ||
|
||||||
|
currentUser.role === STAGE_TO_ROLE_MAP[resignationData?.currentStage]
|
||||||
|
);
|
||||||
|
|
||||||
// Progress stages logic based on live data
|
// Progress stages logic based on live data
|
||||||
const progressStages = [
|
const progressStages = [
|
||||||
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
||||||
@ -81,15 +98,49 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
|
||||||
|
|
||||||
|
const getResignationPermissions = () => {
|
||||||
|
if (!resignationData || !currentUser) {
|
||||||
|
return { canApprove: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStage = resignationData.currentStage;
|
||||||
|
const status = resignationData.status;
|
||||||
|
const userRole = currentUser.role;
|
||||||
|
|
||||||
|
// Final states where no more actions are possible
|
||||||
|
const isFinalState = ['Completed', 'Rejected', 'Withdrawn'].includes(status);
|
||||||
|
|
||||||
|
// Check if it's already in the settlement phase
|
||||||
|
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
|
||||||
|
|
||||||
|
const stageIndex = stagesOrdered.indexOf(currentStage);
|
||||||
|
const nbhIndex = stagesOrdered.indexOf('NBH');
|
||||||
|
const isPastNBH = stageIndex !== -1 && nbhIndex !== -1 && stageIndex >= nbhIndex;
|
||||||
|
|
||||||
|
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === STAGE_TO_ROLE_MAP[currentStage];
|
||||||
|
|
||||||
|
return {
|
||||||
|
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase,
|
||||||
|
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
|
||||||
|
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
|
||||||
|
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
|
||||||
|
!isSettlementPhase && !isFinalState,
|
||||||
|
canAssign: userRole !== 'Dealer' && !isFinalState
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissions = getResignationPermissions();
|
||||||
|
|
||||||
const getStageStatus = (stageKey: string) => {
|
const getStageStatus = (stageKey: string) => {
|
||||||
if (!resignationData) return 'pending';
|
if (!resignationData) return 'pending';
|
||||||
const currentStage = resignationData.currentStage;
|
const currentStage = resignationData.currentStage;
|
||||||
|
|
||||||
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
|
|
||||||
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 (currentIndex === -1) return 'pending';
|
||||||
if (stageIndex < currentIndex) return 'completed';
|
if (stageIndex < currentIndex) return 'completed';
|
||||||
if (stageIndex === currentIndex) return 'active';
|
if (stageIndex === currentIndex) return 'active';
|
||||||
return 'pending';
|
return 'pending';
|
||||||
@ -119,7 +170,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
const payload = {
|
const payload = {
|
||||||
action: actionDialog.type,
|
action: actionDialog.type,
|
||||||
remarks,
|
remarks,
|
||||||
assignTo: assignToUser
|
assignTo: assignToUser,
|
||||||
|
force: forceTriggerFnF
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: any = await API.updateResignationStatus(resignationId, payload);
|
const response: any = await API.updateResignationStatus(resignationId, payload);
|
||||||
@ -154,11 +206,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
formData.append('file', clearanceFile);
|
formData.append('file', clearanceFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
await resignationService.updateClearance(resignationId, formData);
|
const response: any = await resignationService.updateClearance(resignationId, formData);
|
||||||
toast.success(`Successfully updated clearance for ${selectedDept}`);
|
|
||||||
setShowClearanceDialog(false);
|
if (response?.success) {
|
||||||
setClearanceFile(null);
|
toast.success(`Successfully updated clearance for ${selectedDept}`);
|
||||||
fetchResignation();
|
setShowClearanceDialog(false);
|
||||||
|
setClearanceFile(null);
|
||||||
|
fetchResignation();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to update clearance status');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update clearance status');
|
toast.error('Failed to update clearance status');
|
||||||
} finally {
|
} finally {
|
||||||
@ -169,7 +226,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
if (isLoading && !resignationData) {
|
if (isLoading && !resignationData) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
<Loader2 className="w-8 h-8 animate-spin text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -200,56 +257,58 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
|
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
|
||||||
{currentUser?.role !== 'Dealer' && (
|
{permissions.canApprove && (
|
||||||
<>
|
<Button
|
||||||
<Button
|
size="sm"
|
||||||
size="sm"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
||||||
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
onClick={() => handleAction('approve')}
|
||||||
onClick={() => handleAction('approve')}
|
>
|
||||||
>
|
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
|
||||||
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
|
Approve
|
||||||
Approve
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
<Button
|
{permissions.canSendBack && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
disabled={isSubmitting}
|
variant="outline"
|
||||||
className="hover:bg-slate-50 transition-all"
|
disabled={isSubmitting}
|
||||||
onClick={() => handleAction('sendback')}
|
className="hover:bg-slate-50 transition-all font-bold"
|
||||||
>
|
onClick={() => handleAction('sendback')}
|
||||||
{isSubmitting && actionDialog.type === 'sendback' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
|
>
|
||||||
Send Back
|
{isSubmitting && actionDialog.type === 'sendback' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
|
||||||
</Button>
|
Send Back
|
||||||
</>
|
</Button>
|
||||||
|
)}
|
||||||
|
{permissions.canWithdraw && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="text-red-600 border-red-300 hover:bg-red-50 transition-all font-bold"
|
||||||
|
onClick={() => handleAction('withdrawal')}
|
||||||
|
>
|
||||||
|
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
|
||||||
|
Withdrawal
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
|
|
||||||
onClick={() => handleAction('withdrawal')}
|
|
||||||
>
|
|
||||||
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
|
|
||||||
Withdrawal
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Secondary Actions */}
|
{/* Secondary Actions */}
|
||||||
{currentUser?.role !== 'Dealer' && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
{permissions.canPushToFnF && (
|
||||||
{canPushToFnF && resignationData?.status !== 'FNF_INITIATED' && resignationData?.status !== 'Settled' && (
|
<Button
|
||||||
<Button
|
size="sm"
|
||||||
size="sm"
|
variant="outline"
|
||||||
variant="outline"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
className="text-amber-600 border-blue-300 hover:bg-blue-50 transition-all"
|
||||||
className="text-blue-600 border-blue-300 hover:bg-blue-50 transition-all"
|
onClick={() => handleAction('pushfnf')}
|
||||||
onClick={() => handleAction('pushfnf')}
|
>
|
||||||
>
|
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
|
||||||
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
|
Push to F&F
|
||||||
Push to F&F
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
{permissions.canAssign && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -260,8 +319,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
|
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
|
||||||
Assign User
|
Assign User
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Work Notes Button - Independent Section */}
|
{/* Work Notes Button - Independent Section */}
|
||||||
@ -353,7 +412,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<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>{resignationData?.dealer?.dealerProfile?.onboardedAt ? formatDateTime(resignationData.dealer.dealerProfile.onboardedAt, 'date') : (resignationData?.outlet?.inaugurationDate ? new Date(resignationData.outlet.inaugurationDate).toLocaleDateString() : 'N/A')}</p>
|
<p>{resignationData?.dealer?.dealerProfile?.onboardedAt ? formatDateTime(resignationData.dealer.dealerProfile.onboardedAt, 'date') : (resignationData?.outlet?.inaugurationDate ? formatDateTime(resignationData.outlet.inaugurationDate, 'date') : 'N/A')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">LOA Date</Label>
|
<Label className="text-slate-600">LOA Date</Label>
|
||||||
@ -439,7 +498,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||||
status === 'completed' ? 'bg-green-100 text-green-600' :
|
status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||||
status === 'active' ? 'bg-blue-100 text-blue-600' :
|
status === 'active' ? 'bg-blue-100 text-amber-600' :
|
||||||
'bg-slate-100 text-slate-400'
|
'bg-slate-100 text-slate-400'
|
||||||
}`}>
|
}`}>
|
||||||
{status === 'completed' ? (
|
{status === 'completed' ? (
|
||||||
@ -458,7 +517,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h3 className={
|
<h3 className={
|
||||||
status === 'completed' ? 'text-green-600' :
|
status === 'completed' ? 'text-green-600' :
|
||||||
status === 'active' ? 'text-blue-600' :
|
status === 'active' ? 'text-amber-600' :
|
||||||
'text-slate-400'
|
'text-slate-400'
|
||||||
}>{stage.name}</h3>
|
}>{stage.name}</h3>
|
||||||
{timelineEntry && (
|
{timelineEntry && (
|
||||||
@ -477,7 +536,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-2 text-blue-600"
|
className="mt-2 text-amber-600"
|
||||||
onClick={() => handleViewStageDocuments(stage.name)}
|
onClick={() => handleViewStageDocuments(stage.name)}
|
||||||
>
|
>
|
||||||
View Stage Documents
|
View Stage Documents
|
||||||
@ -545,7 +604,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<div className="pt-2 border-t border-slate-100 flex items-center justify-between">
|
<div className="pt-2 border-t border-slate-100 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<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" />
|
<FileText className="w-4 h-4 text-amber-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-tight leading-none mb-1">Evidence Attached</p>
|
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-tight leading-none mb-1">Evidence Attached</p>
|
||||||
@ -566,7 +625,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
documentType: 'Clearance Proof'
|
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"
|
className="flex items-center gap-1.5 text-xs text-amber-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" />
|
<Eye className="w-3.5 h-3.5" />
|
||||||
Preview
|
Preview
|
||||||
@ -579,7 +638,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<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-amber-600 hover:text-blue-700 p-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDept(dept);
|
setSelectedDept(dept);
|
||||||
setClearanceStatus(displayStatus || 'Pending');
|
setClearanceStatus(displayStatus || 'Pending');
|
||||||
@ -627,7 +686,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
...(resignationData?.uploadedDocuments || [])
|
...(resignationData?.uploadedDocuments || [])
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add clearance documents
|
// Add clearance documents from legacy JSON field
|
||||||
if (resignationData?.departmentalClearances) {
|
if (resignationData?.departmentalClearances) {
|
||||||
Object.entries(resignationData.departmentalClearances).forEach(([dept, data]: [string, any]) => {
|
Object.entries(resignationData.departmentalClearances).forEach(([dept, data]: [string, any]) => {
|
||||||
if (data.supportingDocument) {
|
if (data.supportingDocument) {
|
||||||
@ -642,6 +701,21 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add live clearance documents from F&F model
|
||||||
|
if (resignationData?.settlement?.clearances) {
|
||||||
|
resignationData.settlement.clearances.forEach((c: any) => {
|
||||||
|
if (c.supportingDocument) {
|
||||||
|
allDocs.push({
|
||||||
|
name: `${c.department} Clearance NOC`,
|
||||||
|
type: 'Live NOC',
|
||||||
|
path: c.supportingDocument,
|
||||||
|
createdAt: c.clearedAt || c.updatedAt,
|
||||||
|
uploadedBy: 'Department Admin'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (allDocs.length === 0) return (
|
if (allDocs.length === 0) return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
|
<TableCell colSpan={5} className="text-center py-4 text-slate-500">
|
||||||
@ -703,7 +777,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
{(resignationData?.timeline || []).length > 0 ? (
|
{(resignationData?.timeline || []).length > 0 ? (
|
||||||
(resignationData.timeline || []).map((log: any, index: number) => (
|
(resignationData.timeline || []).map((log: any, index: number) => (
|
||||||
<div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
|
<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-blue-600 mt-2" />
|
<div className="w-2 h-2 rounded-full bg-amber-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 className="font-medium text-slate-900">{log.action || log.status}</p>
|
<p className="font-medium text-slate-900">{log.action || log.status}</p>
|
||||||
@ -764,14 +838,35 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
) : actionDialog.type === 'pushfnf' ? (
|
) : actionDialog.type === 'pushfnf' ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label>Remarks (Optional)</Label>
|
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start gap-3">
|
||||||
<Textarea
|
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||||
value={remarks}
|
<div className="text-sm text-amber-800">
|
||||||
onChange={(e) => setRemarks(e.target.value)}
|
<p className="font-bold">Manual Trigger Notice</p>
|
||||||
placeholder="Add any additional notes..."
|
<p>Normally F&F is triggered after LWD. Use manual trigger only if urgent clearance is required.</p>
|
||||||
rows={3}
|
</div>
|
||||||
/>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="forceFnF"
|
||||||
|
checked={forceTriggerFnF}
|
||||||
|
onChange={(e) => setForceTriggerFnF(e.target.checked)}
|
||||||
|
className="w-4 h-4 rounded border-slate-300"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="forceFnF" className="font-medium text-slate-900 cursor-pointer">
|
||||||
|
Force Initiate F&F Settlement Immediately
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Remarks (Optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={remarks}
|
||||||
|
onChange={(e) => setRemarks(e.target.value)}
|
||||||
|
placeholder="Add any additional notes..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -823,7 +918,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<DialogContent className="max-w-3xl">
|
<DialogContent className="max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<FileText className="w-5 h-5 text-blue-600" />
|
<FileText className="w-5 h-5 text-amber-600" />
|
||||||
Documents - {stageDocumentsDialog.stageName}
|
Documents - {stageDocumentsDialog.stageName}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@ -853,7 +948,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
<TableCell>{doc.uploadDate}</TableCell>
|
<TableCell>{doc.uploadDate}</TableCell>
|
||||||
<TableCell>{doc.uploader}</TableCell>
|
<TableCell>{doc.uploader}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button size="sm" variant="outline" className="text-blue-600 hover:text-blue-700">
|
<Button size="sm" variant="outline" className="text-amber-600 hover:text-blue-700">
|
||||||
<FileText className="w-4 h-4 mr-1" />
|
<FileText className="w-4 h-4 mr-1" />
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
@ -948,7 +1043,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center justify-center gap-2">
|
<div className="flex flex-col items-center justify-center gap-2">
|
||||||
{clearanceFile ? (
|
{clearanceFile ? (
|
||||||
<div className="flex items-center gap-2 text-blue-600 font-medium">
|
<div className="flex items-center gap-2 text-amber-600 font-medium">
|
||||||
<FileText className="w-5 h-5" />
|
<FileText className="w-5 h-5" />
|
||||||
<span className="truncate max-w-[200px]">{clearanceFile.name}</span>
|
<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); }} />
|
<X className="w-4 h-4 cursor-pointer text-slate-400 hover:text-red-500" onClick={(e) => { e.stopPropagation(); setClearanceFile(null); }} />
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { User as UserType } from '../../lib/mock-data';
|
import { User as UserType } from '../../lib/mock-data';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface ResignationPageProps {
|
interface ResignationPageProps {
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
@ -163,15 +164,15 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
<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">Dealer Name</p>
|
<p className="text-slate-600">Dealer Name</p>
|
||||||
<p>{request.outlet?.name || 'N/A'}</p>
|
<p>{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Dealer Code</p>
|
<p className="text-slate-600">Dealer Code</p>
|
||||||
<p>{request.outlet?.code || 'N/A'}</p>
|
<p>{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Location</p>
|
<p className="text-slate-600">Location</p>
|
||||||
<p>{request.outlet?.city}, {request.outlet?.state}</p>
|
<p>{request.dealer?.dealerProfile?.registeredAddress || (request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : 'N/A')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Type</p>
|
<p className="text-slate-600">Type</p>
|
||||||
@ -189,7 +190,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
<p className="text-slate-600">Submitted On</p>
|
<p className="text-slate-600">Submitted On</p>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Calendar className="w-4 h-4 text-slate-500" />
|
<Calendar className="w-4 h-4 text-slate-500" />
|
||||||
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
|
<p>{formatDateTime(request.submittedOn)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -238,11 +239,11 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
<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">Dealer Name</p>
|
<p className="text-slate-600">Dealer Name</p>
|
||||||
<p>{request.outlet?.name || 'N/A'}</p>
|
<p>{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Location</p>
|
<p className="text-slate-600">Location</p>
|
||||||
<p>{request.outlet?.city}, {request.outlet?.state}</p>
|
<p>{request.dealer?.dealerProfile?.registeredAddress || (request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : 'N/A')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Current Stage</p>
|
<p className="text-slate-600">Current Stage</p>
|
||||||
@ -250,7 +251,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Submitted On</p>
|
<p className="text-slate-600">Submitted On</p>
|
||||||
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
|
<p>{formatDateTime(request.submittedOn)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -299,11 +300,11 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
<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">Dealer Name</p>
|
<p className="text-slate-600">Dealer Name</p>
|
||||||
<p>{request.outlet?.name}</p>
|
<p>{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Location</p>
|
<p className="text-slate-600">Location</p>
|
||||||
<p>{request.outlet?.city}</p>
|
<p>{request.dealer?.dealerProfile?.registeredAddress || request.outlet?.city || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Final Stage</p>
|
<p className="text-slate-600">Final Stage</p>
|
||||||
@ -311,7 +312,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Submitted On</p>
|
<p className="text-slate-600">Submitted On</p>
|
||||||
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
|
<p>{formatDateTime(request.submittedOn)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -109,6 +109,45 @@ 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);
|
||||||
|
|
||||||
|
// Centralized Permissions Utility for Termination logic (Robust Validation)
|
||||||
|
const getTerminationPermissions = () => {
|
||||||
|
if (!terminationData || !currentUser) {
|
||||||
|
return { canApprove: false, canWithdraw: false, canIssueSCN: false, canUploadSCNResponse: false, canFinalize: false, canPushToFnF: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStage = terminationData.currentStage;
|
||||||
|
const status = terminationData.status;
|
||||||
|
const userRole = currentUser.role;
|
||||||
|
|
||||||
|
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
|
||||||
|
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
|
||||||
|
|
||||||
|
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || (
|
||||||
|
(currentStage === 'RBM Review' && userRole === 'RBM') ||
|
||||||
|
(currentStage === 'ZBH Review' && userRole === 'ZBH') ||
|
||||||
|
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') ||
|
||||||
|
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') ||
|
||||||
|
(currentStage === 'NBH Evaluation' && userRole === 'NBH') ||
|
||||||
|
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
|
||||||
|
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
|
||||||
|
(currentStage === 'CEO Final Approval' && userRole === 'CEO') ||
|
||||||
|
(currentStage === 'Legal - Termination Letter' && userRole === 'Legal Admin')
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && !['Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage),
|
||||||
|
canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState,
|
||||||
|
canUploadSCNResponse: currentStage === 'Show Cause Notice' && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState,
|
||||||
|
canFinalize: ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && (userRole === currentStage.replace(' Approval', '') || userRole === 'Super Admin') && !isFinalState,
|
||||||
|
canPushToFnF: canPushToFnF && !isSettlementPhase && !isFinalState,
|
||||||
|
canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState,
|
||||||
|
isFinalState,
|
||||||
|
isSettlementPhase
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissions = getTerminationPermissions();
|
||||||
|
|
||||||
// Use actual data from backend
|
// Use actual data from backend
|
||||||
const request = terminationData || {};
|
const request = terminationData || {};
|
||||||
|
|
||||||
@ -347,15 +386,19 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
|
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
|
||||||
{currentUser?.role !== 'Dealer' && (
|
{currentUser?.role !== 'Dealer' && (
|
||||||
<>
|
<>
|
||||||
<Button
|
{!permissions.canFinalize && (
|
||||||
size="sm"
|
<>
|
||||||
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
{permissions.canApprove && (
|
||||||
onClick={() => handleAction('approve')}
|
<Button
|
||||||
>
|
size="sm"
|
||||||
<Check className="w-4 h-4 mr-2" />
|
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
|
||||||
Approve
|
onClick={() => handleAction('approve')}
|
||||||
</Button>
|
>
|
||||||
{request.currentStage === 'NBH' && (currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && (
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{permissions.canIssueSCN && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-purple-600 hover:bg-purple-700 transition-all shadow-sm"
|
className="bg-purple-600 hover:bg-purple-700 transition-all shadow-sm"
|
||||||
@ -365,7 +408,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
Issue SCN
|
Issue SCN
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{request.currentStage === 'SCN' && (['Legal', 'DD Admin', 'Super Admin'].includes(currentUser?.role || '')) && (
|
{permissions.canUploadSCNResponse && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm"
|
className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm"
|
||||||
@ -378,32 +421,36 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
Upload SCN Response
|
Upload SCN Response
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{(['NBH Final', 'CCO', 'CEO'].includes(request.currentStage)) && (currentUser?.role === request.currentStage || currentUser?.role === 'Super Admin') && (
|
{permissions.canApprove && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
|
variant="outline"
|
||||||
onClick={() => setShowFinalizeDialog(true)}
|
className="hover:bg-slate-50 transition-all"
|
||||||
|
onClick={() => handleAction('sendback')}
|
||||||
>
|
>
|
||||||
<ShieldCheck className="w-4 h-4 mr-2" />
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
Final Authorization
|
Send Back
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
</>
|
||||||
size="sm"
|
)}
|
||||||
variant="outline"
|
{permissions.canFinalize && (
|
||||||
className="hover:bg-slate-50 transition-all"
|
<Button
|
||||||
onClick={() => handleAction('sendback')}
|
size="sm"
|
||||||
>
|
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
onClick={() => setShowFinalizeDialog(true)}
|
||||||
Send Back
|
>
|
||||||
</Button>
|
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||||
|
Final Authorization
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Secondary Actions */}
|
{/* Secondary Actions */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{canPushToFnF && (
|
{permissions.canPushToFnF && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -414,15 +461,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
Push to F&F
|
Push to F&F
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{!permissions.isFinalState && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
className="hover:bg-slate-50 transition-all"
|
variant="outline"
|
||||||
onClick={() => handleAction('assign')}
|
className="hover:bg-slate-50 transition-all"
|
||||||
>
|
onClick={() => handleAction('assign')}
|
||||||
<UserPlus className="w-4 h-4 mr-2" />
|
>
|
||||||
Assign User
|
<UserPlus className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
Assign User
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -476,7 +525,7 @@ 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.dealer?.dealerCode?.code || 'N/A'}</p>
|
<p>{request.dealer?.dealerCode?.dealerCode || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-600">Dealer Name</Label>
|
<Label className="text-slate-600">Dealer Name</Label>
|
||||||
@ -511,8 +560,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
<p>{request.dealer?.dealerCode?.serviceCode || request.serviceCode || 'N/A'}</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">GMA Code</Label>
|
||||||
<p>{request.dealer?.dealerCode?.accessoriesCode || request.accessoriesCode || 'N/A'}</p>
|
<p>{request.dealer?.dealerCode?.gmaCode || 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>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ 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';
|
import { settlementService } from '../../services/settlement.service';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface FinanceDashboardProps {
|
interface FinanceDashboardProps {
|
||||||
currentUser: User | null;
|
currentUser: User | null;
|
||||||
@ -325,7 +326,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Created On</p>
|
<p className="text-slate-600">Created On</p>
|
||||||
<p>{new Date(app.createdAt).toLocaleDateString()}</p>
|
<p>{formatDateTime(app.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -402,7 +403,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600">Verified On</p>
|
<p className="text-slate-600">Verified On</p>
|
||||||
<p>{app.verificationDate ? new Date(app.verificationDate).toLocaleDateString() : 'N/A'}</p>
|
<p>{app.verificationDate ? formatDateTime(app.verificationDate) : 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { RootState } from '../../store';
|
|||||||
import { logout } from '../../store/slices/authSlice';
|
import { logout } from '../../store/slices/authSlice';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { API } from '../../api/API';
|
import { API } from '../../api/API';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { ProspectiveApplicationDetails } from '../applications/ProspectiveApplicationDetails';
|
import { ProspectiveApplicationDetails } from '../applications/ProspectiveApplicationDetails';
|
||||||
|
|
||||||
@ -206,7 +207,7 @@ function ProspectiveApplicationList() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-xs text-slate-500 font-medium">Applied</span>
|
<span className="text-xs text-slate-500 font-medium">Applied</span>
|
||||||
<span className="text-xs font-bold text-slate-600">{new Date(app.createdAt).toLocaleDateString()}</span>
|
<span className="text-xs font-bold text-slate-600">{formatDateTime(app.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
|||||||
@ -12,6 +12,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 { dealerService } from '../../services/dealer.service';
|
import { dealerService } from '../../services/dealer.service';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface DealerConstitutionalChangePageProps {
|
interface DealerConstitutionalChangePageProps {
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
@ -358,7 +359,7 @@ export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: D
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-600">
|
<TableCell className="text-slate-600">
|
||||||
{new Date(request.createdAt).toLocaleDateString()}
|
{formatDateTime(request.createdAt)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className={`border ${getStatusColor(request.status)}`}>
|
<Badge className={`border ${getStatusColor(request.status)}`}>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { User as UserType } from '../../lib/mock-data';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { dealerService } from '../../services/dealer.service';
|
import { dealerService } from '../../services/dealer.service';
|
||||||
import { masterService } from '../../services/master.service';
|
import { masterService } from '../../services/master.service';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface DealerRelocationPageProps {
|
interface DealerRelocationPageProps {
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
@ -428,7 +429,7 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-600">
|
<TableCell className="text-slate-600">
|
||||||
{new Date(request.createdAt).toLocaleDateString()}
|
{formatDateTime(request.createdAt)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className={`border ${getStatusColor(request.status)}`}>
|
<Badge className={`border ${getStatusColor(request.status)}`}>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { User as UserType } from '../../lib/mock-data';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { dealerService } from '../../services/dealer.service';
|
import { dealerService } from '../../services/dealer.service';
|
||||||
import { resignationService } from '../../services/resignation.service';
|
import { resignationService } from '../../services/resignation.service';
|
||||||
|
import { formatDateTime } from '../ui/utils';
|
||||||
|
|
||||||
interface DealerResignationPageProps {
|
interface DealerResignationPageProps {
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
@ -435,7 +436,7 @@ export function DealerResignationPage({ currentUser, onViewDetails }: DealerResi
|
|||||||
<TableCell className="font-medium text-slate-900">{request.resignationId}</TableCell>
|
<TableCell className="font-medium text-slate-900">{request.resignationId}</TableCell>
|
||||||
<TableCell>{request.outlet?.name}</TableCell>
|
<TableCell>{request.outlet?.name}</TableCell>
|
||||||
<TableCell>{request.resignationType}</TableCell>
|
<TableCell>{request.resignationType}</TableCell>
|
||||||
<TableCell>{new Date(request.submittedOn).toLocaleDateString()}</TableCell>
|
<TableCell>{formatDateTime(request.submittedOn)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className={`border ${getStatusColor(request.status)}`}>
|
<Badge className={`border ${getStatusColor(request.status)}`}>
|
||||||
{request.status}
|
{request.status}
|
||||||
|
|||||||
42
src/lib/dateUtils.ts
Normal file
42
src/lib/dateUtils.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Formats a date string into a localized string with minute accuracy.
|
||||||
|
* Format: "DD MMM YYYY, HH:MM AM/PM" (e.g., "11 Apr 2026, 11:45 PM")
|
||||||
|
*/
|
||||||
|
export const formatDateTime = (dateString: any): string => {
|
||||||
|
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 '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a date string into a localized date string (Date only).
|
||||||
|
* Format: "DD MMM YYYY"
|
||||||
|
*/
|
||||||
|
export const formatDateOnly = (dateString: any): string => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return '-';
|
||||||
|
|
||||||
|
return date.toLocaleDateString('en-IN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -138,6 +138,14 @@ export interface Application {
|
|||||||
districtId?: string;
|
districtId?: string;
|
||||||
fddAssignments?: any[];
|
fddAssignments?: any[];
|
||||||
statutoryStatus?: string;
|
statutoryStatus?: string;
|
||||||
|
panNumber?: string;
|
||||||
|
gstNumber?: string;
|
||||||
|
bankName?: string;
|
||||||
|
accountNumber?: string;
|
||||||
|
ifscCode?: string;
|
||||||
|
branchName?: string;
|
||||||
|
accountHolderName?: string;
|
||||||
|
registeredAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Participant {
|
export interface Participant {
|
||||||
|
|||||||
@ -30,5 +30,10 @@ export const settlementService = {
|
|||||||
const response: any = await API.addLineItem(fnfId, data);
|
const response: any = await API.addLineItem(fnfId, data);
|
||||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to add line item');
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to add line item');
|
||||||
return response.data;
|
return response.data;
|
||||||
|
},
|
||||||
|
updateFnF: async (id: string, data: any) => {
|
||||||
|
const response: any = await API.updateFnF(id, data);
|
||||||
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to update F&F settlement');
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: #030213;
|
--primary: #d97706;
|
||||||
--primary-foreground: oklch(1 0 0);
|
--primary-foreground: oklch(1 0 0);
|
||||||
--secondary: oklch(0.95 0.0058 264.53);
|
--secondary: oklch(0.95 0.0058 264.53);
|
||||||
--secondary-foreground: #030213;
|
--secondary-foreground: #030213;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user