feat : modification de la gestion des jours fériés
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
@@ -9,6 +9,9 @@ use DateTimeImmutable;
final readonly class EmployeeContractChangeRequest
{
/**
* @param null|array<int, int> $workDaysHours iso-day → minutes
*/
public function __construct(
public ?ContractNature $contractNature,
public ?DateTimeImmutable $contractStartDate,
@@ -16,6 +19,7 @@ final readonly class EmployeeContractChangeRequest
public ?bool $contractPaidLeaveSettled,
public ?string $contractComment,
public ?bool $isDriver = null,
public ?array $workDaysHours = null,
) {}
public function hasPeriodChangeRequest(): bool
@@ -20,6 +20,7 @@ final class EmployeeContractChangeRequestFactory
contractPaidLeaveSettled: $employee->getContractPaidLeaveSettled(),
contractComment: $employee->getContractComment(),
isDriver: $employee->getIsDriverInput(),
workDaysHours: $employee->getWorkDaysHoursInput(),
);
}
@@ -12,6 +12,9 @@ use DateTimeImmutable;
final class EmployeeContractPeriodBuilder
{
/**
* @param null|array<int, int> $workDaysHours iso-day → minutes
*/
public function build(
Employee $employee,
Contract $contract,
@@ -19,6 +22,7 @@ final class EmployeeContractPeriodBuilder
?DateTimeImmutable $endDate,
ContractNature $nature,
bool $isDriver = false,
?array $workDaysHours = null,
): EmployeeContractPeriod {
return new EmployeeContractPeriod()
->setEmployee($employee)
@@ -27,6 +31,7 @@ final class EmployeeContractPeriodBuilder
->setEndDate($endDate)
->setContractNature($nature)
->setIsDriver($isDriver)
->setWorkDaysHours($workDaysHours)
;
}
}
@@ -29,15 +29,17 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
?DateTimeImmutable $endDate,
ContractNature $nature,
bool $isDriver = false,
?array $workDaysHours = null,
): void {
$this->periodValidator->assertPeriodDates($startDate, $endDate, $nature);
$this->periodValidator->assertWorkDaysHours($contract, $nature, $workDaysHours);
$covered = $this->periodRepository->findOneCoveringDate($employee, $startDate);
if (null !== $covered) {
return;
}
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver);
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours);
$this->entityManager->flush();
}
@@ -75,8 +77,10 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
ContractNature $nature,
?EmployeeContractPeriod $todayPeriod,
bool $isDriver = false,
?array $workDaysHours = null,
): void {
$this->periodValidator->assertPeriodDates($startDate, $endDate, $nature);
$this->periodValidator->assertWorkDaysHours($contract, $nature, $workDaysHours);
if (null !== $todayPeriod) {
$this->periodValidator->assertNextStartDateCompatible($startDate, $todayPeriod);
@@ -86,10 +90,13 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
}
}
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver);
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours);
$this->entityManager->flush();
}
/**
* @param null|array<int, int> $workDaysHours
*/
private function persistNewPeriod(
Employee $employee,
Contract $contract,
@@ -97,8 +104,9 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
?DateTimeImmutable $endDate,
ContractNature $nature,
bool $isDriver = false,
?array $workDaysHours = null,
): void {
$period = $this->periodBuilder->build($employee, $contract, $startDate, $endDate, $nature, $isDriver);
$period = $this->periodBuilder->build($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours);
$this->entityManager->persist($period);
}
}
@@ -12,6 +12,9 @@ use DateTimeImmutable;
interface EmployeeContractPeriodManagerInterface
{
/**
* @param null|array<int, int> $workDaysHours iso-day → minutes
*/
public function ensureContractPeriodExists(
Employee $employee,
Contract $contract,
@@ -19,6 +22,7 @@ interface EmployeeContractPeriodManagerInterface
?DateTimeImmutable $endDate,
ContractNature $nature,
bool $isDriver = false,
?array $workDaysHours = null,
): void;
public function closeCurrentPeriod(
@@ -29,6 +33,9 @@ interface EmployeeContractPeriodManagerInterface
bool $isAlreadyEnded = false
): void;
/**
* @param null|array<int, int> $workDaysHours iso-day → minutes
*/
public function createNextPeriod(
Employee $employee,
Contract $contract,
@@ -37,5 +44,6 @@ interface EmployeeContractPeriodManagerInterface
ContractNature $nature,
?EmployeeContractPeriod $todayPeriod,
bool $isDriver = false,
?array $workDaysHours = null,
): void;
}
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace App\Service\Contracts;
use App\Entity\Contract;
use App\Entity\EmployeeContractPeriod;
use App\Enum\ContractNature;
use App\Enum\TrackingMode;
use DateTimeImmutable;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
@@ -60,4 +62,63 @@ final class EmployeeContractPeriodValidator
throw new UnprocessableEntityHttpException('contractStartDate must be after current contract end date.');
}
}
/**
* Validates the per-period work schedule (`workDaysHours`) against the contract.
*
* Mandatory for non-standard TIME contracts (weeklyHours ∉ {35, 39}, non-INTERIM,
* non-Forfait). Forbidden on standard/forfait/interim contracts (ambiguity).
* When provided, sum of minutes MUST equal weeklyHours × 60.
*
* @param null|array<int, int> $workDaysHours
*/
public function assertWorkDaysHours(?Contract $contract, ContractNature $nature, ?array $workDaysHours): void
{
if (null === $contract) {
return;
}
$trackingMode = $contract->getTrackingMode();
$weeklyHours = $contract->getWeeklyHours();
$isStandard = 35 === $weeklyHours || 39 === $weeklyHours;
$isForfait = TrackingMode::PRESENCE->value === $trackingMode;
$isInterim = ContractNature::INTERIM === $nature;
if ($isForfait || $isInterim || $isStandard) {
if (null !== $workDaysHours && [] !== $workDaysHours) {
throw new UnprocessableEntityHttpException('workDaysHours must not be provided for Forfait, Interim or 35h/39h contracts.');
}
return;
}
if (null === $workDaysHours || [] === $workDaysHours) {
throw new UnprocessableEntityHttpException('workDaysHours is required for non-standard contracts.');
}
$totalMinutes = 0;
foreach ($workDaysHours as $isoDay => $minutes) {
if (!is_int($isoDay) && !(is_string($isoDay) && ctype_digit($isoDay))) {
throw new UnprocessableEntityHttpException('workDaysHours keys must be iso weekdays 1-5 (Mon-Fri) as integers.');
}
$iso = (int) $isoDay;
if ($iso < 1 || $iso > 5) {
throw new UnprocessableEntityHttpException('workDaysHours keys must be iso weekdays 1-5 (Mon-Fri).');
}
if (!is_int($minutes) || $minutes < 0) {
throw new UnprocessableEntityHttpException('workDaysHours values must be non-negative integer minutes.');
}
$totalMinutes += $minutes;
}
$expectedMinutes = ($weeklyHours ?? 0) * 60;
if ($totalMinutes !== $expectedMinutes) {
throw new UnprocessableEntityHttpException(sprintf(
'workDaysHours total must equal contract weekly hours: got %d min, expected %d min.',
$totalMinutes,
$expectedMinutes
));
}
}
}
@@ -23,6 +23,20 @@ readonly class EmployeeContractResolver
return $period?->getContract();
}
/**
* @return null|array<int, int> workDaysHours (iso day → minutes) for the contract period active on $date
*/
public function resolveWorkDaysMinutesForEmployeeAndDate(Employee $employee, DateTimeImmutable $date): ?array
{
$period = $this->periodRepository->findOneCoveringDate($employee, $date);
$raw = $period?->getWorkDaysHours();
if (null === $raw) {
return null;
}
return $this->normalizeWorkDaysMinutes($raw);
}
public function resolveIsDriverForEmployeeAndDate(Employee $employee, DateTimeImmutable $date): bool
{
$period = $this->periodRepository->findOneCoveringDate($employee, $date);
@@ -84,6 +98,57 @@ readonly class EmployeeContractResolver
return $period?->getContractNatureEnum() ?? ContractNature::CDI;
}
/**
* @param list<Employee> $employees
* @param list<string> $days
*
* @return array<int, array<string, null|array<int, int>>>
*/
public function resolveWorkDaysMinutesForEmployeesAndDays(array $employees, array $days): array
{
$resolved = [];
if ([] === $employees || [] === $days) {
return $resolved;
}
foreach ($employees as $employee) {
$employeeId = $employee->getId();
if (!$employeeId) {
continue;
}
foreach ($days as $day) {
$resolved[$employeeId][$day] = null;
}
}
$from = new DateTimeImmutable(min($days));
$to = new DateTimeImmutable(max($days));
$periods = $this->periodRepository->findByEmployeesAndDateRange($employees, $from, $to);
foreach ($periods as $period) {
$employeeId = $period->getEmployee()?->getId();
if (!$employeeId) {
continue;
}
$raw = $period->getWorkDaysHours();
if (null === $raw) {
continue;
}
$normalized = $this->normalizeWorkDaysMinutes($raw);
$start = $period->getStartDate()->format('Y-m-d');
$end = $period->getEndDate()?->format('Y-m-d') ?? '9999-12-31';
foreach ($days as $day) {
if ($day < $start || $day > $end) {
continue;
}
$resolved[$employeeId][$day] = $normalized;
}
}
return $resolved;
}
/**
* @param list<Employee> $employees
* @param list<string> $days
@@ -177,4 +242,23 @@ readonly class EmployeeContractResolver
return $resolved;
}
/**
* @param array<int|string, mixed> $raw
*
* @return array<int, int>
*/
private function normalizeWorkDaysMinutes(array $raw): array
{
$result = [];
foreach ($raw as $key => $value) {
$iso = (int) $key;
if ($iso < 1 || $iso > 5) {
continue;
}
$result[$iso] = (int) $value;
}
return $result;
}
}