import { useState, useRef } from "react" import { useTranslation } from "react-i18next" import { Plus, Pencil, Trash2, Check, X } from "lucide-react" import { toast } from "sonner" import { useTemplate } from "@/hooks/useTemplate" import { useCategories } from "@/hooks/useCategories" import type { TemplateItem, CategoryType } from "@/lib/types" import { categoryColors } from "@/lib/palette" import { formatCurrency } from "@/lib/format" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" import { Skeleton } from "@/components/ui/skeleton" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const CATEGORY_TYPES: CategoryType[] = [ "income", "bill", "variable_expense", "debt", "saving", "investment", ] const TIERS: TemplateItem["item_tier"][] = ["fixed", "variable"] // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- /** Inline-editable template name heading. */ function TemplateName({ name, onSave, }: { name: string onSave: (next: string) => Promise }) { const [editing, setEditing] = useState(false) const [draft, setDraft] = useState(name) const inputRef = useRef(null) function startEdit() { setDraft(name) setEditing(true) // Let React commit the input before focusing. setTimeout(() => inputRef.current?.select(), 0) } async function commit() { if (draft.trim() && draft.trim() !== name) { await onSave(draft.trim()) } setEditing(false) } function cancel() { setDraft(name) setEditing(false) } if (editing) { return (
setDraft(e.target.value)} className="h-8 text-xl font-semibold" onKeyDown={(e) => { if (e.key === "Enter") void commit() if (e.key === "Escape") cancel() }} />
) } return ( ) } // --------------------------------------------------------------------------- // Dialog state types // --------------------------------------------------------------------------- type DialogMode = "create" | "edit" interface DialogState { mode: DialogMode item?: TemplateItem categoryId: string tier: TemplateItem["item_tier"] amount: string } const DEFAULT_DIALOG: DialogState = { mode: "create", categoryId: "", tier: "fixed", amount: "", } // --------------------------------------------------------------------------- // Main page // --------------------------------------------------------------------------- export default function TemplatePage() { const { t } = useTranslation() const { template, items, loading, updateName, createItem, updateItem, deleteItem } = useTemplate() const { categories } = useCategories() const [dialogOpen, setDialogOpen] = useState(false) const [dialog, setDialog] = useState(DEFAULT_DIALOG) // ------------------------------------------------------------------ // Dialog helpers // ------------------------------------------------------------------ function openCreate() { setDialog({ ...DEFAULT_DIALOG, mode: "create" }) setDialogOpen(true) } function openEdit(item: TemplateItem) { setDialog({ mode: "edit", item, categoryId: item.category_id, tier: item.item_tier, amount: String(item.budgeted_amount), }) setDialogOpen(true) } function closeDialog() { setDialogOpen(false) } async function handleSave() { const parsed = parseFloat(dialog.amount) if (!dialog.categoryId || isNaN(parsed) || parsed < 0) return try { if (dialog.mode === "edit" && dialog.item) { await updateItem.mutateAsync({ id: dialog.item.id, item_tier: dialog.tier, budgeted_amount: parsed, }) } else { await createItem.mutateAsync({ category_id: dialog.categoryId, item_tier: dialog.tier, budgeted_amount: parsed, }) } closeDialog() } catch { toast.error(t("common.error")) } } async function handleDelete(id: string) { try { await deleteItem.mutateAsync(id) } catch { toast.error(t("common.error")) } } async function handleNameSave(name: string) { try { await updateName.mutateAsync(name) } catch { toast.error(t("common.error")) } } // ------------------------------------------------------------------ // Derived data: group items by the category type // ------------------------------------------------------------------ const grouped = CATEGORY_TYPES.map((type) => ({ type, items: items.filter((item) => item.category?.type === type), })).filter((g) => g.items.length > 0) // Categories already belonging to a template item (for the "create" // dialog we still allow duplicates, as the spec doesn't forbid them). const isSaving = createItem.isPending || updateItem.isPending || deleteItem.isPending // ------------------------------------------------------------------ // Render // ------------------------------------------------------------------ if (loading) return (
{[1, 2].map((i) => (
{[1, 2, 3].map((j) => (
))}
))}
) return (
{/* Header — mirrors PageShell's flex items-start justify-between gap-4 layout */}
{/* Empty state */} {items.length === 0 ? (

{t("template.empty")}

) : (
{grouped.map(({ type, items: groupItems }) => (
{/* Group heading */}
{t(`categories.types.${type}`)}
{t("categories.name")} {t("categories.type")} {t("template.budgetedAmount")} {groupItems.map((item) => ( {item.category?.name ?? item.category_id} {formatCurrency(item.budgeted_amount)}
))}
))}
)} {/* Add / Edit dialog */} {dialog.mode === "edit" ? t("common.edit") : t("template.addItem")}
{/* Category picker — only shown when creating */} {dialog.mode === "create" && (
)} {/* Tier */}
{/* Budgeted amount */}
setDialog((prev) => ({ ...prev, amount: e.target.value })) } placeholder="0.00" />
) } // --------------------------------------------------------------------------- // Small helper component // --------------------------------------------------------------------------- function TierBadge({ tier, t, }: { tier: TemplateItem["item_tier"] t: (key: string) => string }) { return ( {t(`template.${tier}`)} ) }