feat: replace the navigation with a dashboard and per-area boards
This commit is contained in:
1 parent
d63df2cc1b
commit
bce8f58be3
26 files changed
+810
-328
No files matched your search
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Booking\BoardPresenter;
|
||||
use App\Booking\DateParam;
|
||||
use App\Booking\TimeRange;
|
||||
use App\Entity\ResourceType;
|
||||
use App\Htmx\HxTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* One booking board per resource kind: a month calendar next to that kind's floor plan.
|
||||
*
|
||||
* The kind is a route parameter rather than a separate controller per kind, so a new
|
||||
* ResourceType case gets a working board without touching this file.
|
||||
*/
|
||||
final class BoardController extends AbstractController
|
||||
{
|
||||
use CurrentUserTrait;
|
||||
use HxTrait;
|
||||
|
||||
public function __construct(private readonly BoardPresenter $board)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/buchen/{type}', name: 'app_board', requirements: ['type' => '[a-z]+'], methods: ['GET'])]
|
||||
public function index(Request $request, string $type): Response
|
||||
{
|
||||
$resourceType = $this->resourceType($type);
|
||||
$date = DateParam::parseOrToday($request->query->getString('tag'));
|
||||
|
||||
return $this->render('board/index.html.twig', $this->board->context(
|
||||
$resourceType,
|
||||
$date,
|
||||
self::timeOf($request),
|
||||
$this->currentUser(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Picking a day -- or narrowing the span -- swaps the calendar and the floor plan in one
|
||||
* response.
|
||||
*
|
||||
* This is the whole reason htmxOobResponse exists: two regions, one round trip, no
|
||||
* client-side event choreography. Requests without the htmx header get a plain redirect
|
||||
* rather than a bare pair of fragments, so the URL stays shareable.
|
||||
*/
|
||||
#[Route('/buchen/{type}/tag/{date}', name: 'app_board_day', requirements: ['type' => '[a-z]+', 'date' => DateParam::PATTERN], methods: ['GET'])]
|
||||
public function day(Request $request, string $type, string $date): Response
|
||||
{
|
||||
$resourceType = $this->resourceType($type);
|
||||
$selected = DateParam::parseOrToday($date);
|
||||
$time = self::timeOf($request);
|
||||
|
||||
if (!$request->headers->has('HX-Request')) {
|
||||
return $this->redirectToRoute('app_board', BoardPresenter::urlParameters($resourceType, $selected, $time));
|
||||
}
|
||||
|
||||
return $this->htmxOobResponse(
|
||||
'board/index.html.twig',
|
||||
['calendar', 'floorplan'],
|
||||
$this->board->context($resourceType, $selected, $time, $this->currentUser()),
|
||||
$this->generateUrl('app_board', BoardPresenter::urlParameters($resourceType, $selected, $time)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A malformed or missing span falls back to the whole day rather than erroring: these are
|
||||
* hand-editable query parameters, not route requirements.
|
||||
*/
|
||||
public static function timeOf(Request $request): TimeRange
|
||||
{
|
||||
return TimeRange::parse($request->query->getString('von'), $request->query->getString('bis'))
|
||||
?? TimeRange::wholeDay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Route requirements cannot call a method, so the slug is matched loosely and resolved here.
|
||||
*/
|
||||
private function resourceType(string $slug): ResourceType
|
||||
{
|
||||
try {
|
||||
return ResourceType::fromSlug($slug);
|
||||
} catch (\ValueError $e) {
|
||||
throw $this->createNotFoundException($e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use App\Booking\TimeRange;
|
||||
use App\Entity\Booking;
|
||||
use App\Entity\BookingSeries;
|
||||
use App\Entity\Resource;
|
||||
use App\Entity\ResourceType;
|
||||
use App\Form\BookingRequest;
|
||||
use App\Form\BookingType;
|
||||
use App\Htmx\HxTrait;
|
||||
@@ -67,7 +68,7 @@ final class BookingController extends AbstractController
|
||||
}
|
||||
|
||||
$selected = DateParam::parseOrToday($date);
|
||||
$boardTime = DashboardController::timeOf($request);
|
||||
$boardTime = BoardController::timeOf($request);
|
||||
|
||||
// Prefilled from whatever span the board is showing, so narrowing the board and then
|
||||
// clicking a desk books exactly what was on screen.
|
||||
@@ -96,7 +97,7 @@ final class BookingController extends AbstractController
|
||||
return $this->invalidModal($form, $resource, $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->boardResponse($data->date, $boardTime);
|
||||
return $this->boardResponse($resource->getType(), $data->date, $boardTime);
|
||||
}
|
||||
|
||||
return $this->render('booking/_modal_create.html.twig', [
|
||||
@@ -114,7 +115,7 @@ final class BookingController extends AbstractController
|
||||
{
|
||||
$this->denyUnlessOwn($booking);
|
||||
|
||||
$boardTime = DashboardController::timeOf($request);
|
||||
$boardTime = BoardController::timeOf($request);
|
||||
$data = new BookingRequest($booking->getDate(), $booking->getTimeRange());
|
||||
|
||||
$form = $this->createForm(BookingType::class, $data, [
|
||||
@@ -134,7 +135,7 @@ final class BookingController extends AbstractController
|
||||
|
||||
$this->addFlash('success', \sprintf('%s geändert (%s).', $booking->getResource()->getLabel(), $booking->getTimeRange()));
|
||||
|
||||
return $this->boardResponse($booking->getDate(), $boardTime);
|
||||
return $this->boardResponse($booking->getResource()->getType(), $booking->getDate(), $boardTime);
|
||||
}
|
||||
|
||||
return $this->render('booking/_modal_edit.html.twig', [
|
||||
@@ -151,6 +152,7 @@ final class BookingController extends AbstractController
|
||||
$this->denyUnlessCsrfValid($request, 'release-'.$booking->getId());
|
||||
|
||||
$date = $booking->getDate();
|
||||
$type = $booking->getResource()->getType();
|
||||
$label = $booking->getResource()->getLabel();
|
||||
$this->bookings->release($booking);
|
||||
|
||||
@@ -161,7 +163,7 @@ final class BookingController extends AbstractController
|
||||
return $this->redirectToRoute('app_booking');
|
||||
}
|
||||
|
||||
return $this->boardResponse($date, DashboardController::timeOf($request));
|
||||
return $this->boardResponse($type, $date, BoardController::timeOf($request));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,13 +237,13 @@ final class BookingController extends AbstractController
|
||||
* The whole response is out-of-band board blocks. Callers pair it with hx-swap="none"
|
||||
* (or an outerHTML target that should vanish, as the modal does).
|
||||
*/
|
||||
private function boardResponse(\DateTimeImmutable $date, TimeRange $time): Response
|
||||
private function boardResponse(ResourceType $type, \DateTimeImmutable $date, TimeRange $time): Response
|
||||
{
|
||||
return $this->htmxOobResponse(
|
||||
'dashboard/index.html.twig',
|
||||
'board/index.html.twig',
|
||||
['calendar', 'floorplan', 'flashes'],
|
||||
$this->board->context($date, $time, $this->currentUser()),
|
||||
$this->generateUrl('app_dashboard', BoardPresenter::urlParameters($date, $time)),
|
||||
$this->board->context($type, $date, $time, $this->currentUser()),
|
||||
$this->generateUrl('app_board', BoardPresenter::urlParameters($type, $date, $time)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,68 +4,36 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Booking\BoardPresenter;
|
||||
use App\Booking\DateParam;
|
||||
use App\Booking\TimeRange;
|
||||
use App\Htmx\HxTrait;
|
||||
use App\Booking\DashboardPresenter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* The booking board: a month calendar next to the floor plan for the selected day and span.
|
||||
* The landing screen: one tile per bookable kind, plus what this person has coming up.
|
||||
*
|
||||
* It replaces the sidebar navigation, so it is also the only route to the booking boards and the
|
||||
* bookings list -- which is why the tiles are generated from ResourceType rather than written
|
||||
* out: a kind with no tile would be unreachable.
|
||||
*/
|
||||
final class DashboardController extends AbstractController
|
||||
{
|
||||
use CurrentUserTrait;
|
||||
use HxTrait;
|
||||
|
||||
public function __construct(private readonly BoardPresenter $board)
|
||||
public function __construct(private readonly DashboardPresenter $dashboard)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/', name: 'app_dashboard', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
public function index(): Response
|
||||
{
|
||||
$date = DateParam::parseOrToday($request->query->getString('tag'));
|
||||
$today = new \DateTimeImmutable('today');
|
||||
$user = $this->currentUser();
|
||||
|
||||
return $this->render('dashboard/index.html.twig', $this->board->context($date, self::timeOf($request), $this->currentUser()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Picking a day -- or narrowing the span -- swaps the calendar and the floor plan in one
|
||||
* response.
|
||||
*
|
||||
* This is the whole reason htmxOobResponse exists: two regions, one round trip, no
|
||||
* client-side event choreography. Requests without the htmx header get a plain redirect
|
||||
* rather than a bare pair of fragments, so the URL stays shareable.
|
||||
*/
|
||||
#[Route('/tag/{date}', name: 'app_dashboard_day', requirements: ['date' => DateParam::PATTERN], methods: ['GET'])]
|
||||
public function day(Request $request, string $date): Response
|
||||
{
|
||||
$selected = DateParam::parseOrToday($date);
|
||||
$time = self::timeOf($request);
|
||||
|
||||
if (!$request->headers->has('HX-Request')) {
|
||||
return $this->redirectToRoute('app_dashboard', BoardPresenter::urlParameters($selected, $time));
|
||||
}
|
||||
|
||||
return $this->htmxOobResponse(
|
||||
'dashboard/index.html.twig',
|
||||
['calendar', 'floorplan'],
|
||||
$this->board->context($selected, $time, $this->currentUser()),
|
||||
$this->generateUrl('app_dashboard', BoardPresenter::urlParameters($selected, $time)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A malformed or missing span falls back to the whole day rather than erroring: these are
|
||||
* hand-editable query parameters, not route requirements.
|
||||
*/
|
||||
public static function timeOf(Request $request): TimeRange
|
||||
{
|
||||
return TimeRange::parse($request->query->getString('von'), $request->query->getString('bis'))
|
||||
?? TimeRange::wholeDay();
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'tiles' => $this->dashboard->tiles($user, $today),
|
||||
'upcoming' => $this->dashboard->upcoming($user, $today),
|
||||
'today' => $today,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user