Files
EpicNext-Cms/src/actions/catalog-bc.ts
T
SimoandCursor 7d50b72ade feat(catalog): Visual Manager for Builder Club with honest canEdit
Mount BC Visual Manager, thread catalogType through tree/editor actions, and stop hardcoding canEdit=true in items/translate tabs.

Co-authored-by: Cursor <[email protected]>
2026-07-18 21:03:07 +02:00

303 lines
7.7 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import type { Prisma } from "@/generated/prisma/client";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
const BC_PAGE_FIELDS = [
"caption",
"parentId",
"pageLayout",
"enabled",
"visible",
"orderNum",
"iconImage",
"iconColor",
"pageHeadline",
"pageTeaser",
"pageSpecial",
"pageText1",
"pageText2",
"pageTextDetails",
"pageTextTeaser",
] as const;
const BC_ITEM_FIELDS = [
"itemIds",
"catalogName",
"orderNumber",
"extradata",
"pageId",
] as const;
function pickAllowed(
fields: Record<string, unknown>,
allowed: readonly string[],
) {
const out: Record<string, unknown> = {};
for (const key of allowed) {
if (Object.hasOwn(fields, key) && fields[key] !== undefined) {
out[key] = fields[key];
}
}
return out;
}
export async function updateBcPage({
id,
...fields
}: { id: number } & Record<string, unknown>) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const data = pickAllowed(fields, BC_PAGE_FIELDS);
if (Object.keys(data).length === 0) {
return { ok: false as const, error: "No valid fields to update" };
}
await prisma.catalogPagesBc.update({
where: { id },
data: data as Prisma.CatalogPagesBcUpdateInput,
});
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_page_update",
description: `Updated BC catalog page #${id}`,
targetType: "catalog_page_bc",
targetId: id,
});
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
}
export async function deleteBcItem({ id }: { id: number }) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItemsBc.delete({ where: { id } });
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_delete",
description: `Deleted BC catalog item #${id}`,
targetType: "catalog_item_bc",
targetId: id,
});
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
}
export async function updateBcItem({
id,
...data
}: {
id: number;
itemIds?: string;
catalogName?: string;
orderNumber?: number;
extradata?: string;
}) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const safe = pickAllowed(data as Record<string, unknown>, BC_ITEM_FIELDS);
if (Object.keys(safe).length === 0) {
return { ok: false as const, error: "No valid fields to update" };
}
await prisma.catalogItemsBc.update({
where: { id },
data: safe as Prisma.CatalogItemsBcUpdateInput,
});
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_update",
description: `Updated BC catalog item #${id}`,
targetType: "catalog_item_bc",
targetId: id,
});
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
}
export async function createBcItem({
pageId,
...data
}: {
pageId: number;
itemIds: string;
catalogName: string;
orderNumber: number;
extradata: string;
}) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const created = await prisma.catalogItemsBc.create({
data: { pageId, ...data },
});
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_create",
description: `Created BC catalog item #${created.id}`,
targetType: "catalog_item_bc",
targetId: created.id,
});
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: { id: created.id } };
}
export async function toggleBcPage({
id,
field,
}: {
id: number;
field: "enabled" | "visible";
}) {
await requirePermission(PERMS.CATALOG_EDIT);
const page = await prisma.catalogPagesBc.findUnique({
where: { id },
select: { enabled: true, visible: true },
});
if (!page) return { ok: false as const, error: "Page not found" };
await prisma.catalogPagesBc.update({
where: { id },
data: { [field]: page[field] === "1" ? "0" : "1" },
});
await rcon.updateCatalog();
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
}
export async function createBcPage(input: {
caption: string;
parentId: number;
pageLayout?: string;
iconImage?: number;
iconColor?: number;
enabled?: "0" | "1";
visible?: "0" | "1";
orderNum?: number;
}) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const created = await prisma.catalogPagesBc.create({
data: {
caption: input.caption,
parentId: input.parentId,
pageLayout: input.pageLayout ?? "default_3x3",
iconColor: input.iconColor ?? 0,
iconImage: input.iconImage ?? 0,
orderNum: input.orderNum ?? 0,
visible: input.visible ?? "1",
enabled: input.enabled ?? "1",
pageHeadline: "",
pageTeaser: "",
},
});
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_page_create",
description: `Created BC catalog page "${input.caption}"`,
targetType: "catalog_page_bc",
targetId: created.id,
});
revalidatePath("/admin/catalog");
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: { id: created.id } };
}
async function moveBcPage(pageId: number, newParentId: number): Promise<void> {
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.catalogPagesBc.findUnique({
where: { id: currentId },
select: { parentId: true },
});
if (!parent || parent.parentId <= 0) break;
currentId = parent.parentId;
}
}
await prisma.catalogPagesBc.update({
where: { id: pageId },
data: { parentId: newParentId },
});
}
export async function reorderBcTreePage(input: {
pageId: number;
newParentId?: number;
newOrderNum: number;
}) {
await requirePermission(PERMS.CATALOG_EDIT);
if (input.newParentId !== undefined) {
try {
await moveBcPage(input.pageId, input.newParentId);
} catch (err) {
return {
ok: false as const,
error: err instanceof Error ? err.message : "Invalid move",
};
}
}
await prisma.catalogPagesBc.update({
where: { id: input.pageId },
data: { orderNum: input.newOrderNum },
});
await rcon.updateCatalog();
revalidatePath("/admin/catalog");
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: {} };
}
export async function deleteBcTreePage(input: {
pageId: number;
mode: "reparent" | "cascade";
}) {
await requirePermission(PERMS.CATALOG_EDIT);
const page = await prisma.catalogPagesBc.findUnique({
where: { id: input.pageId },
select: { parentId: true },
});
if (!page) return { ok: false as const, error: "Page not found" };
if (input.mode === "reparent") {
await prisma.$transaction([
prisma.catalogPagesBc.updateMany({
where: { parentId: input.pageId },
data: { parentId: page.parentId },
}),
prisma.catalogItemsBc.deleteMany({ where: { pageId: input.pageId } }),
prisma.catalogPagesBc.delete({ where: { id: input.pageId } }),
]);
} else {
const toDelete: number[] = [input.pageId];
const queue: number[] = [input.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();
revalidatePath("/admin/catalog");
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: {} };
}