Files
SIRH/src/Service/WorkHours/NightContingentExportBuilder.php
T
tristan b5bd4db5f1
Auto Tag Develop / tag (push) Successful in 9s
feat(heures) : export Contingent heures de nuit (liste employés) (#28)
## Résumé
Nouvel export PDF **Contingent heures de nuit** dans le drawer Export de la liste employés.

- PDF **A4 paysage** : lignes = employés (groupés par site, triés displayOrder/nom/prénom), colonnes = 12 mois civils, chaque mois avec 2 sous-colonnes **H.nuit** et **N.jours**.
- Heures de nuit = minutes dans la fenêtre **21h→6h** via un service partagé `NightHoursCalculator` (mutualisé avec `WorkHourWeeklySummaryProvider` et `YearlyHoursExportBuilder` — duplication supprimée, sans changement de comportement).
- **Conducteurs inclus** via `WorkHour.nightHoursMinutes`. Statut conducteur résolu par date.
- **N.jours** = nb de jours où les minutes de nuit ≥ 240 (4h). Aucun crédit absence/férié.
- Périmètre via `EmployeeRepository::findScoped` (admin → tous, chef de site → ses sites), endpoint `GET /night-hours-contingent/print?year=YYYY` (`ROLE_USER`).
- Sélecteur d'année (année civile). Colonne Nom calibrée, séparateurs de mois épais.

## Composants
- Service `NightHoursCalculator`, builder `NightContingentExportBuilder`, DTO `NightContingentRow`
- Provider `NightHoursContingentPrintProvider` + opération API `NightHoursContingentPrint`
- Gabarit `templates/night-hours-contingent/print.html.twig`
- Option frontend dans `frontend/pages/employees/index.vue`
- Docs : `doc/functional-rules.md`, `CLAUDE.md`, `frontend/data/documentation-content.ts`

## Tests
- Nouveaux tests unitaires : `NightHoursCalculatorTest` (fenêtre 21h→6h, passage minuit, bornes), `NightContingentExportBuilderTest` (agrégation mensuelle, règle ≥4h=1j, conducteur, cas sans heures)
- Suite complète : **208 tests OK**
- Rendu PDF validé visuellement (Twig→Dompdf)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #28
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-11 13:02:30 +00:00

96 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service\WorkHours;
use App\Dto\WorkHours\NightContingentRow;
use App\Entity\Employee;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use DateTimeImmutable;
/**
* Construit, par employe, les totaux mensuels d'heures de nuit et le nombre de
* nuits travaillees (>= 4h de nuit dans la journee). Fenetre 21h->6h via
* NightHoursCalculator. Conducteurs : minutes saisies (nightHoursMinutes).
* Aucun credit absence/ferie : seules les heures reellement travaillees comptent.
*/
final readonly class NightContingentExportBuilder
{
private const int NIGHT_DAY_THRESHOLD_MINUTES = 240;
public function __construct(
private WorkHourReadRepositoryInterface $workHourRepository,
private EmployeeContractResolver $contractResolver,
private NightHoursCalculator $nightHoursCalculator,
) {}
/**
* @param list<Employee> $employees
*
* @return list<NightContingentRow>
*/
public function buildRows(array $employees, int $year): array
{
$from = new DateTimeImmutable(sprintf('%d-01-01', $year));
$to = new DateTimeImmutable(sprintf('%d-12-31', $year));
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, $employees);
$byEmployee = [];
foreach ($workHours as $wh) {
$employeeId = $wh->getEmployee()?->getId();
if (null === $employeeId) {
continue;
}
$byEmployee[$employeeId][] = $wh;
}
$days = [];
foreach ($workHours as $wh) {
$days[$wh->getWorkDate()->format('Y-m-d')] = true;
}
$days = array_keys($days);
$driverMap = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
$rows = [];
foreach ($employees as $employee) {
$employeeId = $employee->getId();
if (null === $employeeId) {
continue;
}
$months = [];
for ($m = 1; $m <= 12; ++$m) {
$months[$m] = ['nightMinutes' => 0, 'nightDays' => 0];
}
foreach ($byEmployee[$employeeId] ?? [] as $wh) {
$date = DateTimeImmutable::createFromInterface($wh->getWorkDate());
$ymd = $date->format('Y-m-d');
$isDriver = $driverMap[$employeeId][$ymd] ?? false;
$nightMin = $this->nightHoursCalculator->nightMinutesForWorkHour($wh, $isDriver);
if ($nightMin <= 0) {
continue;
}
$month = (int) $date->format('n');
$months[$month]['nightMinutes'] += $nightMin;
if ($nightMin >= self::NIGHT_DAY_THRESHOLD_MINUTES) {
++$months[$month]['nightDays'];
}
}
$rows[] = new NightContingentRow(
employeeId: $employeeId,
employeeName: trim($employee->getLastName().' '.$employee->getFirstName()),
months: $months,
);
}
return $rows;
}
}