116 lines
7.3 KiB
Vue
116 lines
7.3 KiB
Vue
<script setup lang="ts">
|
|
definePageMeta({ layout: 'admin' })
|
|
const { data: orders } = useOrders()
|
|
|
|
const valid = computed(() => (orders.value || []).filter(o => o.status !== 'refused' && !o.refunded))
|
|
const caDay = computed(() => valid.value.reduce((a, o) => a + o.total, 0))
|
|
const nDay = computed(() => valid.value.length)
|
|
const avg = computed(() => nDay.value ? Math.round(caDay.value / nDay.value) : 0)
|
|
const livPct = computed(() => nDay.value ? Math.round(valid.value.filter(o => o.mode === 'livraison').length / nDay.value * 100) : 0)
|
|
const COMMISSION = 0.30 // hypothèse : commission Uber Eats ~30 % (à ajuster selon le contrat)
|
|
|
|
// Historique de démonstration (en production : calculé depuis la base)
|
|
const DEMO_WEEK = [41000, 38000, 52000, 47000, 69000, 76000, 61000]
|
|
const DEMO_MONTH = 1526000
|
|
const days = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']
|
|
const todayIdx = (new Date().getDay() + 6) % 7
|
|
const week = computed(() => DEMO_WEEK.map((v, i) => i === todayIdx ? caDay.value : v))
|
|
const weekTotal = computed(() => week.value.reduce((a, v) => a + v, 0))
|
|
const maxW = computed(() => Math.max(...week.value, 1))
|
|
|
|
const top = computed(() => {
|
|
const map = new Map<string, number>()
|
|
for (const o of valid.value) for (const i of o.items) map.set(i.name, (map.get(i.name) || 0) + i.qty)
|
|
const demo: Record<string, number> = { 'Le Crousty': 412, 'Le Pad Thaï': 198, 'Les Nouilles': 176, 'Le Riz Thaï': 131, 'La Baguette': 88 }
|
|
for (const [k, v] of Object.entries(demo)) map.set(k, (map.get(k) || 0) + v)
|
|
const list = [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6)
|
|
const max = list[0]?.[1] || 1
|
|
return list.map(([name, n], i) => ({ name, n, w: Math.round(n / max * 100), first: i === 0 }))
|
|
})
|
|
|
|
const HOURS = [['11h', 2], ['12h', 9], ['13h', 7], ['14h', 2], ['17h', 1], ['18h', 3], ['19h', 10], ['20h', 12], ['21h', 8], ['22h', 3]] as const
|
|
</script>
|
|
|
|
<template>
|
|
<div class="stack" style="gap: 20px">
|
|
<div>
|
|
<h1 style="font-size: 28px; font-weight: 800; color: #fff">Statistiques</h1>
|
|
<p class="small muted">Commandes passées sur le site · chiffres du jour en direct, historique de démonstration</p>
|
|
</div>
|
|
|
|
<div class="kpis">
|
|
<div class="card k"><p class="kl">CA aujourd'hui</p><p class="kv">{{ eur(caDay) }}</p><p class="ks">{{ nDay }} commandes</p></div>
|
|
<div class="card k"><p class="kl">CA semaine · mois</p><p class="kv">{{ eur(weekTotal).replace(',00', '') }}</p><p class="ks">{{ eur(DEMO_MONTH + caDay).replace(',00', '') }} ce mois</p></div>
|
|
<div class="card k"><p class="kl">Panier moyen</p><p class="kv">{{ eur(avg) }}</p><p class="ks">aujourd'hui</p></div>
|
|
<div class="card k"><p class="kl">Livraison · Retrait</p><p class="kv">{{ livPct }} % · {{ 100 - livPct }} %</p>
|
|
<div class="split"><span :style="{ flex: livPct }" style="background: var(--orange-1)" /><span :style="{ flex: 100 - livPct }" style="background: var(--cream)" /></div></div>
|
|
</div>
|
|
|
|
<section class="saved">
|
|
<LeafMonstera class="saved-leaf" color="#8CC63F" />
|
|
<div style="position: relative">
|
|
<p class="eyebrow" style="opacity: 1">Commission économisée ce mois</p>
|
|
<p class="saved-v">{{ eur(Math.round((DEMO_MONTH + caDay) * COMMISSION)).replace(',00', '') }}</p>
|
|
</div>
|
|
<p style="position: relative; flex: 1 1 300px">Ce que tu aurais reversé à Uber Eats sur les commandes passées en direct (hypothèse : commission de 30 %, à ajuster selon ton contrat). Aujourd'hui : <strong>{{ eur(Math.round(caDay * COMMISSION)) }}</strong>.</p>
|
|
</section>
|
|
|
|
<div class="row wrap" style="gap: 20px; align-items: stretch">
|
|
<section class="card ch" style="flex: 3 1 460px" aria-labelledby="ca-h">
|
|
<h2 id="ca-h" class="ct">CA par jour · cette semaine</h2>
|
|
<div class="bars" role="img" :aria-label="`CA par jour : ${days.map((d, i) => `${d} ${eur(week[i])}`).join(', ')}`">
|
|
<div v-for="(v, i) in week" :key="i" class="bar-c">
|
|
<span class="bv">{{ Math.round(v / 100) }} €</span>
|
|
<span class="bar" :style="{ height: `${Math.max(4, v / maxW * 190)}px`, background: i === todayIdx ? 'var(--orange-1)' : 'var(--orange-2)' }" />
|
|
<span class="bl" :class="{ today: i === todayIdx }">{{ days[i] }}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="card ch" style="flex: 2 1 320px" aria-labelledby="top-h">
|
|
<h2 id="top-h" class="ct">Top plats · ce mois</h2>
|
|
<ol class="top">
|
|
<li v-for="t in top" :key="t.name">
|
|
<div class="row" style="justify-content: space-between"><span style="font-weight: 700">{{ t.name }}</span><strong style="color: #fff">{{ t.n }} vendus</strong></div>
|
|
<div class="track"><span :style="{ width: t.w + '%', background: t.first ? 'var(--orange-1)' : '#8A6A4A' }" /></div>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</div>
|
|
|
|
<section class="card ch" aria-labelledby="pk-h">
|
|
<h2 id="pk-h" class="ct">Heures de pointe <span class="small muted" style="font-weight: 600">· commandes moyennes par heure</span></h2>
|
|
<div class="bars small-bars" role="img" aria-label="Pics de commandes à midi et entre 19h et 21h">
|
|
<div v-for="[h, v] in HOURS" :key="h" class="bar-c">
|
|
<span class="bv">{{ v }}</span>
|
|
<span class="bar" :style="{ height: `${Math.max(4, v / 12 * 120)}px`, background: v >= 9 ? 'var(--orange-1)' : 'var(--wood)' }" />
|
|
<span class="bl">{{ h }}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
|
|
.k { padding: 20px; }
|
|
.kl { font-size: 13px; font-weight: 700; opacity: .7; }
|
|
.kv { margin-top: 6px; font-size: 32px; font-weight: 800; color: #fff; letter-spacing: -.5px; }
|
|
.ks { margin-top: 4px; font-size: 13px; font-weight: 700; opacity: .75; }
|
|
.split { margin-top: 10px; display: flex; gap: 3px; height: 10px; border-radius: 5px; overflow: hidden; }
|
|
.saved { position: relative; overflow: hidden; display: flex; flex-wrap: wrap; align-items: center; gap: 24px; padding: 24px 28px; border-radius: 24px; background: linear-gradient(120deg, var(--green), var(--green-dark)); }
|
|
.saved-leaf { position: absolute; right: -40px; top: -60px; width: 220px; opacity: .25; }
|
|
.saved-v { font-size: 50px; font-weight: 800; color: #fff; letter-spacing: -1px; }
|
|
.ch { padding: 22px; }
|
|
.ct { font-size: 18px; font-weight: 800; color: #fff; }
|
|
.bars { margin-top: 20px; display: flex; align-items: flex-end; gap: 12px; height: 250px; }
|
|
.small-bars { height: 175px; }
|
|
.bar-c { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 6px; height: 100%; }
|
|
.bar { width: 100%; max-width: 54px; border-radius: 10px 10px 4px 4px; transform-origin: bottom; animation: grow .7s cubic-bezier(.2,.9,.3,1) both; }
|
|
.bv { font-size: 12px; font-weight: 800; color: #fff; }
|
|
.bl { font-size: 13px; font-weight: 700; opacity: .7; }
|
|
.bl.today { color: var(--orange-1); opacity: 1; }
|
|
@keyframes grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
|
|
.top { list-style: none; margin: 18px 0 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
|
|
.track { margin-top: 6px; height: 10px; border-radius: 5px; background: var(--surface-2); overflow: hidden; }
|
|
.track span { display: block; height: 100%; border-radius: 5px; transition: width .6s ease; }
|
|
</style>
|