import { useState, useEffect } from 'react';
function fmt(value) {
const num = parseFloat(value) || 0;
return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function ProgressBar({ paid, total }) {
const pct = total > 0 ? Math.min(100, (paid / total) * 100) : 0;
return (
= 100 ? 'var(--success)' : 'var(--accent)',
borderRadius: '999px',
transition: 'width 0.3s ease',
}} />
);
}
const TODAY = new Date().toISOString().slice(0, 10);
const EMPTY_FORM = {
name: '',
total_amount: '',
due_date: '',
start_date: TODAY,
assigned_paycheck: 'both',
};
function PlanCard({ plan, onEdit, onDelete }) {
const pct = plan.total_amount > 0 ? Math.min(100, (plan.paid_total / plan.total_amount) * 100) : 0;
return (
{plan.name}
{plan.overdue && (
overdue
)}
{!plan.active && (
paid off
)}
Due {new Date(plan.due_date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' })}
·
{plan.assigned_paycheck == null ? 'Split across both paychecks' : `Paycheck ${plan.assigned_paycheck} only`}
{fmt(plan.paid_total)} paid
0 ? 'var(--text)' : 'var(--success)' }}>
{plan.remaining > 0 ? `${fmt(plan.remaining)} remaining` : 'Paid off'} / {fmt(plan.total_amount)}
{plan.active && (
)}
{!plan.active && (
)}
);
}
export default function Financing() {
const [plans, setPlans] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState(EMPTY_FORM);
const [formError, setFormError] = useState(null);
const [saving, setSaving] = useState(false);
async function load() {
try {
setLoading(true);
const res = await fetch('/api/financing');
if (!res.ok) throw new Error('Failed to load');
setPlans(await res.json());
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, []);
function openAdd() {
setEditingId(null);
setForm(EMPTY_FORM);
setFormError(null);
setShowForm(true);
}
function openEdit(plan) {
setEditingId(plan.id);
setForm({
name: plan.name,
total_amount: plan.total_amount,
due_date: plan.due_date,
start_date: plan.start_date || TODAY,
assigned_paycheck: plan.assigned_paycheck == null ? 'both' : String(plan.assigned_paycheck),
});
setFormError(null);
setShowForm(true);
}
function cancel() {
setShowForm(false);
setEditingId(null);
setForm(EMPTY_FORM);
setFormError(null);
}
function handleChange(e) {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
}
async function handleSave(e) {
e.preventDefault();
setFormError(null);
setSaving(true);
try {
const payload = {
name: form.name,
total_amount: parseFloat(form.total_amount),
due_date: form.due_date,
start_date: form.start_date || TODAY,
assigned_paycheck: form.assigned_paycheck === 'both' ? null : parseInt(form.assigned_paycheck, 10),
};
const url = editingId ? `/api/financing/${editingId}` : '/api/financing';
const method = editingId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) { setFormError(data.error || 'Failed to save'); return; }
await load();
cancel();
} catch (err) {
setFormError(err.message);
} finally {
setSaving(false);
}
}
async function handleDelete(plan) {
if (!window.confirm(`Delete "${plan.name}"? This cannot be undone.`)) return;
try {
const res = await fetch(`/api/financing/${plan.id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete');
await load();
} catch (err) {
alert(err.message);
}
}
const active = plans.filter(p => p.active);
const inactive = plans.filter(p => !p.active);
return (
Financing
{!showForm && (
)}
{showForm && (
{editingId ? 'Edit Financing Plan' : 'New Financing Plan'}
{formError &&
{formError}
}
)}
{loading &&
Loading…
}
{error &&
Error: {error}
}
{!loading && !error && (
<>
{active.length === 0 && !showForm && (
No active financing plans. Click "Add Plan" to get started.
)}
{active.map(plan => (
))}
{inactive.length > 0 && (
<>
Paid Off
{inactive.map(plan => (
))}
>
)}
>
)}
);
}