119 lines
6.6 KiB
Vue
119 lines
6.6 KiB
Vue
<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>
|