x
This commit is contained in:
1 parent
570810acdb
commit
64bcd6388a
5 files changed
+917
-30
No files matched your search
@@ -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" });
|
||||
}
|
||||
});
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()) });
|
||||
Reference in new issue
Block a user