import { and, asc, count, desc, eq, inArray, like, or, type SQL, sql, } from "drizzle-orm"; import type { AnyMySqlColumn } from "drizzle-orm/mysql-core"; 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 Link from "@/components/link"; import { CatalogSearch } from "@/features/catalog/components/catalog-search"; import { CatalogSyncStatus } from "@/features/catalog/components/catalog-sync-status"; import { CatalogPackagesDialog } from "@/features/catalog/components/packages/catalog-packages-dialog"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { CatalogItems, CatalogItemsBc, CatalogPages, CatalogPagesBc, db, } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; 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"; async function rawRows(query: ReturnType): Promise { const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; return rows ?? []; } 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 requestedView = sp.get("view"); const viewMode = requestedView === "table" ? "table" : "tree"; const catalogType = sp.get("catalog") === "bc" ? "bc" : "normal"; 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 [totalBcPagesRows, totalBcItemsRows, enabledBcPagesRows] = await Promise.all([ db.select({ total: count() }).from(CatalogPagesBc), db.select({ total: count() }).from(CatalogItemsBc), db .select({ total: count() }) .from(CatalogPagesBc) .where(eq(CatalogPagesBc.enabled, "1")), ]); const totalBcPages = Number(totalBcPagesRows[0]?.total ?? 0); const totalBcItems = Number(totalBcItemsRows[0]?.total ?? 0); const enabledBcPages = Number(enabledBcPagesRows[0]?.total ?? 0); let bcRootPages: TreeNodeShape[] = []; if (viewMode === "tree") { const bcRootRaw = await db .select() .from(CatalogPagesBc) .where( or(eq(CatalogPagesBc.parentId, -1), eq(CatalogPagesBc.parentId, 0)), ) .orderBy(asc(CatalogPagesBc.orderNum), asc(CatalogPagesBc.id)); const bcRootIds = bcRootRaw.map((p) => p.id); const [bcChildCounts, bcItemCounts] = await Promise.all([ bcRootIds.length > 0 ? db .select({ parentId: CatalogPagesBc.parentId, total: count(), }) .from(CatalogPagesBc) .where(inArray(CatalogPagesBc.parentId, bcRootIds)) .groupBy(CatalogPagesBc.parentId) : [], bcRootIds.length > 0 ? db .select({ pageId: CatalogItemsBc.pageId, total: count(), }) .from(CatalogItemsBc) .where(inArray(CatalogItemsBc.pageId, bcRootIds)) .groupBy(CatalogItemsBc.pageId) : [], ]); const bcChildMap = Object.fromEntries( bcChildCounts.map((c) => [c.parentId, Number(c.total)]), ); const bcItemMap = Object.fromEntries( bcItemCounts.map((c) => [c.pageId, Number(c.total)]), ); 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, })); } 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 conditions: SQL[] = []; const hasParent = parentId !== undefined && !Number.isNaN(parentId); if (search) { conditions.push(like(CatalogPagesBc.caption, `%${search}%`)); } if (hasParent) conditions.push(eq(CatalogPagesBc.parentId, parentId)); const where = conditions.length > 0 ? and(...conditions) : undefined; const sortKeyMap: Record = { parentId: CatalogPagesBc.parentId, pageLayout: CatalogPagesBc.pageLayout, orderNum: CatalogPagesBc.orderNum, caption: CatalogPagesBc.caption, id: CatalogPagesBc.id, }; const sortCol = sort ? (sortKeyMap[sort] ?? CatalogPagesBc.orderNum) : CatalogPagesBc.orderNum; const orderBy = (sort ? order : "asc") === "desc" ? desc(sortCol) : asc(sortCol); const [pages, totalRows] = await Promise.all([ db .select() .from(CatalogPagesBc) .where(where) .orderBy(orderBy) .limit(perPage) .offset((page - 1) * perPage), db.select({ total: count() }).from(CatalogPagesBc).where(where), ]); const total = Number(totalRows[0]?.total ?? 0); 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 ? db .select({ parentId: CatalogPagesBc.parentId, total: count(), }) .from(CatalogPagesBc) .where(inArray(CatalogPagesBc.parentId, pageIds)) .groupBy(CatalogPagesBc.parentId) : [], pageIds.length > 0 ? db .select({ pageId: CatalogItemsBc.pageId, total: count(), }) .from(CatalogItemsBc) .where(inArray(CatalogItemsBc.pageId, pageIds)) .groupBy(CatalogItemsBc.pageId) : [], parentIds.length > 0 ? db .select({ id: CatalogPagesBc.id, caption: CatalogPagesBc.caption, }) .from(CatalogPagesBc) .where(inArray(CatalogPagesBc.id, parentIds)) : [], ]); const childCountMap = Object.fromEntries( childCounts.map((c) => [c.parentId, Number(c.total)]), ); const itemCountMap = Object.fromEntries( itemCounts.map((c) => [c.pageId, Number(c.total)]), ); 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, 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 && ( )} {t("workspace.diagnostics")}
{viewMode === "tree" ? ( ) : ( )}
); } const [totalPagesRows, totalItemsRows, enabledPagesRows] = await Promise.all([ db.select({ total: count() }).from(CatalogPages), db.select({ total: count() }).from(CatalogItems), db .select({ total: count() }) .from(CatalogPages) .where(eq(CatalogPages.enabled, "1")), ]); const totalPages = Number(totalPagesRows[0]?.total ?? 0); const totalItems = Number(totalItemsRows[0]?.total ?? 0); const enabledPages = Number(enabledPagesRows[0]?.total ?? 0); const rootPagesRaw = await db .select() .from(CatalogPages) .where(or(eq(CatalogPages.parentId, -1), eq(CatalogPages.parentId, 0))) .orderBy(asc(CatalogPages.orderNum), asc(CatalogPages.id)); const rootIds = rootPagesRaw.map((p) => p.id); const [rootChildCounts, rootItemCountMap] = await Promise.all([ rootIds.length > 0 ? db .select({ parentId: CatalogPages.parentId, total: count(), }) .from(CatalogPages) .where(inArray(CatalogPages.parentId, rootIds)) .groupBy(CatalogPages.parentId) : [], getCatalogItemCounts(rootIds), ]); const rootChildMap = Object.fromEntries( rootChildCounts.map((c) => [c.parentId, Number(c.total)]), ); 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, })); 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 conditions: SQL[] = []; const hasParent = parentId !== undefined && !Number.isNaN(parentId); if (search) { const searchPattern = `%${search}%`; const parentClause = hasParent ? sql`AND cp.parent_id = ${parentId}` : sql``; const matchedIds = await rawRows<{ id: number }>(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} `); const ids = matchedIds.map((r) => r.id); if (ids.length > 0) conditions.push(inArray(CatalogPages.id, ids)); else conditions.push(sql`1 = 0`); } else if (hasParent) { conditions.push(eq(CatalogPages.parentId, parentId)); } const where = conditions.length > 0 ? and(...conditions) : undefined; const sortColMap: Record = { caption: CatalogPages.caption, parentId: CatalogPages.parentId, pageLayout: CatalogPages.pageLayout, enabled: CatalogPages.enabled, orderNum: CatalogPages.orderNum, id: CatalogPages.id, minRank: CatalogPages.minRank, }; const sortCol = sort ? (sortColMap[sort] ?? CatalogPages.orderNum) : CatalogPages.orderNum; const orderBy = (sort ? order : "asc") === "desc" ? desc(sortCol) : asc(sortCol); const [pages, totalRows] = await Promise.all([ db .select() .from(CatalogPages) .where(where) .orderBy(orderBy) .limit(perPage) .offset((page - 1) * perPage), db.select({ total: count() }).from(CatalogPages).where(where), ]); const total = Number(totalRows[0]?.total ?? 0); pagination = 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, itemCountMapRaw, parentPages] = await Promise.all([ pageIds.length > 0 ? db .select({ parentId: CatalogPages.parentId, total: count(), }) .from(CatalogPages) .where(inArray(CatalogPages.parentId, pageIds)) .groupBy(CatalogPages.parentId) : [], getCatalogItemCounts(pageIds), parentIds.length > 0 ? db .select({ id: CatalogPages.id, caption: CatalogPages.caption }) .from(CatalogPages) .where(inArray(CatalogPages.id, parentIds)) : [], ]); const childCountMap = Object.fromEntries( childCounts.map((c) => [c.parentId, Number(c.total)]), ); 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 && ( )} {t("workspace.diagnostics")}
{viewMode === "tree" ? ( ) : ( )}
); }