feat : modification de la gestion des jours fériés
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s

This commit is contained in:
2026-04-16 15:52:19 +02:00
parent 13c71abddc
commit a8fe244b5c
42 changed files with 1752 additions and 167 deletions

View File

@@ -14,7 +14,6 @@ use App\Enum\HalfDay;
use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\AuditLogger;
use App\Service\PublicHolidayServiceInterface;
use DateInterval;
use DatePeriod;
use DateTime;
@@ -24,7 +23,6 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Throwable;
final readonly class AbsenceWriteProcessor implements ProcessorInterface
{
@@ -33,7 +31,6 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
private AbsenceReadRepositoryInterface $absenceRepository,
private WorkHourReadRepositoryInterface $workHourRepository,
private Security $security,
private PublicHolidayServiceInterface $publicHolidayService,
private AuditLogger $auditLogger,
) {}
@@ -167,15 +164,10 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
throw new UnprocessableEntityHttpException('La demi-journée de fin ne peut pas être avant la demi-journée de début.');
}
$days = new DatePeriod($start, new DateInterval('P1D'), $end->modify('+1 day'));
$publicHolidays = $this->buildPublicHolidayMap($start, $end);
$days = new DatePeriod($start, new DateInterval('P1D'), $end->modify('+1 day'));
$segments = [];
foreach ($days as $day) {
if (isset($publicHolidays[$day->format('Y-m-d')])) {
continue;
}
$isFirst = $day->format('Y-m-d') === $start->format('Y-m-d');
$isLast = $day->format('Y-m-d') === $end->format('Y-m-d');
$isSame = $isFirst && $isLast;
@@ -286,27 +278,4 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
->setIsValid(false)
;
}
/**
* @return array<string, string>
*/
private function buildPublicHolidayMap(DateTimeImmutable $from, DateTimeImmutable $to): array
{
$map = [];
$startYear = (int) $from->format('Y');
$endYear = (int) $to->format('Y');
try {
for ($year = $startYear; $year <= $endYear; ++$year) {
$holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
foreach ($holidays as $date => $label) {
$map[(string) $date] = (string) $label;
}
}
} catch (Throwable) {
return [];
}
return $map;
}
}

View File

@@ -69,6 +69,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
endDate: $changeRequest->contractEndDate,
nature: $nature,
isDriver: $changeRequest->isDriver ?? false,
workDaysHours: $changeRequest->workDaysHours,
);
$data->setEntryDate($startDate);
@@ -138,6 +139,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
nature: $nature,
todayPeriod: $effectivePeriod,
isDriver: $changeRequest->isDriver ?? false,
workDaysHours: $changeRequest->workDaysHours,
);
return $result;

View File

@@ -14,6 +14,7 @@ use App\Repository\Contract\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\FormationReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\HolidayVirtualHoursResolver;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
use DateTimeImmutable;
use Symfony\Bundle\SecurityBundle\Security;
@@ -32,6 +33,7 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
private EmployeeContractResolver $contractResolver,
private AbsenceSegmentsResolver $absenceSegmentsResolver,
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourDayContext
@@ -56,10 +58,12 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
// On initialise toutes les lignes, même sans absence ce jour-là.
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, $workDate);
$workDaysMinutes = $this->contractResolver->resolveWorkDaysMinutesForEmployeeAndDate($employee, $workDate);
$rowsByEmployeeId[$employeeId] = new DayContextRow(
employeeId: $employeeId,
hasContractAtDate: null !== $contract,
isDriverContract: $this->contractResolver->resolveIsDriverForEmployeeAndDate($employee, $workDate),
virtualHolidayMinutes: $this->holidayVirtualHoursResolver->resolveVirtualCredit($contract, $workDate, false, $workDaysMinutes),
);
}
@@ -98,6 +102,14 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
$rowsByEmployeeId[$employeeId]->setFormation('Formation');
}
// If an absence is declared on the day, the absence dictates the hours credited
// (via WorkedHoursCreditPolicy). The holiday virtual credit must not stack on top.
foreach ($rowsByEmployeeId as $row) {
if ($row->absentMorning || $row->absentAfternoon) {
$row->virtualHolidayMinutes = 0;
}
}
$response = new WorkHourDayContext();
$response->workDate = $dateKey;
$response->rows = array_map(

View File

@@ -23,6 +23,8 @@ use App\Repository\Contract\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\DailyReferenceMinutesResolver;
use App\Service\WorkHours\HolidayVirtualHoursResolver;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
use DateTimeImmutable;
use Symfony\Bundle\SecurityBundle\Security;
@@ -41,6 +43,8 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
private AbsenceSegmentsResolver $absenceSegmentsResolver,
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
private EmployeeContractResolver $contractResolver,
private DailyReferenceMinutesResolver $dailyReferenceResolver,
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
@@ -117,6 +121,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$contractsByEmployeeDate = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
$contractNaturesByEmployeeDate = $this->contractResolver->resolveNaturesForEmployeesAndDays($employees, $days);
$isDriverByEmployeeDate = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
$workDaysByEmployeeDate = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
$metricsByEmployeeDate = [];
foreach ($workHours as $workHour) {
$employeeId = $workHour->getEmployee()?->getId();
@@ -276,6 +281,25 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
++$weeklyNightBasketCount;
}
// Apply the Mon-Fri public holiday credit rule: for non-Forfait contracts,
// if the total worked is below the contract-expected daily hours, top it up.
// Virtual minutes are always accounted against the "day" bucket.
// When an absence is declared on the day, the holiday credit is bypassed —
// the absence (via WorkedHoursCreditPolicy) dictates the hours.
$virtualHolidayMinutes = $this->holidayVirtualHoursResolver
->resolveVirtualCredit(
$contractAtDate,
new DateTimeImmutable($date),
$absenceByEmployeeDate[$employeeId][$date] ?? false,
$workDaysByEmployeeDate[$employeeId][$date] ?? null,
)
;
if ($virtualHolidayMinutes > $totalMinutes) {
$delta = $virtualHolidayMinutes - $totalMinutes;
$dayMinutes += $delta;
$totalMinutes = $virtualHolidayMinutes;
}
$weeklyDayMinutes += $dayMinutes;
$weeklyNightMinutes += $nightMinutes;
$weeklyWorkshopMinutes += $workshopMinutes;
@@ -299,6 +323,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
hasLunch: $hasLunch,
hasDinner: $hasDinner,
hasOvernight: $hasOvernight,
virtualHolidayMinutes: $virtualHolidayMinutes,
);
}
@@ -512,23 +537,6 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
private function resolveDailyReferenceMinutes(?int $weeklyHours, int $isoWeekDay): int
{
// Week-end hors base de référence.
if ($isoWeekDay >= 6) {
return 0;
}
if (null === $weeklyHours || $weeklyHours <= 0) {
return 0;
}
if (39 === $weeklyHours) {
return 5 === $isoWeekDay ? 7 * 60 : 8 * 60;
}
if (35 === $weeklyHours) {
return 7 * 60;
}
return (int) round(($weeklyHours * 60) / 5);
return $this->dailyReferenceResolver->resolve($weeklyHours, $isoWeekDay);
}
}