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, ]); Journal::ecriture( 'EQUIPE_CREEE', 'equipe_pta', (int) $id, sprintf('Création de l’équipe « %s » sur le lieu #%d.', $label, (int) $structureId), ['id_structure' => (int) $structureId, 'type_equipe' => $type] ); 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 ); Journal::ecriture( 'EQUIPE_MEMBRE_AJOUTE', 'equipe_pta_membre', (int) $teamId, sprintf('Agent #%d ajouté à l’équipe #%d (%s).', (int) $agentId, (int) $teamId, $function), ['id_agent' => (int) $agentId, 'fonction_equipe' => $function] ); 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); Journal::ecriture( 'EQUIPE_STATUT_MODIFIE', 'equipe_pta', (int) $teamId, sprintf('Équipe #%d passée au statut %s.', (int) $teamId, $status), ['statut' => $status] ); JsonResponse::send(200, ['message' => 'Le statut de l’équipe a été mis à jour.']); } }