Paillasse/portail/app/Http/Controllers/ClasseController.php
2026-10-12 22:25:56 +02:00

227 lines
8.6 KiB
PHP
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.

<?php
namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\DemandeInscription;
use App\Models\Eleve;
use App\Models\LabSession;
use App\Models\Seance;
use App\Services\GenerateurCompteEleve;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\View\View;
class ClasseController extends Controller
{
public function __construct(private readonly GenerateurCompteEleve $generateur) {}
public function index(): View
{
$classes = Auth::guard('web')->user()->classes()->withCount('eleves')->latest()->get();
return view('mes-classes.index', ['classes' => $classes]);
}
/**
* Le tableau de bord (étape 1 du chantier « vrai cockpit », 2026-10-12)
* ne montre que des chiffres réels : rien n'est affiché qui ne vienne
* pas d'une vraie table ou d'un vrai cache (CLAUDE.md : pas de KPI
* inventé). Les fonctions sans backend (quiz, DM, observations, examen)
* n'ont pas de carte ici — elles arriveront une par une.
*/
public function show(Classe $classe): View
{
$this->verifierProprietaire($classe);
// La séance la plus pertinente à montrer : en cours avant salle
// d'attente, la plus récemment démarrée d'abord.
$seance = $classe->seances()
->whereIn('statut', [Seance::SALLE_ATTENTE, Seance::EN_COURS])
->with(['lab', 'labSessions' => fn ($q) => $q->with(['eleve', 'derniereCorrection', 'machines'])])
->latest('demarree_a')
->get()
->sortByDesc(fn ($s) => $s->statut === Seance::EN_COURS)
->first();
$corrections = $seance
? LabSession::query()->whereIn('id', $seance->labSessions->pluck('id'))->whereHas('derniereCorrection')->with('derniereCorrection')->get()->pluck('derniereCorrection')
: collect();
return view('mes-classes.show', [
'classes' => Auth::guard('web')->user()->classes()->withCount('eleves')->orderBy('nom')->get(),
'classe' => $classe->load('eleves'),
'demandes' => $classe->demandesInscription()->where('statut', DemandeInscription::EN_ATTENTE)->oldest()->get(),
'seance' => $seance,
'reussiteMoyenne' => $corrections->isNotEmpty() ? (int) round($corrections->avg(fn ($c) => $c->note_max > 0 ? $c->note / $c->note_max * 100 : 0)) : null,
'mainsLevees' => ($seance && $seance->visioOuverte()) ? VisioController::nombreMainsLevees($seance) : null,
]);
}
public function storeEleve(Request $request, Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
$donnees = $request->validate([
'prenom' => ['required', 'string', 'max:100'],
'nom' => ['nullable', 'string', 'max:100'],
]);
$fiche = $this->creerEleve($classe, $donnees['prenom'], $donnees['nom'] ?? null);
return redirect()->route('mes-classes.show', $classe)
->with('fiches', [$fiche]);
}
public function importerCsv(Request $request, Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
$request->validate([
'fichier' => ['required', 'file', 'mimes:csv,txt'],
]);
$lignes = file($request->file('fichier')->getRealPath(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$fiches = [];
foreach ($lignes as $ligne) {
$colonnes = str_getcsv($ligne);
$prenom = trim($colonnes[0] ?? '');
if ($prenom === '' || Str::lower($prenom) === 'prenom') {
continue; // ligne vide ou en-tête
}
$nom = trim($colonnes[1] ?? '') ?: null;
$fiches[] = $this->creerEleve($classe, $prenom, $nom);
}
return redirect()->route('mes-classes.show', $classe)
->with('fiches', $fiches)
->with('nombre_importes', count($fiches));
}
public function regenererCode(Classe $classe, Eleve $eleve): RedirectResponse
{
$this->verifierProprietaire($classe);
abort_if($eleve->classe_id !== $classe->id, 404);
$codeSecret = $this->generateur->genererCodeSecret();
$jetonQr = Str::random(32);
$eleve->forceFill([
'code_secret' => Hash::make($codeSecret),
'jeton_qr' => $jetonQr,
])->save();
return redirect()->route('mes-classes.show', $classe)
->with('fiches', [[
'identifiant' => $eleve->identifiant,
'prenom' => $eleve->prenom,
'nom' => $eleve->nom,
'code_secret' => $codeSecret,
'jeton_qr' => $jetonQr,
]]);
}
/**
* @return array{identifiant: string, prenom: string, nom: ?string, code_secret: string, jeton_qr: string}
*/
private function creerEleve(Classe $classe, string $prenom, ?string $nom): array
{
$fiche = $this->generateur->creerDans($classe, $prenom, $nom);
unset($fiche['eleve']);
return $fiche;
}
public function accepterDemande(Classe $classe, DemandeInscription $demande): RedirectResponse
{
$this->verifierProprietaire($classe);
abort_if($demande->classe_id !== $classe->id, 404);
if ($demande->statut === DemandeInscription::EN_ATTENTE && ! $classe->estArchivee()) {
$fiche = $this->generateur->creerDans($classe, $demande->prenom, $demande->nom);
$demande->forceFill([
'statut' => DemandeInscription::ACCEPTEE,
'eleve_id' => $fiche['eleve']->id,
'code_secret_chiffre' => $fiche['code_secret'],
])->save();
}
return redirect()->route('mes-classes.show', $classe);
}
public function refuserDemande(Classe $classe, DemandeInscription $demande): RedirectResponse
{
$this->verifierProprietaire($classe);
abort_if($demande->classe_id !== $classe->id, 404);
$demande->forceFill(['statut' => DemandeInscription::REFUSEE])->save();
return redirect()->route('mes-classes.show', $classe);
}
public function nombreDemandes(Classe $classe): JsonResponse
{
$this->verifierProprietaire($classe);
return response()->json(['en_attente' => $classe->demandesInscription()->where('statut', DemandeInscription::EN_ATTENTE)->count()]);
}
/**
* Nouvelles fiches (nouveau code, nouveau QR) pour les élèves cochés :
* la fiche n'est visible qu'une fois, ceci permet de la refaire en lot.
*/
public function nouvellesFiches(Request $request, Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
$ids = $request->validate(['eleves' => ['required', 'array'], 'eleves.*' => ['integer']])['eleves'];
$fiches = [];
foreach ($classe->eleves()->whereIn('id', $ids)->orderBy('prenom')->get() as $eleve) {
$codeSecret = $this->generateur->genererCodeSecret();
$jetonQr = Str::random(32);
$eleve->forceFill(['code_secret' => Hash::make($codeSecret), 'jeton_qr' => $jetonQr])->save();
$fiches[] = ['identifiant' => $eleve->identifiant, 'prenom' => $eleve->prenom, 'nom' => $eleve->nom, 'code_secret' => $codeSecret, 'jeton_qr' => $jetonQr];
}
return redirect()->route('mes-classes.show', $classe)->with('fiches', $fiches);
}
/**
* Fin d'année : plus de séance possible, et suppression définitive de la
* classe et des données de ses élèves 90 jours plus tard.
*/
public function archiver(Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
if ($classe->aDesLabsEnCours()) {
return back()->withErrors(['archivage' => 'Des élèves ont encore un lab en cours : termine la séance avant d’archiver.']);
}
$classe->forceFill(['archivee_a' => now()])->save();
$classe->seances()->whereIn('statut', [Seance::PLANIFIEE, Seance::SALLE_ATTENTE, Seance::EN_COURS])
->update(['statut' => Seance::TERMINEE, 'terminee_a' => now()]);
return redirect()->route('mes-classes.show', $classe);
}
public function desarchiver(Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
$classe->forceFill(['archivee_a' => null])->save();
return redirect()->route('mes-classes.show', $classe);
}
private function verifierProprietaire(Classe $classe): void
{
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
}
}