feat(cms): implement multi-language support across frontend and admin UI

This commit is contained in:
Your Name committed 2026-06-14 09:00:57 -04:00
1 parent 730e2a7b48
commit eac45dd4a7
62 files changed
+1377 -5116

No files matched your search

+258 -172
View File
@@ -10,11 +10,24 @@ datasource db {
}
model Scent {
id Int @id @default(autoincrement())
name String @unique
products Product[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
name String @unique
products Product[]
translations ScentTranslation[]
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)
langCode String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([scentId, langCode])
}
model Category {
@@ -40,21 +53,21 @@ model CategoryTranslation {
}
model Product {
id Int @id @default(autoincrement())
price Float? @default(0)
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
name String?
description String?
details String?
scentId Int
scent Scent @relation(fields: [scentId], references: [id])
variants ProductVariant[]
images ProductImage[]
reviews Review[]
id Int @id @default(autoincrement())
price Float? @default(0)
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
name String?
description String?
details String?
scentId Int
scent Scent @relation(fields: [scentId], references: [id])
variants ProductVariant[]
images ProductImage[]
reviews Review[]
translations ProductTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([scentId, categoryId])
}
@@ -74,11 +87,11 @@ model ProductTranslation {
}
model ProductImage {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
url String
order Int @default(0)
order Int @default(0)
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
}
model ProductVariant {
@@ -124,33 +137,49 @@ model CustomerAddress {
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
recipientName String
phone String
addressLine String // บ้านเลขที่ หมู่บ้าน ถนน ซอย
subdistrict String // แขวง/ตำบล
district String // เขต/อำเภอ
province String // จังหวัด
zipcode String // รหัสไปรษณีย์
addressLine String // บ้านเลขที่ หมู่บ้าน ถนน ซอย
subdistrict String // แขวง/ตำบล
district String // เขต/อำเภอ
province String // จังหวัด
zipcode String // รหัสไปรษณีย์
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Hero {
id Int @id @default(autoincrement())
tagline String
title String
description String
imageUrl String
buttonText String @default("Explore Collection")
buttonLink String @default("/shop")
isActive Boolean @default(true)
translations HeroTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HeroTranslation {
id Int @id @default(autoincrement())
tagline String
title String
description String
imageUrl String
buttonText String @default("Explore Collection")
buttonLink String @default("/shop")
isActive Boolean @default(true)
heroId Int
hero Hero @relation(fields: [heroId], references: [id], onDelete: Cascade)
langCode String
tagline String?
title String?
description String?
buttonText String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([heroId, langCode])
}
model Coupon {
id Int @id @default(autoincrement())
code String @unique
type String // "PERCENTAGE", "FIXED_AMOUNT", "FREE_SHIPPING"
type String // "PERCENTAGE", "FIXED_AMOUNT", "FREE_SHIPPING"
value Float
minPurchase Float @default(0)
maxDiscount Float?
@@ -177,21 +206,38 @@ model Promotion {
}
model StoreSettings {
id Int @id @default(1)
shippingFee Float @default(35)
bankName String @default("KASIKORNBANK (KBANK)")
bankAccountName String @default("HomAroma Co., Ltd.")
bankAccountNumber String @default("012-3-45678-9")
pointsEarnRate Float @default(1.0)
facebookUrl String?
footerDescription String?
id Int @id @default(1)
shippingFee Float @default(35)
bankName String @default("KASIKORNBANK (KBANK)")
bankAccountName String @default("HomAroma Co., Ltd.")
bankAccountNumber String @default("012-3-45678-9")
pointsEarnRate Float @default(1.0)
facebookUrl String?
footerDescription String?
instagramUrl String?
tiktokUrl String?
newsletterTitle String?
newsletterDescription String?
newsletterPlaceholder String?
newsletterButtonText String?
updatedAt DateTime @updatedAt
translations StoreSettingsTranslation[]
updatedAt DateTime @updatedAt
}
model StoreSettingsTranslation {
id Int @id @default(autoincrement())
storeSettingsId Int
storeSettings StoreSettings @relation(fields: [storeSettingsId], references: [id], onDelete: Cascade)
langCode String
footerDescription String?
newsletterTitle String?
newsletterDescription String?
newsletterPlaceholder String?
newsletterButtonText String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([storeSettingsId, langCode])
}
model NewsletterSubscriber {
@@ -202,43 +248,43 @@ model NewsletterSubscriber {
}
model Order {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
customerName String
customerEmail String
customerPhone String
shippingAddress String
subtotal Float @default(0)
subtotal Float @default(0)
promoLevel1Name String?
promoLevel1Disc Float @default(0)
promoLevel1Disc Float @default(0)
promoLevel2Name String?
promoLevel2Disc Float @default(0)
couponCode String?
discountAmount Float @default(0)
pointsUsed Int @default(0)
pointsDiscount Float @default(0)
pointsEarned Int @default(0)
shippingFee Float @default(35)
totalAmount Float
status String @default("PENDING_PAYMENT")
paymentSlipUrl String?
trackingNo String?
customerId Int?
customer Customer? @relation(fields: [customerId], references: [id])
items OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
promoLevel2Disc Float @default(0)
couponCode String?
discountAmount Float @default(0)
pointsUsed Int @default(0)
pointsDiscount Float @default(0)
pointsEarned Int @default(0)
shippingFee Float @default(35)
totalAmount Float
status String @default("PENDING_PAYMENT")
paymentSlipUrl String?
trackingNo String?
customerId Int?
customer Customer? @relation(fields: [customerId], references: [id])
items OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OrderItem {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
orderId Int
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
productId Int
variantId Int?
quantity Int
@@ -249,14 +295,28 @@ model OrderItem {
}
model Article {
id Int @id @default(autoincrement())
slug String @unique
title String
coverImage String?
content String
isPublished Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
slug String @unique
title String
coverImage String?
content String
isPublished Boolean @default(false)
translations ArticleTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ArticleTranslation {
id Int @id @default(autoincrement())
articleId Int
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
langCode String
title String?
content String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([articleId, langCode])
}
model TiktokIncome {
@@ -268,40 +328,40 @@ model TiktokIncome {
totalSettlementAmount Float
totalRevenue Float
totalFees Float
rawData Json // Stores all other 50+ columns
rawData Json // Stores all other 50+ columns
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model TiktokProduct {
id String @id // Product ID
productName String
status String
gmv Float
itemsSold Int
orders Int
rawData Json // Stores remaining tab/live/video metrics
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id // Product ID
productName String
status String
gmv Float
itemsSold Int
orders Int
rawData Json // Stores remaining tab/live/video metrics
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model TiktokProductSku {
id String @id // SKU ID
productId String
skuName String
status String
gmv Float
skuOrders Int
itemsSold Int
rawData Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id // SKU ID
productId String
skuName String
status String
gmv Float
skuOrders Int
itemsSold Int
rawData Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model RawMaterial {
id Int @id @default(autoincrement())
name String @unique
unit String // e.g., 'kg', 'liter', 'piece'
unit String // e.g., 'kg', 'liter', 'piece'
pricePerUnit Float
bomItems BomItem[]
createdAt DateTime @default(now())
@@ -357,12 +417,12 @@ model ShopeeOrderItem {
// ── TikTok Orders ──────────────────────────────────────────────────────────────
model TiktokOrder {
id Int @id @default(autoincrement())
orderId String @unique
shop String @default("hom-aroma")
status String // Order Status (Thai)
subStatus String? // Order Substatus
orderType String? // Normal / Pre-order
id Int @id @default(autoincrement())
orderId String @unique
shop String @default("hom-aroma")
status String // Order Status (Thai)
subStatus String? // Order Substatus
orderType String? // Normal / Pre-order
cancelBy String?
cancelReason String?
fulfillmentType String?
@@ -373,11 +433,11 @@ model TiktokOrder {
buyerUsername String?
paymentMethod String?
// Financials (order-level)
orderAmount Float @default(0) // Total paid by buyer
refundAmount Float @default(0)
shippingFee Float @default(0) // Shipping Fee After Discount
originalShipping Float @default(0)
taxes Float @default(0)
orderAmount Float @default(0) // Total paid by buyer
refundAmount Float @default(0)
shippingFee Float @default(0) // Shipping Fee After Discount
originalShipping Float @default(0)
taxes Float @default(0)
// Geography
province String?
district String?
@@ -390,9 +450,9 @@ model TiktokOrder {
deliveredTime String?
cancelledTime String?
items TiktokOrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items TiktokOrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([shop])
@@index([status])
@@ -400,29 +460,29 @@ model TiktokOrder {
}
model TiktokOrderItem {
id Int @id @default(autoincrement())
orderId String
order TiktokOrder @relation(fields: [orderId], references: [orderId], onDelete: Cascade)
skuId String?
sellerSku String?
productName String
variation String?
quantity Int @default(1)
skuQtyReturn Int @default(0)
unitOriginalPrice Float @default(0)
subtotalBeforeDiscount Float @default(0)
platformDiscount Float @default(0)
sellerDiscount Float @default(0)
subtotalAfterDiscount Float @default(0) // Revenue ระดับ SKU
productCategory String?
weight Float @default(0)
id Int @id @default(autoincrement())
orderId String
order TiktokOrder @relation(fields: [orderId], references: [orderId], onDelete: Cascade)
skuId String?
sellerSku String?
productName String
variation String?
quantity Int @default(1)
skuQtyReturn Int @default(0)
unitOriginalPrice Float @default(0)
subtotalBeforeDiscount Float @default(0)
platformDiscount Float @default(0)
sellerDiscount Float @default(0)
subtotalAfterDiscount Float @default(0) // Revenue ระดับ SKU
productCategory String?
weight Float @default(0)
}
// ── Lazada Orders ──────────────────────────────────────────────────────────────
model LazadaOrder {
id Int @id @default(autoincrement())
orderNumber String @unique
shop String @default("hom-aroma")
id Int @id @default(autoincrement())
orderNumber String @unique
shop String @default("hom-aroma")
createTime String?
updateTime String?
deliveredDate String?
@@ -431,16 +491,16 @@ model LazadaOrder {
shippingPostCode String?
shippingRegion String?
payMethod String?
status String? // Based on items or first item
status String? // Based on items or first item
trackingCode String?
shippingProvider String?
// Aggregate from items
orderAmount Float @default(0) // sum(paidPrice)
shippingFee Float @default(0) // sum(shippingFee)
items LazadaOrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orderAmount Float @default(0) // sum(paidPrice)
shippingFee Float @default(0) // sum(shippingFee)
items LazadaOrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([shop])
@@index([status])
@@ -448,57 +508,70 @@ model LazadaOrder {
}
model LazadaOrderItem {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
orderNumber String
order LazadaOrder @relation(fields: [orderNumber], references: [orderNumber], onDelete: Cascade)
orderItemId String @unique
order LazadaOrder @relation(fields: [orderNumber], references: [orderNumber], onDelete: Cascade)
orderItemId String @unique
sellerSku String?
lazadaSku String?
itemName String
variation String?
unitPrice Float @default(0)
paidPrice Float @default(0)
sellerDiscountTotal Float @default(0)
platformDiscountTotal Float @default(0)
shippingFee Float @default(0)
unitPrice Float @default(0)
paidPrice Float @default(0)
sellerDiscountTotal Float @default(0)
platformDiscountTotal Float @default(0)
shippingFee Float @default(0)
status String?
}
// ── Reviews ────────────────────────────────────────────────────────────────
model Review {
id Int @id @default(autoincrement())
rating Int @default(5)
id Int @id @default(autoincrement())
rating Int @default(5)
comment String?
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
customerId Int?
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
customerName String?
isPublished Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isPublished Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
@@index([customerId])
}
model Page {
id Int @id @default(autoincrement())
slug String @unique // e.g., "home", "about-us"
title String
sections PageSection[]
isPublished Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
slug String @unique // e.g., "home", "about-us"
title String
sections PageSection[]
translations PageTranslation[]
isPublished Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PageTranslation {
id Int @id @default(autoincrement())
pageId Int
page Page @relation(fields: [pageId], references: [id], onDelete: Cascade)
langCode String
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([pageId, langCode])
}
model PageSection {
id Int @id @default(autoincrement())
pageId Int
page Page @relation(fields: [pageId], references: [id], onDelete: Cascade)
type String // e.g., "HERO", "CATEGORIES", "PRODUCTS", "BANNER", "TEXT"
order Int // Display order
payload Json // Dynamic JSON payload
type String // e.g., "HERO", "CATEGORIES", "PRODUCTS", "BANNER", "TEXT"
order Int // Display order
payload Json // Dynamic JSON payload
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -520,12 +593,25 @@ model Menu {
}
model MenuItem {
id Int @id @default(autoincrement())
menuId Int
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
label String
url String
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
menuId Int
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
label String
url String
order Int @default(0)
translations MenuItemTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MenuItemTranslation {
id Int @id @default(autoincrement())
menuItemId Int
menuItem MenuItem @relation(fields: [menuItemId], references: [id], onDelete: Cascade)
langCode String
label String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([menuItemId, langCode])
}