x
This commit is contained in:
1 parent
81ed9af406
commit
4536db6741
56 files changed
+1127
-198
No files matched your search
@@ -2,6 +2,20 @@ import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
|
||||
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.");
|
||||
for (const v of variants) {
|
||||
if (v.option1 && options[0] && !options[0].values.includes(v.option1)) {
|
||||
throw new Error(`Variant option1 '${v.option1}' is not found in options[0].values [${options[0].values.join(', ')}]`);
|
||||
}
|
||||
if (v.option2 && options[1] && !options[1].values.includes(v.option2)) {
|
||||
throw new Error(`Variant option2 '${v.option2}' is not found in options[1].values [${options[1].values.join(', ')}]`);
|
||||
}
|
||||
if (v.option3 && options[2] && !options[2].values.includes(v.option3)) {
|
||||
throw new Error(`Variant option3 '${v.option3}' is not found in options[2].values [${options[2].values.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
})
|
||||
.get("/brands", async () => {
|
||||
return await prisma.productBrand.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
@@ -177,13 +191,22 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
})
|
||||
.put(
|
||||
"/:id",
|
||||
async ({ params, body, status }) => {
|
||||
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 = {};
|
||||
@@ -196,48 +219,93 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
});
|
||||
|
||||
// 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,
|
||||
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
|
||||
}
|
||||
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,
|
||||
}
|
||||
});
|
||||
}
|
||||
} 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
|
||||
await tx.productImage.deleteMany({
|
||||
where: { productId: id },
|
||||
});
|
||||
if (images && Array.isArray(images)) {
|
||||
const existingImages = await tx.productImage.findMany({ where: { productId: id } });
|
||||
const incomingImgIds = images.map((img: any) => img.id).filter(Boolean);
|
||||
|
||||
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),
|
||||
}
|
||||
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;
|
||||
@@ -246,7 +314,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
return { success: true, product: updatedProduct };
|
||||
} catch (err: any) {
|
||||
console.error("Update product error:", err);
|
||||
return status(500, { success: false, message: "Failed to update product" });
|
||||
return status(500, { success: false, message: err?.message || "Failed to update product" });
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -283,7 +351,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
return { success: true, image: newImage };
|
||||
} catch (err: any) {
|
||||
console.error("Add product image error:", err);
|
||||
return status(500, { success: false, message: "Failed to add product image" });
|
||||
return status(500, { success: false, message: err?.message || "Failed to add product image" });
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -294,36 +362,71 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
)
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, status }) => {
|
||||
async ({ body, status, validateVariants }) => {
|
||||
const payload = body as any;
|
||||
try {
|
||||
const product = await prisma.product.create({
|
||||
data: {
|
||||
categoryId: payload.categoryId,
|
||||
brandId: payload.brandId,
|
||||
name: payload.name || "New Product",
|
||||
price: payload.price || 0,
|
||||
description: payload.description || "",
|
||||
details: payload.details || "",
|
||||
options: payload.options || [],
|
||||
if (payload.variants && Array.isArray(payload.variants)) {
|
||||
validateVariants(payload.options || [], payload.variants);
|
||||
}
|
||||
const product = await prisma.$transaction(async (tx) => {
|
||||
const prod = await tx.product.create({
|
||||
data: {
|
||||
categoryId: payload.categoryId,
|
||||
brandId: payload.brandId,
|
||||
name: payload.name || "New Product",
|
||||
price: payload.price || 0,
|
||||
description: payload.description || "",
|
||||
details: payload.details || "",
|
||||
options: payload.options || [],
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also create the TH translation by default
|
||||
await prisma.productTranslation.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
langCode: 'th',
|
||||
name: payload.name || "New Product",
|
||||
description: payload.description || "",
|
||||
details: payload.details || "",
|
||||
|
||||
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: "Failed to create product" });
|
||||
return status(500, { success: false, message: err?.message || "Failed to create product" });
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -402,7 +505,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
return { success: true, product: prod };
|
||||
} catch (err: any) {
|
||||
console.error("Patch product error:", err);
|
||||
return status(500, { success: false, message: "Failed to patch product" });
|
||||
return status(500, { success: false, message: err?.message || "Failed to patch product" });
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user