198 lines
8.3 KiB
PHP
198 lines
8.3 KiB
PHP
<?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.']);
|
||
}
|
||
}
|