x
This commit is contained in:
1 parent
684fd6907b
commit
310e48fe6a
17 files changed
+424
-139
No files matched your search
@@ -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"],
|
||||
|
||||
Reference in new issue
Block a user