Restore broken Quick Add search, correct Translate saves to items_base and FurnitureData, reparent page deletes, sync RCON on create/toggle/BC mutations, and replace native confirms/prompts with dialogs. Co-authored-by: Cursor <[email protected]>
339 lines
9.2 KiB
TypeScript
339 lines
9.2 KiB
TypeScript
import { withAdmin } from "@/lib/api-handler";
|
|
import { apiError, apiOk } from "@/lib/api-response";
|
|
import { PERMS } from "@/lib/permissions";
|
|
import { prisma } from "@/lib/prisma";
|
|
import {
|
|
deletePage,
|
|
getAncestors,
|
|
getTreeFlat,
|
|
} from "@/lib/services/catalog-tree";
|
|
import { rcon } from "@/lib/services/rcon";
|
|
import type { TreeNode } from "@/types/catalog";
|
|
|
|
type PageRow = {
|
|
id: number;
|
|
caption: string;
|
|
parentId: number;
|
|
orderNum: number;
|
|
enabled: string;
|
|
visible: string;
|
|
iconImage: number;
|
|
iconColor: number;
|
|
pageLayout: string;
|
|
};
|
|
|
|
async function getBcTreeFlat(): Promise<TreeNode[]> {
|
|
const [allPages, itemCounts] = await Promise.all([
|
|
prisma.catalogPagesBc.findMany({ orderBy: { orderNum: "asc" } }),
|
|
prisma.catalogItemsBc.groupBy({ by: ["pageId"], _count: true }),
|
|
]);
|
|
const itemCountMap = new Map(itemCounts.map((c) => [c.pageId, c._count]));
|
|
const childCountMap = new Map<number, number>();
|
|
for (const p of allPages) {
|
|
childCountMap.set(p.parentId, (childCountMap.get(p.parentId) ?? 0) + 1);
|
|
}
|
|
const pageMap = new Map(allPages.map((p) => [p.id, p]));
|
|
const depthCache = new Map<number, number>();
|
|
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,
|
|
}));
|
|
}
|
|
|
|
function toTreeNode(
|
|
p: PageRow,
|
|
childCount: number,
|
|
itemCount: number,
|
|
depth = 0,
|
|
): TreeNode {
|
|
return {
|
|
id: p.id,
|
|
caption: p.caption,
|
|
parentId: p.parentId,
|
|
depth,
|
|
orderNum: p.orderNum,
|
|
enabled: String(p.enabled),
|
|
visible: String(p.visible),
|
|
iconImage: p.iconImage,
|
|
iconColor: p.iconColor,
|
|
pageLayout: p.pageLayout,
|
|
childCount,
|
|
itemCount,
|
|
};
|
|
}
|
|
|
|
async function getChildren(
|
|
parentId: number,
|
|
isBc: boolean,
|
|
): Promise<TreeNode[]> {
|
|
const parentFilter =
|
|
parentId === -1
|
|
? { OR: [{ parentId: -1 }, { parentId: 0 }] }
|
|
: { parentId };
|
|
|
|
if (isBc) {
|
|
const pages = await prisma.catalogPagesBc.findMany({
|
|
where: parentFilter,
|
|
orderBy: { orderNum: "asc" },
|
|
});
|
|
const ids = pages.map((p) => p.id);
|
|
const [childCounts, itemCounts] = await Promise.all([
|
|
ids.length
|
|
? prisma.catalogPagesBc.groupBy({
|
|
by: ["parentId"],
|
|
where: { parentId: { in: ids } },
|
|
_count: true,
|
|
})
|
|
: [],
|
|
ids.length
|
|
? prisma.catalogItemsBc.groupBy({
|
|
by: ["pageId"],
|
|
where: { pageId: { in: ids } },
|
|
_count: true,
|
|
})
|
|
: [],
|
|
]);
|
|
const childMap = Object.fromEntries(
|
|
childCounts.map((c) => [c.parentId, c._count]),
|
|
);
|
|
const itemMap = Object.fromEntries(
|
|
itemCounts.map((c) => [c.pageId, c._count]),
|
|
);
|
|
return pages.map((p) =>
|
|
toTreeNode(p, childMap[p.id] ?? 0, itemMap[p.id] ?? 0),
|
|
);
|
|
}
|
|
|
|
const pages = await prisma.catalogPages.findMany({
|
|
where: parentFilter,
|
|
orderBy: { orderNum: "asc" },
|
|
});
|
|
const ids = pages.map((p) => p.id);
|
|
const [childCounts, itemCounts] = await Promise.all([
|
|
ids.length
|
|
? prisma.catalogPages.groupBy({
|
|
by: ["parentId"],
|
|
where: { parentId: { in: ids } },
|
|
_count: true,
|
|
})
|
|
: [],
|
|
ids.length
|
|
? prisma.catalogItems.groupBy({
|
|
by: ["pageId"],
|
|
where: { pageId: { in: ids } },
|
|
_count: true,
|
|
})
|
|
: [],
|
|
]);
|
|
const childMap = Object.fromEntries(
|
|
childCounts.map((c) => [c.parentId, c._count]),
|
|
);
|
|
const itemMap = Object.fromEntries(
|
|
itemCounts.map((c) => [c.pageId, c._count]),
|
|
);
|
|
return pages.map((p) =>
|
|
toTreeNode(p, childMap[p.id] ?? 0, itemMap[p.id] ?? 0),
|
|
);
|
|
}
|
|
|
|
async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
|
|
const flat = isBc ? await getBcTreeFlat() : await getTreeFlat();
|
|
const needle = q.toLowerCase();
|
|
return flat.filter(
|
|
(p) =>
|
|
p.caption.toLowerCase().includes(needle) ||
|
|
String(p.id) === needle,
|
|
);
|
|
}
|
|
|
|
export const GET = withAdmin(
|
|
{ permission: PERMS.CATALOG_VIEW },
|
|
async (request) => {
|
|
const sp = request.nextUrl.searchParams;
|
|
const isBc = sp.get("catalog") === "bc";
|
|
|
|
if (sp.get("mode") === "full") {
|
|
const pages = isBc ? await getBcTreeFlat() : await getTreeFlat();
|
|
return apiOk({ pages });
|
|
}
|
|
|
|
const pageIdRaw = sp.get("pageId");
|
|
if (pageIdRaw) {
|
|
const id = Number(pageIdRaw);
|
|
if (!Number.isFinite(id) || id <= 0) return apiError("Invalid pageId");
|
|
if (isBc) {
|
|
const page = await prisma.catalogPagesBc.findUnique({ where: { id } });
|
|
if (!page) return apiError("Page not found", 404);
|
|
return apiOk({ page });
|
|
}
|
|
const page = await prisma.catalogPages.findUnique({ where: { id } });
|
|
if (!page) return apiError("Page not found", 404);
|
|
return apiOk({ page });
|
|
}
|
|
|
|
const ancestorsRaw = sp.get("ancestors");
|
|
if (ancestorsRaw) {
|
|
const id = Number(ancestorsRaw);
|
|
if (!Number.isFinite(id) || id <= 0) return apiError("Invalid id");
|
|
if (isBc) {
|
|
const chain: Array<{ id: number; caption: string }> = [];
|
|
let currentId = id;
|
|
for (let i = 0; i < 20; i++) {
|
|
const page = await prisma.catalogPagesBc.findUnique({
|
|
where: { id: currentId },
|
|
select: { id: true, caption: true, parentId: true },
|
|
});
|
|
if (!page) break;
|
|
chain.unshift({ id: page.id, caption: page.caption });
|
|
if (page.parentId <= 0) break;
|
|
currentId = page.parentId;
|
|
}
|
|
return apiOk({
|
|
ancestors: chain.filter((a) => a.id !== id),
|
|
});
|
|
}
|
|
const chain = await getAncestors(id);
|
|
return apiOk({ ancestors: chain.filter((a) => a.id !== id) });
|
|
}
|
|
|
|
const search = sp.get("search");
|
|
if (search?.trim()) {
|
|
const pages = await searchPages(search.trim(), isBc);
|
|
return apiOk({ pages });
|
|
}
|
|
|
|
const parentIdRaw = sp.get("parentId");
|
|
if (parentIdRaw !== null) {
|
|
const parentId = Number(parentIdRaw);
|
|
if (!Number.isFinite(parentId)) return apiError("Invalid parentId");
|
|
const pages = await getChildren(parentId, isBc);
|
|
return apiOk({ pages });
|
|
}
|
|
|
|
return apiOk({ pages: [] });
|
|
},
|
|
);
|
|
|
|
export const PATCH = withAdmin(
|
|
{ permission: PERMS.CATALOG_EDIT },
|
|
async (request) => {
|
|
const body = (await request.json()) as {
|
|
pageId?: number;
|
|
toggleField?: "toggleEnabled" | "toggleVisible";
|
|
catalog?: string;
|
|
};
|
|
const pageId = Number(body.pageId);
|
|
const field =
|
|
body.toggleField === "toggleVisible" ? "visible" : "enabled";
|
|
if (!Number.isFinite(pageId) || pageId <= 0) {
|
|
return apiError("Invalid pageId");
|
|
}
|
|
const isBc = body.catalog === "bc";
|
|
if (isBc) {
|
|
const page = await prisma.catalogPagesBc.findUnique({
|
|
where: { id: pageId },
|
|
select: { enabled: true, visible: true },
|
|
});
|
|
if (!page) return apiError("Page not found", 404);
|
|
const current = field === "enabled" ? page.enabled : page.visible;
|
|
await prisma.catalogPagesBc.update({
|
|
where: { id: pageId },
|
|
data: { [field]: current === "1" ? "0" : "1" },
|
|
});
|
|
await rcon.updateCatalog();
|
|
return apiOk({});
|
|
}
|
|
const page = await prisma.catalogPages.findUnique({
|
|
where: { id: pageId },
|
|
select: { enabled: true, visible: true },
|
|
});
|
|
if (!page) return apiError("Page not found", 404);
|
|
const current = field === "enabled" ? page.enabled : page.visible;
|
|
await prisma.catalogPages.update({
|
|
where: { id: pageId },
|
|
data: { [field]: current === "1" ? "0" : "1" },
|
|
});
|
|
await rcon.updateCatalog();
|
|
return apiOk({});
|
|
},
|
|
);
|
|
|
|
export const DELETE = withAdmin(
|
|
{ permission: PERMS.CATALOG_EDIT },
|
|
async (request) => {
|
|
const sp = request.nextUrl.searchParams;
|
|
const pageId = Number(sp.get("pageId"));
|
|
const mode = sp.get("mode") === "cascade" ? "cascade" : "reparent";
|
|
const isBc = sp.get("catalog") === "bc";
|
|
if (!Number.isFinite(pageId) || pageId <= 0) {
|
|
return apiError("Invalid pageId");
|
|
}
|
|
if (isBc) {
|
|
const page = await prisma.catalogPagesBc.findUnique({
|
|
where: { id: pageId },
|
|
select: { parentId: true },
|
|
});
|
|
if (!page) return apiError("Page not found", 404);
|
|
if (mode === "reparent") {
|
|
await prisma.$transaction([
|
|
prisma.catalogPagesBc.updateMany({
|
|
where: { parentId: pageId },
|
|
data: { parentId: page.parentId },
|
|
}),
|
|
prisma.catalogItemsBc.deleteMany({ where: { pageId } }),
|
|
prisma.catalogPagesBc.delete({ where: { id: pageId } }),
|
|
]);
|
|
} else {
|
|
// Deep cascade: collect all descendants
|
|
const toDelete: number[] = [pageId];
|
|
const queue: number[] = [pageId];
|
|
while (queue.length > 0) {
|
|
const children = await prisma.catalogPagesBc.findMany({
|
|
where: { parentId: { in: queue } },
|
|
select: { id: true },
|
|
});
|
|
queue.length = 0;
|
|
for (const child of children) {
|
|
toDelete.push(child.id);
|
|
queue.push(child.id);
|
|
}
|
|
}
|
|
await prisma.$transaction([
|
|
prisma.catalogItemsBc.deleteMany({
|
|
where: { pageId: { in: toDelete } },
|
|
}),
|
|
prisma.catalogPagesBc.deleteMany({
|
|
where: { id: { in: toDelete } },
|
|
}),
|
|
]);
|
|
}
|
|
await rcon.updateCatalog();
|
|
return apiOk({});
|
|
}
|
|
await deletePage(pageId, mode);
|
|
await rcon.updateCatalog();
|
|
return apiOk({});
|
|
},
|
|
);
|