"use client"; import { useEffect, useState, type FormEvent } from "react"; import { getCategoryLabel, type CategoryValue } from "@/lib/categories"; import { formatCurrencyFromCents } from "@/lib/money"; type RecurringDefinition = { id: string; title: string; amountCents: number; category: string; dayOfMonth: number; }; type CategoryOption = { value: string; label: string; }; type Props = { categoryOptions: CategoryOption[]; }; const emptyForm = { title: "", amount: "", dayOfMonth: "1", category: "MISC" as CategoryValue }; export function RecurringExpenseManager({ categoryOptions }: Props) { const [definitions, setDefinitions] = useState([]); const [showAddForm, setShowAddForm] = useState(false); const [editingId, setEditingId] = useState(null); const [formState, setFormState] = useState(emptyForm); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); useEffect(() => { async function load() { const res = await fetch("/recurring-expenses", { cache: "no-store" }); const payload = (await res.json()) as { recurringExpenses?: RecurringDefinition[] }; setDefinitions(payload.recurringExpenses ?? []); } void load(); }, []); function openAdd() { setEditingId(null); setFormState(emptyForm); setError(null); setShowAddForm(true); } function openEdit(def: RecurringDefinition) { setShowAddForm(false); setEditingId(def.id); setFormState({ title: def.title, amount: (def.amountCents / 100).toFixed(2), dayOfMonth: String(def.dayOfMonth), category: def.category as CategoryValue, }); setError(null); } function cancelForm() { setShowAddForm(false); setEditingId(null); setFormState(emptyForm); setError(null); } async function handleSubmit(event: FormEvent) { event.preventDefault(); setBusy(true); setError(null); const isEditing = editingId !== null; const url = isEditing ? `/recurring-expenses/${editingId}` : "/recurring-expenses"; const response = await fetch(url, { method: isEditing ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: formState.title, amount: formState.amount, dayOfMonth: parseInt(formState.dayOfMonth, 10), category: formState.category, }), }); setBusy(false); if (!response.ok) { const payload = (await response.json().catch(() => null)) as { error?: string } | null; setError(payload?.error ?? "Could not save."); return; } const payload = (await response.json()) as { recurringExpense: RecurringDefinition }; if (isEditing) { setDefinitions((current) => current.map((d) => (d.id === editingId ? payload.recurringExpense : d)), ); } else { setDefinitions((current) => [...current, payload.recurringExpense]); } cancelForm(); } async function handleDelete(id: string) { setBusy(true); const response = await fetch(`/recurring-expenses/${id}`, { method: "DELETE" }); setBusy(false); if (!response.ok) { setError("Could not delete."); return; } setDefinitions((current) => current.filter((d) => d.id !== id)); } return (

Fixed monthly costs

Recurring expenses

These appear automatically in every month without manual entry.

{!showAddForm && editingId === null && ( )}
{(showAddForm || editingId !== null) && (

{editingId ? "Edit recurring expense" : "New recurring expense"}

{error}

)} {!showAddForm && editingId === null && error && (

{error}

)} {definitions.length === 0 && !showAddForm ? (
No recurring expenses yet. Add fixed monthly costs like rent or EMI.
) : (
{definitions.map((def) => (

{def.title}

Day {def.dayOfMonth} each month ยท {getCategoryLabel(def.category as CategoryValue)}

{formatCurrencyFromCents(def.amountCents)}

{editingId !== def.id && ( <> )}
))}
)}
); }