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

@@ -13,7 +13,7 @@ import { useCategories } from "@/hooks/useCategories";
import { useTags } from "@/hooks/useTags";
import type { TransactionSort } from "@/api/transactions";
import { useTranslation } from "@/lib/i18n";
import { translateCategoryName } from "@/lib/categoryLabels";
import { buildHierarchicalCategories, translateCategoryName } from "@/lib/categoryLabels";
import type { Transaction, TransactionType } from "@/types";
const PAGE_SIZE = 20;
@@ -35,7 +35,7 @@ function parseYearParam(value: string | null, fallback: number): number {
}
export function TransactionsPage() {
const { t } = useTranslation();
const { t, language } = useTranslation();
const now = new Date();
// Deep-linked from the Dashboard's "All transactions" link, which carries
// the month/year the user was already looking at (?year=&month=) so this
@@ -84,6 +84,17 @@ export function TransactionsPage() {
const deleteTransaction = useDeleteTransaction();
const totalPages = data ? Math.max(1, Math.ceil(data.total / PAGE_SIZE)) : 1;
// Grouped by kind and hierarchical within each group (a subcategory right
// under its own parent, indented) — a bare "Sweets" option next to
// top-level categories reads as if it were one itself.
const expenseCategoryOptions = buildHierarchicalCategories(
(categories ?? []).filter((category) => category.kind === "expense"),
language
);
const incomeCategoryOptions = buildHierarchicalCategories(
(categories ?? []).filter((category) => category.kind === "income"),
language
);
function openCreateModal() {
setEditingTransaction(null);
@@ -199,11 +210,26 @@ export function TransactionsPage() {
className="sm:w-56"
>
<option value="">{t("transactions.allCategories")}</option>
{categories?.map((category) => (
<option key={category.id} value={category.id}>
{translateCategoryName(category.name)}
</option>
))}
{expenseCategoryOptions.length > 0 && (
<optgroup label={t("reports.expenseGroup")}>
{expenseCategoryOptions.map((category) => (
<option key={category.id} value={category.id}>
{category.indented ? ` ↳ ` : ""}
{translateCategoryName(category.name)}
</option>
))}
</optgroup>
)}
{incomeCategoryOptions.length > 0 && (
<optgroup label={t("reports.incomeGroup")}>
{incomeCategoryOptions.map((category) => (
<option key={category.id} value={category.id}>
{category.indented ? ` ↳ ` : ""}
{translateCategoryName(category.name)}
</option>
))}
</optgroup>
)}
</Select>
{tags && tags.length > 0 && (
<Select