x
This commit is contained in:
1 parent
d13dedd215
commit
dba82b26a8
17 files changed
+399
-43
No files matched your search
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "OrderItem" ADD COLUMN "translations" JSONB;
|
||||||
@@ -432,8 +432,10 @@ model OrderItem {
|
|||||||
productName String
|
productName String
|
||||||
variantName String?
|
variantName String?
|
||||||
productImage String?
|
productImage String?
|
||||||
|
translations Json?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model Article {
|
model Article {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
slug String @unique
|
slug String @unique
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
|
|||||||
include: { items: true, customer: true }
|
include: { items: true, customer: true }
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.get("/:id", async ({ params: { id }, set }) => {
|
.get("/:id", async ({ params: { id }, query, set }) => {
|
||||||
const order = await prisma.order.findUnique({
|
const order = await prisma.order.findUnique({
|
||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
include: { items: true, customer: true }
|
include: { items: true, customer: true }
|
||||||
@@ -24,6 +24,42 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
|
|||||||
set.status = 404;
|
set.status = 404;
|
||||||
return { error: "Order not found" };
|
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;
|
return order;
|
||||||
})
|
})
|
||||||
.post("/", async ({ body, set }) => {
|
.post("/", async ({ body, set }) => {
|
||||||
@@ -31,8 +67,9 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
|
|||||||
const data = body as any;
|
const data = body as any;
|
||||||
const settings = await prisma.storeSettings.findFirst()
|
const settings = await prisma.storeSettings.findFirst()
|
||||||
const earnRate = settings?.pointsEarnRate || 1.0
|
const earnRate = settings?.pointsEarnRate || 1.0
|
||||||
const netAmount = data.totalAmount - data.shippingFee
|
const shippingFee = data.shippingFee || 0
|
||||||
const pointsEarned = Math.floor(Math.max(0, netAmount) * earnRate)
|
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) {
|
if (data.customerId && data.pointsUsed && data.pointsUsed > 0) {
|
||||||
// Verify customer has enough points
|
// Verify customer has enough points
|
||||||
@@ -51,6 +88,33 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
|
|||||||
const isPos = data.channel === "POS";
|
const isPos = data.channel === "POS";
|
||||||
const initialStatus = isPos ? "COMPLETED" : "PENDING_PAYMENT";
|
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({
|
const order = await prisma.order.create({
|
||||||
data: {
|
data: {
|
||||||
channel: data.channel || "ONLINE",
|
channel: data.channel || "ONLINE",
|
||||||
@@ -77,20 +141,26 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
|
|||||||
changeAmount: data.changeAmount,
|
changeAmount: data.changeAmount,
|
||||||
cashierId: data.cashierId,
|
cashierId: data.cashierId,
|
||||||
items: {
|
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,
|
productId: item.productId,
|
||||||
variantId: item.variantId,
|
variantId: item.variantId,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
priceAtPurchase: item.priceAtPurchase,
|
priceAtPurchase: item.priceAtPurchase,
|
||||||
productName: item.productName,
|
productName: item.productName,
|
||||||
variantName: item.variantName,
|
variantName: item.variantName,
|
||||||
productImage: item.productImage
|
productImage: item.productImage,
|
||||||
}))
|
translations: translationsSnapshot,
|
||||||
|
};
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
include: { items: true }
|
include: { items: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// For POS orders: Deduct stock immediately and credit points
|
// For POS orders: Deduct stock immediately and credit points
|
||||||
if (isPos) {
|
if (isPos) {
|
||||||
for (const item of data.items) {
|
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> {
|
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
|
||||||
const { id, lang } = await params
|
const { id, lang } = await params
|
||||||
const [result, settings] = await Promise.all([
|
const [result, settings] = await Promise.all([
|
||||||
getOrderResult(id),
|
getOrderResult(id, lang),
|
||||||
getStoreSettings(lang),
|
getStoreSettings(lang),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
|||||||
const { id, lang } = await params
|
const { id, lang } = await params
|
||||||
const t = getServerTranslations(lang)
|
const t = getServerTranslations(lang)
|
||||||
const [orderResult, categoriesResult, cmsLayoutResult, settings] = await Promise.all([
|
const [orderResult, categoriesResult, cmsLayoutResult, settings] = await Promise.all([
|
||||||
getOrderResult(id),
|
getOrderResult(id, lang),
|
||||||
getCategoriesResult(lang),
|
getCategoriesResult(lang),
|
||||||
getCmsPageResult('order-success-layout', lang),
|
getCmsPageResult('order-success-layout', lang),
|
||||||
getStoreSettings(lang),
|
getStoreSettings(lang),
|
||||||
@@ -132,26 +132,29 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
|||||||
{t.order.items.title}
|
{t.order.items.title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-6 space-y-4">
|
<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 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">
|
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-xl bg-surface-card">
|
||||||
<Image
|
<Image
|
||||||
src={item.productImage || '/images/fallback-product.svg'}
|
src={item.productImage || '/images/fallback-product.svg'}
|
||||||
alt={item.productName}
|
alt={displayName}
|
||||||
fill
|
fill
|
||||||
unoptimized
|
unoptimized
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<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">
|
<p className="text-sm text-muted-text">
|
||||||
{item.variantName ? `${item.variantName} · ` : ''}x {item.quantity}
|
{item.variantName ? `${item.variantName} · ` : ''}x {item.quantity}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-bold text-brand-primary">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
<p className="font-bold text-brand-primary">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 space-y-3 border-t border-line-primary/20 pt-5 text-sm">
|
<div className="mt-6 space-y-3 border-t border-line-primary/20 pt-5 text-sm">
|
||||||
|
|||||||
@@ -5,18 +5,19 @@ import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
|||||||
import TrackOrderClient from '@/components/TrackOrderClient'
|
import TrackOrderClient from '@/components/TrackOrderClient'
|
||||||
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
|
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
|
||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
|
import { getServerTranslations } from '@/lib/translations'
|
||||||
|
|
||||||
// lang is extracted from params
|
// lang is extracted from params
|
||||||
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
const { lang } = await params;
|
const { lang } = await params;
|
||||||
const settings = await getStoreSettings(lang);
|
const settings = await getStoreSettings(lang);
|
||||||
|
const t = getServerTranslations(lang);
|
||||||
const brandName = settings?.brandName || '';
|
const brandName = settings?.brandName || '';
|
||||||
const description = brandName
|
const baseDesc = t.track?.seoDescription || 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ';
|
||||||
? `ตรวจสอบสถานะคำสั่งซื้อ ${brandName} ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ`
|
const description = brandName ? `${baseDesc} - ${brandName}` : baseDesc;
|
||||||
: 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ';
|
|
||||||
|
|
||||||
return generateSeoMetadata({
|
return generateSeoMetadata({
|
||||||
title: 'ติดตามคำสั่งซื้อ',
|
title: t.track?.seoTitle || 'ติดตามคำสั่งซื้อ',
|
||||||
description,
|
description,
|
||||||
path: '/track',
|
path: '/track',
|
||||||
lang,
|
lang,
|
||||||
@@ -30,6 +31,7 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st
|
|||||||
getCategoriesResult(lang),
|
getCategoriesResult(lang),
|
||||||
getCmsPageResult('track-order-layout', lang),
|
getCmsPageResult('track-order-layout', lang),
|
||||||
])
|
])
|
||||||
|
const t = getServerTranslations(lang)
|
||||||
const categories = categoriesResult.ok ? categoriesResult.data : []
|
const categories = categoriesResult.ok ? categoriesResult.data : []
|
||||||
const cmsSections = cmsLayoutResult.ok ? cmsLayoutResult.data.sections ?? [] : []
|
const cmsSections = cmsLayoutResult.ok ? cmsLayoutResult.data.sections ?? [] : []
|
||||||
const { announcementSections, heroSections, bottomSections } = splitCmsLayoutSections(cmsSections)
|
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">
|
<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" />
|
<MapPin className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<p className="ui-eyebrow">Track Order</p>
|
<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">Track Your Status</h1>
|
<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">
|
<p className="mx-auto mt-4 max-w-2xl text-muted-text">
|
||||||
กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้
|
{t.track?.hero?.description || 'กรอกหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์เพื่อตรวจสอบสถานะสินค้า หรือกลับมาดูคำสั่งซื้อที่เคยค้นหาบนอุปกรณ์นี้'}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="mx-auto max-w-5xl px-6 py-14 md:py-20">
|
<section className="mx-auto max-w-5xl px-6 py-14 md:py-20">
|
||||||
<TrackOrderClient />
|
<TrackOrderClient lang={lang} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{bottomSections.length > 0 && (
|
{bottomSections.length > 0 && (
|
||||||
@@ -69,3 +73,4 @@ export default async function TrackPage({ params }: { params: Promise<{ lang: st
|
|||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { formatPrice, type CustomerAddress, type Order } from '@homaroma/types'
|
import { formatPrice, type CustomerAddress, type Order } from '@homaroma/types'
|
||||||
|
|
||||||
import { API_BASE } from '../lib/api'
|
import { API_BASE } from '../lib/api'
|
||||||
|
import { useLang } from '../hooks/useLang'
|
||||||
import { orderStatusBadgeClass, toneClass } from '../lib/theme'
|
import { orderStatusBadgeClass, toneClass } from '../lib/theme'
|
||||||
import { useAuthStore, type Customer } from '../store/authStore'
|
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() {
|
export default function ProfileClient() {
|
||||||
|
const lang = useLang()
|
||||||
const { token, customer, setAuth, logout } = useAuthStore()
|
const { token, customer, setAuth, logout } = useAuthStore()
|
||||||
const [activeTab, setActiveTab] = useState<ProfileTab>('orders')
|
const [activeTab, setActiveTab] = useState<ProfileTab>('orders')
|
||||||
const [dbCustomer, setDbCustomer] = useState<ProfileCustomer | null>(null)
|
const [dbCustomer, setDbCustomer] = useState<ProfileCustomer | null>(null)
|
||||||
@@ -401,18 +403,21 @@ export default function ProfileClient() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 space-y-3">
|
<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 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">
|
<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>
|
||||||
<div className="min-w-0 flex-1">
|
<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>
|
<p className="text-xs text-muted-text">{item.variantName ? `${item.variantName} · ` : ''}x {item.quantity}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-bold text-brand-primary">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
<p className="font-bold text-brand-primary">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</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">
|
<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 Link from './LocalizedLink'
|
||||||
import { useRouter } from 'next/navigation'
|
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 { ChevronRight, Loader2, Package, Search } from 'lucide-react'
|
||||||
|
|
||||||
import { API_BASE } from '../lib/api'
|
import { API_BASE } from '../lib/api'
|
||||||
@@ -31,9 +32,22 @@ function orderLabel(id: number) {
|
|||||||
return `#${id.toString().padStart(4, '0')}`
|
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 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 [orderId, setOrderId] = useState('')
|
||||||
const [phone, setPhone] = useState('')
|
const [phone, setPhone] = useState('')
|
||||||
const [localOrders, setLocalOrders] = useState<number[]>([])
|
const [localOrders, setLocalOrders] = useState<number[]>([])
|
||||||
@@ -61,7 +75,7 @@ export default function TrackOrderClient() {
|
|||||||
const normalizedOrderId = Number.parseInt(orderId.replace(/[^0-9]/g, ''), 10)
|
const normalizedOrderId = Number.parseInt(orderId.replace(/[^0-9]/g, ''), 10)
|
||||||
|
|
||||||
if (!Number.isFinite(normalizedOrderId) || normalizedOrderId <= 0) {
|
if (!Number.isFinite(normalizedOrderId) || normalizedOrderId <= 0) {
|
||||||
setError('กรุณากรอกหมายเลขคำสั่งซื้อให้ถูกต้อง')
|
setError(t('track.errors.invalidOrderId'))
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -83,9 +97,9 @@ export default function TrackOrderClient() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setError('error' in data && data.error ? data.error : 'ไม่สามารถยืนยันข้อมูลคำสั่งซื้อได้')
|
setError('error' in data && data.error ? data.error : t('track.errors.notFound'))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'เกิดข้อผิดพลาดในการเชื่อมต่อ กรุณาลองใหม่อีกครั้ง')
|
setError(err instanceof Error ? err.message : t('track.errors.networkError'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -99,8 +113,8 @@ export default function TrackOrderClient() {
|
|||||||
<Search className="h-5 w-5" />
|
<Search className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="ui-eyebrow">Manual Search</p>
|
<p className="ui-eyebrow">{t('track.search.eyebrow')}</p>
|
||||||
<h2 className="text-xl font-semibold text-text-main">ค้นหาคำสั่งซื้อ</h2>
|
<h2 className="text-xl font-semibold text-text-main">{t('track.search.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -113,7 +127,7 @@ export default function TrackOrderClient() {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="orderId" className="ui-form-label">
|
<label htmlFor="orderId" className="ui-form-label">
|
||||||
Order Number
|
{t('track.search.orderIdLabel')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="orderId"
|
id="orderId"
|
||||||
@@ -121,14 +135,14 @@ export default function TrackOrderClient() {
|
|||||||
value={orderId}
|
value={orderId}
|
||||||
onChange={(event) => setOrderId(event.target.value)}
|
onChange={(event) => setOrderId(event.target.value)}
|
||||||
className="ui-form-shell"
|
className="ui-form-shell"
|
||||||
placeholder="เช่น 1024"
|
placeholder={t('track.search.orderIdPlaceholder')}
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="phone" className="ui-form-label">
|
<label htmlFor="phone" className="ui-form-label">
|
||||||
Phone Number
|
{t('track.search.phoneLabel')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="phone"
|
id="phone"
|
||||||
@@ -136,7 +150,7 @@ export default function TrackOrderClient() {
|
|||||||
value={phone}
|
value={phone}
|
||||||
onChange={(event) => setPhone(event.target.value)}
|
onChange={(event) => setPhone(event.target.value)}
|
||||||
className="ui-form-shell"
|
className="ui-form-shell"
|
||||||
placeholder="เบอร์โทรที่ใช้ตอนสั่งซื้อ"
|
placeholder={t('track.search.phonePlaceholder')}
|
||||||
type="tel"
|
type="tel"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +160,7 @@ export default function TrackOrderClient() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="ui-form-inline-button w-full"
|
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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -154,13 +168,13 @@ export default function TrackOrderClient() {
|
|||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<div className="flex items-center gap-3 border-b border-line-primary/20 pb-4">
|
<div className="flex items-center gap-3 border-b border-line-primary/20 pb-4">
|
||||||
<Package className="h-5 w-5 text-brand-primary" />
|
<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>
|
</div>
|
||||||
|
|
||||||
{localOrders.length === 0 ? (
|
{localOrders.length === 0 ? (
|
||||||
<div className="ui-empty-panel p-10 text-center">
|
<div className="ui-empty-panel p-10 text-center">
|
||||||
<p className="text-sm font-semibold text-muted-text">ยังไม่มีประวัติคำสั่งซื้อบนอุปกรณ์นี้</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">ใช้ฟอร์มค้นหาด้านซ้ายเพื่อบันทึกออเดอร์</p>
|
<p className="mt-2 text-xs uppercase tracking-[0.18em] text-muted-text/70">{t('track.recent.emptyDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<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"
|
className="ui-card ui-card-hover group flex items-center justify-between p-5"
|
||||||
>
|
>
|
||||||
<div>
|
<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>
|
<p className="mt-1 font-mono text-2xl font-bold text-text-main">{orderLabel(id)}</p>
|
||||||
</div>
|
</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">
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +114,9 @@ export async function getActiveCouponsResult(): Promise<ApiResult<Coupon[]>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrderResult(id: string | number): Promise<ApiResult<Order>> {
|
export async function getOrderResult(id: string | number, lang = 'all'): Promise<ApiResult<Order>> {
|
||||||
return await fetchJsonResult<Order>(`/orders/${id}`, { next: { revalidate: defaultRevalidate } })
|
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> {
|
export async function getProduct(id: string | number, lang = 'all'): Promise<Product | null> {
|
||||||
|
|||||||
@@ -186,5 +186,36 @@
|
|||||||
"loadFailed": "Unable to load this order",
|
"loadFailed": "Unable to load this order",
|
||||||
"backToTrack": "Back to Order Search"
|
"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",
|
"loadFailed": "Impossibile caricare questo ordine",
|
||||||
"backToTrack": "Torna alla ricerca 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": "注文情報を読み込めませんでした",
|
"loadFailed": "注文情報を読み込めませんでした",
|
||||||
"backToTrack": "注文検索に戻る"
|
"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": "주문 정보를 불러오지 못했습니다",
|
"loadFailed": "주문 정보를 불러오지 못했습니다",
|
||||||
"backToTrack": "주문 조회로 돌아가기"
|
"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": "ບໍ່ສາມາດໂຫຼດຂໍ້ມູນຄຳສັ່ງຊື້ໄດ້",
|
"loadFailed": "ບໍ່ສາມາດໂຫຼດຂໍ້ມູນຄຳສັ່ງຊື້ໄດ້",
|
||||||
"backToTrack": "ກັບໄປຄົ້ນຫາຄຳສັ່ງຊື້"
|
"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": "ไม่สามารถโหลดคำสั่งซื้อนี้ได้",
|
"loadFailed": "ไม่สามารถโหลดคำสั่งซื้อนี้ได้",
|
||||||
"backToTrack": "กลับไปค้นหาคำสั่งซื้อ"
|
"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",
|
"loadFailed": "Không thể tải thông tin đơn hàng",
|
||||||
"backToTrack": "Quay lại tra cứu"
|
"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": "无法加载此订单信息",
|
"loadFailed": "无法加载此订单信息",
|
||||||
"backToTrack": "返回订单查询"
|
"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": "网络连接错误,请稍后重试。"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,8 +485,13 @@ export interface OrderItem {
|
|||||||
productName: string
|
productName: string
|
||||||
variantName?: string | null
|
variantName?: string | null
|
||||||
productImage?: string | null
|
productImage?: string | null
|
||||||
|
translations?: {
|
||||||
|
name?: Record<string, string>
|
||||||
|
variantName?: Record<string, string>
|
||||||
|
} | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
id: number
|
id: number
|
||||||
customerName: string
|
customerName: string
|
||||||
|
|||||||
Reference in new issue
Block a user