import { Store } from "lucide-react"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { CatalogTree } from "@/components/admin/catalog-tree"; import { Prisma } from "@/generated/prisma/client"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { getCatalogItemCounts } from "@/lib/services/catalog-tree"; import { CatalogActions } from "./catalog-actions"; import { CatalogManagerDialog } from "./catalog-manager-dialog-wrapper"; import { CatalogTable } from "./catalog-table"; import { CatalogViewToggle } from "./catalog-view-toggle"; export default async function CatalogPagesPage({ params: paramsPromise, searchParams, }: { params: Promise<{ locale: string }>; searchParams: Promise>; }) { await paramsPromise; const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.CATALOG_VIEW, session.user.rank)) { redirect("/admin"); } const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); const t = await getTranslations("pages.admin.catalog"); const params = await searchParams; const sp = new URLSearchParams(params); const { search, perPage, page, sort, order } = parseListParams(sp); const viewMode = sp.get("view") || "tree"; const catalogType = sp.get("catalog") || "normal"; // ── Builder Club mode ────────────────────────────────────────── type TreeNodeShape = { id: number; caption: string; parentId: number; enabled: string; visible: string; iconImage: number; iconColor: number; pageLayout: string; childCount: number; itemCount: number; }; if (catalogType === "bc") { const [totalBcPages, totalBcItems, enabledBcPages] = await Promise.all([ prisma.catalogPagesBc.count(), prisma.catalogItemsBc.count(), prisma.catalogPagesBc.count({ where: { enabled: "1" } }), ]); // ── BC tree view ────────────────────────────────────────────── let bcRootPages: TreeNodeShape[] = []; if (viewMode === "tree") { const bcRootRaw = await prisma.catalogPagesBc.findMany({ where: { OR: [{ parentId: -1 }, { parentId: 0 }] }, orderBy: { orderNum: "asc" }, }); const bcRootIds = bcRootRaw.map((p) => p.id); const [bcChildCounts, bcItemCounts] = await Promise.all([ bcRootIds.length > 0 ? prisma.catalogPagesBc.groupBy({ by: ["parentId"], where: { parentId: { in: bcRootIds } }, _count: true, }) : [], bcRootIds.length > 0 ? prisma.catalogItemsBc.groupBy({ by: ["pageId"], where: { pageId: { in: bcRootIds } }, _count: true, }) : [], ]); const bcChildMap = Object.fromEntries( bcChildCounts.map((c) => [c.parentId, c._count]), ); const bcItemMap = Object.fromEntries( bcItemCounts.map((c) => [c.pageId, c._count]), ); bcRootPages = bcRootRaw.map((p) => ({ id: p.id, caption: p.caption, parentId: p.parentId, enabled: String(p.enabled), visible: String(p.visible), iconImage: p.iconImage, iconColor: p.iconColor, pageLayout: p.pageLayout, childCount: bcChildMap[p.id] ?? 0, itemCount: bcItemMap[p.id] ?? 0, })); } // ── BC table view ───────────────────────────────────────────── let bcRows: { id: number; caption: string; parentId: number; parentCaption: string; pageLayout: string; enabled: string; visible: string; minRank: number; orderNum: number; itemCount: number; childCount: number; }[] = []; let bcPagination = { total: 0, page: 1, perPage, lastPage: 0 }; if (viewMode === "table") { const parentFilter = sp.get("parent"); const parentId = parentFilter !== null ? parseInt(parentFilter, 10) : undefined; const where: Record = {}; const hasParent = parentId !== undefined && !Number.isNaN(parentId); if (search) { const pattern = { contains: search }; where.OR = [{ caption: pattern }]; } if (hasParent) where.parentId = parentId; // Prisma sort key is snake_case for BC model. Translate known keys. const sortKeyMap: Record = { parentId: "parentId", pageLayout: "pageLayout", orderNum: "orderNum", }; const sortKey = sort ? (sortKeyMap[sort] ?? sort) : "orderNum"; const orderBy = { [sortKey]: sort ? order : "asc" }; const [pages, total] = await Promise.all([ prisma.catalogPagesBc.findMany({ where, orderBy, skip: (page - 1) * perPage, take: perPage, }), prisma.catalogPagesBc.count({ where }), ]); bcPagination = calcPagination(total, page, perPage); const pageIds = pages.map((p) => p.id); const parentIds = [ ...new Set(pages.map((p) => p.parentId).filter((id) => id > 0)), ]; const [childCounts, itemCounts, parentPages] = await Promise.all([ pageIds.length > 0 ? prisma.catalogPagesBc.groupBy({ by: ["parentId"], where: { parentId: { in: pageIds } }, _count: true, }) : [], pageIds.length > 0 ? prisma.catalogItemsBc.groupBy({ by: ["pageId"], where: { pageId: { in: pageIds } }, _count: true, }) : [], parentIds.length > 0 ? prisma.catalogPagesBc.findMany({ where: { id: { in: parentIds } }, select: { id: true, caption: true }, }) : [], ]); const childCountMap = Object.fromEntries( childCounts.map((c) => [c.parentId, c._count]), ); const itemCountMap = Object.fromEntries( itemCounts.map((c) => [c.pageId, c._count]), ); const parentNameMap = Object.fromEntries( parentPages.map((p) => [p.id, p.caption]), ); bcRows = pages.map((p) => ({ id: p.id, caption: p.caption, parentId: p.parentId, parentCaption: p.parentId === -1 ? "Root" : (parentNameMap[p.parentId] ?? `#${p.parentId}`), pageLayout: String(p.pageLayout), enabled: String(p.enabled), visible: String(p.visible), minRank: 0, // BC pages have no min rank column orderNum: p.orderNum, itemCount: itemCountMap[p.id] ?? 0, childCount: childCountMap[p.id] ?? 0, })); } return (

{totalBcPages} BC pages ({enabledBcPages} enabled) ·{" "} {totalBcItems} items

{canEdit && ( ({ id: p.id, caption: p.caption, iconImage: p.iconImage, enabled: p.enabled, orderNum: 0, }))} /> )} {canEdit && ( )}
{viewMode === "tree" ? ( ) : ( )}
); } // ── Normal catalog mode ──────────────────────────────────────── // Global stats (lightweight counts) const [totalPages, totalItems, enabledPages] = await Promise.all([ prisma.catalogPages.count(), prisma.catalogItems.count(), prisma.catalogPages.count({ where: { enabled: "1" } }), ]); // ── Root pages (tree view + Visual Manager seed) ──────────────── const rootPagesRaw = await prisma.catalogPages.findMany({ where: { OR: [{ parentId: -1 }, { parentId: 0 }] }, orderBy: { orderNum: "asc" }, }); const rootIds = rootPagesRaw.map((p) => p.id); const [rootChildCounts, rootItemCountMap] = await Promise.all([ rootIds.length > 0 ? prisma.catalogPages.groupBy({ by: ["parentId"], where: { parentId: { in: rootIds } }, _count: true, }) : [], getCatalogItemCounts(rootIds), ]); const rootChildMap = Object.fromEntries( rootChildCounts.map((c) => [c.parentId, c._count]), ); const rootItemMap = Object.fromEntries(rootItemCountMap); const rootPages: TreeNodeShape[] = rootPagesRaw.map((p) => ({ id: p.id, caption: p.caption, parentId: p.parentId, enabled: String(p.enabled), visible: String(p.visible), iconImage: p.iconImage, iconColor: p.iconColor, pageLayout: p.pageLayout, childCount: rootChildMap[p.id] ?? 0, itemCount: rootItemMap[p.id] ?? 0, })); // ── Table view data (skip if tree) ──────────────────────────── let rows: { id: number; caption: string; parentId: number; parentCaption: string; pageLayout: string; enabled: string; visible: string; minRank: number; orderNum: number; itemCount: number; childCount: number; }[] = []; let pagination = { total: 0, page: 1, perPage, lastPage: 0 }; if (viewMode === "table") { const parentFilter = sp.get("parent"); const parentId = parentFilter !== null ? parseInt(parentFilter, 10) : undefined; const where: Record = {}; const hasParent = parentId !== undefined && !Number.isNaN(parentId); if (search) { const searchPattern = `%${search}%`; const parentClause = hasParent ? Prisma.sql`AND cp.parent_id = ${parentId}` : Prisma.empty; const matchedIds = await prisma.$queryRaw<{ id: number }[]>(Prisma.sql` SELECT DISTINCT cp.id FROM catalog_pages cp LEFT JOIN catalog_items ci ON CAST(ci.page_id AS CHAR) = CAST(cp.id AS CHAR) LEFT JOIN items_base ib ON FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ',')) WHERE (cp.caption LIKE ${searchPattern} OR cp.page_layout LIKE ${searchPattern} OR ci.catalog_name LIKE ${searchPattern} OR ib.public_name LIKE ${searchPattern} OR ib.item_name LIKE ${searchPattern}) ${parentClause} `); where.id = { in: matchedIds.map((r) => r.id) }; } else if (hasParent) { where.parentId = parentId; } const orderBy = sort ? { [sort]: order } : { orderNum: "asc" as const }; const [pages, total] = await Promise.all([ prisma.catalogPages.findMany({ where, orderBy, skip: (page - 1) * perPage, take: perPage, }), prisma.catalogPages.count({ where }), ]); pagination = calcPagination(total, page, perPage); // Count children and items per page, and resolve parent names in parallel const pageIds = pages.map((p) => p.id); const parentIds = [ ...new Set(pages.map((p) => p.parentId).filter((id) => id > 0)), ]; const [childCounts, itemCountMapRaw, parentPages] = await Promise.all([ pageIds.length > 0 ? prisma.catalogPages.groupBy({ by: ["parentId"], where: { parentId: { in: pageIds } }, _count: true, }) : [], getCatalogItemCounts(pageIds), parentIds.length > 0 ? prisma.catalogPages.findMany({ where: { id: { in: parentIds } }, select: { id: true, caption: true }, }) : [], ]); const childCountMap = Object.fromEntries( childCounts.map((c) => [c.parentId, c._count]), ); const itemCountMap = Object.fromEntries(itemCountMapRaw); const parentNameMap = Object.fromEntries( parentPages.map((p) => [p.id, p.caption]), ); rows = pages.map((p) => ({ id: p.id, caption: p.caption, parentId: p.parentId, parentCaption: p.parentId === -1 ? "Root" : (parentNameMap[p.parentId] ?? `#${p.parentId}`), pageLayout: p.pageLayout, enabled: p.enabled, visible: p.visible, minRank: p.minRank, orderNum: p.orderNum, itemCount: itemCountMap[p.id] ?? 0, childCount: childCountMap[p.id] ?? 0, })); } return (

{totalPages} pages ({enabledPages} enabled) · {totalItems}{" "} items

{canEdit && ( ({ id: p.id, caption: p.caption, iconImage: p.iconImage, enabled: p.enabled, orderNum: 0, }))} /> )} {canEdit && ( )}
{viewMode === "tree" ? ( ) : ( )}
); }