import { useState, useEffect, useCallback, useRef } from 'react';
import { toast } from 'sonner';
import { ChevronLeft, ChevronRight, MoreHorizontal, ReceiptText } from 'lucide-react';
import { api } from '@/api.js';
import { cn, fmt, fmtDate, todayStr } from '@/lib/utils';
import { Button, buttonVariants } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
AlertDialogAction,
} from '@/components/ui/alert-dialog';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from '@/components/ui/select';
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from '@/components/ui/table';
// ─── Constants ────────────────────────────────────────────────────────────────
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
const PAYMENT_METHODS = [
{ value: 'bank_transfer', label: 'Bank Transfer' },
{ value: 'card', label: 'Card' },
{ value: 'autopay', label: 'Autopay' },
{ value: 'check', label: 'Check' },
{ value: 'cash', label: 'Cash' },
{ value: 'other', label: 'Other' },
];
// ─── Status Config ────────────────────────────────────────────────────────────
const STATUS = {
paid: { label: 'Paid', dot: 'bg-emerald-400', chip: 'bg-emerald-500/10 text-emerald-500' },
upcoming: { label: 'Upcoming', dot: 'bg-slate-400', chip: 'bg-slate-500/10 text-slate-500 dark:text-slate-400' },
due_soon: { label: 'Due Soon', dot: 'bg-amber-400', chip: 'bg-amber-500/10 text-amber-600 dark:text-amber-400' },
late: { label: 'Late', dot: 'bg-orange-400', chip: 'bg-orange-500/10 text-orange-600 dark:text-orange-400' },
missed: { label: 'Missed', dot: 'bg-red-400', chip: 'bg-red-500/10 text-red-500' },
autodraft: { label: 'Autodraft', dot: 'bg-violet-400', chip: 'bg-violet-500/10 text-violet-500' },
};
// ─── Status Chip ──────────────────────────────────────────────────────────────
function StatusChip({ status }) {
const cfg = STATUS[status] ?? { label: status, dot: 'bg-slate-400', chip: 'bg-slate-500/10 text-slate-500' };
return (
{cfg.label}
);
}
// ─── Payment Dialog ───────────────────────────────────────────────────────────
function PaymentDialog({ open, onOpenChange, bill, payment, onSuccess }) {
const isEdit = !!payment;
const [form, setForm] = useState({
amount: '',
paid_date: todayStr(),
method: '',
notes: '',
});
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open) return;
if (isEdit) {
setForm({
amount: String(payment.amount ?? ''),
paid_date: payment.paid_date ?? todayStr(),
method: payment.method ?? '',
notes: payment.notes ?? '',
});
} else {
setForm({
amount: String(bill?.expected_amount ?? ''),
paid_date: todayStr(),
method: '',
notes: '',
});
}
}, [open, isEdit, payment, bill]);
const set = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }));
async function handleSubmit() {
if (!form.amount || !form.paid_date) {
toast.error('Amount and date are required');
return;
}
const amount = parseFloat(form.amount);
if (isNaN(amount) || amount <= 0) {
toast.error('Enter a valid amount');
return;
}
setSaving(true);
try {
const payload = {
amount,
paid_date: form.paid_date,
method: form.method || null,
notes: form.notes || null,
};
if (isEdit) {
await api.updatePayment(payment.id, payload);
toast.success('Payment updated');
} else {
await api.quickPay({ bill_id: bill.id, ...payload });
toast.success(`Paid ${fmt(amount)} for ${bill.name}`);
}
onOpenChange(false);
onSuccess?.();
} catch (err) {
toast.error(err.message);
} finally {
setSaving(false);
}
}
const title = isEdit
? `Edit Payment — ${bill?.name ?? ''}`
: `Record Payment — ${bill?.name ?? ''}`;
return (
);
}
// ─── Remove Payment Alert Dialog ──────────────────────────────────────────────
function RemovePaymentDialog({ open, onOpenChange, bill, paymentId, onSuccess }) {
const [removing, setRemoving] = useState(false);
async function handleConfirm() {
setRemoving(true);
try {
await api.deletePayment(paymentId);
toast.success('Payment removed');
onOpenChange(false);
onSuccess?.();
} catch (err) {
toast.error(err.message);
} finally {
setRemoving(false);
}
}
return (
{row.name} {row.category_name}
Monthly overview
Total Expected
{fmt(summary.total_expected)}
{rows.length} bill{rows.length !== 1 ? 's' : ''} this month
Paid
{fmt(summary.total_paid)}
{paidCount} of {rows.length} paid
Remaining
{fmt(summary.remaining)}
0 ? 'text-orange-400' : 'text-muted-foreground')}> {overdueCount > 0 ? `includes ${fmt(summary.overdue)} overdue` : `${rows.length - paidCount} unpaid`}
Overdue
0 ? 'text-red-500' : 'text-muted-foreground')}> {fmt(summary.overdue)}
{overdueCount > 0 ? `${overdueCount} bill${overdueCount !== 1 ? 's' : ''} overdue` : 'All clear'}