Files
pta/app/Services/PlanningPdfService.php
Loic Masi 5fbf76868f pour prod
2026-08-07 16:13:43 +02:00

427 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use DateTimeImmutable;
final class PlanningPdfService
{
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.
*/
public function agentWeek(array $agent, array $entries, array $week, array $quota): string
{
return $this->agentWeeks($agent, $entries, [$week], $quota);
}
/**
* 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).
*/
public function agentWeeks(array $agent, array $entries, array $weeks, array $quota): string
{
$weeks = array_values(array_slice($weeks, 0, 7));
if ($weeks === []) {
throw new \InvalidArgumentException('Au moins une semaine est nécessaire pour générer le planning.');
}
$pdf = new SimplePdf();
$entriesByDate = $this->entriesByDate($entries);
$chunks = array_chunk($weeks, 4);
foreach ($chunks as $pageIndex => $pageWeeks) {
if ($pageIndex > 0) {
$pdf->addPage();
}
$this->drawAgentMultiWeekHeader($pdf, $agent, $weeks, $quota, $pageIndex + 1, count($chunks));
$sectionY = 96.0;
foreach ($pageWeeks as $week) {
$this->drawCompactWeek($pdf, $week, $entriesByDate, $sectionY);
$sectionY += 112.0;
}
$this->drawLegend($pdf, self::MARGIN, 563);
$pdf->text(
self::MARGIN,
584,
sprintf('Document genere depuis PTA - %d semaine%s affichee%s.', count($weeks), count($weeks) > 1 ? 's' : '', count($weeks) > 1 ? 's' : ''),
7
);
}
return $pdf->output();
}
public function structureWeek(array $structure, array $agents, array $week): string
{
$pdf = new SimplePdf();
$this->drawDocumentHeader(
$pdf,
'Planning hebdomadaire du lieu d\'affectation',
(string) $structure['nom'] . ' - ' . $this->typeLabel((string) ($structure['type_affectation'] ?? '')),
$this->weekLabel($week)
);
$weekDays = $this->weekDays($week);
$margin = self::MARGIN;
$tableY = 100.0;
$agentWidth = 126.0;
$quotaWidth = 84.0;
$usableWidth = $pdf->width() - ($margin * 2);
$dayWidth = ($usableWidth - $agentWidth - $quotaWidth) / 5;
$headerHeight = 38.0;
$bottomLimit = 548.0;
$drawHeader = function () use ($pdf, $weekDays, $margin, $tableY, $agentWidth, $quotaWidth, $dayWidth, $headerHeight): void {
$x = $margin;
$headers = [['Agent', $agentWidth]];
foreach ($weekDays as $day) {
$headers[] = [$day['label'] . ' ' . $day['display'], $dayWidth];
}
$headers[] = ['Quota restant', $quotaWidth];
foreach ($headers as [$label, $width]) {
$pdf->fillRect($x, $tableY, $width, $headerHeight, 0.92);
$pdf->rect($x, $tableY, $width, $headerHeight, 0.72);
$lines = $pdf->wrap((string) $label, $width - 10, 8);
$lineY = $tableY + 15;
foreach (array_slice($lines, 0, 2) as $line) {
$pdf->text($x + 5, $lineY, $line, 8, true);
$lineY += 10;
}
$x += $width;
}
};
$drawHeader();
$y = $tableY + $headerHeight;
foreach ($agents as $agent) {
$entriesByDate = $this->entriesByDate($agent['entries'] ?? []);
$dayBlocks = [];
$maxCellHeight = 34.0;
foreach ($weekDays as $day) {
$blocks = [];
foreach ($entriesByDate[$day['date']] ?? [] as $entry) {
$labelLines = $this->structureEntryLines($pdf, $entry, $dayWidth - 17);
$blockHeight = max(16.0, 6.0 + (count($labelLines) * 8.0));
$blocks[] = [
'entry' => $entry,
'lines' => $labelLines,
'height' => $blockHeight,
];
}
$dayBlocks[] = $blocks;
$cellHeight = 8.0;
foreach ($blocks as $block) {
$cellHeight += $block['height'] + 4.0;
}
$maxCellHeight = max($maxCellHeight, $cellHeight);
}
$agentLines = $pdf->wrap(
sprintf('%s %s - %s', $agent['prenom'], $agent['nom'], $agent['matricule']),
$agentWidth - 10,
7.5
);
$rowHeight = max(44.0, $maxCellHeight, 14.0 + (count($agentLines) * 9.0));
if ($y + $rowHeight > $bottomLimit) {
$pdf->addPage();
$this->drawDocumentHeader(
$pdf,
'Planning hebdomadaire du lieu d\'affectation - suite',
(string) $structure['nom'] . ' - ' . $this->typeLabel((string) ($structure['type_affectation'] ?? '')),
$this->weekLabel($week)
);
$drawHeader();
$y = $tableY + $headerHeight;
}
$x = $margin;
$widths = [$agentWidth, $dayWidth, $dayWidth, $dayWidth, $dayWidth, $dayWidth, $quotaWidth];
foreach ($widths as $width) {
$pdf->rect($x, $y, $width, $rowHeight, 0.80);
$x += $width;
}
$lineY = $y + 13;
foreach ($agentLines as $index => $line) {
$pdf->text($margin + 5, $lineY, $line, 7.5, $index === 0);
$lineY += 9;
}
foreach ($dayBlocks as $dayIndex => $blocks) {
$cellX = $margin + $agentWidth + ($dayIndex * $dayWidth);
if ($blocks === []) {
$pdf->text($cellX + 6, $y + 18, '-', 7.2);
continue;
}
$blockY = $y + 5;
foreach ($blocks as $block) {
$entry = $block['entry'];
$palette = $this->motifPalette((string) ($entry['motif_code'] ?? 'AUTRE'));
$blockHeight = (float) $block['height'];
$pdf->fillRectColor($cellX + 4, $blockY, $dayWidth - 8, $blockHeight, $palette['background']);
$pdf->fillRectColor($cellX + 4, $blockY, 3.0, $blockHeight, $palette['accent']);
$textY = $blockY + 10;
foreach ($block['lines'] as $lineIndex => $line) {
$isFirst = $lineIndex === 0;
$pdf->text($cellX + 10, $textY, $line, 7.0, $isFirst);
$textY += 8;
}
$blockY += $blockHeight + 4;
}
}
$quotaText = (string) ($agent['quota']['restantes']['libelle'] ?? '-');
$pdf->text($margin + $agentWidth + (5 * $dayWidth) + 5, $y + 18, $quotaText, 8, true);
$pdf->text($margin + $agentWidth + (5 * $dayWidth) + 5, $y + 31, (($agent['lieu_principal'] ?? false) ? 'Principal' : 'Ponctuel'), 7);
$y += $rowHeight;
}
if ($agents === []) {
$pdf->text($margin, $y + 24, 'Aucun agent rattache ou planifie sur ce lieu pour cette semaine.', 10);
}
$this->drawLegend($pdf, self::MARGIN, 562);
$pdf->text(self::MARGIN, 584, 'Document genere depuis PTA - Planning du lundi au vendredi.', 7);
return $pdf->output();
}
private function drawAgentMultiWeekHeader(
SimplePdf $pdf,
array $agent,
array $weeks,
array $quota,
int $page,
int $pageCount
): void {
$first = $weeks[0];
$last = $weeks[count($weeks) - 1];
$periodStart = (new DateTimeImmutable((string) $first['date_debut']))->format('d/m/Y');
$periodEnd = (new DateTimeImmutable((string) $last['date_debut']))->modify('+4 days')->format('d/m/Y');
$principalLocation = (($agent['structure_principale_nom'] ?? '') ?: 'Non renseigne');
$principalType = $this->typeLabel((string) ($agent['structure_principale_type_affectation'] ?? ''));
$pdf->text(self::MARGIN, 25, 'PTA', 9, true);
$pdf->text(self::MARGIN, 44, 'Planning de l\'agent - vue multi-semaines', 16, true);
$pdf->text(self::MARGIN, 60, sprintf('%s %s - %s', $agent['prenom'], $agent['nom'], $agent['matricule']), 9.5, true);
$pdf->text(self::MARGIN, 74, 'Lieu principal : ' . $principalLocation . ' - ' . $principalType, 8);
$pdf->text($pdf->width() - 260, 44, sprintf('Du %s au %s', $periodStart, $periodEnd), 9.5, true);
$pdf->text($pdf->width() - 260, 60, 'Quota restant : ' . ($quota['restantes']['libelle'] ?? '-'), 8.5, true);
if ($pageCount > 1) {
$pdf->text($pdf->width() - 260, 74, sprintf('Page %d / %d', $page, $pageCount), 8);
}
$pdf->line(self::MARGIN, 86, $pdf->width() - self::MARGIN, 86, 0.72);
}
/** @param array<string, array<int, array>> $entriesByDate */
private function drawCompactWeek(SimplePdf $pdf, array $week, array $entriesByDate, float $y): void
{
$weekDays = $this->weekDays($week);
$usableWidth = $pdf->width() - (self::MARGIN * 2);
$colWidth = $usableWidth / 5;
$weekTitleHeight = 16.0;
$dayHeaderHeight = 16.0;
$bodyHeight = 77.0;
$pdf->fillRect(self::MARGIN, $y, $usableWidth, $weekTitleHeight, 0.965);
$pdf->rect(self::MARGIN, $y, $usableWidth, $weekTitleHeight + $dayHeaderHeight + $bodyHeight, 0.78);
$pdf->text(self::MARGIN + 6, $y + 11, $this->weekLabel($week), 8.5, true);
$dayHeaderY = $y + $weekTitleHeight;
$bodyY = $dayHeaderY + $dayHeaderHeight;
foreach ($weekDays as $index => $day) {
$colX = self::MARGIN + ($index * $colWidth);
$pdf->fillRect($colX, $dayHeaderY, $colWidth, $dayHeaderHeight, 0.92);
$pdf->rect($colX, $dayHeaderY, $colWidth, $dayHeaderHeight + $bodyHeight, 0.82);
$pdf->text($colX + 5, $dayHeaderY + 11, $this->shortDayLabel($day['label']) . ' ' . substr($day['display'], 0, 5), 7.2, true);
$dayEntries = $entriesByDate[$day['date']] ?? [];
if ($dayEntries === []) {
$pdf->text($colX + 6, $bodyY + 17, 'Aucune affectation', 6.5);
continue;
}
$cursorY = $bodyY + 4;
$rendered = 0;
foreach ($dayEntries as $entryIndex => $entry) {
$lines = $this->compactAgentEntryLines($pdf, $entry, $colWidth - 18);
$cardHeight = max(18.0, 5.0 + (count($lines) * 7.2));
if ($cursorY + $cardHeight > $bodyY + $bodyHeight - 4) {
$remaining = count($dayEntries) - $rendered;
if ($remaining > 0) {
$pdf->text($colX + 7, $bodyY + $bodyHeight - 7, '+' . $remaining . ' autre(s) affectation(s)', 6.2, true);
}
break;
}
$palette = $this->motifPalette((string) ($entry['motif_code'] ?? 'AUTRE'));
$pdf->fillRectColor($colX + 4, $cursorY, $colWidth - 8, $cardHeight, $palette['background']);
$pdf->fillRectColor($colX + 4, $cursorY, 3.0, $cardHeight, $palette['accent']);
$lineY = $cursorY + 9;
foreach ($lines as $lineIndex => $line) {
$bold = $lineIndex === 0;
$pdf->text($colX + 10, $lineY, $line, 6.5, $bold);
$lineY += 7.2;
}
$cursorY += $cardHeight + 3.0;
$rendered++;
}
}
}
/** @return string[] */
private function compactAgentEntryLines(SimplePdf $pdf, array $entry, float $width): array
{
$lines = [sprintf('%s-%s', $entry['heure_debut'], $entry['heure_fin'])];
$motifLabel = $this->motifDisplayLabel($entry);
if ($motifLabel !== '') {
foreach ($pdf->wrap($motifLabel, $width, 6.3) as $line) {
$lines[] = $line;
}
}
$location = (string) ($entry['structure_nom'] ?? 'Lieu non renseigne');
foreach ($pdf->wrap('Lieu : ' . $location, $width, 6.1) as $line) {
$lines[] = $line;
}
if (($entry['statut'] ?? 'VALIDE') === 'BROUILLON') {
$lines[] = 'Brouillon';
}
return $lines;
}
/** @return string[] */
private function structureEntryLines(SimplePdf $pdf, array $entry, float $width): array
{
$first = sprintf('%s-%s', $entry['heure_debut'], $entry['heure_fin']);
$motif = $this->motifDisplayLabel($entry);
if ($motif !== '') {
$first .= ' ' . $motif;
}
return $pdf->wrap($first, $width, 7.0);
}
private function motifDisplayLabel(array $entry): string
{
return strtoupper((string) ($entry['motif_code'] ?? '')) === 'TRAVAIL'
? ''
: (string) ($entry['motif_libelle'] ?? 'Affectation');
}
/** @return array{accent:string,background:string,label:string} */
private function motifPalette(string $code): array
{
return match (strtoupper($code)) {
'TRAVAIL' => ['accent' => '#2563EB', 'background' => '#EFF6FF', 'label' => 'Travail'],
'FORMATION' => ['accent' => '#7C3AED', 'background' => '#F5F3FF', 'label' => 'Formation'],
'CONGE' => ['accent' => '#059669', 'background' => '#ECFDF5', 'label' => 'Conge'],
'MALADIE' => ['accent' => '#DC2626', 'background' => '#FEF2F2', 'label' => 'Absence'],
default => ['accent' => '#64748B', 'background' => '#F8FAFC', 'label' => 'Autre absence'],
};
}
private function drawLegend(SimplePdf $pdf, float $x, float $y): void
{
$items = [
['TRAVAIL', 'Travail'],
['FORMATION', 'Formation'],
['CONGE', 'Conge'],
['MALADIE', 'Absence'],
['AUTRE', 'Autre absence'],
];
foreach ($items as [$code, $label]) {
$palette = $this->motifPalette($code);
$pdf->fillRectColor($x, $y - 7, 8, 8, $palette['accent']);
$pdf->text($x + 12, $y, $label, 6.5);
$x += 76;
}
}
/** @return array<string, array<int, array>> */
private function entriesByDate(array $entries): array
{
$grouped = [];
foreach ($entries as $entry) {
$grouped[(string) $entry['date_jour']][] = $entry;
}
foreach ($grouped as &$dayEntries) {
usort($dayEntries, static fn (array $a, array $b): int => strcmp((string) $a['heure_debut'], (string) $b['heure_debut']));
}
unset($dayEntries);
return $grouped;
}
private function drawDocumentHeader(SimplePdf $pdf, string $title, string $subtitle, string $weekLabel): void
{
$pdf->text(self::MARGIN, 34, 'PTA', 10, true);
$pdf->text(self::MARGIN, 55, $title, 18, true);
$pdf->text(self::MARGIN, 72, $subtitle, 10);
$pdf->text($pdf->width() - 250, 55, $weekLabel, 10, true);
$pdf->line(self::MARGIN, 90, $pdf->width() - self::MARGIN, 90, 0.72);
}
/** @return array<int, array{date:string,label:string,display:string}> */
private function weekDays(array $week): array
{
$start = new DateTimeImmutable((string) $week['date_debut']);
$days = [];
for ($dayNumber = 1; $dayNumber <= 5; $dayNumber++) {
$date = $start->modify('+' . ($dayNumber - 1) . ' days');
$days[] = [
'date' => $date->format('Y-m-d'),
'label' => self::WEEK_DAYS[$dayNumber],
'display' => $date->format('d/m/Y'),
];
}
return $days;
}
private function weekLabel(array $week): string
{
$start = new DateTimeImmutable((string) $week['date_debut']);
$end = $start->modify('+4 days');
return sprintf('Semaine du %s au %s', $start->format('d/m/Y'), $end->format('d/m/Y'));
}
private function shortDayLabel(string $label): string
{
return match ($label) {
'Lundi' => 'Lun.',
'Mardi' => 'Mar.',
'Mercredi' => 'Mer.',
'Jeudi' => 'Jeu.',
'Vendredi' => 'Ven.',
default => $label,
};
}
private function typeLabel(string $type): string
{
return match (strtoupper($type)) {
'EXTRASCOLAIRE' => 'Extrascolaire',
'PERISCOLAIRE' => 'Periscolaire',
default => 'Non renseigne',
};
}
}