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]>
303 lines
7.7 KiB
TypeScript
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: {} };
|
|
}
|