import { useState, useEffect, useCallback, useRef } from 'react'; import { toast } from 'sonner'; import { Plus, Pencil, Trash2 } from 'lucide-react'; import { api } from '@/api.js'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { InputDialog } from '@/components/ui/input-dialog'; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from '@/components/ui/alert-dialog'; import { buttonVariants } from '@/components/ui/button'; import { cn } from '@/lib/utils'; // ─── CategoriesPage ─────────────────────────────────────────────────────────── export default function CategoriesPage() { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [newName, setNewName] = useState(''); const [adding, setAdding] = useState(false); const addInputRef = useRef(null); // Rename dialog state const [renameTarget, setRenameTarget] = useState(null); // { id, name } const [renaming, setRenaming] = useState(false); // Delete dialog state const [deleteTarget, setDeleteTarget] = useState(null); // { id, name } const [deleting, setDeleting] = useState(false); const load = useCallback(async () => { try { const cats = await api.categories(); setCategories(cats); } catch (err) { toast.error(err.message); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); // ── Add ────────────────────────────────────────────────────────────────────── async function handleAdd(e) { e.preventDefault(); const trimmed = newName.trim(); if (!trimmed) { toast.error('Enter a category name'); addInputRef.current?.focus(); return; } setAdding(true); try { await api.createCategory({ name: trimmed }); toast.success(`"${trimmed}" added`); setNewName(''); addInputRef.current?.focus(); load(); } catch (err) { toast.error(err.message); } finally { setAdding(false); } } // ── Rename ─────────────────────────────────────────────────────────────────── function openRename(cat) { setRenameTarget(cat); } async function handleRename(name) { setRenaming(true); try { await api.updateCategory(renameTarget.id, { name }); toast.success('Category renamed'); setRenameTarget(null); load(); } catch (err) { toast.error(err.message); } finally { setRenaming(false); } } // ── Delete ─────────────────────────────────────────────────────────────────── function openDelete(cat) { setDeleteTarget(cat); } async function handleDelete() { setDeleting(true); try { await api.deleteCategory(deleteTarget.id); toast.success(`"${deleteTarget.name}" deleted`); setDeleteTarget(null); load(); } catch (err) { toast.error(err.message); } finally { setDeleting(false); } } // ── Render ─────────────────────────────────────────────────────────────────── return (
{/* Page header — floats on bg-background */}

Categories

{categories.length} categories

{/* Card layer — lifted above page background */}
{/* Card header with inline add form */}
setNewName(e.target.value)} placeholder="New category name…" disabled={adding} className="h-8 text-sm" />
{/* Category list */} {loading ? (
Loading…
) : categories.length === 0 ? (
No categories yet. Add one above.
) : (
{categories.map((cat) => (
{cat.name} {cat.bill_count > 0 && ( {cat.bill_count} {cat.bill_count === 1 ? 'bill' : 'bills'} )}
))}
)}
{/* /card */} {/* Rename dialog */} { if (!open) setRenameTarget(null); }} title="Rename Category" label="Name" defaultValue={renameTarget?.name ?? ''} placeholder="Category name" confirmLabel="Rename" loading={renaming} onConfirm={handleRename} /> {/* Delete dialog */} { if (!open) setDeleteTarget(null); }}> Delete {deleteTarget?.name}? Bills in this category will become uncategorized. This cannot be undone. Cancel {deleting ? 'Deleting…' : 'Delete Category'}
); }