user_id !== Auth::guard('web')->id(), 403); if ($classe->estArchivee()) { return back()->withErrors(['prevue_a' => 'Cette classe est archivée : désarchive-la pour lancer une séance.']); } $donnees = $request->validate([ 'lab_id' => ['required', 'exists:labs,id'], 'prevue_a' => ['nullable', 'date_format:Y-m-d\TH:i'], ]); if (! empty($donnees['prevue_a'])) { $prevueA = Carbon::createFromFormat('Y-m-d\TH:i', $donnees['prevue_a'], config('app.fuseau_utilisateurs'))->utc(); if ($prevueA->isPast()) { return back()->withErrors(['prevue_a' => 'Choisis une date à venir.'])->withInput(); } $classe->seances()->create([ 'lab_id' => $donnees['lab_id'], 'statut' => Seance::PLANIFIEE, 'prevue_a' => $prevueA, ]); return redirect()->route('mes-classes.show', $classe)->with('seance_planifiee', true); } $seance = $classe->seances()->create([ 'lab_id' => $donnees['lab_id'], 'statut' => Seance::SALLE_ATTENTE, 'demarree_a' => now(), ]); return redirect()->route('seances.attente', $seance); } public function ouvrir(Seance $seance): RedirectResponse { abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403); if ($seance->statut === Seance::PLANIFIEE) { $seance->update(['statut' => Seance::SALLE_ATTENTE, 'demarree_a' => now()]); } return redirect()->route('seances.attente', $seance); } public function attente(Seance $seance): View { abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403); return view('seances.attente', [ 'seance' => $seance->load(['classe', 'lab', 'labSessions.eleve', 'labSessions.machines', 'labSessions.derniereCorrection']), ]); } public function consoleProf(Seance $seance, LabSession $labSession): View { abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403); abort_if($labSession->seance_id !== $seance->id, 404); return view('seances.console-prof', [ 'seance' => $seance->load('lab'), 'labSession' => $labSession->load(['eleve', 'machines']), ]); } /** * Admettre un élève, c'est lancer son lab. Chemin rapide : une stack * prête est prise dans le stock chaud et attribuée tout de suite. * Chemin lent (stock vide) : CreerStackLab la construit (20 à 40 s). * Aussi utilisé pour relancer une session en échec. */ public function admettre(Seance $seance, LabSession $labSession, StockChaud $stock): RedirectResponse { abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403); abort_if($labSession->seance_id !== $seance->id, 404); // Mise à jour conditionnelle en une seule requête SQL : un double clic // (ou deux onglets) ne peut pas lancer deux stacks pour le même élève. $pris = LabSession::whereKey($labSession->id) ->whereIn('etat', [LabSession::EN_ATTENTE, LabSession::ECHEC]) ->update([ 'etat' => LabSession::ADMIS, 'raison_echec' => null, 'expire_a' => now()->addMinutes($labSession->lab->duree_min ?? 45), ]); if ($pris === 0) { return redirect()->route('seances.attente', $seance); } $labSession->refresh(); $pret = $stock->prendre($labSession->lab->identifiant); if ($pret !== null) { $labSession->attribuerStack($pret); $stock->confirmerAttribution($pret); } else { CreerStackLab::dispatch($labSession); } return redirect()->route('seances.attente', $seance); } /** * Un élève rejoint la séance : il patiente jusqu'à ce que le prof l'admette. */ public function rejoindre(Request $request, Seance $seance): RedirectResponse { $eleve = Auth::guard('eleve')->user(); abort_if($eleve->classe_id !== $seance->classe_id, 403); abort_if($seance->classe->estArchivee(), 403); LabSession::firstOrCreate( ['seance_id' => $seance->id, 'eleve_id' => $eleve->id], ['lab_id' => $seance->lab_id, 'etat' => LabSession::EN_ATTENTE], ); return redirect()->route('seances.eleve.attente', $seance); } public function attenteEleve(Seance $seance): View|RedirectResponse { $labSession = $this->sessionDeLEleve($seance); if ($labSession === null) { return redirect()->route('eleve'); } return view('seances.attente-eleve', [ 'seance' => $seance->load('lab'), 'labSession' => $labSession->load(['machines', 'derniereCorrection']), ]); } /** * Interrogé chaque seconde par la page d'attente de l'élève : la page se * recharge dès que l'état change (au lieu d'un rafraîchissement toutes * les 5 s, incompatible avec un lab ouvert en moins de 5 s). */ public function etatEleve(Seance $seance): JsonResponse { return response()->json(['etat' => $this->sessionDeLEleve($seance)?->etat]); } private function sessionDeLEleve(Seance $seance): ?LabSession { $eleve = Auth::guard('eleve')->user(); abort_if($eleve->classe_id !== $seance->classe_id, 403); return LabSession::where('seance_id', $seance->id) ->where('eleve_id', $eleve->id) ->first(); } }