admin/helpers/categoryPath.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

62 lines
2.1 KiB
TypeScript

import type { EventCategory } from '@/services/events'
export interface CategoryTreeOption {
id: number
name: string
depth: number
}
// Defends against a malformed/cyclic parentId chain reaching the client
// (shouldn't happen -- event-categories.md's re-parent guard blocks this
// server-side -- but a client-side tree builder walking `parentId`
// links needs its own bound regardless, since it has no way to verify
// the guard actually ran on this data).
const MAX_DEPTH = 32
/**
* Depth-first flatten of a flat, parentId-linked category list into
* actual tree order (a parent immediately followed by its children,
* siblings ordered by `sortOrder` then name). Used to render an
* indented tree picker (formElements/Input select + option.depth) instead of a plain flat
* <select>.
*
* Also fixes a real ambiguity along the way: event_categories only
* enforces a unique (parent_id, name) pair, not a unique name overall,
* so two categories under different parents can share an identical
* name. A flat name-only dropdown couldn't distinguish them; rendering
* them at their real tree position/indentation does.
*/
export function buildCategoryTree(categories: EventCategory[]): CategoryTreeOption[] {
const childrenByParent = new Map<number | null, EventCategory[]>()
for (const category of categories) {
const key = category.parentId
const siblings = childrenByParent.get(key) ?? []
siblings.push(category)
childrenByParent.set(key, siblings)
}
for (const siblings of Array.from(childrenByParent.values())) {
siblings.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name, 'fa'))
}
const result: CategoryTreeOption[] = []
const seen = new Set<number>()
const visit = (parentId: number | null, depth: number) => {
if (depth >= MAX_DEPTH) return
for (const category of childrenByParent.get(parentId) ?? []) {
if (seen.has(category.id)) continue
seen.add(category.id)
result.push({ id: category.id, name: category.name, depth })
visit(category.id, depth + 1)
}
}
visit(null, 0)
return result
}