ajoute la surveillance d'examen (chantier vrai cockpit, dernière étape)
Une séance peut être lancée en mode examen avec une durée dédiée. Réutilise le mécanisme d'expiration déjà en place (labs:expirer-sessions, expire_a) plutôt que d'en construire un nouveau. Le navigateur de l'élève signale les changements d'onglet et les sorties de plein écran (evenements_examen : un type et un horodatage, rien d'autre - pas de capture d'écran ni de webcam) ; le prof voit le compte par élève et le temps restant dans le cockpit de sa classe et la salle d'attente. Choix d'Adam après discussion des options (suivi chronométré simple / + détection d'onglet / + tentative de plein écran) : la troisième, honnêtement présentée - un navigateur ne permet pas de vraiment empêcher de sortir, seulement de le signaler. Avec cette étape, les quatre fonctions fictives du tableau de bord démo (quiz, devoirs, observations, examen) sont maintenant réelles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
954bb6ca6c
commit
f2bca621cd
15 changed files with 392 additions and 11 deletions
|
|
@ -153,7 +153,11 @@
|
|||
{{-- 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>
|
||||
<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' }}
|
||||
@if ($seance->mode_examen)
|
||||
<span class="rounded-lg border border-[#A63D2A] bg-[#fce8e6] px-2 py-0.5 font-mono text-xs font-bold align-middle">Mode examen · {{ $seance->duree_examen_min }} min</span>
|
||||
@endif
|
||||
</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>
|
||||
|
|
@ -164,7 +168,9 @@
|
|||
@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>
|
||||
<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>
|
||||
@if ($seance->mode_examen)<th class="px-4 py-3 font-semibold">Sorties</th>@endif
|
||||
<th class="px-4 py-3 font-semibold"></th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-[#e3e5e8]">
|
||||
@foreach ($seance->labSessions as $labSession)
|
||||
|
|
@ -195,6 +201,15 @@
|
|||
—
|
||||
@endif
|
||||
</td>
|
||||
@if ($seance->mode_examen)
|
||||
<td class="px-4 py-3 font-mono font-semibold">
|
||||
@if ($labSession->evenementsExamen->isNotEmpty())
|
||||
<span class="text-[#A63D2A]">{{ $labSession->evenementsExamen->count() }}</span>
|
||||
@else
|
||||
0
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
<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>
|
||||
|
|
@ -414,7 +429,7 @@
|
|||
@if (session('seance_planifiee'))
|
||||
<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">
|
||||
<form action="{{ route('mes-classes.seances.store', $classe) }}" method="post" class="mt-4 flex flex-wrap items-end gap-3" id="form-seance">
|
||||
@csrf
|
||||
<label class="grid gap-1 font-bold">Lab<select name="lab_id" required class="rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold">
|
||||
@foreach (\App\Models\Lab::orderBy('titre')->get() as $lab)
|
||||
|
|
@ -422,11 +437,17 @@
|
|||
@endforeach
|
||||
</select></label>
|
||||
<label class="grid gap-1 font-bold">Date et heure (facultatif)<input type="datetime-local" name="prevue_a" value="{{ old('prevue_a') }}" class="rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold"></label>
|
||||
<label class="flex items-center gap-2 font-bold"><input type="checkbox" name="mode_examen" value="1" id="case-mode-examen"> Mode examen</label>
|
||||
<label class="grid gap-1 font-bold" id="champ-duree-examen" hidden>Durée (minutes)<input type="number" name="duree_examen_min" min="5" max="300" value="{{ old('duree_examen_min', 60) }}" class="w-28 rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold"></label>
|
||||
<button type="submit" class="inline-flex h-11 items-center justify-center rounded-full bg-black px-5 font-bold text-white">Ouvrir ou planifier</button>
|
||||
</form>
|
||||
@error('prevue_a')
|
||||
<p class="mt-2 font-semibold text-[#A63D2A]">{{ $message }}</p>
|
||||
@enderror
|
||||
@error('duree_examen_min')
|
||||
<p class="mt-2 font-semibold text-[#A63D2A]">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="mt-2 text-sm font-semibold text-[#5f6368]">Mode examen : chronométré, détecte les changements d'onglet et propose le plein écran. Ça ne bloque pas vraiment l'élève — un navigateur ne le permet pas — mais tu vois les sorties.</p>
|
||||
|
||||
@php($planifiees = $classe->seances()->where('statut', \App\Models\Seance::PLANIFIEE)->with('lab')->orderBy('prevue_a')->get())
|
||||
@if ($planifiees->isNotEmpty())
|
||||
|
|
@ -489,6 +510,10 @@
|
|||
if (!occupe) location.reload();
|
||||
}, 15000);
|
||||
@endif
|
||||
var caseModeExamen = document.getElementById('case-mode-examen');
|
||||
if (caseModeExamen) caseModeExamen.addEventListener('change', function () {
|
||||
document.getElementById('champ-duree-examen').hidden = !caseModeExamen.checked;
|
||||
});
|
||||
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(); });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue