This commit is contained in:
Your Name committed 2026-10-08 08:08:37 +07:00
1 parent d13dedd215
commit dba82b26a8
17 files changed
+399 -43

No files matched your search

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "OrderItem" ADD COLUMN "translations" JSONB;
+2
View File
@@ -432,8 +432,10 @@ model OrderItem {
productName String
variantName String?
productImage String?
translations Json?
}
model Article {
id Int @id @default(autoincrement())
slug String @unique
+76 -6
View File
@@ -15,7 +15,7 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
include: { items: true, customer: true }
});
})
.get("/:id", async ({ params: { id }, set }) => {
.get("/:id", async ({ params: { id }, query, set }) => {
const order = await prisma.order.findUnique({
where: { id: Number(id) },
include: { items: true, customer: true }
@@ -24,6 +24,42 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
set.status = 404;
return { error: "Order not found" };
}
const lang = (query as any)?.lang;
if (lang && lang !== 'all') {
const missingProductIds = order.items
.filter((item: any) => !item.translations || !(item.translations as any)?.name)
.map((item: any) => item.productId)
.filter(Boolean);
let legacyTranslationMap = new Map<number, Record<string, string>>();
if (missingProductIds.length > 0) {
const legacyTranslations = await prisma.productTranslation.findMany({
where: { productId: { in: missingProductIds } },
select: { productId: true, langCode: true, name: true }
});
for (const pt of legacyTranslations) {
if (!legacyTranslationMap.has(pt.productId)) legacyTranslationMap.set(pt.productId, {});
legacyTranslationMap.get(pt.productId)![pt.langCode] = pt.name;
}
}
const localizedItems = order.items.map((item: any) => {
const snapshotName = (item.translations as any)?.name?.[lang];
const legacyName = legacyTranslationMap.get(item.productId)?.[lang];
const localizedName = snapshotName || legacyName || item.productName;
return {
...item,
productName: localizedName,
};
});
return {
...order,
items: localizedItems,
};
}
return order;
})
.post("/", async ({ body, set }) => {
@@ -31,8 +67,9 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
const data = body as any;
const settings = await prisma.storeSettings.findFirst()
const earnRate = settings?.pointsEarnRate || 1.0
const netAmount = data.totalAmount - data.shippingFee
const pointsEarned = Math.floor(Math.max(0, netAmount) * earnRate)
const shippingFee = data.shippingFee || 0
const netAmount = (data.totalAmount || 0) - shippingFee
const pointsEarned = Number.isFinite(netAmount) ? Math.floor(Math.max(0, netAmount) * earnRate) : 0
if (data.customerId && data.pointsUsed && data.pointsUsed > 0) {
// Verify customer has enough points
@@ -51,6 +88,33 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
const isPos = data.channel === "POS";
const initialStatus = isPos ? "COMPLETED" : "PENDING_PAYMENT";
// Pre-fetch product names across all languages for immutable snapshotting
const productIds = Array.from(new Set((data.items || []).map((item: any) => Number(item.productId)).filter(Boolean))) as number[];
const [productRecords, productTranslations] = await Promise.all([
productIds.length > 0 ? prisma.product.findMany({
where: { id: { in: productIds } },
select: { id: true, name: true }
}) : [],
productIds.length > 0 ? prisma.productTranslation.findMany({
where: { productId: { in: productIds } },
select: { productId: true, langCode: true, name: true }
}) : []
]);
const translationMap = new Map<number, Record<string, string>>();
for (const p of productRecords) {
if (p.name) {
if (!translationMap.has(p.id)) translationMap.set(p.id, {});
translationMap.get(p.id)!['th'] = p.name;
}
}
for (const pt of productTranslations) {
if (pt.name) {
if (!translationMap.has(pt.productId)) translationMap.set(pt.productId, {});
translationMap.get(pt.productId)![pt.langCode] = pt.name;
}
}
const order = await prisma.order.create({
data: {
channel: data.channel || "ONLINE",
@@ -77,20 +141,26 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
changeAmount: data.changeAmount,
cashierId: data.cashierId,
items: {
create: data.items.map((item: any) => ({
create: data.items.map((item: any) => {
const names = translationMap.get(Number(item.productId)) || {};
const translationsSnapshot = Object.keys(names).length > 0 ? { name: names } : undefined;
return {
productId: item.productId,
variantId: item.variantId,
quantity: item.quantity,
priceAtPurchase: item.priceAtPurchase,
productName: item.productName,
variantName: item.variantName,
productImage: item.productImage
}))
productImage: item.productImage,
translations: translationsSnapshot,
};
})
}
},
include: { items: true }
});
// For POS orders: Deduct stock immediately and credit points
if (isPos) {
for (const item of data.items) {
@@ -20,7 +20,7 @@ function orderLabel(id: number) {
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
const { id, lang } = await params
const [result, settings] = await Promise.all([
getOrderResult(id),
getOrderResult(id, lang),
getStoreSettings(lang),
])
@@ -45,7 +45,7 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
const { id, lang } = await params
const t = getServerTranslations(lang)
const [orderResult, categoriesResult, cmsLayoutResult, settings] = await Promise.all([
getOrderResult(id),
getOrderResult(id, lang),
getCategoriesResult(lang),
getCmsPageResult('order-success-layout', lang),
getStoreSettings(lang),
@@ -132,26 +132,29 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
{t.order.items.title}
</h2>
<div className="mt-6 space-y-4">
{order.items.map((item) => (
{order.items.map((item) => {
const displayName = (item.translations as any)?.name?.[lang] || item.productName
return (
<div key={item.id} className="flex items-center gap-4 rounded-2xl bg-surface-page p-4">
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-xl bg-surface-card">
<Image
src={item.productImage || '/images/fallback-product.svg'}
alt={item.productName}
alt={displayName}
fill
unoptimized
className="h-full w-full object-cover"
/>
</div>
<div className="min-w-0 flex-1">
<p className="font-semibold text-text-main">{item.productName}</p>
<p className="font-semibold text-text-main">{displayName}</p>
<p className="text-sm text-muted-text">
{item.variantName ? `${item.variantName} · ` : ''}x {item.quantity}
</p>
</div>
<p className="font-bold text-brand-primary">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
</div>
))}
)
})}
</div>
<div className="mt-6 space-y-3 border-t border-line-primary/20 pt-5 text-sm">
+13 -8
View File
@@ -5,18 +5,19 @@ import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import TrackOrderClient from '@/components/TrackOrderClient'
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
import { getServerTranslations } from '@/lib/translations'
// lang is extracted from params
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const t = getServerTranslations(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `ตรวจสอบสถานะคำสั่งซื้อ ${brandName} ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ`
: 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ';
const baseDesc = t.track?.seoDescription || 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ';
const description = brandName ? `${baseDesc} - ${brandName}` : baseDesc;
return generateSeoMetadata({
title: 'ติดตามคำสั่งซื้อ',
title: t.track?.seoTitle || 'ติดตามคำสั่งซื้อ',
description,
path: '/track',
lang,
@@ -30,6 +31,7 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st
getCategoriesResult(lang),
getCmsPageResult('track-order-layout', lang),
])
const t = getServerTranslations(lang)
const categories = categoriesResult.ok ? categoriesResult.data : []
const cmsSections = cmsLayoutResult.ok ? cmsLayoutResult.data.sections ?? [] : []
const { announcementSections, heroSections, bottomSections } = splitCmsLayoutSections(cmsSections)
@@ -51,16 +53,18 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st
<div className="mx-auto mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-surface-card text-brand-primary shadow-card">
<MapPin className="h-8 w-8" />
</div>
<p className="ui-eyebrow">Track Order</p>
<h1 className="mt-4 text-4xl font-semibold text-text-main md:text-5xl">Track Your Status</h1>
<p className="ui-eyebrow">{t.track?.hero?.eyebrow || 'Track Order'}</p>
<h1 className="mt-4 text-4xl font-semibold text-text-main md:text-5xl">
{t.track?.hero?.title || 'Track Your Status'}
</h1>
<p className="mx-auto mt-4 max-w-2xl text-muted-text">
กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้
{t.track?.hero?.description || 'กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้'}
</p>
</section>
)}
<section className="mx-auto max-w-5xl px-6 py-14 md:py-20">
<TrackOrderClient />
<TrackOrderClient lang={lang} />
</section>
{bottomSections.length > 0 && (
@@ -69,3 +73,4 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st
</main>
)
}
@@ -23,6 +23,7 @@ import {
import { formatPrice, type CustomerAddress, type Order } from '@homaroma/types'
import { API_BASE } from '../lib/api'
import { useLang } from '../hooks/useLang'
import { orderStatusBadgeClass, toneClass } from '../lib/theme'
import { useAuthStore, type Customer } from '../store/authStore'
@@ -107,6 +108,7 @@ async function readJson<T>(path: string, init?: RequestInit): Promise<T> {
}
export default function ProfileClient() {
const lang = useLang()
const { token, customer, setAuth, logout } = useAuthStore()
const [activeTab, setActiveTab] = useState<ProfileTab>('orders')
const [dbCustomer, setDbCustomer] = useState<ProfileCustomer | null>(null)
@@ -401,18 +403,21 @@ export default function ProfileClient() {
</div>
<div className="mt-4 space-y-3">
{order.items.map((item) => (
{order.items.map((item) => {
const displayName = (item.translations as any)?.name?.[lang] || item.productName
return (
<div key={item.id} className="flex items-center gap-3 text-sm">
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-xl bg-surface-page">
<Image src={item.productImage || '/images/fallback-product.svg'} alt={item.productName} fill unoptimized className="h-full w-full object-cover" />
<Image src={item.productImage || '/images/fallback-product.svg'} alt={displayName} fill unoptimized className="h-full w-full object-cover" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-semibold text-text-main">{item.productName}</p>
<p className="truncate font-semibold text-text-main">{displayName}</p>
<p className="text-xs text-muted-text">{item.variantName ? `${item.variantName} · ` : ''}x {item.quantity}</p>
</div>
<p className="font-bold text-brand-primary">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
</div>
))}
)
})}
</div>
<div className="mt-5 flex flex-col gap-3 border-t border-line-primary/20 pt-4 text-sm md:flex-row md:items-center md:justify-between">
@@ -2,7 +2,8 @@
import Link from './LocalizedLink'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ChevronRight, Loader2, Package, Search } from 'lucide-react'
import { API_BASE } from '../lib/api'
@@ -31,9 +32,22 @@ function orderLabel(id: number) {
return `#${id.toString().padStart(4, '0')}`
}
export default function TrackOrderClient() {
interface TrackOrderClientProps {
lang?: string
}
export default function TrackOrderClient({ lang: propLang }: TrackOrderClientProps = {}) {
const router = useRouter()
const lang = useLang()
const contextLang = useLang()
const lang = propLang || contextLang || 'th'
const { t: i18nT } = useTranslation()
const t = useCallback(
(key: string, options?: Record<string, unknown>) => {
return i18nT(key, { lng: lang, ...options })
},
[i18nT, lang],
)
const [orderId, setOrderId] = useState('')
const [phone, setPhone] = useState('')
const [localOrders, setLocalOrders] = useState<number[]>([])
@@ -61,7 +75,7 @@ export default function TrackOrderClient() {
const normalizedOrderId = Number.parseInt(orderId.replace(/[^0-9]/g, ''), 10)
if (!Number.isFinite(normalizedOrderId) || normalizedOrderId <= 0) {
setError('กรุณากรอกหมายเลขคำสั่งซื้อให้ถูกต้อง')
setError(t('track.errors.invalidOrderId'))
setLoading(false)
return
}
@@ -83,9 +97,9 @@ export default function TrackOrderClient() {
return
}
setError('error' in data && data.error ? data.error : 'ไม่สามารถยืนยันข้อมูลคำสั่งซื้อได้')
setError('error' in data && data.error ? data.error : t('track.errors.notFound'))
} catch (err) {
setError(err instanceof Error ? err.message : 'เกิดข้อผิดพลาดในการเชื่อมต่อ กรุณาลองใหม่อีกครั้ง')
setError(err instanceof Error ? err.message : t('track.errors.networkError'))
} finally {
setLoading(false)
}
@@ -99,8 +113,8 @@ export default function TrackOrderClient() {
<Search className="h-5 w-5" />
</div>
<div>
<p className="ui-eyebrow">Manual Search</p>
<h2 className="text-xl font-semibold text-text-main">ค้นหาคำสั่งซื้อ</h2>
<p className="ui-eyebrow">{t('track.search.eyebrow')}</p>
<h2 className="text-xl font-semibold text-text-main">{t('track.search.title')}</h2>
</div>
</div>
@@ -113,7 +127,7 @@ export default function TrackOrderClient() {
<div className="space-y-2">
<label htmlFor="orderId" className="ui-form-label">
Order Number
{t('track.search.orderIdLabel')}
</label>
<input
id="orderId"
@@ -121,14 +135,14 @@ export default function TrackOrderClient() {
value={orderId}
onChange={(event) => setOrderId(event.target.value)}
className="ui-form-shell"
placeholder="เช่น 1024"
placeholder={t('track.search.orderIdPlaceholder')}
type="text"
/>
</div>
<div className="space-y-2">
<label htmlFor="phone" className="ui-form-label">
Phone Number
{t('track.search.phoneLabel')}
</label>
<input
id="phone"
@@ -136,7 +150,7 @@ export default function TrackOrderClient() {
value={phone}
onChange={(event) => setPhone(event.target.value)}
className="ui-form-shell"
placeholder="เบอร์โทรที่ใช้ตอนสั่งซื้อ"
placeholder={t('track.search.phonePlaceholder')}
type="tel"
/>
</div>
@@ -146,7 +160,7 @@ export default function TrackOrderClient() {
disabled={loading}
className="ui-form-inline-button w-full"
>
{loading ? <Loader2 className="h-5 w-5 animate-spin" /> : 'Find Order'}
{loading ? <Loader2 className="h-5 w-5 animate-spin" /> : t('track.search.submit')}
</button>
</form>
</section>
@@ -154,13 +168,13 @@ export default function TrackOrderClient() {
<section className="space-y-6">
<div className="flex items-center gap-3 border-b border-line-primary/20 pb-4">
<Package className="h-5 w-5 text-brand-primary" />
<h2 className="text-xl font-semibold text-text-main">Recent Device Orders</h2>
<h2 className="text-xl font-semibold text-text-main">{t('track.recent.title')}</h2>
</div>
{localOrders.length === 0 ? (
<div className="ui-empty-panel p-10 text-center">
<p className="text-sm font-semibold text-muted-text">ยังไม่มีประวัติคำสั่งซื้อบนอุปกรณ์นี้</p>
<p className="mt-2 text-xs uppercase tracking-[0.18em] text-muted-text/70">ใช้ฟอร์มค้นหาด้านซ้ายเพื่อบันทึกออเดอร์</p>
<p className="text-sm font-semibold text-muted-text">{t('track.recent.emptyTitle')}</p>
<p className="mt-2 text-xs uppercase tracking-[0.18em] text-muted-text/70">{t('track.recent.emptyDesc')}</p>
</div>
) : (
<div className="space-y-3">
@@ -171,7 +185,7 @@ export default function TrackOrderClient() {
className="ui-card ui-card-hover group flex items-center justify-between p-5"
>
<div>
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">Order</p>
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">{t('track.recent.orderLabel')}</p>
<p className="mt-1 font-mono text-2xl font-bold text-text-main">{orderLabel(id)}</p>
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-surface-page text-text-main transition group-hover:bg-brand-primary group-hover:text-surface-card">
@@ -185,3 +199,4 @@ export default function TrackOrderClient() {
</div>
)
}
+3 -2
View File
@@ -114,8 +114,9 @@ export async function getActiveCouponsResult(): Promise<ApiResult<Coupon[]>> {
}
}
export async function getOrderResult(id: string | number): Promise<ApiResult<Order>> {
return await fetchJsonResult<Order>(`/orders/${id}`, { next: { revalidate: defaultRevalidate } })
export async function getOrderResult(id: string | number, lang = 'all'): Promise<ApiResult<Order>> {
const query = lang && lang !== 'all' ? `?lang=${lang}` : ''
return await fetchJsonResult<Order>(`/orders/${id}${query}`, { next: { revalidate: defaultRevalidate } })
}
export async function getProduct(id: string | number, lang = 'all'): Promise<Product | null> {
@@ -186,5 +186,36 @@
"loadFailed": "Unable to load this order",
"backToTrack": "Back to Order Search"
}
},
"track": {
"seoTitle": "Track Order",
"seoDescription": "Check your order and shipping status using your order number and phone number.",
"hero": {
"eyebrow": "Order Tracking",
"title": "Track Your Status",
"description": "Enter your order number and phone number to check shipment status, or view previously searched orders on this device."
},
"search": {
"eyebrow": "Manual Search",
"title": "Find Your Order",
"orderIdLabel": "Order Number",
"orderIdPlaceholder": "e.g. 1024",
"phoneLabel": "Phone Number",
"phonePlaceholder": "Phone number used during purchase",
"submit": "Find Order",
"searching": "Searching..."
},
"recent": {
"title": "Recent Device Orders",
"orderLabel": "Order",
"emptyTitle": "No order history on this device yet",
"emptyDesc": "Use the search form on the left to find and save orders"
},
"errors": {
"invalidOrderId": "Please enter a valid order number",
"notFound": "Unable to verify order details",
"networkError": "Connection error. Please try again."
}
}
}
@@ -186,5 +186,36 @@
"loadFailed": "Impossibile caricare questo ordine",
"backToTrack": "Torna alla ricerca ordine"
}
},
"track": {
"seoTitle": "Traccia Ordine",
"seoDescription": "Verifica lo stato del tuo ordine e della spedizione con il numero d'ordine e il numero di telefono.",
"hero": {
"eyebrow": "Tracciamento Spedizione",
"title": "Traccia il tuo Ordine",
"description": "Inserisci il numero dell'ordine e il numero di telefono per verificare la spedizione o consulta gli ordini cercati su questo dispositivo."
},
"search": {
"eyebrow": "Ricerca Manuale",
"title": "Trova il tuo Ordine",
"orderIdLabel": "Numero Ordine",
"orderIdPlaceholder": "es. 1024",
"phoneLabel": "Numero di Telefono",
"phonePlaceholder": "Telefono usato durante l'ordine",
"submit": "Cerca Ordine",
"searching": "Ricerca in corso..."
},
"recent": {
"title": "Ordini Recenti su questo Dispositivo",
"orderLabel": "Ordine",
"emptyTitle": "Nessuna cronologia ordini su questo dispositivo",
"emptyDesc": "Usa il modulo a sinistra per cercare e salvare gli ordini"
},
"errors": {
"invalidOrderId": "Inserisci un numero d'ordine valido",
"notFound": "Impossibile verificare le informazioni dell'ordine",
"networkError": "Errore di connessione. Riprova più tardi."
}
}
}
@@ -207,5 +207,36 @@
"loadFailed": "注文情報を読み込めませんでした",
"backToTrack": "注文検索に戻る"
}
},
"track": {
"seoTitle": "注文追跡",
"seoDescription": "注文番号とお電話番号を入力して配送状況をご確認ください。",
"hero": {
"eyebrow": "配送状況の確認",
"title": "ご注文状況を確認する",
"description": "注文番号とお電話番号を入力して配送状況を確認するか、この端末で以前検索した注文を確認できます。"
},
"search": {
"eyebrow": "手動検索",
"title": "注文を検索",
"orderIdLabel": "注文番号",
"orderIdPlaceholder": "例: 1024",
"phoneLabel": "電話番号",
"phonePlaceholder": "ご注文時の電話番号",
"submit": "注文を検索",
"searching": "検索中..."
},
"recent": {
"title": "この端末での検索履歴",
"orderLabel": "注文",
"emptyTitle": "この端末での注文履歴はまだありません",
"emptyDesc": "左側の検索フォームから注文を検索・保存してください"
},
"errors": {
"invalidOrderId": "有効な注文番号を入力してください",
"notFound": "注文情報を確認できませんでした",
"networkError": "接続エラーが発生しました。もう一度お試しください。"
}
}
}
@@ -207,5 +207,36 @@
"loadFailed": "주문 정보를 불러오지 못했습니다",
"backToTrack": "주문 조회로 돌아가기"
}
},
"track": {
"seoTitle": "주문 배송 조회",
"seoDescription": "주문 번호와 연락처로 주문 및 배송 상태를 확인하세요.",
"hero": {
"eyebrow": "주문 조회",
"title": "배송 상태 확인하기",
"description": "주문 번호와 전화번호를 입력하여 배송 상태를 확인하거나 이 기기에서 검색했던 주문을 다시 확인하세요."
},
"search": {
"eyebrow": "직접 검색",
"title": "주문 검색",
"orderIdLabel": "주문 번호",
"orderIdPlaceholder": "예: 1024",
"phoneLabel": "전화번호",
"phonePlaceholder": "주문 시 입력한 전화번호",
"submit": "주문 찾기",
"searching": "검색 중..."
},
"recent": {
"title": "이 기기의 최근 조회 주문",
"orderLabel": "주문",
"emptyTitle": "이 기기에 저장된 주문 내역이 없습니다",
"emptyDesc": "왼쪽 검색창에서 주문을 조회하여 저장하세요"
},
"errors": {
"invalidOrderId": "올바른 주문 번호를 입력해 주세요",
"notFound": "주문 정보를 확인할 수 없습니다",
"networkError": "네트워크 연결 오류가 발생했습니다. 다시 시도해 주세요."
}
}
}
@@ -186,5 +186,36 @@
"loadFailed": "ບໍ່ສາມາດໂຫຼດຂໍ້ມູນຄຳສັ່ງຊື້ໄດ້",
"backToTrack": "ກັບໄປຄົ້ນຫາຄຳສັ່ງຊື້"
}
},
"track": {
"seoTitle": "ຕິດຕາມຄຳສັ່ງຊື້",
"seoDescription": "ກວດສອບສະຖານະສິນຄ້າດ້ວຍເລກຄຳສັ່ງຊື້ ແລະ ເບີໂທລະສັບທີ່ໃຊ້ສັ່ງຊື້.",
"hero": {
"eyebrow": "ສະຖານະຄຳສັ່ງຊື້",
"title": "ຕິດຕາມສະຖານະສິນຄ້າຂອງທ່ານ",
"description": "ປ້ອນເລກຄຳສັ່ງຊື້ ແລະ ເບີໂທລະສັບເພື່ອຕິດຕາມສະຖານະສິນຄ້າ ຫຼື ກັບມາເບິ່ງຄຳສັ່ງຊື້ທີ່ເຄີຍຄົ້ນຫາໃນອຸປະກອນນີ້."
},
"search": {
"eyebrow": "ຄົ້ນຫາດ້ວຍຕົນເອງ",
"title": "ຄົ້ນຫາຄຳສັ່ງຊື້",
"orderIdLabel": "ເລກຄຳສັ່ງຊື້",
"orderIdPlaceholder": "ຕົວຢ່າງ 1024",
"phoneLabel": "ເບີໂທລະສັບ",
"phonePlaceholder": "ເບີໂທລະສັບທີ່ໃຊ້ສັ່ງຊື້",
"submit": "ຄົ້ນຫາຄຳສັ່ງຊື້",
"searching": "ກຳລັງຄົ້ນຫາ..."
},
"recent": {
"title": "ປະຫວັດຄຳສັ່ງຊື້ໃນອຸປະກອນນີ້",
"orderLabel": "ຄຳສັ່ງຊື້",
"emptyTitle": "ຍັງບໍ່ມີປະຫວັດຄຳສັ່ງຊື້ໃນອຸປະກອນນີ້",
"emptyDesc": "ໃຊ້ແບບຟອມດ້ານຊ້າຍເພື່ອຄົ້ນຫາ ແລະ ບັນທຶກອໍເດີ"
},
"errors": {
"invalidOrderId": "ກະລຸນາປ້ອນເລກຄຳສັ່ງຊື້ໃຫ້ຖືກຕ້ອງ",
"notFound": "ບໍ່ສາມາດຢືນຢັນຂໍ້ມູນຄຳສັ່ງຊື້ໄດ້",
"networkError": "ເກີດຂໍ້ຜິດພາດໃນການເຊື່ອມຕໍ່ ກະລຸນາລອງໃໝ່ອີກຄັ້ງ"
}
}
}
@@ -186,5 +186,36 @@
"loadFailed": "ไม่สามารถโหลดคำสั่งซื้อนี้ได้",
"backToTrack": "กลับไปค้นหาคำสั่งซื้อ"
}
},
"track": {
"seoTitle": "ติดตามคำสั่งซื้อ",
"seoDescription": "ตรวจสอบสถานะคำสั่งซื้อด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ",
"hero": {
"eyebrow": "สถานะคำสั่งซื้อ",
"title": "ติดตามสถานะสินค้าของคุณ",
"description": "กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้"
},
"search": {
"eyebrow": "ค้นหาด้วยตนเอง",
"title": "ค้นหาคำสั่งซื้อ",
"orderIdLabel": "หมายเลขคำสั่งซื้อ",
"orderIdPlaceholder": "เช่น 1024",
"phoneLabel": "เบอร์โทรศัพท์",
"phonePlaceholder": "เบอร์โทรที่ใช้ตอนสั่งซื้อ",
"submit": "ค้นหาคำสั่งซื้อ",
"searching": "กำลังค้นหา..."
},
"recent": {
"title": "ประวัติคำสั่งซื้อบนอุปกรณ์นี้",
"orderLabel": "คำสั่งซื้อ",
"emptyTitle": "ยังไม่มีประวัติคำสั่งซื้อบนอุปกรณ์นี้",
"emptyDesc": "ใช้ฟอร์มค้นหาด้านซ้ายเพื่อค้นหาและบันทึกออเดอร์"
},
"errors": {
"invalidOrderId": "กรุณากรอกหมายเลขคำสั่งซื้อให้ถูกต้อง",
"notFound": "ไม่สามารถยืนยันข้อมูลคำสั่งซื้อได้",
"networkError": "เกิดข้อผิดพลาดในการเชื่อมต่อ กรุณาลองใหม่อีกครั้ง"
}
}
}
@@ -207,5 +207,36 @@
"loadFailed": "Không thể tải thông tin đơn hàng",
"backToTrack": "Quay lại tra cứu"
}
},
"track": {
"seoTitle": "Tra cứu đơn hàng",
"seoDescription": "Kiểm tra tình trạng đơn hàng bằng mã đơn hàng và số điện thoại đã đặt.",
"hero": {
"eyebrow": "Tra cứu đơn hàng",
"title": "Theo dõi trạng thái đơn hàng",
"description": "Nhập mã đơn hàng và số điện thoại để kiểm tra trạng thái giao hàng hoặc xem lại các đơn hàng đã tìm kiếm trên thiết bị này."
},
"search": {
"eyebrow": "Tìm kiếm thủ công",
"title": "Tìm đơn hàng",
"orderIdLabel": "Mã đơn hàng",
"orderIdPlaceholder": "Ví dụ: 1024",
"phoneLabel": "Số điện thoại",
"phonePlaceholder": "Số điện thoại khi đặt hàng",
"submit": "Tìm đơn hàng",
"searching": "Đang tìm kiếm..."
},
"recent": {
"title": "Đơn hàng gần đây trên thiết bị",
"orderLabel": "Đơn hàng",
"emptyTitle": "Chưa có lịch sử đơn hàng trên thiết bị này",
"emptyDesc": "Sử dụng biểu mẫu bên trái để tìm kiếm và lưu đơn hàng"
},
"errors": {
"invalidOrderId": "Vui lòng nhập mã đơn hàng hợp lệ",
"notFound": "Không thể xác nhận thông tin đơn hàng",
"networkError": "Lỗi kết nối. Vui lòng thử lại."
}
}
}
@@ -207,5 +207,36 @@
"loadFailed": "无法加载此订单信息",
"backToTrack": "返回订单查询"
}
},
"track": {
"seoTitle": "订单追踪",
"seoDescription": "输入订单编号和手机号码以查询订单和配送状态。",
"hero": {
"eyebrow": "订单追踪",
"title": "追踪您的订单状态",
"description": "输入您的订单编号和电话号码以查看配送状态,或查看此设备上曾查询过的订单。"
},
"search": {
"eyebrow": "手动查询",
"title": "查询订单",
"orderIdLabel": "订单编号",
"orderIdPlaceholder": "例如 1024",
"phoneLabel": "电话号码",
"phonePlaceholder": "下单时填写的电话号码",
"submit": "查询订单",
"searching": "正在查询..."
},
"recent": {
"title": "此设备最近查询",
"orderLabel": "订单",
"emptyTitle": "此设备暂无订单记录",
"emptyDesc": "使用左侧搜索框查询并保存订单"
},
"errors": {
"invalidOrderId": "请输入有效的订单编号",
"notFound": "无法验证订单信息",
"networkError": "网络连接错误,请稍后重试。"
}
}
}
+5
View File
@@ -485,8 +485,13 @@ export interface OrderItem {
productName: string
variantName?: string | null
productImage?: string | null
translations?: {
name?: Record<string, string>
variantName?: Record<string, string>
} | null
}
export interface Order {
id: number
customerName: string