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

-11
View File
@@ -1,11 +0,0 @@
import * as line from '@line/bot-sdk';
const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN || '';
const channelSecret = process.env.LINE_CHANNEL_SECRET || '';
export const lineConfig = {
channelAccessToken,
channelSecret,
};
export const lineClient = new line.messagingApi.MessagingApiClient({
channelAccessToken,
});
//# sourceMappingURL=lineClient.js.map
-554
View File
@@ -1,554 +0,0 @@
import prisma from "./prisma";
import { lineClient } from "./lineClient";
export async function sendOrderCreatedNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { items: true, customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const settings = await prisma.storeSettings.findFirst();
const bankName = settings?.bankName || "KASIKORNBANK (KBANK)";
const bankAccountNumber = settings?.bankAccountNumber || "012-3-45678-9";
const bankAccountName = settings?.bankAccountName || "HomAroma Co., Ltd.";
const itemContents = order.items.map(item => ({
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `${item.productName}${item.variantSize ? ` (${item.variantSize})` : ""} x ${item.quantity}`,
size: "sm",
color: "#333333",
flex: 3,
wrap: true
},
{
type: "text",
text: `฿${item.priceAtPurchase * item.quantity}`,
size: "sm",
color: "#1C3F3A",
weight: "bold",
align: "end",
flex: 1
}
]
}));
const flexMessage = {
type: "flex",
altText: `HomAroma: คำสั่งซื้อ #${order.id} ได้รับแล้ว กรุณาชำระเงิน`,
contents: {
type: "bubble",
header: {
type: "box",
layout: "vertical",
backgroundColor: "#1C3F3A",
contents: [
{
type: "text",
text: "HomAroma Official",
color: "#D4B16A",
weight: "bold",
size: "xs"
},
{
type: "text",
text: "ได้รับคำสั่งซื้อของคุณแล้ว",
color: "#FFFFFF",
weight: "bold",
size: "md",
margin: "sm"
}
]
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `หมายเลขออเดอร์: #HM-${order.id}`,
weight: "bold",
size: "sm",
color: "#333333"
},
{
type: "separator",
margin: "md"
},
{
type: "box",
layout: "vertical",
margin: "md",
spacing: "sm",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "รายการสินค้า",
size: "xs",
color: "#888888",
flex: 3
},
{
type: "text",
text: "ยอดรวม",
size: "xs",
color: "#888888",
align: "end",
flex: 1
}
]
},
...itemContents
]
},
{
type: "separator",
margin: "md"
},
{
type: "box",
layout: "horizontal",
margin: "md",
contents: [
{
type: "text",
text: "ยอดชำระสุทธิ (รวมค่าส่ง)",
size: "sm",
color: "#555555",
weight: "bold",
flex: 3
},
{
type: "text",
text: `฿${order.totalAmount}`,
size: "sm",
color: "#1C3F3A",
weight: "bold",
align: "end",
flex: 1
}
]
},
{
type: "separator",
margin: "md"
},
{
type: "box",
layout: "vertical",
margin: "md",
contents: [
{
type: "text",
text: "🏦 ช่องทางการชำระเงินโอนเงิน:",
size: "xs",
color: "#555555",
weight: "bold"
},
{
type: "text",
text: `${bankName} (${bankAccountNumber})`,
size: "xs",
color: "#1C3F3A",
margin: "xs",
weight: "bold"
},
{
type: "text",
text: `ชื่อบัญชี: ${bankAccountName}`,
size: "xs",
color: "#555555"
}
]
}
]
},
footer: {
type: "box",
layout: "vertical",
spacing: "sm",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "แนบสลิปชำระเงิน",
"uri": `https://spa.homaroma.shop/order-success?id=${order.id}`
}
},
{
type: "button",
style: "link",
height: "sm",
action: {
"type": "uri",
"label": "ติดตามสถานะออเดอร์",
"uri": "https://spa.homaroma.shop/track-order"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] OrderCreated notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send OrderCreated notification for Order #${orderId}:`, err);
}
}
export async function sendPaymentConfirmedNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const flexMessage = {
type: "flex",
altText: "HomAroma: ได้รับการชำระเงินเรียบร้อยแล้ว",
contents: {
type: "bubble",
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "vertical",
alignItems: "center",
contents: [
{
type: "text",
text: "✨ ชำระเงินสำเร็จ ✨",
weight: "bold",
size: "xl",
color: "#1C3F3A"
},
{
type: "text",
text: `ขอขอบคุณสำหรับคำสั่งซื้อออเดอร์ #HM-${order.id}`,
size: "xs",
color: "#888888",
margin: "xs"
}
]
},
{
type: "box",
layout: "vertical",
backgroundColor: "#F4F6F5",
margin: "lg",
paddingAll: "md",
cornerRadius: "md",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "ได้รับแต้มครั้งนี้",
size: "sm",
color: "#555555"
},
{
type: "text",
text: `+${order.pointsEarned} แต้ม`,
weight: "bold",
size: "sm",
color: "#1C3F3A",
align: "end"
}
]
},
{
type: "box",
layout: "horizontal",
margin: "xs",
contents: [
{
type: "text",
text: "แต้มสะสมรวมล่าสุด",
size: "sm",
color: "#555555"
},
{
type: "text",
text: `${order.customer.points} แต้ม`,
weight: "bold",
size: "sm",
color: "#D4B16A",
align: "end"
}
]
}
]
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "แลกของรางวัล & คูปองสะสม",
"uri": "https://spa.homaroma.shop/profile"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] PaymentConfirmed notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send PaymentConfirmed notification for Order #${orderId}:`, err);
}
}
export async function sendOrderShippedNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const flexMessage = {
type: "flex",
altText: "HomAroma: สินค้าของคุณอยู่ระหว่างจัดส่ง",
contents: {
type: "bubble",
header: {
type: "box",
layout: "vertical",
backgroundColor: "#1C3F3A",
contents: [
{
type: "text",
text: "ออเดอร์ของคุณกำลังเดินทาง 🚚",
color: "#FFFFFF",
weight: "bold",
size: "md"
}
]
},
body: {
type: "box",
layout: "vertical",
spacing: "md",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "หมายเลขออเดอร์:",
size: "sm",
color: "#888888",
flex: 2
},
{
type: "text",
text: `#HM-${order.id}`,
size: "sm",
color: "#333333",
weight: "bold",
flex: 3
}
]
},
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "หมายเลขพัสดุ:",
size: "sm",
color: "#888888",
flex: 2
},
{
type: "text",
text: order.trackingNo || "อยู่ระหว่างเข้ารับพัสดุ",
size: "sm",
color: "#1C3F3A",
weight: "bold",
flex: 3
}
]
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "เช็คสถานะการจัดส่ง",
"uri": "https://spa.homaroma.shop/track-order"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] OrderShipped notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send OrderShipped notification for Order #${orderId}:`, err);
}
}
export async function sendOrderCancelledNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const flexMessage = {
type: "flex",
altText: "HomAroma: คำสั่งซื้อของคุณถูกยกเลิกแล้ว",
contents: {
type: "bubble",
header: {
type: "box",
layout: "vertical",
backgroundColor: "#8C2A2A",
contents: [
{
type: "text",
text: "คำสั่งซื้อถูกยกเลิก 🚫",
color: "#FFFFFF",
weight: "bold",
size: "md"
}
]
},
body: {
type: "box",
layout: "vertical",
spacing: "md",
contents: [
{
type: "text",
text: `คำสั่งซื้อหมายเลข #HM-${order.id} ได้ถูกยกเลิกแล้ว เนื่องจากเกินกำหนดเวลาชำระเงิน หรือสิทธิประโยชน์บางประการ หรือตามความประสงค์ของคุณ`,
size: "sm",
color: "#333333",
wrap: true
},
...(order.pointsUsed > 0 ? [
{
type: "text",
text: `* ระบบได้ทำการคืนคะแนนสะสมจำนวน ${order.pointsUsed} แต้มเข้าบัญชีของคุณเรียบร้อยแล้ว`,
size: "xs",
color: "#8C2A2A",
weight: "bold",
margin: "md"
}
] : [])
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "ช้อปต่ออีกครั้ง",
"uri": "https://spa.homaroma.shop/"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] OrderCancelled notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send OrderCancelled notification for Order #${orderId}:`, err);
}
}
export async function markOrderAsPaid(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId }
});
if (!order) {
console.log(`[Payment] Order #${orderId} not found in markOrderAsPaid.`);
return;
}
if (order.status === "PAID") {
console.log(`[Payment] Order #${orderId} is already PAID.`);
return;
}
// 1. Update status to PAID
await prisma.order.update({
where: { id: orderId },
data: { status: "PAID" }
});
// 2. Grant points
if (order.customerId && order.pointsEarned > 0) {
await prisma.customer.update({
where: { id: order.customerId },
data: { points: { increment: order.pointsEarned } }
});
console.log(`[Payment] Granted ${order.pointsEarned} points to Customer #${order.customerId} for Order #${orderId}`);
}
// 3. Send payment notification
await sendPaymentConfirmedNotification(orderId);
}
catch (err) {
console.error(`[Payment] Error in markOrderAsPaid for Order #${orderId}:`, err);
}
}
//# sourceMappingURL=notifications.js.map
-9
View File
@@ -1,9 +0,0 @@
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import dotenv from "dotenv";
dotenv.config();
const connectionString = process.env.DATABASE_URL;
const adapter = new PrismaPg({ connectionString });
const prisma = new PrismaClient({ adapter });
export default prisma;
//# sourceMappingURL=prisma.js.map