feat: add booking time ranges and repeating series
This commit is contained in:
1 parent
59feca2f8e
commit
d433d271d0
29 files changed
+2135
-180
No files matched your search
+105
-7
@@ -4,18 +4,116 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* What the booking modal edits. The resource is fixed by the URL -- only the day is adjustable,
|
||||
* because the modal is opened by clicking a resource on the floor plan.
|
||||
* What the booking modal edits. The resource is fixed by the URL -- it is chosen by clicking a
|
||||
* shape on the floor plan, not in the form.
|
||||
*/
|
||||
final class BookingRequest
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\NotNull(message: 'Bitte einen Tag wählen.')]
|
||||
#[Assert\GreaterThanOrEqual('today', message: 'Für vergangene Tage kann nicht gebucht werden.')]
|
||||
public ?\DateTimeImmutable $date = null,
|
||||
) {
|
||||
#[Assert\NotNull(message: 'Bitte einen Tag wählen.')]
|
||||
#[Assert\GreaterThanOrEqual('today', message: 'Für vergangene Tage kann nicht gebucht werden.')]
|
||||
public ?\DateTimeImmutable $date = null;
|
||||
|
||||
public bool $allDay = true;
|
||||
|
||||
public ?\DateTimeImmutable $startTime = null;
|
||||
|
||||
public ?\DateTimeImmutable $endTime = null;
|
||||
|
||||
public ?RecurrenceRule $repeat = null;
|
||||
|
||||
/** @var list<int> ISO weekdays, 1 = Monday */
|
||||
public array $weekdays = [];
|
||||
|
||||
public ?\DateTimeImmutable $until = null;
|
||||
|
||||
public function __construct(?\DateTimeImmutable $date = null, ?TimeRange $time = null)
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
if (null !== $time && !$time->isAllDay()) {
|
||||
$this->allDay = false;
|
||||
$this->startTime = self::timeOf($time->startMinute);
|
||||
$this->endTime = self::timeOf($time->endMinute);
|
||||
}
|
||||
}
|
||||
|
||||
public function timeRange(): TimeRange
|
||||
{
|
||||
if ($this->allDay || null === $this->startTime || null === $this->endTime) {
|
||||
return TimeRange::wholeDay();
|
||||
}
|
||||
|
||||
return TimeRange::fromTimes($this->startTime, $this->endTime);
|
||||
}
|
||||
|
||||
public function repeats(): bool
|
||||
{
|
||||
return null !== $this->repeat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-field rules. These live here rather than in the form type so they hold for any caller,
|
||||
* and so the messages land on the field the user has to fix.
|
||||
*/
|
||||
#[Assert\Callback]
|
||||
public function validate(ExecutionContextInterface $context): void
|
||||
{
|
||||
if (!$this->allDay) {
|
||||
foreach (['startTime' => $this->startTime, 'endTime' => $this->endTime] as $field => $value) {
|
||||
if (null === $value) {
|
||||
$context->buildViolation('Bitte eine Uhrzeit angeben.')->atPath($field)->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $this->startTime && null !== $this->endTime
|
||||
&& self::minutesOf($this->endTime) <= self::minutesOf($this->startTime)) {
|
||||
$context->buildViolation('Das Ende muss nach dem Beginn liegen.')->atPath('endTime')->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->repeats()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RecurrenceRule::WEEKLY === $this->repeat && [] === $this->weekdays) {
|
||||
$context->buildViolation('Bitte mindestens einen Wochentag wählen.')->atPath('weekdays')->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->until) {
|
||||
$context->buildViolation('Bitte ein Enddatum für die Serie angeben.')->atPath('until')->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->date && $this->until < $this->date) {
|
||||
$context->buildViolation('Das Serienende muss nach dem Beginn liegen.')->atPath('until')->addViolation();
|
||||
|
||||
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))
|
||||
->atPath('until')
|
||||
->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.
|
||||
return (new \DateTimeImmutable('today'))->setTime(intdiv($minutes, 60), $minutes % 60);
|
||||
}
|
||||
|
||||
private static function minutesOf(\DateTimeInterface $time): int
|
||||
{
|
||||
return (int) $time->format('G') * 60 + (int) $time->format('i');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user