permet aux élèves de rejoindre leur classe avec son code, et les fiches en lot
L'élève saisit le code de classe et son prénom, le prof accepte, l'élève voit une seule fois son identifiant et son code. Aucun e-mail. Le prof peut aussi régénérer les fiches de plusieurs élèves d'un coup. Procédure pour prouver la copie de la clé des sauvegardes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1ac8d4773a
commit
dd4f9c3c55
21 changed files with 573 additions and 26 deletions
|
|
@ -15,6 +15,7 @@
|
|||
<label class="grid gap-2 font-extrabold">Identifiant<input name="identifiant" value="{{ old('identifiant') }}" autocomplete="username" class="w-full rounded-xl border border-[#c4c7c5] bg-white px-4 py-3 text-base font-semibold text-black placeholder:text-[#5f6368] focus:border-[#0b57d0] focus:outline-none focus:ring-2 focus:ring-[#0b57d0]/30 font-mono" placeholder="lea.m.2nde-b"></label>
|
||||
<label class="grid gap-2 font-extrabold">Code secret<input type="password" name="code" autocomplete="current-password" class="w-full rounded-xl border border-[#c4c7c5] bg-white px-4 py-3 text-base font-semibold text-black placeholder:text-[#5f6368] focus:border-[#0b57d0] focus:outline-none focus:ring-2 focus:ring-[#0b57d0]/30 font-mono"></label>
|
||||
<p class="font-medium">Ils sont sur ta fiche de connexion. Tu peux aussi scanner le QR.</p>
|
||||
<p class="font-medium">Pas encore d'identifiant ? <a href="{{ route('rejoindre') }}" class="font-bold underline underline-offset-4">Rejoins ta classe avec son code</a>.</p>
|
||||
<div><button type="submit" style="display:inline-flex;align-items:center;gap:10px;height:52px;padding:0 28px;border:0;border-radius:999px;background:#0b57d0;color:#fff;font-size:16px;font-weight:700;cursor:pointer">Entrer <span class="font-mono" aria-hidden="true">→</span></button></div>
|
||||
</form>
|
||||
<form action="{{ route('connexion.prof') }}" method="post" class="grid content-start gap-5 p-8" style="border:1px solid #e3e5e8;border-radius:24px;background:#fff">
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
<li><strong>aucune adresse e-mail</strong> ;</li>
|
||||
<li>sessions de lab : date, état, machines virtuelles attribuées ;</li>
|
||||
<li>résultats de la vérification automatique (réussi ou non, note, message par vérification).</li>
|
||||
<li>s'il rejoint sa classe avec le code de classe : le prénom et le nom (facultatif) qu'il saisit, le temps que l'enseignant accepte ou refuse sa demande.</li>
|
||||
</ul>
|
||||
<p>Ce que l’élève tape dans la console de sa machine virtuelle n’est pas enregistré par Syllabus. Son enseignant peut suivre la console en direct, en lecture seule, pendant la séance.</p>
|
||||
<h3>Enseignants</h3>
|
||||
|
|
@ -70,6 +71,7 @@
|
|||
<ul>
|
||||
<li><strong>Machines virtuelles</strong> et leur contenu : supprimées à la fin de la session de lab (45 minutes par défaut).</li>
|
||||
<li><strong>Classe et données de ses élèves</strong> : jusqu’à l’archivage de la classe par l’enseignant, puis <strong>supprimées automatiquement 90 jours après l’archivage</strong>.</li>
|
||||
<li><strong>Demande pour rejoindre une classe</strong> : supprimée dès que l’élève a reçu son identifiant, et au plus tard une heure après son envoi.</li>
|
||||
<li><strong>Compte enseignant</strong> : tant qu’il est utilisé ; supprimé sur demande.</li>
|
||||
<li><strong>Journaux techniques</strong> : 14 jours.</li>
|
||||
<li><strong>Sauvegardes</strong> : chiffrées, conservées au plus 5 semaines (7 sauvegardes quotidiennes et 4 hebdomadaires). Une donnée supprimée disparaît donc des sauvegardes au plus tard 5 semaines après sa suppression.</li>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,33 @@
|
|||
<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
|
||||
|
||||
@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">
|
||||
<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>
|
||||
<p class="mt-4 font-mono text-5xl font-bold tracking-[0.3em]">{{ $classe->code_classe }}</p>
|
||||
<h3 class="mt-6 text-lg font-extrabold">Demandes en attente ({{ $demandes->count() }})</h3>
|
||||
@if ($demandes->isEmpty())
|
||||
<p class="mt-2 font-semibold text-[#5f6368]">Aucune pour l'instant. Cette liste se met à jour toute seule.</p>
|
||||
@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">
|
||||
<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>
|
||||
<form action="{{ route('mes-classes.demandes.refuser', [$classe, $demande]) }}" method="post">@csrf<button type="submit" class="inline-flex h-10 items-center rounded-full border border-[#dadce0] px-4 font-bold">Refuser</button></form>
|
||||
</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
<div class="justify-self-center">{!! \App\Support\QrCode::svg(route('rejoindre', ['code' => $classe->code_classe])) !!}</div>
|
||||
</section>
|
||||
@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">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 print:hidden">
|
||||
|
|
@ -69,16 +96,26 @@
|
|||
</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>
|
||||
<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())
|
||||
<form id="form-fiches" action="{{ route('mes-classes.fiches', $classe) }}" method="post" data-confirmer="Générer de nouvelles fiches pour les élèves cochés ? Leurs anciens codes ne fonctionneront plus.">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex h-10 items-center rounded-full border border-[#0b57d0] px-4 font-bold text-[#0b57d0]">Nouvelles fiches pour la sélection</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@error('eleves')<p class="px-6 pt-2 font-semibold text-[#A63D2A]">Coche au moins un élève.</p>@enderror
|
||||
@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>
|
||||
<thead class="bg-[#0b57d0] text-white"><tr><th class="px-4 py-2"><input type="checkbox" data-tout-cocher aria-label="Tout sélectionner"></th><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"><input type="checkbox" name="eleves[]" value="{{ $eleve->id }}" form="form-fiches" aria-label="Sélectionner {{ $eleve->prenom }}"></td>
|
||||
<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>
|
||||
|
|
@ -165,6 +202,22 @@
|
|||
@endif
|
||||
</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(); });
|
||||
|
|
|
|||
21
portail/resources/views/rejoindre/attente.blade.php
Normal file
21
portail/resources/views/rejoindre/attente.blade.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@extends('layouts.vitrine')
|
||||
|
||||
@section('titre', 'Demande envoyée')
|
||||
|
||||
@section('contenu')
|
||||
<div class="mx-auto max-w-xl px-5 py-20 text-center lg:px-10">
|
||||
<p class="font-mono text-sm font-semibold">{{ $demande->classe->nom }}</p>
|
||||
<h1 class="mt-2 text-4xl font-extrabold tracking-tight">Demande envoyée, {{ $demande->prenom }} !</h1>
|
||||
<p class="mt-4 text-lg font-semibold">Attends que ton prof l'accepte. Garde cette page ouverte : ton identifiant et ton code s'afficheront ici.</p>
|
||||
</div>
|
||||
<script nonce="{{ Vite::cspNonce() }}">
|
||||
(function () {
|
||||
setInterval(function () {
|
||||
fetch('{{ route('rejoindre.etat') }}', { headers: { 'Accept': 'application/json' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) { if (d.statut !== 'en_attente') location.reload(); })
|
||||
.catch(function () {});
|
||||
}, 2000);
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
16
portail/resources/views/rejoindre/bienvenue.blade.php
Normal file
16
portail/resources/views/rejoindre/bienvenue.blade.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
@extends('layouts.vitrine')
|
||||
|
||||
@section('titre', 'Bienvenue')
|
||||
|
||||
@section('contenu')
|
||||
<div class="mx-auto max-w-xl px-5 py-14 lg:px-10">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight">Bienvenue, {{ $eleve->prenom }} !</h1>
|
||||
<p class="mt-3 text-lg font-semibold">Note ou photographie ces deux informations <strong>maintenant</strong> : elles ne s'afficheront plus jamais. Tu en auras besoin pour te reconnecter.</p>
|
||||
<div class="mt-8 grid gap-4 rounded-2xl border-2 border-[#13233A] bg-[#fef7e0] p-6">
|
||||
<p class="font-mono text-sm font-semibold">Identifiant<br><span class="text-3xl font-bold">{{ $eleve->identifiant }}</span></p>
|
||||
<p class="font-mono text-sm font-semibold">Code secret<br><span class="text-3xl font-bold tracking-[0.2em]">{{ $codeSecret }}</span></p>
|
||||
</div>
|
||||
<p class="mt-4 font-semibold">Si tu le perds, ton prof peut te donner un nouveau code.</p>
|
||||
<a href="{{ route('eleve') }}" class="mt-8 inline-flex h-12 items-center justify-center rounded-full bg-[#0b57d0] px-6 text-lg font-bold text-white">C'est noté, aller à mon espace</a>
|
||||
</div>
|
||||
@endsection
|
||||
28
portail/resources/views/rejoindre/formulaire.blade.php
Normal file
28
portail/resources/views/rejoindre/formulaire.blade.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
@extends('layouts.vitrine')
|
||||
|
||||
@section('titre', 'Rejoindre ma classe')
|
||||
|
||||
@section('contenu')
|
||||
<div class="mx-auto max-w-xl px-5 py-14 lg:px-10">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight">Rejoindre ma classe</h1>
|
||||
<p class="mt-3 text-lg font-semibold">Ton prof t'a donné un code de classe à 6 caractères. Ton prof va accepter ta demande, puis tu recevras ton identifiant et ton code secret sur cet écran.</p>
|
||||
<form action="{{ route('rejoindre.demander') }}" method="post" class="mt-8 grid gap-4 rounded-2xl border border-[#e3e5e8] bg-white p-6">
|
||||
@csrf
|
||||
<label class="grid gap-1 font-bold">Code de la classe
|
||||
<input name="code" value="{{ old('code', $code) }}" required maxlength="6" autocomplete="off" autocapitalize="characters" spellcheck="false"
|
||||
class="rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-mono text-2xl font-bold uppercase tracking-[0.3em]">
|
||||
</label>
|
||||
@error('code')<p class="font-semibold text-[#A63D2A]">{{ $message }}</p>@enderror
|
||||
<label class="grid gap-1 font-bold">Ton prénom
|
||||
<input name="prenom" value="{{ old('prenom') }}" required maxlength="100" autocomplete="given-name" class="rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold">
|
||||
</label>
|
||||
@error('prenom')<p class="font-semibold text-[#A63D2A]">{{ $message }}</p>@enderror
|
||||
<label class="grid gap-1 font-bold">Ton nom <span class="font-medium">(facultatif, visible de ton prof seulement)</span>
|
||||
<input name="nom" value="{{ old('nom') }}" maxlength="100" autocomplete="family-name" class="rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold">
|
||||
</label>
|
||||
<p class="text-sm font-semibold text-[#5f6368]">Aucune adresse e-mail n'est demandée.</p>
|
||||
<button type="submit" class="inline-flex h-12 items-center justify-center rounded-full bg-[#0b57d0] px-6 text-lg font-bold text-white">Envoyer ma demande</button>
|
||||
</form>
|
||||
<p class="mt-6 font-semibold">Tu as déjà un identifiant ? <a href="{{ route('login') }}" class="underline underline-offset-4">Connecte-toi</a>.</p>
|
||||
</div>
|
||||
@endsection
|
||||
10
portail/resources/views/rejoindre/refusee.blade.php
Normal file
10
portail/resources/views/rejoindre/refusee.blade.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
@extends('layouts.vitrine')
|
||||
|
||||
@section('titre', 'Demande refusée')
|
||||
|
||||
@section('contenu')
|
||||
<div class="mx-auto max-w-xl px-5 py-20 text-center lg:px-10">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight">Ton prof n'a pas accepté la demande</h1>
|
||||
<p class="mt-4 text-lg font-semibold">Vérifie le code de la classe et ton prénom avec lui, puis <a href="{{ route('rejoindre') }}" class="underline underline-offset-4">réessaie</a>.</p>
|
||||
</div>
|
||||
@endsection
|
||||
Loading…
Add table
Add a link
Reference in a new issue