diff --git a/src/pages/AccountsPage.tsx b/src/pages/AccountsPage.tsx index 78f6336..cc00613 100644 --- a/src/pages/AccountsPage.tsx +++ b/src/pages/AccountsPage.tsx @@ -20,6 +20,7 @@ import type { } from "../shared/types"; import { useBalanceAccounts } from "../hooks/useBalanceAccounts"; import AccountForm from "../components/balance/AccountForm"; +import type { CreateBalanceCategoryInput } from "../services/balance.service"; type Tab = "accounts" | "categories"; @@ -43,14 +44,27 @@ export default function AccountsPage() { useState(null); const [showCategoryForm, setShowCategoryForm] = useState(false); - const [newCategoryKey, setNewCategoryKey] = useState(""); - const [newCategoryLabel, setNewCategoryLabel] = useState(""); + /** Local error string for category deletion guard (count + names of linked accounts). */ + const [categoryDeleteError, setCategoryDeleteError] = useState( + null + ); const activeCategories = useMemo( () => state.categories.filter((c) => c.is_active), [state.categories] ); + /** Map category id → array of accounts linked to it (active + archived). */ + const accountsByCategory = useMemo(() => { + const m = new Map(); + for (const acc of state.accounts) { + const list = m.get(acc.balance_category_id) ?? []; + list.push(acc); + m.set(acc.balance_category_id, list); + } + return m; + }, [state.accounts]); + const renderCategoryLabel = (cat: BalanceCategory) => t(cat.i18n_key, { defaultValue: cat.key }); @@ -76,29 +90,39 @@ export default function AccountsPage() { } }; - 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; + const handleCategorySubmit = async (input: CreateBalanceCategoryInput) => { try { - await addCategory({ - key, - i18n_key: i18nKey, - kind: "simple", - sort_order: 100, // user-created categories sort after seeded ones - }); - setNewCategoryKey(""); - setNewCategoryLabel(""); + await addCategory(input); setShowCategoryForm(false); } catch { // Error already surfaced via state.error } }; + /** + * Delete-guard for categories. The service refuses to delete a seeded + * category or one with linked accounts, but we pre-check at the UI to + * surface a richer message that lists the linked-account names. + */ + const handleDeleteCategory = (cat: BalanceCategory) => { + setCategoryDeleteError(null); + if (cat.is_seed) return; + const linked = accountsByCategory.get(cat.id) ?? []; + if (linked.length > 0) { + const sample = linked.slice(0, 3).map((a) => a.name).join(", "); + const more = linked.length > 3 ? ", …" : ""; + setCategoryDeleteError( + t("balance.category.error.has_accounts", { + count: linked.length, + names: `${sample}${more}`, + }) + ); + return; + } + if (!window.confirm(t("balance.category.actions.deleteConfirm"))) return; + removeCategory(cat.id); + }; + return (
@@ -174,6 +198,7 @@ export default function AccountsPage() { : t("balance.account.form.createTitle")} {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")} -

-
- - -
+ setShowCategoryForm(false)} + /> +
+ )} + + {categoryDeleteError && ( +
+ {categoryDeleteError} +
)} @@ -437,28 +421,29 @@ export default function AccountsPage() { > - + {(() => { + const linkedCount = + accountsByCategory.get(cat.id)?.length ?? 0; + const blocked = cat.is_seed || linkedCount > 0; + const titleKey = cat.is_seed + ? t("balance.category.actions.deleteSeedHint") + : linkedCount > 0 + ? t("balance.category.actions.deleteHasAccountsHint", { + count: linkedCount, + }) + : t("common.delete"); + return ( + + ); + })()}