feat(cms): implement multi-language support across frontend and admin UI
This commit is contained in:
1 parent
730e2a7b48
commit
eac45dd4a7
62 files changed
+1377
-5116
No files matched your search
+258
-172
@@ -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])
|
||||
}
|
||||
Reference in new issue
Block a user