742 lines
26 KiB
TypeScript
742 lines
26 KiB
TypeScript
import { Elysia, t } from "elysia";
|
|
import prisma from "../lib/prisma";
|
|
import { localizeProductAttribute } from "../lib/productAttributes";
|
|
|
|
export const productsRoute = new Elysia({ prefix: "/products" })
|
|
.decorate("validateVariants", (options: any[] = [], variants: any[] = []) => {
|
|
if (options.length > 3) throw new Error("Options array length cannot exceed 3.");
|
|
|
|
const getPrimaryValues = (values: any[]) => {
|
|
return values.map(val => {
|
|
if (typeof val === 'string') return val;
|
|
if (typeof val === 'object' && val !== null) {
|
|
return val.th || Object.values(val)[0];
|
|
}
|
|
return String(val);
|
|
});
|
|
};
|
|
|
|
for (const v of variants) {
|
|
if (v.option1 && options[0]) {
|
|
const validValues = getPrimaryValues(options[0].values);
|
|
if (!validValues.includes(v.option1)) {
|
|
throw new Error(`Variant option1 '${v.option1}' is not found in options[0].values [${validValues.join(', ')}]`);
|
|
}
|
|
}
|
|
if (v.option2 && options[1]) {
|
|
const validValues = getPrimaryValues(options[1].values);
|
|
if (!validValues.includes(v.option2)) {
|
|
throw new Error(`Variant option2 '${v.option2}' is not found in options[1].values [${validValues.join(', ')}]`);
|
|
}
|
|
}
|
|
if (v.option3 && options[2]) {
|
|
const validValues = getPrimaryValues(options[2].values);
|
|
if (!validValues.includes(v.option3)) {
|
|
throw new Error(`Variant option3 '${v.option3}' is not found in options[2].values [${validValues.join(', ')}]`);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.get("/brands", async () => {
|
|
return await prisma.productBrand.findMany({
|
|
orderBy: { name: "asc" },
|
|
include: { translations: true }
|
|
});
|
|
})
|
|
.patch("/brands/:id/translations", async ({ params, body, status }) => {
|
|
const id = parseInt(params.id);
|
|
if (isNaN(id)) return status(400, { message: "Invalid brand 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.productBrandTranslation.findUnique({
|
|
where: { brandId_langCode: { brandId: id, langCode: lang } }
|
|
});
|
|
|
|
if (existing) {
|
|
const updated = await prisma.productBrandTranslation.update({
|
|
where: { id: existing.id },
|
|
data: { name },
|
|
});
|
|
results.push(updated);
|
|
} else {
|
|
const created = await prisma.productBrandTranslation.create({
|
|
data: { brandId: id, langCode: lang, name },
|
|
});
|
|
results.push(created);
|
|
}
|
|
}
|
|
|
|
const updated = await prisma.productBrand.findUnique({
|
|
where: { id },
|
|
include: { translations: true },
|
|
});
|
|
|
|
return { success: true, translations: results, brand: updated };
|
|
})
|
|
.get("/", async ({ query }) => {
|
|
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
|
|
const brandId = query.brandId ? parseInt(query.brandId) : 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 }),
|
|
...(brandId && { brandId }),
|
|
...(search && {
|
|
OR: [
|
|
{ translations: { some: { name: { contains: search, mode: "insensitive" as const } } } },
|
|
{ brand: { 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 } },
|
|
brand: true,
|
|
translations: true,
|
|
attributes: {
|
|
include: {
|
|
translations: true,
|
|
attributeDefinition: {
|
|
include: {
|
|
category: true,
|
|
translations: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: [
|
|
{ attributeDefinition: { sortOrder: "asc" } },
|
|
{ id: "asc" },
|
|
],
|
|
},
|
|
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: 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,
|
|
category: { ...p.category, name: p.category.name },
|
|
attributes: p.attributes.map((attribute: any) => localizeProductAttribute(attribute, lang)),
|
|
};
|
|
} else {
|
|
const trans = p.translations.find((t: any) => t.langCode === lang) || p.translations.find((t: any) => 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 || p.name || '',
|
|
description: trans?.description || p.description,
|
|
details: trans?.details || p.details,
|
|
category: { ...p.category, name: catTrans?.name || p.category.name },
|
|
attributes: p.attributes.map((attribute: any) => localizeProductAttribute(attribute, lang)),
|
|
};
|
|
}
|
|
});
|
|
|
|
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 } },
|
|
brand: true,
|
|
translations: true,
|
|
attributes: {
|
|
include: {
|
|
translations: true,
|
|
attributeDefinition: {
|
|
include: {
|
|
category: true,
|
|
translations: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: [
|
|
{ attributeDefinition: { sortOrder: "asc" } },
|
|
{ id: "asc" },
|
|
],
|
|
},
|
|
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: any) => {
|
|
nameObj[t.langCode] = t.name;
|
|
descObj[t.langCode] = t.description;
|
|
detailsObj[t.langCode] = t.details;
|
|
});
|
|
return {
|
|
...product,
|
|
name: nameObj,
|
|
description: descObj,
|
|
details: detailsObj,
|
|
attributes: product.attributes.map((attribute: any) => localizeProductAttribute(attribute, lang)),
|
|
};
|
|
} else {
|
|
const trans = product.translations.find((t: any) => t.langCode === lang) || product.translations.find((t: any) => 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 || product.name || '',
|
|
description: trans?.description || product.description,
|
|
details: trans?.details || product.details,
|
|
category: { ...product.category, name: catTrans?.name || product.category.name },
|
|
attributes: product.attributes.map((attribute: any) => localizeProductAttribute(attribute, lang)),
|
|
};
|
|
}
|
|
})
|
|
.put(
|
|
"/:id",
|
|
async ({ params, body, status, validateVariants }) => {
|
|
const id = parseInt(params.id);
|
|
if (isNaN(id)) return status(400, { message: "Invalid product ID" });
|
|
|
|
const { categoryId, description, details, variants, images } = body as any;
|
|
let options = (body as any).options;
|
|
|
|
try {
|
|
if (variants && Array.isArray(variants)) {
|
|
// If options is not passed but variants are, we might need to fetch existing options
|
|
if (options === undefined) {
|
|
const existingProduct = await prisma.product.findUnique({ where: { id } });
|
|
options = existingProduct?.options || [];
|
|
}
|
|
validateVariants(options, variants);
|
|
}
|
|
const updatedProduct = await prisma.$transaction(async (tx) => {
|
|
// 1. Update basic product info
|
|
const updateData: any = {};
|
|
if (categoryId !== undefined) updateData.categoryId = categoryId;
|
|
if (body.options !== undefined) updateData.options = body.options;
|
|
|
|
const prod = await tx.product.update({
|
|
where: { id },
|
|
data: updateData,
|
|
});
|
|
|
|
// 2. Sync Variants
|
|
if (variants && Array.isArray(variants)) {
|
|
const existingVariants = await tx.productVariant.findMany({ where: { productId: id } });
|
|
const incomingIds = variants.map((v: any) => v.id).filter(Boolean);
|
|
|
|
// Delete variants not in incoming payload
|
|
const variantsToDelete = existingVariants.filter(ev => !incomingIds.includes(ev.id));
|
|
if (variantsToDelete.length > 0) {
|
|
await tx.productVariant.deleteMany({
|
|
where: { id: { in: variantsToDelete.map(ev => ev.id) } }
|
|
});
|
|
}
|
|
|
|
for (const v of variants) {
|
|
if (v.id) {
|
|
// Check if it really belongs to this product
|
|
const exists = existingVariants.find(ev => ev.id === v.id);
|
|
if (exists) {
|
|
await tx.productVariant.update({
|
|
where: { id: v.id },
|
|
data: {
|
|
sku: v.sku || null,
|
|
barcode: v.barcode || null,
|
|
stock: v.stock || 0,
|
|
option1: v.option1 || "",
|
|
option2: v.option2 || "",
|
|
option3: v.option3 || "",
|
|
price: v.price || 0,
|
|
bomItems: v.bomItems ? {
|
|
deleteMany: {},
|
|
create: v.bomItems.map((b: any) => ({
|
|
rawMaterialId: b.rawMaterialId,
|
|
quantity: b.quantity,
|
|
}))
|
|
} : undefined
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
await tx.productVariant.create({
|
|
data: {
|
|
productId: id,
|
|
sku: v.sku || null,
|
|
barcode: v.barcode || null,
|
|
stock: v.stock || 0,
|
|
option1: v.option1 || "",
|
|
option2: v.option2 || "",
|
|
option3: v.option3 || "",
|
|
price: v.price || 0,
|
|
bomItems: v.bomItems && v.bomItems.length > 0 ? {
|
|
create: v.bomItems.map((b: any) => ({
|
|
rawMaterialId: b.rawMaterialId,
|
|
quantity: b.quantity,
|
|
}))
|
|
} : undefined
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Sync Images
|
|
if (images && Array.isArray(images)) {
|
|
const existingImages = await tx.productImage.findMany({ where: { productId: id } });
|
|
const incomingImgIds = images.map((img: any) => img.id).filter(Boolean);
|
|
|
|
const imagesToDelete = existingImages.filter(ei => !incomingImgIds.includes(ei.id));
|
|
if (imagesToDelete.length > 0) {
|
|
await tx.productImage.deleteMany({
|
|
where: { id: { in: imagesToDelete.map(ei => ei.id) } }
|
|
});
|
|
}
|
|
|
|
for (const [idx, img] of images.entries()) {
|
|
if (img.id) {
|
|
const exists = existingImages.find(ei => ei.id === img.id);
|
|
if (exists) {
|
|
await tx.productImage.update({
|
|
where: { id: img.id },
|
|
data: {
|
|
url: img.url,
|
|
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
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: err?.message || "Failed to update product" });
|
|
}
|
|
},
|
|
{
|
|
body: t.Any(),
|
|
detail: {
|
|
summary: "Update Product",
|
|
tags: ["products"],
|
|
},
|
|
}
|
|
)
|
|
.post(
|
|
"/:id/images",
|
|
async ({ params, body, status }) => {
|
|
const id = parseInt(params.id);
|
|
if (isNaN(id)) return status(400, { message: "Invalid product ID" });
|
|
const { url } = body as any;
|
|
if (!url) return status(400, { message: "URL is required" });
|
|
|
|
try {
|
|
const product = await prisma.product.findUnique({
|
|
where: { id },
|
|
include: { images: true }
|
|
});
|
|
if (!product) return status(404, { message: "Product not found" });
|
|
|
|
const newImage = await prisma.productImage.create({
|
|
data: {
|
|
productId: id,
|
|
url,
|
|
order: product.images.length
|
|
}
|
|
});
|
|
|
|
return { success: true, image: newImage };
|
|
} catch (err: any) {
|
|
console.error("Add product image error:", err);
|
|
return status(500, { success: false, message: err?.message || "Failed to add product image" });
|
|
}
|
|
},
|
|
{
|
|
body: t.Object({
|
|
url: t.String()
|
|
})
|
|
}
|
|
)
|
|
.post(
|
|
"/",
|
|
async ({ body, status, validateVariants }) => {
|
|
const payload = body as any;
|
|
try {
|
|
if (payload.variants && Array.isArray(payload.variants)) {
|
|
validateVariants(payload.options || [], payload.variants);
|
|
}
|
|
const product = await prisma.$transaction(async (tx) => {
|
|
let categoryId = payload.categoryId;
|
|
if (!categoryId && payload.categoryName) {
|
|
let cat = await tx.category.findUnique({ where: { name: payload.categoryName } });
|
|
if (!cat) {
|
|
cat = await tx.category.create({ data: { name: payload.categoryName } });
|
|
}
|
|
categoryId = cat.id;
|
|
}
|
|
|
|
let brandId = payload.brandId;
|
|
if (!brandId && payload.brandName) {
|
|
let brand = await tx.productBrand.findUnique({ where: { name: payload.brandName } });
|
|
if (!brand) {
|
|
brand = await tx.productBrand.create({ data: { name: payload.brandName } });
|
|
}
|
|
brandId = brand.id;
|
|
}
|
|
|
|
const prod = await tx.product.create({
|
|
data: {
|
|
categoryId: categoryId,
|
|
brandId: brandId,
|
|
name: payload.name || "New Product",
|
|
price: payload.price || 0,
|
|
description: payload.description || "",
|
|
details: payload.details || "",
|
|
options: payload.options || [],
|
|
}
|
|
});
|
|
|
|
if (payload.translations && typeof payload.translations === 'object') {
|
|
for (const [lang, trans] of Object.entries(payload.translations)) {
|
|
await tx.productTranslation.create({
|
|
data: {
|
|
productId: prod.id,
|
|
langCode: lang,
|
|
name: (trans as any).name || payload.name || "New Product",
|
|
description: (trans as any).description || payload.description || "",
|
|
details: (trans as any).details || payload.details || "",
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
await tx.productTranslation.create({
|
|
data: {
|
|
productId: prod.id,
|
|
langCode: 'th',
|
|
name: payload.name || "New Product",
|
|
description: payload.description || "",
|
|
details: payload.details || "",
|
|
}
|
|
});
|
|
}
|
|
|
|
if (payload.variants && payload.variants.length > 0) {
|
|
for (const v of payload.variants) {
|
|
await tx.productVariant.create({
|
|
data: {
|
|
productId: prod.id,
|
|
sku: v.sku || null,
|
|
barcode: v.barcode || null,
|
|
stock: v.stock || 0,
|
|
option1: v.option1 || "",
|
|
option2: v.option2 || "",
|
|
option3: v.option3 || "",
|
|
price: v.price || 0,
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (payload.images && payload.images.length > 0) {
|
|
for (const [idx, img] of payload.images.entries()) {
|
|
await tx.productImage.create({
|
|
data: {
|
|
productId: prod.id,
|
|
url: img.url,
|
|
order: img.order ?? idx,
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return prod;
|
|
});
|
|
|
|
return { success: true, product };
|
|
} catch (err: any) {
|
|
console.error("Create product error:", err);
|
|
return status(500, { success: false, message: err?.message || "Failed to create product" });
|
|
}
|
|
},
|
|
{
|
|
body: t.Any(),
|
|
detail: {
|
|
summary: "Create Product",
|
|
tags: ["products"],
|
|
},
|
|
}
|
|
)
|
|
.post(
|
|
"/bulk",
|
|
async ({ body, status, validateVariants }) => {
|
|
const payloadArray = body as any[];
|
|
if (!Array.isArray(payloadArray)) {
|
|
return status(400, { success: false, message: "Payload must be an array of products" });
|
|
}
|
|
|
|
try {
|
|
const createdProducts = await prisma.$transaction(async (tx) => {
|
|
const results = [];
|
|
|
|
for (const payload of payloadArray) {
|
|
if (payload.variants && Array.isArray(payload.variants)) {
|
|
validateVariants(payload.options || [], payload.variants);
|
|
}
|
|
|
|
let categoryId = payload.categoryId;
|
|
if (!categoryId && payload.categoryName) {
|
|
let cat = await tx.category.findUnique({ where: { name: payload.categoryName } });
|
|
if (!cat) {
|
|
cat = await tx.category.create({ data: { name: payload.categoryName } });
|
|
}
|
|
categoryId = cat.id;
|
|
}
|
|
|
|
let brandId = payload.brandId;
|
|
if (!brandId && payload.brandName) {
|
|
let brand = await tx.productBrand.findUnique({ where: { name: payload.brandName } });
|
|
if (!brand) {
|
|
brand = await tx.productBrand.create({ data: { name: payload.brandName } });
|
|
}
|
|
brandId = brand.id;
|
|
}
|
|
|
|
const prod = await tx.product.create({
|
|
data: {
|
|
categoryId: categoryId,
|
|
brandId: brandId,
|
|
name: payload.name || "New Product",
|
|
price: payload.price || 0,
|
|
description: payload.description || "",
|
|
details: payload.details || "",
|
|
options: payload.options || [],
|
|
}
|
|
});
|
|
|
|
if (payload.translations && typeof payload.translations === 'object') {
|
|
for (const [lang, trans] of Object.entries(payload.translations)) {
|
|
await tx.productTranslation.create({
|
|
data: {
|
|
productId: prod.id,
|
|
langCode: lang,
|
|
name: (trans as any).name || payload.name || "New Product",
|
|
description: (trans as any).description || payload.description || "",
|
|
details: (trans as any).details || payload.details || "",
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
await tx.productTranslation.create({
|
|
data: {
|
|
productId: prod.id,
|
|
langCode: 'th',
|
|
name: payload.name || "New Product",
|
|
description: payload.description || "",
|
|
details: payload.details || "",
|
|
}
|
|
});
|
|
}
|
|
|
|
if (payload.variants && payload.variants.length > 0) {
|
|
for (const v of payload.variants) {
|
|
await tx.productVariant.create({
|
|
data: {
|
|
productId: prod.id,
|
|
sku: v.sku || null,
|
|
barcode: v.barcode || null,
|
|
stock: v.stock || 0,
|
|
option1: v.option1 || "",
|
|
option2: v.option2 || "",
|
|
option3: v.option3 || "",
|
|
price: v.price || 0,
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (payload.images && payload.images.length > 0) {
|
|
for (const [idx, img] of payload.images.entries()) {
|
|
await tx.productImage.create({
|
|
data: {
|
|
productId: prod.id,
|
|
url: img.url,
|
|
order: img.order ?? idx,
|
|
}
|
|
});
|
|
}
|
|
}
|
|
results.push(prod);
|
|
}
|
|
return results;
|
|
});
|
|
|
|
return { success: true, products: createdProducts };
|
|
} catch (err: any) {
|
|
console.error("Bulk create products error:", err);
|
|
return status(500, { success: false, message: err?.message || "Failed to bulk create products" });
|
|
}
|
|
},
|
|
{
|
|
body: t.Array(t.Any()),
|
|
detail: {
|
|
summary: "Bulk Create Products",
|
|
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.brandId !== undefined) prodData.brandId = payload.brandId;
|
|
if (payload.price !== undefined) prodData.price = payload.price;
|
|
if (payload.options !== undefined) prodData.options = payload.options;
|
|
|
|
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: err?.message || "Failed to patch product" });
|
|
}
|
|
},
|
|
{
|
|
body: t.Any(),
|
|
detail: {
|
|
summary: "Partial Update Product",
|
|
tags: ["products"],
|
|
},
|
|
}
|
|
);
|