Files

244 lines
12 KiB
TypeScript

import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/Components/ui/dialog';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Textarea } from '@/Components/ui/textarea';
import { AlertCircle, FileCheck, Loader2, UploadCloud, X } from 'lucide-react';
import { FormEvent, useRef, useState } from 'react';
import { router } from '@inertiajs/react';
interface Props {
isOpen: boolean;
onClose: () => void;
invoice: {
ulid: string;
invoice_number: string;
total_amount: string | number;
retention_amount?: string | number;
penalty_amount?: string | number;
penalty_rate?: string | number;
retention_rate?: string | number;
project?: { name: string; code: string };
} | null;
}
const formatCurrency = (v: string | number) =>
new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v || 0));
export default function RetentionPaymentProofModal({ isOpen, onClose, invoice }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [notes, setNotes] = useState('');
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
if (!invoice) return null;
const hasPenalty = Number(invoice.penalty_amount || 0) > 0;
const totalRetentionDue = Number(invoice.retention_amount || 0) + Number(invoice.penalty_amount || 0);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const validTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/jpg'];
if (!validTypes.includes(file.type) && !file.name.match(/\.(pdf|jpe?g|png)$/i)) {
setError('Please select a valid document (PDF, JPG, PNG).');
setSelectedFile(null);
return;
}
if (file.size > 10 * 1024 * 1024) {
setError('File size must not exceed 10MB.');
setSelectedFile(null);
return;
}
setError(null);
setSelectedFile(file);
}
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!selectedFile) {
setError('Please upload a payment proof receipt (PDF or Image).');
return;
}
setProcessing(true);
setError(null);
const formData = new FormData();
formData.append('media', selectedFile);
if (notes.trim()) {
formData.append('notes', notes.trim());
}
router.post(route('finance.send-payment-proof', invoice.ulid), formData, {
forceFormData: true,
onSuccess: () => {
setSelectedFile(null);
setNotes('');
setProcessing(false);
onClose();
},
onError: (errors) => {
setProcessing(false);
setError(errors.media || errors.notes || 'Failed to upload payment proof.');
},
onFinish: () => {
setProcessing(false);
},
});
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-[520px] bg-white shadow-2xl border-slate-200">
<DialogHeader>
<DialogTitle className="text-lg font-bold text-slate-900 flex items-center gap-2">
<UploadCloud className="h-5 w-5 text-indigo-600" />
Send Payment Proof
</DialogTitle>
<DialogDescription className="text-xs text-slate-500">
Upload your payment evidence/receipt to submit to Executive for receipt confirmation.
</DialogDescription>
</DialogHeader>
{/* Invoice Summary Box */}
<div className="bg-slate-50 border border-slate-200 rounded-xl p-3.5 space-y-2 text-xs">
<div className="flex justify-between items-center text-slate-600">
<span>Invoice Number:</span>
<span className="font-semibold text-slate-900 font-mono">{invoice.invoice_number}</span>
</div>
{invoice.project && (
<div className="flex justify-between items-center text-slate-600">
<span>Project:</span>
<span className="font-medium text-slate-800">{invoice.project.name}</span>
</div>
)}
<div className="flex justify-between items-center text-slate-600">
<span>Total Invoice Amount:</span>
<span className="font-semibold text-slate-900">{formatCurrency(invoice.total_amount)}</span>
</div>
{invoice.retention_amount !== undefined && (
<div className="flex justify-between items-center pt-1.5 border-t border-slate-200 text-slate-900 font-bold">
<span>{hasPenalty ? 'Total Retention & Penalty Due:' : '10% Retention Remittance:'}</span>
<span className="text-rose-600 font-mono">-{formatCurrency(totalRetentionDue)}</span>
</div>
)}
{hasPenalty && (
<div className="flex justify-between items-center text-[11px] text-slate-500 font-mono">
<span>Breakdown (Base + {invoice.penalty_rate}% Penalty):</span>
<span>{formatCurrency(invoice.retention_amount || 0)} + {formatCurrency(invoice.penalty_amount || 0)}</span>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-4 pt-1">
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-xs text-red-700 flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-red-600 shrink-0 mt-0.5" />
<span>{error}</span>
</div>
)}
{/* File Upload Zone */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-slate-700">Payment Receipt / Deposit Slip *</Label>
<div
onClick={() => fileInputRef.current?.click()}
className={`border-2 border-dashed rounded-xl p-5 text-center cursor-pointer transition-all ${
selectedFile
? 'border-indigo-400 bg-indigo-50/50'
: 'border-slate-300 hover:border-indigo-400 bg-slate-50/50 hover:bg-indigo-50/20'
}`}
>
<input
ref={fileInputRef}
type="file"
accept=".pdf,image/png,image/jpeg,image/jpg"
className="hidden"
onChange={handleFileChange}
/>
{selectedFile ? (
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-2.5 text-left">
<div className="p-2 bg-indigo-100 text-indigo-700 rounded-lg">
<FileCheck className="h-5 w-5" />
</div>
<div>
<p className="text-xs font-semibold text-slate-800 line-clamp-1">{selectedFile.name}</p>
<p className="text-[10px] text-slate-500">{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
</div>
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={(e) => {
e.stopPropagation();
setSelectedFile(null);
if (fileInputRef.current) fileInputRef.current.value = '';
}}
className="text-slate-400 hover:text-red-600"
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div className="space-y-1.5">
<div className="mx-auto w-10 h-10 rounded-full bg-indigo-50 flex items-center justify-center text-indigo-600">
<UploadCloud className="h-5 w-5" />
</div>
<p className="text-xs font-medium text-slate-700">
Click to browse or drag payment proof receipt
</p>
<p className="text-[10px] text-slate-400">PDF, PNG, JPG up to 10MB</p>
</div>
)}
</div>
</div>
{/* Reference / Notes */}
<div className="space-y-1.5">
<Label htmlFor="proof-notes" className="text-xs font-semibold text-slate-700">
Bank Reference / Transaction Notes (Optional)
</Label>
<Textarea
id="proof-notes"
placeholder="e.g. Bank Transfer Ref: BT-981244, Deposited via BDO Online Banking"
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={2}
className="text-xs resize-none"
/>
</div>
<DialogFooter className="pt-2 flex items-center justify-end gap-2">
<Button type="button" variant="outline" size="sm" onClick={onClose} disabled={processing}>
Cancel
</Button>
<Button
type="submit"
size="sm"
className="bg-indigo-600 hover:bg-indigo-700 text-white font-semibold shadow-xs"
disabled={processing || !selectedFile}
>
{processing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Uploading...
</>
) : (
<>
<UploadCloud className="mr-2 h-4 w-4" />
Submit Payment Proof
</>
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}