onboarding flow made stable end to end also fincance team verification implementation done relocation stated
This commit is contained in:
parent
574e648618
commit
830f66b5f7
@ -48,6 +48,8 @@ export const API = {
|
|||||||
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
|
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
|
||||||
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
|
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
|
||||||
retriggerEvaluators: (id: string) => client.post(`/onboarding/applications/${id}/retrigger-evaluators`),
|
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
|
// Documents
|
||||||
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
|
uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, {
|
||||||
@ -158,6 +160,10 @@ export const API = {
|
|||||||
|
|
||||||
// SLA
|
// SLA
|
||||||
getSlaConfigs: () => client.get('/sla/configs'),
|
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;
|
export default API;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import QuestionnaireResponseView from './QuestionnaireResponseView';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { RootState } from '../../store';
|
import { RootState } from '../../store';
|
||||||
import { cn } from '@/components/ui/utils';
|
import { cn } from '@/components/ui/utils';
|
||||||
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
||||||
|
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
@ -39,6 +40,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Eye,
|
Eye,
|
||||||
|
Lock,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Progress } from '../ui/progress';
|
import { Progress } from '../ui/progress';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
@ -83,6 +85,8 @@ interface ProcessStage {
|
|||||||
evaluators?: string[];
|
evaluators?: string[];
|
||||||
documentsUploaded?: number;
|
documentsUploaded?: number;
|
||||||
isParallel?: boolean;
|
isParallel?: boolean;
|
||||||
|
isLocked?: boolean;
|
||||||
|
lockMessage?: string;
|
||||||
branches?: {
|
branches?: {
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
@ -438,6 +442,29 @@ export function ApplicationDetails() {
|
|||||||
const [isSubmittingKT, setIsSubmittingKT] = useState(false);
|
const [isSubmittingKT, setIsSubmittingKT] = useState(false);
|
||||||
const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null);
|
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) => {
|
const handleKTMatrixChange = (criterionName: string, score: number) => {
|
||||||
setKtMatrixScores(prev => ({
|
setKtMatrixScores(prev => ({
|
||||||
@ -749,8 +776,8 @@ export function ApplicationDetails() {
|
|||||||
// Auto-fill participants based on pre-assigned evaluators for this level
|
// Auto-fill participants based on pre-assigned evaluators for this level
|
||||||
const levelNum = parseInt(interviewType.replace('level', '')) || 1;
|
const levelNum = parseInt(interviewType.replace('level', '')) || 1;
|
||||||
const preAssigned = (application?.participants || [])
|
const preAssigned = (application?.participants || [])
|
||||||
.filter((p: any) =>
|
.filter((p: any) =>
|
||||||
p.metadata?.interviewLevel === levelNum ||
|
p.metadata?.interviewLevel === levelNum ||
|
||||||
p.metadata?.interviewLevel === String(levelNum) ||
|
p.metadata?.interviewLevel === String(levelNum) ||
|
||||||
p.metadata?.allAssignments?.includes(levelNum) ||
|
p.metadata?.allAssignments?.includes(levelNum) ||
|
||||||
p.metadata?.allAssignments?.includes(String(levelNum))
|
p.metadata?.allAssignments?.includes(String(levelNum))
|
||||||
@ -909,9 +936,9 @@ export function ApplicationDetails() {
|
|||||||
date: application.level1InterviewDate,
|
date: application.level1InterviewDate,
|
||||||
description: 'DD-ZM + RBM evaluation',
|
description: 'DD-ZM + RBM evaluation',
|
||||||
evaluators: Array.from(new Set((application.participants || [])
|
evaluators: Array.from(new Set((application.participants || [])
|
||||||
.filter((p: any) =>
|
.filter((p: any) =>
|
||||||
p.metadata?.interviewLevel === 1 ||
|
p.metadata?.interviewLevel === 1 ||
|
||||||
p.metadata?.interviewLevel === '1' ||
|
p.metadata?.interviewLevel === '1' ||
|
||||||
p.metadata?.allAssignments?.includes(1) ||
|
p.metadata?.allAssignments?.includes(1) ||
|
||||||
p.metadata?.allAssignments?.includes('1')
|
p.metadata?.allAssignments?.includes('1')
|
||||||
)
|
)
|
||||||
@ -926,9 +953,9 @@ export function ApplicationDetails() {
|
|||||||
date: application.level2InterviewDate,
|
date: application.level2InterviewDate,
|
||||||
description: 'DD Lead + ZBH evaluation',
|
description: 'DD Lead + ZBH evaluation',
|
||||||
evaluators: Array.from(new Set((application.participants || [])
|
evaluators: Array.from(new Set((application.participants || [])
|
||||||
.filter((p: any) =>
|
.filter((p: any) =>
|
||||||
p.metadata?.interviewLevel === 2 ||
|
p.metadata?.interviewLevel === 2 ||
|
||||||
p.metadata?.interviewLevel === '2' ||
|
p.metadata?.interviewLevel === '2' ||
|
||||||
p.metadata?.allAssignments?.includes(2) ||
|
p.metadata?.allAssignments?.includes(2) ||
|
||||||
p.metadata?.allAssignments?.includes('2')
|
p.metadata?.allAssignments?.includes('2')
|
||||||
)
|
)
|
||||||
@ -943,9 +970,9 @@ export function ApplicationDetails() {
|
|||||||
date: application.level3InterviewDate,
|
date: application.level3InterviewDate,
|
||||||
description: 'NBH + DD Head evaluation',
|
description: 'NBH + DD Head evaluation',
|
||||||
evaluators: Array.from(new Set((application.participants || [])
|
evaluators: Array.from(new Set((application.participants || [])
|
||||||
.filter((p: any) =>
|
.filter((p: any) =>
|
||||||
p.metadata?.interviewLevel === 3 ||
|
p.metadata?.interviewLevel === 3 ||
|
||||||
p.metadata?.interviewLevel === '3' ||
|
p.metadata?.interviewLevel === '3' ||
|
||||||
p.metadata?.allAssignments?.includes(3) ||
|
p.metadata?.allAssignments?.includes(3) ||
|
||||||
p.metadata?.allAssignments?.includes('3')
|
p.metadata?.allAssignments?.includes('3')
|
||||||
)
|
)
|
||||||
@ -968,8 +995,8 @@ export function ApplicationDetails() {
|
|||||||
date: application.loiApprovalDate,
|
date: application.loiApprovalDate,
|
||||||
description: 'Letter of Intent approval',
|
description: 'Letter of Intent approval',
|
||||||
evaluators: Array.from(new Set((application.participants || [])
|
evaluators: Array.from(new Set((application.participants || [])
|
||||||
.filter((p: any) =>
|
.filter((p: any) =>
|
||||||
p.metadata?.stageCode === 'LOI_APPROVAL' ||
|
p.metadata?.stageCode === 'LOI_APPROVAL' ||
|
||||||
p.metadata?.allAssignments?.includes('LOI_APPROVAL')
|
p.metadata?.allAssignments?.includes('LOI_APPROVAL')
|
||||||
)
|
)
|
||||||
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
||||||
@ -1120,11 +1147,15 @@ export function ApplicationDetails() {
|
|||||||
id: 12,
|
id: 12,
|
||||||
name: 'LOA',
|
name: 'LOA',
|
||||||
status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'),
|
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,
|
date: application.loaDate,
|
||||||
description: 'Letter of Authorization',
|
description: 'Letter of Authorization',
|
||||||
evaluators: Array.from(new Set((application.participants || [])
|
evaluators: Array.from(new Set((application.participants || [])
|
||||||
.filter((p: any) =>
|
.filter((p: any) =>
|
||||||
p.metadata?.stageCode === 'LOA_APPROVAL' ||
|
p.metadata?.stageCode === 'LOA_APPROVAL' ||
|
||||||
p.metadata?.allAssignments?.includes('LOA_APPROVAL')
|
p.metadata?.allAssignments?.includes('LOA_APPROVAL')
|
||||||
)
|
)
|
||||||
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
||||||
@ -1581,15 +1612,15 @@ export function ApplicationDetails() {
|
|||||||
|
|
||||||
const currentStageCode = policyManagedStages[application.status];
|
const currentStageCode = policyManagedStages[application.status];
|
||||||
const currentUserStageAction = application.stageApprovals?.find(
|
const currentUserStageAction = application.stageApprovals?.find(
|
||||||
(a: any) =>
|
(a: any) =>
|
||||||
a.stageCode === currentStageCode &&
|
a.stageCode === currentStageCode &&
|
||||||
String(a.actorUserId) === String(currentUser?.id)
|
String(a.actorUserId) === String(currentUser?.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasMadeStageDecision = !!currentUserStageAction;
|
const hasMadeStageDecision = !!currentUserStageAction;
|
||||||
|
|
||||||
const hasMadeDecisionForUser = !!currentUserStageAction ||
|
const hasMadeDecisionForUser = !!currentUserStageAction ||
|
||||||
currentUserEvaluation?.decision === 'Approved' ||
|
currentUserEvaluation?.decision === 'Approved' ||
|
||||||
currentUserEvaluation?.decision === 'Rejected' ||
|
currentUserEvaluation?.decision === 'Rejected' ||
|
||||||
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
|
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
|
||||||
|
|
||||||
@ -1819,89 +1850,102 @@ export function ApplicationDetails() {
|
|||||||
<Progress value={application.progress} className="h-3 mb-6" />
|
<Progress value={application.progress} className="h-3 mb-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{(() => {
|
{(() => {
|
||||||
const getApproverStatus = (stageCode: string | number) => {
|
const getApproverStatus = (stageCode: string | number) => {
|
||||||
const stageParticipants = (application.participants || []).filter((p: any) =>
|
const stageParticipants = (application.participants || []).filter((p: any) =>
|
||||||
p.metadata?.stageCode === stageCode ||
|
p.metadata?.stageCode === stageCode ||
|
||||||
p.metadata?.allAssignments?.includes(stageCode) ||
|
p.metadata?.allAssignments?.includes(stageCode) ||
|
||||||
(typeof stageCode === 'number' && (p.metadata?.interviewLevel === 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))))
|
(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) => {
|
return {
|
||||||
const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode;
|
name: p.user?.name || 'Unknown',
|
||||||
const approval = (application.stageApprovals || []).find((sa: any) =>
|
role: p.user?.role || 'Reviewer',
|
||||||
sa.stageCode === saCode &&
|
status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending'
|
||||||
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'
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const stageCode = stageMapping[stageName];
|
const renderApprovers = (stageName: string) => {
|
||||||
if (!stageCode) return null;
|
const stageMapping: Record<string, string | number> = {
|
||||||
|
'1st Level Interview': 1,
|
||||||
const approvers = getApproverStatus(stageCode);
|
'2nd Level Interview': 2,
|
||||||
if (approvers.length === 0) return null;
|
'3rd Level Interview': 3,
|
||||||
|
'LOI Approval': 'LOI_APPROVAL',
|
||||||
return (
|
'LOA': 'LOA_APPROVAL'
|
||||||
<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) => (
|
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 key={stage.id}>
|
||||||
<div className="flex gap-4 pb-8">
|
<div className="flex gap-4 pb-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 z-10 relative ${stage.status === 'completed'
|
<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'
|
? 'bg-green-500 border-green-500 text-white shadow-sm'
|
||||||
: stage.status === 'active'
|
: 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'
|
: 'bg-white border-slate-300 text-slate-400 shadow-none'
|
||||||
}`}>
|
}`}>
|
||||||
{stage.isParallel ? (
|
{stage.isParallel ? (
|
||||||
<GitBranch className="w-5 h-5" />
|
<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' ? (
|
{stage.status === 'completed' ? (
|
||||||
@ -1976,7 +2020,7 @@ export function ApplicationDetails() {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const stageDocsCount = documents.filter(doc =>
|
const stageDocsCount = documents.filter(doc =>
|
||||||
doc.stage === stage.name ||
|
doc.stage === stage.name ||
|
||||||
@ -2478,33 +2522,173 @@ export function ApplicationDetails() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Payments Tab */}
|
{/* Payments Tab */}
|
||||||
<TabsContent value="payments" className="space-y-4">
|
<TabsContent value="payments" className="space-y-6">
|
||||||
<h3 className="text-slate-900 mb-4">Payment Information</h3>
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3>
|
||||||
<div className="space-y-4">
|
<Badge variant="outline" className="bg-slate-50 text-slate-500 border-slate-200">
|
||||||
<div className="p-4 border border-slate-200 rounded-lg">
|
{deposits.length} Payment Record(s)
|
||||||
<div className="flex items-center justify-between mb-2">
|
</Badge>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</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>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Audit Trail Tab */}
|
{/* Audit Trail Tab */}
|
||||||
@ -3925,27 +4109,29 @@ export function ApplicationDetails() {
|
|||||||
'Site Plan': ['Site Plan'],
|
'Site Plan': ['Site Plan'],
|
||||||
'FDD': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'],
|
'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'],
|
'FDD Verification': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'],
|
||||||
'LOA': ['LOA Acceptance Copy'],
|
'LOA': ['LOA Acceptance Copy', 'Final Security Deposit Receipt'],
|
||||||
'LOI Approval': ['LOI Agreement', 'LOI Acknowledgement'],
|
'LOI Approval': ['Initial Security Deposit Receipt'],
|
||||||
|
'LOA Approval': ['Final Security Deposit Receipt'],
|
||||||
|
'LOA Acknowledgement': ['Final Security Deposit Receipt'],
|
||||||
'Inauguration': ['Inauguration Photos', 'Inauguration Report'],
|
'Inauguration': ['Inauguration Photos', 'Inauguration Report'],
|
||||||
'3rd Level Interview': ['AI Recommendation Summary', 'Interview Evaluation Sheet'],
|
'3rd Level Interview': ['AI Recommendation Summary', 'Interview Evaluation Sheet'],
|
||||||
'2nd Level Interview': ['Interview Evaluation Sheet'],
|
'2nd Level Interview': ['Interview Evaluation Sheet'],
|
||||||
'1st 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'];
|
const baseDocs = ['Other'];
|
||||||
let filteredDocs: string[] = [];
|
let filteredDocs: string[] = [];
|
||||||
|
|
||||||
if (!selectedStage) {
|
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', 'Board Resolution', 'Initial Security Deposit Receipt (₹2L)', 'Final Security Deposit Receipt (₹15L)', 'Rental Agreement', 'Property Documents', 'Bank Statement', 'Cancelled Check', 'Other'];
|
||||||
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'];
|
|
||||||
} else {
|
} else {
|
||||||
// Use mapping or fallback to current stage's specific docs
|
const stageName = selectedStage as string;
|
||||||
filteredDocs = [...(STAGE_DOCUMENT_MAP[selectedStage] || []), ...baseDocs];
|
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) => (
|
return Array.from(new Set(filteredDocs)).map((doc, idx) => (
|
||||||
@ -4002,66 +4188,11 @@ export function ApplicationDetails() {
|
|||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
{/* Preview Modal */}
|
<DocumentPreviewModal
|
||||||
<Dialog open={showPreviewModal} onOpenChange={setShowPreviewModal}>
|
isOpen={showPreviewModal}
|
||||||
<DialogContent className="max-w-4xl h-[85vh] flex flex-col p-0 overflow-hidden bg-slate-900 border-slate-800 shadow-2xl">
|
onClose={() => setShowPreviewModal(false)}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-900/50 backdrop-blur-md">
|
document={previewDoc}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import { Label } from '../ui/label';
|
|
||||||
import { Textarea } from '../ui/textarea';
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -13,576 +10,259 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '../ui/table';
|
} from '../ui/table';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '../ui/dialog';
|
|
||||||
import {
|
import {
|
||||||
DollarSign,
|
DollarSign,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
|
||||||
CreditCard,
|
|
||||||
FileText,
|
FileText,
|
||||||
Calendar,
|
Calendar,
|
||||||
User,
|
User,
|
||||||
MapPin,
|
MapPin,
|
||||||
Phone,
|
CreditCard
|
||||||
Mail
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { onboardingService } from '../../services/onboarding.service';
|
||||||
// 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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface FinanceOnboardingPageProps {
|
interface FinanceOnboardingPageProps {
|
||||||
onViewPaymentDetails?: (applicationId: string) => void;
|
onViewPaymentDetails?: (applicationId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardingPageProps = {}) {
|
export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardingPageProps = {}) {
|
||||||
const [selectedApplication, setSelectedApplication] = useState<any>(null);
|
const [applications, setApplications] = useState<any[]>([]);
|
||||||
const [showVerifyDialog, setShowVerifyDialog] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showDetailsDialog, setShowDetailsDialog] = useState(false);
|
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'verified'>('pending');
|
||||||
const [verificationNotes, setVerificationNotes] = useState('');
|
|
||||||
const [receivedAmount, setReceivedAmount] = useState('');
|
|
||||||
const [verificationDate, setVerificationDate] = useState('');
|
|
||||||
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'verified'>('all');
|
|
||||||
|
|
||||||
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 === 'all') return true;
|
||||||
if (filterStatus === 'pending') return app.paymentStatus === 'Pending Verification';
|
if (filterStatus === 'pending') return status !== 'Verified';
|
||||||
if (filterStatus === 'verified') return app.paymentStatus === 'Verified';
|
if (filterStatus === 'verified') return status === 'Verified';
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVerifyPayment = (app: any) => {
|
const handleViewDetails = (appId: string) => {
|
||||||
setSelectedApplication(app);
|
|
||||||
setReceivedAmount(app.securityDeposit);
|
|
||||||
setVerificationDate(new Date().toISOString().split('T')[0]);
|
|
||||||
setShowVerifyDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDetails = (app: any) => {
|
|
||||||
if (onViewPaymentDetails) {
|
if (onViewPaymentDetails) {
|
||||||
onViewPaymentDetails(app.id);
|
onViewPaymentDetails(appId);
|
||||||
} else {
|
|
||||||
setSelectedApplication(app);
|
|
||||||
setShowDetailsDialog(true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmVerification = (approved: boolean) => {
|
const pendingCount = applications.filter(app => getRelevantPaymentStatus(app) !== 'Verified').length;
|
||||||
if (approved) {
|
const verifiedCount = applications.filter(app => getRelevantPaymentStatus(app) === 'Verified').length;
|
||||||
toast.success(`Payment verified for ${selectedApplication.name}`);
|
const totalAmount = applications.length * 200000;
|
||||||
} else {
|
|
||||||
toast.error(`Payment rejected for ${selectedApplication.name}`);
|
|
||||||
}
|
|
||||||
setShowVerifyDialog(false);
|
|
||||||
setVerificationNotes('');
|
|
||||||
setSelectedApplication(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pendingCount = mockFinanceApplications.filter(app => app.paymentStatus === 'Pending Verification').length;
|
if (loading) {
|
||||||
const verifiedCount = mockFinanceApplications.filter(app => app.paymentStatus === 'Verified').length;
|
return (
|
||||||
const totalAmount = mockFinanceApplications.reduce((sum, app) => {
|
<div className="flex items-center justify-center p-20 text-amber-600">
|
||||||
const amount = parseInt(app.securityDeposit.replace(/[₹,]/g, ''));
|
<Clock className="w-8 h-8 animate-spin mr-3" />
|
||||||
return sum + amount;
|
<span>Loading payment queue...</span>
|
||||||
}, 0);
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-slate-900 mb-2">Payment Verification</h1>
|
<div>
|
||||||
<p className="text-slate-600">Verify advance payments for dealership applications</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<Card>
|
<Card className="border-amber-200 bg-amber-50/30">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm text-slate-600">Pending Verification</CardTitle>
|
<CardTitle className="text-sm font-medium text-amber-800">Pending Verification</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-slate-900 text-2xl">{pendingCount}</div>
|
<div className="text-3xl font-bold text-amber-700">{pendingCount}</div>
|
||||||
<Clock className="w-8 h-8 text-amber-600" />
|
<Clock className="w-8 h-8 text-amber-400" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card className="border-green-200 bg-green-50/30">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm text-slate-600">Verified Payments</CardTitle>
|
<CardTitle className="text-sm font-medium text-green-800">Verified This Week</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-slate-900 text-2xl">{verifiedCount}</div>
|
<div className="text-3xl font-bold text-green-700">{verifiedCount}</div>
|
||||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
<CheckCircle className="w-8 h-8 text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card className="border-blue-200 bg-blue-50/30">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm text-slate-600">Total Applications</CardTitle>
|
<CardTitle className="text-sm font-medium text-blue-800">Active Queue</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-slate-900 text-2xl">{mockFinanceApplications.length}</div>
|
<div className="text-3xl font-bold text-blue-700">{applications.length}</div>
|
||||||
<FileText className="w-8 h-8 text-blue-600" />
|
<FileText className="w-8 h-8 text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card className="border-slate-200 bg-slate-50/30">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm text-slate-600">Total Amount</CardTitle>
|
<CardTitle className="text-sm font-medium text-slate-800">Estimated Total (Cr)</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-slate-900 text-2xl">₹{(totalAmount / 100000).toFixed(1)}L</div>
|
<div className="text-3xl font-bold text-slate-700">₹{(totalAmount / 10000000).toFixed(2)}</div>
|
||||||
<DollarSign className="w-8 h-8 text-purple-600" />
|
<DollarSign className="w-8 h-8 text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
{/* Filter Tabs */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 bg-slate-100 p-1 rounded-lg w-fit">
|
||||||
<Button
|
<Button
|
||||||
variant={filterStatus === 'all' ? 'default' : 'outline'}
|
variant={filterStatus === 'pending' ? 'default' : 'ghost'}
|
||||||
onClick={() => setFilterStatus('all')}
|
size="sm"
|
||||||
className={filterStatus === 'all' ? 'bg-amber-600 hover:bg-amber-700' : ''}
|
|
||||||
>
|
|
||||||
All ({mockFinanceApplications.length})
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={filterStatus === 'pending' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setFilterStatus('pending')}
|
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>
|
||||||
<Button
|
<Button
|
||||||
variant={filterStatus === 'verified' ? 'default' : 'outline'}
|
variant={filterStatus === 'verified' ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setFilterStatus('verified')}
|
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})
|
Verified ({verifiedCount})
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Applications Table */}
|
{/* Applications Table */}
|
||||||
<Card>
|
<Card className="border-none shadow-md">
|
||||||
<CardHeader>
|
<CardContent className="p-0">
|
||||||
<CardTitle>Payment Verification Queue</CardTitle>
|
|
||||||
<CardDescription>Review and verify advance payment receipts</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader className="bg-slate-50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Application ID</TableHead>
|
<TableHead className="w-[180px]">Application ID</TableHead>
|
||||||
<TableHead>Applicant Details</TableHead>
|
<TableHead>Applicant Name</TableHead>
|
||||||
<TableHead>Location</TableHead>
|
<TableHead>Location</TableHead>
|
||||||
<TableHead>Payment Details</TableHead>
|
<TableHead>Payment Stage</TableHead>
|
||||||
<TableHead>Amount</TableHead>
|
<TableHead>Current Status</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead className="text-right">Action</TableHead>
|
||||||
<TableHead>Due Date</TableHead>
|
|
||||||
<TableHead>Actions</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredApplications.map((app) => (
|
{filteredApplications.length > 0 ? (
|
||||||
<TableRow key={app.id}>
|
filteredApplications.map((app) => (
|
||||||
<TableCell>
|
<TableRow key={app.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
<div>
|
<TableCell className="font-mono text-sm font-bold">{app.applicationId || app.id}</TableCell>
|
||||||
<div className="text-slate-900">{app.id}</div>
|
<TableCell>
|
||||||
<div className="text-sm text-slate-500">{app.registrationNumber}</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="w-3 h-3 text-slate-400" />
|
<User className="w-4 h-4 text-slate-400" />
|
||||||
<span className="text-slate-900">{app.name}</span>
|
<span className="font-medium text-slate-900">{app.applicantName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
</TableCell>
|
||||||
<Phone className="w-3 h-3 text-slate-400" />
|
<TableCell>
|
||||||
<span className="text-sm text-slate-500">{app.phone}</span>
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
</TableCell>
|
||||||
<Mail className="w-3 h-3 text-slate-400" />
|
<TableCell>
|
||||||
<span className="text-sm text-slate-500">{app.email}</span>
|
<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>
|
</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>
|
</div>
|
||||||
</TableCell>
|
</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>
|
</TableRow>
|
||||||
))}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,108 +1,159 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
import { Label } from '../ui/label';
|
import { Label } from '../ui/label';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
Upload,
|
|
||||||
FileText,
|
FileText,
|
||||||
Calendar,
|
|
||||||
User,
|
|
||||||
MapPin,
|
|
||||||
Phone,
|
|
||||||
Mail,
|
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Building,
|
User,
|
||||||
Hash,
|
Wallet,
|
||||||
Wallet
|
AlertCircle,
|
||||||
|
Clock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
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 {
|
interface FinancePaymentDetailsPageProps {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
onBack: () => void;
|
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) {
|
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({
|
const [paymentDetails, setPaymentDetails] = useState({
|
||||||
verificationTransactionId: '',
|
verificationTransactionId: '',
|
||||||
receivedAmount: application.securityDepositNum.toString(),
|
receivedAmount: '',
|
||||||
receivedDate: new Date().toISOString().split('T')[0],
|
receivedDate: new Date().toISOString().split('T')[0],
|
||||||
verificationRemarks: ''
|
verificationRemarks: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const [uploadedDocuments, setUploadedDocuments] = useState<any[]>([]);
|
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
||||||
|
const [previewDoc, setPreviewDoc] = useState<any>(null);
|
||||||
|
|
||||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const activeDeposit = deposits.find(d => d.depositType === activeType);
|
||||||
const files = event.target.files;
|
|
||||||
if (files && files.length > 0) {
|
useEffect(() => {
|
||||||
const newDocs = Array.from(files).map(file => ({
|
fetchData();
|
||||||
name: file.name,
|
}, [applicationId]);
|
||||||
size: `${(file.size / 1024).toFixed(0)} KB`,
|
|
||||||
uploadedOn: new Date().toISOString().split('T')[0]
|
useEffect(() => {
|
||||||
}));
|
if (activeDeposit) {
|
||||||
setUploadedDocuments([...uploadedDocuments, ...newDocs]);
|
setPaymentDetails({
|
||||||
toast.success(`${files.length} document(s) uploaded successfully`);
|
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) {
|
if (!paymentDetails.verificationTransactionId || !paymentDetails.receivedDate) {
|
||||||
toast.error('Please fill in all required payment details');
|
toast.error('Please fill in all required payment details');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paymentDetails.receivedAmount !== application.securityDepositNum.toString()) {
|
try {
|
||||||
toast.warning('Received amount differs from expected amount');
|
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}`);
|
toast.success(`${activeType === 'INITIAL' ? 'Advance' : 'Final'} payment verified and approved`);
|
||||||
setTimeout(() => onBack(), 1500);
|
await fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to verify payment');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRejectPayment = () => {
|
const handleRejectPayment = async () => {
|
||||||
if (!paymentDetails.verificationRemarks) {
|
if (!paymentDetails.verificationRemarks) {
|
||||||
toast.error('Please provide remarks for rejection');
|
toast.error('Please provide remarks for rejection');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(`Payment rejected for ${application.name}`);
|
try {
|
||||||
setTimeout(() => onBack(), 1500);
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -112,297 +163,325 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
|
|||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl mb-1">Payment Verification</h1>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Banner */}
|
{/* 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">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-full bg-amber-100 flex items-center justify-center">
|
<div className={cn(
|
||||||
<DollarSign className="w-6 h-6 text-amber-600" />
|
"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>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-900">Payment Pending Verification</p>
|
<p className="text-slate-900 font-bold">
|
||||||
<p className="text-sm text-slate-600">Due Date: {application.dueDate}</p>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
<Badge className="bg-amber-600">
|
<Badge className={cn(
|
||||||
{application.paymentStatus}
|
activeDeposit?.status === 'Verified' ? "bg-green-600" :
|
||||||
|
activeDeposit?.status === 'Rejected' ? "bg-red-600" :
|
||||||
|
"bg-amber-600 text-white"
|
||||||
|
)}>
|
||||||
|
{activeDeposit?.status || 'No Record'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<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">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
{/* Application Details */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-xl">
|
||||||
<User className="w-5 h-5" />
|
<User className="w-5 h-5 text-amber-600" />
|
||||||
Applicant Information
|
Applicant Information
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="grid grid-cols-2 gap-y-4 gap-x-8">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div>
|
||||||
<div>
|
<Label className="text-slate-500">Application ID</Label>
|
||||||
<Label className="text-slate-500">Application ID</Label>
|
<p className="text-slate-900 font-medium">{application.applicationId || application.id}</p>
|
||||||
<p className="text-slate-900">{application.id}</p>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<Label className="text-slate-500">Applicant Name</Label>
|
||||||
<Label className="text-slate-500">Registration Number</Label>
|
<p className="text-slate-900 font-medium">{application.applicantName}</p>
|
||||||
<p className="text-slate-900">{application.registrationNumber}</p>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<Label className="text-slate-500">Location</Label>
|
||||||
<Label className="text-slate-500">Applicant Name</Label>
|
<p className="text-slate-900 font-medium">{application.city || application.preferredLocation}, {application.state}</p>
|
||||||
<p className="text-slate-900">{application.name}</p>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<Label className="text-slate-500">Email / Phone</Label>
|
||||||
<Label className="text-slate-500">Email</Label>
|
<p className="text-slate-700 text-sm">{application.email}</p>
|
||||||
<p className="text-slate-900">{application.email}</p>
|
<p className="text-slate-700 text-sm">{application.phone}</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>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Payment Information */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-xl">
|
||||||
<CreditCard className="w-5 h-5" />
|
<CreditCard className="w-5 h-5 text-amber-600" />
|
||||||
Submitted Payment Details
|
Deposit Tracking
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||||||
<Label className="text-slate-500">Security Deposit</Label>
|
<Label className="text-slate-500 block mb-1">Expected Amount</Label>
|
||||||
<p className="text-slate-900 text-2xl text-green-600">{application.securityDeposit}</p>
|
<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>
|
||||||
<div>
|
<div className={cn(
|
||||||
<Label className="text-slate-500">Payment Mode</Label>
|
"p-4 rounded-lg border",
|
||||||
<p className="text-slate-900">{application.paymentMode}</p>
|
activeDeposit?.status === 'Verified' ? "bg-green-50 border-green-200" : "bg-blue-50 border-blue-200"
|
||||||
</div>
|
)}>
|
||||||
<div>
|
<Label className="text-slate-500 block mb-1">Receipt Status</Label>
|
||||||
<Label className="text-slate-500">Transaction ID</Label>
|
<p className={cn(
|
||||||
<p className="text-slate-900">{application.transactionId}</p>
|
"text-2xl font-bold",
|
||||||
</div>
|
activeDeposit?.status === 'Verified' ? "text-green-700" : "text-blue-700"
|
||||||
<div>
|
)}>
|
||||||
<Label className="text-slate-500">Bank Name</Label>
|
{activeDeposit?.status || 'Not Started'}
|
||||||
<p className="text-slate-900">{application.bankName}</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Submitted Documents */}
|
{activeDeposit?.paymentReference && (
|
||||||
<Card>
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||||
<CardHeader>
|
<div>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<Label className="text-slate-500">Payment Reference</Label>
|
||||||
<FileText className="w-5 h-5" />
|
<p className="text-slate-900 font-mono">{activeDeposit.paymentReference}</p>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div>
|
||||||
</div>
|
<Label className="text-slate-500">Verified By</Label>
|
||||||
|
<p className="text-slate-900">{activeDeposit.verifiedBy || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Upload Additional Documents */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-xl">
|
||||||
<Upload className="w-5 h-5" />
|
<FileText className="w-5 h-5 text-amber-600" />
|
||||||
Upload Additional Documents
|
Verification Evidence
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Documents uploaded by the applicant for payment proof</CardDescription>
|
||||||
Upload any additional verification documents or receipts
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
{application.uploadedDocuments?.filter((d: any) =>
|
||||||
<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">
|
activeType === 'INITIAL'
|
||||||
<Upload className="w-8 h-8 text-slate-400 mx-auto mb-2" />
|
? d.documentType?.toLowerCase().includes('initial') && d.documentType?.toLowerCase().includes('deposit')
|
||||||
<p className="text-slate-600 mb-2">Click to upload or drag and drop</p>
|
: d.documentType?.toLowerCase().includes('final') && d.documentType?.toLowerCase().includes('deposit')
|
||||||
<p className="text-sm text-slate-500">PDF, DOC, DOCX, PNG, JPG (max 10MB)</p>
|
).length > 0 ? (
|
||||||
<input
|
<div className="space-y-3">
|
||||||
type="file"
|
{application.uploadedDocuments.filter((d: any) =>
|
||||||
multiple
|
activeType === 'INITIAL'
|
||||||
className="hidden"
|
? d.documentType?.toLowerCase().includes('initial') && d.documentType?.toLowerCase().includes('deposit')
|
||||||
id="file-upload"
|
: d.documentType?.toLowerCase().includes('final') && d.documentType?.toLowerCase().includes('deposit')
|
||||||
onChange={handleFileUpload}
|
).map((doc: any, index: number) => (
|
||||||
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"
|
<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">
|
||||||
<label htmlFor="file-upload">
|
<div className="w-10 h-10 rounded bg-slate-100 flex items-center justify-center">
|
||||||
<Button variant="outline" className="mt-4" asChild>
|
<FileText className="w-5 h-5 text-slate-500" />
|
||||||
<span>Choose Files</span>
|
</div>
|
||||||
</Button>
|
<div>
|
||||||
</label>
|
<p className="text-slate-900 font-medium">{doc.fileName || doc.name}</p>
|
||||||
</div>
|
<p className="text-xs text-slate-500 uppercase">{doc.documentType} • {new Date(doc.createdAt).toLocaleDateString()}</p>
|
||||||
|
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<Button
|
||||||
</div>
|
variant="ghost"
|
||||||
)}
|
size="sm"
|
||||||
</div>
|
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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Verification Form */}
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="sticky top-6">
|
<Card className="border-amber-100 shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader className="bg-amber-50/50">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<Wallet className="w-5 h-5" />
|
<Wallet className="w-5 h-5 text-amber-600" />
|
||||||
Payment Verification
|
Finance Action
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
|
||||||
Enter payment verification details
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="pt-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="verificationTxnId">
|
<Label htmlFor="verificationTxnId" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
|
||||||
Verification Transaction ID <span className="text-red-500">*</span>
|
UTR / Reference Number <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="verificationTxnId"
|
id="verificationTxnId"
|
||||||
placeholder="Enter transaction ID"
|
placeholder="Enter Bank UTR Number"
|
||||||
|
disabled={activeDeposit?.status === 'Verified'}
|
||||||
|
className="mt-1"
|
||||||
value={paymentDetails.verificationTransactionId}
|
value={paymentDetails.verificationTransactionId}
|
||||||
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationTransactionId: e.target.value })}
|
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationTransactionId: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="receivedAmount">
|
<Label htmlFor="receivedAmount" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
|
||||||
Received Amount (₹) <span className="text-red-500">*</span>
|
Amount Received (₹) <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="receivedAmount"
|
id="receivedAmount"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Enter received amount"
|
placeholder={(activeType === 'INITIAL' ? 500000 : 1500000).toString()}
|
||||||
|
disabled={activeDeposit?.status === 'Verified'}
|
||||||
|
className="mt-1"
|
||||||
value={paymentDetails.receivedAmount}
|
value={paymentDetails.receivedAmount}
|
||||||
onChange={(e) => setPaymentDetails({ ...paymentDetails, receivedAmount: e.target.value })}
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="receivedDate">
|
<Label htmlFor="receivedDate" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
|
||||||
Payment Received Date <span className="text-red-500">*</span>
|
Credit Value Date <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="receivedDate"
|
id="receivedDate"
|
||||||
type="date"
|
type="date"
|
||||||
|
disabled={activeDeposit?.status === 'Verified'}
|
||||||
|
className="mt-1"
|
||||||
value={paymentDetails.receivedDate}
|
value={paymentDetails.receivedDate}
|
||||||
onChange={(e) => setPaymentDetails({ ...paymentDetails, receivedDate: e.target.value })}
|
onChange={(e) => setPaymentDetails({ ...paymentDetails, receivedDate: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<Textarea
|
||||||
id="verificationRemarks"
|
id="remarks"
|
||||||
placeholder="Enter any remarks or notes..."
|
placeholder="Any internal notes for reconciliation..."
|
||||||
rows={4}
|
rows={3}
|
||||||
|
className="mt-1"
|
||||||
value={paymentDetails.verificationRemarks}
|
value={paymentDetails.verificationRemarks}
|
||||||
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationRemarks: e.target.value })}
|
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationRemarks: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 border-t">
|
<div className="pt-4 space-y-3">
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-green-600 hover:bg-green-700"
|
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}
|
onClick={handleApprovePayment}
|
||||||
|
disabled={isSubmitting || activeDeposit?.status === 'Verified'}
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
{activeDeposit?.status === 'Verified' ? (
|
||||||
Confirm Payment Received
|
<><CheckCircle className="w-4 h-4 mr-2" /> Verified Successfully</>
|
||||||
|
) : (
|
||||||
|
<><CheckCircle className="w-4 h-4 mr-2" /> Mark as Verified</>
|
||||||
|
)}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Quick Info Card */}
|
<Card className="bg-slate-900 text-white border-none shadow-xl">
|
||||||
<Card className="bg-blue-50 border-blue-200">
|
|
||||||
<CardHeader>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="text-xs text-slate-300 space-y-3">
|
||||||
<ul className="space-y-2 text-sm text-slate-700">
|
<p>Once verified, the following will occur:</p>
|
||||||
<li className="flex items-start gap-2">
|
<ul className="list-disc pl-4 space-y-2">
|
||||||
<CheckCircle className="w-4 h-4 text-blue-600 mt-0.5" />
|
<li>Applicant status will advance to {activeType === 'INITIAL' ? 'LOI Issuance' : 'LOA Approval'}</li>
|
||||||
<span>Verify transaction ID matches bank records</span>
|
<li>Email notification will be sent to Applicant</li>
|
||||||
</li>
|
<li>Digital {activeType === 'INITIAL' ? 'LOI' : 'LOA'} generation will be unlocked</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>
|
|
||||||
</ul>
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Global Preview Modal */}
|
||||||
|
<DocumentPreviewModal
|
||||||
|
isOpen={showPreviewModal}
|
||||||
|
onClose={() => setShowPreviewModal(false)}
|
||||||
|
document={previewDoc}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import {
|
import {
|
||||||
Tabs, TabsContent, TabsList, TabsTrigger
|
Tabs, TabsContent, TabsList, TabsTrigger
|
||||||
} from '../ui/tabs';
|
} 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 { Badge } from '../ui/badge';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ import { ZoneDialog } from './MasterPage/ZoneDialog';
|
|||||||
import { RegionDialog } from './MasterPage/RegionDialog';
|
import { RegionDialog } from './MasterPage/RegionDialog';
|
||||||
import { TemplateDialog } from './MasterPage/TemplateDialog';
|
import { TemplateDialog } from './MasterPage/TemplateDialog';
|
||||||
import { LocationDialog } from './MasterPage/LocationDialog';
|
import { LocationDialog } from './MasterPage/LocationDialog';
|
||||||
|
import { SecurityDepositMaster } from './MasterPage/SecurityDepositMaster';
|
||||||
import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
|
import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
|
||||||
import { RootState } from '../../store';
|
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">
|
<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
|
<MapPin className="w-4 h-4" /> Locations
|
||||||
</TabsTrigger>
|
</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
|
<SlidersHorizontal className="w-4 h-4" /> Approvals
|
||||||
</TabsTrigger>
|
</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>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="hierarchy" className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
<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">
|
<TabsContent value="approvals" className="animate-in fade-in duration-300">
|
||||||
<ApprovalPoliciesPage />
|
<ApprovalPoliciesPage />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="settings" className="animate-in fade-in duration-300">
|
||||||
|
<SecurityDepositMaster />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
156
src/components/applications/MasterPage/SecurityDepositMaster.tsx
Normal file
156
src/components/applications/MasterPage/SecurityDepositMaster.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -119,20 +119,52 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
|||||||
const getCurrentStageIndex = () => {
|
const getCurrentStageIndex = () => {
|
||||||
if (!request) return 1;
|
if (!request) return 1;
|
||||||
const stageMap: Record<string, number> = {
|
const stageMap: Record<string, number> = {
|
||||||
|
'Submitted': 1,
|
||||||
'Dealer': 1,
|
'Dealer': 1,
|
||||||
|
'DD Admin Review': 2,
|
||||||
'ASM': 2,
|
'ASM': 2,
|
||||||
|
'ASM Review': 2,
|
||||||
|
'RBM Review': 3,
|
||||||
'RBM': 3,
|
'RBM': 3,
|
||||||
|
'DD ZM Review': 4,
|
||||||
'DD-ZM': 4,
|
'DD-ZM': 4,
|
||||||
|
'ZBH Review': 5,
|
||||||
'ZBH': 5,
|
'ZBH': 5,
|
||||||
|
'DD Lead Review': 6,
|
||||||
'DD Lead': 6,
|
'DD Lead': 6,
|
||||||
|
'DD Head Review': 7,
|
||||||
'DD Head': 7,
|
'DD Head': 7,
|
||||||
|
'NBH Review': 8,
|
||||||
|
'NBH Approval': 8,
|
||||||
'NBH': 8,
|
'NBH': 8,
|
||||||
'DD H.O': 9, // Parallel branch A
|
'DD H.O': 9, // Parallel branch A
|
||||||
'Architect': 9, // Parallel branch B
|
'Architect': 9, // Parallel branch B
|
||||||
|
'Legal Clearance': 10,
|
||||||
'Closed': 11,
|
'Closed': 11,
|
||||||
'Completed': 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();
|
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'}`}>
|
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||||
Responsible: {stage.role}
|
Responsible: {stage.role}
|
||||||
</p>
|
</p>
|
||||||
|
{getAssignedReviewer(stage.name) && (
|
||||||
|
<p className="text-xs text-blue-600 font-medium mt-1">
|
||||||
|
Assigned: {getAssignedReviewer(stage.name)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Badge className={
|
<Badge className={
|
||||||
isCompleted ? 'bg-green-100 text-green-700 border-green-300' :
|
isCompleted ? 'bg-green-100 text-green-700 border-green-300' :
|
||||||
@ -661,8 +698,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Sidebar - Actions */}
|
{/* Right Sidebar - Actions */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
||||||
{/* Current Status Card */}
|
{/* Current Status Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -747,6 +785,35 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
|||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -231,8 +231,18 @@ export function ProspectiveDashboardPage() {
|
|||||||
<option value="PAN Card">PAN Card</option>
|
<option value="PAN Card">PAN Card</option>
|
||||||
<option value="GST Certificate">GST Certificate</option>
|
<option value="GST Certificate">GST Certificate</option>
|
||||||
<option value="Aadhaar Card">Aadhaar Card</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="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="Property Document">Property Document</option>
|
||||||
<option value="Other">Other</option>
|
<option value="Other">Other</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { User as UserType } from '../../lib/mock-data';
|
import { User as UserType } from '../../lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { dealerService } from '../../services/dealer.service';
|
import { dealerService } from '../../services/dealer.service';
|
||||||
|
import { masterService } from '../../services/master.service';
|
||||||
|
|
||||||
interface DealerRelocationPageProps {
|
interface DealerRelocationPageProps {
|
||||||
currentUser: UserType | null;
|
currentUser: UserType | null;
|
||||||
@ -33,6 +34,13 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
const [newAddress, setNewAddress] = useState('');
|
const [newAddress, setNewAddress] = useState('');
|
||||||
const [reason, setReason] = 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 [outlets, setOutlets] = useState<any[]>([]);
|
||||||
const [requests, setRequests] = useState<any[]>([]);
|
const [requests, setRequests] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -41,8 +49,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
fetchMasterData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Filter districts based on selected state
|
||||||
|
const filteredDistricts = selectedStateId
|
||||||
|
? districts.filter(d => d.stateId === selectedStateId)
|
||||||
|
: districts;
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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) => {
|
const handleOpenRelocationDialog = (outlet: any) => {
|
||||||
setSelectedOutlet(outlet);
|
setSelectedOutlet(outlet);
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
@ -87,11 +138,17 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const payload = {
|
const payload = {
|
||||||
outletId: selectedOutlet.id,
|
outletId: selectedOutlet.id,
|
||||||
currentLocation: selectedOutlet.location,
|
relocationType: 'Intercity',
|
||||||
|
currentAddress: selectedOutlet.address || '',
|
||||||
|
currentCity: selectedOutlet.city || '',
|
||||||
|
currentState: selectedOutlet.state || '',
|
||||||
|
newAddress,
|
||||||
newCity,
|
newCity,
|
||||||
newState,
|
newState,
|
||||||
newAddress,
|
newDistrictId: selectedDistrictId || null,
|
||||||
reason
|
newStateId: selectedStateId || null,
|
||||||
|
reason,
|
||||||
|
proposedDate: null
|
||||||
};
|
};
|
||||||
|
|
||||||
await dealerService.submitRelocationRequest(payload);
|
await dealerService.submitRelocationRequest(payload);
|
||||||
@ -198,29 +255,49 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* New Location Details */}
|
{/* New Location Details - State/District Dropdowns */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="newCity">Proposed City *</Label>
|
<Label htmlFor="newState">Proposed State *</Label>
|
||||||
<Input
|
<Select
|
||||||
id="newCity"
|
value={selectedStateId}
|
||||||
placeholder="Enter new city"
|
onValueChange={handleStateChange}
|
||||||
value={newCity}
|
required
|
||||||
onChange={(e) => setNewCity(e.target.value)}
|
disabled={masterDataLoading}
|
||||||
required
|
>
|
||||||
/>
|
<SelectTrigger id="newState">
|
||||||
</div>
|
<SelectValue placeholder={masterDataLoading ? "Loading..." : "Select state"} />
|
||||||
<div className="space-y-2">
|
</SelectTrigger>
|
||||||
<Label htmlFor="newState">Proposed State *</Label>
|
<SelectContent>
|
||||||
<Input
|
{states.map((state) => (
|
||||||
id="newState"
|
<SelectItem key={state.id} value={state.id}>
|
||||||
placeholder="Enter new state"
|
{state.name}
|
||||||
value={newState}
|
</SelectItem>
|
||||||
onChange={(e) => setNewState(e.target.value)}
|
))}
|
||||||
required
|
</SelectContent>
|
||||||
/>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="newAddress">Proposed Full Address *</Label>
|
<Label htmlFor="newAddress">Proposed Full Address *</Label>
|
||||||
|
|||||||
203
src/components/ui/DocumentPreviewModal.tsx
Normal file
203
src/components/ui/DocumentPreviewModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -110,8 +110,8 @@ export const useMasterData = () => {
|
|||||||
status: r.isActive !== false ? 'Active' : 'Inactive'
|
status: r.isActive !== false ? 'Active' : 'Inactive'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const asms = (bodyAsms?.data || bodyAsms || []);
|
const asms = Array.isArray(bodyAsms?.data) ? bodyAsms.data : (Array.isArray(bodyAsms) ? bodyAsms : []);
|
||||||
const zonalManagers = (bodyZms?.data || bodyZms || []);
|
const zonalManagers = Array.isArray(bodyZms?.data) ? bodyZms.data : (Array.isArray(bodyZms) ? bodyZms : []);
|
||||||
|
|
||||||
const zonalManagerMappings = zonalManagers.length > 0 ? zonalManagers : users.filter((u: any) =>
|
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'))
|
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 || []
|
reminders: s.reminderConfig?.reminders || [], escalations: s.escalationConfig?.escalations || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const ddLeads = Array.isArray(bodyDdLeads?.data) ? bodyDdLeads.data : (Array.isArray(bodyDdLeads) ? bodyDdLeads : []);
|
||||||
|
|
||||||
dispatch(setMasterData({
|
dispatch(setMasterData({
|
||||||
zones, regionalOffices, asms, zonalManagerMappings, zonalManagers,
|
zones, regionalOffices, asms, zonalManagerMappings, zonalManagers,
|
||||||
roles, allStates, allDistricts, allAreas,
|
roles, allStates, allDistricts, allAreas,
|
||||||
@ -169,7 +171,7 @@ export const useMasterData = () => {
|
|||||||
emailTemplates: bodyEmail?.data || [],
|
emailTemplates: bodyEmail?.data || [],
|
||||||
slaConfigs,
|
slaConfigs,
|
||||||
users,
|
users,
|
||||||
ddLeads: bodyDdLeads?.data || bodyDdLeads || [],
|
ddLeads,
|
||||||
loading: false
|
loading: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -140,5 +140,14 @@ export const masterService = {
|
|||||||
saveASM: async (data: any) => {
|
saveASM: async (data: any) => {
|
||||||
// ASMs are handled via user update in this system
|
// ASMs are handled via user update in this system
|
||||||
return API.updateUser(data.userId, data).then(res => res.data);
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -114,5 +114,20 @@ export const onboardingService = {
|
|||||||
const response: any = await API.retriggerEvaluators(id);
|
const response: any = await API.retriggerEvaluators(id);
|
||||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to retrigger evaluators');
|
if (!response.ok) throw new Error(response.data?.message || 'Failed to retrigger evaluators');
|
||||||
return response.data;
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user