V7.10 — Tableau de bord, guide, semaine type et enveloppe de formation

Reprise dans le répertoire de production du travail mené dans le worktree
ux-service-enfance :

- tableau de bord Service Enfance (dashboard.php) et navigation regroupée ;
- guide d'utilisation (faq.php) accessible depuis toute la navigation ;
- éditeur de semaine type mis en avant sur l'écran de saisie ;
- droit à l'enveloppe de formation par agent, en remplacement de la case
  « formation déjà prévue dans la répartition annuelle » ;
- correctif de « [object Object] » sur les cartes de quota du PTA ;
- motif de travail par défaut à la saisie, heures travaillées complètes
  dans les PDF.

Le docker-compose.yml de production est conservé : celui du worktree décrit
une pile de test (conteneurs et volume suffixés « test », ports 9080/9081),
qui aurait fait redémarrer l'application sur un volume MariaDB vide.

Les migrations database/migration_v7_9*.sql sont obligatoires ; le service
db-migrate les applique au déploiement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
PTA
2026-08-17 16:53:04 +02:00
parent 57bd8da5c8
commit 4201894e28
69 changed files with 5228 additions and 246 deletions

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Services;
use App\Core\MotifCategories;
use DateTimeImmutable;
use DateTimeZone;
@@ -66,11 +67,11 @@ final class AgentTimeSummaryPdfService
$rowH = 43.0;
$rows = [
['TRAVAIL', 'Heures travaillées', $totals['TRAVAIL']],
['MALADIE', 'Absence', $totals['MALADIE']],
['FORMATION', 'Formation', $totals['FORMATION']],
['CONGE', 'Congé', $totals['CONGE']],
['AUTRE', 'Absence (non décomptée)', $totals['AUTRE']],
[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);
@@ -78,19 +79,75 @@ final class AgentTimeSummaryPdfService
$pdf->text($x + 12, $y + 21, 'Répartition du temps', 11, true);
$cursorY = $y + $headerH;
foreach ($rows as [$code, $label, $minutes]) {
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']);
$pdf->text($x + 32, $cursorY + 25, $label, 9.4, true);
$pdf->text($x + $w - 130, $cursorY + 25, $this->minutesLabel($minutes), 10, true);
$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<string, int> $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
{
$counted = $totals['TRAVAIL'] + $totals['MALADIE'] + $totals['FORMATION'] + $totals['CONGE'] + $totals['OTHER_COUNTED'];
// 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);
@@ -135,28 +192,38 @@ final class AgentTimeSummaryPdfService
);
}
/**
* 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 = [
'TRAVAIL' => 0,
'MALADIE' => 0,
'FORMATION' => 0,
'CONGE' => 0,
'AUTRE' => 0,
'OTHER_COUNTED' => 0,
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 = strtoupper((string) ($row['motif_code'] ?? 'AUTRE'));
$code = MotifCategories::normalizeCode($row['motif_code'] ?? null);
$minutes = (int) ($row['minutes_total'] ?? 0);
if (array_key_exists($code, $totals) && $code !== 'OTHER_COUNTED') {
$totals[$code] += $minutes;
continue;
$counts = (int) ($row['compte_dans_quota'] ?? 0) === 1;
$category = MotifCategories::category($code, $counts);
$totals[$category] += $minutes;
if ($counts) {
$totals['DECOMPTE'] += $minutes;
}
if ((int) ($row['compte_dans_quota'] ?? 0) === 1) {
$totals['OTHER_COUNTED'] += $minutes;
} else {
$totals['AUTRE'] += $minutes;
if ($category === MotifCategories::TRAVAIL && $minutes > 0) {
$label = trim((string) ($row['motif_libelle'] ?? '')) ?: $code;
$totals['DETAIL_TRAVAIL'][$label] = ($totals['DETAIL_TRAVAIL'][$label] ?? 0) + $minutes;
}
}
@@ -167,10 +234,10 @@ final class AgentTimeSummaryPdfService
private function palette(string $code): array
{
return match (strtoupper($code)) {
'TRAVAIL' => ['accent' => '#2563EB'],
'FORMATION' => ['accent' => '#7C3AED'],
'CONGE' => ['accent' => '#059669'],
'MALADIE' => ['accent' => '#DC2626'],
MotifCategories::TRAVAIL => ['accent' => '#2563EB'],
MotifCategories::FORMATION => ['accent' => '#7C3AED'],
MotifCategories::CONGE => ['accent' => '#059669'],
MotifCategories::ABSENCE, 'MALADIE' => ['accent' => '#DC2626'],
default => ['accent' => '#64748B'],
};
}