feat: add two- and four-weekly series and drop the weekday picker
This commit is contained in:
1 parent
8b88384d68
commit
faef4d70a8
10 files changed
+166
-103
No files matched your search
@@ -4,16 +4,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Booking;
|
||||
|
||||
/**
|
||||
* How often a series repeats.
|
||||
*
|
||||
* Every rule is a whole number of days, which is what lets BookingSeries::occurrences() be a
|
||||
* plain stride from the start date. Every stride that is a multiple of 7 therefore keeps the
|
||||
* weekday fixed by construction -- that is why a series needs no weekday field at all.
|
||||
*
|
||||
* There is deliberately no calendar-monthly rule: "the 31st of each month" has no counterpart in
|
||||
* five months of the year, and it would not hold the weekday. FOUR_WEEKLY is the nearest thing
|
||||
* that behaves, at the cost of drifting against the calendar -- 13 occurrences a year, not 12,
|
||||
* which is why it is not labelled "Monatlich".
|
||||
*/
|
||||
enum RecurrenceRule: string
|
||||
{
|
||||
case DAILY = 'daily';
|
||||
case WEEKLY = 'weekly';
|
||||
case TWO_WEEKLY = 'two_weekly';
|
||||
case FOUR_WEEKLY = 'four_weekly';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DAILY => 'Täglich',
|
||||
self::WEEKLY => 'Wöchentlich',
|
||||
self::TWO_WEEKLY => 'Alle zwei Wochen',
|
||||
self::FOUR_WEEKLY => 'Alle vier Wochen',
|
||||
};
|
||||
}
|
||||
|
||||
/** Days between two occurrences. */
|
||||
public function intervalDays(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::DAILY => 1,
|
||||
self::WEEKLY => 7,
|
||||
self::TWO_WEEKLY => 14,
|
||||
self::FOUR_WEEKLY => 28,
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether the rule lands on the same weekday every time, i.e. whether naming it adds anything. */
|
||||
public function keepsWeekday(): bool
|
||||
{
|
||||
return 0 === $this->intervalDays() % 7;
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ final class BookingController extends AbstractController
|
||||
]);
|
||||
// The day and any recurrence belong to the original booking; only the span is editable
|
||||
// here, so a series is never silently rewritten by editing one of its occurrences.
|
||||
$form->remove('date')->remove('repeat')->remove('weekdays')->remove('until');
|
||||
$form->remove('date')->remove('repeat')->remove('until');
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
@@ -207,7 +207,6 @@ final class BookingController extends AbstractController
|
||||
$data->date,
|
||||
$data->until,
|
||||
$data->timeRange(),
|
||||
$data->weekdays,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -41,15 +41,6 @@ class BookingSeries
|
||||
#[ORM\Column(enumType: RecurrenceRule::class)]
|
||||
private RecurrenceRule $rule;
|
||||
|
||||
/**
|
||||
* ISO weekdays (1 = Monday) the series falls on. Empty for a DAILY rule, which means
|
||||
* every calendar day.
|
||||
*
|
||||
* @var list<int>
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $weekdays = [];
|
||||
|
||||
#[ORM\Column(type: 'date_immutable')]
|
||||
private \DateTimeImmutable $startDate;
|
||||
|
||||
@@ -69,9 +60,6 @@ class BookingSeries
|
||||
#[ORM\OneToMany(targetEntity: Booking::class, mappedBy: 'series')]
|
||||
private Collection $bookings;
|
||||
|
||||
/**
|
||||
* @param list<int> $weekdays
|
||||
*/
|
||||
public function __construct(
|
||||
User $user,
|
||||
Resource $resource,
|
||||
@@ -79,7 +67,6 @@ class BookingSeries
|
||||
\DateTimeImmutable $startDate,
|
||||
\DateTimeImmutable $endDate,
|
||||
TimeRange $time,
|
||||
array $weekdays = [],
|
||||
) {
|
||||
$this->user = $user;
|
||||
$this->resource = $resource;
|
||||
@@ -88,7 +75,6 @@ class BookingSeries
|
||||
$this->endDate = $endDate->setTime(0, 0);
|
||||
$this->startMinute = $time->startMinute;
|
||||
$this->endMinute = $time->endMinute;
|
||||
$this->weekdays = RecurrenceRule::WEEKLY === $rule ? array_values(array_unique($weekdays)) : [];
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->bookings = new ArrayCollection();
|
||||
}
|
||||
@@ -96,17 +82,18 @@ class BookingSeries
|
||||
/**
|
||||
* Every day this series falls on, in order.
|
||||
*
|
||||
* A rule is only a step size, so this is a stride from the start date. For any rule whose
|
||||
* interval is a multiple of a week that means every occurrence lands on the same weekday as
|
||||
* the start -- the series never has to be told which weekday it runs on.
|
||||
*
|
||||
* @return list<\DateTimeImmutable>
|
||||
*/
|
||||
public function occurrences(): array
|
||||
{
|
||||
$dates = [];
|
||||
$step = \sprintf('+%d days', $this->rule->intervalDays());
|
||||
|
||||
for ($day = $this->startDate; $day <= $this->endDate; $day = $day->modify('+1 day')) {
|
||||
if (RecurrenceRule::WEEKLY === $this->rule && !\in_array((int) $day->format('N'), $this->weekdays, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($day = $this->startDate; $day <= $this->endDate; $day = $day->modify($step)) {
|
||||
$dates[] = $day;
|
||||
}
|
||||
|
||||
@@ -133,14 +120,6 @@ class BookingSeries
|
||||
return $this->rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
public function getWeekdays(): array
|
||||
{
|
||||
return $this->weekdays;
|
||||
}
|
||||
|
||||
public function getStartDate(): \DateTimeImmutable
|
||||
{
|
||||
return $this->startDate;
|
||||
@@ -178,12 +157,11 @@ class BookingSeries
|
||||
{
|
||||
$description = $this->rule->label();
|
||||
|
||||
if (RecurrenceRule::WEEKLY === $this->rule && [] !== $this->weekdays) {
|
||||
// Naming the weekday only says something for a rule that actually holds it fixed; for a
|
||||
// daily series it would just repeat the start date back at the reader.
|
||||
if ($this->rule->keepsWeekday()) {
|
||||
$names = ['', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
// Sort the numbers, not the names -- alphabetical would read "Di, Do, Fr, Mi, Mo".
|
||||
$weekdays = $this->weekdays;
|
||||
sort($weekdays);
|
||||
$description .= ' ('.implode(', ', array_map(static fn (int $day): string => $names[$day], $weekdays)).')';
|
||||
$description .= ' ('.$names[(int) $this->startDate->format('N')].')';
|
||||
}
|
||||
|
||||
return $description.', '.$this->getTimeRange();
|
||||
|
||||
@@ -28,9 +28,6 @@ final class BookingRequest
|
||||
|
||||
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)
|
||||
@@ -92,10 +89,6 @@ final class BookingRequest
|
||||
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();
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace App\Form;
|
||||
use App\Booking\RecurrenceRule;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimeType;
|
||||
@@ -49,13 +48,6 @@ final class BookingType extends AbstractType
|
||||
'placeholder' => 'Einmalig',
|
||||
'choice_label' => static fn (RecurrenceRule $rule): string => $rule->label(),
|
||||
])
|
||||
->add('weekdays', ChoiceType::class, [
|
||||
'label' => 'Wochentage',
|
||||
'choices' => ['Mo' => 1, 'Di' => 2, 'Mi' => 3, 'Do' => 4, 'Fr' => 5, 'Sa' => 6, 'So' => 7],
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
])
|
||||
->add('until', DateType::class, [
|
||||
'label' => 'Serie bis',
|
||||
'widget' => 'single_text',
|
||||
|
||||
Reference in new issue
Block a user