This commit is contained in:
Your Name committed 2026-06-13 23:10:33 -04:00
1 parent 684fd6907b
commit 310e48fe6a
17 files changed
+424 -139

No files matched your search

+20 -3
View File
@@ -15,6 +15,7 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
})
.get("/:id/products", async ({ params, query, status }) => {
const id = parseInt(params.id);
const lang = (query as any).lang || 'th';
if (isNaN(id)) return status(400, { message: "Invalid category ID" });
// Pagination params
@@ -30,8 +31,9 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
include: {
scent: true,
category: true,
variants: { orderBy: { price: "asc" } },
images: { orderBy: { order: "asc" } },
translations: true,
variants: { orderBy: { price: "asc" as const } },
images: { orderBy: { order: "asc" as const } },
},
orderBy: { name: "asc" },
skip,
@@ -54,7 +56,22 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
imageUrl: category.imageUrl,
_count: category._count,
},
data: category.products,
data: category.products.map((p: any) => {
if (lang === 'all') {
const nameObj: any = {};
const descObj: any = {};
const detailsObj: any = {};
p.translations?.forEach((t: any) => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj };
} else {
const trans = p.translations?.find((t: any) => t.langCode === lang) || p.translations?.find((t: any) => t.langCode === 'th') || p.translations?.[0];
return { ...p, name: trans?.name || p.scent?.name || '', description: trans?.description, details: trans?.details };
}
}),
meta: {
total,
page,
+18 -1
View File
@@ -28,6 +28,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
})
.get("/:slug", async ({ params: { slug }, query, set }) => {
const includeInactive = String((query as any)?.includeInactive || "").toLowerCase() === "true";
const lang = (query as any)?.lang || 'th';
const page = await prisma.page.findUnique({
where: { slug },
include: {
@@ -56,6 +57,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
const productInclude = {
category: true,
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
@@ -88,7 +90,22 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
products.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
}
populatedPayload.products = products;
populatedPayload.products = products.map((p: any) => {
if (lang === 'all') {
const nameObj: any = {};
const descObj: any = {};
const detailsObj: any = {};
p.translations?.forEach((t: any) => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj };
} else {
const trans = p.translations?.find((t: any) => t.langCode === lang) || p.translations?.find((t: any) => t.langCode === 'th') || p.translations?.[0];
return { ...p, name: trans?.name || p.scent?.name || '', description: trans?.description, details: trans?.details };
}
});
} else if (section.type === 'CATEGORIES') {
let categories: any[] = [];
if (Array.isArray(payload.categoryIds) && payload.categoryIds.length > 0) {
+103 -49
View File
@@ -11,6 +11,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
const scentId = query.scentId ? parseInt(query.scentId) : undefined;
const search = query.search?.trim() || undefined;
const lang = query.lang || 'th';
// Pagination params
const page = query.page ? parseInt(query.page) : 1;
@@ -22,7 +23,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
...(scentId && { scentId }),
...(search && {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ translations: { some: { name: { contains: search, mode: "insensitive" as const } } } },
{ scent: { name: { contains: search, mode: "insensitive" as const } } },
{ category: { name: { contains: search, mode: "insensitive" as const } } },
],
@@ -35,6 +36,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
include: {
category: true,
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
@@ -43,20 +45,38 @@ export const productsRoute = new Elysia({ prefix: "/products" })
},
images: { orderBy: { order: "asc" } },
},
orderBy: { name: "asc" },
skip,
take: limit,
}),
prisma.product.count({ where }),
]);
console.log(`Fetch products paginated: count=${products.length}, total=${total}`)
if (products.length > 0) {
console.log(`Sample product ${products[0].id} images count: ${products[0].images?.length || 0}`)
}
// Map translations
const mappedProducts = products.map(p => {
if (lang === 'all') {
const nameObj: any = {};
const descObj: any = {};
const detailsObj: any = {};
p.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj };
} else {
const trans = p.translations.find(t => t.langCode === lang) || p.translations.find(t => t.langCode === 'th') || p.translations[0];
return { ...p, name: trans?.name || '', description: trans?.description, details: trans?.details };
}
});
mappedProducts.sort((a, b) => {
const aName = typeof a.name === 'string' ? a.name : (a.name?.th || '');
const bName = typeof b.name === 'string' ? b.name : (b.name?.th || '');
return aName.localeCompare(bName);
});
return {
data: products,
data: mappedProducts,
meta: {
total,
page,
@@ -65,15 +85,17 @@ export const productsRoute = new Elysia({ prefix: "/products" })
},
};
})
.get("/:id", async ({ params, status }) => {
.get("/:id", async ({ params, query, status }) => {
const id = parseInt(params.id);
if (isNaN(id)) return status(400, { message: "Invalid product ID" });
const lang = query.lang || 'th';
const product = await prisma.product.findUnique({
where: { id },
include: {
category: true,
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
@@ -85,7 +107,21 @@ export const productsRoute = new Elysia({ prefix: "/products" })
});
if (!product) return status(404, { message: "Product not found" });
return product;
if (lang === 'all') {
const nameObj: any = {};
const descObj: any = {};
const detailsObj: any = {};
product.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...product, name: nameObj, description: descObj, details: detailsObj };
} else {
const trans = product.translations.find(t => t.langCode === lang) || product.translations.find(t => t.langCode === 'th') || product.translations[0];
return { ...product, name: trans?.name || '', description: trans?.description, details: trans?.details };
}
})
.put(
"/:id",
@@ -93,15 +129,14 @@ export const productsRoute = new Elysia({ prefix: "/products" })
const id = parseInt(params.id);
if (isNaN(id)) return status(400, { message: "Invalid product ID" });
const { categoryId, description, details, variants, images } = body;
console.log(`[Product Update] ID=${id}, Category=${categoryId}, Variants=${variants?.length || 0}, Images=${images?.length || 0}`);
const { categoryId, description, details, variants, images } = body as any;
try {
const updatedProduct = await prisma.$transaction(async (tx) => {
// 1. Update basic product info
const prod = await tx.product.update({
where: { id },
data: { categoryId, description, details },
data: { categoryId },
});
// 2. Sync Variants
@@ -133,8 +168,6 @@ export const productsRoute = new Elysia({ prefix: "/products" })
});
if (images && images.length > 0) {
// Using a loop instead of createMany to be more robust with Postgres sequence/ID management
// and avoid unique constraint collisions if IDs were somehow kept in body
for (const [idx, img] of images.entries()) {
await tx.productImage.create({
data: {
@@ -156,33 +189,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
}
},
{
body: t.Object({
categoryId: t.Number(),
description: t.Optional(t.String()),
details: t.Optional(t.String()),
variants: t.Array(
t.Object({
size: t.String(),
price: t.Number(),
bomItems: t.Optional(
t.Array(
t.Object({
rawMaterialId: t.Number(),
quantity: t.Number(),
})
)
),
})
),
images: t.Optional(
t.Array(
t.Object({
url: t.String(),
order: t.Optional(t.Number()),
})
)
),
}),
body: t.Any(),
detail: {
summary: "Update Product",
tags: ["products"],
@@ -196,10 +203,63 @@ export const productsRoute = new Elysia({ prefix: "/products" })
if (isNaN(id)) return status(400, { message: "Invalid product ID" });
try {
const payload = body as any;
const prodData: any = {};
if (payload.categoryId !== undefined) prodData.categoryId = payload.categoryId;
if (payload.scentId !== undefined) prodData.scentId = payload.scentId;
if (payload.price !== undefined) prodData.price = payload.price;
const prod = await prisma.product.update({
where: { id },
data: body,
data: prodData,
});
// Update translations
// If name, description, details are strings, treat as 'th'
// If they are objects, iterate over them
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of languages) {
const updateData: any = {};
let shouldUpdate = false;
if (payload.name) {
const val = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name[lang];
if (val !== undefined && val !== null) { updateData.name = val; shouldUpdate = true; }
}
if (payload.description) {
const val = typeof payload.description === 'string' ? (lang === 'th' ? payload.description : null) : payload.description[lang];
if (val !== undefined && val !== null) { updateData.description = val; shouldUpdate = true; }
}
if (payload.details) {
const val = typeof payload.details === 'string' ? (lang === 'th' ? payload.details : null) : payload.details[lang];
if (val !== undefined && val !== null) { updateData.details = val; shouldUpdate = true; }
}
if (shouldUpdate) {
// First check if translation exists to create or update
const existing = await prisma.productTranslation.findUnique({
where: { productId_langCode: { productId: id, langCode: lang } }
});
if (existing) {
await prisma.productTranslation.update({
where: { id: existing.id },
data: updateData
});
} else if (updateData.name) { // name is required for creation
await prisma.productTranslation.create({
data: {
productId: id,
langCode: lang,
name: updateData.name,
description: updateData.description,
details: updateData.details
}
});
}
}
}
return { success: true, product: prod };
} catch (err: any) {
console.error("Patch product error:", err);
@@ -207,13 +267,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
}
},
{
body: t.Object({
name: t.Optional(t.String()),
categoryId: t.Optional(t.Number()),
scentId: t.Optional(t.Nullable(t.Number())),
description: t.Optional(t.String()),
details: t.Optional(t.String()),
}),
body: t.Any(),
detail: {
summary: "Partial Update Product",
tags: ["products"],