import type { CategoryTreeNode } from "../../shared/types"; import { depthsOf, fitsUnder, subtreeHeight } from "../../utils/categoryHierarchy"; export interface ParentOptionsContext { /** Id of the category being edited; absent or null when creating. */ editingId?: number | null; /** * Parent currently stored on the form. Always kept in the options when it * exists, so a legacy or inconsistent parent never leaves the field blank * while `parent_id` is set. */ currentParentId?: number | null; } /** * Categories that may be chosen as the parent in the category form, and in the * « Vérifier la hiérarchie » window (#393). * * - Roots and level-1 categories only: a level-2 parent would create a fourth * level, which nothing else in the app knows how to display. * - When editing, the edited category is left out, and the allowed parent depth * shrinks by the height of its sub-tree: with children, only roots remain * (the children would otherwise land at level 3); with grandchildren, no * parent at all (they would land at level 4). This alone also leaves out its * whole descent, which would create a cycle that hides the branch everywhere * (#393): a descendant sits at least one level below the edited category, * deeper than any depth still allowed. * - A category caught in a `parent_id` cycle, or under one, has no depth and is * never offered: a child placed under it would be hidden with it. * - The parent currently set on the form is always kept when it exists. * * The depth and height rules are those of `src/utils/categoryHierarchy.ts`, * which `updateCategory` and `reattachCategory` apply again before writing * (#393): the list only spares a refusal at save time. It does not leave out * a parent that holds transactions; the service refuses it with a translated * message. * * Ordering is left to the caller (`CategoryCombobox` sorts hierarchically). */ export function buildParentOptions( categories: CategoryTreeNode[], ctx: ParentOptionsContext = {}, ): CategoryTreeNode[] { const editingId = ctx.editingId ?? null; const currentParentId = ctx.currentParentId ?? null; const depths = depthsOf(categories); const height = editingId !== null ? subtreeHeight(categories, editingId) : 0; return categories.filter((c) => { if (c.id === editingId) return false; // Kept even when the depth rule would drop it: the field must show what is // stored. Keeping it changes nothing on save (it is already the parent). if (c.id === currentParentId) return true; const depth = depths.get(c.id); return depth !== undefined && fitsUnder(depth, height); }); }