From 31312688b573795afb2e64d2ad4236b67a3560b3 Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 11 May 2026 18:34:31 +0200 Subject: [PATCH] feat(orders-list): live status badge + fiat amount + manual refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups to the v1 orders-list page that emerged once the extension started transitioning orders through 'paid → accepted → ready' from the KDS: views/OrdersListPage.vue: - hydrate each entry from api.getOrder(id) on mount so the row reflects the live status (via friendlyOrderStatus) rather than the snapshot at place-time - surface the order's original fiat_amount + currency_display alongside the sat total - floating bottom-right FAB refresh button — the extension has no push channel for order status today (aiolabs/restaurant#9 will replace this with NIP-17 status DMs), so customers need an explicit way to pick up kitchen-side transitions without a full page reload. Bottom-right positioning avoids the global hub nav button at top-right. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../restaurant/views/OrdersListPage.vue | 134 ++++++++++++++++-- 1 file changed, 123 insertions(+), 11 deletions(-) diff --git a/src/modules/restaurant/views/OrdersListPage.vue b/src/modules/restaurant/views/OrdersListPage.vue index fabbc8c..c5928b2 100644 --- a/src/modules/restaurant/views/OrdersListPage.vue +++ b/src/modules/restaurant/views/OrdersListPage.vue @@ -2,27 +2,44 @@ /** * Lists past orders the customer has placed from this device. * - * Source of truth is STORAGE_SERVICE['restaurant.lastOrders.v1'] - * (newest first, cap 50) — appended to by CheckoutPage. Each - * entry is enough to display + deep-link to /orders/:id; the - * detail page re-fetches the live order over REST so we don't - * store stale status here. + * Source of truth for the *list* is STORAGE_SERVICE + * ['restaurant.lastOrders.v1'] (newest first, cap 50) — appended to + * by CheckoutPage. Each stored entry is enough to deep-link to + * /orders/:id, but it freezes the order's totals + status at the + * moment it was placed. + * + * On mount we hydrate each row from `RestaurantAPI.getOrder(id)` so + * the list shows the *live* status (e.g. an order placed an hour + * ago might be `ready` now) and the fiat amount the customer + * originally paid in. Missing / 404 orders fall back to the stored + * snapshot so a deleted order doesn't poison the page. */ import { computed, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' -import { ReceiptText } from 'lucide-vue-next' +import { ReceiptText, RefreshCw } from 'lucide-vue-next' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { + injectService, tryInjectService, SERVICE_TOKENS, } from '@/core/di-container' import type { StorageService } from '@/core/services/StorageService' +import type { RestaurantAPI } from '../services/RestaurantAPI' +import { + KNOWN_ORDER_STATUSES, + friendlyOrderStatus, + type Order, + type OrderStatus, +} from '../types/restaurant' const router = useRouter() const storage = tryInjectService( SERVICE_TOKENS.STORAGE_SERVICE ) +const api = injectService(SERVICE_TOKENS.RESTAURANT_API) interface OrderHistoryEntry { orderId: string @@ -33,19 +50,81 @@ interface OrderHistoryEntry { } const orders = ref([]) +const liveByOrderId = ref>({}) +const isRefreshing = ref(false) -onMounted(() => { +// Re-fetches every history row's live status + fiat. Used both on +// mount and from the manual refresh button — the extension doesn't +// push order-status changes today (NIP-17 status DMs are tracked in +// aiolabs/restaurant#9), so the customer hits this to pick up +// kitchen-side transitions. +async function refresh(): Promise { + isRefreshing.value = true + try { + await Promise.all( + orders.value.map(async (entry) => { + try { + const { order } = await api.getOrder(entry.orderId) + liveByOrderId.value[entry.orderId] = order + } catch { + liveByOrderId.value[entry.orderId] = null + } + }) + ) + } finally { + isRefreshing.value = false + } +} + +onMounted(async () => { orders.value = storage?.getUserData( 'restaurant.lastOrders.v1', [] ) || [] + await refresh() }) function fmtSat(value: number) { return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value / 1000)} sat` } +function fmtFiat(amount: number, currency: string) { + try { + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency, + maximumFractionDigits: 2, + }).format(amount) + } catch { + // Unknown / non-ISO currency code — fall back to a plain number. + return `${amount.toFixed(2)} ${currency}` + } +} + +function statusVariant( + status: OrderStatus | undefined +): 'default' | 'secondary' | 'outline' | 'destructive' { + const known = (KNOWN_ORDER_STATUSES as readonly string[]).includes( + status ?? '' + ) + if (!known) return 'secondary' + switch (status) { + case 'pending': + return 'outline' + case 'paid': + case 'accepted': + case 'ready': + case 'completed': + return 'default' + case 'canceled': + case 'refunded': + return 'destructive' + default: + return 'secondary' + } +} + function fmtTime(ts: number) { return new Date(ts).toLocaleString() } @@ -93,7 +172,7 @@ const grouped = computed(() => { @click="router.push(`/orders/${o.orderId}`)" > -
+

{{ o.restaurantSlug }} @@ -102,15 +181,48 @@ const grouped = computed(() => { {{ o.orderId.slice(0, 12) }}… · {{ fmtTime(o.placedAt) }}

+ + {{ friendlyOrderStatus(liveByOrderId[o.orderId]!.status) }} + +
+
+

+ {{ fmtSat(o.totalMsat) }} +

+

+ ≈ {{ fmtFiat( + liveByOrderId[o.orderId]!.fiat_amount!, + liveByOrderId[o.orderId]!.currency_display + ) }} +

- - {{ fmtSat(o.totalMsat) }} -
+ +