fix: show full category path (parent · subcategory) everywhere, group filters by hierarchy

Транзакция на подкатегории показывала только её имя — непонятно было,
что это подкатегория и к какой родительской категории она относится.
Фильтры по категории в Транзакциях и Отчётах тоже перечисляли
подкатегории наравне с родительскими, без иерархии.

- categoryLabels.ts: categoryPath() и buildHierarchicalCategories() —
  общая логика, вынесенная из TransactionFormModal.
- Список транзакций и «Последние транзакции» на Dashboard показывают
  счёт, затем родителя, затем подкатегорию.
- Фильтр категорий в Транзакциях сгруппирован по типу (как в Отчётах);
  оба списка показывают подкатегорию с отступом под родителем.
This commit is contained in:
ZProger
2026-08-30 19:30:49 +03:00
parent 87b37a10ff
commit dbae7938da
6 changed files with 111 additions and 28 deletions

View File

@@ -29,3 +29,43 @@ export function translateCategoryName(name: string): string {
const key = DEFAULT_CATEGORY_KEYS[name];
return key ? t(key) : name;
}
/** A category's full path for display — "Groceries" alone reads as if it
* were a top-level category even when it's actually a subcategory; showing
* "Groceries · Sweets" makes the parent unambiguous everywhere a bare
* category name used to be shown (transaction rows, filters). `categories`
* is the full list (any kind, any order) to resolve the parent's own name
* from `category.parent_id` — subcategories are one level deep only, so a
* single lookup is enough. */
export function categoryPath(
category: { name: string; parent_id: number | null },
categories: { id: number; name: string }[] | undefined
): string {
if (category.parent_id == null) return translateCategoryName(category.name);
const parent = categories?.find((c) => c.id === category.parent_id);
if (!parent) return translateCategoryName(category.name);
return `${translateCategoryName(parent.name)} · ${translateCategoryName(category.name)}`;
}
/** Orders a category list for display in a dropdown: alphabetical by
* translated name, with each subcategory placed directly under its own
* parent (not scattered by name) and flagged `indented` so callers can
* prefix it visually (e.g. " ↳ "). Shared by every category picker
* (TransactionFormModal, the Transactions/Reports filters) so they all
* read the same way. */
export function buildHierarchicalCategories<C extends { id: number; parent_id: number | null; name: string }>(
categories: C[],
language: string
): (C & { indented: boolean })[] {
const sorted = [...categories].sort((a, b) =>
translateCategoryName(a.name).localeCompare(translateCategoryName(b.name), language)
);
const result: (C & { indented: boolean })[] = [];
for (const parent of sorted.filter((category) => category.parent_id === null)) {
result.push({ ...parent, indented: false });
for (const child of sorted.filter((category) => category.parent_id === parent.id)) {
result.push({ ...child, indented: true });
}
}
return result;
}