import dynamic from 'next/dynamic' import { redirect } from 'next/navigation' 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 { CatalogActions } from './catalog-actions' import { CatalogTable } from './catalog-table' import { CatalogViewToggle } from './catalog-view-toggle' const CatalogManagerDialog = dynamic(() => import('@/components/admin/catalog-manager/catalog-manager-dialog').then((m) => ({ default: m.CatalogManagerDialog, })), ) 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 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.catalog_pages_bc.count(), prisma.catalog_items_bc.count(), prisma.catalog_pages_bc.count({ where: { enabled: '1' } }), ]) // ── BC tree view ────────────────────────────────────────────── let bcRootPages: TreeNodeShape[] = [] if (viewMode === 'tree') { const bcRootRaw = await prisma.catalog_pages_bc.findMany({ where: { OR: [{ parent_id: -1 }, { parent_id: 0 }] }, orderBy: { order_num: 'asc' }, }) const bcRootIds = bcRootRaw.map((p) => p.id) const [bcChildCounts, bcItemCounts] = await Promise.all([ bcRootIds.length > 0 ? prisma.catalog_pages_bc.groupBy({ by: ['parent_id'], where: { parent_id: { in: bcRootIds } }, _count: true, }) : [], bcRootIds.length > 0 ? prisma.catalog_items_bc.groupBy({ by: ['page_id'], where: { page_id: { in: bcRootIds } }, _count: true, }) : [], ]) const bcChildMap = Object.fromEntries(bcChildCounts.map((c) => [c.parent_id, c._count])) const bcItemMap = Object.fromEntries(bcItemCounts.map((c) => [c.page_id, c._count])) bcRootPages = bcRootRaw.map((p) => ({ id: p.id, caption: p.caption, parentId: p.parent_id, enabled: String(p.enabled), visible: String(p.visible), iconImage: p.icon_image, iconColor: p.icon_color, pageLayout: p.page_layout, 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.parent_id = parentId // Prisma sort key is snake_case for BC model. Translate known keys. const sortKeyMap: Record = { parentId: 'parent_id', pageLayout: 'page_layout', orderNum: 'order_num', } const sortKey = sort ? (sortKeyMap[sort] ?? sort) : 'order_num' const orderBy = { [sortKey]: sort ? order : 'asc' } const [pages, total] = await Promise.all([ prisma.catalog_pages_bc.findMany({ where, orderBy, skip: (page - 1) * perPage, take: perPage, }), prisma.catalog_pages_bc.count({ where }), ]) bcPagination = calcPagination(total, page, perPage) const pageIds = pages.map((p) => p.id) const parentIds = [...new Set(pages.map((p) => p.parent_id).filter((id) => id > 0))] const [childCounts, itemCounts, parentPages] = await Promise.all([ pageIds.length > 0 ? prisma.catalog_pages_bc.groupBy({ by: ['parent_id'], where: { parent_id: { in: pageIds } }, _count: true, }) : [], pageIds.length > 0 ? prisma.catalog_items_bc.groupBy({ by: ['page_id'], where: { page_id: { in: pageIds } }, _count: true, }) : [], parentIds.length > 0 ? prisma.catalog_pages_bc.findMany({ where: { id: { in: parentIds } }, select: { id: true, caption: true }, }) : [], ]) const childCountMap = Object.fromEntries(childCounts.map((c) => [c.parent_id, c._count])) const itemCountMap = Object.fromEntries(itemCounts.map((c) => [c.page_id, 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.parent_id, parentCaption: p.parent_id === -1 ? 'Root' : (parentNameMap[p.parent_id] ?? `#${p.parent_id}`), pageLayout: String(p.page_layout), enabled: String(p.enabled), visible: String(p.visible), minRank: 0, // BC pages have no min rank column orderNum: p.order_num, itemCount: itemCountMap[p.id] ?? 0, childCount: childCountMap[p.id] ?? 0, })) } return (

Catalog Pages

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

{canEdit && }
{viewMode === 'tree' ? ( ) : ( )}
) } // ── Normal catalog mode ──────────────────────────────────────── // Global stats (lightweight counts) const [totalPages, totalItems, enabledPages] = await Promise.all([ prisma.catalogPage.count(), prisma.catalogItem.count(), prisma.catalogPage.count({ where: { enabled: '1' } }), ]) // ── Tree view data ────────────────────────────────────────────── let rootPages: TreeNodeShape[] = [] if (viewMode === 'tree') { const rootPagesRaw = await prisma.catalogPage.findMany({ where: { parentId: -1 }, orderBy: { orderNum: 'asc' }, }) const rootIds = rootPagesRaw.map((p) => p.id) const [rootChildCounts, rootItemCounts] = await Promise.all([ rootIds.length > 0 ? prisma.catalogPage.groupBy({ by: ['parentId'], where: { parentId: { in: rootIds } }, _count: true, }) : [], rootIds.length > 0 ? prisma.catalogItem.groupBy({ by: ['pageId'], where: { pageId: { in: rootIds } }, _count: true, }) : [], ]) const rootChildMap = Object.fromEntries(rootChildCounts.map((c) => [c.parentId, c._count])) const rootItemMap = Object.fromEntries(rootItemCounts.map((c) => [c.pageId, c._count])) rootPages = 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 ci.page_id = cp.id 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.catalogPage.findMany({ where, orderBy, skip: (page - 1) * perPage, take: perPage, }), prisma.catalogPage.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, itemCounts, parentPages] = await Promise.all([ pageIds.length > 0 ? prisma.catalogPage.groupBy({ by: ['parentId'], where: { parentId: { in: pageIds } }, _count: true, }) : [], pageIds.length > 0 ? prisma.catalogItem.groupBy({ by: ['pageId'], where: { pageId: { in: pageIds } }, _count: true, }) : [], parentIds.length > 0 ? prisma.catalogPage.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])) 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 (

Catalog Pages

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

{canEdit && } {canEdit && }
{viewMode === 'tree' ? ( ) : ( )}
) }