feat(spa): add order history and automatic address book
This commit is contained in:
1 parent
15cb147c81
commit
ca31564ee7
3 files changed
+103
-8
No files matched your search
@@ -123,6 +123,13 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
|
|||||||
paymentSlipUrl: t.String()
|
paymentSlipUrl: t.String()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.get("/customer/:customerId", async ({ params: { customerId } }) => {
|
||||||
|
return await prisma.order.findMany({
|
||||||
|
where: { customerId: Number(customerId) },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
include: { items: true }
|
||||||
|
});
|
||||||
|
})
|
||||||
.post("/track", async ({ body, set }) => {
|
.post("/track", async ({ body, set }) => {
|
||||||
try {
|
try {
|
||||||
const { orderId, phone } = body as any;
|
const { orderId, phone } = body as any;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function CheckoutPage() {
|
|||||||
address: ''
|
address: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// Prefill form with LINE customer data
|
// Prefill form with LINE customer data and last used address
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customer) {
|
if (customer) {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
@@ -40,6 +40,23 @@ export default function CheckoutPage() {
|
|||||||
email: prev.email || customer.email || '',
|
email: prev.email || customer.email || '',
|
||||||
phone: prev.phone || customer.phone || ''
|
phone: prev.phone || customer.phone || ''
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Fetch latest order to pre-fill address acting as an automatic Address Book
|
||||||
|
fetch(`${API_BASE}/orders/customer/${customer.id}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
const lastOrder = data[0];
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
name: prev.name || lastOrder.customerName || '',
|
||||||
|
phone: prev.phone || lastOrder.customerPhone || '',
|
||||||
|
email: prev.email || lastOrder.customerEmail || '',
|
||||||
|
address: prev.address || lastOrder.shippingAddress || ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
}, [customer])
|
}, [customer])
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,24 @@ import { useNavigate, Link } from 'react-router-dom'
|
|||||||
import Navbar from '../components/Navbar'
|
import Navbar from '../components/Navbar'
|
||||||
import Footer from '../components/Footer'
|
import Footer from '../components/Footer'
|
||||||
import { API_BASE, type Category } from '../types'
|
import { API_BASE, type Category } from '../types'
|
||||||
import { Search, MapPin, Package, Loader2, ChevronRight } from 'lucide-react'
|
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() {
|
export default function TrackOrderPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { customer } = useAuthStore()
|
||||||
const [categories, setCategories] = useState<Category[]>([])
|
const [categories, setCategories] = useState<Category[]>([])
|
||||||
const [localOrders, setLocalOrders] = useState<number[]>([])
|
const [localOrders, setLocalOrders] = useState<number[]>([])
|
||||||
|
const [historyOrders, setHistoryOrders] = useState<OrderHistoryItem[]>([])
|
||||||
|
const [historyLoading, setHistoryLoading] = useState(false)
|
||||||
|
|
||||||
const [orderId, setOrderId] = useState('')
|
const [orderId, setOrderId] = useState('')
|
||||||
const [phone, setPhone] = useState('')
|
const [phone, setPhone] = useState('')
|
||||||
@@ -21,12 +33,23 @@ export default function TrackOrderPage() {
|
|||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
|
||||||
// Load local orders
|
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')
|
const storedStr = localStorage.getItem('homaroma_local_orders')
|
||||||
if (storedStr) {
|
if (storedStr) {
|
||||||
setLocalOrders(JSON.parse(storedStr))
|
setLocalOrders(JSON.parse(storedStr))
|
||||||
}
|
}
|
||||||
}, [])
|
}
|
||||||
|
}, [customer])
|
||||||
|
|
||||||
const handleTrackForm = async (e: React.FormEvent) => {
|
const handleTrackForm = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -127,16 +150,63 @@ export default function TrackOrderPage() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Local Order History */}
|
{/* Local / Account Order History */}
|
||||||
<div className="space-y-6">
|
<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">
|
<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">
|
||||||
<Package className="w-5 h-5 text-spa-stone/60" /> Recent Device Orders
|
{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>
|
</h2>
|
||||||
|
|
||||||
{localOrders.length === 0 ? (
|
{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">
|
<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-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">Use manual search if you ordered elsewhere</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>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -156,6 +226,7 @@ export default function TrackOrderPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user