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,75 +1725,72 @@ 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);
const isAdministrativeStage = [
'Level 3 Approved', 'FDD Verification',
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration'
].includes(application.status);
const finalDepositVerified = getDeposit('FIRST_FILL')?.status === 'Verified';
const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified;
// Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5)
// Approval Chain: DD-Head -> NBH
// Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5)
// Approval Chain: DD-Head -> NBH
const ddHeadApproved = application.stageApprovals?.some(
(a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'
);
// Sequential Enforcement for LOA APPROVAL (SRS 6.18.3.1)
// Approval Chain: DD-Head -> NBH
const ddHeadLoaApproved = application.stageApprovals?.some(
(a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'
);
let sequenceMet = true;
if (!['Super Admin', 'DD Admin'].includes(currentUser?.role || '')) {
// FDD Stage Restriction: Only DD Admin or Super Admin can approve
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') {
sequenceMet = false;
// 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 };
}
// 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;
}
}
// 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',
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration'
].includes(application.status);
// 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;
}
}
}
const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected' || application.status === 'Approved';
// 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)
// 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
);
const shouldShowDecisionMessage = !!hasMadeDecisionForUser && (!isAdministrativeStage || !!hasMadeStageDecision);
// 3. Sequential Sequence Check
const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
let sequenceMet = true;
if (!['Super Admin', 'DD Admin'].includes(currentUser.role)) {
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') sequenceMet = false;
if (application.status === 'LOI In Progress') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadApproved : (currentUser.role === 'DD Head');
if (application.status === 'LOA Pending') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadLoaApproved : (currentUser.role === 'DD Head');
}
// 4. Decision Tracking
const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id));
const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || '');
const hasMadeDecisionTotal = hasMadeStageDecision || hasMadeInterviewDecision;
// 5. Final Permission Bits
const isDecisionMade = hasMadeDecisionTotal || hasMadeStageDecision;
const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && (
(!!activeInterviewForUser && !!hasSubmittedFeedback) ||
(isAdminRole && isAdministrativeStage && sequenceMet)
);
return {
canApprove: canApproveReject,
canReject: canApproveReject,
isLoaLocked,
showDecisionMessage: isDecisionMade && (!isAdministrativeStage || hasMadeStageDecision),
canSchedule: ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
!isFinalState &&
!([1, 2, 3].every(level => (interviews || []).some(i => i.level === level))),
canAssign: ['DD Admin', 'Super Admin', 'DD AM'].includes(currentUser.role)
};
};
const permissions = getApplicationPermissions();
@ -2045,8 +2095,7 @@ export const ApplicationDetails = () => {
<FileText className="w-6 h-6 text-red-500" />
</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,17 +3641,16 @@ 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))) && (
<Button
variant="outline"
className="w-full"
onClick={() => setShowScheduleModal(true)}
>
<Calendar className="w-4 h-4 mr-2" />
Schedule Interview
</Button>
)}
{permissions.canSchedule && (
<Button
variant="outline"
className="w-full"
onClick={() => setShowScheduleModal(true)}
>
<Calendar className="w-4 h-4 mr-2" />
Schedule Interview
</Button>
)}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && (
<>
@ -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,32 +640,44 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => handleAction('approve')}
disabled={isActionLoading}
>
{isActionLoading && actionType === 'approve' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 mr-2" />
)}
Approve Request
</Button>
{permissions.canApprove && (
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => handleAction('approve')}
disabled={isActionLoading}
>
{isActionLoading && actionType === 'approve' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 mr-2" />
)}
Approve Request
</Button>
)}
<Button
variant="destructive"
className="w-full"
onClick={() => handleAction('reject')}
disabled={isActionLoading}
>
{isActionLoading && actionType === 'reject' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<AlertCircle className="w-4 h-4 mr-2" />
)}
Reject Request
</Button>
{permissions.canReject && (
<Button
variant="destructive"
className="w-full"
onClick={() => handleAction('reject')}
disabled={isActionLoading}
>
{isActionLoading && actionType === 'reject' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<AlertCircle className="w-4 h-4 mr-2" />
)}
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',
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'
},
submittedDate: formatDateTime(s.createdAt),
createdAt: s.createdAt,
dueDate: s.settlementDate ? formatDateTime(s.settlementDate) : 'TBD',
status: s.status,
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 if (li.itemType === 'Deduction') {
dItems.push(item);
} else {
// Check if it's a deduction (usually Warranty related in this UI)
if (li.department.toLowerCase().includes('warranty')) {
dItems.push(item);
} else {
rItems.push(item);
}
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');
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 for ${fnfCase.dealerName}`);
setTimeout(() => onBack(), 1500);
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>
<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)
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-500">Account Holder Name</Label>
<p className="text-slate-900">{fnfCase.bankDetails.accountName}</p>
</div>
<div>
<Label className="text-slate-500">Account Number</Label>
<p className="text-slate-900">{fnfCase.bankDetails.accountNumber}</p>
</div>
<div>
<Label className="text-slate-500">IFSC Code</Label>
<p className="text-slate-900">{fnfCase.bankDetails.ifscCode}</p>
</div>
<div>
<Label className="text-slate-500">Bank Name</Label>
<p className="text-slate-900">{fnfCase.bankDetails.bankName}</p>
</div>
<div>
<Label className="text-slate-500">Branch</Label>
<p className="text-slate-900">{fnfCase.bankDetails.branch}</p>
</div>
<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>
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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{bankDetails.length > 0 ? (
bankDetails.map((bank: any) => (
<Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-amber-500 bg-blue-50/30' : ''}`}>
{bank.isPrimary && (
<div className="absolute top-0 right-0 p-1 bg-amber-600 text-white text-[10px] uppercase font-bold px-2 rounded-bl">
Primary
</div>
)}
<CardContent className="p-4 pt-6">
<div className="space-y-3">
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Holder</Label>
<p className="text-sm font-semibold">{bank.accountHolderName}</p>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Bank</Label>
<p className="text-xs truncate">{bank.bankName}</p>
</div>
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">IFSC</Label>
<p className="text-xs">{bank.ifscCode}</p>
</div>
</div>
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Number</Label>
<p className="text-xs font-mono">{bank.accountNumber}</p>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-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>
)}
</div>
</CardContent>
</Card>
@ -1511,168 +1658,236 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="paymentMode">
Payment Mode <span className="text-red-500">*</span>
</Label>
<Input
id="paymentMode"
placeholder="e.g., NEFT, RTGS, Cheque"
value={settlementDetails.paymentMode}
onChange={(e) => setSettlementDetails({ ...settlementDetails, paymentMode: e.target.value })}
/>
</div>
{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>
<Label htmlFor="verificationTxnId">
Transaction ID / Reference <span className="text-red-500">*</span>
</Label>
<Input
id="verificationTxnId"
placeholder="Enter transaction reference"
value={settlementDetails.verificationTransactionId}
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationTransactionId: e.target.value })}
/>
</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>
<div>
<Label htmlFor="bankReference">
Bank Reference Number
</Label>
<Input
id="bankReference"
placeholder="Enter bank reference"
value={settlementDetails.bankReference}
onChange={(e) => setSettlementDetails({ ...settlementDetails, bankReference: e.target.value })}
/>
</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>
)}
<div>
<Label htmlFor="settlementAmount">
Settlement Amount () <span className="text-red-500">*</span>
</Label>
<Input
id="settlementAmount"
type="number"
placeholder="Enter settlement amount"
value={settlementDetails.settlementAmount}
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementAmount: e.target.value })}
/>
</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="adjustments">
Adjustments ()
</Label>
<Input
id="adjustments"
type="number"
placeholder="Enter any adjustments"
value={settlementDetails.adjustments}
onChange={(e) => {
const adjustments = e.target.value;
const adjustedAmount = settlement.settlementAmount + parseFloat(adjustments || '0');
setSettlementDetails({
...settlementDetails,
adjustments,
settlementAmount: adjustedAmount.toString()
});
}}
/>
{parseFloat(settlementDetails.adjustments) !== 0 && (
<p className="text-sm text-amber-600 mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Adjusted amount: {settlementDetails.settlementAmount}
</p>
)}
</div>
<div>
<Label htmlFor="paymentMode">
Payment Mode <span className="text-red-500">*</span>
</Label>
<Input
id="paymentMode"
placeholder="e.g., NEFT, RTGS, Cheque"
value={settlementDetails.paymentMode}
onChange={(e) => setSettlementDetails({ ...settlementDetails, paymentMode: e.target.value })}
/>
</div>
<div>
<Label htmlFor="settlementDate">
Settlement Date <span className="text-red-500">*</span>
</Label>
<Input
id="settlementDate"
type="date"
value={settlementDetails.settlementDate}
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementDate: e.target.value })}
/>
</div>
<div>
<Label htmlFor="verificationTxnId">
Transaction ID / Reference <span className="text-red-500">*</span>
</Label>
<Input
id="verificationTxnId"
placeholder="Enter transaction reference"
value={settlementDetails.verificationTransactionId}
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationTransactionId: e.target.value })}
/>
</div>
<div>
<Label htmlFor="verificationRemarks">Verification Remarks</Label>
<Textarea
id="verificationRemarks"
placeholder="Enter any remarks or notes..."
rows={4}
value={settlementDetails.verificationRemarks}
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationRemarks: e.target.value })}
/>
</div>
<div>
<Label htmlFor="bankReference">
Bank Reference Number
</Label>
<Input
id="bankReference"
placeholder="Enter bank reference"
value={settlementDetails.bankReference}
onChange={(e) => setSettlementDetails({ ...settlementDetails, bankReference: e.target.value })}
/>
</div>
<div className="pt-4 space-y-3 border-t">
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={handleApproveSettlement}
>
<CheckCircle className="w-4 h-4 mr-2" />
Approve Settlement
</Button>
<Button
variant="outline"
className="w-full border-blue-300 text-blue-600 hover:bg-blue-50"
onClick={handleRequestClarification}
>
<Send className="w-4 h-4 mr-2" />
Request Clarification
</Button>
<div>
<Label htmlFor="settlementAmount">
Settlement Amount () <span className="text-red-500">*</span>
</Label>
<Input
id="settlementAmount"
type="number"
placeholder="Enter settlement amount"
value={settlementDetails.settlementAmount}
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementAmount: e.target.value })}
/>
</div>
<Button
variant="outline"
className="w-full border-red-300 text-red-600 hover:bg-red-50"
onClick={handleRejectSettlement}
>
<XCircle className="w-4 h-4 mr-2" />
Reject Settlement
</Button>
</div>
<div>
<Label htmlFor="adjustments">
Adjustments ()
</Label>
<Input
id="adjustments"
type="number"
placeholder="Enter any adjustments"
value={settlementDetails.adjustments}
onChange={(e) => {
const adjustments = e.target.value;
const adjustedAmount = settlement.settlementAmount + parseFloat(adjustments || '0');
setSettlementDetails({
...settlementDetails,
adjustments,
settlementAmount: adjustedAmount.toString()
});
}}
/>
{parseFloat(settlementDetails.adjustments) !== 0 && (
<p className="text-sm text-amber-600 mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Adjusted amount: {settlementDetails.settlementAmount}
</p>
)}
</div>
<div>
<Label htmlFor="settlementDate">
Settlement Date <span className="text-red-500">*</span>
</Label>
<Input
id="settlementDate"
type="date"
value={settlementDetails.settlementDate}
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementDate: e.target.value })}
/>
</div>
<div>
<Label htmlFor="verificationRemarks">Verification Remarks</Label>
<Textarea
id="verificationRemarks"
placeholder="Enter any remarks or notes..."
rows={4}
value={settlementDetails.verificationRemarks}
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationRemarks: e.target.value })}
/>
</div>
<div className="pt-4 space-y-3 border-t">
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={handleApproveSettlement}
disabled={submitting || checklist.length < SETTLEMENT_CHECKLIST.length}
>
{submitting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<CheckCircle className="w-4 h-4 mr-2" />
)}
Complete Settlement
</Button>
{checklist.length < SETTLEMENT_CHECKLIST.length && (
<p className="text-[10px] text-center text-red-500 mt-2 italic">
Check all compliance items to enable settlement
</p>
)}
<Button
variant="outline"
className="w-full border-blue-300 text-amber-600 hover:bg-blue-50"
onClick={handleRequestClarification}
disabled={submitting}
>
<Send className="w-4 h-4 mr-2" />
Request Clarification
</Button>
<Button
variant="outline"
className="w-full border-red-300 text-red-600 hover:bg-red-50"
onClick={handleRejectSettlement}
disabled={submitting}
>
<XCircle className="w-4 h-4 mr-2" />
Reject Settlement
</Button>
</div>
</>
)}
</CardContent>
</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,84 +158,236 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
return (
<div className="space-y-6">
<div className="flex items-center mb-4">
<button
onClick={onBack}
className="mr-3 p-1.5 rounded-full hover:bg-slate-200 text-slate-600 transition-colors"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-slate-900 text-2xl font-bold mb-1">Application Details</h1>
<div className="flex items-center gap-2">
<p className="text-slate-600 font-medium">
{details.applicationId || 'Loading...'}
</p>
{details.districtId ? (
<span className="text-[10px] bg-green-100 text-green-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Opportunity</span>
) : (
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Future Reference</span>
)}
<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"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-slate-900 text-2xl font-bold mb-1">Application Details</h1>
<div className="flex items-center gap-2">
<p className="text-slate-600 font-medium">
{details.applicationId || 'Loading...'}
</p>
{details.districtId ? (
<span className="text-[10px] bg-green-100 text-green-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Opportunity</span>
) : (
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Future Reference</span>
)}
</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>
<div>
<p className="text-sm text-slate-500 mb-1">Current Stage</p>
<p className="font-medium text-slate-900">{details.currentStage || '-'}</p>
</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>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Applied Date</p>
<p className="font-medium text-slate-900">
{details.createdAt ? formatDateTime(details.createdAt) : '-'}
</p>
<div 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>
{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 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-[10px] text-slate-500 uppercase font-bold">Applicant</p>
<p className="text-sm font-semibold text-slate-900">{details.applicantName}</p>
<p className="text-xs text-slate-600">{details.email} | {details.phone}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="p-2 bg-amber-50 rounded-lg">
<MapPin className="w-4 h-4 text-amber-600" />
</div>
<div>
<p className="text-[10px] text-slate-500 uppercase font-bold">Proposed Location</p>
<p className="text-sm font-semibold text-slate-900">{details.city}, {details.state}</p>
<p className="text-xs text-slate-600">{details.preferredLocation || 'Standard Area'}</p>
</div>
</div>
</div>
)}
<div 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 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-[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="w-full bg-slate-100 rounded-full h-2.5">
<div className="bg-amber-500 h-2.5 rounded-full transition-all" style={{ width: `${details.progressPercentage || 0}%` }}></div>
<div className="bg-slate-50 rounded-xl p-4 flex flex-col justify-center border border-slate-100">
<div className="flex justify-between items-center mb-2">
<p className="text-xs font-bold text-slate-700 uppercase tracking-tight">Onboarding Progress</p>
<p className="text-xs font-black text-amber-600">{details.progressPercentage || 0}%</p>
</div>
<div className="w-full bg-slate-200 rounded-full h-2 shadow-inner">
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000 ease-out shadow-sm" style={{ width: `${details.progressPercentage || 0}%` }}></div>
</div>
{details.statusHistory?.[0]?.changeReason && (
<p className="mt-3 text-[11px] text-slate-600 italic leading-relaxed border-l-2 border-amber-300 pl-2">
"{details.statusHistory[0].changeReason}"
</p>
)}
</div>
</div>
</div>
<div 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);
toast.success(`Successfully updated clearance for ${selectedDept}`);
setShowClearanceDialog(false);
setClearanceFile(null);
fetchResignation();
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,56 +257,58 @@ 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' && (
<>
<Button
size="sm"
disabled={isSubmitting}
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all"
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.canApprove && (
<Button
size="sm"
disabled={isSubmitting}
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
)}
{permissions.canSendBack && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
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 font-bold"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
)}
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
</div>
{/* Secondary Actions */}
{currentUser?.role !== 'Dealer' && (
<div className="flex items-center gap-2">
{canPushToFnF && resignationData?.status !== 'FNF_INITIATED' && resignationData?.status !== 'Settled' && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-blue-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>
)}
<div className="flex items-center gap-2">
{permissions.canPushToFnF && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
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,8 +319,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
Assign User
</Button>
</div>
)}
)}
</div>
</div>
{/* Work Notes Button - Independent Section */}
@ -353,7 +412,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div>
<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,14 +838,35 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</Select>
</div>
) : actionDialog.type === 'pushfnf' ? (
<div className="space-y-2">
<Label>Remarks (Optional)</Label>
<Textarea
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
placeholder="Add any additional notes..."
rows={3}
/>
<div 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
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
placeholder="Add any additional notes..."
rows={3}
/>
</div>
</div>
) : (
<div className="space-y-2">
@ -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,15 +386,19 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
{currentUser?.role !== 'Dealer' && (
<>
<Button
size="sm"
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
<Check className="w-4 h-4 mr-2" />
Approve
</Button>
{request.currentStage === 'NBH' && (currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && (
{!permissions.canFinalize && (
<>
{permissions.canApprove && (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
<Check className="w-4 h-4 mr-2" />
Approve
</Button>
)}
{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,32 +421,36 @@ 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') && (
{permissions.canApprove && (
<Button
size="sm"
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
onClick={() => setShowFinalizeDialog(true)}
variant="outline"
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('sendback')}
>
<ShieldCheck className="w-4 h-4 mr-2" />
Final Authorization
<RotateCcw className="w-4 h-4 mr-2" />
Send Back
</Button>
)}
<Button
size="sm"
variant="outline"
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('sendback')}
>
<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,15 +461,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
Push to F&F
</Button>
)}
<Button
size="sm"
variant="outline"
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('assign')}
>
<UserPlus className="w-4 h-4 mr-2" />
Assign User
</Button>
{!permissions.isFinalState && (
<Button
size="sm"
variant="outline"
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('assign')}
>
<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;