normalizeTotals($rows); $this->drawHeader($pdf, $agent, $periodLabel, $generatedAt); $this->drawIdentity($pdf, $agent); $this->drawBreakdown($pdf, $totals); $this->drawResult($pdf, $quota, $totals); $this->drawFooter($pdf, $agent, $generatedAt); return $pdf->output(); } private function drawHeader(SimplePdf $pdf, array $agent, string $periodLabel, DateTimeImmutable $generatedAt): void { $pdf->text(self::MARGIN, 34, 'PTA', 10, true); $pdf->text(self::MARGIN, 58, 'Compte-rendu du temps de l\'agent', 17, true); $pdf->text(self::MARGIN, 79, 'Synthèse des heures planifiées et décomptées pour la période ' . $periodLabel, 9.5); $pdf->text(self::PAGE_WIDTH - 190, 34, 'Édité le ' . $generatedAt->format('d/m/Y'), 8); $pdf->line(self::MARGIN, 94, self::PAGE_WIDTH - self::MARGIN, 94, 0.72); } private function drawIdentity(SimplePdf $pdf, array $agent): void { $x = self::MARGIN; $y = 118.0; $w = self::PAGE_WIDTH - (self::MARGIN * 2); $h = 90.0; $pdf->fillRect($x, $y, $w, 28, 0.93); $pdf->rect($x, $y, $w, $h, 0.68); $pdf->text($x + 12, $y + 19, 'Agent', 10.5, true); $pdf->text($x + 12, $y + 50, 'Nom : ' . (($agent['nom'] ?? '') ?: '-'), 9.2); $pdf->text($x + 270, $y + 50, 'Prénom : ' . (($agent['prenom'] ?? '') ?: '-'), 9.2); $pdf->text($x + 12, $y + 73, 'Matricule : ' . (($agent['matricule'] ?? '') ?: '-'), 9.2); $pdf->text($x + 270, $y + 73, 'Lieu principal : ' . (($agent['structure_principale_nom'] ?? '') ?: 'Non renseigné'), 9.2); } private function drawBreakdown(SimplePdf $pdf, array $totals): void { $x = self::MARGIN; $y = 232.0; $w = self::PAGE_WIDTH - (self::MARGIN * 2); $headerH = 31.0; $rowH = 43.0; $rows = [ [MotifCategories::TRAVAIL, 'Heures travaillées', $totals['TRAVAIL'], $totals['DETAIL_TRAVAIL']], [MotifCategories::ABSENCE, 'Absence', $totals['ABSENCE'], []], [MotifCategories::FORMATION, 'Formation', $totals['FORMATION'], []], [MotifCategories::CONGE, 'Congé', $totals['CONGE'], []], [MotifCategories::NON_DECOMPTE, 'Absence (non décomptée)', $totals['NON_DECOMPTE'], []], ]; $pdf->fillRect($x, $y, $w, $headerH, 0.92); $pdf->rect($x, $y, $w, $headerH + (count($rows) * $rowH), 0.65); $pdf->text($x + 12, $y + 21, 'Répartition du temps', 11, true); $cursorY = $y + $headerH; foreach ($rows as [$code, $label, $minutes, $detail]) { $palette = $this->palette($code); $pdf->line($x, $cursorY, $x + $w, $cursorY, 0.83); $pdf->fillRectColor($x + 10, $cursorY + 11, 10, 20, $palette['accent']); $detailLines = $detail === [] ? [] : $this->detailLines($pdf, $detail, $w - 174); if ($detailLines === []) { $pdf->text($x + 32, $cursorY + 25, $label, 9.4, true); $pdf->text($x + $w - 130, $cursorY + 25, $this->minutesLabel($minutes), 10, true); } else { $pdf->text($x + 32, $cursorY + 19, $label, 9.4, true); $pdf->text($x + $w - 130, $cursorY + 19, $this->minutesLabel($minutes), 10, true); $detailY = $cursorY + 30; foreach ($detailLines as $line) { $pdf->text($x + 32, $detailY, $line, 7); $detailY += 9; } } $cursorY += $rowH; } } /** * Détaille les motifs regroupés dans les heures travaillées (ALP matin, ALP soir, * restauration, préparations…), sur deux lignes au maximum. * * @param array $detail * @return string[] */ private function detailLines(SimplePdf $pdf, array $detail, float $width): array { arsort($detail); $parts = []; foreach ($detail as $label => $minutes) { if ($minutes > 0) { $parts[] = $label . ' ' . $this->minutesLabel($minutes); } } if ($parts === []) { return []; } // Les motifs sont ajoutés un par un : la coupure tombe ainsi entre deux motifs // et jamais au milieu d'un libellé, en gardant la place des points de suspension. $text = ''; $truncated = false; foreach ($parts as $part) { $candidate = $text === '' ? 'Dont ' . $part : $text . ' · ' . $part; if (count($pdf->wrap($candidate . ' …', $width, 7)) > 2) { $truncated = true; break; } $text = $candidate; } if ($text === '') { return []; } return $pdf->wrap($truncated ? $text . ' …' : $text, $width, 7); } private function drawResult(SimplePdf $pdf, array $quota, array $totals): void { // Le décompte suit le drapeau compte_dans_quota du motif, comme QuotaModel : // additionner les catégories affichées y ferait entrer les congés, hors quota. $counted = (int) $totals['DECOMPTE']; $target = (int) ($quota['quota_cible_minutes'] ?? 0); $remaining = $target - $counted; $validated = (int) ($quota['minutes_valides'] ?? 0); $draft = (int) ($quota['minutes_brouillon'] ?? 0); $x = self::MARGIN; $y = 507.0; $w = self::PAGE_WIDTH - (self::MARGIN * 2); $pdf->line($x, $y, $x + $w, $y, 0.55); $pdf->text($x, $y + 32, 'Résultat - heures décomptées', 12, true); $pdf->text($x + $w - 150, $y + 32, $this->minutesLabel($counted), 13, true); $pdf->text($x, $y + 55, sprintf('Dont %s validées et %s en brouillon.', $this->minutesLabel($validated), $this->minutesLabel($draft)), 8.5); $boxY = $y + 82; $boxH = 126.0; $pdf->fillRect($x, $boxY, $w, $boxH, 0.965); $pdf->rect($x, $boxY, $w, $boxH, 0.65); $pdf->text($x + 14, $boxY + 27, 'Quota à effectuer sur la période', 10, true); $pdf->text($x + $w - 160, $boxY + 27, $this->minutesLabel($target), 11, true); $pdf->line($x + 12, $boxY + 43, $x + $w - 12, $boxY + 43, 0.85); $pdf->text($x + 14, $boxY + 72, 'Heures décomptées à ce jour', 10, true); $pdf->text($x + $w - 160, $boxY + 72, $this->minutesLabel($counted), 11, true); $pdf->line($x + 12, $boxY + 88, $x + $w - 12, $boxY + 88, 0.85); $pdf->text($x + 14, $boxY + 114, $remaining >= 0 ? 'Heures restant à effectuer' : 'Dépassement du quota', 10.5, true); $pdf->textColor($x + $w - 160, $boxY + 114, $this->minutesLabel(abs($remaining)), $remaining >= 0 ? '#1D4ED8' : '#B91C1C', 12, true); $noteY = $boxY + $boxH + 27; foreach ($pdf->wrap('Le résultat ci-dessus additionne uniquement les motifs qui décomptent le quota annuel. Les absences non décomptées apparaissent dans le détail mais ne réduisent pas le nombre d’heures restant à effectuer.', $w, 8.5) as $line) { $pdf->text($x, $noteY, $line, 8.5); $noteY += 12; } } private function drawFooter(SimplePdf $pdf, array $agent, DateTimeImmutable $generatedAt): void { $pdf->text( self::MARGIN, 812, sprintf('Document généré par PTA le %s - Référence agent : %s', $generatedAt->format('d/m/Y H:i'), (string) ($agent['matricule'] ?? '-')), 7 ); } /** * Regroupe les minutes par catégorie métier. Tous les motifs de travail effectif * (ALP matin, ALP soir, restauration, mercredi périscolaire, extrascolaire, * préparations, ménage de fond…) alimentent la ligne « Heures travaillées », et pas * seulement le motif générique TRAVAIL. */ private function normalizeTotals(array $rows): array { $totals = [ MotifCategories::TRAVAIL => 0, MotifCategories::ABSENCE => 0, MotifCategories::FORMATION => 0, MotifCategories::CONGE => 0, MotifCategories::NON_DECOMPTE => 0, 'DECOMPTE' => 0, 'DETAIL_TRAVAIL' => [], ]; foreach ($rows as $row) { $code = MotifCategories::normalizeCode($row['motif_code'] ?? null); $minutes = (int) ($row['minutes_total'] ?? 0); $counts = (int) ($row['compte_dans_quota'] ?? 0) === 1; $category = MotifCategories::category($code, $counts); $totals[$category] += $minutes; if ($counts) { $totals['DECOMPTE'] += $minutes; } if ($category === MotifCategories::TRAVAIL && $minutes > 0) { $label = trim((string) ($row['motif_libelle'] ?? '')) ?: $code; $totals['DETAIL_TRAVAIL'][$label] = ($totals['DETAIL_TRAVAIL'][$label] ?? 0) + $minutes; } } return $totals; } /** @return array{accent:string} */ private function palette(string $code): array { return match (strtoupper($code)) { MotifCategories::TRAVAIL => ['accent' => '#2563EB'], MotifCategories::FORMATION => ['accent' => '#7C3AED'], MotifCategories::CONGE => ['accent' => '#059669'], MotifCategories::ABSENCE, 'MALADIE' => ['accent' => '#DC2626'], default => ['accent' => '#64748B'], }; } private function minutesLabel(int $minutes): string { $sign = $minutes < 0 ? '-' : ''; $absolute = abs($minutes); return sprintf('%s%d h %02d', $sign, intdiv($absolute, 60), $absolute % 60); } }