206 lines
6.2 KiB
TypeScript
206 lines
6.2 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
IonButton,
|
|
IonButtons,
|
|
IonContent,
|
|
IonHeader,
|
|
IonItem,
|
|
IonLabel,
|
|
IonList,
|
|
IonLoading,
|
|
IonPage,
|
|
IonText,
|
|
IonTitle,
|
|
IonToolbar,
|
|
useIonViewWillEnter,
|
|
} from '@ionic/react';
|
|
import { RouteComponentProps, useHistory } from 'react-router-dom';
|
|
import { supabase } from '../supabase';
|
|
import type { Tables } from '../database.types';
|
|
import {
|
|
setStatusBarBackground,
|
|
setStatusBarStyle,
|
|
Style,
|
|
} from '../utils/statusBar';
|
|
|
|
type Props = RouteComponentProps<{ id: string }>;
|
|
type Order = Tables<'orders'>;
|
|
type OrderItem = Tables<'order_items'>;
|
|
|
|
const statusSteps = [
|
|
'pending',
|
|
'confirmed',
|
|
'packed',
|
|
'out_for_delivery',
|
|
'delivered',
|
|
] as const;
|
|
|
|
const statusTitles: Record<(typeof statusSteps)[number], string> = {
|
|
pending: 'Pending',
|
|
confirmed: 'Confirmed',
|
|
packed: 'Packed',
|
|
out_for_delivery: 'Out for delivery',
|
|
delivered: 'Delivered',
|
|
};
|
|
|
|
const OrderDetail: React.FC<Props> = ({ match }) => {
|
|
const history = useHistory();
|
|
const [order, setOrder] = useState<Order | null>(null);
|
|
const [items, setItems] = useState<OrderItem[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
|
|
useIonViewWillEnter(() => {
|
|
setStatusBarStyle(Style.Dark);
|
|
setStatusBarBackground('#ffffff');
|
|
});
|
|
|
|
useEffect(() => {
|
|
const loadOrder = async () => {
|
|
setIsLoading(true);
|
|
|
|
const [
|
|
{ data: orderData, error: orderError },
|
|
{ data: itemData, error: itemError },
|
|
] = await Promise.all([
|
|
supabase.from('orders').select('*').eq('id', match.params.id).single(),
|
|
supabase
|
|
.from('order_items')
|
|
.select('*')
|
|
.eq('order_id', match.params.id)
|
|
.order('created_at', { ascending: true }),
|
|
]);
|
|
|
|
if (orderError || itemError || !orderData) {
|
|
setErrorMessage('We could not load this order.');
|
|
window.setTimeout(() => setErrorMessage(''), 4000);
|
|
setOrder(null);
|
|
setItems([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
setOrder(orderData);
|
|
setItems(itemData ?? []);
|
|
setIsLoading(false);
|
|
};
|
|
|
|
loadOrder();
|
|
}, [match.params.id]);
|
|
|
|
const activeStepIndex = useMemo(() => {
|
|
if (!order) return 0;
|
|
const index = statusSteps.indexOf(
|
|
order.status as (typeof statusSteps)[number]
|
|
);
|
|
return index >= 0 ? index : 0;
|
|
}, [order]);
|
|
|
|
return (
|
|
<IonPage>
|
|
<IonHeader className="ion-no-border">
|
|
<IonToolbar>
|
|
<IonButtons slot="start">
|
|
<IonButton fill="clear" onClick={() => history.replace('/orders')}>
|
|
Back
|
|
</IonButton>
|
|
</IonButtons>
|
|
<IonTitle>Track Order</IonTitle>
|
|
</IonToolbar>
|
|
</IonHeader>
|
|
<IonContent className="ion-padding">
|
|
<IonLoading isOpen={isLoading} message="Loading order details..." />
|
|
|
|
{errorMessage && (
|
|
<IonText color="danger">
|
|
<p>{errorMessage}</p>
|
|
</IonText>
|
|
)}
|
|
|
|
{!isLoading && !order ? (
|
|
<div className="empty-state cart-empty">
|
|
<h3>Order not found</h3>
|
|
<p className="muted-text">
|
|
We could not find that order. Try returning to your order list.
|
|
</p>
|
|
<IonButton onClick={() => history.replace('/orders')}>
|
|
Back to orders
|
|
</IonButton>
|
|
</div>
|
|
) : (
|
|
order && (
|
|
<>
|
|
<div className="tracking-summary-card">
|
|
<span className="order-label">Order number</span>
|
|
<h1>{order.order_number}</h1>
|
|
<p className="muted-text">
|
|
Placed on {new Date(order.created_at).toLocaleString()}
|
|
</p>
|
|
<div className="order-card-row">
|
|
<span>Current status</span>
|
|
<strong>
|
|
{statusTitles[
|
|
order.status as (typeof statusSteps)[number]
|
|
] ?? order.status}
|
|
</strong>
|
|
</div>
|
|
<div className="order-card-row">
|
|
<span>Total</span>
|
|
<strong>${Number(order.total).toFixed(2)}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="timeline-card">
|
|
<h2>Delivery progress</h2>
|
|
<div className="tracking-timeline">
|
|
{statusSteps.map((step, index) => {
|
|
const isActive = index <= activeStepIndex;
|
|
return (
|
|
<div
|
|
key={step}
|
|
className={`timeline-step ${isActive ? 'active' : ''}`}
|
|
>
|
|
<div className="timeline-dot" />
|
|
<div>
|
|
<strong>{statusTitles[step]}</strong>
|
|
<p className="muted-text">
|
|
{index < activeStepIndex
|
|
? 'Completed'
|
|
: index === activeStepIndex
|
|
? 'Current step'
|
|
: 'Up next'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="timeline-card">
|
|
<h2>Items in this order</h2>
|
|
<IonList lines="none" className="cart-list">
|
|
{items.map((item) => (
|
|
<IonItem key={item.id} className="cart-item">
|
|
<IonLabel>
|
|
<h2>{item.product_name}</h2>
|
|
<p>${Number(item.unit_price).toFixed(2)} each</p>
|
|
<strong>
|
|
Qty {item.quantity} · $
|
|
{(Number(item.unit_price) * item.quantity).toFixed(2)}
|
|
</strong>
|
|
</IonLabel>
|
|
</IonItem>
|
|
))}
|
|
</IonList>
|
|
</div>
|
|
</>
|
|
)
|
|
)}
|
|
</IonContent>
|
|
</IonPage>
|
|
);
|
|
};
|
|
|
|
export default OrderDetail;
|