111 lines
4.0 KiB
PHP
111 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Booking;
|
|
|
|
use App\Entity\ResourceType;
|
|
use App\Entity\User;
|
|
use App\Repository\BookingRepository;
|
|
use App\Repository\ResourceRepository;
|
|
|
|
/**
|
|
* Builds the context behind the two fragments that make up a booking board: the month 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 (or a time span) updates the grid and the plan in a single out-of-band response.
|
|
*
|
|
* Everything here is relative to a ResourceType **and** a TimeRange. One board shows one kind of
|
|
* resource, and with part-day bookings "is this free?" has no answer without a span -- so both
|
|
* are carried through the occupancy counts and the plan colours alike, and the number in a
|
|
* calendar cell always means the same thing as the colours on the plan beside it.
|
|
*/
|
|
final readonly class BoardPresenter
|
|
{
|
|
private const WEEKS = 6;
|
|
|
|
public function __construct(
|
|
private BookingRepository $bookings,
|
|
private ResourceRepository $resources,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function context(ResourceType $type, \DateTimeImmutable $selected, TimeRange $time, User $user): array
|
|
{
|
|
$selected = $selected->setTime(0, 0);
|
|
$today = new \DateTimeImmutable('today');
|
|
|
|
$monthStart = $selected->modify('first day of this month');
|
|
$gridStart = $monthStart->modify('monday this week');
|
|
$gridEnd = $gridStart->modify(\sprintf('+%d days', self::WEEKS * 7 - 1));
|
|
|
|
$counts = $this->bookings->countPerDateInRange($gridStart, $gridEnd, $time, $type);
|
|
|
|
$mine = [];
|
|
foreach ($this->bookings->findForUserInRange($user, $gridStart, $gridEnd, $type) as $booking) {
|
|
$mine[$booking->getDate()->format('Y-m-d')][] = $booking;
|
|
}
|
|
|
|
$weeks = [];
|
|
$cursor = $gridStart;
|
|
for ($week = 0; $week < self::WEEKS; ++$week) {
|
|
$days = [];
|
|
for ($weekday = 0; $weekday < 7; ++$weekday) {
|
|
$key = $cursor->format('Y-m-d');
|
|
$days[] = [
|
|
'date' => $cursor,
|
|
'in_month' => $cursor->format('Y-m') === $monthStart->format('Y-m'),
|
|
'is_today' => $cursor == $today,
|
|
'is_selected' => $cursor == $selected,
|
|
'is_past' => $cursor < $today,
|
|
'is_weekend' => (int) $cursor->format('N') >= 6,
|
|
'count' => $counts[$key] ?? 0,
|
|
'mine' => $mine[$key] ?? [],
|
|
];
|
|
$cursor = $cursor->modify('+1 day');
|
|
}
|
|
$weeks[] = $days;
|
|
}
|
|
|
|
$byResource = [];
|
|
foreach ($this->bookings->findForDate($selected, $time, $type) 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 [
|
|
'type' => $type,
|
|
'selected_date' => $selected,
|
|
'selected_time' => $time,
|
|
'month_start' => $monthStart,
|
|
'weeks' => $weeks,
|
|
'is_past' => $selected < $today,
|
|
'resources' => $this->resources->findActiveByType($type),
|
|
'bookings_by_resource' => $byResource,
|
|
'my_bookings_today' => $mine[$selected->format('Y-m-d')] ?? [],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* The route parameters that reproduce a board view, so links and HX-Push-Url agree.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public static function urlParameters(ResourceType $type, \DateTimeImmutable $date, TimeRange $time): array
|
|
{
|
|
$parameters = ['type' => $type->value, 'date' => $date->format('Y-m-d')];
|
|
|
|
if (!$time->isAllDay()) {
|
|
$parameters['from'] = $time->start();
|
|
$parameters['to'] = $time->end();
|
|
}
|
|
|
|
return $parameters;
|
|
}
|
|
}
|