Certificat Let's Encrypt obtenu et renouvelé par certbot (authentificateur nginx), expiration surveillée par Prometheus. TLS selon Mozilla v6.0 intermediate, HSTS, CSP stricte avec nonce sur l'application et adaptée sur la vitrine, xterm.js servi par le portail. Page de statut prévue sur status.syllabuslab.fr, administration de Grafana et Kuma par tunnel seul. Corrige aussi une faille XSS dans la page de classe, la page /cours en erreur depuis l'étape 8, et NodeSource relancé à chaque déploiement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
10 KiB
PHP
142 lines
10 KiB
PHP
@extends('layouts.vitrine')
|
||
|
||
@section('titre', $classe->nom)
|
||
|
||
@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">
|
||
<div>
|
||
<h1 class="text-4xl font-extrabold tracking-tight">{{ $classe->nom }}</h1>
|
||
@if ($classe->formation)
|
||
<p class="mt-1 text-lg font-semibold">{{ $classe->formation }}</p>
|
||
@endif
|
||
</div>
|
||
<p class="font-mono text-sm font-semibold">Code de classe : <span class="rounded-lg border border-[#dadce0] bg-white px-2 py-1">{{ $classe->code_classe }}</span></p>
|
||
</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>
|
||
@endif
|
||
|
||
@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">
|
||
<div class="flex flex-wrap items-center justify-between gap-3 print:hidden">
|
||
<h2 id="t-fiches" class="text-2xl font-extrabold">
|
||
Fiches à imprimer
|
||
@if (session('nombre_importes'))
|
||
<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>
|
||
</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="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>
|
||
<p class="mt-2 font-mono text-sm">Identifiant<br><span class="text-lg font-bold">{{ $fiche['identifiant'] }}</span></p>
|
||
<p class="mt-2 font-mono text-sm">Code secret<br><span class="text-lg font-bold">{{ $fiche['code_secret'] }}</span></p>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</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">
|
||
<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">
|
||
@csrf
|
||
<label class="grid gap-1 font-bold">Prénom<input name="prenom" required class="w-full rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold"></label>
|
||
<label class="grid gap-1 font-bold">Nom <span class="font-medium">(facultatif, visible de toi seul)</span><input name="nom" class="w-full rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold"></label>
|
||
<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">
|
||
<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
|
||
<form action="{{ route('mes-classes.eleves.importer', $classe) }}" method="post" enctype="multipart/form-data" class="mt-4 grid gap-3">
|
||
@csrf
|
||
<input type="file" name="fichier" accept=".csv,text/csv" required class="w-full rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold">
|
||
<button type="submit" class="mt-2 inline-flex h-11 items-center justify-center rounded-full bg-[#0b57d0] px-5 font-bold text-white">Importer</button>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
|
||
<section class="mt-10 border border-[#e3e5e8] rounded-2xl bg-white" aria-labelledby="t-eleves">
|
||
<h2 id="t-eleves" class="p-6 pb-0 text-2xl font-extrabold">Élèves ({{ $classe->eleves->count() }})</h2>
|
||
@if ($classe->eleves->isEmpty())
|
||
<p class="p-6 font-semibold">Aucun élève pour l'instant.</p>
|
||
@else
|
||
<div class="overflow-x-auto">
|
||
<table class="mt-4 w-full min-w-[480px] text-left">
|
||
<thead class="bg-[#0b57d0] text-white"><tr><th class="px-4 py-2">Élève</th><th class="px-4 py-2 font-mono text-xs">Identifiant</th><th class="px-4 py-2">Dernière connexion</th><th class="px-4 py-2"></th></tr></thead>
|
||
<tbody class="divide-y divide-[#e3e5e8]">
|
||
@foreach ($classe->eleves as $eleve)
|
||
<tr>
|
||
<td class="px-4 py-3 font-extrabold">{{ $eleve->nomAffiche() }}</td>
|
||
<td class="px-4 py-3 font-mono text-sm">{{ $eleve->identifiant }}</td>
|
||
<td class="px-4 py-3 font-semibold">{{ $eleve->derniere_connexion_a?->diffForHumans() ?? 'jamais' }}</td>
|
||
<td class="px-4 py-3">
|
||
<form action="{{ route('mes-classes.eleves.regenererCode', [$classe, $eleve]) }}" method="post" data-confirmer="Régénérer le code de {{ $eleve->prenom }} ? L’ancien code ne fonctionnera plus.">
|
||
@csrf
|
||
<button type="submit" class="font-bold underline underline-offset-4">Régénérer le code</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@endif
|
||
</section>
|
||
|
||
<section class="mt-10 border border-[#e3e5e8] rounded-2xl 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>
|
||
@endif
|
||
<form action="{{ route('mes-classes.seances.store', $classe) }}" method="post" class="mt-4 flex flex-wrap items-end gap-3">
|
||
@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)
|
||
<option value="{{ $lab->id }}">{{ $lab->titre }}</option>
|
||
@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>
|
||
<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
|
||
|
||
@php($planifiees = $classe->seances()->where('statut', \App\Models\Seance::PLANIFIEE)->with('lab')->orderBy('prevue_a')->get())
|
||
@if ($planifiees->isNotEmpty())
|
||
<h3 class="mt-6 text-lg font-extrabold">Séances planifiées</h3>
|
||
<ul class="mt-2 grid gap-2">
|
||
@foreach ($planifiees as $planifiee)
|
||
<li class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#e3e5e8] p-3">
|
||
<span><span class="font-mono font-bold">{{ $planifiee->prevue_a->timezone(config('app.fuseau_utilisateurs'))->translatedFormat('d/m/Y H:i') }}</span> · <span class="font-bold">{{ $planifiee->lab->titre }}</span></span>
|
||
<form action="{{ route('seances.ouvrir', $planifiee) }}" method="post">
|
||
@csrf
|
||
<button type="submit" class="font-bold underline underline-offset-4">Ouvrir la salle d'attente</button>
|
||
</form>
|
||
</li>
|
||
@endforeach
|
||
</ul>
|
||
@endif
|
||
</section>
|
||
</div>
|
||
<script nonce="{{ Vite::cspNonce() }}">
|
||
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
|