289 lines
13 KiB
TypeScript
289 lines
13 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useNavigate, Link } from 'react-router-dom'
|
|
import { useTranslation } from 'react-i18next'
|
|
import Navbar from '../components/Navbar'
|
|
import Footer from '../components/Footer'
|
|
import DynamicPageRenderer from '../components/DynamicPageRenderer'
|
|
import { API_BASE, type Category, type PageSection } from '../types'
|
|
import { Search, MapPin, Package, Loader2, ChevronRight, Clock } from 'lucide-react'
|
|
import { useAuthStore } from '../store/authStore'
|
|
|
|
interface OrderHistoryItem {
|
|
id: number
|
|
status: string
|
|
totalAmount: number
|
|
createdAt: string
|
|
items: any[]
|
|
}
|
|
|
|
export default function TrackOrderPage() {
|
|
const navigate = useNavigate()
|
|
const { customer } = useAuthStore()
|
|
const { i18n } = useTranslation()
|
|
const [categories, setCategories] = useState<Category[]>([])
|
|
const [localOrders, setLocalOrders] = useState<number[]>([])
|
|
const [historyOrders, setHistoryOrders] = useState<OrderHistoryItem[]>([])
|
|
const [historyLoading, setHistoryLoading] = useState(false)
|
|
|
|
const [orderId, setOrderId] = useState('')
|
|
const [phone, setPhone] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
|
|
|
useEffect(() => {
|
|
fetch(`${API_BASE}/pages/track-order-layout`)
|
|
.then((r) => {
|
|
if (!r.ok) throw new Error('Page not found');
|
|
return r.json();
|
|
})
|
|
.then((data) => {
|
|
if (data && data.sections) {
|
|
setCmsSections(data.sections);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.log('Track Order dynamic layout sections not loaded:', err.message);
|
|
});
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetch(`${API_BASE}/categories?lang=${i18n.language}`)
|
|
.then(res => res.json())
|
|
.then(setCategories)
|
|
.catch(console.error)
|
|
|
|
if (customer) {
|
|
setHistoryLoading(true)
|
|
fetch(`${API_BASE}/orders/customer/${customer.id}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (Array.isArray(data)) setHistoryOrders(data)
|
|
})
|
|
.catch(console.error)
|
|
.finally(() => setHistoryLoading(false))
|
|
} else {
|
|
// Load local orders if not logged in
|
|
const storedStr = localStorage.getItem('homaroma_local_orders')
|
|
if (storedStr) {
|
|
setLocalOrders(JSON.parse(storedStr))
|
|
}
|
|
}
|
|
}, [customer, i18n.language])
|
|
|
|
const handleTrackForm = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/orders/track`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
orderId: parseInt(orderId.replace(/[^0-9]/g, ''), 10) || 0,
|
|
phone
|
|
})
|
|
})
|
|
const data = await res.json()
|
|
|
|
if (res.ok && data.success) {
|
|
// Automatically save to local history so they don't have to type it again
|
|
if (!localOrders.includes(data.orderId)) {
|
|
const newOrders = [data.orderId, ...localOrders]
|
|
setLocalOrders(newOrders)
|
|
localStorage.setItem('homaroma_local_orders', JSON.stringify(newOrders))
|
|
}
|
|
navigate(`/order/${data.orderId}`)
|
|
} else {
|
|
setError(data.error || 'Failed to authenticate order details.')
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
setError('An error occurred. Please check your internet connection.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const announcementSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() === 'ANNOUNCEMENT_BAR');
|
|
const heroSections = cmsSections.filter(s => s.isActive && (s.type.toUpperCase() === 'HERO' || s.type.toUpperCase() === 'BANNER'));
|
|
const bottomSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR' && s.type.toUpperCase() !== 'HERO' && s.type.toUpperCase() !== 'BANNER');
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col bg-pop-bg">
|
|
{announcementSections.length > 0 && (
|
|
<DynamicPageRenderer
|
|
sections={announcementSections}
|
|
categories={categories}
|
|
categoriesLoading={false}
|
|
/>
|
|
)}
|
|
|
|
<Navbar categories={categories} />
|
|
|
|
<main className="flex-1 max-w-4xl w-full mx-auto px-6 flex flex-col justify-center pt-32 pb-24 space-y-12 animate-fade-in">
|
|
|
|
{/* Header */}
|
|
{heroSections.length > 0 ? (
|
|
<DynamicPageRenderer
|
|
sections={heroSections}
|
|
categories={categories}
|
|
categoriesLoading={false}
|
|
/>
|
|
) : (
|
|
<div className="text-center space-y-4">
|
|
<div className="w-16 h-16 bg-spa-sage/30 rounded-full flex items-center justify-center mx-auto border border-spa-stone/10">
|
|
<MapPin className="w-8 h-8 text-spa-forest" />
|
|
</div>
|
|
<h1 className="text-4xl font-bold tracking-tight text-pop-black">Track Your Status</h1>
|
|
<p className="text-spa-stone">Enter your order details below to check shipping status or upload payment slips.</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid md:grid-cols-2 gap-12">
|
|
|
|
{/* Tracking Form */}
|
|
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 self-start shadow-sm">
|
|
<h2 className="text-lg font-bold uppercase tracking-widest border-b border-spa-stone/10 pb-4 flex items-center gap-2 text-spa-forest">
|
|
<Search className="w-5 h-5" /> Manual Search
|
|
</h2>
|
|
|
|
<form onSubmit={handleTrackForm} className="space-y-5">
|
|
{error && (
|
|
<div className="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-500 font-medium rounded-lg text-sm text-center">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">Order Number (ID)</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={orderId}
|
|
onChange={e => setOrderId(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3.5 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all placeholder:text-spa-stone/50"
|
|
placeholder="e.g. 1024"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">Phone Number</label>
|
|
<input
|
|
type="tel"
|
|
required
|
|
value={phone}
|
|
onChange={e => setPhone(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3.5 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all placeholder:text-spa-stone/50"
|
|
placeholder="The phone number used during checkout"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full py-4 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone hover:shadow-lg transition-all duration-300 disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Find Order'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Local / Account Order History */}
|
|
<div className="space-y-6">
|
|
<h2 className="text-lg font-bold uppercase tracking-widest border-b border-spa-stone/10 pb-4 flex items-center gap-2 text-pop-black">
|
|
{customer ? (
|
|
<><Clock className="w-5 h-5 text-spa-stone/60" /> My Order History</>
|
|
) : (
|
|
<><Package className="w-5 h-5 text-spa-stone/60" /> Recent Device Orders</>
|
|
)}
|
|
</h2>
|
|
|
|
{customer ? (
|
|
historyLoading ? (
|
|
<div className="flex justify-center py-12"><Loader2 className="w-8 h-8 animate-spin text-spa-forest" /></div>
|
|
) : historyOrders.length === 0 ? (
|
|
<div className="text-center py-12 bg-spa-light rounded-2xl border border-dashed border-spa-stone/20 space-y-3">
|
|
<p className="text-spa-stone text-sm font-semibold">No order history found.</p>
|
|
<p className="text-[10px] uppercase text-spa-stone/60 tracking-widest font-bold">Start shopping to see your orders here</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{historyOrders.map((order) => (
|
|
<Link
|
|
key={order.id}
|
|
to={`/order/${order.id}`}
|
|
className="flex justify-between items-center p-5 bg-white rounded-xl border border-spa-stone/10 shadow-sm hover:border-spa-forest transition-all group"
|
|
>
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<p className="text-xs text-spa-stone/80 uppercase tracking-widest font-semibold">Order</p>
|
|
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-widest ${
|
|
order.status === 'PENDING_PAYMENT' ? 'bg-orange-100 text-orange-700' :
|
|
order.status === 'PAID' ? 'bg-green-100 text-green-700' : 'bg-spa-light text-spa-stone'
|
|
}`}>
|
|
{order.status.replace('_', ' ')}
|
|
</span>
|
|
</div>
|
|
<p className="text-xl font-bold font-mono text-spa-forest">#{order.id.toString().padStart(4, '0')}</p>
|
|
<p className="text-xs text-spa-stone mt-1">{new Date(order.createdAt).toLocaleDateString()}</p>
|
|
</div>
|
|
<div className="text-right flex items-center gap-4">
|
|
<div className="hidden sm:block">
|
|
<p className="text-sm font-bold text-pop-black">฿{order.totalAmount.toLocaleString()}</p>
|
|
<p className="text-[10px] text-spa-stone uppercase">{order.items.length} items</p>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-spa-light flex items-center justify-center group-hover:bg-spa-forest transition-colors shrink-0">
|
|
<ChevronRight className="w-5 h-5 text-spa-stone/80 group-hover:text-white" />
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)
|
|
) : (
|
|
localOrders.length === 0 ? (
|
|
<div className="text-center py-12 bg-spa-light rounded-2xl border border-dashed border-spa-stone/20 space-y-3">
|
|
<p className="text-spa-stone text-sm font-semibold">No recent orders found on this device.</p>
|
|
<p className="text-[10px] uppercase text-spa-stone/60 tracking-widest font-bold">Log in with LINE or use manual search</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{localOrders.map((id) => (
|
|
<Link
|
|
key={id}
|
|
to={`/order/${id}`}
|
|
className="flex justify-between items-center p-5 bg-white rounded-xl border border-spa-stone/10 shadow-sm hover:border-spa-forest transition-all group"
|
|
>
|
|
<div>
|
|
<p className="text-xs text-spa-stone/80 uppercase tracking-widest mb-1 font-semibold">Order</p>
|
|
<p className="text-xl font-bold font-mono text-spa-forest">#{id.toString().padStart(4, '0')}</p>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-spa-light flex items-center justify-center group-hover:bg-spa-forest transition-colors">
|
|
<ChevronRight className="w-5 h-5 text-spa-stone/80 group-hover:text-white" />
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</main>
|
|
|
|
{bottomSections.length > 0 && (
|
|
<DynamicPageRenderer
|
|
sections={bottomSections}
|
|
categories={categories}
|
|
categoriesLoading={false}
|
|
/>
|
|
)}
|
|
|
|
<Footer categories={categories} />
|
|
</div>
|
|
)
|
|
}
|