193 lines
7.9 KiB
PHP
193 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\State;
|
|
|
|
use ApiPlatform\Metadata\Operation;
|
|
use ApiPlatform\State\ProviderInterface;
|
|
use App\ApiResource\EmployeeRttSummary;
|
|
use App\Dto\Rtt\EmployeeRttWeekSummary;
|
|
use App\Dto\Rtt\RttMonthPayment;
|
|
use App\Dto\Rtt\WeekRecoveryDetail;
|
|
use App\Entity\Employee;
|
|
use App\Entity\User;
|
|
use App\Repository\EmployeeRepository;
|
|
use App\Repository\EmployeeRttBalanceRepository;
|
|
use App\Repository\EmployeeRttPaymentRepository;
|
|
use App\Security\EmployeeScopeService;
|
|
use App\Service\Rtt\RttRecoveryComputationService;
|
|
use DateTimeImmutable;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
|
|
final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
|
{
|
|
public function __construct(
|
|
private Security $security,
|
|
private RequestStack $requestStack,
|
|
private EmployeeRepository $employeeRepository,
|
|
private EmployeeScopeService $employeeScopeService,
|
|
private EmployeeRttBalanceRepository $rttBalanceRepository,
|
|
private EmployeeRttPaymentRepository $rttPaymentRepository,
|
|
private RttRecoveryComputationService $rttRecoveryService,
|
|
) {}
|
|
|
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EmployeeRttSummary
|
|
{
|
|
$user = $this->security->getUser();
|
|
if (!$user instanceof User) {
|
|
throw new AccessDeniedHttpException('Authentication required.');
|
|
}
|
|
|
|
$employeeId = (int) ($uriVariables['id'] ?? 0);
|
|
if ($employeeId <= 0) {
|
|
throw new UnprocessableEntityHttpException('id must be a positive integer.');
|
|
}
|
|
|
|
$employee = $this->employeeRepository->find($employeeId);
|
|
if (!$employee instanceof Employee) {
|
|
throw new NotFoundHttpException('Employee not found.');
|
|
}
|
|
|
|
if (!$this->employeeScopeService->canAccessEmployee($user, $employee)) {
|
|
throw new AccessDeniedHttpException('Employee outside your scope.');
|
|
}
|
|
|
|
$year = $this->resolveYear();
|
|
$today = new DateTimeImmutable('today');
|
|
$currentExerciseYear = $this->resolveCurrentExerciseYear($today);
|
|
[$periodFrom, $periodTo] = $this->rttRecoveryService->resolveExerciseBounds($year);
|
|
$weeks = $this->rttRecoveryService->buildWeeksForExercise($periodFrom, $periodTo);
|
|
$weekRanges = array_map(
|
|
static fn (array $week): array => [
|
|
'month' => (int) $week['month'],
|
|
'weekNumber' => (int) $week['weekNumber'],
|
|
'start' => $week['start'],
|
|
'end' => $week['end'],
|
|
],
|
|
$weeks
|
|
);
|
|
|
|
$limitDate = null;
|
|
if ($year > $currentExerciseYear) {
|
|
$limitDate = $periodFrom->modify('-1 day');
|
|
}
|
|
|
|
$currentByWeekStart = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate);
|
|
[$carry, $carryMonth] = $this->resolveCarry($employee, $year);
|
|
|
|
$summary = new EmployeeRttSummary();
|
|
$summary->year = $year;
|
|
$summary->carryMonth = $carryMonth;
|
|
$summary->carryFromPreviousYearMinutes = $carry->totalMinutes;
|
|
$summary->carryBase25Minutes = $carry->base25Minutes;
|
|
$summary->carryBonus25Minutes = $carry->bonus25Minutes;
|
|
$summary->carryBase50Minutes = $carry->base50Minutes;
|
|
$summary->carryBonus50Minutes = $carry->bonus50Minutes;
|
|
$summary->currentYearRecoveryMinutes = array_sum(array_map(static fn ($d) => $d->totalMinutes, $currentByWeekStart));
|
|
$summary->availableMinutes = $summary->carryFromPreviousYearMinutes + $summary->currentYearRecoveryMinutes;
|
|
$summary->weeks = array_map(
|
|
static function (array $week) use ($currentByWeekStart) {
|
|
$detail = $currentByWeekStart[$week['start']->format('Y-m-d')] ?? new WeekRecoveryDetail();
|
|
|
|
return new EmployeeRttWeekSummary(
|
|
month: (int) $week['month'],
|
|
weekNumber: (int) $week['weekNumber'],
|
|
weekStart: $week['start']->format('Y-m-d'),
|
|
weekEnd: $week['end']->format('Y-m-d'),
|
|
overtimeMinutes: $detail->overtimeMinutes,
|
|
base25Minutes: $detail->base25Minutes,
|
|
bonus25Minutes: $detail->bonus25Minutes,
|
|
base50Minutes: $detail->base50Minutes,
|
|
bonus50Minutes: $detail->bonus50Minutes,
|
|
totalMinutes: $detail->totalMinutes,
|
|
);
|
|
},
|
|
$weekRanges
|
|
);
|
|
|
|
$payments = $this->rttPaymentRepository->findByEmployeeAndYear($employee, $year);
|
|
$monthBuckets = [];
|
|
|
|
foreach ($payments as $payment) {
|
|
$m = $payment->getMonth();
|
|
if (!isset($monthBuckets[$m])) {
|
|
$monthBuckets[$m] = ['base25' => 0, 'bonus25' => 0, 'base50' => 0, 'bonus50' => 0];
|
|
}
|
|
$monthBuckets[$m]['base25'] += $payment->getBase25Minutes();
|
|
$monthBuckets[$m]['bonus25'] += $payment->getBonus25Minutes();
|
|
$monthBuckets[$m]['base50'] += $payment->getBase50Minutes();
|
|
$monthBuckets[$m]['bonus50'] += $payment->getBonus50Minutes();
|
|
}
|
|
|
|
$monthPayments = [];
|
|
$totalPaidMinutes = 0;
|
|
|
|
foreach ($monthBuckets as $m => $bucket) {
|
|
$monthPayments[] = new RttMonthPayment($m, $bucket['base25'], $bucket['bonus25'], $bucket['base50'], $bucket['bonus50']);
|
|
$totalPaidMinutes += $bucket['base25'] + $bucket['bonus25'] + $bucket['base50'] + $bucket['bonus50'];
|
|
}
|
|
|
|
$summary->totalPaidMinutes = $totalPaidMinutes;
|
|
$summary->monthPayments = $monthPayments;
|
|
$summary->availableMinutes -= $totalPaidMinutes;
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* @return array{WeekRecoveryDetail, int} [carry, month]
|
|
*/
|
|
private function resolveCarry(Employee $employee, int $year): array
|
|
{
|
|
$balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $year);
|
|
if (null !== $balance) {
|
|
return [
|
|
new WeekRecoveryDetail(
|
|
base25Minutes: $balance->getOpeningBase25Minutes(),
|
|
bonus25Minutes: $balance->getOpeningBonus25Minutes(),
|
|
base50Minutes: $balance->getOpeningBase50Minutes(),
|
|
bonus50Minutes: $balance->getOpeningBonus50Minutes(),
|
|
totalMinutes: $balance->getTotalOpeningMinutes(),
|
|
),
|
|
$balance->getMonth(),
|
|
];
|
|
}
|
|
|
|
return [
|
|
$this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $year - 1),
|
|
5,
|
|
];
|
|
}
|
|
|
|
private function resolveYear(): int
|
|
{
|
|
$raw = (string) ($this->requestStack->getCurrentRequest()?->query->get('year') ?? '');
|
|
if ('' === $raw) {
|
|
return $this->resolveCurrentExerciseYear(new DateTimeImmutable('today'));
|
|
}
|
|
if (!preg_match('/^\d{4}$/', $raw)) {
|
|
throw new UnprocessableEntityHttpException('year must use YYYY format.');
|
|
}
|
|
|
|
$year = (int) $raw;
|
|
if ($year < 2000 || $year > 2100) {
|
|
throw new UnprocessableEntityHttpException('year must be between 2000 and 2100.');
|
|
}
|
|
|
|
return $year;
|
|
}
|
|
|
|
private function resolveCurrentExerciseYear(DateTimeImmutable $today): int
|
|
{
|
|
$year = (int) $today->format('Y');
|
|
$month = (int) $today->format('n');
|
|
|
|
return $month >= 6 ? $year + 1 : $year;
|
|
}
|
|
}
|