Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
62 lines
2.1 KiB
TypeScript
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
|
|
}
|