feat: add booking time ranges and repeating series

This commit is contained in:
fromme committed 2026-09-13 17:34:28 +02:00
1 parent 59feca2f8e
commit d433d271d0
29 files changed
+2135 -180

No files matched your search

+25 -11
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Booking;
use App\Entity\Booking;
use App\Entity\ResourceType;
use App\Entity\User;
use App\Repository\BookingRepository;
@@ -15,7 +14,12 @@ use App\Repository\ResourceRepository;
* calendar and the floor plan.
*
* Both are always rendered from the same context so that one htmx round trip can swap both --
* picking a day updates the grid and the plan in a single out-of-band response.
* picking a day (or a time span) updates the grid and the plan in a single out-of-band response.
*
* Everything here is relative to a TimeRange. With part-day bookings "is this desk free?" has no
* answer on its own, only "is it free between 09:00 and 13:00?", so the span the user picked is
* carried through the occupancy counts and the plan colours alike -- the number in a calendar
* cell always means the same thing as the colours on the plan.
*/
final readonly class BoardPresenter
{
@@ -30,7 +34,7 @@ final readonly class BoardPresenter
/**
* @return array<string, mixed>
*/
public function context(\DateTimeImmutable $selected, User $user): array
public function context(\DateTimeImmutable $selected, TimeRange $time, User $user): array
{
$selected = $selected->setTime(0, 0);
$today = new \DateTimeImmutable('today');
@@ -39,7 +43,7 @@ final readonly class BoardPresenter
$gridStart = $monthStart->modify('monday this week');
$gridEnd = $gridStart->modify(\sprintf('+%d days', self::WEEKS * 7 - 1));
$counts = $this->bookings->countPerDateInRange($gridStart, $gridEnd);
$counts = $this->bookings->countPerDateInRange($gridStart, $gridEnd, $time);
$mine = [];
foreach ($this->bookings->findForUserInRange($user, $gridStart, $gridEnd) as $booking) {
@@ -68,29 +72,39 @@ final readonly class BoardPresenter
}
$byResource = [];
foreach ($this->bookings->findForDate($selected) as $booking) {
$byResource[$booking->getResource()->getId()] = $booking;
foreach ($this->bookings->findForDate($selected, $time) as $booking) {
// Several bookings can overlap the span on one resource once times are in play; the
// first is enough to colour the shape, and the tooltip lists them all.
$byResource[$booking->getResource()->getId()][] = $booking;
}
return [
'selected_date' => $selected,
'selected_time' => $time,
'month_start' => $monthStart,
'weeks' => $weeks,
'is_past' => $selected < $today,
'desks' => $this->resources->findActiveByType(ResourceType::DESK),
'parking' => $this->resources->findActiveByType(ResourceType::PARKING),
'bookings_by_resource' => $byResource,
'my_bookings_today' => $this->mineOn($selected, $mine),
'my_bookings_today' => $mine[$selected->format('Y-m-d')] ?? [],
];
}
/**
* @param array<string, list<Booking>> $mine
* The query parameters that reproduce a board view, so links and HX-Push-Url agree.
*
* @return list<Booking>
* @return array<string, string>
*/
private function mineOn(\DateTimeImmutable $date, array $mine): array
public static function urlParameters(\DateTimeImmutable $date, TimeRange $time): array
{
return $mine[$date->format('Y-m-d')] ?? [];
$parameters = ['tag' => $date->format('Y-m-d')];
if (!$time->isAllDay()) {
$parameters['von'] = $time->start();
$parameters['bis'] = $time->end();
}
return $parameters;
}
}