onboarding flow made stable end to end also fincance team verification implementation done relocation stated

This commit is contained in:
laxman h 2026-04-02 19:31:58 +05:30
parent 574e648618
commit 830f66b5f7
14 changed files with 1740 additions and 1452 deletions

View File

@ -48,6 +48,8 @@ export const API = {
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
retriggerEvaluators: (id: string) => client.post(`/onboarding/applications/${id}/retrigger-evaluators`),
getSecurityDeposit: (applicationId: string) => client.get(`/loa/security-deposit/${applicationId}`),
updateSecurityDeposit: (data: any) => client.post('/loa/security-deposit', data),
// Documents
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
@ -158,6 +160,10 @@ export const API = {
// SLA
getSlaConfigs: () => client.get('/sla/configs'),
// System Configs
getSystemConfigs: (params?: any) => client.get('/master/system-configs', params),
saveSystemConfig: (data: any) => client.post('/master/system-configs', data),
};
export default API;

View File

@ -9,6 +9,7 @@ import QuestionnaireResponseView from './QuestionnaireResponseView';
import { useSelector } from 'react-redux';
import { RootState } from '../../store';
import { cn } from '@/components/ui/utils';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
@ -39,6 +40,7 @@ import {
Zap,
ShieldCheck,
Eye,
Lock,
} from 'lucide-react';
import { Progress } from '../ui/progress';
import { Textarea } from '../ui/textarea';
@ -83,6 +85,8 @@ interface ProcessStage {
evaluators?: string[];
documentsUploaded?: number;
isParallel?: boolean;
isLocked?: boolean;
lockMessage?: string;
branches?: {
name: string;
color: string;
@ -438,6 +442,29 @@ export function ApplicationDetails() {
const [isSubmittingKT, setIsSubmittingKT] = useState(false);
const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null);
// Payment Details State
const [deposits, setDeposits] = useState<any[]>([]);
const [paymentConfigs, setPaymentConfigs] = useState<any>({});
useEffect(() => {
if (applicationId) {
const fetchPaymentData = async () => {
try {
const [depositData, configData] = await Promise.all([
onboardingService.getSecurityDeposit(applicationId),
onboardingService.getSystemConfigs({ category: 'SECURITY_DEPOSIT', format: 'map' })
]);
setDeposits(Array.isArray(depositData) ? depositData : [depositData].filter(Boolean));
setPaymentConfigs(configData || {});
} catch (error) {
console.error('Failed to fetch payment data', error);
}
};
fetchPaymentData();
}
}, [applicationId]);
const getDeposit = (type: string) => deposits.find(d => d.depositType === type);
const handleKTMatrixChange = (criterionName: string, score: number) => {
setKtMatrixScores(prev => ({
@ -749,8 +776,8 @@ export function ApplicationDetails() {
// Auto-fill participants based on pre-assigned evaluators for this level
const levelNum = parseInt(interviewType.replace('level', '')) || 1;
const preAssigned = (application?.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === levelNum ||
.filter((p: any) =>
p.metadata?.interviewLevel === levelNum ||
p.metadata?.interviewLevel === String(levelNum) ||
p.metadata?.allAssignments?.includes(levelNum) ||
p.metadata?.allAssignments?.includes(String(levelNum))
@ -909,9 +936,9 @@ export function ApplicationDetails() {
date: application.level1InterviewDate,
description: 'DD-ZM + RBM evaluation',
evaluators: Array.from(new Set((application.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === 1 ||
p.metadata?.interviewLevel === '1' ||
.filter((p: any) =>
p.metadata?.interviewLevel === 1 ||
p.metadata?.interviewLevel === '1' ||
p.metadata?.allAssignments?.includes(1) ||
p.metadata?.allAssignments?.includes('1')
)
@ -926,9 +953,9 @@ export function ApplicationDetails() {
date: application.level2InterviewDate,
description: 'DD Lead + ZBH evaluation',
evaluators: Array.from(new Set((application.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === 2 ||
p.metadata?.interviewLevel === '2' ||
.filter((p: any) =>
p.metadata?.interviewLevel === 2 ||
p.metadata?.interviewLevel === '2' ||
p.metadata?.allAssignments?.includes(2) ||
p.metadata?.allAssignments?.includes('2')
)
@ -943,9 +970,9 @@ export function ApplicationDetails() {
date: application.level3InterviewDate,
description: 'NBH + DD Head evaluation',
evaluators: Array.from(new Set((application.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === 3 ||
p.metadata?.interviewLevel === '3' ||
.filter((p: any) =>
p.metadata?.interviewLevel === 3 ||
p.metadata?.interviewLevel === '3' ||
p.metadata?.allAssignments?.includes(3) ||
p.metadata?.allAssignments?.includes('3')
)
@ -968,8 +995,8 @@ export function ApplicationDetails() {
date: application.loiApprovalDate,
description: 'Letter of Intent approval',
evaluators: Array.from(new Set((application.participants || [])
.filter((p: any) =>
p.metadata?.stageCode === 'LOI_APPROVAL' ||
.filter((p: any) =>
p.metadata?.stageCode === 'LOI_APPROVAL' ||
p.metadata?.allAssignments?.includes('LOI_APPROVAL')
)
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
@ -1120,11 +1147,15 @@ export function ApplicationDetails() {
id: 12,
name: 'LOA',
status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'),
isLocked: application.status === 'LOA Pending' &&
getDeposit('FINAL')?.status !== 'Verified' &&
!documents.some(d => (d.documentType?.toLowerCase().includes('final') && d.documentType?.toLowerCase().includes('deposit')) && d.status === 'Approved'),
lockMessage: 'Final Security Deposit (₹15L) must be verified by Finance before LOA Approval.',
date: application.loaDate,
description: 'Letter of Authorization',
evaluators: Array.from(new Set((application.participants || [])
.filter((p: any) =>
p.metadata?.stageCode === 'LOA_APPROVAL' ||
.filter((p: any) =>
p.metadata?.stageCode === 'LOA_APPROVAL' ||
p.metadata?.allAssignments?.includes('LOA_APPROVAL')
)
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
@ -1581,15 +1612,15 @@ export function ApplicationDetails() {
const currentStageCode = policyManagedStages[application.status];
const currentUserStageAction = application.stageApprovals?.find(
(a: any) =>
a.stageCode === currentStageCode &&
(a: any) =>
a.stageCode === currentStageCode &&
String(a.actorUserId) === String(currentUser?.id)
);
const hasMadeStageDecision = !!currentUserStageAction;
const hasMadeDecisionForUser = !!currentUserStageAction ||
currentUserEvaluation?.decision === 'Approved' ||
const hasMadeDecisionForUser = !!currentUserStageAction ||
currentUserEvaluation?.decision === 'Approved' ||
currentUserEvaluation?.decision === 'Rejected' ||
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
@ -1819,89 +1850,102 @@ export function ApplicationDetails() {
<Progress value={application.progress} className="h-3 mb-6" />
</div>
<div className="relative">
{(() => {
const getApproverStatus = (stageCode: string | number) => {
const stageParticipants = (application.participants || []).filter((p: any) =>
p.metadata?.stageCode === stageCode ||
p.metadata?.allAssignments?.includes(stageCode) ||
(typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) ||
(typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode))))
<div className="relative">
{(() => {
const getApproverStatus = (stageCode: string | number) => {
const stageParticipants = (application.participants || []).filter((p: any) =>
p.metadata?.stageCode === stageCode ||
p.metadata?.allAssignments?.includes(stageCode) ||
(typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) ||
(typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode))))
);
return stageParticipants.map((p: any) => {
const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode;
const approval = (application.stageApprovals || []).find((sa: any) =>
sa.stageCode === saCode &&
String(sa.actorUserId) === String(p.userId)
);
return stageParticipants.map((p: any) => {
const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode;
const approval = (application.stageApprovals || []).find((sa: any) =>
sa.stageCode === saCode &&
String(sa.actorUserId) === String(p.userId)
);
return {
name: p.user?.name || 'Unknown',
role: p.user?.role || 'Reviewer',
status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending'
};
});
};
const renderApprovers = (stageName: string) => {
const stageMapping: Record<string, string | number> = {
'1st Level Interview': 1,
'2nd Level Interview': 2,
'3rd Level Interview': 3,
'LOI Approval': 'LOI_APPROVAL',
'LOA': 'LOA_APPROVAL'
return {
name: p.user?.name || 'Unknown',
role: p.user?.role || 'Reviewer',
status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending'
};
});
};
const stageCode = stageMapping[stageName];
if (!stageCode) return null;
const approvers = getApproverStatus(stageCode);
if (approvers.length === 0) return null;
return (
<div className="flex flex-wrap gap-2 mt-3">
{approvers.map((approver, i) => (
<div key={i} className="group relative flex items-center gap-1.5 bg-slate-50 border border-slate-200 rounded-full pl-1 pr-2.5 py-0.5 transition-all hover:bg-white hover:shadow-sm">
<div className={cn(
"w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-slate-300"
)}>
{approver.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-[10px] font-medium text-slate-700 leading-none">{approver.name}</span>
<span className="text-[8px] text-slate-500 leading-none mt-0.5">{approver.role}</span>
</div>
{/* Status Dot Overlay */}
<div className={cn(
"absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-amber-400"
)} />
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-slate-900 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
{approver.role}: {approver.status.toUpperCase()}
</div>
</div>
))}
</div>
);
const renderApprovers = (stageName: string) => {
const stageMapping: Record<string, string | number> = {
'1st Level Interview': 1,
'2nd Level Interview': 2,
'3rd Level Interview': 3,
'LOI Approval': 'LOI_APPROVAL',
'LOA': 'LOA_APPROVAL'
};
return processStages.map((stage, index) => (
const stageCode = stageMapping[stageName];
if (!stageCode) return null;
const approvers = getApproverStatus(stageCode);
if (approvers.length === 0) return null;
return (
<div className="flex flex-wrap gap-2 mt-3">
{approvers.map((approver, i) => (
<div key={i} className="group relative flex items-center gap-1.5 bg-slate-50 border border-slate-200 rounded-full pl-1 pr-2.5 py-0.5 transition-all hover:bg-white hover:shadow-sm">
<div className={cn(
"w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-slate-300"
)}>
{approver.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-[10px] font-medium text-slate-700 leading-none">{approver.name}</span>
<span className="text-[8px] text-slate-500 leading-none mt-0.5">{approver.role}</span>
</div>
{/* Status Dot Overlay */}
<div className={cn(
"absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-amber-400"
)} />
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-slate-900 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
{approver.role}: {approver.status.toUpperCase()}
</div>
</div>
))}
</div>
);
};
return processStages.map((stage, index) => (
<div key={stage.id}>
<div className="flex gap-4 pb-8">
<div className="relative">
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 z-10 relative ${stage.status === 'completed'
? 'bg-green-500 border-green-500 text-white shadow-sm'
: stage.status === 'active'
? 'bg-amber-500 border-amber-500 text-white animate-pulse-subtle'
? stage.isLocked ? 'bg-slate-400 border-slate-400 text-white' : 'bg-amber-500 border-amber-500 text-white animate-pulse-subtle'
: 'bg-white border-slate-300 text-slate-400 shadow-none'
}`}>
{stage.isParallel ? (
<GitBranch className="w-5 h-5" />
) : stage.isLocked ? (
<div className="group relative">
<Lock className="w-5 h-5 text-white cursor-help" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-slate-900 text-white text-[10px] rounded shadow-xl opacity-0 group-hover:opacity-100 pointer-events-none transition-all duration-200 whitespace-nowrap z-[100] border border-slate-700">
<div className="flex flex-col gap-1">
<span className="font-bold text-amber-400 flex items-center gap-1">
<AlertCircle className="w-3 h-3" /> Stage Locked
</span>
<span>{stage.lockMessage}</span>
</div>
<div className="absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-slate-900"></div>
</div>
</div>
) : (
<>
{stage.status === 'completed' ? (
@ -1976,7 +2020,7 @@ export function ApplicationDetails() {
}
return null;
})()}
{(() => {
const stageDocsCount = documents.filter(doc =>
doc.stage === stage.name ||
@ -2478,33 +2522,173 @@ export function ApplicationDetails() {
</TabsContent>
{/* Payments Tab */}
<TabsContent value="payments" className="space-y-4">
<h3 className="text-slate-900 mb-4">Payment Information</h3>
<div className="space-y-4">
<div className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-slate-600">Advance Payment</span>
<Badge variant="default">Paid</Badge>
</div>
<p className="text-slate-900">5,00,000</p>
<p className="text-slate-500 mt-2">Receipt: RCP-2025-001.pdf</p>
</div>
<div className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-slate-600">Final Payment</span>
<Badge variant="secondary">Pending</Badge>
</div>
<p className="text-slate-900">15,00,000</p>
</div>
{application.status === 'EOR In Progress' && (
<Button className="w-full bg-amber-600 hover:bg-amber-700">
Request Payment
</Button>
)}
<TabsContent value="payments" className="space-y-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3>
<Badge variant="outline" className="bg-slate-50 text-slate-500 border-slate-200">
{deposits.length} Payment Record(s)
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Initial Security Deposit */}
{(() => {
const deposit = getDeposit('INITIAL');
const config = paymentConfigs.INITIAL_SECURITY_DEPOSIT;
const expectedAmount = config?.amount || 500000;
return (
<Card className={cn(
"border-l-4",
deposit?.status === 'Verified' ? "border-l-green-500" :
deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-amber-500"
)}>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-amber-50 flex items-center justify-center text-amber-600">
<ClipboardList className="w-4 h-4" />
</div>
<span className="font-semibold text-slate-700">Advance Payment</span>
</div>
<Badge className={cn(
deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" :
deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" :
"bg-amber-100 text-amber-700 hover:bg-amber-100"
)}>
{deposit?.status || 'Awaiting'}
</Badge>
</div>
<div className="space-y-3">
<div className="flex justify-between items-baseline">
<span className="text-xs text-slate-500 uppercase font-bold tracking-wider">Amount Received</span>
<span className="text-lg font-bold text-slate-900">{Number(deposit?.amount || 0).toLocaleString()}</span>
</div>
<div className="flex justify-between items-baseline border-t border-slate-100 pt-2">
<span className="text-xs text-slate-500">Expected Total</span>
<span className="text-sm font-medium text-slate-600">{expectedAmount.toLocaleString()}</span>
</div>
{deposit?.paymentReference && (
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
<span>Ref: {deposit.paymentReference}</span>
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>}
</div>
)}
{deposit?.remarks && (
<div className="text-[11px] text-slate-500 bg-red-50/50 p-2 rounded border border-red-100 italic">
"{deposit.remarks}"
</div>
)}
</div>
</CardContent>
</Card>
);
})()}
{/* Final Security Deposit */}
{(() => {
const deposit = getDeposit('FINAL');
const config = paymentConfigs.FINAL_SECURITY_DEPOSIT;
const expectedAmount = config?.amount || 1500000;
return (
<Card className={cn(
"border-l-4",
deposit?.status === 'Verified' ? "border-l-green-500" :
deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-amber-500"
)}>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center text-blue-600">
<ShieldCheck className="w-4 h-4" />
</div>
<span className="font-semibold text-slate-700">Final Security Deposit</span>
</div>
<Badge className={cn(
deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" :
deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" :
"bg-amber-100 text-amber-700 hover:bg-amber-100"
)}>
{deposit?.status || 'Awaiting'}
</Badge>
</div>
<div className="space-y-3">
<div className="flex justify-between items-baseline">
<span className="text-xs text-slate-500 uppercase font-bold tracking-wider">Amount Received</span>
<span className="text-lg font-bold text-slate-900">{Number(deposit?.amount || 0).toLocaleString()}</span>
</div>
<div className="flex justify-between items-baseline border-t border-slate-100 pt-2">
<span className="text-xs text-slate-500">Expected Total</span>
<span className="text-sm font-medium text-slate-600">{expectedAmount.toLocaleString()}</span>
</div>
{deposit?.paymentReference && (
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
<span>Ref: {deposit.paymentReference}</span>
{deposit.verifiedAt && <span>{new Date(deposit.verifiedAt).toLocaleDateString()}</span>}
</div>
)}
{deposit?.remarks && (
<div className="text-[11px] text-slate-500 bg-red-50/50 p-2 rounded border border-red-100 italic">
"{deposit.remarks}"
</div>
)}
</div>
</CardContent>
</Card>
);
})()}
</div>
{/* Payment Proof Documents */}
<Card className="mt-6">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<FileText className="w-4 h-4 text-amber-600" />
Verification Documents
</CardTitle>
</CardHeader>
<CardContent>
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('deposit')).length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('deposit')).map((doc: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 border border-slate-100 rounded-lg bg-slate-50/50 hover:bg-slate-50 transition-colors">
<div className="flex items-center gap-3 overflow-hidden">
<div className="flex-shrink-0 w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200">
<FileText className="w-4 h-4 text-slate-400" />
</div>
<div className="overflow-hidden">
<p className="text-xs font-medium text-slate-900 truncate">{doc.fileName || doc.name}</p>
<p className="text-[10px] text-slate-500 uppercase">{doc.documentType}</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="h-7 text-amber-600 hover:text-amber-700 hover:bg-amber-50"
onClick={() => {
setPreviewDoc(doc);
setShowPreviewModal(true);
}}
>
View
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-6 border-2 border-dashed border-slate-100 rounded-lg">
<p className="text-sm text-slate-400 font-medium italic">No payment proofs uploaded yet.</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Audit Trail Tab */}
@ -3925,27 +4109,29 @@ export function ApplicationDetails() {
'Site Plan': ['Site Plan'],
'FDD': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'],
'FDD Verification': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'],
'LOA': ['LOA Acceptance Copy'],
'LOI Approval': ['LOI Agreement', 'LOI Acknowledgement'],
'LOA': ['LOA Acceptance Copy', 'Final Security Deposit Receipt'],
'LOI Approval': ['Initial Security Deposit Receipt'],
'LOA Approval': ['Final Security Deposit Receipt'],
'LOA Acknowledgement': ['Final Security Deposit Receipt'],
'Inauguration': ['Inauguration Photos', 'Inauguration Report'],
'3rd Level Interview': ['AI Recommendation Summary', 'Interview Evaluation Sheet'],
'2nd Level Interview': ['Interview Evaluation Sheet'],
'1st Level Interview': ['Interview Evaluation Sheet'],
'Shortlist': ['CIBIL Report', 'Proposed Site City Map']
'Shortlist': ['CIBIL Report', 'Proposed Site City Map', 'PAN Card', 'GST Certificate', 'Aadhaar']
};
const baseDocs = ['Other'];
let filteredDocs: string[] = [];
if (!selectedStage) {
// Show standard core docs if no stage select context
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Bank Statement', 'Other'];
} else if (selectedStage.startsWith('EOR: ')) {
// Map EOR specific item directly
filteredDocs = [selectedStage.replace('EOR: ', ''), 'Other'];
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Board Resolution', 'Initial Security Deposit Receipt (₹2L)', 'Final Security Deposit Receipt (₹15L)', 'Rental Agreement', 'Property Documents', 'Bank Statement', 'Cancelled Check', 'Other'];
} else {
// Use mapping or fallback to current stage's specific docs
filteredDocs = [...(STAGE_DOCUMENT_MAP[selectedStage] || []), ...baseDocs];
const stageName = selectedStage as string;
if (stageName.startsWith('EOR: ')) {
filteredDocs = [stageName.replace('EOR: ', ''), 'Other'];
} else {
filteredDocs = [...(STAGE_DOCUMENT_MAP[stageName] || []), ...baseDocs];
}
}
return Array.from(new Set(filteredDocs)).map((doc, idx) => (
@ -4002,66 +4188,11 @@ export function ApplicationDetails() {
)}
</DialogContent>
</Dialog>
{/* Preview Modal */}
<Dialog open={showPreviewModal} onOpenChange={setShowPreviewModal}>
<DialogContent className="max-w-4xl h-[85vh] flex flex-col p-0 overflow-hidden bg-slate-900 border-slate-800 shadow-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-900/50 backdrop-blur-md">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20">
<Eye className="w-5 h-5 text-blue-400" />
</div>
<div>
<DialogTitle className="text-white text-lg font-bold leading-none mb-1">
{previewDoc?.fileName}
</DialogTitle>
<DialogDescription className="text-slate-400 text-xs">
{previewDoc?.documentType || 'Document'} {new Date(previewDoc?.createdAt).toLocaleDateString()}
</DialogDescription>
</div>
</div>
</div>
<div className="flex-1 bg-slate-950 relative group overflow-auto flex items-center justify-center p-4 min-h-0">
{previewDoc && (
<div className="w-full h-full flex items-center justify-center">
{previewDoc.fileName?.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? (
<img
src={`http://localhost:5000/${previewDoc.filePath}`}
alt={previewDoc.fileName}
className="max-h-full max-w-full object-contain shadow-2xl rounded-lg transition-transform duration-500"
/>
) : previewDoc.fileName?.match(/\.pdf$/i) ? (
<iframe
src={`http://localhost:5000/${previewDoc.filePath}#toolbar=0`}
className="w-full h-full border-0 rounded-lg bg-white"
title={previewDoc.fileName}
/>
) : (
<div className="text-center p-12">
<div className="w-24 h-24 rounded-3xl bg-slate-800 flex items-center justify-center mx-auto mb-6 border border-slate-700 shadow-xl">
<FileText className="w-12 h-12 text-slate-500" />
</div>
<h3 className="text-white text-xl font-bold mb-2">Preview Not Available</h3>
<p className="text-slate-400 mb-8 max-w-xs mx-auto">This file type cannot be previewed in the browser. Please download it to view.</p>
<div className="flex justify-center gap-4">
<Button
className="bg-white hover:bg-slate-100 text-slate-900 font-bold px-8 rounded-xl h-12 shadow-lg shadow-white/5"
onClick={() => {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000';
window.open(`${baseUrl}/${previewDoc.filePath}`, '_blank');
}}
>
<Download className="w-4 h-4 mr-2" />
Download File
</Button>
</div>
</div>
)}
</div>
)}
</div>
</DialogContent>
</Dialog>
<DocumentPreviewModal
isOpen={showPreviewModal}
onClose={() => setShowPreviewModal(false)}
document={previewDoc}
/>
</div>
);
};

View File

@ -1,10 +1,7 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import {
Table,
TableBody,
@ -13,576 +10,259 @@ import {
TableHeader,
TableRow,
} from '../ui/table';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import {
DollarSign,
CheckCircle,
XCircle,
Clock,
AlertCircle,
CreditCard,
FileText,
Calendar,
User,
MapPin,
Phone,
Mail
CreditCard
} from 'lucide-react';
import { toast } from 'sonner';
// Mock data for applications pending finance approval
const mockFinanceApplications = [
{
id: 'APP-2025-001',
registrationNumber: 'RE-MUM-2025-001',
name: 'Amit Sharma',
email: 'amit.sharma@example.com',
phone: '+91 98765 43210',
location: 'Mumbai, Maharashtra',
securityDeposit: '₹10,00,000',
paymentStatus: 'Pending Verification',
submittedDate: '2025-10-08',
dueDate: '2025-10-15',
paymentMode: 'NEFT',
transactionId: 'TXN2025001234',
bankName: 'HDFC Bank',
accountNumber: '****5678',
},
{
id: 'APP-2025-002',
registrationNumber: 'RE-DEL-2025-002',
name: 'Priya Patel',
email: 'priya.patel@example.com',
phone: '+91 98765 43211',
location: 'Delhi, NCR',
securityDeposit: '₹15,00,000',
paymentStatus: 'Pending Verification',
submittedDate: '2025-10-09',
dueDate: '2025-10-16',
paymentMode: 'RTGS',
transactionId: 'TXN2025001235',
bankName: 'ICICI Bank',
accountNumber: '****9012',
},
{
id: 'APP-2025-003',
registrationNumber: 'RE-BLR-2025-003',
name: 'Raj Kumar',
email: 'raj.kumar@example.com',
phone: '+91 98765 43212',
location: 'Bangalore, Karnataka',
securityDeposit: '₹12,00,000',
paymentStatus: 'Verified',
submittedDate: '2025-10-05',
approvedDate: '2025-10-07',
paymentMode: 'IMPS',
transactionId: 'TXN2025001230',
bankName: 'SBI',
accountNumber: '****3456',
},
];
import { onboardingService } from '../../services/onboarding.service';
interface FinanceOnboardingPageProps {
onViewPaymentDetails?: (applicationId: string) => void;
}
export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardingPageProps = {}) {
const [selectedApplication, setSelectedApplication] = useState<any>(null);
const [showVerifyDialog, setShowVerifyDialog] = useState(false);
const [showDetailsDialog, setShowDetailsDialog] = useState(false);
const [verificationNotes, setVerificationNotes] = useState('');
const [receivedAmount, setReceivedAmount] = useState('');
const [verificationDate, setVerificationDate] = useState('');
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'verified'>('all');
const [applications, setApplications] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'verified'>('pending');
const filteredApplications = mockFinanceApplications.filter(app => {
useEffect(() => {
fetchApplications();
}, []);
const fetchApplications = async () => {
try {
setLoading(true);
const data = await onboardingService.getApplications();
// Filter for applications that are in stages requiring finance verification
const financeApps = data.filter((app: any) => {
const s = app.overallStatus || app.status;
const stage = app.currentStage;
return [
'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation',
'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT'
].includes(s) || stage === 'Finance';
});
setApplications(financeApps);
} catch (error) {
console.error('Fetch error:', error);
toast.error('Failed to fetch applications');
} finally {
setLoading(false);
}
};
const getRelevantPaymentStatus = (app: any) => {
if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment';
const s = app.overallStatus || app.status;
const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION') ? 'INITIAL' : 'FINAL';
const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType);
return deposit ? deposit.status : 'Awaiting Payment';
};
const filteredApplications = applications.filter(app => {
const status = getRelevantPaymentStatus(app);
if (filterStatus === 'all') return true;
if (filterStatus === 'pending') return app.paymentStatus === 'Pending Verification';
if (filterStatus === 'verified') return app.paymentStatus === 'Verified';
if (filterStatus === 'pending') return status !== 'Verified';
if (filterStatus === 'verified') return status === 'Verified';
return true;
});
const handleVerifyPayment = (app: any) => {
setSelectedApplication(app);
setReceivedAmount(app.securityDeposit);
setVerificationDate(new Date().toISOString().split('T')[0]);
setShowVerifyDialog(true);
};
const handleViewDetails = (app: any) => {
const handleViewDetails = (appId: string) => {
if (onViewPaymentDetails) {
onViewPaymentDetails(app.id);
} else {
setSelectedApplication(app);
setShowDetailsDialog(true);
onViewPaymentDetails(appId);
}
};
const confirmVerification = (approved: boolean) => {
if (approved) {
toast.success(`Payment verified for ${selectedApplication.name}`);
} else {
toast.error(`Payment rejected for ${selectedApplication.name}`);
}
setShowVerifyDialog(false);
setVerificationNotes('');
setSelectedApplication(null);
};
const pendingCount = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified').length;
const verifiedCount = applications.filter(app => getRelevantPaymentStatus(app) === 'Verified').length;
const totalAmount = applications.length * 200000;
const pendingCount = mockFinanceApplications.filter(app => app.paymentStatus === 'Pending Verification').length;
const verifiedCount = mockFinanceApplications.filter(app => app.paymentStatus === 'Verified').length;
const totalAmount = mockFinanceApplications.reduce((sum, app) => {
const amount = parseInt(app.securityDeposit.replace(/[₹,]/g, ''));
return sum + amount;
}, 0);
if (loading) {
return (
<div className="flex items-center justify-center p-20 text-amber-600">
<Clock className="w-8 h-8 animate-spin mr-3" />
<span>Loading payment queue...</span>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div>
<h1 className="text-slate-900 mb-2">Payment Verification</h1>
<p className="text-slate-600">Verify advance payments for dealership applications</p>
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-1">Financial Compliance</h1>
<p className="text-slate-600">Verify security deposits and advance payments for dealership onboarding</p>
</div>
<Button onClick={fetchApplications} variant="outline" size="sm">
Refresh List
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-600">Pending Verification</CardTitle>
<Card className="border-amber-200 bg-amber-50/30">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-amber-800">Pending Verification</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">{pendingCount}</div>
<Clock className="w-8 h-8 text-amber-600" />
<div className="text-3xl font-bold text-amber-700">{pendingCount}</div>
<Clock className="w-8 h-8 text-amber-400" />
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-600">Verified Payments</CardTitle>
<Card className="border-green-200 bg-green-50/30">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-green-800">Verified This Week</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">{verifiedCount}</div>
<CheckCircle className="w-8 h-8 text-green-600" />
<div className="text-3xl font-bold text-green-700">{verifiedCount}</div>
<CheckCircle className="w-8 h-8 text-green-400" />
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-600">Total Applications</CardTitle>
<Card className="border-blue-200 bg-blue-50/30">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-blue-800">Active Queue</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">{mockFinanceApplications.length}</div>
<FileText className="w-8 h-8 text-blue-600" />
<div className="text-3xl font-bold text-blue-700">{applications.length}</div>
<FileText className="w-8 h-8 text-blue-400" />
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-600">Total Amount</CardTitle>
<Card className="border-slate-200 bg-slate-50/30">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-800">Estimated Total (Cr)</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">{(totalAmount / 100000).toFixed(1)}L</div>
<DollarSign className="w-8 h-8 text-purple-600" />
<div className="text-3xl font-bold text-slate-700">{(totalAmount / 10000000).toFixed(2)}</div>
<DollarSign className="w-8 h-8 text-slate-400" />
</div>
</CardContent>
</Card>
</div>
{/* Filter Tabs */}
<div className="flex gap-2">
<div className="flex gap-2 bg-slate-100 p-1 rounded-lg w-fit">
<Button
variant={filterStatus === 'all' ? 'default' : 'outline'}
onClick={() => setFilterStatus('all')}
className={filterStatus === 'all' ? 'bg-amber-600 hover:bg-amber-700' : ''}
>
All ({mockFinanceApplications.length})
</Button>
<Button
variant={filterStatus === 'pending' ? 'default' : 'outline'}
variant={filterStatus === 'pending' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFilterStatus('pending')}
className={filterStatus === 'pending' ? 'bg-amber-600 hover:bg-amber-700' : ''}
className={filterStatus === 'pending' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
>
Pending ({pendingCount})
Pending Verifications ({pendingCount})
</Button>
<Button
variant={filterStatus === 'verified' ? 'default' : 'outline'}
variant={filterStatus === 'verified' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFilterStatus('verified')}
className={filterStatus === 'verified' ? 'bg-amber-600 hover:bg-amber-700' : ''}
className={filterStatus === 'verified' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
>
Verified ({verifiedCount})
</Button>
<Button
variant={filterStatus === 'all' ? 'default' : 'ghost'}
size="sm"
onClick={() => setFilterStatus('all')}
className={filterStatus === 'all' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600'}
>
View All
</Button>
</div>
{/* Applications Table */}
<Card>
<CardHeader>
<CardTitle>Payment Verification Queue</CardTitle>
<CardDescription>Review and verify advance payment receipts</CardDescription>
</CardHeader>
<CardContent>
<Card className="border-none shadow-md">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableHeader className="bg-slate-50">
<TableRow>
<TableHead>Application ID</TableHead>
<TableHead>Applicant Details</TableHead>
<TableHead className="w-[180px]">Application ID</TableHead>
<TableHead>Applicant Name</TableHead>
<TableHead>Location</TableHead>
<TableHead>Payment Details</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Actions</TableHead>
<TableHead>Payment Stage</TableHead>
<TableHead>Current Status</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredApplications.map((app) => (
<TableRow key={app.id}>
<TableCell>
<div>
<div className="text-slate-900">{app.id}</div>
<div className="text-sm text-slate-500">{app.registrationNumber}</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
{filteredApplications.length > 0 ? (
filteredApplications.map((app) => (
<TableRow key={app.id} className="hover:bg-slate-50/50 transition-colors">
<TableCell className="font-mono text-sm font-bold">{app.applicationId || app.id}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<User className="w-3 h-3 text-slate-400" />
<span className="text-slate-900">{app.name}</span>
<User className="w-4 h-4 text-slate-400" />
<span className="font-medium text-slate-900">{app.applicantName}</span>
</div>
<div className="flex items-center gap-2">
<Phone className="w-3 h-3 text-slate-400" />
<span className="text-sm text-slate-500">{app.phone}</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-2 text-slate-600">
<MapPin className="w-4 h-4" />
<span>{app.city || app.preferredLocation}, {app.state}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="w-3 h-3 text-slate-400" />
<span className="text-sm text-slate-500">{app.email}</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4 text-slate-400" />
<span className="text-sm">
{app.status === 'LOI_APPROVAL' || app.status === 'PAYMENT_VERIFICATION' ? 'Initial Deposit (₹2L)' : 'Final Deposit (₹15L)'}
</span>
</div>
</TableCell>
<TableCell>
<Badge
className={
getRelevantPaymentStatus(app) === 'Verified' ? 'bg-green-100 text-green-700 border-green-200' :
getRelevantPaymentStatus(app) === 'Rejected' ? 'bg-red-100 text-red-700 border-red-200' :
'bg-amber-100 text-amber-700 border-amber-200'
}
variant="outline"
>
{getRelevantPaymentStatus(app)}
</Badge>
</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant={getRelevantPaymentStatus(app) === 'Verified' ? 'outline' : 'default'}
className={getRelevantPaymentStatus(app) !== 'Verified' ? 'bg-amber-600 hover:bg-amber-700' : ''}
onClick={() => handleViewDetails(app.applicationId || app.id)}
>
<FileText className="w-4 h-4 mr-2" />
{app.paymentStatus === 'Verified' ? 'View Details' : 'Verify Now'}
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="h-40 text-center text-slate-500">
<div className="flex flex-col items-center gap-2">
<CheckCircle className="w-10 h-10 text-slate-200" />
<p>No applications found matching this filter.</p>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-slate-400" />
<span className="text-slate-900">{app.location}</span>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center gap-2">
<CreditCard className="w-3 h-3 text-slate-400" />
<span className="text-sm text-slate-900">{app.paymentMode}</span>
</div>
<div className="text-sm text-slate-500">TXN: {app.transactionId}</div>
<div className="text-sm text-slate-500">{app.bankName}</div>
<div className="text-sm text-slate-500">A/C: {app.accountNumber}</div>
</div>
</TableCell>
<TableCell>
<div className="text-slate-900">{app.securityDeposit}</div>
</TableCell>
<TableCell>
<Badge
variant={app.paymentStatus === 'Verified' ? 'default' : 'secondary'}
className={app.paymentStatus === 'Verified' ? 'bg-green-600' : 'bg-amber-600'}
>
{app.paymentStatus}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-slate-400" />
<span className="text-slate-900">
{app.paymentStatus === 'Verified' ? app.approvedDate : app.dueDate}
</span>
</div>
</TableCell>
<TableCell>
<Button
size="sm"
variant={app.paymentStatus === 'Pending Verification' ? 'default' : 'outline'}
className={app.paymentStatus === 'Pending Verification' ? 'bg-amber-600 hover:bg-amber-700' : ''}
onClick={() => handleViewDetails(app)}
>
<FileText className="w-4 h-4 mr-2" />
View Details
</Button>
</TableCell>
</TableRow>
))}
)}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Verification Dialog */}
<Dialog open={showVerifyDialog} onOpenChange={setShowVerifyDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Verify Payment</DialogTitle>
<DialogDescription>
Review and verify the advance payment for {selectedApplication?.name}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Application Summary */}
<Card>
<CardHeader>
<CardTitle className="text-base">Application Details</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-slate-500">Application ID</p>
<p className="text-slate-900">{selectedApplication?.id}</p>
</div>
<div>
<p className="text-sm text-slate-500">Applicant Name</p>
<p className="text-slate-900">{selectedApplication?.name}</p>
</div>
<div>
<p className="text-sm text-slate-500">Location</p>
<p className="text-slate-900">{selectedApplication?.location}</p>
</div>
<div>
<p className="text-sm text-slate-500">Expected Amount</p>
<p className="text-slate-900">{selectedApplication?.securityDeposit}</p>
</div>
</div>
</CardContent>
</Card>
{/* Payment Details */}
<Card>
<CardHeader>
<CardTitle className="text-base">Payment Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-slate-500">Payment Mode</p>
<p className="text-slate-900">{selectedApplication?.paymentMode}</p>
</div>
<div>
<p className="text-sm text-slate-500">Transaction ID</p>
<p className="text-slate-900">{selectedApplication?.transactionId}</p>
</div>
<div>
<p className="text-sm text-slate-500">Bank Name</p>
<p className="text-slate-900">{selectedApplication?.bankName}</p>
</div>
<div>
<p className="text-sm text-slate-500">Account Number</p>
<p className="text-slate-900">{selectedApplication?.accountNumber}</p>
</div>
</div>
</CardContent>
</Card>
{/* Verification Form */}
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="receivedAmount">Received Amount</Label>
<Input
id="receivedAmount"
value={receivedAmount}
onChange={(e) => setReceivedAmount(e.target.value)}
placeholder="₹10,00,000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="verificationDate">Verification Date</Label>
<Input
id="verificationDate"
type="date"
value={verificationDate}
onChange={(e) => setVerificationDate(e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Verification Notes</Label>
<Textarea
id="notes"
value={verificationNotes}
onChange={(e) => setVerificationNotes(e.target.value)}
placeholder="Enter any notes or remarks about this payment verification..."
rows={4}
/>
</div>
</div>
{/* Warning */}
<div className="flex items-start gap-3 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div>
<p className="text-sm text-slate-900 mb-1">Important</p>
<p className="text-sm text-slate-600">
Please ensure you have verified the payment details with the bank before approving.
This action will allow the application to proceed to the next stage.
</p>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowVerifyDialog(false)}>
Cancel
</Button>
<Button
variant="outline"
className="text-red-600 hover:bg-red-50"
onClick={() => confirmVerification(false)}
>
<XCircle className="w-4 h-4 mr-2" />
Reject Payment
</Button>
<Button
className="bg-green-600 hover:bg-green-700"
onClick={() => confirmVerification(true)}
>
<CheckCircle className="w-4 h-4 mr-2" />
Approve Payment
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* View Details Dialog */}
<Dialog open={showDetailsDialog} onOpenChange={setShowDetailsDialog}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Payment Details</DialogTitle>
<DialogDescription>
Complete information for {selectedApplication?.name}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Application Info */}
<Card>
<CardHeader>
<CardTitle className="text-base">Application Information</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-slate-500">Application ID</p>
<p className="text-slate-900">{selectedApplication?.id}</p>
</div>
<div>
<p className="text-sm text-slate-500">Registration Number</p>
<p className="text-slate-900">{selectedApplication?.registrationNumber}</p>
</div>
<div>
<p className="text-sm text-slate-500">Applicant Name</p>
<p className="text-slate-900">{selectedApplication?.name}</p>
</div>
<div>
<p className="text-sm text-slate-500">Email</p>
<p className="text-slate-900">{selectedApplication?.email}</p>
</div>
<div>
<p className="text-sm text-slate-500">Phone</p>
<p className="text-slate-900">{selectedApplication?.phone}</p>
</div>
<div>
<p className="text-sm text-slate-500">Location</p>
<p className="text-slate-900">{selectedApplication?.location}</p>
</div>
</div>
</CardContent>
</Card>
{/* Payment Info */}
<Card>
<CardHeader>
<CardTitle className="text-base">Payment Information</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-slate-500">Security Deposit</p>
<p className="text-slate-900 text-lg">{selectedApplication?.securityDeposit}</p>
</div>
<div>
<p className="text-sm text-slate-500">Payment Status</p>
<Badge
variant={selectedApplication?.paymentStatus === 'Verified' ? 'default' : 'secondary'}
className={selectedApplication?.paymentStatus === 'Verified' ? 'bg-green-600' : 'bg-amber-600'}
>
{selectedApplication?.paymentStatus}
</Badge>
</div>
<div>
<p className="text-sm text-slate-500">Payment Mode</p>
<p className="text-slate-900">{selectedApplication?.paymentMode}</p>
</div>
<div>
<p className="text-sm text-slate-500">Transaction ID</p>
<p className="text-slate-900">{selectedApplication?.transactionId}</p>
</div>
<div>
<p className="text-sm text-slate-500">Bank Name</p>
<p className="text-slate-900">{selectedApplication?.bankName}</p>
</div>
<div>
<p className="text-sm text-slate-500">Account Number</p>
<p className="text-slate-900">{selectedApplication?.accountNumber}</p>
</div>
<div>
<p className="text-sm text-slate-500">Submitted Date</p>
<p className="text-slate-900">{selectedApplication?.submittedDate}</p>
</div>
{selectedApplication?.paymentStatus === 'Verified' && (
<div>
<p className="text-sm text-slate-500">Approved Date</p>
<p className="text-slate-900">{selectedApplication?.approvedDate}</p>
</div>
)}
{selectedApplication?.paymentStatus === 'Pending Verification' && (
<div>
<p className="text-sm text-slate-500">Due Date</p>
<p className="text-slate-900">{selectedApplication?.dueDate}</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowDetailsDialog(false)}>
Close
</Button>
{selectedApplication?.paymentStatus === 'Pending Verification' && (
<Button
className="bg-amber-600 hover:bg-amber-700"
onClick={() => {
setShowDetailsDialog(false);
handleVerifyPayment(selectedApplication);
}}
>
Verify Payment
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,108 +1,159 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import {
import {
ArrowLeft,
DollarSign,
CheckCircle,
XCircle,
Upload,
DollarSign,
CheckCircle,
XCircle,
FileText,
Calendar,
User,
MapPin,
Phone,
Mail,
CreditCard,
Building,
Hash,
Wallet
User,
Wallet,
AlertCircle,
Clock
} from 'lucide-react';
import { toast } from 'sonner';
import { onboardingService } from '../../services/onboarding.service';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
// Simple helper for class merging if 'cn' is not available
const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
interface FinancePaymentDetailsPageProps {
applicationId: string;
onBack: () => void;
}
// Mock data - in real app this would come from API
const getApplicationData = (id: string) => {
return {
id: id,
registrationNumber: 'REG-2024-001',
name: 'Amit Sharma',
email: 'amit.sharma@email.com',
phone: '+91 98765 43210',
location: 'Mumbai, Maharashtra',
securityDeposit: '₹5,00,000',
securityDepositNum: 500000,
paymentMode: 'NEFT',
transactionId: 'NEFT24567890123',
bankName: 'HDFC Bank',
accountNumber: '****6789',
submittedDate: '2025-10-01',
dueDate: '2025-10-08',
paymentStatus: 'Pending Verification',
documents: [
{ name: 'Payment Receipt.pdf', size: '245 KB', uploadedOn: '2025-10-01' },
{ name: 'Bank Statement.pdf', size: '512 KB', uploadedOn: '2025-10-01' }
]
};
};
export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaymentDetailsPageProps) {
const application = getApplicationData(applicationId);
const [application, setApplication] = useState<any>(null);
const [deposits, setDeposits] = useState<any[]>([]);
const [activeType, setActiveType] = useState<'INITIAL' | 'FINAL'>('INITIAL');
const [loading, setLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [configs, setConfigs] = useState<any>({});
const [paymentDetails, setPaymentDetails] = useState({
verificationTransactionId: '',
receivedAmount: application.securityDepositNum.toString(),
receivedAmount: '',
receivedDate: new Date().toISOString().split('T')[0],
verificationRemarks: ''
});
const [uploadedDocuments, setUploadedDocuments] = useState<any[]>([]);
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [previewDoc, setPreviewDoc] = useState<any>(null);
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
const newDocs = Array.from(files).map(file => ({
name: file.name,
size: `${(file.size / 1024).toFixed(0)} KB`,
uploadedOn: new Date().toISOString().split('T')[0]
}));
setUploadedDocuments([...uploadedDocuments, ...newDocs]);
toast.success(`${files.length} document(s) uploaded successfully`);
const activeDeposit = deposits.find(d => d.depositType === activeType);
useEffect(() => {
fetchData();
}, [applicationId]);
useEffect(() => {
if (activeDeposit) {
setPaymentDetails({
verificationTransactionId: activeDeposit.paymentReference || '',
receivedAmount: activeDeposit.amount?.toString() || '',
receivedDate: activeDeposit.verifiedAt ? new Date(activeDeposit.verifiedAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
verificationRemarks: activeDeposit.remarks || ''
});
} else {
const initialDefault = configs.INITIAL_SECURITY_DEPOSIT?.amount || 500000;
const finalDefault = configs.FINAL_SECURITY_DEPOSIT?.amount || 1500000;
setPaymentDetails({
verificationTransactionId: '',
receivedAmount: activeType === 'INITIAL' ? initialDefault.toString() : finalDefault.toString(),
receivedDate: new Date().toISOString().split('T')[0],
verificationRemarks: ''
});
}
}, [activeType, activeDeposit, configs]);
const fetchData = async () => {
try {
setLoading(true);
const [appData, depositData, configData] = await Promise.all([
onboardingService.getApplicationById(applicationId),
onboardingService.getSecurityDeposit(applicationId),
onboardingService.getSystemConfigs({ category: 'SECURITY_DEPOSIT', format: 'map' })
]);
setApplication(appData);
setDeposits(Array.isArray(depositData) ? depositData : [depositData].filter(Boolean));
setConfigs(configData || {});
} catch (error) {
console.error('Fetch error:', error);
toast.error('Failed to load payment data');
} finally {
setLoading(false);
}
};
const handleApprovePayment = () => {
const handleApprovePayment = async () => {
if (!paymentDetails.verificationTransactionId || !paymentDetails.receivedDate) {
toast.error('Please fill in all required payment details');
return;
}
if (paymentDetails.receivedAmount !== application.securityDepositNum.toString()) {
toast.warning('Received amount differs from expected amount');
}
try {
setIsSubmitting(true);
await onboardingService.updateSecurityDeposit({
applicationId,
depositType: activeType,
amount: Number(paymentDetails.receivedAmount),
paymentReference: paymentDetails.verificationTransactionId,
status: 'Verified'
});
toast.success(`Payment verified and approved for ${application.name}`);
setTimeout(() => onBack(), 1500);
toast.success(`${activeType === 'INITIAL' ? 'Advance' : 'Final'} payment verified and approved`);
await fetchData();
} catch (error) {
toast.error('Failed to verify payment');
} finally {
setIsSubmitting(false);
}
};
const handleRejectPayment = () => {
const handleRejectPayment = async () => {
if (!paymentDetails.verificationRemarks) {
toast.error('Please provide remarks for rejection');
return;
}
toast.error(`Payment rejected for ${application.name}`);
setTimeout(() => onBack(), 1500);
try {
setIsSubmitting(true);
await onboardingService.updateSecurityDeposit({
applicationId,
depositType: activeType,
status: 'Rejected',
remarks: paymentDetails.verificationRemarks
});
toast.error(`${activeType === 'INITIAL' ? 'Advance' : 'Final'} payment rejected`);
await fetchData();
} catch (error) {
toast.error('Failed to reject payment');
} finally {
setIsSubmitting(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div>
</div>
);
}
if (!application) {
return <div className="p-20 text-center">Application not found</div>;
}
return (
<div className="space-y-6">
{/* Header */}
@ -112,297 +163,325 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</Button>
<div>
<h1 className="text-3xl mb-1">Payment Verification</h1>
<p className="text-slate-600">Review and verify advance payment details</p>
<div className="flex gap-2 mt-2">
<Button
size="sm"
variant={activeType === 'INITIAL' ? 'default' : 'outline'}
className={activeType === 'INITIAL' ? 'bg-amber-600 hover:bg-amber-700' : ''}
onClick={() => setActiveType('INITIAL')}
>
Advance Payment
</Button>
<Button
size="sm"
variant={activeType === 'FINAL' ? 'default' : 'outline'}
className={activeType === 'FINAL' ? 'bg-amber-600 hover:bg-amber-700' : ''}
onClick={() => setActiveType('FINAL')}
>
Final Security Deposit
</Button>
</div>
</div>
</div>
{/* Status Banner */}
<Card className="border-amber-200 bg-amber-50">
<Card className={cn(
"border",
activeDeposit?.status === 'Verified' ? "border-green-200 bg-green-50" :
activeDeposit?.status === 'Rejected' ? "border-red-200 bg-red-50" :
"border-amber-200 bg-amber-50"
)}>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-amber-100 flex items-center justify-center">
<DollarSign className="w-6 h-6 text-amber-600" />
<div className={cn(
"w-12 h-12 rounded-full flex items-center justify-center",
activeDeposit?.status === 'Verified' ? "bg-green-100" :
activeDeposit?.status === 'Rejected' ? "bg-red-100" :
"bg-amber-100"
)}>
<DollarSign className={cn(
"w-6 h-6",
activeDeposit?.status === 'Verified' ? "text-green-600" :
activeDeposit?.status === 'Rejected' ? "text-red-600" :
"text-amber-600"
)} />
</div>
<div>
<p className="text-slate-900">Payment Pending Verification</p>
<p className="text-sm text-slate-600">Due Date: {application.dueDate}</p>
<p className="text-slate-900 font-bold">
{activeType === 'INITIAL' ? 'Advance Payment' : 'Final Security Deposit'}
</p>
<p className="text-sm text-slate-600">
{activeDeposit?.status === 'Verified'
? `Verified on ${new Date(activeDeposit.verifiedAt).toLocaleDateString()}`
: activeDeposit?.status === 'Rejected'
? 'Payment Rejected'
: 'Awaiting Verification'}
</p>
</div>
</div>
<Badge className="bg-amber-600">
{application.paymentStatus}
<Badge className={cn(
activeDeposit?.status === 'Verified' ? "bg-green-600" :
activeDeposit?.status === 'Rejected' ? "bg-red-600" :
"bg-amber-600 text-white"
)}>
{activeDeposit?.status || 'No Record'}
</Badge>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Application & Payment Info */}
<div className="lg:col-span-2 space-y-6">
{/* Application Details */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="w-5 h-5" />
<CardTitle className="flex items-center gap-2 text-xl">
<User className="w-5 h-5 text-amber-600" />
Applicant Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-500">Application ID</Label>
<p className="text-slate-900">{application.id}</p>
</div>
<div>
<Label className="text-slate-500">Registration Number</Label>
<p className="text-slate-900">{application.registrationNumber}</p>
</div>
<div>
<Label className="text-slate-500">Applicant Name</Label>
<p className="text-slate-900">{application.name}</p>
</div>
<div>
<Label className="text-slate-500">Email</Label>
<p className="text-slate-900">{application.email}</p>
</div>
<div>
<Label className="text-slate-500">Phone</Label>
<p className="text-slate-900">{application.phone}</p>
</div>
<div>
<Label className="text-slate-500">Location</Label>
<p className="text-slate-900">{application.location}</p>
</div>
<CardContent className="grid grid-cols-2 gap-y-4 gap-x-8">
<div>
<Label className="text-slate-500">Application ID</Label>
<p className="text-slate-900 font-medium">{application.applicationId || application.id}</p>
</div>
<div>
<Label className="text-slate-500">Applicant Name</Label>
<p className="text-slate-900 font-medium">{application.applicantName}</p>
</div>
<div>
<Label className="text-slate-500">Location</Label>
<p className="text-slate-900 font-medium">{application.city || application.preferredLocation}, {application.state}</p>
</div>
<div>
<Label className="text-slate-500">Email / Phone</Label>
<p className="text-slate-700 text-sm">{application.email}</p>
<p className="text-slate-700 text-sm">{application.phone}</p>
</div>
</CardContent>
</Card>
{/* Payment Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CreditCard className="w-5 h-5" />
Submitted Payment Details
<CardTitle className="flex items-center gap-2 text-xl">
<CreditCard className="w-5 h-5 text-amber-600" />
Deposit Tracking
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-500">Security Deposit</Label>
<p className="text-slate-900 text-2xl text-green-600">{application.securityDeposit}</p>
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200">
<Label className="text-slate-500 block mb-1">Expected Amount</Label>
<p className="text-2xl font-bold text-amber-900">
{(activeType === 'INITIAL'
? (configs.INITIAL_SECURITY_DEPOSIT?.amount || 500000)
: (configs.FINAL_SECURITY_DEPOSIT?.amount || 1500000)
).toLocaleString()}
</p>
</div>
<div>
<Label className="text-slate-500">Payment Mode</Label>
<p className="text-slate-900">{application.paymentMode}</p>
</div>
<div>
<Label className="text-slate-500">Transaction ID</Label>
<p className="text-slate-900">{application.transactionId}</p>
</div>
<div>
<Label className="text-slate-500">Bank Name</Label>
<p className="text-slate-900">{application.bankName}</p>
</div>
<div>
<Label className="text-slate-500">Account Number</Label>
<p className="text-slate-900">{application.accountNumber}</p>
</div>
<div>
<Label className="text-slate-500">Submitted Date</Label>
<p className="text-slate-900">{application.submittedDate}</p>
<div className={cn(
"p-4 rounded-lg border",
activeDeposit?.status === 'Verified' ? "bg-green-50 border-green-200" : "bg-blue-50 border-blue-200"
)}>
<Label className="text-slate-500 block mb-1">Receipt Status</Label>
<p className={cn(
"text-2xl font-bold",
activeDeposit?.status === 'Verified' ? "text-green-700" : "text-blue-700"
)}>
{activeDeposit?.status || 'Not Started'}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Submitted Documents */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Submitted Documents
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{application.documents.map((doc, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border border-slate-200">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-slate-900">{doc.name}</p>
<p className="text-sm text-slate-500">{doc.size} Uploaded on {doc.uploadedOn}</p>
</div>
</div>
<Button variant="outline" size="sm">
Download
</Button>
{activeDeposit?.paymentReference && (
<div className="grid grid-cols-2 gap-4 pt-2">
<div>
<Label className="text-slate-500">Payment Reference</Label>
<p className="text-slate-900 font-mono">{activeDeposit.paymentReference}</p>
</div>
))}
</div>
<div>
<Label className="text-slate-500">Verified By</Label>
<p className="text-slate-900">{activeDeposit.verifiedBy || 'N/A'}</p>
</div>
</div>
)}
</CardContent>
</Card>
{/* Upload Additional Documents */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5" />
Upload Additional Documents
<CardTitle className="flex items-center gap-2 text-xl">
<FileText className="w-5 h-5 text-amber-600" />
Verification Evidence
</CardTitle>
<CardDescription>
Upload any additional verification documents or receipts
</CardDescription>
<CardDescription>Documents uploaded by the applicant for payment proof</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:border-amber-400 hover:bg-amber-50 transition-colors">
<Upload className="w-8 h-8 text-slate-400 mx-auto mb-2" />
<p className="text-slate-600 mb-2">Click to upload or drag and drop</p>
<p className="text-sm text-slate-500">PDF, DOC, DOCX, PNG, JPG (max 10MB)</p>
<input
type="file"
multiple
className="hidden"
id="file-upload"
onChange={handleFileUpload}
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"
/>
<label htmlFor="file-upload">
<Button variant="outline" className="mt-4" asChild>
<span>Choose Files</span>
</Button>
</label>
</div>
{uploadedDocuments.length > 0 && (
<div className="space-y-2">
<Label>Uploaded Documents</Label>
{uploadedDocuments.map((doc, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-green-600" />
<div>
<p className="text-slate-900">{doc.name}</p>
<p className="text-sm text-slate-500">{doc.size}</p>
</div>
{application.uploadedDocuments?.filter((d: any) =>
activeType === 'INITIAL'
? d.documentType?.toLowerCase().includes('initial') && d.documentType?.toLowerCase().includes('deposit')
: d.documentType?.toLowerCase().includes('final') && d.documentType?.toLowerCase().includes('deposit')
).length > 0 ? (
<div className="space-y-3">
{application.uploadedDocuments.filter((d: any) =>
activeType === 'INITIAL'
? d.documentType?.toLowerCase().includes('initial') && d.documentType?.toLowerCase().includes('deposit')
: d.documentType?.toLowerCase().includes('final') && d.documentType?.toLowerCase().includes('deposit')
).map((doc: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-white rounded-lg border border-slate-200 hover:shadow-sm transition-shadow">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded bg-slate-100 flex items-center justify-center">
<FileText className="w-5 h-5 text-slate-500" />
</div>
<div>
<p className="text-slate-900 font-medium">{doc.fileName || doc.name}</p>
<p className="text-xs text-slate-500 uppercase">{doc.documentType} {new Date(doc.createdAt).toLocaleDateString()}</p>
</div>
</div>
))}
</div>
)}
</div>
<Button
variant="ghost"
size="sm"
className="text-amber-600 hover:text-amber-700 hover:bg-amber-50"
onClick={() => {
setPreviewDoc(doc);
setShowPreviewModal(true);
}}
>
View Receipt
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-10 bg-slate-50 rounded-lg border-2 border-dashed border-slate-200">
<AlertCircle className="w-8 h-8 text-slate-300 mx-auto mb-2" />
<p className="text-slate-500">No payment documents found in this application.</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Right Column - Verification Form */}
<div className="space-y-6">
<Card className="sticky top-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Wallet className="w-5 h-5" />
Payment Verification
<Card className="border-amber-100 shadow-sm">
<CardHeader className="bg-amber-50/50">
<CardTitle className="flex items-center gap-2 text-lg">
<Wallet className="w-5 h-5 text-amber-600" />
Finance Action
</CardTitle>
<CardDescription>
Enter payment verification details
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="pt-6 space-y-4">
<div>
<Label htmlFor="verificationTxnId">
Verification Transaction ID <span className="text-red-500">*</span>
<Label htmlFor="verificationTxnId" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
UTR / Reference Number <span className="text-red-500">*</span>
</Label>
<Input
id="verificationTxnId"
placeholder="Enter transaction ID"
placeholder="Enter Bank UTR Number"
disabled={activeDeposit?.status === 'Verified'}
className="mt-1"
value={paymentDetails.verificationTransactionId}
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationTransactionId: e.target.value })}
/>
</div>
<div>
<Label htmlFor="receivedAmount">
Received Amount () <span className="text-red-500">*</span>
<Label htmlFor="receivedAmount" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
Amount Received () <span className="text-red-500">*</span>
</Label>
<Input
id="receivedAmount"
type="number"
placeholder="Enter received amount"
placeholder={(activeType === 'INITIAL' ? 500000 : 1500000).toString()}
disabled={activeDeposit?.status === 'Verified'}
className="mt-1"
value={paymentDetails.receivedAmount}
onChange={(e) => setPaymentDetails({ ...paymentDetails, receivedAmount: e.target.value })}
/>
{paymentDetails.receivedAmount !== application.securityDepositNum.toString() && (
<p className="text-sm text-amber-600 mt-1 flex items-center gap-1">
<XCircle className="w-3 h-3" />
Amount differs from expected: {application.securityDeposit}
</p>
)}
</div>
<div>
<Label htmlFor="receivedDate">
Payment Received Date <span className="text-red-500">*</span>
<Label htmlFor="receivedDate" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
Credit Value Date <span className="text-red-500">*</span>
</Label>
<Input
id="receivedDate"
type="date"
disabled={activeDeposit?.status === 'Verified'}
className="mt-1"
value={paymentDetails.receivedDate}
onChange={(e) => setPaymentDetails({ ...paymentDetails, receivedDate: e.target.value })}
/>
</div>
<div>
<Label htmlFor="verificationRemarks">Verification Remarks</Label>
<Label htmlFor="remarks" className="text-xs uppercase text-slate-500 font-bold tracking-wider">Verification Remarks</Label>
<Textarea
id="verificationRemarks"
placeholder="Enter any remarks or notes..."
rows={4}
id="remarks"
placeholder="Any internal notes for reconciliation..."
rows={3}
className="mt-1"
value={paymentDetails.verificationRemarks}
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationRemarks: e.target.value })}
/>
</div>
<div className="pt-4 border-t">
<Button
className="w-full bg-green-600 hover:bg-green-700"
<div className="pt-4 space-y-3">
<Button
className={cn(
"w-full transition-all duration-200",
activeDeposit?.status === 'Verified' ? "bg-green-600 hover:bg-green-600 opacity-90" : "bg-amber-600 hover:bg-amber-700"
)}
onClick={handleApprovePayment}
disabled={isSubmitting || activeDeposit?.status === 'Verified'}
>
<CheckCircle className="w-4 h-4 mr-2" />
Confirm Payment Received
{activeDeposit?.status === 'Verified' ? (
<><CheckCircle className="w-4 h-4 mr-2" /> Verified Successfully</>
) : (
<><CheckCircle className="w-4 h-4 mr-2" /> Mark as Verified</>
)}
</Button>
{activeDeposit?.status !== 'Verified' && activeDeposit?.status !== 'Rejected' && (
<Button
variant="ghost"
className="w-full text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={handleRejectPayment}
disabled={isSubmitting}
>
<XCircle className="w-4 h-4 mr-2" />
Reject / Flag Discrepancy
</Button>
)}
</div>
</CardContent>
</Card>
{/* Quick Info Card */}
<Card className="bg-blue-50 border-blue-200">
<Card className="bg-slate-900 text-white border-none shadow-xl">
<CardHeader>
<CardTitle className="text-base">Verification Checklist</CardTitle>
<CardTitle className="text-base font-medium flex items-center gap-2">
<Clock className="w-4 h-4 text-amber-400" />
Next Steps
</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 transaction ID matches bank records</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
<span>Confirm amount received in company account</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 bank verification receipt if available</span>
</li>
<CardContent className="text-xs text-slate-300 space-y-3">
<p>Once verified, the following will occur:</p>
<ul className="list-disc pl-4 space-y-2">
<li>Applicant status will advance to {activeType === 'INITIAL' ? 'LOI Issuance' : 'LOA Approval'}</li>
<li>Email notification will be sent to Applicant</li>
<li>Digital {activeType === 'INITIAL' ? 'LOI' : 'LOA'} generation will be unlocked</li>
</ul>
</CardContent>
</Card>
</div>
</div>
{/* Global Preview Modal */}
<DocumentPreviewModal
isOpen={showPreviewModal}
onClose={() => setShowPreviewModal(false)}
document={previewDoc}
/>
</div>
);
}

View File

@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import {
Tabs, TabsContent, TabsList, TabsTrigger
} from '../ui/tabs';
import { Globe, Shield, Clock, Mail, MapPin, SlidersHorizontal } from 'lucide-react';
import { Globe, Shield, Clock, Mail, MapPin, SlidersHorizontal, Settings } from 'lucide-react';
import { Badge } from '../ui/badge';
import { toast } from 'sonner';
@ -31,6 +31,7 @@ import { ZoneDialog } from './MasterPage/ZoneDialog';
import { RegionDialog } from './MasterPage/RegionDialog';
import { TemplateDialog } from './MasterPage/TemplateDialog';
import { LocationDialog } from './MasterPage/LocationDialog';
import { SecurityDepositMaster } from './MasterPage/SecurityDepositMaster';
import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
import { RootState } from '../../store';
@ -455,9 +456,12 @@ export const MasterPage: React.FC = () => {
<TabsTrigger value="locations" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<MapPin className="w-4 h-4" /> Locations
</TabsTrigger>
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<SlidersHorizontal className="w-4 h-4" /> Approvals
</TabsTrigger>
<TabsTrigger value="settings" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<Settings className="w-4 h-4" /> App Settings
</TabsTrigger>
</TabsList>
<TabsContent value="hierarchy" className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
@ -551,6 +555,10 @@ export const MasterPage: React.FC = () => {
<TabsContent value="approvals" className="animate-in fade-in duration-300">
<ApprovalPoliciesPage />
</TabsContent>
<TabsContent value="settings" className="animate-in fade-in duration-300">
<SecurityDepositMaster />
</TabsContent>
</Tabs>
)}

View File

@ -0,0 +1,156 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../../ui/card';
import { Input } from '../../ui/input';
import { Button } from '../../ui/button';
import { Label } from '../../ui/label';
import { Save, RefreshCw, IndianRupee, Settings } from 'lucide-react';
import { masterService } from '../../../services/master.service';
import { toast } from 'sonner';
export const SecurityDepositMaster: React.FC = () => {
const [configs, setConfigs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isSaving, setIsSaving] = useState<string | null>(null);
const fetchConfigs = async () => {
try {
setLoading(true);
const res = await masterService.getSystemConfigs({ category: 'SECURITY_DEPOSIT' }) as any;
if (res.success) {
setConfigs(res.data);
}
} catch (error) {
console.error('Fetch error:', error);
toast.error('Failed to load deposit configurations');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchConfigs();
}, []);
const handleUpdateAmount = (id: string, newAmount: string) => {
setConfigs(prev => prev.map(c =>
c.id === id ? { ...c, value: { ...c.value, amount: parseInt(newAmount) || 0 } } : c
));
};
const handleSave = async (config: any) => {
try {
setIsSaving(config.id);
const res = await masterService.saveSystemConfig(config) as any;
if (res.success) {
toast.success(`Configuration for ${config.key} updated successfully`);
}
} catch (error) {
toast.error('Failed to save configuration');
} finally {
setIsSaving(null);
}
};
if (loading) {
return (
<div className="flex flex-col items-center justify-center p-20 space-y-4">
<div className="w-10 h-10 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div>
<p className="text-slate-600 animate-pulse">Loading settings...</p>
</div>
);
}
return (
<div className="space-y-4 animate-in fade-in duration-500 max-w-4xl">
<Card className="border-none shadow-lg bg-white/80 backdrop-blur-md">
<CardHeader className="py-4 border-b bg-slate-50/50">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center">
<Settings className="w-5 h-5 text-amber-600" />
</div>
<div>
<CardTitle className="text-lg font-bold text-slate-900">
Global Payment Settings
</CardTitle>
<CardDescription className="text-xs">
Configure base security deposit amounts for onboarding workflows.
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-4">
<div className="grid gap-3">
{configs.map((config) => (
<div key={config.id} className="p-3 border rounded-xl bg-white hover:shadow-sm transition-shadow flex items-center justify-between border-slate-100">
<div className="space-y-0.5 max-w-md">
<h4 className="font-bold text-slate-800 text-sm flex items-center gap-2">
{config.key.replace(/_/g, ' ')}
<span className="text-[9px] bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded-full uppercase tracking-wider">Master</span>
</h4>
<p className="text-xs text-slate-500 leading-tight">{config.description}</p>
</div>
<div className="flex items-center gap-4">
<div className="space-y-1">
<Label htmlFor={`amount-${config.id}`} className="text-[10px] font-semibold text-slate-400 uppercase tracking-wider block">Amount (INR)</Label>
<div className="relative w-40">
<IndianRupee className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400 font-bold" />
<Input
id={`amount-${config.id}`}
type="number"
className="pl-8 h-9 text-base font-bold bg-slate-50/50 border-slate-200 focus:ring-amber-500 focus:border-amber-500 rounded-lg"
value={config.value?.amount || ''}
onChange={(e) => handleUpdateAmount(config.id, e.target.value)}
/>
</div>
</div>
<div className="pt-4">
<Button
onClick={() => handleSave(config)}
disabled={isSaving === config.id}
className="h-9 px-4 bg-amber-600 hover:bg-amber-700 text-white rounded-lg shadow-md shadow-amber-600/10 active:scale-95 transition-all flex items-center gap-1.5 group"
>
{isSaving === config.id ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<>
<Save className="w-4 h-4 group-hover:scale-110 transition-transform" />
<span className="font-bold text-sm">Update</span>
</>
)}
</Button>
</div>
</div>
</div>
))}
</div>
{configs.length === 0 && (
<div className="text-center py-12 bg-slate-50/50 rounded-xl border border-dashed border-slate-200">
<div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-3">
<Settings className="w-6 h-6 text-slate-400" />
</div>
<p className="text-slate-500 text-sm font-medium">No configurations found.</p>
<Button variant="outline" className="mt-4 rounded-lg h-9 text-xs" onClick={fetchConfigs}>
<RefreshCw className="w-3.5 h-3.5 mr-1.5" /> Reload
</Button>
</div>
)}
</CardContent>
</Card>
<div className="bg-amber-50/50 rounded-xl p-4 border border-amber-100/50 flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center flex-shrink-0">
<Settings className="w-4 h-4 text-amber-700" />
</div>
<div>
<h5 className="font-bold text-amber-900 text-sm">Super Admin Notice</h5>
<p className="text-[11px] text-amber-800/80 leading-snug">
Updates made here take immediate effect. These values define the default expected amounts for all current and future onboarding payments.
</p>
</div>
</div>
</div>
);
};

View File

@ -119,20 +119,52 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
const getCurrentStageIndex = () => {
if (!request) return 1;
const stageMap: Record<string, number> = {
'Submitted': 1,
'Dealer': 1,
'DD Admin Review': 2,
'ASM': 2,
'ASM Review': 2,
'RBM Review': 3,
'RBM': 3,
'DD ZM Review': 4,
'DD-ZM': 4,
'ZBH Review': 5,
'ZBH': 5,
'DD Lead Review': 6,
'DD Lead': 6,
'DD Head Review': 7,
'DD Head': 7,
'NBH Review': 8,
'NBH Approval': 8,
'NBH': 8,
'DD H.O': 9, // Parallel branch A
'Architect': 9, // Parallel branch B
'Legal Clearance': 10,
'Closed': 11,
'Completed': 11
};
return stageMap[request.currentStage] || 1;
// Map backend stage values to frontend stage indices
const backendStage = request.currentStage?.replace(/_/g, ' ') || 'DD Admin Review';
return stageMap[backendStage] || 2; // Default to stage 2 (DD Admin Review) for new requests
};
// Helper to find assigned reviewer for a stage
const getAssignedReviewer = (stageKey: string) => {
if (!request || !request.participants || request.participants.length === 0) return null;
const stageMap: Record<string, string> = {
'ASM Review': 'ASM_REVIEW',
'RBM Review': 'RBM_REVIEW',
'DD ZM Review': 'DD_ZM_REVIEW',
'ZBH Review': 'ZBH_REVIEW',
'DD Lead Review': 'DD_LEAD_REVIEW',
'NBH Review': 'NBH_REVIEW',
'Legal Clearance': 'LEGAL_CLEARANCE'
};
const targetStage = stageMap[stageKey];
if (!targetStage) return null;
const participant = request.participants.find((p: any) => p.metadata?.stage === targetStage);
if (!participant) return null;
return participant.user?.fullName || participant.user?.roleCode || null;
};
const currentStageIndex = getCurrentStageIndex();
@ -451,6 +483,11 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}>
Responsible: {stage.role}
</p>
{getAssignedReviewer(stage.name) && (
<p className="text-xs text-blue-600 font-medium mt-1">
Assigned: {getAssignedReviewer(stage.name)}
</p>
)}
</div>
<Badge className={
isCompleted ? 'bg-green-100 text-green-700 border-green-300' :
@ -661,8 +698,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</Card>
</div>
{/* Right Sidebar - Actions */}
<div className="space-y-6">
{/* Right Sidebar - Actions */}
<div className="space-y-6">
{/* Current Status Card */}
<Card>
<CardHeader>
@ -747,6 +785,35 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</Button>
</CardContent>
</Card>
{/* Assigned Evaluators Card */}
{request.participants && request.participants.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Assigned Evaluators</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{request.participants.map((participant: any) => (
<div key={participant.id} className="flex items-center justify-between p-2 bg-slate-50 rounded-lg">
<div>
<p className="text-slate-900 text-sm font-medium">{participant.user?.fullName || 'Unknown'}</p>
<p className="text-slate-600 text-xs">{participant.user?.roleCode || 'User'}</p>
{participant.metadata?.stage && (
<Badge variant="outline" className="mt-1 text-xs">
{participant.metadata.stage.replace(/_/g, ' ')}
</Badge>
)}
</div>
{participant.metadata?.autoAssigned && (
<Badge className="bg-blue-100 text-blue-700 border-blue-300 text-xs">
Auto-assigned
</Badge>
)}
</div>
))}
</CardContent>
</Card>
)}
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -231,8 +231,18 @@ export function ProspectiveDashboardPage() {
<option value="PAN Card">PAN Card</option>
<option value="GST Certificate">GST Certificate</option>
<option value="Aadhaar Card">Aadhaar Card</option>
<option value="Trade License">Trade License</option>
<option value="Partnership Deed">Partnership Deed</option>
<option value="LLP Agreement">LLP Agreement</option>
<option value="Certificate of Incorporation">Certificate of Incorporation</option>
<option value="MOA">MOA</option>
<option value="AOA">AOA</option>
<option value="Board Resolution">Board Resolution</option>
<option value="Initial Security Deposit Receipt">Initial Security Deposit Receipt</option>
<option value="Final Security Deposit Receipt">Final Security Deposit Receipt</option>
<option value="Trade License/Firm Registration">Trade License/Firm Registration</option>
<option value="Bank Statement">Bank Statement</option>
<option value="Cancelled Check">Cancelled Check</option>
<option value="Rental Agreement">Rental Agreement</option>
<option value="Property Document">Property Document</option>
<option value="Other">Other</option>
</select>

View File

@ -12,6 +12,7 @@ import { useState, useEffect } from 'react';
import { User as UserType } from '../../lib/mock-data';
import { toast } from 'sonner';
import { dealerService } from '../../services/dealer.service';
import { masterService } from '../../services/master.service';
interface DealerRelocationPageProps {
currentUser: UserType | null;
@ -33,6 +34,13 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
const [newAddress, setNewAddress] = useState('');
const [reason, setReason] = useState('');
// State/District dropdown data
const [states, setStates] = useState<any[]>([]);
const [districts, setDistricts] = useState<any[]>([]);
const [selectedStateId, setSelectedStateId] = useState('');
const [selectedDistrictId, setSelectedDistrictId] = useState('');
const [masterDataLoading, setMasterDataLoading] = useState(false);
const [outlets, setOutlets] = useState<any[]>([]);
const [requests, setRequests] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@ -41,8 +49,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
useEffect(() => {
fetchData();
fetchMasterData();
}, []);
// Filter districts based on selected state
const filteredDistricts = selectedStateId
? districts.filter(d => d.stateId === selectedStateId)
: districts;
const fetchData = async () => {
try {
setLoading(true);
@ -60,6 +74,43 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
}
};
const fetchMasterData = async () => {
try {
setMasterDataLoading(true);
const [statesRes, districtsRes] = await Promise.all([
masterService.getStates().catch(() => ({ success: false })) as Promise<any>,
masterService.getDistricts({ limit: 'all' }).catch(() => ({ success: false })) as Promise<any>
]);
const statesData = statesRes?.success ? (statesRes.data?.states || statesRes.data || []) : [];
const districtsData = districtsRes?.success ? (districtsRes.data?.districts || districtsRes.data || []) : [];
setStates(statesData);
setDistricts(districtsData);
} catch (error) {
console.error('Fetch master data error:', error);
} finally {
setMasterDataLoading(false);
}
};
const handleStateChange = (stateId: string) => {
setSelectedStateId(stateId);
setSelectedDistrictId(''); // Reset district when state changes
const selectedState = states.find(s => s.id === stateId);
if (selectedState) {
setNewState(selectedState.name);
}
};
const handleDistrictChange = (districtId: string) => {
setSelectedDistrictId(districtId);
const selectedDistrict = districts.find(d => d.id === districtId);
if (selectedDistrict) {
setNewCity(selectedDistrict.name);
}
};
const handleOpenRelocationDialog = (outlet: any) => {
setSelectedOutlet(outlet);
setIsDialogOpen(true);
@ -87,11 +138,17 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
setSubmitting(true);
const payload = {
outletId: selectedOutlet.id,
currentLocation: selectedOutlet.location,
relocationType: 'Intercity',
currentAddress: selectedOutlet.address || '',
currentCity: selectedOutlet.city || '',
currentState: selectedOutlet.state || '',
newAddress,
newCity,
newState,
newAddress,
reason
newDistrictId: selectedDistrictId || null,
newStateId: selectedStateId || null,
reason,
proposedDate: null
};
await dealerService.submitRelocationRequest(payload);
@ -198,29 +255,49 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
</div>
)}
{/* New Location Details */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="newCity">Proposed City *</Label>
<Input
id="newCity"
placeholder="Enter new city"
value={newCity}
onChange={(e) => setNewCity(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="newState">Proposed State *</Label>
<Input
id="newState"
placeholder="Enter new state"
value={newState}
onChange={(e) => setNewState(e.target.value)}
required
/>
</div>
</div>
{/* New Location Details - State/District Dropdowns */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="newState">Proposed State *</Label>
<Select
value={selectedStateId}
onValueChange={handleStateChange}
required
disabled={masterDataLoading}
>
<SelectTrigger id="newState">
<SelectValue placeholder={masterDataLoading ? "Loading..." : "Select state"} />
</SelectTrigger>
<SelectContent>
{states.map((state) => (
<SelectItem key={state.id} value={state.id}>
{state.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="newCity">Proposed City/District *</Label>
<Select
value={selectedDistrictId}
onValueChange={handleDistrictChange}
required
disabled={!selectedStateId || masterDataLoading}
>
<SelectTrigger id="newCity">
<SelectValue placeholder={!selectedStateId ? "Select state first" : masterDataLoading ? "Loading..." : "Select district"} />
</SelectTrigger>
<SelectContent>
{filteredDistricts.map((district) => (
<SelectItem key={district.id} value={district.id}>
{district.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="newAddress">Proposed Full Address *</Label>

View File

@ -0,0 +1,203 @@
import React, { useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription
} from './dialog';
import { Button } from './button';
import {
Eye,
Download,
RotateCw,
RefreshCw,
Plus,
Minus,
FileText
} from 'lucide-react';
interface DocumentPreviewModalProps {
isOpen: boolean;
onClose: () => void;
document: {
fileName: string;
filePath: string;
documentType?: string;
createdAt?: string;
mimeType?: string;
} | null;
}
export const DocumentPreviewModal: React.FC<DocumentPreviewModalProps> = ({
isOpen,
onClose,
document
}) => {
const [zoomScale, setZoomScale] = useState(1);
const [rotation, setRotation] = useState(0);
if (!document) return null;
const handleReset = () => {
setZoomScale(1);
setRotation(0);
};
const baseUrl = 'http://localhost:5000';
const fileUrl = `${baseUrl}/${document.filePath}`;
return (
<Dialog open={isOpen} onOpenChange={(open) => {
if (!open) {
handleReset();
onClose();
}
}}>
{/* Overriding sm:max-w-lg with sm:max-w-[95vw] and sm:h-[95vh] */}
{/* Also hiding the default close button injected by DialogContent */}
<DialogContent className="fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-[80vw] max-w-[80vw] h-[80vh] sm:max-w-[80vw] sm:h-[80vh] overflow-hidden flex flex-col p-0 bg-white border-slate-200 shadow-2xl [&>button]:hidden z-[100]">
{/* Simple Standard Header */}
<div className="flex items-center justify-between px-6 py-3 border-b bg-slate-50 flex-shrink-0">
<div className="flex items-center gap-3 min-w-0 pr-4">
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center border border-amber-200 flex-shrink-0">
<Eye className="w-4 h-4 text-amber-600" />
</div>
<div className="min-w-0 truncate">
<DialogTitle className="text-slate-900 text-sm font-semibold truncate">
{document.fileName || 'Document Preview'}
</DialogTitle>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[10px] text-slate-500 font-medium truncate">{document.documentType || 'Document'}</span>
{document.createdAt && (
<>
<span className="text-slate-300 text-[10px] flex-shrink-0"></span>
<span className="text-[10px] text-slate-400 truncate">
{new Date(document.createdAt).toLocaleDateString()}
</span>
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-4 flex-shrink-0 pl-2 pr-10"> {/* pr-10 to leave space for the X button we'll reveal */}
{/* Standard Control Group */}
<div className="flex items-center gap-1 bg-white border border-slate-200 p-1 rounded-lg shadow-sm">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={() => setZoomScale(s => Math.max(0.25, s - 0.25))}
title="Zoom Out"
>
<Minus className="w-3.5 h-3.5" />
</Button>
<div className="px-2 min-w-[50px] text-center">
<span className="text-[11px] font-medium text-slate-600">
{Math.round(zoomScale * 100)}%
</span>
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={() => setZoomScale(s => Math.min(4, s + 0.25))}
title="Zoom In"
>
<Plus className="w-3.5 h-3.5" />
</Button>
<div className="w-px h-4 bg-slate-200 mx-1"></div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={() => setRotation(r => (r + 90) % 360)}
title="Rotate"
>
<RotateCw className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-500 hover:text-slate-900 hover:bg-slate-100"
onClick={handleReset}
title="Reset"
>
<RefreshCw className="w-3.5 h-3.5" />
</Button>
</div>
<Button
variant="outline"
size="sm"
className="h-8 gap-2 text-xs font-medium border-slate-200 hover:bg-slate-50 hidden sm:flex"
onClick={() => window.open(fileUrl, '_blank')}
>
<Download className="w-3.5 h-3.5 text-slate-500" />
Download
</Button>
</div>
{/* Manually absolute-positioned standard close for the Header */}
<Button
variant="ghost"
size="icon"
className="absolute top-2.5 right-3 h-8 w-8 text-slate-400 hover:text-slate-600 rounded-lg z-50 transition-colors"
onClick={onClose}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
<span className="sr-only">Close</span>
</Button>
</div>
{/* Clean Standard Viewport Area */}
<div className="flex-1 relative overflow-hidden flex items-center justify-center bg-slate-100">
<div className="w-full h-full flex items-center justify-center overflow-auto p-4 sm:p-8 scrollbar-thin">
{document.fileName?.match(/\.(jpg|jpeg|png|gif|webp)$/i) || document.mimeType?.includes('image') ? (
<div
className="transition-transform duration-200 ease-out"
style={{ transform: `scale(${zoomScale}) rotate(${rotation}deg)` }}
>
<img
src={fileUrl}
alt={document.fileName}
className="max-h-[90vh] max-w-full object-contain shadow-xl rounded-sm border border-white"
/>
</div>
) : document.fileName?.match(/\.pdf$/i) || document.mimeType?.includes('pdf') ? (
<div className="w-full h-full flex items-center justify-center">
<iframe
src={`${fileUrl}#toolbar=0`}
className="w-full h-full max-w-7xl bg-white shadow-lg border border-slate-200"
style={{ transform: `scale(${zoomScale}) rotate(${rotation}deg)` }}
title={document.fileName}
/>
</div>
) : (
<div className="bg-white p-12 rounded-xl shadow-sm border border-slate-200 text-center max-w-md">
<div className="w-16 h-16 rounded-full bg-slate-50 flex items-center justify-center mx-auto mb-4 border border-slate-100">
<FileText className="w-8 h-8 text-slate-400" />
</div>
<h3 className="text-slate-900 font-semibold mb-2">Preview not available</h3>
<p className="text-slate-500 text-sm mb-6">
This file format cannot be previewed in the browser. You can download it to view locally.
</p>
<Button
className="bg-amber-600 hover:bg-amber-700 text-white gap-2"
onClick={() => window.open(fileUrl, '_blank')}
>
<Download className="w-4 h-4" />
Download file
</Button>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -110,8 +110,8 @@ export const useMasterData = () => {
status: r.isActive !== false ? 'Active' : 'Inactive'
}));
const asms = (bodyAsms?.data || bodyAsms || []);
const zonalManagers = (bodyZms?.data || bodyZms || []);
const asms = Array.isArray(bodyAsms?.data) ? bodyAsms.data : (Array.isArray(bodyAsms) ? bodyAsms : []);
const zonalManagers = Array.isArray(bodyZms?.data) ? bodyZms.data : (Array.isArray(bodyZms) ? bodyZms : []);
const zonalManagerMappings = zonalManagers.length > 0 ? zonalManagers : users.filter((u: any) =>
u.allRoles?.some((r: string) => (r === 'ZM' || r === 'DD-ZM' || r.includes('ZONAL MANAGER')) && !r.includes('HEAD'))
@ -162,6 +162,8 @@ export const useMasterData = () => {
reminders: s.reminderConfig?.reminders || [], escalations: s.escalationConfig?.escalations || []
}));
const ddLeads = Array.isArray(bodyDdLeads?.data) ? bodyDdLeads.data : (Array.isArray(bodyDdLeads) ? bodyDdLeads : []);
dispatch(setMasterData({
zones, regionalOffices, asms, zonalManagerMappings, zonalManagers,
roles, allStates, allDistricts, allAreas,
@ -169,7 +171,7 @@ export const useMasterData = () => {
emailTemplates: bodyEmail?.data || [],
slaConfigs,
users,
ddLeads: bodyDdLeads?.data || bodyDdLeads || [],
ddLeads,
loading: false
}));

View File

@ -140,5 +140,14 @@ export const masterService = {
saveASM: async (data: any) => {
// ASMs are handled via user update in this system
return API.updateUser(data.userId, data).then(res => res.data);
},
// System Configs
getSystemConfigs: async (params?: any) => {
const response = await API.getSystemConfigs(params);
return response.data;
},
saveSystemConfig: async (data: any) => {
const response = await API.saveSystemConfig(data);
return response.data;
}
};

View File

@ -114,5 +114,20 @@ export const onboardingService = {
const response: any = await API.retriggerEvaluators(id);
if (!response.ok) throw new Error(response.data?.message || 'Failed to retrigger evaluators');
return response.data;
},
getSecurityDeposit: async (applicationId: string) => {
const response: any = await API.getSecurityDeposit(applicationId);
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch security deposit');
return response.data?.data || response.data;
},
updateSecurityDeposit: async (data: any) => {
const response: any = await API.updateSecurityDeposit(data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to update security deposit');
return response.data;
},
getSystemConfigs: async (params?: any) => {
const response: any = await API.getSystemConfigs(params);
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch system configurations');
return response.data?.data || response.data;
}
};