new mail templates added dealer table enhanced add bank details added

This commit is contained in:
laxmanhalaki 2026-04-13 09:25:42 +05:30
parent dad534c169
commit 7126d4b6bf
28 changed files with 2631 additions and 1030 deletions

View File

@ -101,6 +101,9 @@ export const API = {
getDealerById: (id: string) => client.get(`/dealer/${id}`),
updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data),
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
getEmailTemplates: () => client.get('/admin/email-templates'),
@ -152,6 +155,7 @@ export const API = {
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
updateFnF: (id: string, data: any) => client.put(`/settlement/fnf/${id}`, data),
getSettlementDepartments: () => client.get('/settlement/departments'),
// Line items
addLineItem: (fnfId: string, data: any) => client.post(`/settlement/fnf/${fnfId}/line-items`, data),

View File

@ -50,6 +50,7 @@ import {
Info,
ShieldAlert,
CheckCircle2,
CreditCard,
} from 'lucide-react';
import { Progress } from '../ui/progress';
import { Textarea } from '../ui/textarea';
@ -356,6 +357,14 @@ export const ApplicationDetails = () => {
constitutionType: data.constitutionType,
architectureStatus: data.architectureStatus,
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);
if (data.uploadedDocuments) {
@ -477,6 +486,50 @@ export const ApplicationDetails = () => {
const [previewDoc, setPreviewDoc] = useState<any>(null);
const [showPreviewModal, setShowPreviewModal] = useState(false);
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 [isScheduling, setIsScheduling] = useState(false);
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
@ -1672,8 +1725,14 @@ export const ApplicationDetails = () => {
currentUserEvaluation?.decision === 'Rejected' ||
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
// Final visibility flags
const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
// Centralized Permissions Utility (Consolidates 500 lines of fragmented logic)
const getApplicationPermissions = () => {
if (!application || !currentUser) {
return { canApprove: false, canReject: false, canSchedule: false, canAssign: false, isLoaLocked: false, showDecisionMessage: false };
}
// 1. Core Flags
const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
const isAdministrativeStage = [
'Level 3 Approved', 'FDD Verification',
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
@ -1684,63 +1743,54 @@ export const ApplicationDetails = () => {
'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;
const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected' || application.status === 'Approved';
// 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'
// 2. Interview Specific Logic
const activeInterviewForUser = (interviews || []).find(i =>
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
const hasSubmittedFeedback = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
(e: any) => e.evaluatorId === currentUser?.id
);
// 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'
);
// 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 || '')) {
// FDD Stage Restriction: Only DD Admin or Super Admin can approve
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') {
sequenceMet = false;
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');
}
// LOI Sequence Enforcement
if (application.status === 'LOI In Progress') {
if (currentUser?.role === 'NBH') {
sequenceMet = !!ddHeadApproved; // NBH can only approve after DD Head
}
// Roles not in the sequence (like Admin or Finance) should not see the buttons for LOI issuance decision
if (!['DD Head', 'NBH'].includes(currentUser?.role || '')) {
sequenceMet = false;
}
}
// 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;
// LOA Sequence Enforcement
if (application.status === 'LOA Pending') {
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:
// 1. It's an interview and feedback is submitted AND no decision made yet
// 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet AND sequence is valid
const shouldShowApproveReject =
!isLoaLocked && (
(!hasMadeDecisionForUser && !!hasSubmittedFeedbackForActive) ||
(!!isAdmin && !!isAdministrativeStage && !hasMadeStageDecision && !!sequenceMet)
// 5. Final Permission Bits
const isDecisionMade = hasMadeDecisionTotal || hasMadeStageDecision;
const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && (
(!!activeInterviewForUser && !!hasSubmittedFeedback) ||
(isAdminRole && isAdministrativeStage && sequenceMet)
);
const shouldShowDecisionMessage = !!hasMadeDecisionForUser && (!isAdministrativeStage || !!hasMadeStageDecision);
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" />
</div>
<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 {new Date(report.createdAt).toLocaleDateString()}</p>
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {formatDateTime(report.createdAt)}</p>
</div>
</div>
<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-900">{application.pastExperience || 'N/A'}</p>
</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>
</Card>
@ -2679,38 +2863,7 @@ export const ApplicationDetails = () => {
</div>
</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>
{isExpanded && (
@ -2826,7 +2979,7 @@ export const ApplicationDetails = () => {
<span className="truncate max-w-[150px] md:max-w-[300px]">{doc.fileName}</span>
</TableCell>
<TableCell>{doc.documentType}</TableCell>
<TableCell>{new Date(doc.createdAt).toLocaleDateString()}</TableCell>
<TableCell>{formatDateTime(doc.createdAt)}</TableCell>
<TableCell>
{doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')}
</TableCell>
@ -2919,7 +3072,7 @@ export const ApplicationDetails = () => {
<h4 className="font-semibold text-slate-800 mb-2">
Level {interview.level} Interview
<span className="font-normal text-slate-500 text-sm ml-2">
({new Date(interview.scheduleDate).toLocaleDateString()} - {interview.interviewType})
({formatDateTime(interview.scheduleDate)} - {interview.interviewType})
</span>
</h4>
{interview.evaluations && interview.evaluations.length > 0 ? (
@ -3204,7 +3357,7 @@ export const ApplicationDetails = () => {
{deposit?.paymentReference && (
<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>
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>}
{deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
</div>
)}
@ -3287,7 +3440,7 @@ export const ApplicationDetails = () => {
{deposit?.paymentReference && (
<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>
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>}
{deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
</div>
)}
@ -3352,7 +3505,7 @@ export const ApplicationDetails = () => {
<div className="flex items-start justify-between">
<p className="text-slate-900 font-medium">{log.description || log.action}</p>
<span className="text-slate-500 text-sm whitespace-nowrap ml-4">
{new Date(log.timestamp).toLocaleString()}
{formatDateTime(log.timestamp)}
</span>
</div>
<p className="text-slate-600 mt-1">by {log.userName || 'System'}</p>
@ -3418,7 +3571,7 @@ export const ApplicationDetails = () => {
{application.deadline && (
<div>
<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>
)}
</CardContent>
@ -3434,7 +3587,7 @@ export const ApplicationDetails = () => {
</CardHeader>
<CardContent className="space-y-3">
{/* Show Approve/Reject block */}
{isLoaLocked && (
{permissions.isLoaLocked && (
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
<Lock className="w-4 h-4 text-amber-600" />
<AlertTitle className="text-amber-900 font-semibold">Stage Locked</AlertTitle>
@ -3444,10 +3597,10 @@ export const ApplicationDetails = () => {
</Alert>
)}
{shouldShowApproveReject && (
{permissions.canApprove && (
<>
<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)}
>
<CheckCircle className="w-4 h-4 mr-2" />
@ -3456,7 +3609,7 @@ export const ApplicationDetails = () => {
<Button
variant="destructive"
className="w-full"
className="w-full font-bold"
onClick={() => setShowRejectModal(true)}
>
<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'}`}>
You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
</div>
@ -3488,8 +3641,7 @@ export const ApplicationDetails = () => {
Work Note
</Button>
{currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
!([1, 2, 3].every(level => interviews.some(i => i.level === level))) && (
{permissions.canSchedule && (
<Button
variant="outline"
className="w-full"
@ -4639,7 +4791,7 @@ export const ApplicationDetails = () => {
</Badge>
</TableCell>
<TableCell className="py-3 whitespace-nowrap text-slate-600">
{new Date(doc.createdAt).toLocaleDateString('en-GB')}
{formatDateTime(doc.createdAt)}
</TableCell>
<TableCell className="py-3 text-slate-600">
{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}

View 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>
);
};

View File

@ -81,7 +81,7 @@ const getStatusColor = (status: string) => {
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 [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
@ -152,6 +152,41 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
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') => {
setActionType(type);
setIsActionDialogOpen(true);
@ -203,7 +238,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
<div>
<h1 className="text-slate-900">{request.requestId} - Constitutional Change Details</h1>
<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>
</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>
<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-600 text-sm">{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-900">{request.dealer?.dealerProfile?.businessName || request.outlet?.name || '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.dealer?.dealerProfile?.registeredAddress || request.outlet?.city || request.outlet?.address || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
@ -605,6 +640,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{permissions.canApprove && (
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => handleAction('approve')}
@ -617,7 +653,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
)}
Approve Request
</Button>
)}
{permissions.canReject && (
<Button
variant="destructive"
className="w-full"
@ -631,6 +669,15 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
)}
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">
<Button

View File

@ -432,11 +432,15 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
<TableRow key={request.requestId}>
<TableCell>
<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>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || request.outlet?.address || '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.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>
<div className="flex items-center gap-2">
@ -507,11 +511,14 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
<TableRow key={request.requestId}>
<TableCell>
<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>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || '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.dealer?.dealerProfile?.registeredAddress ||
(request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : (request.outlet?.address || 'N/A'))}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
@ -578,11 +585,15 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
<TableRow key={request.requestId}>
<TableCell>
<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>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || '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.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>
<div className="flex items-center gap-2">
@ -655,11 +666,14 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
<TableRow key={request.requestId}>
<TableCell>
<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>
<div className="font-medium text-slate-900">{request.outlet?.name || 'N/A'}</div>
<div className="text-slate-600 text-sm">{request.outlet?.city || '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.dealer?.dealerProfile?.registeredAddress ||
(request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : (request.outlet?.address || 'N/A'))}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">

View File

@ -34,6 +34,7 @@ import { toast } from 'sonner';
import { onboardingService } from '../../services/onboarding.service';
import { worknoteService } from '../../services/worknote.service';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
// Simple helper for class merging
const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
@ -264,7 +265,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div>
<p className="text-sm font-bold text-slate-800">{docType.label}</p>
<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>
</div>
</div>
@ -371,7 +372,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</div>
<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-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 className="flex gap-1">
@ -556,7 +557,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<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>
<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">
@ -614,7 +615,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div>
<p className="text-sm font-bold text-slate-900">{step.stageName}</p>
<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>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { settlementService } from '../../services/settlement.service';
import { Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
@ -31,15 +32,27 @@ import {
Edit2,
Trash2,
Save,
Paperclip
Paperclip,
FileDown
} from 'lucide-react';
import { toast } from 'sonner';
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 = [
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts',
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT',
'Legal', 'Quality', 'Logistics', 'Customer Relations'
// Will be updated from API
let ALL_DEPARTMENTS = [
'Sales', 'Service', 'Spares / Parts', 'Finance', 'Accounts', 'Warranty',
'Marketing', 'HR', 'IT', 'Legal', 'Logistics', 'Quality', 'FDD', 'Apparel',
'DMS', 'Admin / DD-Admin'
];
interface FinanceFnFDetailsPageProps {
@ -50,38 +63,13 @@ interface FinanceFnFDetailsPageProps {
// Removing mock data functions as we use live API
const getDepartmentStatusColor = (status: string) => {
switch (status) {
case 'NOC Submitted':
return 'bg-green-100 text-green-700 border-green-300';
case 'Dues Pending':
return 'bg-red-100 text-red-700 border-red-300';
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 '-';
}
};
const SETTLEMENT_CHECKLIST = [
{ id: 'calculations', label: 'Verified All Department Calculations' },
{ id: 'bank', label: 'Confirmed Bank Account Details' },
{ id: 'docs', label: 'Reviewed All Supporting Documents' },
{ id: 'sap', label: 'Synced Final Dues with SAP' },
{ id: 'noc', label: 'Received All Mandatory NOCs' }
];
interface FinancialLineItem {
id: string;
@ -99,11 +87,28 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
const [previewDocument, setPreviewDocument] = useState<any>(null);
const [bankDetails, setBankDetails] = useState<any[]>([]);
const [isBankModalOpen, setIsBankModalOpen] = useState(false);
const [editingBank, setEditingBank] = useState<any>(null);
const [checklist, setChecklist] = useState<string[]>([]);
useEffect(() => {
fetchDepartments();
fetchFnFDetails();
}, [fnfId]);
const fetchDepartments = async () => {
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 () => {
try {
setLoading(true);
@ -111,23 +116,23 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const data = response.data as any;
if (data.success) {
const s = data.fnf;
setFnfCase({
const mappedCase = {
id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(),
dealerName: s.outlet?.dealer?.fullName || s.outlet?.name || 'N/A',
dealerCode: s.outlet?.code || 'N/A',
caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || 'N/A',
dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination',
submittedDate: new Date(s.createdAt).toLocaleDateString(),
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD',
submittedDate: formatDateTime(s.createdAt),
createdAt: s.createdAt,
dueDate: s.settlementDate ? formatDateTime(s.settlementDate) : 'TBD',
status: s.status,
bankDetails: {
accountName: s.outlet?.dealer?.fullName || 'N/A',
accountNumber: 'N/A', // These should come from dealer model in a real app
ifscCode: 'N/A',
bankName: 'N/A',
branch: 'N/A'
},
dealerId: s.outlet?.dealer?.id || s.dealerId,
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.requestId || s.terminationRequest?.id || "N/A",
salesCode: s.dealer?.dealerCode?.salesCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A',
serviceCode: s.dealer?.dealerCode?.serviceCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.serviceCode || 'N/A',
gearCode: s.dealer?.dealerCode?.gearCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gearCode || 'N/A',
gmaCode: s.dealer?.dealerCode?.gmaCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gmaCode || 'N/A',
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
@ -157,7 +162,17 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
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
const pItems: FinancialLineItem[] = [];
@ -172,21 +187,29 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
amount: Math.abs(li.amount)
};
if (li.amount < 0) {
if (li.itemType === 'Payable') {
pItems.push(item);
} else {
// Check if it's a deduction (usually Warranty related in this UI)
if (li.department.toLowerCase().includes('warranty')) {
} else if (li.itemType === 'Deduction') {
dItems.push(item);
} else {
rItems.push(item);
}
}
});
setPayableItems(pItems);
setReceivableItems(rItems);
setDeductionItems(dItems);
// Populate settlement details from backend
setSettlementDetails({
verificationTransactionId: s.transactionReference || '',
settlementAmount: (s.settlementAmount || calculateDynamicSettlement().settlementAmount).toString(),
settlementDate: s.settlementDate ? new Date(s.settlementDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
paymentMode: s.paymentMode || '',
bankReference: '', // Optional field
verificationRemarks: s.remarks || '',
adjustments: '0'
});
}
} catch (error) {
console.error('Fetch F&F error:', error);
@ -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
const [newPayable, setNewPayable] = useState({ department: '', description: '', amount: '' });
const [newReceivable, setNewReceivable] = useState({ department: '', description: '', amount: '' });
@ -225,6 +303,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const settlement = calculateDynamicSettlement();
// Get primary bank for display
const primaryBank = bankDetails.find(b => b.isPrimary) || bankDetails[0];
const [settlementDetails, setSettlementDetails] = useState({
verificationTransactionId: '',
settlementAmount: settlement.settlementAmount.toString(),
@ -248,7 +329,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const response = await API.addLineItem(fnfId, {
department: newPayable.department,
remarks: newPayable.description,
amount: amount
amount: Math.abs(parseFloat(newPayable.amount)),
itemType: 'Payable'
});
const data = response.data as any;
if (data.success) {
@ -312,7 +394,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const response = await API.addLineItem(fnfId, {
department: newReceivable.department,
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;
if (data.success) {
@ -371,7 +454,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const response = await API.addLineItem(fnfId, {
department: newDeduction.department,
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;
if (data.success) {
@ -426,7 +510,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const newDocs = Array.from(files).map(file => ({
name: file.name,
size: `${(file.size / 1024).toFixed(0)} KB`,
uploadedOn: new Date().toISOString().split('T')[0],
uploadedOn: new Date().toISOString(),
type: 'Settlement Verification'
}));
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) {
toast.error('Please fill in all required settlement details');
return;
}
const adjustedAmount = settlement.settlementAmount + parseFloat(settlementDetails.adjustments || '0');
if (adjustedAmount.toString() !== settlementDetails.settlementAmount) {
toast.warning('Settlement amount has been adjusted');
}
try {
setSubmitting(true);
const adjustedAmount = (settlement.settlementAmount || 0) + parseFloat(settlementDetails.adjustments || '0');
toast.success(`F&F Settlement approved for ${fnfCase.dealerName}`);
await settlementService.updateFnF(fnfId, {
status: 'Completed',
finalSettlementAmount: adjustedAmount,
settlementDate: settlementDetails.settlementDate,
paymentMode: settlementDetails.paymentMode,
transactionReference: settlementDetails.verificationTransactionId,
remarks: settlementDetails.verificationRemarks || 'Approved by Finance'
});
toast.success(`F&F Settlement approved and completed for ${fnfCase.dealerName}`);
setTimeout(() => onBack(), 1500);
} catch (error: any) {
console.error('Approve settlement error:', error);
toast.error(error.message || 'Failed to approve settlement');
} finally {
setSubmitting(false);
}
};
const handleRejectSettlement = () => {
@ -636,7 +736,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<Label className="text-slate-500">Request Age</Label>
<p className="text-slate-900">
{(() => {
const submitted = new Date(fnfCase.submittedDate);
const submitted = new Date(fnfCase.createdAt);
const today = new Date();
const diffTime = Math.abs(today.getTime() - submitted.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
@ -646,19 +746,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div>
<div>
<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>
<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>
<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>
<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>
</CardContent>
@ -718,8 +818,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
<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-amber-600 mt-0.5" />
<div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<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">
<CardHeader>
<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
</CardTitle>
</CardHeader>
@ -1210,8 +1310,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-white border border-blue-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="flex items-start gap-3 p-4 bg-white border border-amber-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600">
@ -1292,7 +1392,11 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<TableRow key={dept.id}>
<TableCell>{dept.departmentName}</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}
</Badge>
</TableCell>
@ -1330,7 +1434,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
filePath: dept.supportingDocument,
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" />
View Proof
@ -1346,10 +1450,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card>
{/* Important Notes */}
<Card className="bg-blue-50 border-blue-200">
<Card className="bg-blue-50 border-amber-200">
<CardContent className="pt-6">
<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>
<p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p>
<ul className="text-sm text-slate-700 space-y-1">
@ -1363,6 +1467,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card>
</TabsContent>
<TabsContent value="documents" className="space-y-4">
{/* Submitted Documents */}
<Card>
@ -1446,51 +1551,93 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</TabsContent>
<TabsContent value="bank" className="space-y-4">
{/* Bank Account Details */}
<Card>
<CardHeader>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Building className="w-5 h-5" />
Dealer Bank Account Details
</CardTitle>
<CardDescription>
Bank account for settlement transfer (if payable to dealer)
Manage bank accounts for settlement transfer
</CardDescription>
</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 className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<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-slate-500">Account Holder Name</Label>
<p className="text-slate-900">{fnfCase.bankDetails.accountName}</p>
<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-slate-500">Account Number</Label>
<p className="text-slate-900">{fnfCase.bankDetails.accountNumber}</p>
<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-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>
<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-start gap-3">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="text-sm text-slate-900 mb-1">Bank Verification Required</p>
<p className="text-sm text-slate-600">
Please verify bank account details before processing settlement payment
</p>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<Button
variant="ghost"
size="sm"
className="h-7 text-[11px] text-amber-600"
onClick={() => {
setEditingBank(bank);
setIsBankModalOpen(true);
}}
>
<Edit2 className="w-3 h-3 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 text-[11px] text-red-600"
onClick={() => handleDeleteBank(bank.id)}
>
<Trash2 className="w-3 h-3 mr-1" />
Delete
</Button>
</div>
</div>
</CardContent>
</Card>
))
) : (
<div className="col-span-full py-12 text-center border-2 border-dashed rounded-lg bg-slate-50">
<Building className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-600 text-sm">No bank details found</p>
</div>
)}
</div>
</CardContent>
</Card>
@ -1511,6 +1658,79 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{fnfCase.status === 'Completed' ? (
<div className="space-y-6">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-3 text-green-700 mb-2">
<CheckCircle className="w-5 h-5" />
<span className="font-semibold">Settlement Completed</span>
</div>
<p className="text-sm text-green-600">
This settlement has been finalized and processed.
</p>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Settlement Date</span>
<span className="text-slate-900 font-medium">{formatDateTime(settlementDetails.settlementDate)}</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Payment Mode</span>
<span className="text-slate-900 font-medium">{settlementDetails.paymentMode}</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Transaction ID</span>
<span className="text-slate-900 font-medium truncate ml-4 max-w-[150px]" title={settlementDetails.verificationTransactionId}>
{settlementDetails.verificationTransactionId}
</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Final Amount</span>
<span className="text-slate-900 font-bold text-lg">{parseFloat(settlementDetails.settlementAmount).toLocaleString()}</span>
</div>
</div>
{settlementDetails.verificationRemarks && (
<div className="mt-4">
<Label className="text-slate-500 mb-1 block">Finance Remarks</Label>
<div className="p-3 bg-slate-50 rounded border text-sm text-slate-700">
{settlementDetails.verificationRemarks}
</div>
</div>
)}
<Button variant="outline" className="w-full mt-4" onClick={() => window.print()}>
<FileDown className="w-4 h-4 mr-2" />
Download Settlement Letter
</Button>
</div>
) : (
<>
{/* Settlement Checklist */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4">
<p className="text-sm font-bold text-slate-900 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-amber-600" />
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>
<Label htmlFor="paymentMode">
Payment Mode <span className="text-red-500">*</span>
@ -1614,15 +1834,26 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<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" />
Approve Settlement
)}
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-blue-600 hover:bg-blue-50"
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
@ -1632,47 +1863,31 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
variant="outline"
className="w-full border-red-300 text-red-600 hover:bg-red-50"
onClick={handleRejectSettlement}
disabled={submitting}
>
<XCircle className="w-4 h-4 mr-2" />
Reject Settlement
</Button>
</div>
</>
)}
</CardContent>
</Card>
{/* 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>
<BankDetailsModal
isOpen={isBankModalOpen}
onClose={() => {
setIsBankModalOpen(false);
setEditingBank(null);
}}
onSubmit={handleUpsertBank}
editingBank={editingBank}
isSubmitting={false}
/>
<DocumentPreviewModal
isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)}

View File

@ -37,6 +37,7 @@ import {
TrendingDown,
MapPin
} from 'lucide-react';
import { formatDateTime } from '../ui/utils';
import { toast } from 'sonner';
// Using live data from API instead of mockFnFCases
@ -76,12 +77,12 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
const getMappedData = (s: any) => ({
id: s.id,
caseId: s.resignation?.resignationId || s.id,
dealerCode: s.outlet?.code || 'N/A',
dealerName: s.outlet?.dealer?.name || 'N/A',
caseId: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A',
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),
financialData: {
totalPayables: parseFloat(s.totalPayables) || 0,
@ -90,7 +91,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
},
settlementAmount: Math.abs(parseFloat(s.netAmount) || 0),
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);

View File

@ -336,7 +336,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</div>
<div>
<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>
<Button

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { User } from '../../lib/mock-data';
import { API } from '../../api/API';
import { toast } from 'sonner';
import { formatDateTime } from '../ui/utils';
interface FnFPageProps {
currentUser: User | null;
@ -85,11 +86,11 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
dealershipName: s.outlet?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || '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',
totalRecoveryAmount: parseFloat(s.totalReceivables) || 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 || []
});

View File

@ -6,6 +6,7 @@ import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { Mail, Plus, Edit2, Trash2, Calendar } from 'lucide-react';
import { RootState } from '../../../store';
import { formatDateTime } from '../../ui/utils';
interface EmailTemplatesProps {
onAddTemplate: () => void;
@ -63,7 +64,7 @@ export const EmailTemplates: React.FC<EmailTemplatesProps> = ({
<TableCell className="text-slate-500 text-sm">
<div className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5" />
{template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : '-'}
{template.updatedAt ? formatDateTime(template.updatedAt) : '-'}
</div>
</TableCell>
<TableCell className="text-right">

View File

@ -6,6 +6,7 @@ import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
import { RootState } from '../../../store';
import { formatDateTime } from '../../ui/utils';
interface LocationManagementProps {
onAddLocation: () => void;
@ -85,13 +86,13 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
<div className="flex items-center gap-2 text-xs">
<span className="text-slate-600">From:</span>
<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>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="text-slate-600">To:</span>
<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>
</div>
</div>

View File

@ -26,6 +26,7 @@ import {
TableRow,
} from '../ui/table';
import { toast } from 'sonner';
import { formatDateTime } from '../ui/utils';
interface NonOpportunitiesPageProps {
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-900">{lead.education}</TableCell>
<TableCell className="text-slate-600">
{new Date(lead.submissionDate).toLocaleDateString()}
{formatDateTime(lead.submissionDate)}
</TableCell>
<TableCell className="text-right">
<Button

View File

@ -4,7 +4,14 @@ import {
Upload,
Clock,
RefreshCw,
File
File,
CreditCard,
Building,
Landmark,
CheckCircle2,
Info,
User,
MapPin
} from 'lucide-react';
import { toast } from 'sonner';
import { API } from '../../api/API';
@ -22,6 +29,19 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
const [selectedDocType, setSelectedDocType] = useState('');
const [file, setFile] = useState<File | null>(null);
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(() => {
fetchData();
@ -36,7 +56,18 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
]);
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) {
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>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
@ -109,7 +158,8 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
return (
<div className="space-y-6">
<div className="flex items-center mb-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<button
onClick={onBack}
className="mr-3 p-1.5 rounded-full hover:bg-slate-200 text-slate-600 transition-colors"
@ -130,63 +180,214 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
</div>
</div>
</div>
</div>
<div className="animate-in fade-in duration-500">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6 mb-6">
<h4 className="text-lg font-semibold text-slate-900 mb-4 border-b pb-2">Status & Tracking</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-slate-500 mb-1">Overall Status</p>
<p className="font-medium text-slate-900">{details.overallStatus || '-'}</p>
<div className="animate-in fade-in duration-500 space-y-6">
{/* Status & Tracking Summary Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6">
<div className="flex items-center justify-between mb-4 border-b pb-2">
<h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<Info className="w-5 h-5 text-amber-600" /> Application Summary
</h4>
<div className="text-right">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Current Stage</p>
<span className="bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide">
{details.currentStage || details.overallStatus}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-4">
<div className="flex items-start gap-3">
<div className="p-2 bg-blue-50 rounded-lg">
<User className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Current Stage</p>
<p className="font-medium text-slate-900">{details.currentStage || '-'}</p>
<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-sm text-slate-500 mb-1">Location</p>
<p className="font-medium text-slate-900">{details.city}, {details.state}</p>
<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 className="space-y-4">
<div className="flex items-start gap-3">
<div className="p-2 bg-green-50 rounded-lg">
<Building className="w-4 h-4 text-green-600" />
</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 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 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 && (
<div className="mt-4 p-3 bg-amber-50 border border-amber-100 rounded-lg">
<p className="text-xs font-semibold text-amber-800 uppercase tracking-wider mb-1">Latest Feedback</p>
<p className="text-sm text-amber-900 italic">"{details.statusHistory[0].changeReason}"</p>
</div>
<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 className="mt-6">
<div className="flex justify-between items-center mb-1">
<p className="text-sm font-medium text-slate-700">Application Progress</p>
<p className="text-sm font-medium text-amber-600">{details.progressPercentage || 0}%</p>
</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>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="p-6 border-b border-slate-200">
<h4 className="flex items-center gap-2 text-lg font-semibold text-slate-900">
<Upload className="w-5 h-5 text-blue-600" /> Document Upload
{/* Statutory & Bank Details Form */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 bg-slate-900 text-white flex justify-between items-center">
<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>
</div>
<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="space-y-2">
<label className="text-sm font-medium text-slate-900">Document Type</label>
<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-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
<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}
onChange={(e) => setSelectedDocType(e.target.value)}
disabled={isUploading}
@ -213,46 +414,54 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<option value="Other">Other</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">File</label>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Select File</label>
<input
type="file"
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}
disabled={isUploading}
/>
</div>
<div className="md:col-span-2 flex justify-end mt-2">
<div className="md:col-span-2 flex justify-end">
<button
onClick={handleUpload}
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" />}
Upload
{isUploading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
Upload Document
</button>
</div>
</div>
<div className="space-y-3">
<h3 className="font-medium text-slate-900">Uploaded Documents ({documents.length})</h3>
<div className="space-y-2">
<div className="space-y-4">
<div className="flex items-center gap-2 text-slate-900 border-b pb-1">
<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) => (
<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">
<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>
<p className="text-sm font-medium">{doc.documentType}</p>
<p className="text-xs text-slate-500">{doc.fileName}</p>
<p className="text-[11px] font-bold text-slate-900">{doc.documentType}</p>
<p className="text-[10px] text-slate-400 truncate w-32">{doc.fileName}</p>
</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'}
</span>
</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>
@ -260,31 +469,36 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
</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="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">
<Clock className="w-4 h-4 text-amber-600" /> Timeline
<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" /> Recent Updates
</h3>
</div>
<div className="p-6">
{details.statusHistory?.length > 0 ? (
<div className="relative space-y-6">
<div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-200"></div>
{[...details.statusHistory].reverse().map((item: any) => (
<div key={item.id} className="relative pl-8">
<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="w-2 h-2 rounded-full bg-green-500"></div>
<div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-100"></div>
{[...details.statusHistory].reverse().map((item: any, idx: number) => (
<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-amber-500 shadow-sm">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
</div>
<div>
<p className="text-sm font-semibold text-slate-900">{item.newStatus}</p>
<p className="text-[11px] text-slate-500">{new Date(item.createdAt).toLocaleString()}</p>
<p className="text-xs font-bold text-slate-900 uppercase tracking-tight">{item.newStatus}</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>
) : (
<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>

View File

@ -8,6 +8,7 @@ import { useState, useEffect } from 'react';
import { User } from '../../lib/mock-data';
import { toast } from 'sonner';
import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils';
interface RelocationRequestPageProps {
currentUser: User | null;
@ -205,7 +206,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
<div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
<div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div>
</TableCell>
<TableCell>
@ -396,7 +397,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{new Date(request.createdAt).toLocaleDateString()}</div>
<div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
</TableCell>
<TableCell>
<Button

View File

@ -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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
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 { toast } from 'sonner';
import { resignationService } from '../../services/resignation.service';
import { Loader2 } from 'lucide-react';
import { API } from '../../api/API';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
@ -30,11 +30,22 @@ interface ResignationDetailsProps {
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) {
const navigate = useNavigate();
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
const [remarks, setRemarks] = 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 [showClearanceDialog, setShowClearanceDialog] = useState(false);
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)
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
const progressStages = [
{ 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' }
];
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) => {
if (!resignationData) return 'pending';
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 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 'active';
return 'pending';
@ -119,7 +170,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const payload = {
action: actionDialog.type,
remarks,
assignTo: assignToUser
assignTo: assignToUser,
force: forceTriggerFnF
};
const response: any = await API.updateResignationStatus(resignationId, payload);
@ -154,11 +206,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
formData.append('file', clearanceFile);
}
await resignationService.updateClearance(resignationId, formData);
const response: any = await resignationService.updateClearance(resignationId, formData);
if (response?.success) {
toast.success(`Successfully updated clearance for ${selectedDept}`);
setShowClearanceDialog(false);
setClearanceFile(null);
fetchResignation();
} else {
toast.error(response?.message || 'Failed to update clearance status');
}
} catch (error) {
toast.error('Failed to update clearance status');
} finally {
@ -169,7 +226,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
if (isLoading && !resignationData) {
return (
<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>
);
}
@ -200,8 +257,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
{currentUser?.role !== 'Dealer' && (
<>
{permissions.canApprove && (
<Button
size="sm"
disabled={isSubmitting}
@ -211,45 +267,48 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
)}
{permissions.canSendBack && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all"
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
</Button>
</>
)}
{permissions.canWithdraw && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
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>
)}
</div>
{/* Secondary Actions */}
{currentUser?.role !== 'Dealer' && (
<div className="flex items-center gap-2">
{canPushToFnF && resignationData?.status !== 'FNF_INITIATED' && resignationData?.status !== 'Settled' && (
{permissions.canPushToFnF && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-blue-600 border-blue-300 hover:bg-blue-50 transition-all"
className="text-amber-600 border-blue-300 hover:bg-blue-50 transition-all"
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" />}
Push to F&F
</Button>
)}
{permissions.canAssign && (
<Button
size="sm"
variant="outline"
@ -260,9 +319,9 @@ 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" />}
Assign User
</Button>
</div>
)}
</div>
</div>
{/* Work Notes Button - Independent Section */}
<div className="flex items-center justify-between pt-4 border-t border-slate-200">
@ -353,7 +412,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div>
<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>
<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={`w-10 h-10 rounded-full flex items-center justify-center ${
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'
}`}>
{status === 'completed' ? (
@ -458,7 +517,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex items-center justify-between mb-1">
<h3 className={
status === 'completed' ? 'text-green-600' :
status === 'active' ? 'text-blue-600' :
status === 'active' ? 'text-amber-600' :
'text-slate-400'
}>{stage.name}</h3>
{timelineEntry && (
@ -477,7 +536,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<Button
variant="ghost"
size="sm"
className="mt-2 text-blue-600"
className="mt-2 text-amber-600"
onClick={() => handleViewStageDocuments(stage.name)}
>
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="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center border border-blue-100">
<FileText className="w-4 h-4 text-blue-500" />
<FileText className="w-4 h-4 text-amber-500" />
</div>
<div>
<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'
});
}}
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" />
Preview
@ -579,7 +638,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<Button
variant="ghost"
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={() => {
setSelectedDept(dept);
setClearanceStatus(displayStatus || 'Pending');
@ -627,7 +686,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
...(resignationData?.uploadedDocuments || [])
];
// Add clearance documents
// Add clearance documents from legacy JSON field
if (resignationData?.departmentalClearances) {
Object.entries(resignationData.departmentalClearances).forEach(([dept, data]: [string, any]) => {
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 (
<TableRow>
<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 || []).map((log: any, index: number) => (
<div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" />
<div className="w-2 h-2 rounded-full bg-amber-600 mt-2" />
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<p className="font-medium text-slate-900">{log.action || log.status}</p>
@ -764,6 +838,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</Select>
</div>
) : actionDialog.type === 'pushfnf' ? (
<div className="space-y-4">
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div className="text-sm text-amber-800">
<p className="font-bold">Manual Trigger Notice</p>
<p>Normally F&F is triggered after LWD. Use manual trigger only if urgent clearance is required.</p>
</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
@ -773,6 +867,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
rows={3}
/>
</div>
</div>
) : (
<div className="space-y-2">
<Label>Remarks *</Label>
@ -823,7 +918,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<DialogContent className="max-w-3xl">
<DialogHeader>
<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}
</DialogTitle>
<DialogDescription>
@ -853,7 +948,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<TableCell>{doc.uploadDate}</TableCell>
<TableCell>{doc.uploader}</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" />
View
</Button>
@ -948,7 +1043,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
/>
<div className="flex flex-col items-center justify-center gap-2">
{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" />
<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); }} />

View File

@ -7,6 +7,7 @@ import { useState, useEffect } from 'react';
import { API } from '../../api/API';
import { toast } from 'sonner';
import { User as UserType } from '../../lib/mock-data';
import { formatDateTime } from '../ui/utils';
interface ResignationPageProps {
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>
<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>
<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>
<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>
<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>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4 text-slate-500" />
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
<p>{formatDateTime(request.submittedOn)}</p>
</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>
<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>
<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>
<p className="text-slate-600">Current Stage</p>
@ -250,7 +251,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
<p>{formatDateTime(request.submittedOn)}</p>
</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>
<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>
<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>
<p className="text-slate-600">Final Stage</p>
@ -311,7 +312,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div>
<div>
<p className="text-slate-600">Submitted On</p>
<p>{new Date(request.submittedOn).toLocaleDateString()}</p>
<p>{formatDateTime(request.submittedOn)}</p>
</div>
</div>
</div>

View File

@ -109,6 +109,45 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
// Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
// 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
const request = terminationData || {};
@ -347,6 +386,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
{currentUser?.role !== 'Dealer' && (
<>
{!permissions.canFinalize && (
<>
{permissions.canApprove && (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
@ -355,7 +397,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<Check className="w-4 h-4 mr-2" />
Approve
</Button>
{request.currentStage === 'NBH' && (currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && (
)}
{permissions.canIssueSCN && (
<Button
size="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
</Button>
)}
{request.currentStage === 'SCN' && (['Legal', 'DD Admin', 'Super Admin'].includes(currentUser?.role || '')) && (
{permissions.canUploadSCNResponse && (
<Button
size="sm"
className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm"
@ -378,16 +421,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
Upload SCN Response
</Button>
)}
{(['NBH Final', 'CCO', 'CEO'].includes(request.currentStage)) && (currentUser?.role === request.currentStage || currentUser?.role === 'Super Admin') && (
<Button
size="sm"
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
onClick={() => setShowFinalizeDialog(true)}
>
<ShieldCheck className="w-4 h-4 mr-2" />
Final Authorization
</Button>
)}
{permissions.canApprove && (
<Button
size="sm"
variant="outline"
@ -397,13 +431,26 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<RotateCcw className="w-4 h-4 mr-2" />
Send Back
</Button>
)}
</>
)}
{permissions.canFinalize && (
<Button
size="sm"
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
onClick={() => setShowFinalizeDialog(true)}
>
<ShieldCheck className="w-4 h-4 mr-2" />
Final Authorization
</Button>
)}
</>
)}
</div>
{/* Secondary Actions */}
<div className="flex items-center gap-2">
{canPushToFnF && (
{permissions.canPushToFnF && (
<Button
size="sm"
variant="outline"
@ -414,6 +461,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
Push to F&F
</Button>
)}
{!permissions.isFinalState && (
<Button
size="sm"
variant="outline"
@ -423,6 +471,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<UserPlus className="w-4 h-4 mr-2" />
Assign User
</Button>
)}
</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>
<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>
<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>
</div>
<div>
<Label className="text-slate-600">Accessories Code</Label>
<p>{request.dealer?.dealerCode?.accessoriesCode || request.accessoriesCode || 'N/A'}</p>
<Label className="text-slate-600">GMA Code</Label>
<p>{request.dealer?.dealerCode?.gmaCode || request.accessoriesCode || 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">GMA Code</Label>

View File

@ -14,6 +14,7 @@ import { User } from '../../lib/mock-data';
import { toast } from 'sonner';
import { onboardingService } from '../../services/onboarding.service';
import { settlementService } from '../../services/settlement.service';
import { formatDateTime } from '../ui/utils';
interface FinanceDashboardProps {
currentUser: User | null;
@ -325,7 +326,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</div>
<div>
<p className="text-slate-600">Created On</p>
<p>{new Date(app.createdAt).toLocaleDateString()}</p>
<p>{formatDateTime(app.createdAt)}</p>
</div>
</div>
</div>
@ -402,7 +403,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</div>
<div>
<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>

View File

@ -22,6 +22,7 @@ import { RootState } from '../../store';
import { logout } from '../../store/slices/authSlice';
import { toast } from 'sonner';
import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils';
import { Badge } from '../ui/badge';
import { ProspectiveApplicationDetails } from '../applications/ProspectiveApplicationDetails';
@ -206,7 +207,7 @@ function ProspectiveApplicationList() {
</div>
<div className="flex justify-between items-center">
<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 className="mt-6">
<div className="flex justify-between items-center mb-1">

View File

@ -12,6 +12,7 @@ import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { dealerService } from '../../services/dealer.service';
import { formatDateTime } from '../ui/utils';
interface DealerConstitutionalChangePageProps {
currentUser: UserType | null;
@ -358,7 +359,7 @@ export function DealerConstitutionalChangePage({ currentUser, onViewDetails }: D
</Badge>
</TableCell>
<TableCell className="text-slate-600">
{new Date(request.createdAt).toLocaleDateString()}
{formatDateTime(request.createdAt)}
</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>

View File

@ -13,6 +13,7 @@ import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { dealerService } from '../../services/dealer.service';
import { masterService } from '../../services/master.service';
import { formatDateTime } from '../ui/utils';
interface DealerRelocationPageProps {
currentUser: UserType | null;
@ -428,7 +429,7 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
</div>
</TableCell>
<TableCell className="text-slate-600">
{new Date(request.createdAt).toLocaleDateString()}
{formatDateTime(request.createdAt)}
</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>

View File

@ -13,6 +13,7 @@ import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { dealerService } from '../../services/dealer.service';
import { resignationService } from '../../services/resignation.service';
import { formatDateTime } from '../ui/utils';
interface DealerResignationPageProps {
currentUser: UserType | null;
@ -435,7 +436,7 @@ export function DealerResignationPage({ currentUser, onViewDetails }: DealerResi
<TableCell className="font-medium text-slate-900">{request.resignationId}</TableCell>
<TableCell>{request.outlet?.name}</TableCell>
<TableCell>{request.resignationType}</TableCell>
<TableCell>{new Date(request.submittedOn).toLocaleDateString()}</TableCell>
<TableCell>{formatDateTime(request.submittedOn)}</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>
{request.status}

42
src/lib/dateUtils.ts Normal file
View 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 '-';
}
};

View File

@ -138,6 +138,14 @@ export interface Application {
districtId?: string;
fddAssignments?: any[];
statutoryStatus?: string;
panNumber?: string;
gstNumber?: string;
bankName?: string;
accountNumber?: string;
ifscCode?: string;
branchName?: string;
accountHolderName?: string;
registeredAddress?: string;
}
export interface Participant {

View File

@ -30,5 +30,10 @@ export const settlementService = {
const response: any = await API.addLineItem(fnfId, data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to add line item');
return response.data;
},
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;
}
};

View File

@ -9,7 +9,7 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #030213;
--primary: #d97706;
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.95 0.0058 264.53);
--secondary-foreground: #030213;