Files
Lesstime/src/Controller/Absence/AbsencePreviewController.php
T

96 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controller\Absence;
use App\Enum\AbsenceType;
use App\Enum\HalfDay;
use App\Repository\AbsenceBalanceRepository;
use App\Repository\AbsencePolicyRepository;
use App\Service\AbsenceBalanceService;
use App\Service\AbsenceDayCalculator;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Dry-run endpoint for the "new request" form: returns the number of deducted
* days and the projected balance without creating anything. Required because
* public holidays are computed server-side.
*/
class AbsencePreviewController extends AbstractController
{
public function __construct(
private readonly Security $security,
private readonly AbsenceDayCalculator $calculator,
private readonly AbsencePolicyRepository $policyRepository,
private readonly AbsenceBalanceRepository $balanceRepository,
private readonly AbsenceBalanceService $balanceService,
) {}
#[Route('/api/absence_requests/preview', name: 'absence_request_preview', methods: ['POST'], priority: 1)]
#[IsGranted('ROLE_USER')]
public function __invoke(Request $request): JsonResponse
{
/** @var array<string, mixed> $payload */
$payload = json_decode($request->getContent(), true) ?? [];
$type = AbsenceType::tryFrom((string) ($payload['type'] ?? ''));
if (null === $type) {
throw new UnprocessableEntityHttpException('Unknown absence type.');
}
$startRaw = (string) ($payload['startDate'] ?? '');
$endRaw = (string) ($payload['endDate'] ?? '');
if ('' === $startRaw || '' === $endRaw) {
throw new UnprocessableEntityHttpException('Start date and end date are required.');
}
$start = new DateTimeImmutable($startRaw);
$end = new DateTimeImmutable($endRaw);
if ($end < $start) {
throw new UnprocessableEntityHttpException('End date must be on or after start date.');
}
$policy = $this->policyRepository->findOneByType($type);
$workingDaysOnly = $policy?->isCountWorkingDaysOnly() ?? true;
$countedDays = $this->calculator->countWorkingDays(
$start,
$end,
isset($payload['startHalfDay']) ? HalfDay::tryFrom((string) $payload['startHalfDay']) : null,
isset($payload['endHalfDay']) ? HalfDay::tryFrom((string) $payload['endHalfDay']) : null,
$workingDaysOnly,
);
$user = $this->security->getUser();
assert($user instanceof UserInterface);
$available = null;
$projectedAvailable = null;
$period = null;
if ($type->decrementsBalance()) {
$period = $this->balanceService->periodFor($user, $type, $start);
$balance = $this->balanceRepository->findOneForPeriod($user, $type, $period);
$available = $balance?->getAvailable() ?? 0.0;
$projectedAvailable = $available - $countedDays;
}
return $this->json([
'countedDays' => $countedDays,
'period' => $period,
'available' => $available,
'projectedAvailable' => $projectedAvailable,
'justificationRequired' => $policy?->isJustificationRequired() ?? false,
]);
}
}