Files
pta/app/Controllers/TeamController.php
2026-08-20 13:22:49 +02:00

198 lines
8.3 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
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Access;
use App\Core\Csrf;
use App\Core\JsonResponse;
use App\Core\Request;
use App\Models\AgentModel;
use App\Models\StructureModel;
use App\Models\TeamModel;
use DateTimeImmutable;
use PDO;
use Throwable;
/**
* Équipes des mercredis et des vacances.
*
* Une équipe rassemble des agents sur un lieu et une période, avec des règles
* d'encadrement propres à l'accueil collectif de mineurs : effectifs d'enfants
* par tranche d'âge, taux d'encadrement (1 agent pour 8 enfants de moins de
* 6 ans, 1 pour 12 au-delà, par défaut) et proportion minimale d'agents
* diplômés. Ces paramètres sont réglables par équipe, les seuils réglementaires
* pouvant évoluer.
*
* Une équipe suit son propre cycle de vie, distinct de celui des plannings :
* BROUILLON → A_CONTROLER → VALIDEE.
*
* Réservé au Service Enfance.
*/
final class TeamController
{
private TeamModel $teamModel;
private AgentModel $agentModel;
private StructureModel $structureModel;
private Access $access;
public function __construct(private PDO $pdo)
{
$this->teamModel = new TeamModel($pdo);
$this->agentModel = new AgentModel($pdo);
$this->structureModel = new StructureModel($pdo);
$this->access = new Access($pdo);
}
/**
* api/teams.php — liste (GET) ou création (POST) d'une équipe.
*
* Endpoint à double verbe, comme AgentController::assignments(). Le GET
* accepte un intervalle de dates facultatif pour ne montrer que les équipes
* actives sur une période.
*
* Les valeurs numériques sont bornées par max() plutôt que rejetées : un
* effectif négatif n'a pas de sens mais ne doit pas bloquer la création, un
* ratio à zéro provoquerait une division par zéro au calcul du taux
* d'encadrement.
*
* Charge utile (POST) : structure_id, type_equipe, libelle, date_debut,
* date_fin, effectifs et ratios facultatifs, csrf_token.
*/
public function index(): void
{
$this->access->requireService();
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
$start = trim((string) ($_GET['date_debut'] ?? '')) ?: null;
$end = trim((string) ($_GET['date_fin'] ?? '')) ?: null;
JsonResponse::send(200, ['teams' => $this->teamModel->all($start, $end)]);
}
Request::requireMethod('POST');
$payload = Request::json();
Csrf::assertPayload($payload);
$structureId = filter_var($payload['structure_id'] ?? null, FILTER_VALIDATE_INT);
$type = strtoupper(trim((string) ($payload['type_equipe'] ?? '')));
$label = trim((string) ($payload['libelle'] ?? ''));
$start = trim((string) ($payload['date_debut'] ?? ''));
$end = trim((string) ($payload['date_fin'] ?? ''));
$startDate = DateTimeImmutable::createFromFormat('!Y-m-d', $start);
$endDate = DateTimeImmutable::createFromFormat('!Y-m-d', $end);
if (!$structureId || $this->structureModel->findActive((int) $structureId) === null
|| !in_array($type, ['MERCREDI_PERISCOLAIRE', 'EXTRASCOLAIRE', 'PERISCOLAIRE_SEMAINE'], true)
|| $label === '' || !$startDate || !$endDate
|| $startDate->format('Y-m-d') !== $start || $endDate->format('Y-m-d') !== $end
|| $endDate < $startDate) {
JsonResponse::send(422, ['error' => 'Les informations de l’équipe sont incomplètes ou invalides.']);
}
$under6 = max(0, (int) ($payload['nombre_enfants_moins_6'] ?? 0));
$over6 = max(0, (int) ($payload['nombre_enfants_6_plus'] ?? 0));
$ratioUnder6 = max(1, (int) ($payload['ratio_moins_6'] ?? 8));
$ratioOver6 = max(1, (int) ($payload['ratio_6_plus'] ?? 12));
$minAgents = max(1, (int) ($payload['minimum_agents'] ?? 1));
$qualified = (float) ($payload['pourcentage_diplomes_min'] ?? 50);
if ($qualified < 0 || $qualified > 100) {
JsonResponse::send(422, ['error' => 'Le pourcentage minimal de diplômés doit être compris entre 0 et 100 %.']);
}
try {
$id = $this->teamModel->create([
'id_structure' => (int) $structureId,
'type_equipe' => $type,
'libelle' => $label,
'date_debut' => $start,
'date_fin' => $end,
'nombre_enfants_moins_6' => $under6,
'nombre_enfants_6_plus' => $over6,
'ratio_moins_6' => $ratioUnder6,
'ratio_6_plus' => $ratioOver6,
'minimum_agents' => $minAgents,
'pourcentage_diplomes_min' => $qualified,
'commentaire' => trim((string) ($payload['commentaire'] ?? '')) ?: null,
]);
JsonResponse::send(201, ['message' => 'L’équipe a été créée.', 'id_equipe' => $id]);
} catch (Throwable $e) {
JsonResponse::send(500, ['error' => 'Impossible de créer l’équipe.', 'details' => $e->getMessage()]);
}
}
/**
* POST api/team_members.php — ajoute ou retire un agent d'une équipe.
*
* `action` vaut ADD (défaut) ou REMOVE. Trois fonctions possibles :
* RESPONSABLE, ANIMATION, RESTAURATION_ENTRETIEN — elles conditionnent le
* calcul du taux d'encadrement.
*
* L'exception d'ajout ressort en 422 avec SON message : le modèle y place
* les refus métier lisibles (agent déjà membre, par exemple), destinés à
* l'utilisateur.
*
* Charge utile : action, team_id, agent_id, fonction_equipe, commentaire,
* csrf_token.
*/
public function member(): void
{
$this->access->requireService();
Request::requireMethod('POST');
$payload = Request::json();
Csrf::assertPayload($payload);
$action = strtoupper(trim((string) ($payload['action'] ?? 'ADD')));
$teamId = filter_var($payload['team_id'] ?? null, FILTER_VALIDATE_INT);
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
if (!$teamId || !$agentId || $this->agentModel->findActive((int) $agentId) === null) {
JsonResponse::send(422, ['error' => 'Équipe ou agent invalide.']);
}
if ($action === 'REMOVE') {
$deleted = $this->teamModel->removeMember((int) $teamId, (int) $agentId);
JsonResponse::send($deleted ? 200 : 404, ['message' => $deleted ? 'L’agent a été retiré de l’équipe.' : 'Affectation introuvable.']);
}
$function = strtoupper(trim((string) ($payload['fonction_equipe'] ?? 'ANIMATION')));
if (!in_array($function, ['RESPONSABLE', 'ANIMATION', 'RESTAURATION_ENTRETIEN'], true)) {
JsonResponse::send(422, ['error' => 'Fonction dans l’équipe invalide.']);
}
try {
$this->teamModel->addMember(
(int) $teamId,
(int) $agentId,
$function,
trim((string) ($payload['commentaire'] ?? '')) ?: null
);
JsonResponse::send(200, ['message' => 'L’agent a été ajouté à l’équipe.']);
} catch (Throwable $e) {
JsonResponse::send(422, ['error' => $e->getMessage()]);
}
}
/**
* POST api/team_status.php — fait avancer une équipe dans son cycle de vie.
*
* Aucune transition n'est imposée : tout statut valide est acceptable depuis
* n'importe quel autre, y compris un retour en arrière. Le suivi est
* indicatif, pas un verrou.
*
* Charge utile : team_id, statut (BROUILLON|A_CONTROLER|VALIDEE), csrf_token.
*/
public function status(): void
{
$this->access->requireService();
Request::requireMethod('POST');
$payload = Request::json();
Csrf::assertPayload($payload);
$teamId = filter_var($payload['team_id'] ?? null, FILTER_VALIDATE_INT);
$status = strtoupper(trim((string) ($payload['statut'] ?? '')));
if (!$teamId || !in_array($status, ['BROUILLON', 'A_CONTROLER', 'VALIDEE'], true)) {
JsonResponse::send(422, ['error' => 'Équipe ou statut invalide.']);
}
$this->teamModel->setStatus((int) $teamId, $status);
JsonResponse::send(200, ['message' => 'Le statut de l’équipe a été mis à jour.']);
}
}