Dealer_Onboard_Frontend/src/components/applications/FinancePaymentDetailsPage.tsx

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>
);
}