Files
ephub/src/Booking/BoardPresenter.php
T

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;
}
}