// AccountsPage — CRUD UI for balance accounts and balance categories. // // Issue #138 (Bilan #1a) ships the route `/balance/accounts` with two tabs: // - Comptes : full CRUD over balance_accounts (create/edit/archive) // - Catégories : list of seeded + user-created categories. Users can add // simple-kind categories (the priced toggle lands in #140), // rename them, and delete the ones they created (the seeded // ones are protected at the service layer). // // The sidebar entry "Bilan" is intentionally NOT added here — per spec-plan // v2 it lands in Issue #141 (Bilan #3) when the `/balance` overview page // becomes navigable. Until then the route is reachable directly via URL. import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ArchiveRestore, Edit2, Plus, Trash2, Wallet } from "lucide-react"; import type { BalanceAccountWithCategory, BalanceCategory, } from "../shared/types"; import { useBalanceAccounts } from "../hooks/useBalanceAccounts"; import AccountForm from "../components/balance/AccountForm"; type Tab = "accounts" | "categories"; export default function AccountsPage() { const { t } = useTranslation(); const { state, setIncludeArchived, addAccount, editAccount, archiveAccount, unarchiveAccount, addCategory, editCategory, removeCategory, } = useBalanceAccounts(); const [activeTab, setActiveTab] = useState("accounts"); const [showAccountForm, setShowAccountForm] = useState(false); const [editingAccount, setEditingAccount] = useState(null); const [showCategoryForm, setShowCategoryForm] = useState(false); const [newCategoryKey, setNewCategoryKey] = useState(""); const [newCategoryLabel, setNewCategoryLabel] = useState(""); const activeCategories = useMemo( () => state.categories.filter((c) => c.is_active), [state.categories] ); const renderCategoryLabel = (cat: BalanceCategory) => t(cat.i18n_key, { defaultValue: cat.key }); const closeAccountForm = () => { setShowAccountForm(false); setEditingAccount(null); }; const handleAccountSubmit = async ( payload: | Parameters[0] | Parameters[1] ) => { try { if (editingAccount) { await editAccount(editingAccount.id, payload as Parameters[1]); } else { await addAccount(payload as Parameters[0]); } closeAccountForm(); } catch { // Error already surfaced via state.error } }; const handleCreateCategory = async () => { const key = newCategoryKey.trim(); const label = newCategoryLabel.trim(); if (!key) return; // For user-created categories we use the literal label as the i18n_key // fallback — they don't ship in the locale bundle, so renderers default // to this string. (The CategoryCombobox does the same for legacy v2 rows.) const i18nKey = label || key; try { await addCategory({ key, i18n_key: i18nKey, kind: "simple", sort_order: 100, // user-created categories sort after seeded ones }); setNewCategoryKey(""); setNewCategoryLabel(""); setShowCategoryForm(false); } catch { // Error already surfaced via state.error } }; return (

{t("balance.accountsPage.title")}

{state.error && (
{state.errorCode ? t(`balance.errors.${state.errorCode}`, { defaultValue: state.error, }) : state.error}
)}
{activeTab === "accounts" && (
{showAccountForm ? (

{editingAccount ? t("balance.account.form.editTitle") : t("balance.account.form.createTitle")}

) : null} {state.accounts.length === 0 ? (
{t("balance.accountsPage.empty")}
) : (
{state.accounts.map((acc) => { const isArchived = !!acc.archived_at; return ( ); })}
{t("balance.account.fields.name")} {t("balance.account.fields.category")} {t("balance.account.fields.symbol")} {t("balance.account.fields.currency")} {t("balance.account.fields.status")} {t("balance.account.fields.actions")}
{acc.name} {t(acc.category_i18n_key, { defaultValue: acc.category_key, })} {acc.symbol ?? "—"} {acc.currency} {isArchived ? ( {t("balance.account.status.archived")} ) : ( {t("balance.account.status.active")} )}
{isArchived ? ( ) : ( )}
)}
)} {activeTab === "categories" && (

{t("balance.category.intro")}

{showCategoryForm && (

{t("balance.category.form.createTitle")}

setNewCategoryKey(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--card)] text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" placeholder={t("balance.category.form.keyPlaceholder")} autoComplete="off" />
setNewCategoryLabel(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--card)] text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" placeholder={t("balance.category.form.labelPlaceholder")} autoComplete="off" />

{t("balance.category.form.simpleOnlyNotice")}

)}
{state.categories.map((cat) => ( ))}
{t("balance.category.fields.name")} {t("balance.category.fields.key")} {t("balance.category.fields.kind")} {t("balance.category.fields.origin")} {t("balance.category.fields.actions")}
{renderCategoryLabel(cat)} {cat.key} {t(`balance.category.kind.${cat.kind}`)} {cat.is_seed ? ( {t("balance.category.origin.seeded")} ) : ( {t("balance.category.origin.user")} )}
)}
); }