Files
homaroma/apps/spa/src/pages/TrackOrderPage.tsx
T
2026-06-14 18:58:45 -04:00

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>
)
}