feat: replace the navigation with a dashboard and per-area boards

This commit is contained in:
fromme committed 2026-09-13 18:08:39 +02:00
1 parent d63df2cc1b
commit bce8f58be3
26 files changed
+810 -328

No files matched your search

+7 -7
View File
@@ -44,11 +44,11 @@ class BookingControllerTest extends WebTestCase
{
$this->client->loginUser($this->user('[email protected]'));
$this->client->request('GET', '/tag/2026-10-01', server: ['HTTP_HX-Request' => 'true']);
$this->client->request('GET', '/buchen/schreibtische/tag/2026-10-01', server: ['HTTP_HX-Request' => 'true']);
$response = $this->client->getResponse();
self::assertResponseIsSuccessful();
self::assertSame('/?tag=2026-10-01', $response->headers->get('HX-Push-Url'));
self::assertSame('/buchen/schreibtische?tag=2026-10-01', $response->headers->get('HX-Push-Url'));
self::assertSame(2, substr_count((string) $response->getContent(), 'hx-swap-oob="true"'));
self::assertStringContainsString('id="board-calendar"', (string) $response->getContent());
self::assertStringContainsString('id="board-floorplan"', (string) $response->getContent());
@@ -58,9 +58,9 @@ class BookingControllerTest extends WebTestCase
{
$this->client->loginUser($this->user('[email protected]'));
$this->client->request('GET', '/tag/2026-10-01');
$this->client->request('GET', '/buchen/schreibtische/tag/2026-10-01');
self::assertResponseRedirects('/?tag=2026-10-01');
self::assertResponseRedirects('/buchen/schreibtische?tag=2026-10-01');
}
/**
@@ -205,7 +205,7 @@ class BookingControllerTest extends WebTestCase
$this->em->flush();
$this->client->loginUser($anna);
$crawler = $this->client->request('GET', '/?tag=2026-10-01');
$crawler = $this->client->request('GET', '/buchen/schreibtische?tag=2026-10-01');
self::assertCount(1, $crawler->filter(\sprintf('[hx-get="/buchungen/%d/bearbeiten"]', $booking->getId())));
}
@@ -351,10 +351,10 @@ class BookingControllerTest extends WebTestCase
{
$this->client->loginUser($this->user('[email protected]'));
$this->client->request('GET', '/tag/2026-10-01?von=09:00&bis=13:00', server: ['HTTP_HX-Request' => 'true']);
$this->client->request('GET', '/buchen/schreibtische/tag/2026-10-01?von=09:00&bis=13:00', server: ['HTTP_HX-Request' => 'true']);
self::assertResponseIsSuccessful();
self::assertSame('/?tag=2026-10-01&von=09:00&bis=13:00', $this->client->getResponse()->headers->get('HX-Push-Url'));
self::assertSame('/buchen/schreibtische?tag=2026-10-01&von=09:00&bis=13:00', $this->client->getResponse()->headers->get('HX-Push-Url'));
}
public function testOverridingOneOccurrenceLeavesTheSeriesAlone(): void
@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace App\Tests\Controller;
use App\Booking\TimeRange;
use App\Entity\Booking;
use App\Entity\Resource;
use App\Entity\ResourceType;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class DashboardControllerTest extends WebTestCase
{
private KernelBrowser $client;
private EntityManagerInterface $em;
protected function setUp(): void
{
$this->client = self::createClient();
$this->client->disableReboot();
$this->em = self::getContainer()->get(EntityManagerInterface::class);
$this->em->getConnection()->beginTransaction();
}
protected function tearDown(): void
{
$this->em->getConnection()->rollBack();
parent::tearDown();
}
/**
* The extensibility guarantee, asserted against the enum rather than against a fixed list:
* a new ResourceType case must produce a working tile without anyone editing a template.
*/
public function testThereIsATileForEveryBookableKind(): void
{
$this->client->loginUser($this->user());
$crawler = $this->client->request('GET', '/');
self::assertResponseIsSuccessful();
foreach (ResourceType::cases() as $type) {
self::assertCount(
1,
$crawler->filter(\sprintf('a[href="/buchen/%s"]', $type->slug())),
\sprintf('no dashboard tile links to the %s board', $type->slug()),
);
self::assertStringContainsString($type->plural(), $crawler->html());
}
}
/**
* The bookings list is only reachable from here now that the sidebar is gone.
*/
public function testTheDashboardLinksToTheBookingsList(): void
{
$this->client->loginUser($this->user());
$crawler = $this->client->request('GET', '/');
self::assertCount(1, $crawler->filter('a[href="/buchungen"]'));
}
public function testTheNavigationIsGone(): void
{
$this->client->loginUser($this->user());
$crawler = $this->client->request('GET', '/');
self::assertCount(0, $crawler->filter('#mobilenav'));
self::assertCount(1, $crawler->filter('a[href="/logout"]'));
}
public function testATileCountsWhatIsStillFreeToday(): void
{
$user = $this->user();
$today = new \DateTimeImmutable('today');
$this->resource('D1');
$taken = $this->resource('D2');
$this->em->persist(new Booking($this->user('[email protected]'), $taken, $today));
$this->em->flush();
$this->client->loginUser($user);
$crawler = $this->client->request('GET', '/');
$tile = $crawler->filter('a[href="/buchen/schreibtische"]')->text();
self::assertStringContainsString('von 2 heute frei', $tile);
self::assertStringContainsString('1', $tile);
}
/**
* A part-day booking still makes the resource "not free today" -- the dashboard has no span
* to qualify the answer with, so it takes the whole day.
*/
public function testAPartDayBookingCountsAsTakenForTheDay(): void
{
$desk = $this->resource('D1');
$this->em->persist(new Booking($this->user('[email protected]'), $desk, new \DateTimeImmutable('today'), TimeRange::fromMinutes(540, 780)));
$this->em->flush();
$this->client->loginUser($this->user());
$crawler = $this->client->request('GET', '/');
self::assertStringContainsString('heute alles belegt', $crawler->filter('a[href="/buchen/schreibtische"]')->text());
}
public function testYourOwnBookingIsCalledOutOnTheTile(): void
{
$user = $this->user();
$this->em->persist(new Booking($user, $this->resource('D1'), new \DateTimeImmutable('today')));
$this->em->flush();
$this->client->loginUser($user);
$crawler = $this->client->request('GET', '/');
self::assertStringContainsString('D1', $crawler->filter('a[href="/buchen/schreibtische"]')->text());
}
public function testABoardOnlyShowsItsOwnKindOfResource(): void
{
$this->resource('D1', svgElementId: 'desk-01');
$this->resource('P1', ResourceType::PARKING, 'parking-01');
$this->client->loginUser($this->user());
$desks = $this->client->request('GET', '/buchen/schreibtische')->html();
self::assertStringContainsString('D1', $desks);
self::assertStringNotContainsString('P1', $desks);
$parking = $this->client->request('GET', '/buchen/parkplaetze')->html();
self::assertStringContainsString('P1', $parking);
self::assertStringNotContainsString('D1', $parking);
}
public function testAnUnknownBookingAreaIsNotFound(): void
{
$this->client->loginUser($this->user());
$this->client->request('GET', '/buchen/fahrraeder');
self::assertResponseStatusCodeSame(404);
}
public function testABoardWithNothingSetUpSaysSo(): void
{
$this->client->loginUser($this->user());
self::assertStringContainsString(
'noch nichts eingerichtet',
$this->client->request('GET', '/buchen/parkplaetze')->html(),
);
}
private function user(string $email = '[email protected]'): User
{
$user = new User($email, 'Test');
$user->setRoles(['ROLE_EMPLOYEE']);
$this->em->persist($user);
$this->em->flush();
return $user;
}
private function resource(string $label, ResourceType $type = ResourceType::DESK, ?string $svgElementId = null): Resource
{
$resource = new Resource($type, $label, $svgElementId);
$this->em->persist($resource);
$this->em->flush();
return $resource;
}
}