329 lines
11 KiB
TypeScript
329 lines
11 KiB
TypeScript
import { Elysia, t } from "elysia";
|
|
import prisma from "../lib/prisma";
|
|
|
|
export const productsRoute = new Elysia({ prefix: "/products" })
|
|
.get("/scents", async () => {
|
|
return await prisma.scent.findMany({
|
|
orderBy: { name: "asc" },
|
|
include: { translations: true }
|
|
});
|
|
})
|
|
.patch("/scents/:id/translations", async ({ params, body, status }) => {
|
|
const id = parseInt(params.id);
|
|
if (isNaN(id)) return status(400, { message: "Invalid scent ID" });
|
|
|
|
const payload = body as any;
|
|
const results: any[] = [];
|
|
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
|
|
|
|
for (const lang of LANGS) {
|
|
const name = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name?.[lang];
|
|
if (!name) continue;
|
|
|
|
const existing = await prisma.scentTranslation.findUnique({
|
|
where: { scentId_langCode: { scentId: id, langCode: lang } }
|
|
});
|
|
|
|
if (existing) {
|
|
const updated = await prisma.scentTranslation.update({
|
|
where: { id: existing.id },
|
|
data: { name },
|
|
});
|
|
results.push(updated);
|
|
} else {
|
|
const created = await prisma.scentTranslation.create({
|
|
data: { scentId: id, langCode: lang, name },
|
|
});
|
|
results.push(created);
|
|
}
|
|
}
|
|
|
|
const updated = await prisma.scent.findUnique({
|
|
where: { id },
|
|
include: { translations: true },
|
|
});
|
|
|
|
return { success: true, translations: results, scent: updated };
|
|
})
|
|
.get("/", async ({ query }) => {
|
|
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;
|
|
const limit = query.limit ? parseInt(query.limit) : 12;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where = {
|
|
...(categoryId && { categoryId }),
|
|
...(scentId && { scentId }),
|
|
...(search && {
|
|
OR: [
|
|
{ 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 } } },
|
|
],
|
|
}),
|
|
} as const;
|
|
|
|
const [products, total] = await Promise.all([
|
|
prisma.product.findMany({
|
|
where,
|
|
include: {
|
|
category: { include: { translations: true } },
|
|
scent: true,
|
|
translations: true,
|
|
variants: {
|
|
include: {
|
|
bomItems: { include: { rawMaterial: true } }
|
|
},
|
|
orderBy: { price: "asc" }
|
|
},
|
|
images: { orderBy: { order: "asc" } },
|
|
},
|
|
skip,
|
|
take: limit,
|
|
}),
|
|
prisma.product.count({ where }),
|
|
]);
|
|
|
|
// 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, category: { ...p.category, name: p.category.name } }; // Actually for 'all', keep as is, but wait, category name needs resolving if we want it? Let's just resolve it simply below.
|
|
} else {
|
|
const trans = p.translations.find(t => t.langCode === lang) || p.translations.find(t => t.langCode === 'th') || p.translations[0];
|
|
const catTrans = p.category.translations?.find((t: any) => t.langCode === lang) || p.category.translations?.find((t: any) => t.langCode === 'th') || p.category.translations?.[0];
|
|
return {
|
|
...p,
|
|
name: trans?.name || '',
|
|
description: trans?.description,
|
|
details: trans?.details,
|
|
category: { ...p.category, name: catTrans?.name || p.category.name }
|
|
};
|
|
}
|
|
});
|
|
|
|
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: mappedProducts,
|
|
meta: {
|
|
total,
|
|
page,
|
|
limit,
|
|
lastPage: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
})
|
|
.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: { include: { translations: true } },
|
|
scent: true,
|
|
translations: true,
|
|
variants: {
|
|
include: {
|
|
bomItems: { include: { rawMaterial: true } }
|
|
},
|
|
orderBy: { price: "asc" }
|
|
},
|
|
images: { orderBy: { order: "asc" } },
|
|
},
|
|
});
|
|
|
|
if (!product) return status(404, { message: "Product not found" });
|
|
|
|
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];
|
|
const catTrans = product.category.translations?.find((t: any) => t.langCode === lang) || product.category.translations?.find((t: any) => t.langCode === 'th') || product.category.translations?.[0];
|
|
return {
|
|
...product,
|
|
name: trans?.name || '',
|
|
description: trans?.description,
|
|
details: trans?.details,
|
|
category: { ...product.category, name: catTrans?.name || product.category.name }
|
|
};
|
|
}
|
|
})
|
|
.put(
|
|
"/:id",
|
|
async ({ params, body, status }) => {
|
|
const id = parseInt(params.id);
|
|
if (isNaN(id)) return status(400, { message: "Invalid product ID" });
|
|
|
|
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 },
|
|
});
|
|
|
|
// 2. Sync Variants
|
|
await tx.productVariant.deleteMany({
|
|
where: { productId: id },
|
|
});
|
|
|
|
if (variants && variants.length > 0) {
|
|
for (const v of variants) {
|
|
await tx.productVariant.create({
|
|
data: {
|
|
productId: id,
|
|
size: v.size,
|
|
price: v.price,
|
|
bomItems: v.bomItems && v.bomItems.length > 0 ? {
|
|
create: v.bomItems.map((b: any) => ({
|
|
rawMaterialId: b.rawMaterialId,
|
|
quantity: b.quantity,
|
|
}))
|
|
} : undefined
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 3. Sync Images
|
|
await tx.productImage.deleteMany({
|
|
where: { productId: id },
|
|
});
|
|
|
|
if (images && images.length > 0) {
|
|
for (const [idx, img] of images.entries()) {
|
|
await tx.productImage.create({
|
|
data: {
|
|
productId: id,
|
|
url: img.url,
|
|
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return prod;
|
|
});
|
|
|
|
return { success: true, product: updatedProduct };
|
|
} catch (err: any) {
|
|
console.error("Update product error:", err);
|
|
return status(500, { success: false, message: "Failed to update product" });
|
|
}
|
|
},
|
|
{
|
|
body: t.Any(),
|
|
detail: {
|
|
summary: "Update Product",
|
|
tags: ["products"],
|
|
},
|
|
}
|
|
)
|
|
.patch(
|
|
"/:id",
|
|
async ({ params, body, status }) => {
|
|
const id = parseInt(params.id);
|
|
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: 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);
|
|
return status(500, { success: false, message: "Failed to patch product" });
|
|
}
|
|
},
|
|
{
|
|
body: t.Any(),
|
|
detail: {
|
|
summary: "Partial Update Product",
|
|
tags: ["products"],
|
|
},
|
|
}
|
|
);
|