feat: add admin views, a booking horizon and confirmation modals

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5 committed 2026-09-14 09:40:17 +02:00
1 parent 8caf618950
commit 1a3640b15c
27 files changed
+1269 -62

No files matched your search

+30 -4
View File
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Form;
use App\Booking\Horizon;
use App\Booking\RecurrenceRule;
use App\Booking\TimeRange;
use App\Entity\BookingSeries;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -20,6 +20,13 @@ final class BookingRequest
#[Assert\GreaterThanOrEqual('today', message: 'Für vergangene Tage kann nicht gebucht werden.')]
public ?\DateTimeImmutable $date = null;
/**
* Lifts the Horizon cap for this request. Set by the controller for ROLE_ADMIN -- a DTO
* cannot reach the security container, and giving it one would make these rules untestable
* in isolation.
*/
public bool $unlimitedHorizon = false;
public bool $allDay = true;
public ?\DateTimeImmutable $startTime = null;
@@ -85,6 +92,8 @@ final class BookingRequest
}
}
$this->validateHorizon($context);
if (!$this->repeats()) {
return;
}
@@ -101,14 +110,31 @@ final class BookingRequest
return;
}
$latest = ($this->date ?? new \DateTimeImmutable('today'))->modify(\sprintf('+%d months', BookingSeries::MAX_MONTHS));
if ($this->until > $latest) {
$context->buildViolation(\sprintf('Eine Serie darf höchstens %d Monate laufen.', BookingSeries::MAX_MONTHS))
if (!$this->unlimitedHorizon && $this->until > Horizon::latest()) {
$context->buildViolation(\sprintf('Eine Serie darf höchstens %d Monate in die Zukunft laufen.', Horizon::MONTHS))
->atPath('until')
->addViolation();
}
}
/**
* The booking horizon, measured from today rather than from the chosen day -- see Horizon.
* Not an attribute constraint because it has to be liftable per role, which an attribute
* cannot express.
*/
private function validateHorizon(ExecutionContextInterface $context): void
{
if ($this->unlimitedHorizon || null === $this->date) {
return;
}
if ($this->date > Horizon::latest()) {
$context->buildViolation(\sprintf('Es kann höchstens %d Monate im Voraus gebucht werden.', Horizon::MONTHS))
->atPath('date')
->addViolation();
}
}
private static function timeOf(int $minutes): \DateTimeImmutable
{
// 24:00 has no representation as a time-of-day, so an all-day end never reaches here.