feat: add a booking event history for admins

Admins get /admin/history: one line per booking created, rescheduled or
released, per series created or ended, and per resource deleted with its
bookings. Sortable by time, kind, resource, booked day and both people;
filterable by period, kinds, who acted and resource type.

Events are snapshots written inside the same transaction as the change,
so a rolled-back booking leaves none and a released booking stays
readable. app:booking-events:prune keeps twelve months and needs a cron
entry on the server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
AdminandClaude Opus 5.5 authored and Björn Fromme committed 2026-10-16 17:56:41 +02:00
1 parent d68b6b156c
commit a9aa3a38be
18 files changed
+1578 -6

No files matched your search

@@ -5,11 +5,13 @@ declare(strict_types=1);
namespace App\Tests\Controller\Admin;
use App\Entity\Booking;
use App\Entity\BookingEvent;
use App\Entity\BookingSeries;
use App\Entity\Resource;
use App\Entity\ResourceGroup;
use App\Entity\ResourceTypeSettings;
use App\Entity\User;
use App\Enum\BookingEventType;
use App\Enum\RecurrenceRule;
use App\Enum\ResourceType;
use App\Model\TimeRange;
@@ -268,6 +270,29 @@ class ResourceAdminControllerTest extends WebTestCase
$remaining = $this->em->getRepository(Booking::class)->findAll();
self::assertCount(1, $remaining);
self::assertSame($other->getId(), $remaining[0]->getResource()->getId());
// The bookings went without passing through BookingManager, so the history gets one line
// for the lot here.
$events = $this->em->getRepository(BookingEvent::class)->findAll();
self::assertCount(1, $events);
self::assertSame(BookingEventType::RESOURCE_DELETED, $events[0]->getType());
self::assertSame('Schreibtisch 01', $events[0]->getResourceLabel());
self::assertSame(3, $events[0]->getDetails()['removed']);
}
/**
* An unused resource is configuration, not booking history.
*/
public function testDeletingAResourceWithoutBookingsRecordsNoEvent(): void
{
$desk = $this->resource('Schreibtisch 01');
$this->client->loginUser($this->admin());
$uri = \sprintf('/admin/resources/%d/delete', $desk->getId());
$this->client->request('POST', $uri, ['_token' => $this->confirmationToken($uri)], server: ['HTTP_HX-Request' => 'true']);
self::assertSame('/admin/resources', $this->client->getResponse()->headers->get('HX-Redirect'));
self::assertSame(0, $this->em->getRepository(BookingEvent::class)->count([]));
}
public function testDeletingAResourceNeedsAValidToken(): void