x
This commit is contained in:
1 parent
ed69a4b840
commit
992335a52b
22 files changed
+1064
-346
No files matched your search
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `scentId` on the `Product` table. All the data in the column will be lost.
|
||||
- You are about to drop the `Hero` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `HeroTranslation` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Scent` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `ScentTranslation` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `brandId` to the `Product` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "HeroTranslation" DROP CONSTRAINT "HeroTranslation_heroId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Product" DROP CONSTRAINT "Product_scentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ScentTranslation" DROP CONSTRAINT "ScentTranslation_scentId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Product_scentId_categoryId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" DROP COLUMN "scentId",
|
||||
ADD COLUMN "brandId" INTEGER NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Hero";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "HeroTranslation";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Scent";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "ScentTranslation";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProductBrand" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProductBrand_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProductBrandTranslation" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"brandId" INTEGER NOT NULL,
|
||||
"langCode" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProductBrandTranslation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProductBrand_name_key" ON "ProductBrand"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProductBrandTranslation_brandId_langCode_key" ON "ProductBrandTranslation"("brandId", "langCode");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProductBrandTranslation" ADD CONSTRAINT "ProductBrandTranslation_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "ProductBrand"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Product" ADD CONSTRAINT "Product_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "ProductBrand"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -9,25 +9,25 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
model Scent {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
model ProductBrand {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
products Product[]
|
||||
translations ScentTranslation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
translations ProductBrandTranslation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ScentTranslation {
|
||||
id Int @id @default(autoincrement())
|
||||
scentId Int
|
||||
scent Scent @relation(fields: [scentId], references: [id], onDelete: Cascade)
|
||||
model ProductBrandTranslation {
|
||||
id Int @id @default(autoincrement())
|
||||
brandId Int
|
||||
brand ProductBrand @relation(fields: [brandId], references: [id], onDelete: Cascade)
|
||||
langCode String
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([scentId, langCode])
|
||||
@@unique([brandId, langCode])
|
||||
}
|
||||
|
||||
model Category {
|
||||
@@ -60,16 +60,15 @@ model Product {
|
||||
name String?
|
||||
description String?
|
||||
details String?
|
||||
scentId Int
|
||||
scent Scent @relation(fields: [scentId], references: [id])
|
||||
brandId Int
|
||||
brand ProductBrand @relation(fields: [brandId], references: [id])
|
||||
options Json? @default("[]") // Expected shape: [{ name: "Color", values: ["Red", "Blue"] }, ...]
|
||||
variants ProductVariant[]
|
||||
images ProductImage[]
|
||||
reviews Review[]
|
||||
translations ProductTranslation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([scentId, categoryId])
|
||||
}
|
||||
|
||||
model ProductTranslation {
|
||||
@@ -96,15 +95,22 @@ model ProductImage {
|
||||
|
||||
model ProductVariant {
|
||||
id Int @id @default(autoincrement())
|
||||
size String
|
||||
sku String? @unique
|
||||
barcode String?
|
||||
price Float? @default(0)
|
||||
stock Int @default(0)
|
||||
|
||||
option1 String @default("") // Maps to Product.options[0]
|
||||
option2 String @default("") // Maps to Product.options[1]
|
||||
option3 String @default("") // Maps to Product.options[2]
|
||||
|
||||
productId Int
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
bomItems BomItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([productId, size])
|
||||
@@unique([productId, option1, option2, option3])
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -147,7 +153,6 @@ model CustomerAddress {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
||||
model Coupon {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
|
||||
@@ -11,7 +11,7 @@ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
const adapter = new PrismaPg(pool);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const scentNames = [
|
||||
const brandNames = [
|
||||
'ตะไคร้', 'ลาเวนเดอร์', 'วานิลลา', 'ซากุระ', 'มะลิ', 'ยูคาลิปตัส', 'เฟรชลาเวนเดอร์',
|
||||
'เปเปอร์มิ้น', 'โอเชี่ยน', 'เซเว่นซี', 'ดอกแก้ว', 'ดอกโมก', 'ลีลาวดี', 'ส้ม', 'มะนาว',
|
||||
'แคนตาลูป', 'แอปเปิ้ลแดง', 'แอปเปิ้ลเขียว', 'ดอกลิลลี่', 'โรสเมรี่', 'อัญชัน',
|
||||
@@ -65,7 +65,7 @@ async function main() {
|
||||
|
||||
// Auto-create with default template if not exists
|
||||
if (!fs.existsSync(descPath)) {
|
||||
const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
|
||||
const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{brand}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
|
||||
fs.writeFileSync(descPath, defaultDesc, 'utf8');
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ async function main() {
|
||||
}
|
||||
const detailPath = path.join(detailsDir, `${safeName}.txt`);
|
||||
if (!fs.existsSync(detailPath)) {
|
||||
const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{scent}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
|
||||
const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{brand}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
|
||||
fs.writeFileSync(detailPath, defaultDetail, 'utf8');
|
||||
}
|
||||
categoryConfigs[config.name] = categoryConfigs[config.name] || {};
|
||||
@@ -121,17 +121,17 @@ async function main() {
|
||||
}
|
||||
process.stdout.write('✅ Seeded Core Menus.\n');
|
||||
|
||||
// 1. Seed Scents & Categories
|
||||
const scents = [];
|
||||
for (const name of scentNames) {
|
||||
const s = await prisma.scent.upsert({
|
||||
// 1. Seed Brands & Categories
|
||||
const brands = [];
|
||||
for (const name of brandNames) {
|
||||
const s = await prisma.productBrand.upsert({
|
||||
where: { name },
|
||||
update: {},
|
||||
create: { name }
|
||||
});
|
||||
scents.push(s);
|
||||
brands.push(s);
|
||||
}
|
||||
process.stdout.write(`✅ Seeded ${scents.length} Scents.\n`);
|
||||
process.stdout.write(`✅ Seeded ${brands.length} Brands.\n`);
|
||||
|
||||
const categories = [];
|
||||
for (const config of categoryConfigs) {
|
||||
@@ -144,43 +144,49 @@ async function main() {
|
||||
}
|
||||
process.stdout.write(`✅ Seeded ${categories.length} Categories.\n`);
|
||||
|
||||
// 2. Cross Scents x Categories to create Products
|
||||
// 2. Cross Brands x Categories to create Products
|
||||
process.stdout.write('📦 Generating Products and Variants...\n');
|
||||
let productCount = 0;
|
||||
let variantCount = 0;
|
||||
|
||||
for (const scent of scents) {
|
||||
for (const brand of brands) {
|
||||
for (const category of categories) {
|
||||
const productName = `${scent.name} ${category.name}`;
|
||||
const productName = `${brand.name} ${category.name}`;
|
||||
|
||||
// Get category description template and replace {{scent}}
|
||||
let productDescription = categoryDescriptions[category.name] || `Product ${scent.name} in category ${category.name}`;
|
||||
productDescription = productDescription.replace(/{{scent}}/g, scent.name);
|
||||
// Get category description template and replace {{brand}}
|
||||
let productDescription = categoryDescriptions[category.name] || `Product ${brand.name} in category ${category.name}`;
|
||||
productDescription = productDescription.replace(/{{brand}}/g, brand.name);
|
||||
productDescription = productDescription.replace(/{{scent}}/g, brand.name);
|
||||
|
||||
let productDetails = category.detailsTemplate || '';
|
||||
productDetails = productDetails.replace(/{{scent}}/g, scent.name);
|
||||
productDetails = productDetails.replace(/{{brand}}/g, brand.name);
|
||||
productDetails = productDetails.replace(/{{scent}}/g, brand.name);
|
||||
|
||||
// Upsert Product
|
||||
const product = await prisma.product.upsert({
|
||||
where: {
|
||||
scentId_categoryId: {
|
||||
scentId: scent.id,
|
||||
categoryId: category.id
|
||||
}
|
||||
},
|
||||
update: {
|
||||
name: productName,
|
||||
description: productDescription,
|
||||
details: productDetails
|
||||
},
|
||||
create: {
|
||||
name: productName,
|
||||
scentId: scent.id,
|
||||
categoryId: category.id,
|
||||
description: productDescription,
|
||||
details: productDetails
|
||||
}
|
||||
// Upsert Product without unique constraint on brandId + categoryId
|
||||
let product = await prisma.product.findFirst({
|
||||
where: { brandId: brand.id, categoryId: category.id }
|
||||
});
|
||||
if (product) {
|
||||
product = await prisma.product.update({
|
||||
where: { id: product.id },
|
||||
data: {
|
||||
name: productName,
|
||||
description: productDescription,
|
||||
details: productDetails
|
||||
}
|
||||
});
|
||||
} else {
|
||||
product = await prisma.product.create({
|
||||
data: {
|
||||
name: productName,
|
||||
brandId: brand.id,
|
||||
categoryId: category.id,
|
||||
description: productDescription,
|
||||
details: productDetails,
|
||||
options: category.allowedSizes.length > 0 ? [{ name: "Size", values: category.allowedSizes }] : []
|
||||
}
|
||||
});
|
||||
}
|
||||
productCount++;
|
||||
|
||||
// 3. Create Default Product Image
|
||||
@@ -214,15 +220,20 @@ async function main() {
|
||||
const price = category.priceMap?.[size] ?? 0;
|
||||
await prisma.productVariant.upsert({
|
||||
where: {
|
||||
productId_size: {
|
||||
productId_option1_option2_option3: {
|
||||
productId: product.id,
|
||||
size: size
|
||||
option1: size,
|
||||
option2: "",
|
||||
option3: ""
|
||||
}
|
||||
},
|
||||
update: { price }, // อัปเดตราคาเมื่อ re-seed
|
||||
create: {
|
||||
productId: product.id,
|
||||
size: size,
|
||||
sku: `${brand.id}-${category.id}-${size}`.replace(/\s+/g, '-').toUpperCase(),
|
||||
option1: size,
|
||||
option2: "",
|
||||
option3: "",
|
||||
price: price
|
||||
}
|
||||
});
|
||||
@@ -234,7 +245,7 @@ async function main() {
|
||||
|
||||
process.stdout.write(`\n✨ Seeding Complete!\n`);
|
||||
process.stdout.write(`📊 Summary:\n`);
|
||||
process.stdout.write(` - Scents: ${scents.length}\n`);
|
||||
process.stdout.write(` - Brands: ${brands.length}\n`);
|
||||
process.stdout.write(` - Categories: ${categories.length}\n`);
|
||||
process.stdout.write(` - Products: ${productCount}\n`);
|
||||
process.stdout.write(` - Variants: ${variantCount}\n`);
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
--- seed.cjs 2026-06-23
|
||||
+++ seed.cjs.new 2026-06-23
|
||||
@@ -12,7 +12,7 @@
|
||||
const adapter = new PrismaPg(pool);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
-const scentNames = [
|
||||
+const brandNames = [
|
||||
'ตะไคร้', 'ลาเวนเดอร์', 'วานิลลา', 'ซากุระ', 'มะลิ', 'ยูคาลิปตัส', 'เฟรชลาเวนเดอร์',
|
||||
'เปเปอร์มิ้น', 'โอเชี่ยน', 'เซเว่นซี', 'ดอกแก้ว', 'ดอกโมก', 'ลีลาวดี', 'ส้ม', 'มะนาว',
|
||||
'แคนตาลูป', 'แอปเปิ้ลแดง', 'แอปเปิ้ลเขียว', 'ดอกลิลลี่', 'โรสเมรี่', 'อัญชัน',
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
// Auto-create with default template if not exists
|
||||
if (!fs.existsSync(descPath)) {
|
||||
- const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
|
||||
+ const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{brand}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
|
||||
fs.writeFileSync(descPath, defaultDesc, 'utf8');
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
}
|
||||
const detailPath = path.join(detailsDir, `${safeName}.txt`);
|
||||
if (!fs.existsSync(detailPath)) {
|
||||
- const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{scent}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
|
||||
+ const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{brand}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
|
||||
fs.writeFileSync(detailPath, defaultDetail, 'utf8');
|
||||
}
|
||||
categoryConfigs[config.name] = categoryConfigs[config.name] || {};
|
||||
@@ -122,17 +122,17 @@
|
||||
process.stdout.write('✅ Seeded Core Menus.\n');
|
||||
|
||||
- // 1. Seed Scents & Categories
|
||||
- const scents = [];
|
||||
- for (const name of scentNames) {
|
||||
- const s = await prisma.scent.upsert({
|
||||
+ // 1. Seed Brands & Categories
|
||||
+ const brands = [];
|
||||
+ for (const name of brandNames) {
|
||||
+ const s = await prisma.productBrand.upsert({
|
||||
where: { name },
|
||||
update: {},
|
||||
create: { name }
|
||||
});
|
||||
- scents.push(s);
|
||||
+ brands.push(s);
|
||||
}
|
||||
- process.stdout.write(`✅ Seeded ${scents.length} Scents.\n`);
|
||||
+ process.stdout.write(`✅ Seeded ${brands.length} Brands.\n`);
|
||||
|
||||
const categories = [];
|
||||
for (const config of categoryConfigs) {
|
||||
@@ -145,43 +145,46 @@
|
||||
}
|
||||
process.stdout.write(`✅ Seeded ${categories.length} Categories.\n`);
|
||||
|
||||
- // 2. Cross Scents x Categories to create Products
|
||||
+ // 2. Cross Brands x Categories to create Products
|
||||
process.stdout.write('📦 Generating Products and Variants...\n');
|
||||
let productCount = 0;
|
||||
let variantCount = 0;
|
||||
|
||||
- for (const scent of scents) {
|
||||
+ for (const brand of brands) {
|
||||
for (const category of categories) {
|
||||
- const productName = `${scent.name} ${category.name}`;
|
||||
+ const productName = `${brand.name} ${category.name}`;
|
||||
|
||||
- // Get category description template and replace {{scent}}
|
||||
- let productDescription = categoryDescriptions[category.name] || `Product ${scent.name} in category ${category.name}`;
|
||||
- productDescription = productDescription.replace(/{{scent}}/g, scent.name);
|
||||
+ // Get category description template and replace {{brand}}
|
||||
+ let productDescription = categoryDescriptions[category.name] || `Product ${brand.name} in category ${category.name}`;
|
||||
+ productDescription = productDescription.replace(/{{brand}}/g, brand.name);
|
||||
+ // fallback for old template files that might still have {{scent}}
|
||||
+ productDescription = productDescription.replace(/{{scent}}/g, brand.name);
|
||||
|
||||
let productDetails = category.detailsTemplate || '';
|
||||
- productDetails = productDetails.replace(/{{scent}}/g, scent.name);
|
||||
+ productDetails = productDetails.replace(/{{brand}}/g, brand.name);
|
||||
+ productDetails = productDetails.replace(/{{scent}}/g, brand.name);
|
||||
|
||||
- // Upsert Product
|
||||
- const product = await prisma.product.upsert({
|
||||
- where: {
|
||||
- scentId_categoryId: {
|
||||
- scentId: scent.id,
|
||||
- categoryId: category.id
|
||||
- }
|
||||
- },
|
||||
- update: {
|
||||
- name: productName,
|
||||
- description: productDescription,
|
||||
- details: productDetails
|
||||
- },
|
||||
- create: {
|
||||
- name: productName,
|
||||
- scentId: scent.id,
|
||||
- categoryId: category.id,
|
||||
- description: productDescription,
|
||||
- details: productDetails
|
||||
- }
|
||||
- });
|
||||
+ // Upsert Product manually since we don't have a unique constraint on brandId + categoryId
|
||||
+ let product = await prisma.product.findFirst({
|
||||
+ where: { brandId: brand.id, categoryId: category.id }
|
||||
+ });
|
||||
+ if (product) {
|
||||
+ product = await prisma.product.update({
|
||||
+ where: { id: product.id },
|
||||
+ data: {
|
||||
+ name: productName,
|
||||
+ description: productDescription,
|
||||
+ details: productDetails
|
||||
+ }
|
||||
+ });
|
||||
+ } else {
|
||||
+ product = await prisma.product.create({
|
||||
+ data: {
|
||||
+ name: productName,
|
||||
+ brandId: brand.id,
|
||||
+ categoryId: category.id,
|
||||
+ description: productDescription,
|
||||
+ details: productDetails
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
productCount++;
|
||||
|
||||
// 3. Create Default Product Image
|
||||
@@ -235,7 +238,7 @@
|
||||
|
||||
process.stdout.write(`\n✨ Seeding Complete!\n`);
|
||||
process.stdout.write(`📊 Summary:\n`);
|
||||
- process.stdout.write(` - Scents: ${scents.length}\n`);
|
||||
+ process.stdout.write(` - Brands: ${brands.length}\n`);
|
||||
process.stdout.write(` - Categories: ${categories.length}\n`);
|
||||
process.stdout.write(` - Products: ${productCount}\n`);
|
||||
process.stdout.write(` - Variants: ${variantCount}\n`);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -29,7 +29,7 @@ import { mediaRoute } from "./routes/media";
|
||||
import { contactsRoute } from "./routes/contacts";
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_scent";
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_brand";
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
|
||||
const isPrivateLanHostname = (hostname: string) => {
|
||||
|
||||
@@ -50,7 +50,7 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
||||
translations: true,
|
||||
products: {
|
||||
include: {
|
||||
scent: true,
|
||||
brand: true,
|
||||
category: { include: { translations: true } },
|
||||
translations: true,
|
||||
variants: { orderBy: { price: "asc" as const } },
|
||||
@@ -98,7 +98,7 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
||||
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 || '',
|
||||
name: trans?.name || p.brand?.name || '',
|
||||
description: trans?.description,
|
||||
details: trans?.details,
|
||||
category: { ...p.category, name: resolveCatName(p.category, lang) },
|
||||
|
||||
@@ -559,8 +559,17 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
const id = Number(sectionId);
|
||||
const { type, order, payload, isActive } = body as any;
|
||||
|
||||
if (payload && type) {
|
||||
const validationError = validateSectionPayload(type, payload);
|
||||
if (payload) {
|
||||
let validationType = type;
|
||||
if (!validationType) {
|
||||
const existingSection = await prisma.pageSection.findUnique({ where: { id } });
|
||||
if (!existingSection) {
|
||||
set.status = 404;
|
||||
return { error: "Section not found" };
|
||||
}
|
||||
validationType = existingSection.type;
|
||||
}
|
||||
const validationError = validateSectionPayload(validationType, payload);
|
||||
if (validationError) {
|
||||
set.status = 400;
|
||||
return { error: validationError };
|
||||
|
||||
@@ -2,15 +2,15 @@ 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({
|
||||
.get("/brands", async () => {
|
||||
return await prisma.productBrand.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
include: { translations: true }
|
||||
});
|
||||
})
|
||||
.patch("/scents/:id/translations", async ({ params, body, status }) => {
|
||||
.patch("/brands/:id/translations", async ({ params, body, status }) => {
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id)) return status(400, { message: "Invalid scent ID" });
|
||||
if (isNaN(id)) return status(400, { message: "Invalid brand ID" });
|
||||
|
||||
const payload = body as any;
|
||||
const results: any[] = [];
|
||||
@@ -20,34 +20,34 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
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 } }
|
||||
const existing = await prisma.productBrandTranslation.findUnique({
|
||||
where: { brandId_langCode: { brandId: id, langCode: lang } }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
const updated = await prisma.scentTranslation.update({
|
||||
const updated = await prisma.productBrandTranslation.update({
|
||||
where: { id: existing.id },
|
||||
data: { name },
|
||||
});
|
||||
results.push(updated);
|
||||
} else {
|
||||
const created = await prisma.scentTranslation.create({
|
||||
data: { scentId: id, langCode: lang, name },
|
||||
const created = await prisma.productBrandTranslation.create({
|
||||
data: { brandId: id, langCode: lang, name },
|
||||
});
|
||||
results.push(created);
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.scent.findUnique({
|
||||
const updated = await prisma.productBrand.findUnique({
|
||||
where: { id },
|
||||
include: { translations: true },
|
||||
});
|
||||
|
||||
return { success: true, translations: results, scent: updated };
|
||||
return { success: true, translations: results, brand: updated };
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
|
||||
const scentId = query.scentId ? parseInt(query.scentId) : undefined;
|
||||
const brandId = query.brandId ? parseInt(query.brandId) : undefined;
|
||||
const search = query.search?.trim() || undefined;
|
||||
const lang = query.lang || 'th';
|
||||
|
||||
@@ -58,11 +58,11 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
|
||||
const where = {
|
||||
...(categoryId && { categoryId }),
|
||||
...(scentId && { scentId }),
|
||||
...(brandId && { brandId }),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ translations: { some: { name: { contains: search, mode: "insensitive" as const } } } },
|
||||
{ scent: { name: { contains: search, mode: "insensitive" as const } } },
|
||||
{ brand: { name: { contains: search, mode: "insensitive" as const } } },
|
||||
{ category: { name: { contains: search, mode: "insensitive" as const } } },
|
||||
],
|
||||
}),
|
||||
@@ -73,7 +73,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
where,
|
||||
include: {
|
||||
category: { include: { translations: true } },
|
||||
scent: true,
|
||||
brand: true,
|
||||
translations: true,
|
||||
variants: {
|
||||
include: {
|
||||
@@ -106,9 +106,9 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
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,
|
||||
name: trans?.name || p.name || '',
|
||||
description: trans?.description || p.description,
|
||||
details: trans?.details || p.details,
|
||||
category: { ...p.category, name: catTrans?.name || p.category.name }
|
||||
};
|
||||
}
|
||||
@@ -139,7 +139,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
where: { id },
|
||||
include: {
|
||||
category: { include: { translations: true } },
|
||||
scent: true,
|
||||
brand: true,
|
||||
translations: true,
|
||||
variants: {
|
||||
include: {
|
||||
@@ -168,9 +168,9 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
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,
|
||||
name: trans?.name || product.name || '',
|
||||
description: trans?.description || product.description,
|
||||
details: trans?.details || product.details,
|
||||
category: { ...product.category, name: catTrans?.name || product.category.name }
|
||||
};
|
||||
}
|
||||
@@ -186,9 +186,13 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
try {
|
||||
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: { categoryId },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
// 2. Sync Variants
|
||||
@@ -201,8 +205,13 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
await tx.productVariant.create({
|
||||
data: {
|
||||
productId: id,
|
||||
size: v.size,
|
||||
price: v.price,
|
||||
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,
|
||||
@@ -248,6 +257,83 @@ export const productsRoute = new Elysia({ prefix: "/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: "Failed to add product image" });
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
url: t.String()
|
||||
})
|
||||
}
|
||||
)
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, status }) => {
|
||||
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 || [],
|
||||
}
|
||||
});
|
||||
|
||||
// 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 || "",
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, product };
|
||||
} catch (err: any) {
|
||||
console.error("Create product error:", err);
|
||||
return status(500, { success: false, message: "Failed to create product" });
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Any(),
|
||||
detail: {
|
||||
summary: "Create Product",
|
||||
tags: ["products"],
|
||||
},
|
||||
}
|
||||
)
|
||||
.patch(
|
||||
"/:id",
|
||||
async ({ params, body, status }) => {
|
||||
@@ -258,8 +344,9 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
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.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 },
|
||||
|
||||
Reference in new issue
Block a user