"use client"; import * as React from "react"; import { TextFieldRoot, TextFieldInput, DialogRoot, DialogBackdrop, DialogPositioner, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Skeleton, } from "@seed-design/react"; import { MercuryButton } from "@/ds/MercuryButton"; import { useCategories } from "@/api/hooks/reads"; import { useCategoryMutations } from "@/api/hooks/mutations"; import type { Category } from "@/api/schemas"; import { profileStrings } from "./strings"; // A small curated glyph set stands in for the iOS `SeedMulticolorIcon` catalog // (not ported to web) โ€” plain emoji stored verbatim in the `icon` string field. const ICON_CHOICES = ["๐Ÿ”", "๐Ÿš—", "๐Ÿ ", "๐Ÿ’Š", "๐ŸŽฎ", "๐Ÿ›๏ธ", "๐Ÿ’ก", "๐Ÿ“š", "โœˆ๏ธ", "๐ŸŽ", "๐Ÿ’ฐ", "๐Ÿ“ฑ", "๐Ÿพ", "โšฝ", "โ˜•", "๐Ÿงพ"]; interface CategoryGroup { name: string; icon?: string | null; children: Category[]; } /** Mirrors `CategoriesModel.load()` in CategoriesView.swift: a depth-0 row * starts a new group, every following depth>0 row until the next depth-0 * belongs to it. */ function groupCategories(categories: Category[]): CategoryGroup[] { const groups: CategoryGroup[] = []; let current: CategoryGroup | null = null; for (const c of categories) { if (c.depth === 0) { current = { name: c.name, icon: c.icon, children: [] }; groups.push(current); } else if (current) { current.children.push(c); } } return groups; } type SheetState = { mode: "add" } | { mode: "edit"; category: Category } | null; export interface CategoriesManagerProps { onBack?: () => void; } export function CategoriesManager({ onBack }: CategoriesManagerProps) { const { data: categories, isLoading } = useCategories(); const { add, update, delete: remove } = useCategoryMutations(); const [sheet, setSheet] = React.useState(null); const [deleteTarget, setDeleteTarget] = React.useState(null); const groups = React.useMemo(() => groupCategories(categories ?? []), [categories]); const parentNames = React.useMemo(() => groups.map((g) => g.name), [groups]); async function handleConfirmDelete() { if (!deleteTarget) return; await remove.mutateAsync(deleteTarget); setDeleteTarget(null); } if (sheet) { return ( setSheet(null)} onSave={async (values) => { if (sheet.mode === "add") { await add.mutateAsync({ name: values.name, kind: "expense", parent: values.parent || undefined, icon: values.icon, }); } else { await update.mutateAsync({ oldName: sheet.category.name, newName: values.name, parent: values.parent ? values.parent : null, icon: values.icon, }); } setSheet(null); }} /> ); } return (
{onBack && ( )}

{profileStrings.categories.title}

setSheet({ mode: "add" })}> + {profileStrings.categories.add}
{isLoading && (
)} {!isLoading && groups.length === 0 && (

{profileStrings.categories.empty}

)} {groups.map((group) => (
setSheet({ mode: "edit", category: { name: group.name, kind: "expense", depth: 0, icon: group.icon } })} onDelete={() => setDeleteTarget(group.name)} /> {group.children.length > 0 && (
{group.children.map((child) => ( setSheet({ mode: "edit", category: child })} onDelete={() => setDeleteTarget(child.name)} /> ))}
)}
))} { if (!open) setDeleteTarget(null); }}> {profileStrings.categories.deleteTitle} {deleteTarget ? profileStrings.categories.deleteMessage(deleteTarget) : ""} setDeleteTarget(null)} style={{ flex: 1 }}> {profileStrings.categories.cancel} {profileStrings.categories.deleteAction}
); } function CategoryRow({ icon, name, heading, onEdit, onDelete, }: { icon?: string | null; name: string; heading?: boolean; onEdit: () => void; onDelete: () => void; }) { return (
{icon || "๐Ÿท๏ธ"} {name}
); } function CategoryChip({ category, onEdit, onDelete, }: { category: Category; onEdit: () => void; onDelete: () => void; }) { return (
{category.icon || "๐Ÿท๏ธ"} {category.name}
); } function CategoryEditor({ parents, initial, onCancel, onSave, }: { parents: string[]; initial?: Category; onCancel: () => void; onSave: (values: { name: string; parent: string; icon: string }) => Promise; }) { const [name, setName] = React.useState(initial?.name ?? ""); const [parent, setParent] = React.useState(""); const [icon, setIcon] = React.useState(initial?.icon || ICON_CHOICES[0]); const [saving, setSaving] = React.useState(false); const editing = Boolean(initial); const canSave = name.trim().length > 0 && !saving; async function handleSave() { if (!canSave) return; setSaving(true); try { await onSave({ name: name.trim(), parent, icon }); } finally { setSaving(false); } } return (

{editing ? profileStrings.categories.edit : profileStrings.categories.add}

{profileStrings.categories.icon}

{ICON_CHOICES.map((choice) => ( ))}
{profileStrings.categories.save}
); }