Ajout des commentaires dans le code

This commit is contained in:
PTA
2026-08-20 13:22:49 +02:00
parent 4201894e28
commit 5d8c0993a6
154 changed files with 5071 additions and 47 deletions

View File

@@ -7,13 +7,35 @@ namespace App\Services;
use App\Core\MotifCategories;
use DateTimeImmutable;
/**
* Mise en page des plannings au format PDF, par agent ou par lieu.
*
* Deux documents, tous deux en A4 PAYSAGE (SimplePdf par défaut) :
* - agentWeeks() : un agent sur 1 à 7 semaines, en blocs compacts ;
* - structureWeek() : un lieu sur une semaine, en tableau agents × jours, avec
* saut de page géré à la main.
*
* CONTRAINTE TRANSVERSE — les libellés sont écrits SANS ACCENT dans tout ce
* fichier (« Document genere », « Periscolaire »). Ce n'est pas une négligence :
* SimplePdf convertit en Windows-1252 avec translittération, et cette écriture
* garantit un rendu identique même quand iconv n'est pas disponible. Les données
* venant de la base, elles, gardent leurs accents.
*
* Toutes les coordonnées sont en POINTS PDF, origine en HAUT à gauche (voir
* SimplePdf). Les valeurs numériques éparpillées dans les méthodes de dessin
* sont des positions absolues ajustées visuellement : les modifier suppose de
* regénérer un document pour vérifier.
*/
final class PlanningPdfService
{
/** Marge extérieure commune à tous les documents. */
private const MARGIN = 28.0;
private const WEEK_DAYS = [1 => 'Lundi', 2 => 'Mardi', 3 => 'Mercredi', 4 => 'Jeudi', 5 => 'Vendredi'];
/**
* Compatibilité avec l'ancien export d'une seule semaine.
* Conservée pour d'éventuels appels externes ; le code actuel passe
* directement par agentWeeks().
*/
public function agentWeek(array $agent, array $entries, array $week, array $quota): string
{
@@ -23,6 +45,24 @@ final class PlanningPdfService
/**
* Génère un planning compact de 1 à 7 semaines. Avec 7 semaines, le rendu
* tient volontairement sur deux pages A4 paysage maximum (4 + 3 semaines).
*
* Le découpage en pages est fait par array_chunk(..., 4) : QUATRE SEMAINES
* PAR PAGE, chaque bloc occupant 112 points de haut à partir de y = 96. Ce
* plafond de quatre est une contrainte physique, SimplePdf ne sachant pas
* reporter un tableau sur la page suivante. Passer à cinq exigerait de
* réduire la hauteur des blocs.
*
* Au-delà de sept semaines, la liste est silencieusement TRONQUÉE : le
* contrôleur n'accepte de toute façon que 6 ou 7.
*
* @param array<string, mixed> $agent
* @param array<int, array<string, mixed>> $entries Créneaux de toute la période.
* @param array<int, array<string, mixed>> $weeks Semaines à rendre.
* @param array<string, mixed> $quota
*
* @return string Octets du PDF.
*
* @throws \InvalidArgumentException Si aucune semaine n'est fournie.
*/
public function agentWeeks(array $agent, array $entries, array $weeks, array $quota): string
{
@@ -60,6 +100,24 @@ final class PlanningPdfService
return $pdf->output();
}
/**
* Planning d'un lieu sur une semaine : un tableau agents × cinq jours.
*
* Contrairement à agentWeeks(), la hauteur des lignes n'est PAS connue à
* l'avance — elle dépend du nombre de créneaux de chaque agent et du
* découpage des libellés. La méthode mesure donc chaque ligne avant de la
* tracer, et déclenche une nouvelle page dès que $bottomLimit serait
* dépassé. La closure $drawHeader existe pour cette raison : l'en-tête du
* tableau doit être redessiné en tête de CHAQUE page, sans quoi les colonnes
* ne seraient plus identifiables.
*
* @param array<string, mixed> $structure
* @param array<int, array<string, mixed>> $agents Chaque agent porte ses
* propres clés `entries` et `quota`.
* @param array<string, mixed> $week
*
* @return string Octets du PDF.
*/
public function structureWeek(array $structure, array $agents, array $week): string
{
$pdf = new SimplePdf();
@@ -201,6 +259,10 @@ final class PlanningPdfService
return $pdf->output();
}
/**
* En-tête du planning agent : identité, lieu principal, période couverte,
* quota restant, et pagination si le document tient sur plusieurs pages.
*/
private function drawAgentMultiWeekHeader(
SimplePdf $pdf,
array $agent,
@@ -228,7 +290,19 @@ final class PlanningPdfService
$pdf->line(self::MARGIN, 86, $pdf->width() - self::MARGIN, 86, 0.72);
}
/** @param array<string, array<int, array>> $entriesByDate */
/**
* Dessine UN bloc de semaine : bandeau de titre, en-têtes de jours, puis les
* créneaux de chaque journée dans une colonne.
*
* La hauteur du corps est FIXE (77 points) pour que les blocs s'empilent à
* pas régulier. Les créneaux qui n'y tiennent pas ne sont pas rendus — le
* compteur $rendered plus bas s'en charge. Une journée très chargée peut
* donc apparaître tronquée : c'est le compromis retenu pour garantir sept
* semaines sur deux pages.
*
* @param array<string, array<int, array>> $entriesByDate Créneaux indexés par date.
* @param float $y Ordonnée du haut du bloc.
*/
private function drawCompactWeek(SimplePdf $pdf, array $week, array $entriesByDate, float $y): void
{
$weekDays = $this->weekDays($week);
@@ -287,7 +361,13 @@ final class PlanningPdfService
}
}
/** @return string[] */
/**
* Lignes d'un créneau dans la vue agent : horaires, motif si informatif,
* lieu, et la mention « Brouillon » quand le planning n'est pas validé —
* un document imprimé doit dire s'il est définitif.
*
* @return string[]
*/
private function compactAgentEntryLines(SimplePdf $pdf, array $entry, float $width): array
{
$lines = [sprintf('%s-%s', $entry['heure_debut'], $entry['heure_fin'])];
@@ -310,7 +390,12 @@ final class PlanningPdfService
return $lines;
}
/** @return string[] */
/**
* Lignes d'un créneau dans la vue lieu : plus condensées que dans la vue
* agent, le lieu étant déjà celui du document.
*
* @return string[]
*/
private function structureEntryLines(SimplePdf $pdf, array $entry, float $width): array
{
$first = sprintf('%s-%s', $entry['heure_debut'], $entry['heure_fin']);
@@ -333,7 +418,11 @@ final class PlanningPdfService
: (string) ($entry['motif_libelle'] ?? 'Affectation');
}
/** @return array{accent:string,background:string,label:string} */
/**
* Palette d'un créneau, déduite de son motif.
*
* @return array{accent:string,background:string,label:string}
*/
private function entryPalette(array $entry): array
{
return $this->motifPalette(
@@ -342,7 +431,15 @@ final class PlanningPdfService
);
}
/** @return array{accent:string,background:string,label:string} */
/**
* Couleurs par CATÉGORIE de motif — travail en bleu, formation en violet,
* congé en vert, absence en rouge, reste en gris.
*
* Le classement passe par App\Core\MotifCategories, partagé avec l'écran et
* les autres PDF : une même catégorie garde partout la même couleur.
*
* @return array{accent:string,background:string,label:string}
*/
private function motifPalette(string $code, bool $countsInQuota = false): array
{
return match (MotifCategories::category($code, $countsInQuota)) {
@@ -354,6 +451,12 @@ final class PlanningPdfService
};
}
/**
* Légende des couleurs, en une ligne au pied du document.
*
* Les entrées sont espacées selon la LARGEUR ESTIMÉE de leur libellé et non
* à pas fixe : la première est trop longue pour une colonne régulière.
*/
private function drawLegend(SimplePdf $pdf, float $x, float $y): void
{
$items = [
@@ -374,7 +477,15 @@ final class PlanningPdfService
}
}
/** @return array<string, array<int, array>> */
/**
* Indexe les créneaux par date et les trie par heure de début.
*
* Le tri est refait ici bien que le SQL trie déjà : les créneaux peuvent
* avoir été fusionnés depuis plusieurs sources (plusieurs lieux, plusieurs
* plannings) avant d'arriver.
*
* @return array<string, array<int, array>>
*/
private function entriesByDate(array $entries): array
{
$grouped = [];
@@ -388,6 +499,7 @@ final class PlanningPdfService
return $grouped;
}
/** En-tête commun du planning par lieu : titre, sous-titre, semaine, filet. */
private function drawDocumentHeader(SimplePdf $pdf, string $title, string $subtitle, string $weekLabel): void
{
$pdf->text(self::MARGIN, 34, 'PTA', 10, true);
@@ -397,7 +509,12 @@ final class PlanningPdfService
$pdf->line(self::MARGIN, 90, $pdf->width() - self::MARGIN, 90, 0.72);
}
/** @return array<int, array{date:string,label:string,display:string}> */
/**
* Les cinq jours ouvrés d'une semaine, à partir de sa date de début.
* S'arrête au vendredi, le week-end n'étant pas planifiable.
*
* @return array<int, array{date:string,label:string,display:string}>
*/
private function weekDays(array $week): array
{
$start = new DateTimeImmutable((string) $week['date_debut']);
@@ -413,6 +530,7 @@ final class PlanningPdfService
return $days;
}
/** « Semaine du 16/02/2026 au 20/02/2026 » — bornée au vendredi. */
private function weekLabel(array $week): string
{
$start = new DateTimeImmutable((string) $week['date_debut']);
@@ -420,6 +538,7 @@ final class PlanningPdfService
return sprintf('Semaine du %s au %s', $start->format('d/m/Y'), $end->format('d/m/Y'));
}
/** Abrège le nom du jour pour les colonnes étroites : « Lundi » → « Lun. ». */
private function shortDayLabel(string $label): string
{
return match ($label) {
@@ -432,6 +551,7 @@ final class PlanningPdfService
};
}
/** Libellé du type de lieu, sans accent (contrainte d'encodage du PDF). */
private function typeLabel(string $type): string
{
return match (strtoupper($type)) {