b5bd4db5f1
Auto Tag Develop / tag (push) Successful in 9s
## 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>
124 lines
4.4 KiB
PHP
124 lines
4.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Service\WorkHours;
|
|
|
|
use App\Entity\Employee;
|
|
use App\Entity\WorkHour;
|
|
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
|
use App\Service\Contracts\EmployeeContractResolver;
|
|
use App\Service\WorkHours\NightContingentExportBuilder;
|
|
use App\Service\WorkHours\NightHoursCalculator;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ReflectionProperty;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
final class NightContingentExportBuilderTest extends TestCase
|
|
{
|
|
public function testAggregatesNightMinutesAndDaysPerMonth(): void
|
|
{
|
|
$employee = $this->makeEmployee(1, 'Dupont', 'Jean');
|
|
|
|
// Janvier : un jour 4h de nuit (>=240 -> 1 jour) + un jour 3h59 (<240 -> 0 jour).
|
|
$whFull = new WorkHour()->setEmployee($employee)
|
|
->setWorkDate(new DateTimeImmutable('2026-01-10'))
|
|
->setEveningFrom('21:00')->setEveningTo('01:00') // 240 min nuit
|
|
;
|
|
$whShort = new WorkHour()->setEmployee($employee)
|
|
->setWorkDate(new DateTimeImmutable('2026-01-11'))
|
|
->setEveningFrom('21:00')->setEveningTo('00:59') // 239 min nuit
|
|
;
|
|
|
|
$workHourRepo = $this->createStub(WorkHourReadRepositoryInterface::class);
|
|
$workHourRepo->method('findByDateRangeAndEmployees')->willReturn([$whFull, $whShort]);
|
|
|
|
$contractResolver = $this->createStub(EmployeeContractResolver::class);
|
|
$contractResolver->method('resolveIsDriverForEmployeesAndDays')->willReturn([
|
|
1 => ['2026-01-10' => false, '2026-01-11' => false],
|
|
]);
|
|
|
|
$builder = new NightContingentExportBuilder(
|
|
$workHourRepo,
|
|
$contractResolver,
|
|
new NightHoursCalculator(),
|
|
);
|
|
|
|
$rows = $builder->buildRows([$employee], 2026);
|
|
|
|
self::assertCount(1, $rows);
|
|
self::assertSame(479, $rows[0]->months[1]['nightMinutes']); // 240 + 239
|
|
self::assertSame(1, $rows[0]->months[1]['nightDays']); // seul le jour >=240
|
|
self::assertSame(0, $rows[0]->months[2]['nightMinutes']); // fevrier vide
|
|
self::assertSame(0, $rows[0]->months[2]['nightDays']);
|
|
}
|
|
|
|
public function testDriverUsesManualNightMinutes(): void
|
|
{
|
|
$employee = $this->makeEmployee(2, 'Martin', 'Paul');
|
|
|
|
$wh = new WorkHour()->setEmployee($employee)
|
|
->setWorkDate(new DateTimeImmutable('2026-03-05'))
|
|
->setNightHoursMinutes(300)
|
|
->setMorningFrom('08:00')->setMorningTo('12:00') // ignore (driver)
|
|
;
|
|
|
|
$workHourRepo = $this->createStub(WorkHourReadRepositoryInterface::class);
|
|
$workHourRepo->method('findByDateRangeAndEmployees')->willReturn([$wh]);
|
|
|
|
$contractResolver = $this->createStub(EmployeeContractResolver::class);
|
|
$contractResolver->method('resolveIsDriverForEmployeesAndDays')->willReturn([
|
|
2 => ['2026-03-05' => true],
|
|
]);
|
|
|
|
$builder = new NightContingentExportBuilder(
|
|
$workHourRepo,
|
|
$contractResolver,
|
|
new NightHoursCalculator(),
|
|
);
|
|
|
|
$rows = $builder->buildRows([$employee], 2026);
|
|
|
|
self::assertSame(300, $rows[0]->months[3]['nightMinutes']);
|
|
self::assertSame(1, $rows[0]->months[3]['nightDays']); // 300 >= 240
|
|
}
|
|
|
|
public function testEmployeeWithoutWorkHoursYieldsAllZeroMonths(): void
|
|
{
|
|
$employee = $this->makeEmployee(3, 'Durand', 'Marie');
|
|
|
|
$workHourRepo = $this->createStub(WorkHourReadRepositoryInterface::class);
|
|
$workHourRepo->method('findByDateRangeAndEmployees')->willReturn([]);
|
|
|
|
$contractResolver = $this->createStub(EmployeeContractResolver::class);
|
|
$contractResolver->method('resolveIsDriverForEmployeesAndDays')->willReturn([]);
|
|
|
|
$builder = new NightContingentExportBuilder(
|
|
$workHourRepo,
|
|
$contractResolver,
|
|
new NightHoursCalculator(),
|
|
);
|
|
|
|
$rows = $builder->buildRows([$employee], 2026);
|
|
|
|
self::assertCount(1, $rows);
|
|
for ($m = 1; $m <= 12; ++$m) {
|
|
self::assertSame(0, $rows[0]->months[$m]['nightMinutes']);
|
|
self::assertSame(0, $rows[0]->months[$m]['nightDays']);
|
|
}
|
|
}
|
|
|
|
private function makeEmployee(int $id, string $last, string $first): Employee
|
|
{
|
|
$employee = new Employee();
|
|
$employee->setLastName($last)->setFirstName($first);
|
|
$ref = new ReflectionProperty(Employee::class, 'id');
|
|
$ref->setValue($employee, $id);
|
|
|
|
return $employee;
|
|
}
|
|
}
|