Files
tirages-classes/src/App.vue
T
2026-09-04 22:35:41 +02:00

27 lines
6.4 KiB
Vue
Raw 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>
import {ref,computed,onMounted} from 'vue'
import {load,get,save,remove,exportData,importData} from './storage'
import {genererId,tirer} from './models'
const classes=ref([]),view=ref('list'),active=ref(null),editId=ref(null),showHelp=ref(false),fileInput=ref(null)
const form=ref({nom:'',nombreEleves:24,ponderationMax:2})
const sorted=computed(()=>[...classes.value].sort((a,b)=>a.nom.localeCompare(b.nom,'fr')))
async function refresh(){classes.value=await load()}
function newClass(){editId.value=null;form.value={nom:'',nombreEleves:24,ponderationMax:2};view.value='form'}
async function edit(c){editId.value=c.id;form.value={nom:c.nom,nombreEleves:c.nombreEleves,ponderationMax:c.ponderationMax};view.value='form'}
async function submit(){if(!form.value.nom.trim())return alert('Nom obligatoire');if(!Number.isInteger(Number(form.value.nombreEleves))||Number(form.value.nombreEleves)<1)return alert('Nombre d’élèves invalide');if(Number(form.value.ponderationMax)<1)return alert('Pondération invalide');const old=editId.value?await get(editId.value):null;const c={id:editId.value||genererId(),nom:form.value.nom.trim(),nombreEleves:Number(form.value.nombreEleves),ponderationMax:Number(form.value.ponderationMax),dateCreation:old?.dateCreation||new Date().toISOString(),tirages:old?.tirages||[]};await save(c);await refresh();view.value='list'}
async function del(c){if(confirm(`Supprimer « ${c.nom} » ? Cette action est irréversible.`)){await remove(c.id);await refresh()}}
async function open(c){active.value=await get(c.id);view.value='detail'}
async function draw(){tirer(active.value);await save(active.value);active.value=await get(active.value.id)}
async function redraw(){if(!active.value.tirages.length)return;active.value.tirages.shift();await save(active.value);await draw()}
async function doExport(){const text=await exportData();const blob=new Blob([text],{type:'application/json'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=`tirages-export-${new Date().toISOString().slice(0,10)}.json`;a.click();URL.revokeObjectURL(url)}
function chooseImport(){fileInput.value?.click()}
async function onImport(e){const f=e.target.files?.[0];if(!f)return;try{await importData(await f.text());await refresh();alert('Import terminé. Les données du navigateur ont été remplacées par celles du fichier.')}catch(err){alert(err.message||'Import impossible.')}e.target.value=''}
onMounted(refresh)
</script>
<template><div class="app"><header><button v-if="view!=='list'" class="icon" @click="view='list';active=null">‹</button><h1>{{view==='list'?'Tirages':view==='form'?(editId?'Modifier la classe':'Nouvelle classe'):active?.nom}}</h1><div class="actions"><button v-if="view==='list'" class="icon" @click="showHelp=true">?</button><button v-if="view==='list'" class="icon" @click="newClass">+</button></div></header>
<main>
<section v-if="view==='list'"><div v-if="!sorted.length" class="empty"><b>Aucune classe</b><span>Créez une classe pour commencer.</span><button class="primary" @click="newClass">Créer une classe</button></div><div v-else class="cards"><article v-for="c in sorted" :key="c.id"><button class="main" @click="open(c)"><span><b>{{c.nom}}</b><small>{{c.nombreEleves}} élèves</small></span><strong>›</strong></button><footer><button @click="edit(c)">Modifier</button><button @click="del(c)">Supprimer</button></footer></article></div><div class="data-actions"><button @click="doExport">Exporter les données</button><button @click="chooseImport">Importer des données</button><input ref="fileInput" type="file" accept="application/json,.json" hidden @change="onImport"></div></section>
<section v-else-if="view==='form'" class="form"><label>Nom de la classe<input v-model="form.nom" placeholder="ex. 3A" autofocus></label><label>Nombre d’élèves<input v-model.number="form.nombreEleves" type="number" min="1" step="1"></label><label>Pondération maximale<input v-model.number="form.ponderationMax" type="number" min="1" step="0.1"><small>1 = équiprobable. Plus la valeur est élevée, plus les élèves qui n’ont pas été tirés récemment sont favorisés.</small></label><button class="primary" @click="submit">Enregistrer</button></section>
<section v-else><p class="muted">{{active.nombreEleves}} élèves · pondération max ×{{active.ponderationMax}}</p><div class="draw"><button class="primary" @click="draw">🎲 Tirer un élève au sort</button><strong v-if="active.tirages.length">n°{{active.tirages[0].numeroEleve}}</strong><button v-if="active.tirages.length" @click="redraw">🔁 Re-tirer (annule et retire)</button></div><h2>Historique des tirages</h2><div v-if="!active.tirages.length" class="empty small">Aucun tirage pour le moment.</div><div v-else class="history"><div v-for="(t,i) in active.tirages" :key="t.id"><b>{{t.numeroEleve}}</b><span>{{new Date(t.dateTirage).toLocaleString('fr-FR')}}</span><em v-if="i===0">dernier</em></div></div></section>
</main>
<div v-if="showHelp" class="overlay" @click.self="showHelp=false"><section class="help"><button class="close" @click="showHelp=false">×</button><h2>À propos et aide</h2><h3>Données privées et locales</h3><p>Les classes et l’historique sont stockés dans <b>le navigateur de votre appareil</b>, dans une base de données locale IndexedDB. Les données de tirage ne sont envoyées à aucun serveur.</p><p>Le serveur ne sert qu’à fournir les fichiers de l’application. Une fois la PWA installée et mise en cache, elle peut fonctionner hors connexion.</p><h3>Exporter les données</h3><p><b>Exporter</b> crée un fichier JSON contenant vos classes et leurs historiques. Gardez-le comme sauvegarde, notamment avant de changer de téléphone, de navigateur ou de supprimer les données du site.</p><p>L’export permet aussi de transférer vos données vers une autre installation grâce à <b>Importer des données</b>.</p><h3>Installer sur un mobile</h3><p><b>iPhone / iPad :</b> ouvrez l’application dans Safari, touchez <b>Partager</b>, puis <b>Sur l’écran d’accueil</b> et validez.</p><p><b>Android :</b> ouvrez l’application dans Chrome puis utilisez <b>Installer l’application</b> ou <b>Ajouter à l’écran d’accueil</b>, selon la version.</p><p class="note">L’installation nécessite une connexion HTTPS. Le nom et les menus peuvent légèrement varier selon le navigateur.</p><h3>Code source</h3><p><a href="https://git.spaier.fr/cspaier/tirages-classes" target="_blank" rel="noopener">Dépôt Git : git.spaier.fr/cspaier/tirages-classes</a></p></section></div></div></template>