import { prisma } from "@/lib/prisma"; export type { NestedTreeNode, TreeNode } from "@/types/catalog"; import type { TreeNode } from "@/types/catalog"; /** * Get the full catalog tree recursively using iterative Prisma queries. * Returns a flat list with depth info for client-side tree building. * Depth is computed from parentId hierarchy (not stored in DB). */ export async function getTreeFlat(): Promise { const [allPages, itemCounts] = await Promise.all([ prisma.catalogPages.findMany({ orderBy: { orderNum: "asc" }, }), prisma.catalogItems.groupBy({ by: ["pageId"], _count: true, }), ]); const itemCountMap = new Map(itemCounts.map((c) => [c.pageId, c._count])); // Count children per page const childCountMap = new Map(); for (const p of allPages) { childCountMap.set(p.parentId, (childCountMap.get(p.parentId) ?? 0) + 1); } // Compute depth from parent hierarchy const pageMap = new Map(allPages.map((p) => [p.id, p])); const depthCache = new Map(); function computeDepth(pageId: number): number { if (depthCache.has(pageId)) return depthCache.get(pageId)!; const page = pageMap.get(pageId); if (!page || page.parentId <= 0) { depthCache.set(pageId, 0); return 0; } const d = computeDepth(page.parentId) + 1; depthCache.set(pageId, d); return d; } return allPages.map((p) => ({ id: p.id, caption: p.caption, parentId: p.parentId, depth: computeDepth(p.id), orderNum: p.orderNum, enabled: String(p.enabled), visible: String(p.visible), iconImage: p.iconImage, iconColor: p.iconColor, pageLayout: p.pageLayout, childCount: childCountMap.get(p.id) ?? 0, itemCount: itemCountMap.get(p.id) ?? 0, })); } /** * Build a nested tree structure from flat nodes. */ import type { NestedTreeNode } from "@/types/catalog"; export function buildNestedTree(flat: TreeNode[], rootParentId = -1): NestedTreeNode[] { const childrenMap = new Map(); const nodeMap = new Map(); for (const node of flat) { const nested: NestedTreeNode = { ...node, children: [] }; nodeMap.set(node.id, nested); if (!childrenMap.has(node.parentId)) childrenMap.set(node.parentId, []); childrenMap.get(node.parentId)!.push(nested); } // Attach children for (const node of nodeMap.values()) { node.children = childrenMap.get(node.id) ?? []; } return childrenMap.get(rootParentId) ?? []; } /** * Create a new catalog page. */ export async function createPage(data: { parentId: number; caption: string; captionSave?: string; pageLayout?: string; iconImage?: number; iconColor?: number; minRank?: number; orderNum?: number; visible?: boolean; enabled?: boolean; }): Promise<{ id: number }> { // Auto-generate captionSave if not provided const captionSave = data.captionSave || data.caption .toLowerCase() .replace(/[^a-z0-9_]/g, "_") .substring(0, 25); // Get next orderNum if not provided let orderNum = data.orderNum; if (orderNum === undefined) { const lastSibling = await prisma.catalogPages.findFirst({ where: { parentId: data.parentId }, orderBy: { orderNum: "desc" }, select: { orderNum: true }, }); orderNum = (lastSibling?.orderNum ?? 0) + 1; } const created = await prisma.catalogPages.create({ data: { parentId: data.parentId, caption: data.caption, captionSave, pageLayout: data.pageLayout ?? "default_3x3", iconImage: data.iconImage ?? 1, iconColor: data.iconColor ?? 1, minRank: data.minRank ?? 1, orderNum, visible: (data.visible ?? true) ? "1" : "0", enabled: (data.enabled ?? true) ? "1" : "0", pageHeadline: "", pageTeaser: "", pageSpecial: "", pageText1: "", pageText2: "", pageTextDetails: "", pageTextTeaser: "", includes: "", }, select: { id: true }, }); return created; } /** * Move a page to a new parent. * Validates against circular hierarchy (A → B → C → A). */ export async function movePage(pageId: number, newParentId: number): Promise { // Walk up from newParentId to root — if we hit pageId, it's circular if (newParentId > 0) { let currentId = newParentId; for (let i = 0; i < 50; i++) { if (currentId === pageId) { throw new Error("Cannot move page: would create a circular hierarchy"); } const parent = await prisma.catalogPages.findUnique({ where: { id: currentId }, select: { parentId: true }, }); if (!parent || parent.parentId <= 0) break; currentId = parent.parentId; } } await prisma.catalogPages.update({ where: { id: pageId }, data: { parentId: newParentId }, }); } /** * Delete a page with cascade or reparent mode. * - cascade: deletes all children and their items * - reparent: moves children to the deleted page's parent */ export async function deletePage( pageId: number, mode: "cascade" | "reparent" = "reparent", ): Promise<{ deletedPages: number; movedChildren: number }> { const page = await prisma.catalogPages.findUnique({ where: { id: pageId }, select: { parentId: true }, }); if (!page) return { deletedPages: 0, movedChildren: 0 }; if (mode === "reparent") { // Move children to page's parent const result = await prisma.catalogPages.updateMany({ where: { parentId: pageId }, data: { parentId: page.parentId }, }); // Delete items in this page await prisma.catalogItems.deleteMany({ where: { pageId } }); // Delete the page await prisma.catalogPages.delete({ where: { id: pageId } }); return { deletedPages: 1, movedChildren: result.count }; } // Cascade: delete all descendants const deleted = await cascadeDelete(pageId); return { deletedPages: deleted, movedChildren: 0 }; } async function cascadeDelete(pageId: number): Promise { // Collect all descendant IDs iteratively to avoid N+1 recursive queries const toDelete: number[] = [pageId]; const queue: number[] = [pageId]; while (queue.length > 0) { const children = await prisma.catalogPages.findMany({ where: { parentId: { in: queue } }, select: { id: true }, }); queue.length = 0; for (const child of children) { toDelete.push(child.id); queue.push(child.id); } } // Delete all items and pages in bulk (children first, then parents) await prisma.catalogItems.deleteMany({ where: { pageId: { in: toDelete } } }); // Delete in reverse order (deepest first) to avoid FK issues for (let i = toDelete.length - 1; i >= 0; i--) { await prisma.catalogPages.delete({ where: { id: toDelete[i] } }); } return toDelete.length; } /** * Get ancestors (breadcrumb) from root to the given page. */ export async function getAncestors(pageId: number): Promise> { const ancestors: Array<{ id: number; caption: string }> = []; let currentId = pageId; // Safety limit to prevent infinite loops for (let i = 0; i < 20; i++) { const page = await prisma.catalogPages.findUnique({ where: { id: currentId }, select: { id: true, caption: true, parentId: true }, }); if (!page) break; ancestors.unshift({ id: page.id, caption: page.caption }); if (page.parentId <= 0) break; currentId = page.parentId; } return ancestors; }