Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline. Co-authored-by: Cursor <cursoragent@cursor.com>
241 lines
8.1 KiB
TypeScript
241 lines
8.1 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import { router, useLocalSearchParams } from "expo-router";
|
|
import { Alert, Image, ScrollView, View } from "react-native";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card, InfoRow, Section } from "@/components/ui/Card";
|
|
import { AppHeader } from "@/components/ui/Header";
|
|
import { Screen } from "@/components/ui/Screen";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { getBranchById } from "@/lib/data/branches";
|
|
import { routes } from "@/lib/routes";
|
|
import { useOrdersStore } from "@/lib/stores/ordersStore";
|
|
import { colors, gutter, radii } from "@/lib/theme";
|
|
import { formatETB } from "@/lib/utils";
|
|
|
|
const toneFor = {
|
|
requested: "neutral",
|
|
confirmed: "info",
|
|
paid: "brand",
|
|
dispatched: "warn",
|
|
delivered: "brand",
|
|
completed: "brand",
|
|
cancelled: "danger",
|
|
} as const;
|
|
|
|
export default function OrderDetailScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const order = useOrdersStore((s) => s.getById(id));
|
|
const cancelOrder = useOrdersStore((s) => s.cancelOrder);
|
|
const updateStatus = useOrdersStore((s) => s.updateStatus);
|
|
|
|
if (!order) {
|
|
return (
|
|
<Screen>
|
|
<AppHeader title="Order" />
|
|
<View style={{ padding: gutter }}>
|
|
<Text variant="body" color={colors.muted}>
|
|
Order not found.
|
|
</Text>
|
|
</View>
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
const branch = order.branchId ? getBranchById(order.branchId) : undefined;
|
|
const canCancel = !["delivered", "completed", "cancelled"].includes(order.status);
|
|
|
|
return (
|
|
<Screen>
|
|
<AppHeader
|
|
title="Order details"
|
|
subtitle={new Date(order.createdAt).toLocaleString()}
|
|
right={
|
|
<Badge label={order.status} tone={toneFor[order.status]} />
|
|
}
|
|
/>
|
|
<ScrollView
|
|
contentContainerStyle={{
|
|
paddingHorizontal: gutter,
|
|
paddingBottom: 40,
|
|
gap: 14,
|
|
}}
|
|
>
|
|
<Card>
|
|
<Text variant="caption" color={colors.subtle}>
|
|
{order.id}
|
|
</Text>
|
|
<Text variant="h2" style={{ marginTop: 4 }}>
|
|
{order.fulfillment === "pickup" ? "Pickup order" : "Delivery order"}
|
|
</Text>
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 6 }}>
|
|
{order.customerName} · {order.customerPhone}
|
|
</Text>
|
|
{order.notes ? (
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 6 }}>
|
|
Notes: {order.notes}
|
|
</Text>
|
|
) : null}
|
|
</Card>
|
|
|
|
<Section title="Items">
|
|
<Card elevated={false}>
|
|
{order.items.map((item, index) => (
|
|
<View
|
|
key={item.productId}
|
|
style={{
|
|
flexDirection: "row",
|
|
gap: 12,
|
|
paddingVertical: 12,
|
|
borderBottomWidth: index === order.items.length - 1 ? 0 : 1,
|
|
borderBottomColor: colors.line,
|
|
}}
|
|
>
|
|
<Image
|
|
source={{ uri: item.image }}
|
|
style={{
|
|
width: 52,
|
|
height: 52,
|
|
borderRadius: radii.sm,
|
|
backgroundColor: colors.surfaceAlt,
|
|
}}
|
|
resizeMode="contain"
|
|
/>
|
|
<View style={{ flex: 1 }}>
|
|
<Text variant="h3">{item.name}</Text>
|
|
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
|
|
{item.unit} · Qty {item.quantity}
|
|
{item.requiresPrescription ? " · RX" : ""}
|
|
</Text>
|
|
</View>
|
|
<Text variant="label">{formatETB(item.price * item.quantity)}</Text>
|
|
</View>
|
|
))}
|
|
</Card>
|
|
</Section>
|
|
|
|
<Section title="Fulfillment">
|
|
<Card elevated={false}>
|
|
<InfoRow
|
|
label="Method"
|
|
value={order.fulfillment === "pickup" ? "Pickup" : "Delivery"}
|
|
/>
|
|
<InfoRow label="Area" value={order.areaLabel || "—"} />
|
|
{order.addressLine ? (
|
|
<InfoRow label="Address" value={order.addressLine} />
|
|
) : null}
|
|
{branch ? (
|
|
<InfoRow label="Branch" value={`${branch.name} · ${branch.hours}`} last />
|
|
) : (
|
|
<InfoRow label="Branch" value="—" last />
|
|
)}
|
|
</Card>
|
|
</Section>
|
|
|
|
<Section title="Payment summary">
|
|
<Card elevated={false}>
|
|
<InfoRow label="Subtotal" value={formatETB(order.subtotal)} />
|
|
{(order.discount ?? 0) > 0 ? (
|
|
<InfoRow
|
|
label={`Discount${order.voucherCode ? ` (${order.voucherCode})` : ""}`}
|
|
value={`- ${formatETB(order.discount)}`}
|
|
valueColor={colors.brandText}
|
|
/>
|
|
) : null}
|
|
<InfoRow
|
|
label="Delivery fee"
|
|
value={order.deliveryFee ? formatETB(order.deliveryFee) : "Free"}
|
|
/>
|
|
{(order.corporateCovered ?? 0) > 0 ? (
|
|
<InfoRow
|
|
label="Corporate covered"
|
|
value={formatETB(order.corporateCovered!)}
|
|
valueColor={colors.brandText}
|
|
/>
|
|
) : null}
|
|
<InfoRow label="Payment method" value={order.paymentMethod} />
|
|
<InfoRow
|
|
label="Total"
|
|
value={formatETB(order.total)}
|
|
emphasis
|
|
last
|
|
/>
|
|
</Card>
|
|
</Section>
|
|
|
|
<Section title="Status timeline">
|
|
<Card elevated={false}>
|
|
{(order.timeline ?? []).map((step, index, arr) => (
|
|
<View
|
|
key={`${step.at}-${step.label}`}
|
|
style={{ flexDirection: "row", gap: 12, paddingVertical: 8 }}
|
|
>
|
|
<View style={{ alignItems: "center", width: 18 }}>
|
|
<View
|
|
style={{
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
backgroundColor: colors.brand,
|
|
marginTop: 4,
|
|
}}
|
|
/>
|
|
{index < arr.length - 1 ? (
|
|
<View
|
|
style={{
|
|
width: 2,
|
|
flex: 1,
|
|
backgroundColor: colors.brandBorder,
|
|
marginTop: 2,
|
|
}}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
<View style={{ flex: 1, paddingBottom: 8 }}>
|
|
<Text variant="h3">{step.label}</Text>
|
|
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
|
|
{new Date(step.at).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</Card>
|
|
</Section>
|
|
|
|
<View style={{ gap: 10 }}>
|
|
{order.status !== "cancelled" && order.status !== "delivered" ? (
|
|
<Button
|
|
title="Track order"
|
|
icon="navigate-outline"
|
|
onPress={() => router.push(routes.tracking(order.id))}
|
|
/>
|
|
) : null}
|
|
{order.status === "confirmed" || order.status === "paid" ? (
|
|
<Button
|
|
title="Simulate dispatch"
|
|
variant="soft"
|
|
onPress={() => updateStatus(order.id, "dispatched")}
|
|
/>
|
|
) : null}
|
|
{canCancel ? (
|
|
<Button
|
|
title="Cancel order"
|
|
variant="danger"
|
|
onPress={() =>
|
|
Alert.alert("Cancel order?", "This cannot be undone in the demo.", [
|
|
{ text: "Keep", style: "cancel" },
|
|
{
|
|
text: "Cancel order",
|
|
style: "destructive",
|
|
onPress: () => cancelOrder(order.id),
|
|
},
|
|
])
|
|
}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
}
|