feat : ajout du système d'historique de contrat + correction affichage des absences sur la vue Jour
This commit is contained in:
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/sirh.sql" dialect="GenericSQL" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -99,10 +99,11 @@
|
||||
:disabled="isRowLocked(employee.id) || (!isAdmin && isEveningLockedByAbsence(employee.id))"
|
||||
/>
|
||||
</div>
|
||||
<div class="pl-2 self-stretch flex flex-col justify-between py-0.5">
|
||||
<div class="pl-2 min-w-0 self-stretch flex flex-col justify-between py-0.5">
|
||||
<p
|
||||
class="text-sm text-neutral-700 truncate"
|
||||
class="w-full min-w-0 text-sm text-neutral-700 truncate"
|
||||
:class="getRowAbsenceLabel(employee.id) ? '' : 'invisible'"
|
||||
:title="getRowAbsenceLabel(employee.id) || ''"
|
||||
>
|
||||
{{ getRowAbsenceLabel(employee.id) || '—' }}
|
||||
</p>
|
||||
|
||||
40
migrations/Version20260220133000.php
Normal file
40
migrations/Version20260220133000.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260220133000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add employee contract periods history table and seed current contracts';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE employee_contract_periods (id SERIAL NOT NULL, employee_id INT NOT NULL, contract_id INT NOT NULL, start_date DATE NOT NULL, end_date DATE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX idx_emp_contract_period_employee_start ON employee_contract_periods (employee_id, start_date)');
|
||||
$this->addSql('CREATE INDEX idx_emp_contract_period_employee_end ON employee_contract_periods (employee_id, end_date)');
|
||||
$this->addSql('CREATE INDEX IDX_831EED7A8C03F15C ON employee_contract_periods (employee_id)');
|
||||
$this->addSql('CREATE INDEX IDX_831EED7A2576E0FD ON employee_contract_periods (contract_id)');
|
||||
$this->addSql('ALTER TABLE employee_contract_periods ADD CONSTRAINT FK_831EED7A8C03F15C FOREIGN KEY (employee_id) REFERENCES employees (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE employee_contract_periods ADD CONSTRAINT FK_831EED7A2576E0FD FOREIGN KEY (contract_id) REFERENCES contracts (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
|
||||
// Initialise l\'historique avec le contrat actuel de chaque employé.
|
||||
$this->addSql("INSERT INTO employee_contract_periods (employee_id, contract_id, start_date, end_date, created_at)
|
||||
SELECT id, contract_id, DATE '1970-01-01', NULL, NOW()
|
||||
FROM employees
|
||||
WHERE contract_id IS NOT NULL");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE employee_contract_periods DROP CONSTRAINT FK_831EED7A8C03F15C');
|
||||
$this->addSql('ALTER TABLE employee_contract_periods DROP CONSTRAINT FK_831EED7A2576E0FD');
|
||||
$this->addSql('DROP TABLE employee_contract_periods');
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace App\Entity;
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\EmployeeRepository;
|
||||
use App\State\EmployeeWriteProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
@@ -15,7 +16,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
normalizationContext: ['groups' => ['employee:read', 'site:read']],
|
||||
denormalizationContext: ['groups' => ['employee:write']],
|
||||
paginationEnabled: false,
|
||||
security: "is_granted('ROLE_ADMIN')"
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
processor: EmployeeWriteProcessor::class,
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: EmployeeRepository::class)]
|
||||
#[ORM\Table(name: 'employees')]
|
||||
|
||||
102
src/Entity/EmployeeContractPeriod.php
Normal file
102
src/Entity/EmployeeContractPeriod.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: EmployeeContractPeriodRepository::class)]
|
||||
#[ORM\Table(name: 'employee_contract_periods')]
|
||||
#[ORM\Index(columns: ['employee_id', 'start_date'], name: 'idx_emp_contract_period_employee_start')]
|
||||
#[ORM\Index(columns: ['employee_id', 'end_date'], name: 'idx_emp_contract_period_employee_end')]
|
||||
class EmployeeContractPeriod
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Employee::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private ?Employee $employee = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Contract::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Contract $contract = null;
|
||||
|
||||
#[ORM\Column(type: 'date_immutable')]
|
||||
private DateTimeImmutable $startDate;
|
||||
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->startDate = new DateTimeImmutable('today');
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmployee(): ?Employee
|
||||
{
|
||||
return $this->employee;
|
||||
}
|
||||
|
||||
public function setEmployee(?Employee $employee): self
|
||||
{
|
||||
$this->employee = $employee;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContract(): ?Contract
|
||||
{
|
||||
return $this->contract;
|
||||
}
|
||||
|
||||
public function setContract(?Contract $contract): self
|
||||
{
|
||||
$this->contract = $contract;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStartDate(): DateTimeImmutable
|
||||
{
|
||||
return $this->startDate;
|
||||
}
|
||||
|
||||
public function setStartDate(DateTimeImmutable $startDate): self
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
public function setEndDate(?DateTimeImmutable $endDate): self
|
||||
{
|
||||
$this->endDate = $endDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
}
|
||||
75
src/Repository/EmployeeContractPeriodRepository.php
Normal file
75
src/Repository/EmployeeContractPeriodRepository.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<EmployeeContractPeriod>
|
||||
*/
|
||||
final class EmployeeContractPeriodRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, EmployeeContractPeriod::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Employee> $employees
|
||||
*
|
||||
* @return list<EmployeeContractPeriod>
|
||||
*/
|
||||
public function findByEmployeesAndDateRange(array $employees, DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||
{
|
||||
if ([] === $employees) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->createQueryBuilder('p')
|
||||
->andWhere('p.employee IN (:employees)')
|
||||
->andWhere('p.startDate <= :to')
|
||||
->andWhere('p.endDate IS NULL OR p.endDate >= :from')
|
||||
->setParameter('employees', $employees)
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to)
|
||||
->orderBy('p.startDate', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function findOneCoveringDate(Employee $employee, DateTimeImmutable $date): ?EmployeeContractPeriod
|
||||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->andWhere('p.employee = :employee')
|
||||
->andWhere('p.startDate <= :date')
|
||||
->andWhere('p.endDate IS NULL OR p.endDate >= :date')
|
||||
->setParameter('employee', $employee)
|
||||
->setParameter('date', $date)
|
||||
->orderBy('p.startDate', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function closeOpenPeriods(Employee $employee, DateTimeImmutable $endDate): int
|
||||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->update()
|
||||
->set('p.endDate', ':endDate')
|
||||
->andWhere('p.employee = :employee')
|
||||
->andWhere('p.endDate IS NULL')
|
||||
->setParameter('employee', $employee)
|
||||
->setParameter('endDate', $endDate)
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
}
|
||||
97
src/Service/Contracts/EmployeeContractResolver.php
Normal file
97
src/Service/Contracts/EmployeeContractResolver.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Contracts;
|
||||
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use DateTimeImmutable;
|
||||
use LogicException;
|
||||
|
||||
readonly class EmployeeContractResolver
|
||||
{
|
||||
public function __construct(
|
||||
private EmployeeContractPeriodRepository $periodRepository,
|
||||
) {}
|
||||
|
||||
public function resolveForEmployeeAndDate(Employee $employee, DateTimeImmutable $date): ?Contract
|
||||
{
|
||||
$period = $this->periodRepository->findOneCoveringDate($employee, $date);
|
||||
$contract = $period?->getContract();
|
||||
if (null === $contract) {
|
||||
throw new LogicException(sprintf(
|
||||
'Missing contract period for employee %d on %s.',
|
||||
$employee->getId() ?? 0,
|
||||
$date->format('Y-m-d')
|
||||
));
|
||||
}
|
||||
|
||||
return $contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Employee> $employees
|
||||
* @param list<string> $days
|
||||
*
|
||||
* @return array<int, array<string, ?Contract>>
|
||||
*/
|
||||
public function resolveForEmployeesAndDays(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();
|
||||
$contract = $period->getContract();
|
||||
if (!$employeeId || null === $contract) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$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] = $contract;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($employees as $employee) {
|
||||
$employeeId = $employee->getId();
|
||||
if (!$employeeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($days as $day) {
|
||||
if (null === ($resolved[$employeeId][$day] ?? null)) {
|
||||
throw new LogicException(sprintf(
|
||||
'Missing contract period for employee %d on %s.',
|
||||
$employeeId,
|
||||
$day
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,16 @@ namespace App\Service\WorkHours;
|
||||
|
||||
use App\Entity\Absence;
|
||||
use App\Enum\TrackingMode;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use DateMalformedStringException;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class WorkedHoursCreditPolicy
|
||||
final readonly class WorkedHoursCreditPolicy
|
||||
{
|
||||
public function __construct(
|
||||
private EmployeeContractResolver $contractResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws DateMalformedStringException
|
||||
*/
|
||||
@@ -23,14 +28,19 @@ final class WorkedHoursCreditPolicy
|
||||
}
|
||||
|
||||
$employee = $absence->getEmployee();
|
||||
if (null === $employee) {
|
||||
return 0;
|
||||
}
|
||||
$workDate = new DateTimeImmutable($dateYmd);
|
||||
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, $workDate);
|
||||
// Les contrats suivis en "présence" ne cumulent pas d'heures en minutes.
|
||||
if (TrackingMode::TIME->value !== $employee?->getContract()?->getTrackingMode()) {
|
||||
if (TrackingMode::TIME->value !== $contract?->getTrackingMode()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$weekday = (int) new DateTimeImmutable($dateYmd)->format('N');
|
||||
$weekday = (int) $workDate->format('N');
|
||||
// On applique la règle de crédit dépendante du contrat (35h / 39h / fallback).
|
||||
$dayMinutes = $this->resolveContractDayMinutes($employee->getContract()?->getWeeklyHours(), $weekday);
|
||||
$dayMinutes = $this->resolveContractDayMinutes($contract?->getWeeklyHours(), $weekday);
|
||||
if ($dayMinutes <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -41,15 +51,26 @@ final class WorkedHoursCreditPolicy
|
||||
return (int) round(($dayMinutes / 2) * $halfUnits);
|
||||
}
|
||||
|
||||
public function computeCreditedPresenceUnits(Absence $absence, bool $absentMorning, bool $absentAfternoon): float
|
||||
{
|
||||
/**
|
||||
* @throws DateMalformedStringException
|
||||
*/
|
||||
public function computeCreditedPresenceUnits(
|
||||
Absence $absence,
|
||||
string $dateYmd,
|
||||
bool $absentMorning,
|
||||
bool $absentAfternoon
|
||||
): float {
|
||||
$type = $absence->getType();
|
||||
if (!$type?->getCountAsWorkedHours()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$employee = $absence->getEmployee();
|
||||
if (TrackingMode::PRESENCE->value !== $employee?->getContract()?->getTrackingMode()) {
|
||||
if (null === $employee) {
|
||||
return 0.0;
|
||||
}
|
||||
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, new DateTimeImmutable($dateYmd));
|
||||
if (TrackingMode::PRESENCE->value !== $contract?->getTrackingMode()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
||||
121
src/State/EmployeeWriteProcessor.php
Normal file
121
src/State/EmployeeWriteProcessor.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\DeleteOperationInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||
private ProcessorInterface $persistProcessor,
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')]
|
||||
private ProcessorInterface $removeProcessor,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private EmployeeContractPeriodRepository $periodRepository,
|
||||
) {}
|
||||
|
||||
public function process(
|
||||
mixed $data,
|
||||
Operation $operation,
|
||||
array $uriVariables = [],
|
||||
array $context = []
|
||||
): mixed {
|
||||
if ($operation instanceof DeleteOperationInterface) {
|
||||
return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
if (!$data instanceof Employee) {
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
$isNew = null === $data->getId();
|
||||
$previousContract = $this->resolvePreviousContract($data);
|
||||
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
|
||||
$currentContract = $data->getContract();
|
||||
if (!$currentContract instanceof Contract) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$today = new DateTimeImmutable('today');
|
||||
if ($isNew) {
|
||||
$this->ensureContractPeriodExists($data, $currentContract, $today);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($this->isSameContract($previousContract, $currentContract)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$todayPeriod = $this->periodRepository->findOneCoveringDate($data, $today);
|
||||
if (null !== $todayPeriod && null === $todayPeriod->getEndDate() && $todayPeriod->getStartDate() === $today) {
|
||||
$todayPeriod->setContract($currentContract);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$this->periodRepository->closeOpenPeriods($data, $today->modify('-1 day'));
|
||||
$this->createPeriod($data, $currentContract, $today);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function resolvePreviousContract(Employee $employee): ?Contract
|
||||
{
|
||||
if (null === $employee->getId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$originalData = $this->entityManager->getUnitOfWork()->getOriginalEntityData($employee);
|
||||
$original = $originalData['contract'] ?? null;
|
||||
|
||||
return $original instanceof Contract ? $original : null;
|
||||
}
|
||||
|
||||
private function isSameContract(?Contract $first, ?Contract $second): bool
|
||||
{
|
||||
if (null === $first || null === $second) {
|
||||
return $first === $second;
|
||||
}
|
||||
|
||||
return $first->getId() === $second->getId();
|
||||
}
|
||||
|
||||
private function ensureContractPeriodExists(Employee $employee, Contract $contract, DateTimeImmutable $startDate): void
|
||||
{
|
||||
$covered = $this->periodRepository->findOneCoveringDate($employee, $startDate);
|
||||
if (null !== $covered) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->createPeriod($employee, $contract, $startDate);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
private function createPeriod(Employee $employee, Contract $contract, DateTimeImmutable $startDate): void
|
||||
{
|
||||
$period = new EmployeeContractPeriod()
|
||||
->setEmployee($employee)
|
||||
->setContract($contract)
|
||||
->setStartDate($startDate)
|
||||
->setEndDate(null)
|
||||
;
|
||||
|
||||
$this->entityManager->persist($period);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use App\Entity\WorkHour;
|
||||
use App\Enum\TrackingMode;
|
||||
use App\Repository\EmployeeRepository;
|
||||
use App\Repository\WorkHourRepository;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
@@ -27,6 +28,7 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
private Security $security,
|
||||
private EmployeeRepository $employeeRepository,
|
||||
private WorkHourRepository $workHourRepository,
|
||||
private EmployeeContractResolver $contractResolver,
|
||||
) {}
|
||||
|
||||
public function process(
|
||||
@@ -75,7 +77,8 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
throw new AccessDeniedHttpException(sprintf('Employee %d is outside your scope.', $employeeId));
|
||||
}
|
||||
|
||||
$isPresenceTracking = TrackingMode::PRESENCE->value === $employee->getContract()?->getTrackingMode();
|
||||
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, $workDate);
|
||||
$isPresenceTracking = TrackingMode::PRESENCE->value === $contract?->getTrackingMode();
|
||||
$normalized = $this->normalizeEntry($entry, $employeeId, $isPresenceTracking);
|
||||
$existing = $existingByEmployeeId[$employeeId] ?? null;
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
|
||||
|
||||
// Calcule le crédit d'heures selon la politique métier (type d'absence + contrat).
|
||||
$creditedMinutes = $this->workedHoursCreditPolicy->computeCreditedMinutes($absence, $dateKey, $absentMorning, $absentAfternoon);
|
||||
$creditedPresenceUnits = $this->workedHoursCreditPolicy->computeCreditedPresenceUnits($absence, $absentMorning, $absentAfternoon);
|
||||
$creditedPresenceUnits = $this->workedHoursCreditPolicy->computeCreditedPresenceUnits($absence, $dateKey, $absentMorning, $absentAfternoon);
|
||||
$rowsByEmployeeId[$employeeId]->addAbsence(
|
||||
label: $absence->getType()?->getLabel(),
|
||||
morning: $absentMorning,
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Dto\WorkHours\WeeklyDaySummary;
|
||||
use App\Dto\WorkHours\WeeklySummaryRow;
|
||||
use App\Dto\WorkHours\WorkMetrics;
|
||||
use App\Entity\Absence;
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkHour;
|
||||
@@ -19,6 +20,7 @@ use App\Enum\TrackingMode;
|
||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
||||
use DateTimeImmutable;
|
||||
@@ -37,6 +39,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
private AbsenceReadRepositoryInterface $absenceRepository,
|
||||
private AbsenceSegmentsResolver $absenceSegmentsResolver,
|
||||
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
|
||||
private EmployeeContractResolver $contractResolver,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
|
||||
@@ -58,7 +61,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
$summary->weekStart = $weekStart->format('Y-m-d');
|
||||
$summary->weekEnd = $weekEnd->format('Y-m-d');
|
||||
$summary->days = $days;
|
||||
$summary->rows = $this->buildRows($employees, $workHours, $absences, $days);
|
||||
$summary->rows = $this->buildRows($employees, $workHours, $absences, $days, $anchorDate->format('Y-m-d'));
|
||||
|
||||
return $summary;
|
||||
}
|
||||
@@ -108,9 +111,10 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
*
|
||||
* @return list<WeeklySummaryRow>
|
||||
*/
|
||||
private function buildRows(array $employees, array $workHours, array $absences, array $days): array
|
||||
private function buildRows(array $employees, array $workHours, array $absences, array $days, string $anchorDateYmd): array
|
||||
{
|
||||
$metricsByEmployeeDate = [];
|
||||
$contractsByEmployeeDate = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
|
||||
$metricsByEmployeeDate = [];
|
||||
foreach ($workHours as $workHour) {
|
||||
$employeeId = $workHour->getEmployee()?->getId();
|
||||
if (!$employeeId) {
|
||||
@@ -158,7 +162,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
$creditedByEmployeeDate[$employeeId][$date] = ($creditedByEmployeeDate[$employeeId][$date] ?? 0)
|
||||
+ $this->workedHoursCreditPolicy->computeCreditedMinutes($absence, $date, $absentMorning, $absentAfternoon);
|
||||
$creditedPresenceByEmployeeDate[$employeeId][$date] = ($creditedPresenceByEmployeeDate[$employeeId][$date] ?? 0.0)
|
||||
+ $this->workedHoursCreditPolicy->computeCreditedPresenceUnits($absence, $absentMorning, $absentAfternoon);
|
||||
+ $this->workedHoursCreditPolicy->computeCreditedPresenceUnits($absence, $date, $absentMorning, $absentAfternoon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +179,20 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
$weeklyPresenceCount = 0.0;
|
||||
$daily = [];
|
||||
// Les contrats au suivi "présence" ne manipulent pas les heures, mais des demi-journées.
|
||||
$isPresenceTracking = TrackingMode::PRESENCE->value === $employee->getContract()?->getTrackingMode();
|
||||
$weekAnchorContract = $contractsByEmployeeDate[$employeeId][$anchorDateYmd]
|
||||
?? $contractsByEmployeeDate[$employeeId][$days[0]]
|
||||
?? null;
|
||||
$employeeContractsByDate = [];
|
||||
foreach ($days as $date) {
|
||||
$employeeContractsByDate[$date] = $contractsByEmployeeDate[$employeeId][$date] ?? null;
|
||||
}
|
||||
|
||||
foreach ($days as $date) {
|
||||
$entry = $metricsByEmployeeDate[$employeeId][$date] ?? null;
|
||||
$metrics = $entry['metrics'] ?? new WorkMetrics();
|
||||
$creditedMinutes = $creditedByEmployeeDate[$employeeId][$date] ?? 0;
|
||||
$entry = $metricsByEmployeeDate[$employeeId][$date] ?? null;
|
||||
$metrics = $entry['metrics'] ?? new WorkMetrics();
|
||||
$creditedMinutes = $creditedByEmployeeDate[$employeeId][$date] ?? 0;
|
||||
$contractAtDate = $employeeContractsByDate[$date] ?? null;
|
||||
$isPresenceTracking = TrackingMode::PRESENCE->value === $contractAtDate?->getTrackingMode();
|
||||
// Les absences "comptées comme travaillées" alimentent le total du jour.
|
||||
$metrics->addCreditedMinutes($creditedMinutes);
|
||||
$present = null;
|
||||
@@ -210,18 +222,20 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
);
|
||||
}
|
||||
|
||||
$contractWeeklyHours = $employee->getContract()?->getWeeklyHours();
|
||||
$disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($employee);
|
||||
$weeklyOvertimeTotalMinutes = $isPresenceTracking
|
||||
$isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode();
|
||||
$disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract);
|
||||
$overtimeReferenceMinutes = $this->computeWeeklyOvertimeReferenceMinutes($days, $employeeContractsByDate);
|
||||
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($days, $employeeContractsByDate);
|
||||
$weeklyOvertimeTotalMinutes = $isWeekPresenceTracking
|
||||
? 0
|
||||
: $this->computeOvertimeTotalMinutes($weeklyTotalMinutes, $contractWeeklyHours);
|
||||
$weeklyOvertime25Minutes = ($isPresenceTracking || $disableOvertimeBonuses)
|
||||
: max(0, $weeklyTotalMinutes - $overtimeReferenceMinutes);
|
||||
$weeklyOvertime25Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
|
||||
? 0
|
||||
: $this->computeOvertime25BonusMinutes($weeklyTotalMinutes, $contractWeeklyHours);
|
||||
$weeklyOvertime50Minutes = ($isPresenceTracking || $disableOvertimeBonuses)
|
||||
: $this->computeOvertime25BonusMinutes($weeklyTotalMinutes, $overtime25StartMinutes);
|
||||
$weeklyOvertime50Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
|
||||
? 0
|
||||
: $this->computeOvertime50BonusMinutes($weeklyTotalMinutes);
|
||||
$weeklyRecoveryMinutes = ($isPresenceTracking || $disableOvertimeBonuses)
|
||||
$weeklyRecoveryMinutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
|
||||
? 0
|
||||
: $weeklyOvertimeTotalMinutes + $weeklyOvertime25Minutes + $weeklyOvertime50Minutes;
|
||||
|
||||
@@ -230,9 +244,9 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
firstName: $employee->getFirstName(),
|
||||
lastName: $employee->getLastName(),
|
||||
siteName: $employee->getSite()?->getName(),
|
||||
contractName: $employee->getContract()?->getName(),
|
||||
contractType: $employee->getContract()?->getType()->value,
|
||||
trackingMode: $employee->getContract()?->getTrackingMode(),
|
||||
contractName: $weekAnchorContract?->getName(),
|
||||
contractType: $weekAnchorContract?->getType()->value,
|
||||
trackingMode: $weekAnchorContract?->getTrackingMode(),
|
||||
daily: $daily,
|
||||
weeklyDayMinutes: $weeklyDayMinutes,
|
||||
weeklyNightMinutes: $weeklyNightMinutes,
|
||||
@@ -344,25 +358,43 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
return max(0, $end - $start);
|
||||
}
|
||||
|
||||
private function computeOvertimeTotalMinutes(int $weeklyTotalMinutes, ?int $contractWeeklyHours): int
|
||||
/**
|
||||
* @param array<string, ?Contract> $contractsByDate
|
||||
*/
|
||||
private function computeWeeklyOvertimeReferenceMinutes(array $days, array $contractsByDate): int
|
||||
{
|
||||
if (null === $contractWeeklyHours || $contractWeeklyHours <= 0) {
|
||||
return 0;
|
||||
$total = 0;
|
||||
foreach ($days as $date) {
|
||||
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
||||
$contract = $contractsByDate[$date] ?? null;
|
||||
$hours = $contract?->getWeeklyHours();
|
||||
$referenceHours = (null !== $hours && $hours > 0) ? max(35, $hours) : null;
|
||||
$total += $this->resolveDailyReferenceMinutes($referenceHours, $isoDay);
|
||||
}
|
||||
|
||||
// Règle métier: tout contrat < 35h est traité comme un 35h pour la base supp.
|
||||
$referenceHours = max(35, $contractWeeklyHours);
|
||||
|
||||
return max(0, $weeklyTotalMinutes - ($referenceHours * 60));
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function computeOvertime25BonusMinutes(int $weeklyTotalMinutes, ?int $contractWeeklyHours): int
|
||||
/**
|
||||
* @param array<string, ?Contract> $contractsByDate
|
||||
*/
|
||||
private function computeWeeklyOvertime25StartMinutes(array $days, array $contractsByDate): int
|
||||
{
|
||||
// Règle métier:
|
||||
// - contrats <= 35h: 25% entre 35h et 43h
|
||||
// - contrats >= 39h: 25% entre 39h et 43h
|
||||
$startHours = (null !== $contractWeeklyHours && $contractWeeklyHours >= 39) ? 39 : 35;
|
||||
$trancheMinutes = max(0, min($weeklyTotalMinutes, 43 * 60) - ($startHours * 60));
|
||||
$total = 0;
|
||||
foreach ($days as $date) {
|
||||
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
||||
$contract = $contractsByDate[$date] ?? null;
|
||||
$hours = $contract?->getWeeklyHours();
|
||||
$startHours = (null !== $hours && $hours >= 39) ? 39 : 35;
|
||||
$total += $this->resolveDailyReferenceMinutes($startHours, $isoDay);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function computeOvertime25BonusMinutes(int $weeklyTotalMinutes, int $startMinutes): int
|
||||
{
|
||||
$trancheMinutes = max(0, min($weeklyTotalMinutes, 43 * 60) - $startMinutes);
|
||||
|
||||
return (int) round($trancheMinutes * 0.25);
|
||||
}
|
||||
@@ -375,10 +407,9 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
return (int) round($trancheMinutes * 0.5);
|
||||
}
|
||||
|
||||
private function hasDisabledOvertimeBonuses(Employee $employee): bool
|
||||
private function hasDisabledOvertimeBonuses(?Contract $contract): bool
|
||||
{
|
||||
$contract = $employee->getContract();
|
||||
$type = ContractType::resolve(
|
||||
$type = ContractType::resolve(
|
||||
$contract?->getName(),
|
||||
$contract?->getTrackingMode(),
|
||||
$contract?->getWeeklyHours()
|
||||
@@ -386,4 +417,26 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
|
||||
return ContractType::INTERIM === $type;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Entity\Absence;
|
||||
use App\Entity\AbsenceType;
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
||||
use DateTime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -19,7 +20,7 @@ final class WorkedHoursCreditPolicyTest extends TestCase
|
||||
{
|
||||
public function testComputeCreditedMinutesFor35hHalfDay(): void
|
||||
{
|
||||
$policy = new WorkedHoursCreditPolicy();
|
||||
$policy = new WorkedHoursCreditPolicy($this->buildResolverStub());
|
||||
$absence = $this->buildAbsence(trackMode: Contract::TRACKING_TIME, weeklyHours: 35, countAsWorked: true);
|
||||
|
||||
$minutes = $policy->computeCreditedMinutes($absence, '2026-02-16', true, false);
|
||||
@@ -29,7 +30,7 @@ final class WorkedHoursCreditPolicyTest extends TestCase
|
||||
|
||||
public function testComputeCreditedMinutesFor4hContractFullDay(): void
|
||||
{
|
||||
$policy = new WorkedHoursCreditPolicy();
|
||||
$policy = new WorkedHoursCreditPolicy($this->buildResolverStub());
|
||||
$absence = $this->buildAbsence(trackMode: Contract::TRACKING_TIME, weeklyHours: 4, countAsWorked: true);
|
||||
|
||||
$minutes = $policy->computeCreditedMinutes($absence, '2026-02-16', true, true);
|
||||
@@ -39,21 +40,21 @@ final class WorkedHoursCreditPolicyTest extends TestCase
|
||||
|
||||
public function testComputeCreditedPresenceUnitsForPresenceContract(): void
|
||||
{
|
||||
$policy = new WorkedHoursCreditPolicy();
|
||||
$policy = new WorkedHoursCreditPolicy($this->buildResolverStub());
|
||||
$absence = $this->buildAbsence(trackMode: Contract::TRACKING_PRESENCE, weeklyHours: null, countAsWorked: true);
|
||||
|
||||
$units = $policy->computeCreditedPresenceUnits($absence, true, false);
|
||||
$units = $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, false);
|
||||
|
||||
self::assertSame(0.5, $units);
|
||||
}
|
||||
|
||||
public function testNoCreditWhenAbsenceTypeDoesNotCount(): void
|
||||
{
|
||||
$policy = new WorkedHoursCreditPolicy();
|
||||
$policy = new WorkedHoursCreditPolicy($this->buildResolverStub());
|
||||
$absence = $this->buildAbsence(trackMode: Contract::TRACKING_TIME, weeklyHours: 35, countAsWorked: false);
|
||||
|
||||
self::assertSame(0, $policy->computeCreditedMinutes($absence, '2026-02-16', true, true));
|
||||
self::assertSame(0.0, $policy->computeCreditedPresenceUnits($absence, true, true));
|
||||
self::assertSame(0.0, $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, true));
|
||||
}
|
||||
|
||||
private function buildAbsence(string $trackMode, ?int $weeklyHours, bool $countAsWorked): Absence
|
||||
@@ -79,6 +80,18 @@ final class WorkedHoursCreditPolicyTest extends TestCase
|
||||
->setEmployee($employee)
|
||||
->setType($type)
|
||||
->setStartDate(new DateTime('2026-02-16'))
|
||||
->setEndDate(new DateTime('2026-02-16'));
|
||||
->setEndDate(new DateTime('2026-02-16'))
|
||||
;
|
||||
}
|
||||
|
||||
private function buildResolverStub(): EmployeeContractResolver
|
||||
{
|
||||
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||
$resolver
|
||||
->method('resolveForEmployeeAndDate')
|
||||
->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract())
|
||||
;
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Enum\HalfDay;
|
||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
||||
use App\State\WorkHourDayContextProvider;
|
||||
@@ -53,7 +54,7 @@ final class WorkHourDayContextProviderTest extends TestCase
|
||||
$this->employeeRepository,
|
||||
$this->absenceRepository,
|
||||
new AbsenceSegmentsResolver(),
|
||||
new WorkedHoursCreditPolicy()
|
||||
new WorkedHoursCreditPolicy($this->buildResolverStub())
|
||||
);
|
||||
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
@@ -71,7 +72,7 @@ final class WorkHourDayContextProviderTest extends TestCase
|
||||
$this->employeeRepository,
|
||||
$this->absenceRepository,
|
||||
new AbsenceSegmentsResolver(),
|
||||
new WorkedHoursCreditPolicy()
|
||||
new WorkedHoursCreditPolicy($this->buildResolverStub())
|
||||
);
|
||||
|
||||
$this->expectException(UnprocessableEntityHttpException::class);
|
||||
@@ -95,7 +96,7 @@ final class WorkHourDayContextProviderTest extends TestCase
|
||||
$this->employeeRepository,
|
||||
$this->absenceRepository,
|
||||
new AbsenceSegmentsResolver(),
|
||||
new WorkedHoursCreditPolicy()
|
||||
new WorkedHoursCreditPolicy($this->buildResolverStub())
|
||||
);
|
||||
|
||||
$result = $provider->provide(new Get());
|
||||
@@ -151,4 +152,15 @@ final class WorkHourDayContextProviderTest extends TestCase
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($entity, $id);
|
||||
}
|
||||
|
||||
private function buildResolverStub(): EmployeeContractResolver
|
||||
{
|
||||
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||
$resolver
|
||||
->method('resolveForEmployeeAndDate')
|
||||
->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract())
|
||||
;
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Enum\HalfDay;
|
||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
||||
use App\State\WorkHourWeeklySummaryProvider;
|
||||
@@ -58,7 +59,8 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
||||
$this->workHourRepository,
|
||||
$this->absenceRepository,
|
||||
new AbsenceSegmentsResolver(),
|
||||
new WorkedHoursCreditPolicy()
|
||||
new WorkedHoursCreditPolicy($this->buildResolverStub()),
|
||||
$this->buildResolverStub()
|
||||
);
|
||||
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
@@ -117,7 +119,8 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
||||
$this->workHourRepository,
|
||||
$this->absenceRepository,
|
||||
new AbsenceSegmentsResolver(),
|
||||
new WorkedHoursCreditPolicy()
|
||||
new WorkedHoursCreditPolicy($this->buildResolverStub()),
|
||||
$this->buildWeeklyResolverStub($employees)
|
||||
);
|
||||
|
||||
$result = $provider->provide(new Get());
|
||||
@@ -167,4 +170,50 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($entity, $id);
|
||||
}
|
||||
|
||||
private function buildResolverStub(): EmployeeContractResolver
|
||||
{
|
||||
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||
$resolver
|
||||
->method('resolveForEmployeeAndDate')
|
||||
->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract())
|
||||
;
|
||||
$resolver
|
||||
->method('resolveForEmployeesAndDays')
|
||||
->willReturn([])
|
||||
;
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Employee> $employees
|
||||
*/
|
||||
private function buildWeeklyResolverStub(array $employees): EmployeeContractResolver
|
||||
{
|
||||
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||
$resolver
|
||||
->method('resolveForEmployeeAndDate')
|
||||
->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract())
|
||||
;
|
||||
$resolver
|
||||
->method('resolveForEmployeesAndDays')
|
||||
->willReturnCallback(static function (array $scopedEmployees, array $days): array {
|
||||
$map = [];
|
||||
foreach ($scopedEmployees as $employee) {
|
||||
$employeeId = $employee->getId();
|
||||
if (!$employeeId) {
|
||||
continue;
|
||||
}
|
||||
foreach ($days as $day) {
|
||||
$map[$employeeId][$day] = $employee->getContract();
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
})
|
||||
;
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user