site-panzai/pages/commande/[id].vue
2026-11-13 13:57:04 +01:00

119 lines
6.6 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { Order } from '~/server/utils/store'
const route = useRoute()
const { data: order, refresh, error } = await useFetch<Order>(`/api/orders/${route.params.id}`, { key: `order-${route.params.id}` })
useHead({ title: () => order.value ? `Commande #${order.value.num} — PANZAÏ` : 'Suivi de commande — PANZAÏ' })
// Le suivi se met à jour tout seul quand le resto change le statut
onMounted(() => {
const t = setInterval(() => refresh(), 2500)
onBeforeUnmount(() => clearInterval(t))
})
const liv = computed(() => order.value?.mode === 'livraison')
const idx = computed(() => ({ new: 0, prep: 1, ready: 2, done: 3, refused: -1 } as const)[order.value?.status || 'new'])
const steps = computed(() => [
{ label: 'Commande reçue', sub: order.value ? hhmm(order.value.createdAt) : '' },
{ label: 'En préparation au wok 🔥', sub: order.value?.readyBy ? `prête vers ${hhmm(order.value.readyBy)}` : 'en attente de confirmation du resto' },
{ label: liv.value ? 'En livraison' : 'Prête — viens la récupérer', sub: liv.value ? 'ton livreur arrive' : '2 avenue de Champagne' },
{ label: liv.value ? 'Livrée' : 'Récupérée', sub: 'bon appétit !' }
])
const headline = computed(() => {
if (!order.value) return ''
if (order.value.status === 'refused') return 'Commande annulée'
return ['C’est reçu !', 'Ça chauffe au wok', liv.value ? 'Ça arrive !' : 'C’est prêt !', 'Bon appétit !'][idx.value]
})
const eta = computed(() => {
const o = order.value
if (!o) return ''
if (o.status === 'done') return liv.value ? 'Livrée' : 'Récupérée'
if (o.status === 'refused') return '—'
if (o.status === 'ready' && !liv.value) return 'Maintenant'
if (o.readyBy) return hhmm(o.readyBy + (liv.value ? 12 * 60000 : 0))
return '~' + hhmm(o.createdAt + (liv.value ? 35 : 20) * 60000)
})
</script>
<template>
<main class="container wrap-c">
<div v-if="error" class="card" style="padding: 28px; text-align: center">
<p class="strong">Commande introuvable.</p>
<NuxtLink to="/" class="btn btn-primary" style="margin-top: 16px">Retour à l'accueil</NuxtLink>
</div>
<template v-else-if="order">
<LeafMonstera class="leaf" />
<div class="toucan-in" style="width: 90px"><Toucan :size="90" label="Toucan PANZAÏ" /></div>
<p class="small muted" style="margin-top: 12px">Commande <strong style="color: #fff">#{{ order.num }}</strong></p>
<h1 class="display" style="font-size: clamp(30px, 5vw, 46px); margin-top: 6px" aria-live="polite">{{ headline }}</h1>
<div v-if="order.status === 'refused'" class="card refused">
<p>Le restaurant n'a pas pu accepter ta commande. <template v-if="order.refunded">Tu as été intégralement remboursé.</template></p>
<a href="tel:+33485478262" class="btn btn-primary">Appeler le resto</a>
</div>
<template v-else>
<p class="muted" style="margin-top: 8px">On te prévient par SMS à chaque étape.</p>
<div class="card eta">
<div>
<p class="eyebrow">{{ liv ? 'Arrivée estimée' : 'Prête vers' }}</p>
<p class="eta-v">{{ eta }}</p>
</div>
<div style="text-align: right">
<p class="small muted">{{ liv ? 'Livraison' : 'Retrait' }}</p>
<p class="strong" style="font-size: 14px">{{ liv ? order.address : order.slot }}</p>
</div>
</div>
<ol class="steps" aria-label="Étapes de la commande">
<li v-for="(s, i) in steps" :key="i" :class="{ done: i < idx || (idx === 3 && i === 3), cur: i === idx && idx !== 3 }">
<span class="node">
<Icon v-if="i < idx || (idx === 3 && i === 3)" name="check" :stroke="3" />
<Icon v-else-if="i === idx" name="flame" :stroke="2.4" />
</span>
<div>
<p class="strong">{{ s.label }}</p>
<p class="small muted">{{ s.sub }}</p>
</div>
</li>
</ol>
</template>
<section class="card recap" aria-label="Détail de la commande">
<div v-for="(it, i) in order.items" :key="i" class="row" style="justify-content: space-between; gap: 12px">
<span>{{ it.qty }}× {{ it.name }} {{ it.size }} · {{ it.meat }}<template v-if="it.sauce"> · {{ it.sauce }}</template></span>
<strong>{{ eur(it.unit * it.qty) }}</strong>
</div>
<div v-if="order.fee" class="row muted" style="justify-content: space-between"><span>Livraison</span><span>{{ eur(order.fee) }}</span></div>
<div class="row tot" style="justify-content: space-between">
<span class="strong">{{ order.refunded ? 'Remboursé' : order.paid ? `Payé · ${payLabel[order.payment]}` : 'À régler au retrait' }}</span>
<span class="price" style="font-size: 18px">{{ eur(order.total) }}</span>
</div>
</section>
<div class="stack" style="gap: 10px; margin-top: 20px">
<a href="tel:+33485478262" class="btn btn-primary btn-lg"><Icon name="phone" /> Appeler le resto</a>
<NuxtLink to="/menu" class="btn btn-ghost">Recommander</NuxtLink>
</div>
</template>
</main>
</template>
<style scoped>
.wrap-c { position: relative; max-width: 620px; padding-top: 28px; padding-bottom: 40px; }
.leaf { position: absolute; right: -60px; top: 0; width: 170px; z-index: -1; }
.eta { margin-top: 20px; padding: 18px 20px; display: flex; align-items: center; gap: 16px; justify-content: space-between; }
.eta-v { font-size: 34px; font-weight: 800; color: #fff; letter-spacing: -.5px; }
.steps { list-style: none; margin: 26px 0 0; padding: 0; }
.steps li { position: relative; display: flex; gap: 16px; padding-bottom: 28px; }
.steps li:not(:last-child)::before { content: ''; position: absolute; left: 19px; top: 44px; bottom: 4px; width: 3px; border-radius: 2px; background: rgba(243,227,195,.14); }
.steps li.done:not(:last-child)::before { background: var(--leaf); }
.node { width: 40px; height: 40px; border-radius: 50%; border: 2px solid rgba(243,227,195,.25); flex: none; display: flex; align-items: center; justify-content: center; color: var(--bg); }
.done .node { background: var(--leaf); border: 0; }
.cur .node { background: var(--grad); border: 0; animation: ring 1.4s ease-out infinite; }
.steps li > div { padding-top: 8px; }
.refused { margin-top: 18px; padding: 20px; display: flex; flex-direction: column; gap: 14px; align-items: flex-start; border: 1px solid rgba(232,98,42,.45); }
.recap { margin-top: 8px; padding: 18px; display: flex; flex-direction: column; gap: 8px; font-size: 14px; }
.recap .tot { margin-top: 6px; padding-top: 12px; border-top: 1px solid var(--line); }
</style>