This commit is contained in:
Your Name committed 2026-10-15 06:56:21 +07:00
1 parent 570810acdb
commit 64bcd6388a
5 files changed
+917 -30

No files matched your search

+24 -11
View File
@@ -1,6 +1,7 @@
import { Elysia, t } from "elysia";
import type { Prisma } from "@prisma/client";
import prisma from "../lib/prisma";
import { purgeCategories } from "../lib/catalogMaintenance";
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
const CATEGORY_MEDIA_ROLES = ['thumbnail', 'hero_desktop', 'hero_mobile', 'card_portrait', 'social_share'] as const;
@@ -599,28 +600,40 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
media: t.Optional(t.Array(categoryMediaInput, { maxItems: CATEGORY_MEDIA_ROLES.length })),
})
})
.delete("/:id", async ({ params, status }) => {
.delete("/:id", async ({ params, query, status }) => {
const id = parseInt(params.id);
if (isNaN(id)) return status(400, { message: "Invalid category ID" });
const force = (query as any)?.force === 'true' || (query as any)?.cascade === 'true';
try {
const category = await prisma.category.findUnique({
where: { id, deletedAt: null },
include: { _count: { select: { products: { where: { isDeleted: false } } } } }
where: { id },
include: { _count: { select: { products: true } } },
});
if (!category) return status(404, { message: "Category not found" });
if (category._count.products > 0) {
return status(400, { message: "Cannot delete category with existing products" });
if (category._count.products > 0 && !force) {
return status(400, {
message: `Cannot delete category: ${category._count.products} product(s) still exist. Use ?force=true to cascade delete.`,
});
}
const deletedAt = new Date();
await prisma.category.update({
where: { id },
data: { deletedAt },
const result = await prisma.$transaction(async (tx) => {
return await purgeCategories(tx, {
categoryIds: [id],
forceCascadeProducts: force,
});
});
return { success: true, softDeleted: true, id, deletedAt };
return {
success: true,
id,
deletedProducts: result.deletedProducts,
message: `Category deleted successfully${result.deletedProducts > 0 ? ` with ${result.deletedProducts} cascade products` : ""}`,
};
} catch (err: any) {
return status(500, { message: "Failed to delete category" });
console.error("[Categories] Delete error:", err);
return status(500, { message: err.message || "Failed to delete category" });
}
});
+28 -4
View File
@@ -1,6 +1,7 @@
import { Elysia, t } from "elysia";
import type { Prisma } from "@prisma/client";
import prisma from "../lib/prisma";
import { purgeProducts } from "../lib/catalogMaintenance";
import { localizeProductAttribute } from "../lib/productAttributes";
import {
createCustomProductDetailSection,
@@ -1036,7 +1037,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
)
.delete(
"/:id",
async ({ params, status }) => {
async ({ params, query, status }) => {
const id = parseInt(params.id);
if (isNaN(id)) return status(400, { success: false, message: "Invalid product ID" });
@@ -1049,6 +1050,15 @@ export const productsRoute = new Elysia({ prefix: "/products" })
return status(404, { success: false, message: "Product not found" });
}
const hard = (query as any)?.hard === 'true' || (query as any)?.purge === 'true';
if (hard) {
await prisma.$transaction(async (tx) => {
await purgeProducts(tx, [id]);
});
return { success: true, hardDeleted: true, message: "Product hard-deleted successfully" };
}
await prisma.product.update({
where: { id },
data: {
@@ -1061,14 +1071,14 @@ export const productsRoute = new Elysia({ prefix: "/products" })
},
{
detail: {
summary: "Soft Delete Product",
summary: "Delete Product (Soft or Hard via ?hard=true)",
tags: ["products"],
},
}
)
.post(
"/bulk-delete",
async ({ body, status }) => {
async ({ body, query, status }) => {
const { ids } = body as { ids?: number[] };
if (!Array.isArray(ids) || ids.length === 0) {
return status(400, { success: false, message: "Invalid or empty IDs array" });
@@ -1079,6 +1089,20 @@ export const productsRoute = new Elysia({ prefix: "/products" })
return status(400, { success: false, message: "No valid IDs provided" });
}
const hard = (query as any)?.hard === 'true' || (query as any)?.purge === 'true';
if (hard) {
const result = await prisma.$transaction(async (tx) => {
return await purgeProducts(tx, validIds);
});
return {
success: true,
count: result.count,
hardDeleted: true,
message: `Successfully hard-deleted ${result.count} products`,
};
}
const result = await prisma.product.updateMany({
where: { id: { in: validIds } },
data: {
@@ -1098,7 +1122,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
ids: t.Array(t.Numeric()),
}),
detail: {
summary: "Bulk Soft Delete Products",
summary: "Bulk Delete Products (Soft or Hard via ?hard=true)",
tags: ["products"],
},
}
+116 -1
View File
@@ -7,6 +7,12 @@ import {
mergeFooterLayoutSettings,
normalizeFooterLayoutSettings,
} from "../lib/footerLayoutSettings";
import {
getCatalogStats,
purgeProducts,
purgeCategories,
purgeDemoHomAroma,
} from "../lib/catalogMaintenance";
const FooterLayoutUpdateRequestSchema = FooterLayoutSettingsPatchSchema.extend({
expectedVersion: z.string().optional(),
@@ -879,4 +885,113 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
set.status = 500;
return { error: "Failed to seed pages" };
}
});
})
.get("/system/catalog-stats", async ({ set }: { set: any }) => {
try {
const stats = await getCatalogStats();
return { success: true, stats };
} catch (err: any) {
console.error("[Settings] Catalog stats error:", err);
set.status = 500;
return { error: err.message || "Failed to fetch catalog stats" };
}
})
.post("/system/purge-products", async ({ body, set }: { body: any; set: any }) => {
try {
const { productIds } = body || {};
const result = await prisma.$transaction(async (tx: any) => {
return await purgeProducts(tx, productIds);
});
return {
success: true,
count: result.count,
message: `ลบสินค้าทั้งหมดสำเร็จ: ${result.count} รายการ (พร้อมล้างข้อมูล Variant, รูปภาพ, สเปก และรีวิวที่เกี่ยวข้องเรียบร้อย)`,
};
} catch (err: any) {
console.error("[Settings] Purge products error:", err);
set.status = 500;
return { error: err.message || "Failed to purge products" };
}
}, { body: t.Optional(t.Any()) })
.post("/system/purge-categories", async ({ body, set }: { body: any; set: any }) => {
try {
const { categoryIds, forceCascadeProducts } = body || {};
const result = await prisma.$transaction(async (tx: any) => {
return await purgeCategories(tx, {
categoryIds,
forceCascadeProducts: Boolean(forceCascadeProducts),
});
});
return {
success: true,
deletedCategories: result.deletedCategories,
deletedProducts: result.deletedProducts,
message: `ลบหมวดหมู่สำเร็จ: ${result.deletedCategories} หมวดหมู่${result.deletedProducts > 0 ? ` (และลบสินค้าที่ผูกอยู่ ${result.deletedProducts} รายการ)` : ""}`,
};
} catch (err: any) {
console.error("[Settings] Purge categories error:", err);
set.status = 400;
return { error: err.message || "Failed to purge categories" };
}
}, { body: t.Optional(t.Any()) })
.post("/system/purge-demo-homaroma", async ({ set }: { set: any }) => {
try {
const result = await prisma.$transaction(async (tx: any) => {
return await purgeDemoHomAroma(tx);
});
return {
success: true,
...result,
message: `ล้างข้อมูล Demo ของ Hom Aroma สำเร็จ: หมวดหมู่ ${result.deletedCategories} รายการ, สินค้า ${result.deletedProducts} รายการ, แบรนด์กลิ่น ${result.deletedBrands} รายการ`,
};
} catch (err: any) {
console.error("[Settings] Purge demo error:", err);
set.status = 500;
return { error: err.message || "Failed to purge demo data" };
}
})
.post("/system/purge-catalog", async ({ body, set }: { body: any; set: any }) => {
try {
const { clearProducts = true, clearCategories = true, clearBrands = false } = body || {};
const result = await prisma.$transaction(async (tx: any) => {
let pCount = 0;
let cCount = 0;
let bCount = 0;
if (clearProducts || clearCategories) {
const pRes = await purgeProducts(tx);
pCount = pRes.count;
}
if (clearCategories) {
const cRes = await purgeCategories(tx, { forceCascadeProducts: true });
cCount = cRes.deletedCategories;
}
if (clearBrands) {
const unusedBrands = await tx.productBrand.findMany({
where: { products: { none: {} } },
select: { id: true },
});
if (unusedBrands.length > 0) {
const bIds = unusedBrands.map((b: any) => b.id);
await tx.productBrandTranslation.deleteMany({ where: { brandId: { in: bIds } } });
const bRes = await tx.productBrand.deleteMany({ where: { id: { in: bIds } } });
bCount = bRes.count;
}
}
return { deletedProducts: pCount, deletedCategories: cCount, deletedBrands: bCount };
});
return {
success: true,
...result,
message: `ล้างแคตตาล็อกทั้งหมดสำเร็จ: สินค้า ${result.deletedProducts} รายการ, หมวดหมู่ ${result.deletedCategories} รายการ${result.deletedBrands > 0 ? `, แบรนด์ ${result.deletedBrands} รายการ` : ""}`,
};
} catch (err: any) {
console.error("[Settings] Purge catalog error:", err);
set.status = 500;
return { error: err.message || "Failed to purge catalog" };
}
}, { body: t.Optional(t.Any()) });