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

View File

@ -50,6 +50,7 @@ import {
Info, Info,
ShieldAlert, ShieldAlert,
CheckCircle2, CheckCircle2,
CreditCard,
} from 'lucide-react'; } from 'lucide-react';
import { Progress } from '../ui/progress'; import { Progress } from '../ui/progress';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';
@ -356,6 +357,14 @@ export const ApplicationDetails = () => {
constitutionType: data.constitutionType, constitutionType: data.constitutionType,
architectureStatus: data.architectureStatus, architectureStatus: data.architectureStatus,
statutoryStatus: data.statutoryStatus, statutoryStatus: data.statutoryStatus,
panNumber: data.panNumber,
gstNumber: data.gstNumber,
bankName: data.bankName,
accountNumber: data.accountNumber,
ifscCode: data.ifscCode,
branchName: data.branchName,
accountHolderName: data.accountHolderName,
registeredAddress: data.registeredAddress,
}; };
setApplication(mappedApp); setApplication(mappedApp);
if (data.uploadedDocuments) { if (data.uploadedDocuments) {
@ -477,6 +486,50 @@ export const ApplicationDetails = () => {
const [previewDoc, setPreviewDoc] = useState<any>(null); const [previewDoc, setPreviewDoc] = useState<any>(null);
const [showPreviewModal, setShowPreviewModal] = useState(false); const [showPreviewModal, setShowPreviewModal] = useState(false);
const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>(''); const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>('');
const [isEditingStatutory, setIsEditingStatutory] = useState(false);
const [statutoryForm, setStatutoryForm] = useState({
accountHolderName: '',
panNumber: '',
gstNumber: '',
bankName: '',
accountNumber: '',
ifscCode: '',
registeredAddress: ''
});
const handleEditStatutory = () => {
if (!application) return;
setStatutoryForm({
accountHolderName: application.accountHolderName || '',
panNumber: application.panNumber || '',
gstNumber: application.gstNumber || '',
bankName: application.bankName || '',
accountNumber: application.accountNumber || '',
ifscCode: application.ifscCode || '',
registeredAddress: application.registeredAddress || ''
});
setIsEditingStatutory(true);
};
const [isSavingStatutory, setIsSavingStatutory] = useState(false);
const handleSaveStatutory = async () => {
try {
setIsSavingStatutory(true);
await onboardingService.updateApplication(applicationId!, statutoryForm);
toast.success('Statutory & Bank details updated successfully');
setIsEditingStatutory(false);
fetchApplication(true);
} catch (error) {
console.error('Failed to update statutory details', error);
toast.error('Failed to update details');
} finally {
setIsSavingStatutory(false);
}
};
const canEditStatutory = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin';
const isAdmin = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin';
const [interviews, setInterviews] = useState<any[]>([]); const [interviews, setInterviews] = useState<any[]>([]);
const [isScheduling, setIsScheduling] = useState(false); const [isScheduling, setIsScheduling] = useState(false);
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false); const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
@ -1672,75 +1725,72 @@ export const ApplicationDetails = () => {
currentUserEvaluation?.decision === 'Rejected' || currentUserEvaluation?.decision === 'Rejected' ||
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || ''); ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
// Final visibility flags // Centralized Permissions Utility (Consolidates 500 lines of fragmented logic)
const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role); const getApplicationPermissions = () => {
const isAdministrativeStage = [ if (!application || !currentUser) {
'Level 3 Approved', 'FDD Verification', return { canApprove: false, canReject: false, canSchedule: false, canAssign: false, isLoaLocked: false, showDecisionMessage: false };
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration'
].includes(application.status);
const finalDepositVerified = getDeposit('FIRST_FILL')?.status === 'Verified';
const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified;
// Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5)
// Approval Chain: DD-Head -> NBH
// Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5)
// Approval Chain: DD-Head -> NBH
const ddHeadApproved = application.stageApprovals?.some(
(a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'
);
// Sequential Enforcement for LOA APPROVAL (SRS 6.18.3.1)
// Approval Chain: DD-Head -> NBH
const ddHeadLoaApproved = application.stageApprovals?.some(
(a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'
);
let sequenceMet = true;
if (!['Super Admin', 'DD Admin'].includes(currentUser?.role || '')) {
// FDD Stage Restriction: Only DD Admin or Super Admin can approve
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') {
sequenceMet = false;
} }
// LOI Sequence Enforcement // 1. Core Flags
if (application.status === 'LOI In Progress') { const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
if (currentUser?.role === 'NBH') { const isAdministrativeStage = [
sequenceMet = !!ddHeadApproved; // NBH can only approve after DD Head 'Level 3 Approved', 'FDD Verification',
} 'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
// Roles not in the sequence (like Admin or Finance) should not see the buttons for LOI issuance decision 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
if (!['DD Head', 'NBH'].includes(currentUser?.role || '')) { 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
sequenceMet = false; 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
} 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
} 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration'
].includes(application.status);
// LOA Sequence Enforcement const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
if (application.status === 'LOA Pending') { const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected' || application.status === 'Approved';
if (currentUser?.role === 'NBH') {
sequenceMet = !!ddHeadLoaApproved; // NBH can only approve after DD Head
}
// Roles not in the sequence (like Finance or FDD) should not see the buttons for LOA decision
if (!['DD Head', 'NBH'].includes(currentUser?.role || '')) {
sequenceMet = false;
}
}
}
// Show Approve/Reject if: // 2. Interview Specific Logic
// 1. It's an interview and feedback is submitted AND no decision made yet const activeInterviewForUser = (interviews || []).find(i =>
// 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet AND sequence is valid ['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
const shouldShowApproveReject = i.participants?.some((p: any) => p.userId === currentUser?.id)
!isLoaLocked && ( );
(!hasMadeDecisionForUser && !!hasSubmittedFeedbackForActive) || const hasSubmittedFeedback = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
(!!isAdmin && !!isAdministrativeStage && !hasMadeStageDecision && !!sequenceMet) (e: any) => e.evaluatorId === currentUser?.id
); );
const shouldShowDecisionMessage = !!hasMadeDecisionForUser && (!isAdministrativeStage || !!hasMadeStageDecision); // 3. Sequential Sequence Check
const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
let sequenceMet = true;
if (!['Super Admin', 'DD Admin'].includes(currentUser.role)) {
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') sequenceMet = false;
if (application.status === 'LOI In Progress') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadApproved : (currentUser.role === 'DD Head');
if (application.status === 'LOA Pending') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadLoaApproved : (currentUser.role === 'DD Head');
}
// 4. Decision Tracking
const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id));
const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || '');
const hasMadeDecisionTotal = hasMadeStageDecision || hasMadeInterviewDecision;
// 5. Final Permission Bits
const isDecisionMade = hasMadeDecisionTotal || hasMadeStageDecision;
const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && (
(!!activeInterviewForUser && !!hasSubmittedFeedback) ||
(isAdminRole && isAdministrativeStage && sequenceMet)
);
return {
canApprove: canApproveReject,
canReject: canApproveReject,
isLoaLocked,
showDecisionMessage: isDecisionMade && (!isAdministrativeStage || hasMadeStageDecision),
canSchedule: ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
!isFinalState &&
!([1, 2, 3].every(level => (interviews || []).some(i => i.level === level))),
canAssign: ['DD Admin', 'Super Admin', 'DD AM'].includes(currentUser.role)
};
};
const permissions = getApplicationPermissions();
@ -2045,8 +2095,7 @@ export const ApplicationDetails = () => {
<FileText className="w-6 h-6 text-red-500" /> <FileText className="w-6 h-6 text-red-500" />
</div> </div>
<div className="overflow-hidden"> <div className="overflow-hidden">
<p className="text-slate-900 font-bold text-sm truncate max-w-[180px]">{report.reportDocument.fileName}</p> <p className="text-slate-500 text-[10px] font-medium">SUBMITTED {formatDateTime(report.createdAt)}</p>
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {new Date(report.createdAt).toLocaleDateString()}</p>
</div> </div>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
@ -2387,6 +2436,141 @@ export const ApplicationDetails = () => {
<p className="text-slate-600 mb-2">Past Experience</p> <p className="text-slate-600 mb-2">Past Experience</p>
<p className="text-slate-900">{application.pastExperience || 'N/A'}</p> <p className="text-slate-900">{application.pastExperience || 'N/A'}</p>
</div> </div>
<div className="pt-6 border-t mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-black text-slate-900 uppercase tracking-widest flex items-center gap-2">
<CreditCard className="w-4 h-4 text-amber-600" /> Statutory & Bank Information
</h3>
{canEditStatutory && !isEditingStatutory && (
<Button
variant="ghost"
size="sm"
onClick={handleEditStatutory}
className="h-8 text-amber-600 hover:text-amber-700 hover:bg-amber-50 gap-1.5"
>
<Pencil className="w-3.5 h-3.5" />
Edit Details
</Button>
)}
</div>
{isEditingStatutory ? (
<div className="bg-slate-50/50 p-6 rounded-xl border-2 border-amber-100 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Legal Entity Name</Label>
<Input
value={statutoryForm.accountHolderName}
onChange={(e) => setStatutoryForm({...statutoryForm, accountHolderName: e.target.value})}
placeholder="Enter Legal Entity Name"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">PAN Number</Label>
<Input
value={statutoryForm.panNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, panNumber: e.target.value.toUpperCase()})}
placeholder="10-digit PAN"
maxLength={10}
className="bg-white border-slate-200 uppercase"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">GST Number</Label>
<Input
value={statutoryForm.gstNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, gstNumber: e.target.value.toUpperCase()})}
placeholder="15-digit GSTIN"
maxLength={15}
className="bg-white border-slate-200 uppercase"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Registered Address</Label>
<Input
value={statutoryForm.registeredAddress}
onChange={(e) => setStatutoryForm({...statutoryForm, registeredAddress: e.target.value})}
placeholder="Enter Registered Office Address"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Bank Name</Label>
<Input
value={statutoryForm.bankName}
onChange={(e) => setStatutoryForm({...statutoryForm, bankName: e.target.value})}
placeholder="Enter Bank Name"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Account Number</Label>
<Input
value={statutoryForm.accountNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, accountNumber: e.target.value})}
placeholder="Enter Account Number"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">IFSC Code</Label>
<Input
value={statutoryForm.ifscCode}
onChange={(e) => setStatutoryForm({...statutoryForm, ifscCode: e.target.value.toUpperCase()})}
placeholder="11-digit IFSC"
maxLength={11}
className="bg-white border-slate-200 uppercase"
/>
</div>
</div>
<div className="flex justify-end gap-3 pt-4 border-t">
<Button
variant="outline"
size="sm"
onClick={() => setIsEditingStatutory(false)}
disabled={isSavingStatutory}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSaveStatutory}
disabled={isSavingStatutory}
className="bg-amber-600 hover:bg-amber-700"
>
{isSavingStatutory ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Save Details'}
</Button>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 bg-slate-50/50 p-4 rounded-xl border border-slate-100">
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Legal Entity Name</p>
<p className="text-xs font-semibold text-slate-900">{application.accountHolderName || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">PAN Number</p>
<p className="text-xs font-semibold text-slate-900 uppercase">{application.panNumber || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">GST Number</p>
<p className="text-xs font-semibold text-slate-900 uppercase">{application.gstNumber || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Registered Address</p>
<p className="text-xs font-semibold text-slate-900">{application.registeredAddress || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Bank Details</p>
<p className="text-xs font-semibold text-slate-900">{application.bankName || 'N/A'}</p>
<p className="text-[10px] text-slate-600">A/C: {application.accountNumber || 'N/A'}</p>
<p className="text-[10px] text-slate-600">IFSC: {application.ifscCode || 'N/A'}</p>
</div>
</div>
)}
</div>
</CardContent> </CardContent>
</Card> </Card>
@ -2679,38 +2863,7 @@ export const ApplicationDetails = () => {
</div> </div>
</button> </button>
{/* Branch Approval Button */}
{(() => {
const branchConfig = branchColor === 'blue'
? { status: application.architectureStatus, role: 'ARCHITECTURE', stageCode: 'ARCHITECTURE_WORK' }
: { status: application.statutoryStatus, role: 'Legal Admin', stageCode: 'STATUTORY_WORK' };
if (branchConfig.status !== 'COMPLETED' && (currentUser?.roleCode === branchConfig.role || currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin')) {
return (
<Button
size="sm"
className={cn(
"h-14 px-6 font-bold shadow-lg border-b-4 active:border-b-0 active:translate-y-1 transition-all",
branchColor === 'blue' ? "bg-blue-600 hover:bg-blue-700 border-blue-800" : "bg-green-600 hover:bg-green-700 border-green-800"
)}
onClick={() => {
onboardingService.submitStageDecision({
applicationId: application.id,
stageCode: branchConfig.stageCode,
decision: 'Approved',
remarks: `Consolidated approval for ${branch.name} track`
}).then(() => {
toast.success(`${branch.name} approved successfully`);
fetchApplication();
});
}}
>
Approve Track
</Button>
);
}
return null;
})()}
</div> </div>
{isExpanded && ( {isExpanded && (
@ -2826,7 +2979,7 @@ export const ApplicationDetails = () => {
<span className="truncate max-w-[150px] md:max-w-[300px]">{doc.fileName}</span> <span className="truncate max-w-[150px] md:max-w-[300px]">{doc.fileName}</span>
</TableCell> </TableCell>
<TableCell>{doc.documentType}</TableCell> <TableCell>{doc.documentType}</TableCell>
<TableCell>{new Date(doc.createdAt).toLocaleDateString()}</TableCell> <TableCell>{formatDateTime(doc.createdAt)}</TableCell>
<TableCell> <TableCell>
{doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')} {doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')}
</TableCell> </TableCell>
@ -2919,7 +3072,7 @@ export const ApplicationDetails = () => {
<h4 className="font-semibold text-slate-800 mb-2"> <h4 className="font-semibold text-slate-800 mb-2">
Level {interview.level} Interview Level {interview.level} Interview
<span className="font-normal text-slate-500 text-sm ml-2"> <span className="font-normal text-slate-500 text-sm ml-2">
({new Date(interview.scheduleDate).toLocaleDateString()} - {interview.interviewType}) ({formatDateTime(interview.scheduleDate)} - {interview.interviewType})
</span> </span>
</h4> </h4>
{interview.evaluations && interview.evaluations.length > 0 ? ( {interview.evaluations && interview.evaluations.length > 0 ? (
@ -3204,7 +3357,7 @@ export const ApplicationDetails = () => {
{deposit?.paymentReference && ( {deposit?.paymentReference && (
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center"> <div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
<span>Ref: {deposit.paymentReference}</span> <span>Ref: {deposit.paymentReference}</span>
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>} {deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
</div> </div>
)} )}
@ -3287,7 +3440,7 @@ export const ApplicationDetails = () => {
{deposit?.paymentReference && ( {deposit?.paymentReference && (
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center"> <div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
<span>Ref: {deposit.paymentReference}</span> <span>Ref: {deposit.paymentReference}</span>
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>} {deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
</div> </div>
)} )}
@ -3352,7 +3505,7 @@ export const ApplicationDetails = () => {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<p className="text-slate-900 font-medium">{log.description || log.action}</p> <p className="text-slate-900 font-medium">{log.description || log.action}</p>
<span className="text-slate-500 text-sm whitespace-nowrap ml-4"> <span className="text-slate-500 text-sm whitespace-nowrap ml-4">
{new Date(log.timestamp).toLocaleString()} {formatDateTime(log.timestamp)}
</span> </span>
</div> </div>
<p className="text-slate-600 mt-1">by {log.userName || 'System'}</p> <p className="text-slate-600 mt-1">by {log.userName || 'System'}</p>
@ -3418,7 +3571,7 @@ export const ApplicationDetails = () => {
{application.deadline && ( {application.deadline && (
<div> <div>
<p className="text-slate-600">Questionnaire Deadline</p> <p className="text-slate-600">Questionnaire Deadline</p>
<p className="text-slate-900">{new Date(application.deadline).toLocaleDateString()}</p> <p className="text-slate-900">{formatDateTime(application.deadline)}</p>
</div> </div>
)} )}
</CardContent> </CardContent>
@ -3434,7 +3587,7 @@ export const ApplicationDetails = () => {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{/* Show Approve/Reject block */} {/* Show Approve/Reject block */}
{isLoaLocked && ( {permissions.isLoaLocked && (
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800"> <Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
<Lock className="w-4 h-4 text-amber-600" /> <Lock className="w-4 h-4 text-amber-600" />
<AlertTitle className="text-amber-900 font-semibold">Stage Locked</AlertTitle> <AlertTitle className="text-amber-900 font-semibold">Stage Locked</AlertTitle>
@ -3444,10 +3597,10 @@ export const ApplicationDetails = () => {
</Alert> </Alert>
)} )}
{shouldShowApproveReject && ( {permissions.canApprove && (
<> <>
<Button <Button
className="w-full bg-green-600 hover:bg-green-700" className="w-full bg-green-600 hover:bg-green-700 font-bold"
onClick={() => setShowApproveModal(true)} onClick={() => setShowApproveModal(true)}
> >
<CheckCircle className="w-4 h-4 mr-2" /> <CheckCircle className="w-4 h-4 mr-2" />
@ -3456,7 +3609,7 @@ export const ApplicationDetails = () => {
<Button <Button
variant="destructive" variant="destructive"
className="w-full" className="w-full font-bold"
onClick={() => setShowRejectModal(true)} onClick={() => setShowRejectModal(true)}
> >
<XCircle className="w-4 h-4 mr-2" /> <XCircle className="w-4 h-4 mr-2" />
@ -3465,7 +3618,7 @@ export const ApplicationDetails = () => {
</> </>
)} )}
{(shouldShowDecisionMessage) && ( {permissions.showDecisionMessage && (
<div className={`w-full p-2 text-center rounded border ${(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'}`}> <div className={`w-full p-2 text-center rounded border ${(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'}`}>
You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'} You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
</div> </div>
@ -3488,17 +3641,16 @@ export const ApplicationDetails = () => {
Work Note Work Note
</Button> </Button>
{currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) && {permissions.canSchedule && (
!([1, 2, 3].every(level => interviews.some(i => i.level === level))) && ( <Button
<Button variant="outline"
variant="outline" className="w-full"
className="w-full" onClick={() => setShowScheduleModal(true)}
onClick={() => setShowScheduleModal(true)} >
> <Calendar className="w-4 h-4 mr-2" />
<Calendar className="w-4 h-4 mr-2" /> Schedule Interview
Schedule Interview </Button>
</Button> )}
)}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && ( {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && (
<> <>
@ -4639,7 +4791,7 @@ export const ApplicationDetails = () => {
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="py-3 whitespace-nowrap text-slate-600"> <TableCell className="py-3 whitespace-nowrap text-slate-600">
{new Date(doc.createdAt).toLocaleDateString('en-GB')} {formatDateTime(doc.createdAt)}
</TableCell> </TableCell>
<TableCell className="py-3 text-slate-600"> <TableCell className="py-3 text-slate-600">
{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')} {doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}

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'; return 'bg-slate-100 text-slate-700 border-slate-300';
}; };
export function ConstitutionalChangeDetails({ requestId, onBack }: ConstitutionalChangeDetailsProps) { export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ConstitutionalChangeDetailsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [isActionDialogOpen, setIsActionDialogOpen] = useState(false); const [isActionDialogOpen, setIsActionDialogOpen] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve'); const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold'>('approve');
@ -152,6 +152,41 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
const currentStageIndex = getCurrentStageIndex(); const currentStageIndex = getCurrentStageIndex();
// Centralized Permissions Utility (Fixes security gap where buttons showed for everyone)
const getConstitutionalPermissions = () => {
if (!request || !currentUser) {
return { canApprove: false, canReject: false, canHold: false };
}
const currentStage = request.currentStage;
const status = request.status;
const userRole = currentUser.role;
const isFinalState = ['Completed', 'Rejected', 'Hold'].includes(status);
// Find stage definition
const stageDef = workflowStages.find(s => s.name === currentStage || s.key === currentStage);
// Role matching logic (Handles Role names from constants vs workflow mapping)
const isCurrentlyAssigned = currentUser.roleCode === 'SUPER_ADMIN' || (
(stageDef?.role === 'ASM' && userRole === 'ASM') ||
(stageDef?.role === 'ZM/RBM' && (userRole === 'ZM' || userRole === 'RBM')) ||
(stageDef?.role === 'ZBH' && userRole === 'ZBH') ||
(stageDef?.role === 'DD Lead' && userRole === 'DD Lead') ||
(stageDef?.role === 'DD Head' && userRole === 'DD Head') ||
(stageDef?.role === 'NBH' && userRole === 'NBH') ||
(stageDef?.role === 'Legal Team' && (userRole === 'Legal' || userRole === 'Legal Admin'))
);
return {
canApprove: isCurrentlyAssigned && !isFinalState,
canReject: isCurrentlyAssigned && !isFinalState,
canHold: isCurrentlyAssigned && !isFinalState
};
};
const permissions = getConstitutionalPermissions();
const handleAction = (type: 'approve' | 'reject' | 'hold') => { const handleAction = (type: 'approve' | 'reject' | 'hold') => {
setActionType(type); setActionType(type);
setIsActionDialogOpen(true); setIsActionDialogOpen(true);
@ -203,7 +238,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
<div> <div>
<h1 className="text-slate-900">{request.requestId} - Constitutional Change Details</h1> <h1 className="text-slate-900">{request.requestId} - Constitutional Change Details</h1>
<p className="text-slate-600"> <p className="text-slate-600">
{request.outlet?.name || 'N/A'} ({request.outlet?.code || 'N/A'}) {request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'} ({request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'})
</p> </p>
</div> </div>
</div> </div>
@ -221,9 +256,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div>
<p className="text-slate-600 text-sm mb-1">Dealer Details</p> <p className="text-slate-600 text-sm mb-1">Dealer Details</p>
<p className="text-slate-900">{request.outlet?.name || 'N/A'}</p> <p className="text-slate-900">{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
<p className="text-slate-600 text-sm">{request.outlet?.code || 'N/A'}</p> <p className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</p>
<p className="text-slate-600 text-sm">{request.outlet?.city || request.outlet?.address || 'N/A'}</p> <p className="text-slate-600 text-sm">{request.dealer?.dealerProfile?.registeredAddress || request.outlet?.city || request.outlet?.address || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p> <p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
@ -605,32 +640,44 @@ export function ConstitutionalChangeDetails({ requestId, onBack }: Constitutiona
<CardTitle>Actions</CardTitle> <CardTitle>Actions</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<Button {permissions.canApprove && (
className="w-full bg-green-600 hover:bg-green-700" <Button
onClick={() => handleAction('approve')} className="w-full bg-green-600 hover:bg-green-700"
disabled={isActionLoading} onClick={() => handleAction('approve')}
> disabled={isActionLoading}
{isActionLoading && actionType === 'approve' ? ( >
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> {isActionLoading && actionType === 'approve' ? (
) : ( <Loader2 className="w-4 h-4 mr-2 animate-spin" />
<CheckCircle2 className="w-4 h-4 mr-2" /> ) : (
)} <CheckCircle2 className="w-4 h-4 mr-2" />
Approve Request )}
</Button> Approve Request
</Button>
)}
<Button {permissions.canReject && (
variant="destructive" <Button
className="w-full" variant="destructive"
onClick={() => handleAction('reject')} className="w-full"
disabled={isActionLoading} onClick={() => handleAction('reject')}
> disabled={isActionLoading}
{isActionLoading && actionType === 'reject' ? ( >
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> {isActionLoading && actionType === 'reject' ? (
) : ( <Loader2 className="w-4 h-4 mr-2 animate-spin" />
<AlertCircle className="w-4 h-4 mr-2" /> ) : (
)} <AlertCircle className="w-4 h-4 mr-2" />
Reject Request )}
</Button> Reject Request
</Button>
)}
{!permissions.canApprove && !permissions.canReject && (
<div className="text-center py-4 bg-slate-50 rounded-lg border border-dashed border-slate-200">
<p className="text-slate-500 text-xs px-4">
{permissions.isFinalState ? 'This request is finalized.' : 'No actions available for your role at this stage.'}
</p>
</div>
)}
<div className="border-t border-slate-200 pt-3 mt-3"> <div className="border-t border-slate-200 pt-3 mt-3">
<Button <Button

View File

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

View File

@ -34,6 +34,7 @@ import { toast } from 'sonner';
import { onboardingService } from '../../services/onboarding.service'; import { onboardingService } from '../../services/onboarding.service';
import { worknoteService } from '../../services/worknote.service'; import { worknoteService } from '../../services/worknote.service';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
// Simple helper for class merging // Simple helper for class merging
const cn = (...classes: any[]) => classes.filter(Boolean).join(' '); const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
@ -264,7 +265,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div> <div>
<p className="text-sm font-bold text-slate-800">{docType.label}</p> <p className="text-sm font-bold text-slate-800">{docType.label}</p>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter"> <p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">
{doc ? `Uploaded: ${new Date(doc.createdAt).toLocaleDateString()}` : 'Missing in Documentation'} {doc ? `Uploaded: ${formatDateTime(doc.createdAt)}` : 'Missing in Documentation'}
</p> </p>
</div> </div>
</div> </div>
@ -371,7 +372,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</div> </div>
<div className="overflow-hidden"> <div className="overflow-hidden">
<p className="text-slate-900 font-black text-sm truncate max-w-[140px] uppercase">{report.reportDocument.fileName}</p> <p className="text-slate-900 font-black text-sm truncate max-w-[140px] uppercase">{report.reportDocument.fileName}</p>
<p className="text-slate-500 text-[10px] font-bold">SUBMITTED {new Date(report.createdAt).toLocaleDateString()}</p> <p className="text-slate-500 text-[10px] font-bold">SUBMITTED {formatDateTime(report.createdAt)}</p>
</div> </div>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
@ -556,7 +557,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-sm font-black text-slate-900">{note.author?.fullName || 'System'}</h4> <h4 className="text-sm font-black text-slate-900">{note.author?.fullName || 'System'}</h4>
<span className="text-[10px] font-bold text-slate-400 uppercase">{new Date(note.createdAt).toLocaleString()}</span> <span className="text-[10px] font-bold text-slate-400 uppercase">{formatDateTime(note.createdAt)}</span>
</div> </div>
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">{note.author?.roleCode || 'RE Stakeholder'}</p> <p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">{note.author?.roleCode || 'RE Stakeholder'}</p>
<div className="mt-4 p-4 bg-white rounded-xl border border-slate-100 text-sm text-slate-700 leading-relaxed shadow-sm"> <div className="mt-4 p-4 bg-white rounded-xl border border-slate-100 text-sm text-slate-700 leading-relaxed shadow-sm">
@ -614,7 +615,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div> <div>
<p className="text-sm font-bold text-slate-900">{step.stageName}</p> <p className="text-sm font-bold text-slate-900">{step.stageName}</p>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest"> <p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">
{step.stageCompletedAt ? `Completed ${new Date(step.stageCompletedAt).toLocaleDateString()}` : 'Pending'} {step.stageCompletedAt ? `Completed ${formatDateTime(step.stageCompletedAt)}` : 'Pending'}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { settlementService } from '../../services/settlement.service';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
@ -31,15 +32,27 @@ import {
Edit2, Edit2,
Trash2, Trash2,
Save, Save,
Paperclip Paperclip,
FileDown
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime, formatDateOnly } from '../../lib/dateUtils';
import { BankDetailsModal } from './BankDetailsModal';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
const ALL_DEPARTMENTS = [ // Will be updated from API
'Warranty', 'Accessories', 'Sales', 'RTO', 'Service', 'Parts', let ALL_DEPARTMENTS = [
'Finance', 'Insurance', 'Inventory', 'Marketing', 'HR', 'IT', 'Sales', 'Service', 'Spares / Parts', 'Finance', 'Accounts', 'Warranty',
'Legal', 'Quality', 'Logistics', 'Customer Relations' 'Marketing', 'HR', 'IT', 'Legal', 'Logistics', 'Quality', 'FDD', 'Apparel',
'DMS', 'Admin / DD-Admin'
]; ];
interface FinanceFnFDetailsPageProps { interface FinanceFnFDetailsPageProps {
@ -50,38 +63,13 @@ interface FinanceFnFDetailsPageProps {
// Removing mock data functions as we use live API // Removing mock data functions as we use live API
const getDepartmentStatusColor = (status: string) => { const SETTLEMENT_CHECKLIST = [
switch (status) { { id: 'calculations', label: 'Verified All Department Calculations' },
case 'NOC Submitted': { id: 'bank', label: 'Confirmed Bank Account Details' },
return 'bg-green-100 text-green-700 border-green-300'; { id: 'docs', label: 'Reviewed All Supporting Documents' },
case 'Dues Pending': { id: 'sap', label: 'Synced Final Dues with SAP' },
return 'bg-red-100 text-red-700 border-red-300'; { id: 'noc', label: 'Received All Mandatory NOCs' }
case 'Completed': ];
return 'bg-green-100 text-green-700 border-green-300';
case 'Pending':
return 'bg-slate-100 text-slate-700 border-slate-300';
default:
return 'bg-slate-100 text-slate-700 border-slate-300';
}
};
const formatDateTime = (dateString: any) => {
if (!dateString) return '-';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return '-';
return date.toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
} catch (e) {
return '-';
}
};
interface FinancialLineItem { interface FinancialLineItem {
id: string; id: string;
@ -99,11 +87,28 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]); const [receivableItems, setReceivableItems] = useState<FinancialLineItem[]>([]);
const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]); const [deductionItems, setDeductionItems] = useState<FinancialLineItem[]>([]);
const [previewDocument, setPreviewDocument] = useState<any>(null); const [previewDocument, setPreviewDocument] = useState<any>(null);
const [bankDetails, setBankDetails] = useState<any[]>([]);
const [isBankModalOpen, setIsBankModalOpen] = useState(false);
const [editingBank, setEditingBank] = useState<any>(null);
const [checklist, setChecklist] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
fetchDepartments();
fetchFnFDetails(); fetchFnFDetails();
}, [fnfId]); }, [fnfId]);
const fetchDepartments = async () => {
try {
const response = await API.getSettlementDepartments();
const data = response.data as any;
if (data && data.success) {
ALL_DEPARTMENTS = data.departments;
}
} catch (error) {
console.error("Fetch departments error:", error);
}
};
const fetchFnFDetails = async () => { const fetchFnFDetails = async () => {
try { try {
setLoading(true); setLoading(true);
@ -111,23 +116,23 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const data = response.data as any; const data = response.data as any;
if (data.success) { if (data.success) {
const s = data.fnf; const s = data.fnf;
setFnfCase({ const mappedCase = {
id: s.id, id: s.id,
caseNumber: s.id.substring(0, 8).toUpperCase(), caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
dealerName: s.outlet?.dealer?.fullName || s.outlet?.name || 'N/A', dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || 'N/A',
dealerCode: s.outlet?.code || 'N/A', dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A', location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination', terminationType: s.resignationId ? 'Resignation' : 'Termination',
submittedDate: new Date(s.createdAt).toLocaleDateString(), submittedDate: formatDateTime(s.createdAt),
dueDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : 'TBD', createdAt: s.createdAt,
status: s.status, dueDate: s.settlementDate ? formatDateTime(s.settlementDate) : 'TBD',
bankDetails: { status: s.status,
accountName: s.outlet?.dealer?.fullName || 'N/A', dealerId: s.outlet?.dealer?.id || s.dealerId,
accountNumber: 'N/A', // These should come from dealer model in a real app originalRequestId: s.resignation?.resignationId || s.terminationRequest?.requestId || s.terminationRequest?.id || "N/A",
ifscCode: 'N/A', salesCode: s.dealer?.dealerCode?.salesCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A',
bankName: 'N/A', serviceCode: s.dealer?.dealerCode?.serviceCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.serviceCode || 'N/A',
branch: 'N/A' gearCode: s.dealer?.dealerCode?.gearCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gearCode || 'N/A',
}, gmaCode: s.dealer?.dealerCode?.gmaCode || s.outlet?.dealer?.dealerProfile?.dealerCode?.gmaCode || 'N/A',
departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => { departmentResponses: ALL_DEPARTMENTS.map((deptName: string) => {
const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName); const c = (s.clearances || []).find((clearance: any) => clearance.department === deptName);
const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName); const relatedItems = (s.lineItems || []).filter((li: any) => li.department === deptName);
@ -157,7 +162,17 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
url: c.supportingDocument url: c.supportingDocument
})) }))
] ]
}); };
setFnfCase(mappedCase);
// Sync bank details from the pre-fetched data inside the settlement object
const preFetchedBankDetails = s.bankDetails || s.dealer?.bankDetails || s.outlet?.dealer?.dealerProfile?.bankDetails;
if (preFetchedBankDetails && preFetchedBankDetails.length > 0) {
setBankDetails(preFetchedBankDetails);
} else if (s.outlet?.dealer?.id || s.dealerId) {
fetchBankDetails(s.outlet?.dealer?.id || s.dealerId);
}
// Split line items into categories // Split line items into categories
const pItems: FinancialLineItem[] = []; const pItems: FinancialLineItem[] = [];
@ -172,21 +187,29 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
amount: Math.abs(li.amount) amount: Math.abs(li.amount)
}; };
if (li.amount < 0) { if (li.itemType === 'Payable') {
pItems.push(item); pItems.push(item);
} else if (li.itemType === 'Deduction') {
dItems.push(item);
} else { } else {
// Check if it's a deduction (usually Warranty related in this UI) rItems.push(item);
if (li.department.toLowerCase().includes('warranty')) {
dItems.push(item);
} else {
rItems.push(item);
}
} }
}); });
setPayableItems(pItems); setPayableItems(pItems);
setReceivableItems(rItems); setReceivableItems(rItems);
setDeductionItems(dItems); setDeductionItems(dItems);
// Populate settlement details from backend
setSettlementDetails({
verificationTransactionId: s.transactionReference || '',
settlementAmount: (s.settlementAmount || calculateDynamicSettlement().settlementAmount).toString(),
settlementDate: s.settlementDate ? new Date(s.settlementDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
paymentMode: s.paymentMode || '',
bankReference: '', // Optional field
verificationRemarks: s.remarks || '',
adjustments: '0'
});
} }
} catch (error) { } catch (error) {
console.error('Fetch F&F error:', error); console.error('Fetch F&F error:', error);
@ -196,6 +219,61 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
} }
}; };
const fetchBankDetails = async (dealerId: string) => {
try {
const response = await API.getDealerBankDetails(dealerId);
const data = response.data as any;
if (data.success) {
setBankDetails(data.bankDetails || []);
}
} catch (error) {
console.error('Fetch bank details error:', error);
}
};
const handleUpsertBank = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData.entries());
try {
const dealerId = fnfCase?.dealerId;
const response = await API.saveBankDetail(dealerId, {
...data,
id: editingBank?.id,
isPrimary: formData.get('isPrimary') === 'on'
}) as any;
if (response.data.success) {
toast.success('Bank details saved');
fetchBankDetails(dealerId);
setIsBankModalOpen(false);
setEditingBank(null);
}
} catch (error) {
toast.error('Failed to save bank details');
}
};
const handleDeleteBank = async (id: string) => {
if (!confirm('Are you sure you want to delete this bank account?')) return;
try {
const response = await API.deleteBankDetail(id) as any;
if (response.data.success) {
toast.success('Bank detail deleted');
fetchBankDetails(fnfCase?.dealerId);
}
} catch (error) {
toast.error('Failed to delete bank details');
}
};
const toggleChecklist = (id: string) => {
setChecklist(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
// Form states for adding new items // Form states for adding new items
const [newPayable, setNewPayable] = useState({ department: '', description: '', amount: '' }); const [newPayable, setNewPayable] = useState({ department: '', description: '', amount: '' });
const [newReceivable, setNewReceivable] = useState({ department: '', description: '', amount: '' }); const [newReceivable, setNewReceivable] = useState({ department: '', description: '', amount: '' });
@ -225,6 +303,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const settlement = calculateDynamicSettlement(); const settlement = calculateDynamicSettlement();
// Get primary bank for display
const primaryBank = bankDetails.find(b => b.isPrimary) || bankDetails[0];
const [settlementDetails, setSettlementDetails] = useState({ const [settlementDetails, setSettlementDetails] = useState({
verificationTransactionId: '', verificationTransactionId: '',
settlementAmount: settlement.settlementAmount.toString(), settlementAmount: settlement.settlementAmount.toString(),
@ -248,7 +329,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const response = await API.addLineItem(fnfId, { const response = await API.addLineItem(fnfId, {
department: newPayable.department, department: newPayable.department,
remarks: newPayable.description, remarks: newPayable.description,
amount: amount amount: Math.abs(parseFloat(newPayable.amount)),
itemType: 'Payable'
}); });
const data = response.data as any; const data = response.data as any;
if (data.success) { if (data.success) {
@ -312,7 +394,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const response = await API.addLineItem(fnfId, { const response = await API.addLineItem(fnfId, {
department: newReceivable.department, department: newReceivable.department,
remarks: newReceivable.description, remarks: newReceivable.description,
amount: Math.abs(parseFloat(newReceivable.amount)) // Receivable is positive amount: Math.abs(parseFloat(newReceivable.amount)),
itemType: 'Receivable'
}); });
const data = response.data as any; const data = response.data as any;
if (data.success) { if (data.success) {
@ -371,7 +454,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const response = await API.addLineItem(fnfId, { const response = await API.addLineItem(fnfId, {
department: newDeduction.department, department: newDeduction.department,
remarks: newDeduction.description, remarks: newDeduction.description,
amount: Math.abs(parseFloat(newDeduction.amount)) // Deductions are positive (act as receivables) amount: Math.abs(parseFloat(newDeduction.amount)),
itemType: 'Deduction'
}); });
const data = response.data as any; const data = response.data as any;
if (data.success) { if (data.success) {
@ -426,7 +510,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
const newDocs = Array.from(files).map(file => ({ const newDocs = Array.from(files).map(file => ({
name: file.name, name: file.name,
size: `${(file.size / 1024).toFixed(0)} KB`, size: `${(file.size / 1024).toFixed(0)} KB`,
uploadedOn: new Date().toISOString().split('T')[0], uploadedOn: new Date().toISOString(),
type: 'Settlement Verification' type: 'Settlement Verification'
})); }));
setUploadedDocuments([...uploadedDocuments, ...newDocs]); setUploadedDocuments([...uploadedDocuments, ...newDocs]);
@ -434,19 +518,35 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
} }
}; };
const handleApproveSettlement = () => { const [submitting, setSubmitting] = useState(false);
const handleApproveSettlement = async () => {
if (!settlementDetails.verificationTransactionId || !settlementDetails.settlementDate || !settlementDetails.paymentMode) { if (!settlementDetails.verificationTransactionId || !settlementDetails.settlementDate || !settlementDetails.paymentMode) {
toast.error('Please fill in all required settlement details'); toast.error('Please fill in all required settlement details');
return; return;
} }
const adjustedAmount = settlement.settlementAmount + parseFloat(settlementDetails.adjustments || '0'); try {
if (adjustedAmount.toString() !== settlementDetails.settlementAmount) { setSubmitting(true);
toast.warning('Settlement amount has been adjusted'); const adjustedAmount = (settlement.settlementAmount || 0) + parseFloat(settlementDetails.adjustments || '0');
}
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}`); toast.success(`F&F Settlement approved and completed for ${fnfCase.dealerName}`);
setTimeout(() => onBack(), 1500); setTimeout(() => onBack(), 1500);
} catch (error: any) {
console.error('Approve settlement error:', error);
toast.error(error.message || 'Failed to approve settlement');
} finally {
setSubmitting(false);
}
}; };
const handleRejectSettlement = () => { const handleRejectSettlement = () => {
@ -636,7 +736,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<Label className="text-slate-500">Request Age</Label> <Label className="text-slate-500">Request Age</Label>
<p className="text-slate-900"> <p className="text-slate-900">
{(() => { {(() => {
const submitted = new Date(fnfCase.submittedDate); const submitted = new Date(fnfCase.createdAt);
const today = new Date(); const today = new Date();
const diffTime = Math.abs(today.getTime() - submitted.getTime()); const diffTime = Math.abs(today.getTime() - submitted.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
@ -646,19 +746,19 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
<div> <div>
<Label className="text-slate-500">Sales Code</Label> <Label className="text-slate-500">Sales Code</Label>
<p className="text-slate-900">SAL-{fnfCase.dealerCode}</p> <p className="text-slate-900">{fnfCase.salesCode}</p>
</div> </div>
<div> <div>
<Label className="text-slate-500">Service Code</Label> <Label className="text-slate-500">Service Code</Label>
<p className="text-slate-900">SRV-{fnfCase.dealerCode}</p> <p className="text-slate-900">{fnfCase.serviceCode}</p>
</div> </div>
<div> <div>
<Label className="text-slate-500">Gear Code</Label> <Label className="text-slate-500">Gear Code</Label>
<p className="text-slate-900">GER-{fnfCase.dealerCode}</p> <p className="text-slate-900">{fnfCase.gearCode}</p>
</div> </div>
<div> <div>
<Label className="text-slate-500">GMA Code</Label> <Label className="text-slate-500">GMA Code</Label>
<p className="text-slate-900">GMA-{fnfCase.dealerCode}</p> <p className="text-slate-900">{fnfCase.gmaCode}</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -718,8 +818,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
</div> </div>
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-lg"> <div className="flex items-start gap-3 p-4 bg-blue-50 border border-amber-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div> <div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p> <p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600">
@ -1160,7 +1260,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<Card className="border-2 border-blue-300 bg-blue-50"> <Card className="border-2 border-blue-300 bg-blue-50">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-blue-600" /> <CheckCircle className="w-5 h-5 text-amber-600" />
Final Settlement Summary Final Settlement Summary
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -1210,8 +1310,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
</div> </div>
<div className="flex items-start gap-3 p-4 bg-white border border-blue-200 rounded-lg"> <div className="flex items-start gap-3 p-4 bg-white border border-amber-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div> <div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p> <p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600">
@ -1292,7 +1392,11 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<TableRow key={dept.id}> <TableRow key={dept.id}>
<TableCell>{dept.departmentName}</TableCell> <TableCell>{dept.departmentName}</TableCell>
<TableCell> <TableCell>
<Badge className={getDepartmentStatusColor(dept.status)}> <Badge className={`border ${
dept.status === 'NOC Submitted' ? 'bg-green-100 text-green-700 border-green-300' :
dept.status === 'Dues Pending' ? 'bg-red-100 text-red-700 border-red-300' :
'bg-slate-100 text-slate-700 border-slate-300'
}`}>
{dept.status} {dept.status}
</Badge> </Badge>
</TableCell> </TableCell>
@ -1330,7 +1434,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
filePath: dept.supportingDocument, filePath: dept.supportingDocument,
documentType: 'Departmental Clearance Proof' documentType: 'Departmental Clearance Proof'
})} })}
className="flex items-center gap-1 text-[10px] text-blue-600 hover:underline" className="flex items-center gap-1 text-[10px] text-amber-600 hover:underline"
> >
<Paperclip className="w-3 h-3" /> <Paperclip className="w-3 h-3" />
View Proof View Proof
@ -1346,10 +1450,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card> </Card>
{/* Important Notes */} {/* Important Notes */}
<Card className="bg-blue-50 border-blue-200"> <Card className="bg-blue-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div> <div>
<p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p> <p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p>
<ul className="text-sm text-slate-700 space-y-1"> <ul className="text-sm text-slate-700 space-y-1">
@ -1363,6 +1467,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="documents" className="space-y-4"> <TabsContent value="documents" className="space-y-4">
{/* Submitted Documents */} {/* Submitted Documents */}
<Card> <Card>
@ -1446,51 +1551,93 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</TabsContent> </TabsContent>
<TabsContent value="bank" className="space-y-4"> <TabsContent value="bank" className="space-y-4">
{/* Bank Account Details */}
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2"> <div>
<Building className="w-5 h-5" /> <CardTitle className="flex items-center gap-2">
Dealer Bank Account Details <Building className="w-5 h-5" />
</CardTitle> Dealer Bank Account Details
<CardDescription> </CardTitle>
Bank account for settlement transfer (if payable to dealer) <CardDescription>
</CardDescription> Manage bank accounts for settlement transfer
</CardHeader> </CardDescription>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-500">Account Holder Name</Label>
<p className="text-slate-900">{fnfCase.bankDetails.accountName}</p>
</div>
<div>
<Label className="text-slate-500">Account Number</Label>
<p className="text-slate-900">{fnfCase.bankDetails.accountNumber}</p>
</div>
<div>
<Label className="text-slate-500">IFSC Code</Label>
<p className="text-slate-900">{fnfCase.bankDetails.ifscCode}</p>
</div>
<div>
<Label className="text-slate-500">Bank Name</Label>
<p className="text-slate-900">{fnfCase.bankDetails.bankName}</p>
</div>
<div>
<Label className="text-slate-500">Branch</Label>
<p className="text-slate-900">{fnfCase.bankDetails.branch}</p>
</div>
</div> </div>
<Button
size="sm"
className="bg-amber-600"
onClick={() => {
setEditingBank(null);
setIsBankModalOpen(true);
}}
>
<Plus className="w-4 h-4 mr-2" />
Add Bank Account
</Button>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{bankDetails.length > 0 ? (
bankDetails.map((bank: any) => (
<Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-amber-500 bg-blue-50/30' : ''}`}>
{bank.isPrimary && (
<div className="absolute top-0 right-0 p-1 bg-amber-600 text-white text-[10px] uppercase font-bold px-2 rounded-bl">
Primary
</div>
)}
<CardContent className="p-4 pt-6">
<div className="space-y-3">
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Holder</Label>
<p className="text-sm font-semibold">{bank.accountHolderName}</p>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Bank</Label>
<p className="text-xs truncate">{bank.bankName}</p>
</div>
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">IFSC</Label>
<p className="text-xs">{bank.ifscCode}</p>
</div>
</div>
<div>
<Label className="text-[10px] text-slate-500 uppercase font-bold">Account Number</Label>
<p className="text-xs font-mono">{bank.accountNumber}</p>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg"> <div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<div className="flex items-start gap-3"> <Button
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" /> variant="ghost"
<div> size="sm"
<p className="text-sm text-slate-900 mb-1">Bank Verification Required</p> className="h-7 text-[11px] text-amber-600"
<p className="text-sm text-slate-600"> onClick={() => {
Please verify bank account details before processing settlement payment setEditingBank(bank);
</p> setIsBankModalOpen(true);
}}
>
<Edit2 className="w-3 h-3 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 text-[11px] text-red-600"
onClick={() => handleDeleteBank(bank.id)}
>
<Trash2 className="w-3 h-3 mr-1" />
Delete
</Button>
</div>
</div>
</CardContent>
</Card>
))
) : (
<div className="col-span-full py-12 text-center border-2 border-dashed rounded-lg bg-slate-50">
<Building className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-600 text-sm">No bank details found</p>
</div> </div>
</div> )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -1511,168 +1658,236 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div> {fnfCase.status === 'Completed' ? (
<Label htmlFor="paymentMode"> <div className="space-y-6">
Payment Mode <span className="text-red-500">*</span> <div className="p-4 bg-green-50 border border-green-200 rounded-lg">
</Label> <div className="flex items-center gap-3 text-green-700 mb-2">
<Input <CheckCircle className="w-5 h-5" />
id="paymentMode" <span className="font-semibold">Settlement Completed</span>
placeholder="e.g., NEFT, RTGS, Cheque" </div>
value={settlementDetails.paymentMode} <p className="text-sm text-green-600">
onChange={(e) => setSettlementDetails({ ...settlementDetails, paymentMode: e.target.value })} This settlement has been finalized and processed.
/> </p>
</div> </div>
<div> <div className="space-y-3">
<Label htmlFor="verificationTxnId"> <div className="flex justify-between items-center py-2 border-b">
Transaction ID / Reference <span className="text-red-500">*</span> <span className="text-slate-500 text-sm">Settlement Date</span>
</Label> <span className="text-slate-900 font-medium">{formatDateTime(settlementDetails.settlementDate)}</span>
<Input </div>
id="verificationTxnId" <div className="flex justify-between items-center py-2 border-b">
placeholder="Enter transaction reference" <span className="text-slate-500 text-sm">Payment Mode</span>
value={settlementDetails.verificationTransactionId} <span className="text-slate-900 font-medium">{settlementDetails.paymentMode}</span>
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationTransactionId: e.target.value })} </div>
/> <div className="flex justify-between items-center py-2 border-b">
</div> <span className="text-slate-500 text-sm">Transaction ID</span>
<span className="text-slate-900 font-medium truncate ml-4 max-w-[150px]" title={settlementDetails.verificationTransactionId}>
{settlementDetails.verificationTransactionId}
</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-slate-500 text-sm">Final Amount</span>
<span className="text-slate-900 font-bold text-lg">{parseFloat(settlementDetails.settlementAmount).toLocaleString()}</span>
</div>
</div>
<div> {settlementDetails.verificationRemarks && (
<Label htmlFor="bankReference"> <div className="mt-4">
Bank Reference Number <Label className="text-slate-500 mb-1 block">Finance Remarks</Label>
</Label> <div className="p-3 bg-slate-50 rounded border text-sm text-slate-700">
<Input {settlementDetails.verificationRemarks}
id="bankReference" </div>
placeholder="Enter bank reference" </div>
value={settlementDetails.bankReference} )}
onChange={(e) => setSettlementDetails({ ...settlementDetails, bankReference: e.target.value })}
/>
</div>
<div> <Button variant="outline" className="w-full mt-4" onClick={() => window.print()}>
<Label htmlFor="settlementAmount"> <FileDown className="w-4 h-4 mr-2" />
Settlement Amount () <span className="text-red-500">*</span> Download Settlement Letter
</Label> </Button>
<Input </div>
id="settlementAmount" ) : (
type="number" <>
placeholder="Enter settlement amount" {/* Settlement Checklist */}
value={settlementDetails.settlementAmount} <div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4">
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementAmount: e.target.value })} <p className="text-sm font-bold text-slate-900 mb-3 flex items-center gap-2">
/> <CheckCircle className="w-4 h-4 text-amber-600" />
</div> Compliance Checklist
</p>
<div className="space-y-3">
{SETTLEMENT_CHECKLIST.map(item => (
<div key={item.id} className="flex items-start gap-3">
<input
type="checkbox"
id={`check-${item.id}`}
checked={checklist.includes(item.id)}
onChange={() => toggleChecklist(item.id)}
className="w-4 h-4 mt-1 rounded border-slate-300 text-amber-600 focus:ring-amber-500"
/>
<label htmlFor={`check-${item.id}`} className="text-sm text-slate-700 leading-tight">
{item.label}
</label>
</div>
))}
</div>
</div>
<div> <div>
<Label htmlFor="adjustments"> <Label htmlFor="paymentMode">
Adjustments () Payment Mode <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="adjustments" id="paymentMode"
type="number" placeholder="e.g., NEFT, RTGS, Cheque"
placeholder="Enter any adjustments" value={settlementDetails.paymentMode}
value={settlementDetails.adjustments} onChange={(e) => setSettlementDetails({ ...settlementDetails, paymentMode: e.target.value })}
onChange={(e) => { />
const adjustments = e.target.value; </div>
const adjustedAmount = settlement.settlementAmount + parseFloat(adjustments || '0');
setSettlementDetails({
...settlementDetails,
adjustments,
settlementAmount: adjustedAmount.toString()
});
}}
/>
{parseFloat(settlementDetails.adjustments) !== 0 && (
<p className="text-sm text-amber-600 mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Adjusted amount: {settlementDetails.settlementAmount}
</p>
)}
</div>
<div> <div>
<Label htmlFor="settlementDate"> <Label htmlFor="verificationTxnId">
Settlement Date <span className="text-red-500">*</span> Transaction ID / Reference <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="settlementDate" id="verificationTxnId"
type="date" placeholder="Enter transaction reference"
value={settlementDetails.settlementDate} value={settlementDetails.verificationTransactionId}
onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementDate: e.target.value })} onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationTransactionId: e.target.value })}
/> />
</div> </div>
<div> <div>
<Label htmlFor="verificationRemarks">Verification Remarks</Label> <Label htmlFor="bankReference">
<Textarea Bank Reference Number
id="verificationRemarks" </Label>
placeholder="Enter any remarks or notes..." <Input
rows={4} id="bankReference"
value={settlementDetails.verificationRemarks} placeholder="Enter bank reference"
onChange={(e) => setSettlementDetails({ ...settlementDetails, verificationRemarks: e.target.value })} value={settlementDetails.bankReference}
/> onChange={(e) => setSettlementDetails({ ...settlementDetails, bankReference: e.target.value })}
</div> />
</div>
<div className="pt-4 space-y-3 border-t"> <div>
<Button <Label htmlFor="settlementAmount">
className="w-full bg-green-600 hover:bg-green-700" Settlement Amount () <span className="text-red-500">*</span>
onClick={handleApproveSettlement} </Label>
> <Input
<CheckCircle className="w-4 h-4 mr-2" /> id="settlementAmount"
Approve Settlement type="number"
</Button> placeholder="Enter settlement amount"
value={settlementDetails.settlementAmount}
<Button onChange={(e) => setSettlementDetails({ ...settlementDetails, settlementAmount: e.target.value })}
variant="outline" />
className="w-full border-blue-300 text-blue-600 hover:bg-blue-50" </div>
onClick={handleRequestClarification}
>
<Send className="w-4 h-4 mr-2" />
Request Clarification
</Button>
<Button <div>
variant="outline" <Label htmlFor="adjustments">
className="w-full border-red-300 text-red-600 hover:bg-red-50" Adjustments ()
onClick={handleRejectSettlement} </Label>
> <Input
<XCircle className="w-4 h-4 mr-2" /> id="adjustments"
Reject Settlement type="number"
</Button> placeholder="Enter any adjustments"
</div> 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> </CardContent>
</Card> </Card>
{/* Quick Info Card */}
<Card className="bg-blue-50 border-blue-200">
<CardHeader>
<CardTitle className="text-base">Settlement Checklist</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-slate-700">
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
<span>Verify all financial calculations</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
<span>Confirm bank account details</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
<span>Review all submitted documents</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
<span>Upload settlement proof documents</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
<span>Enter transaction details accurately</span>
</li>
</ul>
</CardContent>
</Card>
</div> </div>
</div> </div>
<BankDetailsModal
isOpen={isBankModalOpen}
onClose={() => {
setIsBankModalOpen(false);
setEditingBank(null);
}}
onSubmit={handleUpsertBank}
editingBank={editingBank}
isSubmitting={false}
/>
<DocumentPreviewModal <DocumentPreviewModal
isOpen={!!previewDocument} isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)} onClose={() => setPreviewDocument(null)}

View File

@ -37,6 +37,7 @@ import {
TrendingDown, TrendingDown,
MapPin MapPin
} from 'lucide-react'; } from 'lucide-react';
import { formatDateTime } from '../ui/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
// Using live data from API instead of mockFnFCases // Using live data from API instead of mockFnFCases
@ -76,12 +77,12 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
const getMappedData = (s: any) => ({ const getMappedData = (s: any) => ({
id: s.id, id: s.id,
caseId: s.resignation?.resignationId || s.id, caseId: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8),
dealerCode: s.outlet?.code || 'N/A', dealerCode: s.outlet?.code || s.dealer?.dealerCode?.dealerCode || 'N/A',
dealerName: s.outlet?.dealer?.name || 'N/A', dealerName: s.outlet?.dealer?.fullName || s.dealer?.fullName || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A', location: s.outlet?.city || s.outlet?.location || 'N/A',
terminationType: s.resignationId ? 'Resignation' : 'Termination', terminationType: s.resignationId ? 'Resignation' : 'Termination',
submittedDate: new Date(s.createdAt).toLocaleDateString(), submittedDate: formatDateTime(s.createdAt),
status: s.status === 'Calculated' ? 'Pending Finance Review' : (s.status === 'Settled' ? 'Settled' : s.status), status: s.status === 'Calculated' ? 'Pending Finance Review' : (s.status === 'Settled' ? 'Settled' : s.status),
financialData: { financialData: {
totalPayables: parseFloat(s.totalPayables) || 0, totalPayables: parseFloat(s.totalPayables) || 0,
@ -90,7 +91,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
}, },
settlementAmount: Math.abs(parseFloat(s.netAmount) || 0), settlementAmount: Math.abs(parseFloat(s.netAmount) || 0),
settlementType: parseFloat(s.netAmount) > 0 ? 'Payable to Dealer' : 'Receivable from Dealer', settlementType: parseFloat(s.netAmount) > 0 ? 'Payable to Dealer' : 'Receivable from Dealer',
approvedDate: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : null approvedDate: s.settlementDate ? formatDateTime(s.settlementDate) : null
}); });
const displaySettlements = settlements.map(getMappedData); const displaySettlements = settlements.map(getMappedData);

View File

@ -336,7 +336,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</div> </div>
<div> <div>
<p className="text-slate-900 font-medium">{doc.fileName || doc.name}</p> <p className="text-slate-900 font-medium">{doc.fileName || doc.name}</p>
<p className="text-xs text-slate-500 uppercase">{doc.documentType} {new Date(doc.createdAt).toLocaleDateString()}</p> <p className="text-xs text-slate-500 uppercase">{doc.documentType} {formatDateTime(doc.createdAt)}</p>
</div> </div>
</div> </div>
<Button <Button

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { User } from '../../lib/mock-data'; import { User } from '../../lib/mock-data';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { formatDateTime } from '../ui/utils';
interface FnFPageProps { interface FnFPageProps {
currentUser: User | null; currentUser: User | null;
@ -85,11 +86,11 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
dealershipName: s.outlet?.name || 'N/A', dealershipName: s.outlet?.name || 'N/A',
location: s.outlet?.city || s.outlet?.location || 'N/A', location: s.outlet?.city || s.outlet?.location || 'N/A',
originalRequestId: s.resignation?.resignationId || s.terminationRequest?.id || 'N/A', originalRequestId: s.resignation?.resignationId || s.terminationRequest?.id || 'N/A',
submittedOn: new Date(s.createdAt).toLocaleDateString(), submittedOn: formatDateTime(s.createdAt),
financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending', financeReportStatus: s.status === 'Calculated' || s.status === 'Settled' ? 'Completed' : 'Pending',
totalRecoveryAmount: parseFloat(s.totalReceivables) || 0, totalRecoveryAmount: parseFloat(s.totalReceivables) || 0,
totalPayableAmount: parseFloat(s.totalPayables) || 0, totalPayableAmount: parseFloat(s.totalPayables) || 0,
completedOn: s.settlementDate ? new Date(s.settlementDate).toLocaleDateString() : null, completedOn: s.settlementDate ? formatDateTime(s.settlementDate) : null,
departmentResponses: s.lineItems || [] departmentResponses: s.lineItems || []
}); });

View File

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

View File

@ -6,6 +6,7 @@ import { Badge } from '../../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react'; import { MapPin, Plus, Edit2, Trash2, Globe } from 'lucide-react';
import { RootState } from '../../../store'; import { RootState } from '../../../store';
import { formatDateTime } from '../../ui/utils';
interface LocationManagementProps { interface LocationManagementProps {
onAddLocation: () => void; onAddLocation: () => void;
@ -85,13 +86,13 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<span className="text-slate-600">From:</span> <span className="text-slate-600">From:</span>
<Badge variant="outline" className="text-xs font-medium"> <Badge variant="outline" className="text-xs font-medium">
{new Date(district.openFrom).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} {formatDateTime(district.openFrom)}
</Badge> </Badge>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<span className="text-slate-600">To:</span> <span className="text-slate-600">To:</span>
<Badge variant="outline" className="text-xs font-medium"> <Badge variant="outline" className="text-xs font-medium">
{new Date(district.openTo).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} {formatDateTime(district.openTo)}
</Badge> </Badge>
</div> </div>
</div> </div>

View File

@ -26,6 +26,7 @@ import {
TableRow, TableRow,
} from '../ui/table'; } from '../ui/table';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { formatDateTime } from '../ui/utils';
interface NonOpportunitiesPageProps { interface NonOpportunitiesPageProps {
onViewDetails: (id: string) => void; onViewDetails: (id: string) => void;
@ -230,7 +231,7 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
<TableCell className="text-slate-600">{lead.pastExperience}</TableCell> <TableCell className="text-slate-600">{lead.pastExperience}</TableCell>
<TableCell className="text-slate-900">{lead.education}</TableCell> <TableCell className="text-slate-900">{lead.education}</TableCell>
<TableCell className="text-slate-600"> <TableCell className="text-slate-600">
{new Date(lead.submissionDate).toLocaleDateString()} {formatDateTime(lead.submissionDate)}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <Button

View File

@ -4,7 +4,14 @@ import {
Upload, Upload,
Clock, Clock,
RefreshCw, RefreshCw,
File File,
CreditCard,
Building,
Landmark,
CheckCircle2,
Info,
User,
MapPin
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '../../api/API'; import { API } from '../../api/API';
@ -22,6 +29,19 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
const [selectedDocType, setSelectedDocType] = useState(''); const [selectedDocType, setSelectedDocType] = useState('');
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// Statutory & Bank State
const [form, setForm] = useState({
panNumber: '',
gstNumber: '',
registeredAddress: '',
bankName: '',
accountNumber: '',
ifscCode: '',
branchName: '',
accountHolderName: ''
});
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@ -36,7 +56,18 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
]); ]);
if (detailsRes.data?.success) { if (detailsRes.data?.success) {
setDetails(detailsRes.data.data); const data = detailsRes.data.data;
setDetails(data);
setForm({
panNumber: data.panNumber || '',
gstNumber: data.gstNumber || '',
registeredAddress: data.registeredAddress || data.address || '',
bankName: data.bankName || '',
accountNumber: data.accountNumber || '',
ifscCode: data.ifscCode || '',
branchName: data.branchName || '',
accountHolderName: data.accountHolderName || data.applicantName || ''
});
} }
if (docsRes.data?.success || docsRes.ok) { if (docsRes.data?.success || docsRes.ok) {
setDocuments(docsRes.data.data || []); setDocuments(docsRes.data.data || []);
@ -49,6 +80,24 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
} }
}; };
const handleSaveDetails = async () => {
setIsSaving(true);
try {
const response: any = await API.updateApplication(id, form);
if (response.data?.success || response.ok) {
toast.success('Business details updated successfully');
fetchData();
} else {
toast.error(response.data?.message || 'Update failed');
}
} catch (error) {
console.error('Save details error:', error);
toast.error('Failed to save details');
} finally {
setIsSaving(false);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]); setFile(e.target.files[0]);
@ -109,84 +158,236 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center mb-4"> <div className="flex items-center justify-between mb-4">
<button <div className="flex items-center">
onClick={onBack} <button
className="mr-3 p-1.5 rounded-full hover:bg-slate-200 text-slate-600 transition-colors" onClick={onBack}
> className="mr-3 p-1.5 rounded-full hover:bg-slate-200 text-slate-600 transition-colors"
<ChevronLeft className="w-5 h-5" /> >
</button> <ChevronLeft className="w-5 h-5" />
<div> </button>
<h1 className="text-slate-900 text-2xl font-bold mb-1">Application Details</h1> <div>
<div className="flex items-center gap-2"> <h1 className="text-slate-900 text-2xl font-bold mb-1">Application Details</h1>
<p className="text-slate-600 font-medium"> <div className="flex items-center gap-2">
{details.applicationId || 'Loading...'} <p className="text-slate-600 font-medium">
</p> {details.applicationId || 'Loading...'}
{details.districtId ? ( </p>
<span className="text-[10px] bg-green-100 text-green-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Opportunity</span> {details.districtId ? (
) : ( <span className="text-[10px] bg-green-100 text-green-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Opportunity</span>
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Future Reference</span> ) : (
)} <span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded uppercase tracking-wider">Future Reference</span>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="animate-in fade-in duration-500"> <div className="animate-in fade-in duration-500 space-y-6">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6 mb-6"> {/* Status & Tracking Summary Card */}
<h4 className="text-lg font-semibold text-slate-900 mb-4 border-b pb-2">Status & Tracking</h4> <div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="flex items-center justify-between mb-4 border-b pb-2">
<div> <h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<p className="text-sm text-slate-500 mb-1">Overall Status</p> <Info className="w-5 h-5 text-amber-600" /> Application Summary
<p className="font-medium text-slate-900">{details.overallStatus || '-'}</p> </h4>
</div> <div className="text-right">
<div> <p className="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Current Stage</p>
<p className="text-sm text-slate-500 mb-1">Current Stage</p> <span className="bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide">
<p className="font-medium text-slate-900">{details.currentStage || '-'}</p> {details.currentStage || details.overallStatus}
</div> </span>
<div>
<p className="text-sm text-slate-500 mb-1">Location</p>
<p className="font-medium text-slate-900">{details.city}, {details.state}</p>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Applied Date</p>
<p className="font-medium text-slate-900">
{details.createdAt ? formatDateTime(details.createdAt) : '-'}
</p>
</div> </div>
</div> </div>
{details.statusHistory?.[0]?.changeReason && ( <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="mt-4 p-3 bg-amber-50 border border-amber-100 rounded-lg"> <div className="space-y-4">
<p className="text-xs font-semibold text-amber-800 uppercase tracking-wider mb-1">Latest Feedback</p> <div className="flex items-start gap-3">
<p className="text-sm text-amber-900 italic">"{details.statusHistory[0].changeReason}"</p> <div className="p-2 bg-blue-50 rounded-lg">
<User className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="text-[10px] text-slate-500 uppercase font-bold">Applicant</p>
<p className="text-sm font-semibold text-slate-900">{details.applicantName}</p>
<p className="text-xs text-slate-600">{details.email} | {details.phone}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="p-2 bg-amber-50 rounded-lg">
<MapPin className="w-4 h-4 text-amber-600" />
</div>
<div>
<p className="text-[10px] text-slate-500 uppercase font-bold">Proposed Location</p>
<p className="text-sm font-semibold text-slate-900">{details.city}, {details.state}</p>
<p className="text-xs text-slate-600">{details.preferredLocation || 'Standard Area'}</p>
</div>
</div>
</div> </div>
)}
<div className="mt-6"> <div className="space-y-4">
<div className="flex justify-between items-center mb-1"> <div className="flex items-start gap-3">
<p className="text-sm font-medium text-slate-700">Application Progress</p> <div className="p-2 bg-green-50 rounded-lg">
<p className="text-sm font-medium text-amber-600">{details.progressPercentage || 0}%</p> <Building className="w-4 h-4 text-green-600" />
</div>
<div>
<p className="text-[10px] text-slate-500 uppercase font-bold">Business Concept</p>
<p className="text-sm font-semibold text-slate-900">{details.businessType} - {details.constitutionType}</p>
<p className="text-xs text-slate-600">Investment: {details.investmentCapacity}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="p-2 bg-slate-50 rounded-lg">
<Clock className="w-4 h-4 text-slate-600" />
</div>
<div>
<p className="text-[10px] text-slate-500 uppercase font-bold">Applied On</p>
<p className="text-sm font-semibold text-slate-900">
{details.createdAt ? new Date(details.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' }) : '-'}
</p>
</div>
</div>
</div> </div>
<div className="w-full bg-slate-100 rounded-full h-2.5">
<div className="bg-amber-500 h-2.5 rounded-full transition-all" style={{ width: `${details.progressPercentage || 0}%` }}></div> <div className="bg-slate-50 rounded-xl p-4 flex flex-col justify-center border border-slate-100">
<div className="flex justify-between items-center mb-2">
<p className="text-xs font-bold text-slate-700 uppercase tracking-tight">Onboarding Progress</p>
<p className="text-xs font-black text-amber-600">{details.progressPercentage || 0}%</p>
</div>
<div className="w-full bg-slate-200 rounded-full h-2 shadow-inner">
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000 ease-out shadow-sm" style={{ width: `${details.progressPercentage || 0}%` }}></div>
</div>
{details.statusHistory?.[0]?.changeReason && (
<p className="mt-3 text-[11px] text-slate-600 italic leading-relaxed border-l-2 border-amber-300 pl-2">
"{details.statusHistory[0].changeReason}"
</p>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm"> {/* Statutory & Bank Details Form */}
<div className="p-6 border-b border-slate-200"> <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<h4 className="flex items-center gap-2 text-lg font-semibold text-slate-900"> <div className="p-4 bg-slate-900 text-white flex justify-between items-center">
<Upload className="w-5 h-5 text-blue-600" /> Document Upload <h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest">
<CreditCard className="w-4 h-4 text-amber-400" /> Statutory & Bank Details
</h4>
<button
onClick={handleSaveDetails}
disabled={isSaving}
className="text-xs bg-amber-600 hover:bg-amber-700 text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50"
>
{isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />}
Save Business Info
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Business Name</label>
<input
type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
value={form.accountHolderName}
onChange={(e) => setForm({ ...form, accountHolderName: e.target.value })}
placeholder="As per legal documents"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Permanent Account Number (PAN)</label>
<input
type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
value={form.panNumber}
onChange={(e) => setForm({ ...form, panNumber: e.target.value.toUpperCase() })}
placeholder="ABCDE1234F"
maxLength={10}
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">GST Identification Number (GSTIN)</label>
<input
type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
value={form.gstNumber}
onChange={(e) => setForm({ ...form, gstNumber: e.target.value.toUpperCase() })}
placeholder="27ABCDE1234F1Z5"
maxLength={15}
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Office Address</label>
<textarea
rows={1}
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
value={form.registeredAddress}
onChange={(e) => setForm({ ...form, registeredAddress: e.target.value })}
placeholder="Full legal address"
/>
</div>
<div className="md:col-span-2 border-t pt-2 mt-2">
<h5 className="text-xs font-black text-slate-900 uppercase flex items-center gap-2 mb-3">
<Landmark className="w-3.5 h-3.5 text-blue-600" /> Primary Bank Information
</h5>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Bank Name</label>
<input
type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
value={form.bankName}
onChange={(e) => setForm({ ...form, bankName: e.target.value })}
placeholder="e.g. HDFC Bank"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Account Number</label>
<input
type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
value={form.accountNumber}
onChange={(e) => setForm({ ...form, accountNumber: e.target.value })}
placeholder="Bank account number"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">IFSC Code</label>
<input
type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase"
value={form.ifscCode}
onChange={(e) => setForm({ ...form, ifscCode: e.target.value.toUpperCase() })}
placeholder="HDFC0001234"
maxLength={11}
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Branch Name</label>
<input
type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all"
value={form.branchName}
onChange={(e) => setForm({ ...form, branchName: e.target.value })}
placeholder="e.g. South Mumbai"
/>
</div>
</div>
</div>
</div>
{/* Document Upload Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-slate-900">
<Upload className="w-4 h-4 text-blue-600" /> Required Documents
</h4> </h4>
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg border border-slate-200"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg border border-slate-100">
<div className="space-y-2"> <div className="space-y-1">
<label className="text-sm font-medium text-slate-900">Document Type</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
<select <select
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-amber-500"
value={selectedDocType} value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)} onChange={(e) => setSelectedDocType(e.target.value)}
disabled={isUploading} disabled={isUploading}
@ -213,46 +414,54 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<option value="Other">Other</option> <option value="Other">Other</option>
</select> </select>
</div> </div>
<div className="space-y-2"> <div className="space-y-1">
<label className="text-sm font-medium text-slate-900">File</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Select File</label>
<input <input
type="file" type="file"
id="file-upload" id="file-upload"
className="w-full text-sm" className="w-full text-xs text-slate-600 file:mr-4 file:py-1 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100"
onChange={handleFileChange} onChange={handleFileChange}
disabled={isUploading} disabled={isUploading}
/> />
</div> </div>
<div className="md:col-span-2 flex justify-end mt-2"> <div className="md:col-span-2 flex justify-end">
<button <button
onClick={handleUpload} onClick={handleUpload}
disabled={!file || !selectedDocType || isUploading} disabled={!file || !selectedDocType || isUploading}
className="bg-amber-600 text-white px-4 py-2 rounded-md hover:bg-amber-700 disabled:opacity-50 flex items-center gap-2" className="bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 text-xs font-bold transition-all shadow-sm flex items-center gap-2"
> >
{isUploading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />} {isUploading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
Upload Upload Document
</button> </button>
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-4">
<h3 className="font-medium text-slate-900">Uploaded Documents ({documents.length})</h3> <div className="flex items-center gap-2 text-slate-900 border-b pb-1">
<div className="space-y-2"> <File className="w-4 h-4 text-blue-600" />
<h3 className="text-xs font-black uppercase tracking-tighter">Uploaded Library ({documents.length})</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{documents.length > 0 ? documents.map((doc) => ( {documents.length > 0 ? documents.map((doc) => (
<div key={doc.id} className="flex justify-between items-center p-3 border border-slate-200 rounded-lg bg-white"> <div key={doc.id} className="flex justify-between items-center p-3 border border-slate-100 rounded-xl bg-slate-50 group hover:border-amber-200 transition-all">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<File className="w-5 h-5 text-blue-600" /> <div className="w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200 group-hover:bg-blue-50">
<File className="w-4 h-4 text-slate-400 group-hover:text-blue-600" />
</div>
<div> <div>
<p className="text-sm font-medium">{doc.documentType}</p> <p className="text-[11px] font-bold text-slate-900">{doc.documentType}</p>
<p className="text-xs text-slate-500">{doc.fileName}</p> <p className="text-[10px] text-slate-400 truncate w-32">{doc.fileName}</p>
</div> </div>
</div> </div>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold uppercase ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}> <span className={`text-[9px] px-2 py-0.5 rounded-full font-black uppercase tracking-tighter ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
{doc.status || 'Pending'} {doc.status || 'Pending'}
</span> </span>
</div> </div>
)) : ( )) : (
<p className="text-sm text-slate-500 italic text-center py-4 bg-slate-50 rounded-lg">No documents uploaded yet.</p> <div className="col-span-2 py-8 text-center bg-slate-50 rounded-xl border border-dashed border-slate-200">
<Info className="w-6 h-6 text-slate-300 mx-auto mb-2" />
<p className="text-xs text-slate-500 font-medium">No documents in your library yet</p>
</div>
)} )}
</div> </div>
</div> </div>
@ -260,31 +469,36 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
</div> </div>
</div> </div>
<div className="lg:col-span-1"> <div className="lg:col-span-1 space-y-6">
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden"> <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 bg-slate-50 border-b border-slate-200"> <div className="p-4 bg-slate-50 border-b border-slate-200">
<h3 className="text-sm font-bold text-slate-900 flex items-center gap-2 uppercase tracking-wide"> <h3 className="text-xs font-bold text-slate-900 flex items-center gap-2 uppercase tracking-widest">
<Clock className="w-4 h-4 text-amber-600" /> Timeline <Clock className="w-4 h-4 text-amber-600" /> Recent Updates
</h3> </h3>
</div> </div>
<div className="p-6"> <div className="p-6">
{details.statusHistory?.length > 0 ? ( {details.statusHistory?.length > 0 ? (
<div className="relative space-y-6"> <div className="relative space-y-6">
<div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-200"></div> <div className="absolute left-[11px] top-2 bottom-4 w-0.5 bg-slate-100"></div>
{[...details.statusHistory].reverse().map((item: any) => ( {[...details.statusHistory].reverse().map((item: any, idx: number) => (
<div key={item.id} className="relative pl-8"> <div key={item.id} className="relative pl-8 animate-in slide-in-from-left duration-300" style={{ animationDelay: `${idx * 100}ms` }}>
<div className="absolute left-0 top-1 w-[24px] h-[24px] rounded-full border-2 bg-white flex items-center justify-center border-green-500"> <div className="absolute left-0 top-1 w-[24px] h-[24px] rounded-full border-2 bg-white flex items-center justify-center border-amber-500 shadow-sm">
<div className="w-2 h-2 rounded-full bg-green-500"></div> <div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
</div> </div>
<div> <div>
<p className="text-sm font-semibold text-slate-900">{item.newStatus}</p> <p className="text-xs font-bold text-slate-900 uppercase tracking-tight">{item.newStatus}</p>
<p className="text-[11px] text-slate-500">{new Date(item.createdAt).toLocaleString()}</p> <p className="text-[10px] text-slate-400 font-medium">{new Date(item.createdAt).toLocaleString('en-IN')}</p>
{item.changeReason && (
<p className="text-[10px] text-slate-500 mt-1 italic leading-tight">"{item.changeReason}"</p>
)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<p className="text-sm text-slate-500 italic text-center">No history available</p> <div className="text-center py-6">
<p className="text-xs text-slate-500 italic">No history available yet</p>
</div>
)} )}
</div> </div>
</div> </div>

View File

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

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 { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
@ -13,7 +13,7 @@ import { useNavigate } from 'react-router-dom';
import { User as UserType } from '../../lib/mock-data'; import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { resignationService } from '../../services/resignation.service'; import { resignationService } from '../../services/resignation.service';
import { Loader2 } from 'lucide-react';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils'; import { formatDateTime } from '../ui/utils';
@ -30,11 +30,22 @@ interface ResignationDetailsProps {
currentUser: UserType | null; currentUser: UserType | null;
} }
const STAGE_TO_ROLE_MAP: Record<string, string> = {
'ASM': 'ASM',
'RBM': 'RBM',
'ZBH': 'ZBH',
'DD Lead': 'DD Lead',
'NBH': 'NBH',
'DD Admin': 'DD Admin',
'Legal': 'Legal Admin'
};
export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) { export function ResignationDetails({ resignationId, onBack, currentUser }: ResignationDetailsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null }); const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendback' | 'assign' | 'pushfnf' | null }>({ open: false, type: null });
const [remarks, setRemarks] = useState(''); const [remarks, setRemarks] = useState('');
const [assignToUser, setAssignToUser] = useState(''); const [assignToUser, setAssignToUser] = useState('');
const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] }); const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
const [showClearanceDialog, setShowClearanceDialog] = useState(false); const [showClearanceDialog, setShowClearanceDialog] = useState(false);
const [selectedDept, setSelectedDept] = useState(''); const [selectedDept, setSelectedDept] = useState('');
@ -68,6 +79,12 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
// Check if user can push to F&F (DD Lead and above) // Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role); const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
// Check if user is assigned to the current stage
const isCurrentlyAssigned = currentUser && (
currentUser.role === 'Super Admin' ||
currentUser.role === STAGE_TO_ROLE_MAP[resignationData?.currentStage]
);
// Progress stages logic based on live data // Progress stages logic based on live data
const progressStages = [ const progressStages = [
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' }, { id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
@ -81,15 +98,49 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' } { id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
]; ];
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
const getResignationPermissions = () => {
if (!resignationData || !currentUser) {
return { canApprove: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false };
}
const currentStage = resignationData.currentStage;
const status = resignationData.status;
const userRole = currentUser.role;
// Final states where no more actions are possible
const isFinalState = ['Completed', 'Rejected', 'Withdrawn'].includes(status);
// Check if it's already in the settlement phase
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
const stageIndex = stagesOrdered.indexOf(currentStage);
const nbhIndex = stagesOrdered.indexOf('NBH');
const isPastNBH = stageIndex !== -1 && nbhIndex !== -1 && stageIndex >= nbhIndex;
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === STAGE_TO_ROLE_MAP[currentStage];
return {
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase,
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
!isSettlementPhase && !isFinalState,
canAssign: userRole !== 'Dealer' && !isFinalState
};
};
const permissions = getResignationPermissions();
const getStageStatus = (stageKey: string) => { const getStageStatus = (stageKey: string) => {
if (!resignationData) return 'pending'; if (!resignationData) return 'pending';
const currentStage = resignationData.currentStage; const currentStage = resignationData.currentStage;
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed'];
const currentIndex = stagesOrdered.indexOf(currentStage); const currentIndex = stagesOrdered.indexOf(currentStage);
const stageIndex = stagesOrdered.indexOf(stageKey); const stageIndex = stagesOrdered.indexOf(stageKey);
if (currentIndex === -1) return 'pending'; // Fallback for rejected/other states if (currentIndex === -1) return 'pending';
if (stageIndex < currentIndex) return 'completed'; if (stageIndex < currentIndex) return 'completed';
if (stageIndex === currentIndex) return 'active'; if (stageIndex === currentIndex) return 'active';
return 'pending'; return 'pending';
@ -119,7 +170,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const payload = { const payload = {
action: actionDialog.type, action: actionDialog.type,
remarks, remarks,
assignTo: assignToUser assignTo: assignToUser,
force: forceTriggerFnF
}; };
const response: any = await API.updateResignationStatus(resignationId, payload); const response: any = await API.updateResignationStatus(resignationId, payload);
@ -154,11 +206,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
formData.append('file', clearanceFile); formData.append('file', clearanceFile);
} }
await resignationService.updateClearance(resignationId, formData); const response: any = await resignationService.updateClearance(resignationId, formData);
toast.success(`Successfully updated clearance for ${selectedDept}`);
setShowClearanceDialog(false); if (response?.success) {
setClearanceFile(null); toast.success(`Successfully updated clearance for ${selectedDept}`);
fetchResignation(); setShowClearanceDialog(false);
setClearanceFile(null);
fetchResignation();
} else {
toast.error(response?.message || 'Failed to update clearance status');
}
} catch (error) { } catch (error) {
toast.error('Failed to update clearance status'); toast.error('Failed to update clearance status');
} finally { } finally {
@ -169,7 +226,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
if (isLoading && !resignationData) { if (isLoading && !resignationData) {
return ( return (
<div className="flex items-center justify-center min-h-[400px]"> <div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" /> <Loader2 className="w-8 h-8 animate-spin text-amber-600" />
</div> </div>
); );
} }
@ -200,56 +257,58 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span> <span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
{currentUser?.role !== 'Dealer' && ( {permissions.canApprove && (
<> <Button
<Button size="sm"
size="sm" disabled={isSubmitting}
disabled={isSubmitting} className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md" onClick={() => handleAction('approve')}
onClick={() => handleAction('approve')} >
> {isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />} Approve
Approve </Button>
</Button> )}
<Button {permissions.canSendBack && (
size="sm" <Button
variant="outline" size="sm"
disabled={isSubmitting} variant="outline"
className="hover:bg-slate-50 transition-all" disabled={isSubmitting}
onClick={() => handleAction('sendback')} className="hover:bg-slate-50 transition-all font-bold"
> onClick={() => handleAction('sendback')}
{isSubmitting && actionDialog.type === 'sendback' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />} >
Send Back {isSubmitting && actionDialog.type === 'sendback' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
</Button> Send Back
</> </Button>
)}
{permissions.canWithdraw && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-red-600 border-red-300 hover:bg-red-50 transition-all font-bold"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
)} )}
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-red-600 border-red-300 hover:bg-red-50 transition-all"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
</div> </div>
{/* Secondary Actions */} {/* Secondary Actions */}
{currentUser?.role !== 'Dealer' && ( <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> {permissions.canPushToFnF && (
{canPushToFnF && resignationData?.status !== 'FNF_INITIATED' && resignationData?.status !== 'Settled' && ( <Button
<Button size="sm"
size="sm" variant="outline"
variant="outline" disabled={isSubmitting}
disabled={isSubmitting} className="text-amber-600 border-blue-300 hover:bg-blue-50 transition-all"
className="text-blue-600 border-blue-300 hover:bg-blue-50 transition-all" onClick={() => handleAction('pushfnf')}
onClick={() => handleAction('pushfnf')} >
> {isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />} Push to F&F
Push to F&F </Button>
</Button> )}
)} {permissions.canAssign && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -260,8 +319,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />} {isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
Assign User Assign User
</Button> </Button>
</div> )}
)} </div>
</div> </div>
{/* Work Notes Button - Independent Section */} {/* Work Notes Button - Independent Section */}
@ -353,7 +412,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="grid grid-cols-2 md:grid-cols-3 gap-6"> <div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div> <div>
<Label className="text-slate-600">Inauguration</Label> <Label className="text-slate-600">Inauguration</Label>
<p>{resignationData?.dealer?.dealerProfile?.onboardedAt ? formatDateTime(resignationData.dealer.dealerProfile.onboardedAt, 'date') : (resignationData?.outlet?.inaugurationDate ? new Date(resignationData.outlet.inaugurationDate).toLocaleDateString() : 'N/A')}</p> <p>{resignationData?.dealer?.dealerProfile?.onboardedAt ? formatDateTime(resignationData.dealer.dealerProfile.onboardedAt, 'date') : (resignationData?.outlet?.inaugurationDate ? formatDateTime(resignationData.outlet.inaugurationDate, 'date') : 'N/A')}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">LOA Date</Label> <Label className="text-slate-600">LOA Date</Label>
@ -439,7 +498,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${ <div className={`w-10 h-10 rounded-full flex items-center justify-center ${
status === 'completed' ? 'bg-green-100 text-green-600' : status === 'completed' ? 'bg-green-100 text-green-600' :
status === 'active' ? 'bg-blue-100 text-blue-600' : status === 'active' ? 'bg-blue-100 text-amber-600' :
'bg-slate-100 text-slate-400' 'bg-slate-100 text-slate-400'
}`}> }`}>
{status === 'completed' ? ( {status === 'completed' ? (
@ -458,7 +517,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<h3 className={ <h3 className={
status === 'completed' ? 'text-green-600' : status === 'completed' ? 'text-green-600' :
status === 'active' ? 'text-blue-600' : status === 'active' ? 'text-amber-600' :
'text-slate-400' 'text-slate-400'
}>{stage.name}</h3> }>{stage.name}</h3>
{timelineEntry && ( {timelineEntry && (
@ -477,7 +536,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="mt-2 text-blue-600" className="mt-2 text-amber-600"
onClick={() => handleViewStageDocuments(stage.name)} onClick={() => handleViewStageDocuments(stage.name)}
> >
View Stage Documents View Stage Documents
@ -545,7 +604,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="pt-2 border-t border-slate-100 flex items-center justify-between"> <div className="pt-2 border-t border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center border border-blue-100"> <div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center border border-blue-100">
<FileText className="w-4 h-4 text-blue-500" /> <FileText className="w-4 h-4 text-amber-500" />
</div> </div>
<div> <div>
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-tight leading-none mb-1">Evidence Attached</p> <p className="text-[10px] text-slate-400 uppercase font-bold tracking-tight leading-none mb-1">Evidence Attached</p>
@ -566,7 +625,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
documentType: 'Clearance Proof' documentType: 'Clearance Proof'
}); });
}} }}
className="flex items-center gap-1.5 text-xs text-blue-600 hover:text-blue-700 hover:underline font-bold bg-blue-50/50 px-2.5 py-1.5 rounded-md border border-blue-100/50 transition-colors" className="flex items-center gap-1.5 text-xs text-amber-600 hover:text-blue-700 hover:underline font-bold bg-blue-50/50 px-2.5 py-1.5 rounded-md border border-blue-100/50 transition-colors"
> >
<Eye className="w-3.5 h-3.5" /> <Eye className="w-3.5 h-3.5" />
Preview Preview
@ -579,7 +638,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="mt-2 text-blue-600 hover:text-blue-700 p-0" className="mt-2 text-amber-600 hover:text-blue-700 p-0"
onClick={() => { onClick={() => {
setSelectedDept(dept); setSelectedDept(dept);
setClearanceStatus(displayStatus || 'Pending'); setClearanceStatus(displayStatus || 'Pending');
@ -627,7 +686,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
...(resignationData?.uploadedDocuments || []) ...(resignationData?.uploadedDocuments || [])
]; ];
// Add clearance documents // Add clearance documents from legacy JSON field
if (resignationData?.departmentalClearances) { if (resignationData?.departmentalClearances) {
Object.entries(resignationData.departmentalClearances).forEach(([dept, data]: [string, any]) => { Object.entries(resignationData.departmentalClearances).forEach(([dept, data]: [string, any]) => {
if (data.supportingDocument) { if (data.supportingDocument) {
@ -642,6 +701,21 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}); });
} }
// Add live clearance documents from F&F model
if (resignationData?.settlement?.clearances) {
resignationData.settlement.clearances.forEach((c: any) => {
if (c.supportingDocument) {
allDocs.push({
name: `${c.department} Clearance NOC`,
type: 'Live NOC',
path: c.supportingDocument,
createdAt: c.clearedAt || c.updatedAt,
uploadedBy: 'Department Admin'
});
}
});
}
if (allDocs.length === 0) return ( if (allDocs.length === 0) return (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center py-4 text-slate-500"> <TableCell colSpan={5} className="text-center py-4 text-slate-500">
@ -703,7 +777,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{(resignationData?.timeline || []).length > 0 ? ( {(resignationData?.timeline || []).length > 0 ? (
(resignationData.timeline || []).map((log: any, index: number) => ( (resignationData.timeline || []).map((log: any, index: number) => (
<div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0"> <div key={index} className="flex gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" /> <div className="w-2 h-2 rounded-full bg-amber-600 mt-2" />
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="font-medium text-slate-900">{log.action || log.status}</p> <p className="font-medium text-slate-900">{log.action || log.status}</p>
@ -764,14 +838,35 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</Select> </Select>
</div> </div>
) : actionDialog.type === 'pushfnf' ? ( ) : actionDialog.type === 'pushfnf' ? (
<div className="space-y-2"> <div className="space-y-4">
<Label>Remarks (Optional)</Label> <div className="p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start gap-3">
<Textarea <AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
value={remarks} <div className="text-sm text-amber-800">
onChange={(e) => setRemarks(e.target.value)} <p className="font-bold">Manual Trigger Notice</p>
placeholder="Add any additional notes..." <p>Normally F&F is triggered after LWD. Use manual trigger only if urgent clearance is required.</p>
rows={3} </div>
/> </div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="forceFnF"
checked={forceTriggerFnF}
onChange={(e) => setForceTriggerFnF(e.target.checked)}
className="w-4 h-4 rounded border-slate-300"
/>
<Label htmlFor="forceFnF" className="font-medium text-slate-900 cursor-pointer">
Force Initiate F&F Settlement Immediately
</Label>
</div>
<div className="space-y-2">
<Label>Remarks (Optional)</Label>
<Textarea
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
placeholder="Add any additional notes..."
rows={3}
/>
</div>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
@ -823,7 +918,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-600" /> <FileText className="w-5 h-5 text-amber-600" />
Documents - {stageDocumentsDialog.stageName} Documents - {stageDocumentsDialog.stageName}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
@ -853,7 +948,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<TableCell>{doc.uploadDate}</TableCell> <TableCell>{doc.uploadDate}</TableCell>
<TableCell>{doc.uploader}</TableCell> <TableCell>{doc.uploader}</TableCell>
<TableCell> <TableCell>
<Button size="sm" variant="outline" className="text-blue-600 hover:text-blue-700"> <Button size="sm" variant="outline" className="text-amber-600 hover:text-blue-700">
<FileText className="w-4 h-4 mr-1" /> <FileText className="w-4 h-4 mr-1" />
View View
</Button> </Button>
@ -948,7 +1043,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
/> />
<div className="flex flex-col items-center justify-center gap-2"> <div className="flex flex-col items-center justify-center gap-2">
{clearanceFile ? ( {clearanceFile ? (
<div className="flex items-center gap-2 text-blue-600 font-medium"> <div className="flex items-center gap-2 text-amber-600 font-medium">
<FileText className="w-5 h-5" /> <FileText className="w-5 h-5" />
<span className="truncate max-w-[200px]">{clearanceFile.name}</span> <span className="truncate max-w-[200px]">{clearanceFile.name}</span>
<X className="w-4 h-4 cursor-pointer text-slate-400 hover:text-red-500" onClick={(e) => { e.stopPropagation(); setClearanceFile(null); }} /> <X className="w-4 h-4 cursor-pointer text-slate-400 hover:text-red-500" onClick={(e) => { e.stopPropagation(); setClearanceFile(null); }} />

View File

@ -7,6 +7,7 @@ import { useState, useEffect } from 'react';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { User as UserType } from '../../lib/mock-data'; import { User as UserType } from '../../lib/mock-data';
import { formatDateTime } from '../ui/utils';
interface ResignationPageProps { interface ResignationPageProps {
currentUser: UserType | null; currentUser: UserType | null;
@ -163,15 +164,15 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
<p className="text-slate-600">Dealer Name</p> <p className="text-slate-600">Dealer Name</p>
<p>{request.outlet?.name || 'N/A'}</p> <p>{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Dealer Code</p> <p className="text-slate-600">Dealer Code</p>
<p>{request.outlet?.code || 'N/A'}</p> <p>{request.dealer?.dealerProfile?.dealerCode?.dealerCode || request.outlet?.code || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Location</p> <p className="text-slate-600">Location</p>
<p>{request.outlet?.city}, {request.outlet?.state}</p> <p>{request.dealer?.dealerProfile?.registeredAddress || (request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : 'N/A')}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Type</p> <p className="text-slate-600">Type</p>
@ -189,7 +190,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<p className="text-slate-600">Submitted On</p> <p className="text-slate-600">Submitted On</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="w-4 h-4 text-slate-500" /> <Calendar className="w-4 h-4 text-slate-500" />
<p>{new Date(request.submittedOn).toLocaleDateString()}</p> <p>{formatDateTime(request.submittedOn)}</p>
</div> </div>
</div> </div>
</div> </div>
@ -238,11 +239,11 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
<p className="text-slate-600">Dealer Name</p> <p className="text-slate-600">Dealer Name</p>
<p>{request.outlet?.name || 'N/A'}</p> <p>{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Location</p> <p className="text-slate-600">Location</p>
<p>{request.outlet?.city}, {request.outlet?.state}</p> <p>{request.dealer?.dealerProfile?.registeredAddress || (request.outlet?.city && request.outlet?.state ? `${request.outlet.city}, ${request.outlet.state}` : 'N/A')}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Current Stage</p> <p className="text-slate-600">Current Stage</p>
@ -250,7 +251,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Submitted On</p> <p className="text-slate-600">Submitted On</p>
<p>{new Date(request.submittedOn).toLocaleDateString()}</p> <p>{formatDateTime(request.submittedOn)}</p>
</div> </div>
</div> </div>
</div> </div>
@ -299,11 +300,11 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
<p className="text-slate-600">Dealer Name</p> <p className="text-slate-600">Dealer Name</p>
<p>{request.outlet?.name}</p> <p>{request.dealer?.dealerProfile?.businessName || request.outlet?.name || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Location</p> <p className="text-slate-600">Location</p>
<p>{request.outlet?.city}</p> <p>{request.dealer?.dealerProfile?.registeredAddress || request.outlet?.city || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Final Stage</p> <p className="text-slate-600">Final Stage</p>
@ -311,7 +312,7 @@ export function ResignationPage({ currentUser, onViewDetails }: ResignationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Submitted On</p> <p className="text-slate-600">Submitted On</p>
<p>{new Date(request.submittedOn).toLocaleDateString()}</p> <p>{formatDateTime(request.submittedOn)}</p>
</div> </div>
</div> </div>
</div> </div>

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) // Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role); const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
// Centralized Permissions Utility for Termination logic (Robust Validation)
const getTerminationPermissions = () => {
if (!terminationData || !currentUser) {
return { canApprove: false, canWithdraw: false, canIssueSCN: false, canUploadSCNResponse: false, canFinalize: false, canPushToFnF: false };
}
const currentStage = terminationData.currentStage;
const status = terminationData.status;
const userRole = currentUser.role;
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || (
(currentStage === 'RBM Review' && userRole === 'RBM') ||
(currentStage === 'ZBH Review' && userRole === 'ZBH') ||
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') ||
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') ||
(currentStage === 'NBH Evaluation' && userRole === 'NBH') ||
(currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
(currentStage === 'CCO Approval' && userRole === 'CCO') ||
(currentStage === 'CEO Final Approval' && userRole === 'CEO') ||
(currentStage === 'Legal - Termination Letter' && userRole === 'Legal Admin')
);
return {
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && !['Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage),
canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState,
canUploadSCNResponse: currentStage === 'Show Cause Notice' && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState,
canFinalize: ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && (userRole === currentStage.replace(' Approval', '') || userRole === 'Super Admin') && !isFinalState,
canPushToFnF: canPushToFnF && !isSettlementPhase && !isFinalState,
canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState,
isFinalState,
isSettlementPhase
};
};
const permissions = getTerminationPermissions();
// Use actual data from backend // Use actual data from backend
const request = terminationData || {}; const request = terminationData || {};
@ -347,15 +386,19 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span> <span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
{currentUser?.role !== 'Dealer' && ( {currentUser?.role !== 'Dealer' && (
<> <>
<Button {!permissions.canFinalize && (
size="sm" <>
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md" {permissions.canApprove && (
onClick={() => handleAction('approve')} <Button
> size="sm"
<Check className="w-4 h-4 mr-2" /> className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
Approve onClick={() => handleAction('approve')}
</Button> >
{request.currentStage === 'NBH' && (currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && ( <Check className="w-4 h-4 mr-2" />
Approve
</Button>
)}
{permissions.canIssueSCN && (
<Button <Button
size="sm" size="sm"
className="bg-purple-600 hover:bg-purple-700 transition-all shadow-sm" className="bg-purple-600 hover:bg-purple-700 transition-all shadow-sm"
@ -365,7 +408,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
Issue SCN Issue SCN
</Button> </Button>
)} )}
{request.currentStage === 'SCN' && (['Legal', 'DD Admin', 'Super Admin'].includes(currentUser?.role || '')) && ( {permissions.canUploadSCNResponse && (
<Button <Button
size="sm" size="sm"
className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm" className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm"
@ -378,32 +421,36 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
Upload SCN Response Upload SCN Response
</Button> </Button>
)} )}
{(['NBH Final', 'CCO', 'CEO'].includes(request.currentStage)) && (currentUser?.role === request.currentStage || currentUser?.role === 'Super Admin') && ( {permissions.canApprove && (
<Button <Button
size="sm" size="sm"
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm" variant="outline"
onClick={() => setShowFinalizeDialog(true)} className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('sendback')}
> >
<ShieldCheck className="w-4 h-4 mr-2" /> <RotateCcw className="w-4 h-4 mr-2" />
Final Authorization Send Back
</Button> </Button>
)} )}
<Button </>
size="sm" )}
variant="outline" {permissions.canFinalize && (
className="hover:bg-slate-50 transition-all" <Button
onClick={() => handleAction('sendback')} size="sm"
> className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
<RotateCcw className="w-4 h-4 mr-2" /> onClick={() => setShowFinalizeDialog(true)}
Send Back >
</Button> <ShieldCheck className="w-4 h-4 mr-2" />
Final Authorization
</Button>
)}
</> </>
)} )}
</div> </div>
{/* Secondary Actions */} {/* Secondary Actions */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canPushToFnF && ( {permissions.canPushToFnF && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -414,15 +461,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
Push to F&F Push to F&F
</Button> </Button>
)} )}
<Button {!permissions.isFinalState && (
size="sm" <Button
variant="outline" size="sm"
className="hover:bg-slate-50 transition-all" variant="outline"
onClick={() => handleAction('assign')} className="hover:bg-slate-50 transition-all"
> onClick={() => handleAction('assign')}
<UserPlus className="w-4 h-4 mr-2" /> >
Assign User <UserPlus className="w-4 h-4 mr-2" />
</Button> Assign User
</Button>
)}
</div> </div>
</div> </div>
@ -476,7 +525,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="grid grid-cols-2 md:grid-cols-3 gap-6"> <div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div> <div>
<Label className="text-slate-600">Dealer Code</Label> <Label className="text-slate-600">Dealer Code</Label>
<p>{request.dealer?.dealerCode?.code || 'N/A'}</p> <p>{request.dealer?.dealerCode?.dealerCode || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Dealer Name</Label> <Label className="text-slate-600">Dealer Name</Label>
@ -511,8 +560,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<p>{request.dealer?.dealerCode?.serviceCode || request.serviceCode || 'N/A'}</p> <p>{request.dealer?.dealerCode?.serviceCode || request.serviceCode || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">Accessories Code</Label> <Label className="text-slate-600">GMA Code</Label>
<p>{request.dealer?.dealerCode?.accessoriesCode || request.accessoriesCode || 'N/A'}</p> <p>{request.dealer?.dealerCode?.gmaCode || request.accessoriesCode || 'N/A'}</p>
</div> </div>
<div> <div>
<Label className="text-slate-600">GMA Code</Label> <Label className="text-slate-600">GMA Code</Label>

View File

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

View File

@ -22,6 +22,7 @@ import { RootState } from '../../store';
import { logout } from '../../store/slices/authSlice'; import { logout } from '../../store/slices/authSlice';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '../../api/API'; import { API } from '../../api/API';
import { formatDateTime } from '../ui/utils';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { ProspectiveApplicationDetails } from '../applications/ProspectiveApplicationDetails'; import { ProspectiveApplicationDetails } from '../applications/ProspectiveApplicationDetails';
@ -206,7 +207,7 @@ function ProspectiveApplicationList() {
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xs text-slate-500 font-medium">Applied</span> <span className="text-xs text-slate-500 font-medium">Applied</span>
<span className="text-xs font-bold text-slate-600">{new Date(app.createdAt).toLocaleDateString()}</span> <span className="text-xs font-bold text-slate-600">{formatDateTime(app.createdAt)}</span>
</div> </div>
<div className="mt-6"> <div className="mt-6">
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">

View File

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

View File

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

View File

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

42
src/lib/dateUtils.ts Normal file
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; districtId?: string;
fddAssignments?: any[]; fddAssignments?: any[];
statutoryStatus?: string; statutoryStatus?: string;
panNumber?: string;
gstNumber?: string;
bankName?: string;
accountNumber?: string;
ifscCode?: string;
branchName?: string;
accountHolderName?: string;
registeredAddress?: string;
} }
export interface Participant { export interface Participant {

View File

@ -30,5 +30,10 @@ export const settlementService = {
const response: any = await API.addLineItem(fnfId, data); const response: any = await API.addLineItem(fnfId, data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to add line item'); if (!response.ok) throw new Error(response.data?.message || 'Failed to add line item');
return response.data; return response.data;
},
updateFnF: async (id: string, data: any) => {
const response: any = await API.updateFnF(id, data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to update F&F settlement');
return response.data;
} }
}; };

View File

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