reconstruit le tableau de bord du prof dans le style de /cockpit, avec les vraies données (chantier vrai cockpit, étape 1)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-12 22:25:56 +02:00
parent 78030de488
commit 0573894721
6 changed files with 302 additions and 41 deletions

View file

@ -2,11 +2,49 @@
@section('titre', $classe->nom)
{{-- Tableau de bord prof (chantier « vrai cockpit », étape 1 — 2026-10-12) :
même mise en page que la démonstration /cockpit (sélecteur de classe,
cartes d'indicateurs, tableau des élèves), mais chaque chiffre vient
d'une vraie table ou d'un vrai cache. Rien n'est inventé (CLAUDE.md) :
les cartes des fonctions pas encore construites (quiz, DM,
observations, examen) n'existent pas ici tant qu'elles n'existent pas
vraiment — elles arriveront une par une. --}}
@section('contenu')
<div class="mx-auto max-w-5xl px-5 py-14 lg:px-10">
<div class="flex flex-wrap items-end justify-between gap-4">
@php
$etats = \App\Models\LabSession::class;
@endphp
<div class="mx-auto grid max-w-[1600px] lg:grid-cols-[240px_minmax(0,1fr)]">
<aside class="border-b border-[#e3e5e8] bg-[#f8f9fa] p-5 lg:border-b-0 lg:border-r lg:border-[#e3e5e8]">
<p class="font-mono text-[11px] font-semibold uppercase tracking-wider text-[#5f6368]">Mes classes</p>
<label class="mt-2 block">
<span class="sr-only">Changer de classe</span>
<select class="h-11 w-full rounded-lg border border-[#dadce0] bg-white px-2 font-bold" onchange="location = this.value">
@foreach ($classes as $autre)
<option value="{{ route('mes-classes.show', $autre) }}" @selected($autre->id === $classe->id)>{{ $autre->nom }} ({{ $autre->eleves_count }})</option>
@endforeach
</select>
</label>
<p class="mt-6 font-mono text-[11px] font-semibold uppercase tracking-wider text-[#5f6368]">Cette classe</p>
<ul class="mt-2 grid gap-1 text-sm font-bold">
<li><a href="#apercu" class="flex items-center rounded-lg bg-[#0b57d0] px-3 py-2 text-white">Vue d'ensemble</a></li>
<li><a href="#rejoindre" class="flex items-center rounded-lg border-2 border-transparent px-3 py-2 hover:border-black">Faire rejoindre</a></li>
<li><a href="#eleves" class="flex items-center rounded-lg border-2 border-transparent px-3 py-2 hover:border-black">Élèves</a></li>
<li><a href="#seance" class="flex items-center rounded-lg border-2 border-transparent px-3 py-2 hover:border-black">{{ $classe->estArchivee() ? 'Classe archivée' : 'Séance' }}</a></li>
</ul>
@if ($seance)
<div class="mt-6 rounded-lg border border-[#dadce0] bg-white p-3 text-sm font-semibold">
<p class="font-extrabold">Séance en cours</p>
<p class="mt-1">{{ $seance->lab->titre }}</p>
<p class="mt-2 font-mono text-xs">{{ $seance->statut === \App\Models\Seance::EN_COURS ? 'En cours' : 'Salle d\'attente' }}</p>
</div>
@endif
</aside>
<div class="min-w-0 p-5 lg:p-8">
<div id="apercu" class="flex flex-wrap items-end justify-between gap-4">
<div>
<h1 class="text-4xl font-extrabold tracking-tight">{{ $classe->nom }}</h1>
<p class="font-mono text-xs font-semibold text-[#5f6368]">Tableau de bord</p>
<h1 class="mt-1 text-4xl font-extrabold tracking-tight">{{ $classe->nom }}</h1>
@if ($classe->formation)
<p class="mt-1 text-lg font-semibold">{{ $classe->formation }}</p>
@endif
@ -15,11 +53,101 @@
</div>
@if (session('classe_creee'))
<p class="mt-4 border border-[#e3e5e8] rounded-2xl bg-[#e6f4ea] p-4 font-semibold">Classe créée. Ajoute maintenant tes élèves ci-dessous.</p>
<p class="mt-4 rounded-lg border border-[#e3e5e8] bg-[#e6f4ea] p-4 font-semibold">Classe créée. Ajoute maintenant tes élèves ci-dessous.</p>
@endif
{{-- Indicateurs réels : rien ici qui ne vienne d'une vraie table ou d'un vrai cache. --}}
<ul class="mt-6 grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-4" aria-label="Indicateurs">
<li class="flex flex-col rounded-lg border border-[#dadce0] bg-white p-4">
<p class="text-sm font-bold">Élèves</p>
<p class="mt-2 font-mono text-3xl font-semibold leading-none">{{ $classe->eleves->count() }}</p>
</li>
<li class="flex flex-col rounded-lg border border-[#dadce0] bg-white p-4">
<p class="text-sm font-bold">Demandes en attente</p>
<p class="mt-2 font-mono text-3xl font-semibold leading-none">{{ $demandes->count() }}</p>
</li>
@if ($seance)
<li class="flex flex-col rounded-lg border border-[#dadce0] bg-white p-4">
<p class="text-sm font-bold">Dans la séance</p>
<p class="mt-2 font-mono text-3xl font-semibold leading-none">{{ $seance->labSessions->whereIn('etat', [$etats::ACTIF, $etats::ADMIS])->count() }}/{{ $classe->eleves->count() }}</p>
<p class="mt-1 text-xs font-bold text-[#5f6368]">admis ou en préparation</p>
</li>
@if ($reussiteMoyenne !== null)
<li class="flex flex-col rounded-lg border border-[#dadce0] bg-white p-4">
<p class="text-sm font-bold">Réussite (dernière vérif.)</p>
<p class="mt-2 font-mono text-3xl font-semibold leading-none">{{ $reussiteMoyenne }}%</p>
</li>
@endif
@if ($mainsLevees !== null)
<li class="flex flex-col rounded-lg border border-[#dadce0] bg-[#fce8e6] p-4">
<p class="text-sm font-bold">Mains levées (visio)</p>
<p class="mt-2 font-mono text-3xl font-semibold leading-none">{{ $mainsLevees }}</p>
</li>
@endif
@endif
</ul>
@if ($seance)
{{-- Vraies données de la séance en cours : un élève, son état, sa dernière vérification. --}}
<section class="mt-6 rounded-lg border border-[#dadce0] bg-white" aria-labelledby="t-seance-encours">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-[#e3e5e8] p-4">
<h2 id="t-seance-encours" class="text-lg font-extrabold">{{ $seance->lab->titre }} — {{ $seance->statut === \App\Models\Seance::EN_COURS ? 'en cours' : 'salle d\'attente' }}</h2>
<span class="flex flex-wrap gap-2">
<a href="{{ route('seances.attente', $seance) }}" class="inline-flex h-10 items-center rounded-lg border border-[#dadce0] px-4 text-sm font-extrabold hover:bg-[#f8f9fa]">Salle d'attente</a>
<a href="{{ route('seances.visio.prof', $seance) }}" class="inline-flex h-10 items-center rounded-lg bg-[#0b57d0] px-4 text-sm font-extrabold text-white">{{ $seance->visioOuverte() ? 'Rejoindre la visio' : 'Lancer la visio' }}</a>
</span>
</div>
@if ($seance->labSessions->isEmpty())
<p class="p-4 font-semibold">Personne n'a encore rejoint.</p>
@else
<div class="overflow-x-auto"><table class="w-full min-w-[640px] border-collapse text-left text-sm">
<thead class="border-b border-[#e3e5e8] bg-[#f8f9fa] font-mono text-[11px] uppercase tracking-wider">
<tr><th class="px-4 py-3 font-semibold">Élève</th><th class="px-4 py-3 font-semibold">État</th><th class="px-4 py-3 font-semibold">Dernière vérification</th><th class="px-4 py-3 font-semibold"></th></tr>
</thead>
<tbody class="divide-y divide-[#e3e5e8]">
@foreach ($seance->labSessions as $labSession)
<tr>
<td class="px-4 py-3 font-extrabold">{{ $labSession->eleve->nomAffiche() }}</td>
<td class="px-4 py-3">
@switch($labSession->etat)
@case($etats::EN_ATTENTE)
<form action="{{ route('seances.admettre', [$seance, $labSession]) }}" method="post">@csrf<button type="submit" class="inline-flex h-9 items-center rounded-lg bg-[#0b57d0] px-3 text-xs font-extrabold text-white">Admettre</button></form>
@break
@case($etats::ADMIS)
<span class="rounded-lg border border-[#dadce0] bg-[#fef7e0] px-2 py-0.5 font-mono text-xs font-bold">Préparation des VM…</span>
@break
@case($etats::ACTIF)
<span class="rounded-lg border border-[#dadce0] bg-[#e6f4ea] px-2 py-0.5 font-mono text-xs font-bold">Admis</span>
@break
@case($etats::ECHEC)
<span class="rounded-lg border border-[#A63D2A] bg-[#fce8e6] px-2 py-0.5 font-mono text-xs font-bold" title="{{ $labSession->raison_echec }}">Échec (3 essais)</span>
@break
@default
<span class="rounded-lg border border-[#dadce0] px-2 py-0.5 font-mono text-xs font-bold">Terminé</span>
@endswitch
</td>
<td class="px-4 py-3 font-mono font-semibold">
@if ($labSession->derniereCorrection)
<span class="{{ $labSession->derniereCorrection->reussi ? 'text-[#3C8A5A]' : '' }}">{{ $labSession->derniereCorrection->reussi ? 'Réussi' : 'En cours' }} · {{ $labSession->derniereCorrection->note }}/{{ $labSession->derniereCorrection->note_max }}</span>
@else
—
@endif
</td>
<td class="px-4 py-3 text-right">
@if ($labSession->etat === $etats::ACTIF)
<a href="{{ route('seances.console.prof.page', [$seance, $labSession]) }}" class="font-bold underline underline-offset-4">Voir sa console</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table></div>
@endif
</section>
@endif
@unless ($classe->estArchivee())
<section class="mt-10 grid gap-6 rounded-2xl border border-[#e3e5e8] bg-white p-6 md:grid-cols-[1fr_auto]" aria-labelledby="t-rejoindre">
<section id="rejoindre" class="mt-6 grid gap-6 rounded-lg border border-[#dadce0] bg-white p-6 md:grid-cols-[1fr_auto]" aria-labelledby="t-rejoindre">
<div>
<h2 id="t-rejoindre" class="text-2xl font-extrabold">Faire rejoindre tes élèves</h2>
<p class="mt-2 font-semibold">Projette ce code ou ce QR. Chaque élève va sur <span class="font-mono">{{ preg_replace('#^https?://#', '', route('rejoindre')) }}</span>, tape le code et son prénom ; tu acceptes sa demande ici, et il reçoit à l'écran son identifiant et son code secret. Aucun e-mail.</p>
@ -30,7 +158,7 @@
@else
<ul class="mt-2 grid gap-2">
@foreach ($demandes as $demande)
<li class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#e3e5e8] p-3">
<li class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-[#e3e5e8] p-3">
<span class="font-extrabold">{{ $demande->prenom }}@if ($demande->nom) {{ $demande->nom }}@endif <span class="font-mono text-sm font-semibold text-[#5f6368]">· {{ $demande->created_at->diffForHumans() }}</span></span>
<span class="flex gap-2">
<form action="{{ route('mes-classes.demandes.accepter', [$classe, $demande]) }}" method="post">@csrf<button type="submit" class="inline-flex h-10 items-center rounded-full bg-[#0b57d0] px-4 font-bold text-white">Accepter</button></form>
@ -46,7 +174,7 @@
@endunless
@if (session('fiches'))
<section class="mt-10 border border-[#e3e5e8] rounded-2xl bg-[#fef7e0] p-6 print:border-0 print:bg-white" aria-labelledby="t-fiches">
<section class="mt-6 rounded-lg border border-[#e3e5e8] bg-[#fef7e0] p-6 print:border-0 print:bg-white" aria-labelledby="t-fiches">
<div class="flex flex-wrap items-center justify-between gap-3 print:hidden">
<h2 id="t-fiches" class="text-2xl font-extrabold">
Fiches à imprimer
@ -54,12 +182,12 @@
<span class="font-mono text-sm font-semibold">({{ session('nombre_importes') }} élève(s) importé(s))</span>
@endif
</h2>
<button type="button" data-imprimer class="border border-[#dadce0] rounded-lg bg-black px-4 py-2 font-extrabold text-white">Imprimer</button>
<button type="button" data-imprimer class="rounded-lg border border-[#dadce0] bg-black px-4 py-2 font-extrabold text-white">Imprimer</button>
</div>
<p class="mt-2 font-semibold print:hidden">Le code secret ne sera plus jamais affiché après cette page : imprime ou note-le maintenant.</p>
<div class="mt-6 grid gap-4 sm:grid-cols-2 print:grid-cols-1">
@foreach (session('fiches') as $fiche)
<div class="flex items-center gap-4 border border-[#dadce0] rounded-2xl bg-white p-5 print:break-inside-avoid">
<div class="flex items-center gap-4 rounded-lg border border-[#dadce0] bg-white p-5 print:break-inside-avoid">
<div class="shrink-0">{!! \App\Support\QrCode::svg(route('connexion.qr', $fiche['jeton_qr'])) !!}</div>
<div class="min-w-0">
<p class="text-lg font-extrabold">{{ $fiche['prenom'] }}@if($fiche['nom']) {{ $fiche['nom'] }}@endif</p>
@ -72,8 +200,8 @@
</section>
@endif
<div class="mt-10 grid gap-6 sm:grid-cols-2">
<section class="border border-[#e3e5e8] rounded-2xl bg-white p-6" aria-labelledby="t-ajout">
<div class="mt-6 grid gap-6 sm:grid-cols-2">
<section class="rounded-lg border border-[#dadce0] bg-white p-6" aria-labelledby="t-ajout">
<h2 id="t-ajout" class="text-xl font-extrabold">Ajouter un élève</h2>
@error('prenom')<p class="mt-2 font-bold text-[#A63D2A]">{{ $message }}</p>@enderror
<form action="{{ route('mes-classes.eleves.store', $classe) }}" method="post" class="mt-4 grid gap-3">
@ -83,7 +211,7 @@
<button type="submit" class="mt-2 inline-flex h-11 items-center justify-center rounded-full bg-[#0b57d0] px-5 font-bold text-white">Ajouter</button>
</form>
</section>
<section class="border border-[#e3e5e8] rounded-2xl bg-white p-6" aria-labelledby="t-import">
<section class="rounded-lg border border-[#dadce0] bg-white p-6" aria-labelledby="t-import">
<h2 id="t-import" class="text-xl font-extrabold">Importer une liste (CSV)</h2>
<p class="mt-2 font-semibold">Une ligne par élève : <code class="font-mono">prenom,nom</code> (le nom peut être vide).</p>
@error('fichier')<p class="mt-2 font-bold text-[#A63D2A]">{{ $message }}</p>@enderror
@ -95,7 +223,7 @@
</section>
</div>
<section class="mt-10 border border-[#e3e5e8] rounded-2xl bg-white" aria-labelledby="t-eleves">
<section id="eleves" class="mt-6 rounded-lg border border-[#dadce0] bg-white" aria-labelledby="t-eleves">
<div class="flex flex-wrap items-center justify-between gap-3 p-6 pb-0">
<h2 id="t-eleves" class="text-2xl font-extrabold">Élèves ({{ $classe->eleves->count() }})</h2>
@if ($classe->eleves->isNotEmpty())
@ -133,8 +261,9 @@
@endif
</section>
<div id="seance">
@if ($classe->estArchivee())
<section class="mt-10 rounded-2xl border border-[#A63D2A] bg-[#fce8e6] p-6" aria-labelledby="t-archivee">
<section class="mt-6 rounded-lg border border-[#A63D2A] bg-[#fce8e6] p-6" aria-labelledby="t-archivee">
<h2 id="t-archivee" class="text-2xl font-extrabold">Classe archivée</h2>
<p class="mt-2 font-semibold">
Archivée le {{ $classe->archivee_a->timezone(config('app.fuseau_utilisateurs'))->format('d/m/Y') }}.
@ -147,11 +276,11 @@
</form>
</section>
@else
<section class="mt-10 border border-[#e3e5e8] rounded-2xl bg-white p-6" aria-labelledby="t-seance">
<section class="mt-6 rounded-lg border border-[#dadce0] bg-white p-6" aria-labelledby="t-seance">
<h2 id="t-seance" class="text-2xl font-extrabold">Lancer ou planifier une séance</h2>
<p class="mt-2 font-semibold">Les élèves qui rejoignent patientent dans la salle d'attente jusqu'à ce que tu les admettes. Si tu planifies la séance, les machines de toute la classe sont préparées 10 minutes avant : elles s'ouvrent alors en quelques secondes.</p>
@if (session('seance_planifiee'))
<p class="mt-4 rounded-2xl border border-[#e3e5e8] bg-[#e6f4ea] p-4 font-semibold">Séance planifiée.</p>
<p class="mt-4 rounded-lg border border-[#e3e5e8] bg-[#e6f4ea] p-4 font-semibold">Séance planifiée.</p>
@endif
<form action="{{ route('mes-classes.seances.store', $classe) }}" method="post" class="mt-4 flex flex-wrap items-end gap-3">
@csrf
@ -184,7 +313,7 @@
@endif
</section>
<section class="mt-10 rounded-2xl border border-[#e3e5e8] bg-white p-6" aria-labelledby="t-archiver">
<section class="mt-6 rounded-lg border border-[#e3e5e8] bg-white p-6" aria-labelledby="t-archiver">
<h2 id="t-archiver" class="text-2xl font-extrabold">Fin d'année</h2>
<p class="mt-2 font-semibold">
Archiver la classe ferme ses séances. {{ \App\Models\Classe::DELAI_PURGE_JOURS }} jours plus tard, la classe
@ -200,27 +329,37 @@
</form>
</section>
@endif
</div>
</div>
<script nonce="{{ Vite::cspNonce() }}">
document.querySelectorAll('[data-tout-cocher]').forEach(function (c) {
c.addEventListener('change', function () { document.querySelectorAll('input[name="eleves[]"]').forEach(function (x) { x.checked = c.checked; }); });
});
@unless ($classe->estArchivee())
(function () {
var vues = {{ $demandes->count() }};
setInterval(function () {
fetch('{{ route('mes-classes.demandes.nombre', $classe) }}', { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.json(); })
.then(function (d) {
var occupe = document.activeElement && /INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName);
if (d.en_attente !== vues && !occupe) location.reload();
}).catch(function () {});
}, 4000);
})();
@endunless
document.querySelectorAll('[data-imprimer]').forEach(function (b) { b.addEventListener('click', function () { window.print(); }); });
document.querySelectorAll('form[data-confirmer]').forEach(function (f) {
f.addEventListener('submit', function (e) { if (!window.confirm(f.dataset.confirmer)) e.preventDefault(); });
});
</script>
</div>
<script nonce="{{ Vite::cspNonce() }}">
document.querySelectorAll('[data-tout-cocher]').forEach(function (c) {
c.addEventListener('change', function () { document.querySelectorAll('input[name="eleves[]"]').forEach(function (x) { x.checked = c.checked; }); });
});
@unless ($classe->estArchivee())
(function () {
var vues = {{ $demandes->count() }};
setInterval(function () {
fetch('{{ route('mes-classes.demandes.nombre', $classe) }}', { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.json(); })
.then(function (d) {
var occupe = document.activeElement && /INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName);
if (d.en_attente !== vues && !occupe) location.reload();
}).catch(function () {});
}, 4000);
})();
@endunless
@if ($seance)
// La séance en cours bouge vite (admissions, corrections, mains levées) :
// un rafraîchissement discret suffit, sans rien casser si le prof tape.
setInterval(function () {
var occupe = document.activeElement && /INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName);
if (!occupe) location.reload();
}, 15000);
@endif
document.querySelectorAll('[data-imprimer]').forEach(function (b) { b.addEventListener('click', function () { window.print(); }); });
document.querySelectorAll('form[data-confirmer]').forEach(function (f) {
f.addEventListener('submit', function (e) { if (!window.confirm(f.dataset.confirmer)) e.preventDefault(); });
});
</script>
@endsection