Files
chopin-frontend/src/components/Admin/AdminStat.vue
T
2026-10-08 10:06:45 +02:00

103 lines
3.3 KiB
Vue

<template>
<component :is="isClickable() ? 'button' : 'div'" :type="isClickable() ? 'button' : undefined"
:class="['adm-stat', `adm-tone-${toneName}`, { 'adm-tone-custom': !!color }]"
:style="color ? { '--adm-tone': color } : null"
@click="onClick">
<span v-if="icon" class="adm-stat__chip" aria-hidden="true"><Icon :icon="icon"/></span>
<span class="adm-stat__info">
<span class="adm-stat__label">{{ label }}</span>
<span :class="['adm-stat__value', { 'adm-stat__value--mono': mono }]"><slot>{{ value }}</slot></span>
<span v-if="sub || $slots.sub" class="adm-stat__sub"><slot name="sub">{{ sub }}</slot></span>
<AdminMeter v-if="hasProgress" :value="progress" :label="progressLabel || label" :tone="progressTone"/>
</span>
<template v-if="isClickable()">
<Icon class="adm-stat__go" icon="fa-solid fa-chevron-right" aria-hidden="true"/>
<span v-if="to" class="sr-only">, ouvrir la section {{ targetLabel }}</span>
</template>
</component>
</template>
<script setup>
/**
* Tuile de chiffre clé. `to` (id de section) la rend cliquable (bouton → adminStore.setSection),
* tout comme un écouteur @click. `progress` (0..1) ajoute une jauge sous la valeur.
*/
import { computed, inject, useAttrs } from 'vue';
import { useAdminStore } from '@/stores/adminStore';
import AdminMeter from './AdminMeter.vue';
import { ADMIN_NAV_KEY, normalizeTone, toneValidator } from './adminUi';
import './admin.scss';
const props = defineProps({
label: {
type: String,
required: true
},
value: {
default: 0
},
sub: {
type: String,
default: null
},
icon: {
type: String,
default: "fa-solid fa-chart-bar"
},
// Historique : couleur libre de la pastille (prioritaire sur `tone`)
color: {
type: String,
default: null
},
tone: {
type: String,
default: 'default',
validator: toneValidator
},
// Id de section ('players', 'users'…) : la tuile ouvre cette section
to: {
type: String,
default: null
},
// Libellé lisible de la section cible, pour les lecteurs d'écran
toLabel: {
type: String,
default: null
},
progress: {
type: Number,
default: null
},
progressLabel: {
type: String,
default: null
},
// 'auto' : rose, orange au-delà de 75 %, rouge au-delà de 90 %
progressTone: {
type: String,
default: 'auto'
},
mono: {
type: Boolean,
default: false
}
});
const attrs = useAttrs();
const adminStore = useAdminStore();
const nav = inject(ADMIN_NAV_KEY, null);
const toneName = computed(() => normalizeTone(props.tone));
// Pas de defineEmits('click') : l'écouteur du parent reste dans attrs et passe au bouton racine.
// useAttrs() n'est pas réactif : fonction relue à chaque rendu plutôt qu'un computed
function isClickable() {
return !!props.to || !!attrs.onClick;
}
const hasProgress = computed(() => typeof props.progress === 'number' && Number.isFinite(props.progress));
const targetLabel = computed(() => props.toLabel || nav?.sections?.find(s => s.id === props.to)?.label || props.to);
function onClick() {
if (props.to) adminStore.setSection(props.to);
}
</script>