490 lines
20 KiB
TypeScript
490 lines
20 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
|
import { Badge } from '../ui/badge';
|
|
import { Button } from '../ui/button';
|
|
import { Input } from '../ui/input';
|
|
import { Label } from '../ui/label';
|
|
import { Textarea } from '../ui/textarea';
|
|
import {
|
|
ArrowLeft,
|
|
IndianRupee,
|
|
CheckCircle,
|
|
XCircle,
|
|
FileText,
|
|
CreditCard,
|
|
User,
|
|
Wallet,
|
|
AlertCircle,
|
|
Clock
|
|
} from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { onboardingService } from '../../services/onboarding.service';
|
|
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
|
|
import { formatDateTime } from '../ui/utils';
|
|
|
|
// Simple helper for class merging if 'cn' is not available
|
|
const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
|
|
|
|
interface FinancePaymentDetailsPageProps {
|
|
applicationId: string;
|
|
onBack: () => void;
|
|
}
|
|
|
|
export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaymentDetailsPageProps) {
|
|
const [application, setApplication] = useState<any>(null);
|
|
const [deposits, setDeposits] = useState<any[]>([]);
|
|
const [activeType, setActiveType] = useState<'SECURITY_DEPOSIT' | 'FIRST_FILL'>('SECURITY_DEPOSIT');
|
|
const [loading, setLoading] = useState(true);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [configs, setConfigs] = useState<any>({});
|
|
|
|
const [paymentDetails, setPaymentDetails] = useState({
|
|
verificationTransactionId: '',
|
|
receivedAmount: '',
|
|
receivedDate: new Date().toISOString().split('T')[0],
|
|
verificationRemarks: ''
|
|
});
|
|
|
|
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
|
const [previewDoc, setPreviewDoc] = useState<any>(null);
|
|
|
|
const activeDeposit = deposits.find(d => d.depositType === activeType);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [applicationId]);
|
|
|
|
useEffect(() => {
|
|
if (activeDeposit) {
|
|
setPaymentDetails({
|
|
verificationTransactionId: activeDeposit.paymentReference || '',
|
|
receivedAmount: activeDeposit.amount?.toString() || '',
|
|
receivedDate: activeDeposit.verifiedAt ? new Date(activeDeposit.verifiedAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
|
verificationRemarks: activeDeposit.remarks || ''
|
|
});
|
|
} else {
|
|
const initialDefault = configs.SECURITY_DEPOSIT?.amount || 500000;
|
|
const finalDefault = configs.FIRST_FILL?.amount || 1500000;
|
|
|
|
setPaymentDetails({
|
|
verificationTransactionId: '',
|
|
receivedAmount: activeType === 'SECURITY_DEPOSIT' ? 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 = async () => {
|
|
if (!paymentDetails.verificationTransactionId || !paymentDetails.receivedDate) {
|
|
toast.error('Please fill in all required payment details');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
await onboardingService.updateSecurityDeposit({
|
|
applicationId,
|
|
depositType: activeType,
|
|
amount: Number(paymentDetails.receivedAmount),
|
|
paymentReference: paymentDetails.verificationTransactionId,
|
|
status: 'Verified'
|
|
});
|
|
|
|
toast.success(`${activeType === 'SECURITY_DEPOSIT' ? 'Security Deposit' : 'First Fill'} verified and approved`);
|
|
await fetchData();
|
|
} catch (error) {
|
|
toast.error('Failed to verify payment');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleRejectPayment = async () => {
|
|
if (!paymentDetails.verificationRemarks) {
|
|
toast.error('Please provide remarks for rejection');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
await onboardingService.updateSecurityDeposit({
|
|
applicationId,
|
|
depositType: activeType,
|
|
status: 'Rejected',
|
|
remarks: paymentDetails.verificationRemarks
|
|
});
|
|
|
|
toast.error(`${activeType === 'SECURITY_DEPOSIT' ? 'Security Deposit' : 'First Fill'} rejected`);
|
|
await fetchData();
|
|
} catch (error) {
|
|
toast.error('Failed to reject payment');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center p-20">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!application) {
|
|
return <div className="p-20 text-center">Application not found</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="outline" size="icon" onClick={onBack}>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-3xl mb-1">Payment Verification</h1>
|
|
<div className="flex gap-2 mt-2">
|
|
<Button
|
|
size="sm"
|
|
variant={activeType === 'SECURITY_DEPOSIT' ? 'default' : 'outline'}
|
|
className={activeType === 'SECURITY_DEPOSIT' ? 'bg-amber-600 hover:bg-amber-700' : ''}
|
|
onClick={() => setActiveType('SECURITY_DEPOSIT')}
|
|
>
|
|
Security Deposit
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant={activeType === 'FIRST_FILL' ? 'default' : 'outline'}
|
|
className={activeType === 'FIRST_FILL' ? 'bg-amber-600 hover:bg-amber-700' : ''}
|
|
onClick={() => setActiveType('FIRST_FILL')}
|
|
>
|
|
First Fill
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Banner */}
|
|
<Card className={cn(
|
|
"border",
|
|
activeDeposit?.status === 'Verified' ? "border-green-200 bg-green-50" :
|
|
activeDeposit?.status === 'Rejected' ? "border-red-200 bg-red-50" :
|
|
"border-amber-200 bg-amber-50"
|
|
)}>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className={cn(
|
|
"w-12 h-12 rounded-full flex items-center justify-center",
|
|
activeDeposit?.status === 'Verified' ? "bg-green-100" :
|
|
activeDeposit?.status === 'Rejected' ? "bg-red-100" :
|
|
"bg-amber-100"
|
|
)}>
|
|
<IndianRupee className={cn(
|
|
"w-6 h-6",
|
|
activeDeposit?.status === 'Verified' ? "text-green-600" :
|
|
activeDeposit?.status === 'Rejected' ? "text-red-600" :
|
|
"text-amber-600"
|
|
)} />
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-900 font-bold">
|
|
{activeType === 'SECURITY_DEPOSIT' ? 'Security Deposit' : 'First Fill'}
|
|
</p>
|
|
<p className="text-sm text-slate-600">
|
|
{activeDeposit?.status === 'Verified'
|
|
? `Verified on ${formatDateTime(activeDeposit.verifiedAt)}`
|
|
: activeDeposit?.status === 'Rejected'
|
|
? 'Payment Rejected'
|
|
: 'Awaiting Verification'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Badge className={cn(
|
|
activeDeposit?.status === 'Verified' ? "bg-green-600" :
|
|
activeDeposit?.status === 'Rejected' ? "bg-red-600" :
|
|
"bg-amber-600 text-white"
|
|
)}>
|
|
{activeDeposit?.status || 'No Record'}
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-xl">
|
|
<User className="w-5 h-5 text-amber-600" />
|
|
Applicant Information
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-2 gap-y-4 gap-x-8">
|
|
<div>
|
|
<Label className="text-slate-500">Application ID</Label>
|
|
<p className="text-slate-900 font-medium">{application.applicationId || application.id}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-500">Applicant Name</Label>
|
|
<p className="text-slate-900 font-medium">{application.applicantName}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-500">Location</Label>
|
|
<p className="text-slate-900 font-medium">{application.city || application.preferredLocation}, {application.state}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-500">Email / Phone</Label>
|
|
<p className="text-slate-700 text-sm">{application.email}</p>
|
|
<p className="text-slate-700 text-sm">{application.phone}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-xl">
|
|
<CreditCard className="w-5 h-5 text-amber-600" />
|
|
Deposit Tracking
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200">
|
|
<Label className="text-slate-500 block mb-1">Expected Amount</Label>
|
|
<p className="text-2xl font-bold text-amber-900">
|
|
₹{(activeType === 'SECURITY_DEPOSIT'
|
|
? (configs.SECURITY_DEPOSIT?.amount || 500000)
|
|
: (configs.FIRST_FILL?.amount || 1500000)
|
|
).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
<div className={cn(
|
|
"p-4 rounded-lg border",
|
|
activeDeposit?.status === 'Verified' ? "bg-green-50 border-green-200" : "bg-blue-50 border-blue-200"
|
|
)}>
|
|
<Label className="text-slate-500 block mb-1">Receipt Status</Label>
|
|
<p className={cn(
|
|
"text-2xl font-bold",
|
|
activeDeposit?.status === 'Verified' ? "text-green-700" : "text-blue-700"
|
|
)}>
|
|
{activeDeposit?.status || 'Not Started'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{activeDeposit?.paymentReference && (
|
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
|
<div>
|
|
<Label className="text-slate-500">Payment Reference</Label>
|
|
<p className="text-slate-900 font-mono">{activeDeposit.paymentReference}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-500">Verified By</Label>
|
|
<p className="text-slate-900">{activeDeposit.verifier?.fullName || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-xl">
|
|
<FileText className="w-5 h-5 text-amber-600" />
|
|
Verification Evidence
|
|
</CardTitle>
|
|
<CardDescription>Documents uploaded by the applicant for payment proof</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{application.uploadedDocuments?.filter((d: any) =>
|
|
activeType === 'SECURITY_DEPOSIT'
|
|
? d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')
|
|
: d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')
|
|
).length > 0 ? (
|
|
<div className="space-y-3">
|
|
{application.uploadedDocuments.filter((d: any) =>
|
|
activeType === 'SECURITY_DEPOSIT'
|
|
? d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')
|
|
: d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')
|
|
).map((doc: any, index: number) => (
|
|
<div key={index} className="flex items-center justify-between p-3 bg-white rounded-lg border border-slate-200 hover:shadow-sm transition-shadow">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded bg-slate-100 flex items-center justify-center">
|
|
<FileText className="w-5 h-5 text-slate-500" />
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-900 font-medium">{doc.fileName || doc.name}</p>
|
|
<p className="text-xs text-slate-500 uppercase">{doc.documentType} • {formatDateTime(doc.createdAt)}</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-amber-600 hover:text-amber-700 hover:bg-amber-50"
|
|
onClick={() => {
|
|
setPreviewDoc(doc);
|
|
setShowPreviewModal(true);
|
|
}}
|
|
>
|
|
View Receipt
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-10 bg-slate-50 rounded-lg border-2 border-dashed border-slate-200">
|
|
<AlertCircle className="w-8 h-8 text-slate-300 mx-auto mb-2" />
|
|
<p className="text-slate-500">No payment documents found in this application.</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<Card className="border-amber-100 shadow-sm">
|
|
<CardHeader className="bg-amber-50/50">
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Wallet className="w-5 h-5 text-amber-600" />
|
|
Finance Action
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-6 space-y-4">
|
|
<div>
|
|
<Label htmlFor="verificationTxnId" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
|
|
UTR / Reference Number <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="verificationTxnId"
|
|
placeholder="Enter Bank UTR Number"
|
|
disabled={activeDeposit?.status === 'Verified'}
|
|
className="mt-1"
|
|
value={paymentDetails.verificationTransactionId}
|
|
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationTransactionId: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="receivedAmount" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
|
|
Amount Received (₹) <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="receivedAmount"
|
|
type="number"
|
|
placeholder={(activeType === 'SECURITY_DEPOSIT' ? 500000 : 1500000).toString()}
|
|
disabled={activeDeposit?.status === 'Verified'}
|
|
className="mt-1"
|
|
value={paymentDetails.receivedAmount}
|
|
onChange={(e) => setPaymentDetails({ ...paymentDetails, receivedAmount: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="receivedDate" className="text-xs uppercase text-slate-500 font-bold tracking-wider">
|
|
Credit Value Date <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="receivedDate"
|
|
type="date"
|
|
disabled={activeDeposit?.status === 'Verified'}
|
|
className="mt-1"
|
|
value={paymentDetails.receivedDate}
|
|
onChange={(e) => setPaymentDetails({ ...paymentDetails, receivedDate: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="remarks" className="text-xs uppercase text-slate-500 font-bold tracking-wider">Verification Remarks</Label>
|
|
<Textarea
|
|
id="remarks"
|
|
placeholder="Any internal notes for reconciliation..."
|
|
rows={3}
|
|
className="mt-1"
|
|
value={paymentDetails.verificationRemarks}
|
|
onChange={(e) => setPaymentDetails({ ...paymentDetails, verificationRemarks: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="pt-4 space-y-3">
|
|
<Button
|
|
className={cn(
|
|
"w-full transition-all duration-200",
|
|
activeDeposit?.status === 'Verified' ? "bg-green-600 hover:bg-green-600 opacity-90" : "bg-amber-600 hover:bg-amber-700"
|
|
)}
|
|
onClick={handleApprovePayment}
|
|
disabled={isSubmitting || activeDeposit?.status === 'Verified'}
|
|
>
|
|
{activeDeposit?.status === 'Verified' ? (
|
|
<><CheckCircle className="w-4 h-4 mr-2" /> Verified Successfully</>
|
|
) : (
|
|
<><CheckCircle className="w-4 h-4 mr-2" /> Mark as Verified</>
|
|
)}
|
|
</Button>
|
|
|
|
{activeDeposit?.status !== 'Verified' && activeDeposit?.status !== 'Rejected' && (
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
onClick={handleRejectPayment}
|
|
disabled={isSubmitting}
|
|
>
|
|
<XCircle className="w-4 h-4 mr-2" />
|
|
Reject / Flag Discrepancy
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-slate-900 text-white border-none shadow-xl">
|
|
<CardHeader>
|
|
<CardTitle className="text-base font-medium flex items-center gap-2">
|
|
<Clock className="w-4 h-4 text-amber-400" />
|
|
Next Steps
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="text-xs text-slate-300 space-y-3">
|
|
<p>Once verified, the following will occur:</p>
|
|
<ul className="list-disc pl-4 space-y-2">
|
|
<li>Applicant status will advance to {activeType === 'SECURITY_DEPOSIT' ? 'LOI Issuance' : 'LOA Approval'}</li>
|
|
<li>Email notification will be sent to Applicant</li>
|
|
<li>Digital {activeType === 'SECURITY_DEPOSIT' ? 'LOI' : 'LOA'} generation will be unlocked</li>
|
|
<li>This payment confirms the {activeType === 'SECURITY_DEPOSIT' ? 'Security Deposit' : 'First Fill'}</li>
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Global Preview Modal */}
|
|
<DocumentPreviewModal
|
|
isOpen={showPreviewModal}
|
|
onClose={() => setShowPreviewModal(false)}
|
|
document={previewDoc}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|