244 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|