Files
ephub/templates/admin/booking_events.html.twig
T
AdminandClaude Opus 5.5 dcdce4d248 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]>
2026-10-16 16:02:07 +02:00

120 lines
6.6 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{#
The event history. One row per thing that happened -- a series is one row, not one per day --
so a year of standing bookings does not drown out this morning.
"Für" and "Durchgeführt von" are both shown on every row; when they differ, somebody acted on
another person's booking, and the row says so with a badge rather than leaving it to be
noticed.
#}
{% extends 'layout.html.twig' %}
{% block title %}Ereignishistorie · E&P Platz{% endblock %}
{% block content %}
<a class="text-sm text-gray-500 hover:text-gray-900" href="{{ path('app_dashboard') }}">← Übersicht</a>
<h1 class="pt-2">Ereignishistorie</h1>
<p class="subline">Angelegte, geänderte und freigegebene Buchungen der letzten zwölf Monate.</p>
{{ form_start(form, { attr: { class: 'mt-6 rounded-md border border-gray-200 p-4' } }) }}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{{ form_row(form.from) }}
{{ form_row(form.to) }}
{{ form_row(form.actor) }}
{{ form_row(form.resourceType) }}
</div>
<fieldset class="pt-4">
<legend class="block font-bold pb-2">{{ form.types.vars.label }}</legend>
<div class="flex flex-wrap gap-x-6 gap-y-2 text-sm">
{% for choice in form.types %}
<label class="inline-flex items-center gap-2">
{{ form_widget(choice) }}
{{ choice.vars.label }}
</label>
{% endfor %}
</div>
</fieldset>
<div class="flex justify-end gap-2 pt-2">
<a class="button button--small button--neutral" href="{{ path('app_admin_booking_event') }}">Zurücksetzen</a>
<button type="submit" class="button button--small button--primary">Filtern</button>
</div>
{{ form_end(form) }}
<p class="subline pt-8 pb-4">
{{ filter.fromDate|format_datetime(pattern: 'd. MMMM y') }} bis
{{ filter.toDate|format_datetime(pattern: 'd. MMMM y') }}
· {{ pagination.totalItemCount }} {{ pagination.totalItemCount == 1 ? 'Ereignis' : 'Ereignisse' }}
</p>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<th>{{ knp_pagination_sortable(pagination, 'Zeitpunkt', 'e.occurredAt') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Ereignis', 'e.type') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Ressource', 'e.resourceLabel') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Buchung', 'e.date') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Für', 'e.holderName') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Durchgeführt von', 'e.actorName') }}</th>
</tr>
</thead>
<tbody>
{% for event in pagination %}
{% set type = event.type.value %}
<tr>
<td class="whitespace-nowrap">{{ event.occurredAt|format_datetime(pattern: 'dd.MM.y, HH:mm') }}</td>
<td class="whitespace-nowrap">
<span class="badge {{ event.type.badgeClass }}">{{ event.type.label }}</span>
</td>
<td>
{{ event.resourceLabel }}
<span class="block text-xs text-gray-500">{{ event.resourceType.label }}</span>
</td>
<td>
{% if type in ['series_created', 'series_ended'] %}
{{ event.rule ? event.rule.label }}{% if event.timeRange %}, {{ event.timeRange }}{% endif %}
<span class="block text-xs text-gray-500">
{% if type == 'series_created' %}
{{ event.date|format_datetime(pattern: 'd. MMM y') }} – {{ event.endDate|format_datetime(pattern: 'd. MMM y') }}
· {{ event.details.created }} {{ event.details.created == 1 ? 'Termin' : 'Termine' }}
{%- if event.details.skipped is not empty %}, {{ event.details.skipped|length }} übersprungen{% endif %}
{% else %}
ab {{ event.date|format_datetime(pattern: 'd. MMM y') }}
· {{ event.details.removed }} {{ event.details.removed == 1 ? 'Termin' : 'Termine' }} entfernt
{% endif %}
</span>
{% elseif type == 'resource_deleted' %}
{{ event.details.removed }} {{ event.details.removed == 1 ? 'Buchung' : 'Buchungen' }} gelöscht
{% else %}
<span class="whitespace-nowrap">{{ event.date|format_datetime(pattern: 'EEE, d. MMM y') }}</span>
<span class="block text-xs text-gray-500">
{% if event.previousTimeRange %}
{{ event.previousTimeRange }} → {{ event.timeRange }}
{% else %}
{{ event.timeRange }}
{% endif %}
{%- if event.seriesId %} · wiederkehrend{% endif %}
</span>
{% endif %}
</td>
<td>{{ event.holderName ?? '–' }}</td>
<td>
{{ event.actorName }}
{% if event.onBehalf %}
<span class="badge bg-gray-100 text-gray-600">stellvertretend</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-gray-500">Keine Ereignisse in diesem Zeitraum.</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% endblock %}