pour prod

This commit is contained in:
Loic Masi
2026-08-07 16:13:43 +02:00
commit 5fbf76868f
157 changed files with 24085 additions and 0 deletions

View File

@@ -0,0 +1,529 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Access;
use App\Core\Csrf;
use App\Core\JsonResponse;
use App\Core\PdfResponse;
use App\Core\Request;
use App\Models\AgentModel;
use App\Models\QuotaModel;
use App\Models\StructureModel;
use App\Services\AgentHoursContractPdfService;
use App\Services\AgentTimeSummaryPdfService;
use App\Services\PlanningPdfService;
use DateTimeImmutable;
use PDO;
use Throwable;
final class AgentController
{
private AgentModel $agentModel;
private StructureModel $structureModel;
private QuotaModel $quotaModel;
private Access $access;
public function __construct(private PDO $pdo)
{
$this->agentModel = new AgentModel($pdo);
$this->structureModel = new StructureModel($pdo);
$this->quotaModel = new QuotaModel($pdo);
$this->access = new Access($pdo);
}
public function listForPlanning(): void
{
$this->access->requireDraftEditor();
$selectedLocation = filter_input(INPUT_GET, 'structure_id', FILTER_VALIDATE_INT) ?: 0;
if ($selectedLocation) {
$this->access->requireStructureEdit((int) $selectedLocation);
}
JsonResponse::send(200, ['agents' => $this->agentModel->listForLocation((int) $selectedLocation)]);
}
public function overview(): void
{
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
$weekValue = trim((string) ($_GET['week'] ?? ''));
$week = $this->parseWeek($weekValue);
if (!$agentId || $week === null) {
JsonResponse::send(400, ['error' => 'Agent ou semaine invalide.']);
}
$this->access->requireAgentView((int) $agentId);
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
JsonResponse::send(404, ['error' => 'Agent introuvable.']);
}
[$year, $weekNumber] = $week;
$weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->format('Y-m-d');
$weekEnd = (new DateTimeImmutable())->setISODate($year, $weekNumber, 7)->format('Y-m-d');
JsonResponse::send(200, [
'agent' => $agent,
'week' => ['value' => $weekValue, 'date_debut' => $weekStart, 'date_fin' => $weekEnd],
'quota' => $this->quotaModel->forDateForApi((int) $agentId, $weekStart),
'entries' => $this->agentModel->entriesForWeek((int) $agentId, $year, $weekNumber),
]);
}
public function monthOverview(): void
{
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
$monthValue = trim((string) ($_GET['month'] ?? ''));
if (!$agentId || !preg_match('/^(\d{4})-(\d{2})$/', $monthValue, $matches)) {
JsonResponse::send(400, ['error' => 'Agent ou mois invalide.']);
}
$this->access->requireAgentView((int) $agentId);
$year = (int) $matches[1];
$monthNumber = (int) $matches[2];
if ($monthNumber < 1 || $monthNumber > 12) {
JsonResponse::send(400, ['error' => 'Mois invalide.']);
}
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
JsonResponse::send(404, ['error' => 'Agent introuvable.']);
}
$firstDay = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $monthNumber));
$lastDay = $firstDay->modify('last day of this month');
$gridStart = $firstDay->modify('-' . ((int) $firstDay->format('N') - 1) . ' days');
$lastWeekMonday = $lastDay->modify('-' . ((int) $lastDay->format('N') - 1) . ' days');
$gridEnd = $lastWeekMonday->modify('+4 days');
$dayLabels = [1 => 'Lundi', 2 => 'Mardi', 3 => 'Mercredi', 4 => 'Jeudi', 5 => 'Vendredi'];
$monthLabels = [
1 => 'Janvier', 2 => 'Février', 3 => 'Mars', 4 => 'Avril', 5 => 'Mai', 6 => 'Juin',
7 => 'Juillet', 8 => 'Août', 9 => 'Septembre', 10 => 'Octobre', 11 => 'Novembre', 12 => 'Décembre',
];
$weeks = [];
for ($monday = $gridStart; $monday <= $lastWeekMonday; $monday = $monday->modify('+7 days')) {
$days = [];
for ($weekday = 1; $weekday <= 5; $weekday++) {
$date = $monday->modify('+' . ($weekday - 1) . ' days');
$days[] = [
'date' => $date->format('Y-m-d'),
'label' => $dayLabels[$weekday],
'display' => $date->format('d/m'),
'in_month' => (int) $date->format('n') === $monthNumber,
];
}
$weeks[] = [
'value' => sprintf('%04d-W%02d', (int) $monday->format('o'), (int) $monday->format('W')),
'number' => (int) $monday->format('W'),
'date_debut' => $monday->format('Y-m-d'),
'date_fin' => $monday->modify('+4 days')->format('Y-m-d'),
'days' => $days,
];
}
JsonResponse::send(200, [
'agent' => $agent,
'month' => [
'value' => $monthValue,
'year' => $year,
'month' => $monthNumber,
'label' => $monthLabels[$monthNumber] . ' ' . $year,
'date_debut' => $firstDay->format('Y-m-d'),
'date_fin' => $lastDay->format('Y-m-d'),
'grid_start' => $gridStart->format('Y-m-d'),
'grid_end' => $gridEnd->format('Y-m-d'),
],
'weeks' => $weeks,
'quota' => $this->quotaModel->forDateForApi((int) $agentId, $firstDay->format('Y-m-d')),
'entries' => $this->agentModel->entriesForDateRange(
(int) $agentId,
$gridStart->format('Y-m-d'),
$gridEnd->format('Y-m-d')
),
]);
}
public function timeSummaryPdf(): void
{
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
$dateValue = trim((string) ($_GET['date'] ?? ''));
$year = filter_input(INPUT_GET, 'year', FILTER_VALIDATE_INT);
if ($dateValue === '' && $year) {
$dateValue = sprintf('%04d-01-01', (int) $year);
}
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $dateValue);
if (!$agentId || !$date || $date->format('Y-m-d') !== $dateValue) {
http_response_code(400);
echo 'Agent ou date de référence invalide.';
return;
}
$this->access->requireAgentView((int) $agentId);
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
http_response_code(404);
echo 'Agent introuvable.';
return;
}
$quota = $this->quotaModel->forDateForApi((int) $agentId, $dateValue);
$summary = $this->agentModel->timeSummaryForRange(
(int) $agentId,
(string) $quota['date_debut_periode'],
(string) $quota['date_fin_periode']
);
$pdf = (new AgentTimeSummaryPdfService())->annualSummary($agent, $quota, $summary);
PdfResponse::inline(
$pdf,
sprintf(
'bilan_heures_%s_%s_%s_%s.pdf',
$agent['nom'],
$agent['prenom'],
str_replace('-', '', (string) $quota['date_debut_periode']),
str_replace('-', '', (string) $quota['date_fin_periode'])
)
);
}
public function pdf(): void
{
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
$weekValue = trim((string) ($_GET['week'] ?? ''));
$week = $this->parseWeek($weekValue);
$weekCount = filter_input(INPUT_GET, 'weeks', FILTER_VALIDATE_INT) ?: 7;
$weekCount = in_array((int) $weekCount, [6, 7], true) ? (int) $weekCount : 7;
if (!$agentId || $week === null) {
http_response_code(400);
echo 'Agent ou semaine invalide.';
return;
}
$this->access->requireAgentView((int) $agentId);
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
http_response_code(404);
echo 'Agent introuvable.';
return;
}
[$year, $weekNumber] = $week;
$firstMonday = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1);
$weeks = [];
for ($index = 0; $index < $weekCount; $index++) {
$monday = $firstMonday->modify('+' . ($index * 7) . ' days');
$isoYear = (int) $monday->format('o');
$isoWeek = (int) $monday->format('W');
$weeks[] = [
'value' => sprintf('%04d-W%02d', $isoYear, $isoWeek),
'date_debut' => $monday->format('Y-m-d'),
'date_fin' => $monday->modify('+6 days')->format('Y-m-d'),
];
}
$lastFriday = $firstMonday->modify('+' . (($weekCount - 1) * 7 + 4) . ' days');
$entries = $this->agentModel->entriesForDateRange(
(int) $agentId,
$firstMonday->format('Y-m-d'),
$lastFriday->format('Y-m-d')
);
$pdf = (new PlanningPdfService())->agentWeeks(
$agent,
$entries,
$weeks,
$this->quotaModel->forDateForApi((int) $agentId, $firstMonday->format('Y-m-d'))
);
PdfResponse::inline(
$pdf,
sprintf(
'planning_%s_%s_%s_%d_semaines.pdf',
$agent['nom'],
$agent['prenom'],
str_replace('-W', '_S', $weekValue),
$weekCount
)
);
}
public function hoursContractPdf(): void
{
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
$dateValue = trim((string) ($_GET['date'] ?? ''));
$year = filter_input(INPUT_GET, 'year', FILTER_VALIDATE_INT);
if ($dateValue === '' && $year) {
$dateValue = sprintf('%04d-01-01', (int) $year);
}
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $dateValue);
if (!$agentId || !$date || $date->format('Y-m-d') !== $dateValue) {
http_response_code(400);
echo 'Agent ou date de référence invalide.';
return;
}
$this->access->requireAgentView((int) $agentId);
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
http_response_code(404);
echo 'Agent introuvable.';
return;
}
$quota = $this->quotaModel->forDateForApi((int) $agentId, $dateValue);
$pdf = (new AgentHoursContractPdfService())->annualContract($agent, $quota);
PdfResponse::inline(
$pdf,
sprintf(
'contrat_horaire_%s_%s_%s.pdf',
$agent['nom'],
$agent['prenom'],
str_replace('-', '', (string) $quota['date_debut_periode'])
)
);
}
public function assignments(): void
{
$this->access->requireService();
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
JsonResponse::send(200, ['agents' => $this->agentModel->allActive()]);
}
Request::requireMethod('POST');
$payload = Request::json();
Csrf::assertPayload($payload);
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
$rawStructure = $payload['structure_id'] ?? null;
$structureId = ($rawStructure === null || $rawStructure === '' || $rawStructure === 0)
? null
: filter_var($rawStructure, FILTER_VALIDATE_INT);
if (!$agentId) {
JsonResponse::send(422, ['error' => 'Agent invalide.']);
}
if ($this->agentModel->findActive((int) $agentId) === null) {
JsonResponse::send(404, ['error' => 'Agent introuvable.']);
}
if ($structureId !== null && $this->structureModel->findActive((int) $structureId) === null) {
JsonResponse::send(422, ['error' => 'Lieu daffectation invalide ou inactif.']);
}
try {
$this->agentModel->updateAssignment((int) $agentId, $structureId !== null ? (int) $structureId : null);
} catch (Throwable $e) {
JsonResponse::send(500, [
'error' => 'Impossible de modifier les paramètres de lagent.',
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
]);
}
JsonResponse::send(200, [
'message' => $structureId === null
? 'Les paramètres de lagent ont été mis à jour sans lieu principal.'
: 'Le lieu principal de lagent a été mis à jour.',
]);
}
public function create(): void
{
$this->access->requireService();
Request::requireMethod('POST');
$payload = Request::json();
Csrf::assertPayload($payload);
$matricule = trim((string) ($payload['matricule'] ?? ''));
$nom = trim((string) ($payload['nom'] ?? ''));
$prenom = trim((string) ($payload['prenom'] ?? ''));
$email = trim((string) ($payload['email'] ?? ''));
$telephone = trim((string) ($payload['telephone'] ?? ''));
$adresse = trim((string) ($payload['adresse'] ?? ''));
$estDiplome = filter_var($payload['est_diplome'] ?? false, FILTER_VALIDATE_BOOLEAN);
$diplomeLibelle = trim((string) ($payload['diplome_libelle'] ?? ''));
$structureId = filter_var($payload['structure_id'] ?? null, FILTER_VALIDATE_INT);
$rate = filter_var($payload['quotite_travail'] ?? null, FILTER_VALIDATE_FLOAT);
$contractStart = trim((string) ($payload['date_debut_contrat'] ?? ''));
$contractStartDate = DateTimeImmutable::createFromFormat('!Y-m-d', $contractStart);
$contractEnd = trim((string) ($payload['date_fin_contrat'] ?? ''));
$contractEndDate = $contractEnd !== '' ? DateTimeImmutable::createFromFormat('!Y-m-d', $contractEnd) : null;
$postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT);
$contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT')));
$trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN);
$ptaComment = trim((string) ($payload['commentaire_pta'] ?? ''));
$validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts());
if ($matricule === '' || $nom === '' || $prenom === '' || !$structureId || !$postId
|| !in_array((int) $postId, $validPostIds, true)
|| !in_array($contractType, ['PERMANENT', 'TEMPORAIRE'], true)
|| !$contractStartDate || $contractStartDate->format('Y-m-d') !== $contractStart
|| ($contractEnd !== '' && (!$contractEndDate || $contractEndDate->format('Y-m-d') !== $contractEnd || $contractEndDate < $contractStartDate))) {
JsonResponse::send(422, ['error' => 'Le matricule, le nom, le prénom, le poste, le type de contrat, le lieu principal et des dates de contrat cohérentes sont obligatoires.']);
}
if ($rate === false || $rate <= 0 || $rate > 100) {
JsonResponse::send(422, ['error' => 'La quotité de travail doit être comprise entre 1 et 100 %.']);
}
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
JsonResponse::send(422, ['error' => 'Ladresse e-mail saisie nest pas valide.']);
}
if (strlen($matricule) > 50 || strlen($nom) > 100 || strlen($prenom) > 100 || strlen($email) > 255
|| strlen($telephone) > 30 || strlen($adresse) > 255 || strlen($diplomeLibelle) > 150 || strlen($ptaComment) > 1000) {
JsonResponse::send(422, ['error' => 'Un des champs dépasse la longueur autorisée.']);
}
if ($this->structureModel->findActive((int) $structureId) === null) {
JsonResponse::send(422, ['error' => 'Le lieu daffectation principal sélectionné est invalide ou inactif.']);
}
if ($this->agentModel->matriculeExists($matricule)) {
JsonResponse::send(409, ['error' => 'Un agent utilise déjà ce matricule.']);
}
try {
$created = $this->agentModel->createWithDefaults([
'matricule' => $matricule,
'nom' => $nom,
'prenom' => $prenom,
'email' => $email !== '' ? $email : null,
'telephone' => $telephone !== '' ? $telephone : null,
'adresse' => $adresse !== '' ? $adresse : null,
'est_diplome' => $estDiplome,
'diplome_libelle' => $estDiplome && $diplomeLibelle !== '' ? $diplomeLibelle : null,
'structure_id' => (int) $structureId,
'quotite_travail' => (float) $rate,
'date_debut_contrat' => $contractStart,
'date_fin_contrat' => $contractEnd !== '' ? $contractEnd : null,
'id_poste' => (int) $postId,
'type_contrat' => $contractType,
'formation_repartition_annuelle' => $trainingPlanned,
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
]);
} catch (Throwable $e) {
JsonResponse::send(500, [
'error' => 'Impossible de créer lagent.',
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
]);
}
$structure = $this->structureModel->findActive((int) $structureId);
JsonResponse::send(201, [
'message' => sprintf('%s %s a été ajouté avec le lieu principal « %s ».', $prenom, $nom, $structure['nom']),
'agent' => array_merge($created, [
'matricule' => $matricule,
'nom' => $nom,
'prenom' => $prenom,
'email' => $email !== '' ? $email : null,
'telephone' => $telephone !== '' ? $telephone : null,
'adresse' => $adresse !== '' ? $adresse : null,
'est_diplome' => $estDiplome ? 1 : 0,
'diplome_libelle' => $estDiplome && $diplomeLibelle !== '' ? $diplomeLibelle : null,
'id_structure' => (int) $structureId,
'structure_nom' => (string) $structure['nom'],
'structure_type_affectation' => (string) $structure['type_affectation'],
'quotite_travail' => (float) $rate,
'date_debut_contrat' => $contractStart,
'date_fin_contrat' => $contractEnd !== '' ? $contractEnd : null,
'id_poste' => (int) $postId,
'poste_libelle' => (string) (array_values(array_filter($this->agentModel->allPosts(), static fn(array $post): bool => (int) $post['id_poste'] === (int) $postId))[0]['libelle'] ?? ''),
'type_contrat' => $contractType,
'formation_repartition_annuelle' => $trainingPlanned ? 1 : 0,
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
]),
]);
}
public function updateProfile(): void
{
$this->access->requireService();
Request::requireMethod('POST');
$payload = Request::json();
Csrf::assertPayload($payload);
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
$email = trim((string) ($payload['email'] ?? ''));
$telephone = trim((string) ($payload['telephone'] ?? ''));
$adresse = trim((string) ($payload['adresse'] ?? ''));
$estDiplome = filter_var($payload['est_diplome'] ?? false, FILTER_VALIDATE_BOOLEAN);
$diplomeLibelle = trim((string) ($payload['diplome_libelle'] ?? ''));
$contractStart = trim((string) ($payload['date_debut_contrat'] ?? ''));
$contractStartDate = DateTimeImmutable::createFromFormat('!Y-m-d', $contractStart);
$contractEnd = trim((string) ($payload['date_fin_contrat'] ?? ''));
$contractEndDate = $contractEnd !== '' ? DateTimeImmutable::createFromFormat('!Y-m-d', $contractEnd) : null;
$postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT);
$contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT')));
$trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN);
$ptaComment = trim((string) ($payload['commentaire_pta'] ?? ''));
$validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts());
$rawStructure = $payload['structure_id'] ?? null;
$structureId = ($rawStructure === null || $rawStructure === '' || $rawStructure === 0)
? null
: filter_var($rawStructure, FILTER_VALIDATE_INT);
if (!$agentId) {
JsonResponse::send(422, ['error' => 'Agent invalide.']);
}
if ($this->agentModel->findActive((int) $agentId) === null) {
JsonResponse::send(404, ['error' => 'Agent introuvable.']);
}
if (!$postId || !in_array((int) $postId, $validPostIds, true)
|| !in_array($contractType, ['PERMANENT', 'TEMPORAIRE'], true)
|| !$contractStartDate || $contractStartDate->format('Y-m-d') !== $contractStart
|| ($contractEnd !== '' && (!$contractEndDate || $contractEndDate->format('Y-m-d') !== $contractEnd || $contractEndDate < $contractStartDate))) {
JsonResponse::send(422, ['error' => 'Le poste, le type de contrat et les dates du contrat doivent être valides.']);
}
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
JsonResponse::send(422, ['error' => 'Ladresse e-mail saisie nest pas valide.']);
}
if (strlen($email) > 255 || strlen($telephone) > 30 || strlen($adresse) > 255 || strlen($diplomeLibelle) > 150 || strlen($ptaComment) > 1000) {
JsonResponse::send(422, ['error' => 'Un des champs dépasse la longueur autorisée.']);
}
if ($structureId !== null && $this->structureModel->findActive((int) $structureId) === null) {
JsonResponse::send(422, ['error' => 'Lieu daffectation principal invalide ou inactif.']);
}
try {
$this->agentModel->updateProfile((int) $agentId, [
'email' => $email !== '' ? $email : null,
'telephone' => $telephone !== '' ? $telephone : null,
'adresse' => $adresse !== '' ? $adresse : null,
'est_diplome' => $estDiplome,
'diplome_libelle' => $estDiplome && $diplomeLibelle !== '' ? $diplomeLibelle : null,
'date_debut_contrat' => $contractStart,
'date_fin_contrat' => $contractEnd !== '' ? $contractEnd : null,
'id_poste' => (int) $postId,
'type_contrat' => $contractType,
'formation_repartition_annuelle' => $trainingPlanned,
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
'structure_id' => $structureId !== null ? (int) $structureId : null,
]);
} catch (Throwable $e) {
JsonResponse::send(500, [
'error' => 'Impossible de modifier les informations de lagent.',
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
]);
}
JsonResponse::send(200, [
'message' => 'Les informations de lagent ont été mises à jour.',
'agent' => $this->agentModel->findActive((int) $agentId),
]);
}
private function parseWeek(string $value): ?array
{
if (!preg_match('/^(\d{4})-W(\d{2})$/', $value, $matches)) {
return null;
}
$week = (int) $matches[2];
return $week >= 1 && $week <= 53 ? [(int) $matches[1], $week] : null;
}
}