fix: check category parents and repair orphan categories (#393) #472

Open
maximus wants to merge 7 commits from issue-393-category-hierarchy into issue-429-category-combobox-portal

7 commits

Author SHA1 Message Date
le king fu
8a9d9909f9 test: pin the reattachment count to active descendants (#393)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m9s
A deactivated child of the reattached category is not counted in the
number of categories put back, which the Journal line reports.

Resolves #393

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:28:45 -04:00
le king fu
bad53fe85b docs: name the budget as a reader of the hierarchy module (#393)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m6s
The architecture section said the module had three consumers; the walks
do, and the budget reads displaysAsRoot as well.

Resolves #393

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:28:24 -04:00
le king fu
cb0e2d2536 fix: reload the category tree even when the check after a reattachment fails (#393)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m13s
The reattachment is committed before the orphan list is checked again: if
that check failed, the tree kept showing the categories as they were. The
tree now reloads in a finally block, and the failure shows in the window.

Resolves #393

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:24:31 -04:00
le king fu
03e32afc02 docs: describe the hierarchy check and the parent rules (#393)
CHANGELOG (Added: the "Check hierarchy" window; Fixed: the parent checks
at save time and the budget rows of a category whose parent is deleted or
gone), the Categories section of the user guide and its docs.* keys in
both languages, and docs/architecture.md: the categoryHierarchy module, the
diagnostic, the validation and its typed error, the window, the budget
root rule, and what stays out of scope (drag-and-drop, archive restore).

Resolves #393

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:23:14 -04:00
le king fu
21da7a3827 fix: show a category whose parent is deleted or gone in the budget (#393)
The budget grid and the Budget vs Actual report read active categories
only and started from the categories without a parent, so a category whose
parent was deactivated or no longer exists vanished from both, budget
amounts included. It now stands at the top level, as in the tree and the
category pickers (displaysAsRoot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:16:25 -04:00
le king fu
db5ee65720 fix: refuse a looping or too deep parent, and reattach orphan categories (#393)
updateCategory checks a new parent before writing: the category itself,
one of its descendants or a category caught in a cycle (cycle), a fourth
level counting its sub-tree (too_deep), a parent holding transactions of
its own (parent_has_transactions). The refusal is a CategoryHierarchyError
that the Categories page shows by the translation of its code. The reads
and the writes, the new parent's is_inputable included, run in one
transaction through its handle; an unchanged parent is not checked, so a
category stored before these rules can still be renamed.

getCategoryDepth keeps the ids it has seen and ends on a cycle.

A "Check hierarchy" button, between "All keywords" and "Re-initialize",
opens a window that lists the orphan categories (caught in a parent loop,
parent deleted, parent gone), one per group, with a new parent for each:
the root by default, or a parent of the category form's list.
reattachCategory applies the same rules; after it, the list and the tree
reload and the Journal gets one line with the reason and the number of
categories put back, no name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:16:21 -04:00
le king fu
df3fefe934 feat: gather the category hierarchy rules in one module (#393)
src/utils/categoryHierarchy.ts holds the depth, height and descent walks
that buildParentOptions had inline, each with a set of the categories it
has seen, so a parent_id cycle ends the walk. A category caught in a cycle,
or under one, now has no depth instead of a made-up one, and the parent
list of the category form never offers it: a child placed there would be
hidden with it. The module also carries the orphan diagnostic, the parent
change refusal and the root rule that the service, the Categories page and
the budget use next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:16:14 -04:00