feat: add supervisor role

This commit is contained in:
fromme committed 2026-10-05 17:10:02 +02:00
1 parent dcd85bb911
commit 3af326251d
22 files changed
+270 -103

No files matched your search

-52
View File
@@ -1,52 +0,0 @@
{#
What an administrator sees when clicking an occupied shape on the floor plan.
Not the edit modal: a shape can hold several bookings on one day once part-day spans exist,
so "the booking" has no single referent -- and an administrator releases rather than
reschedules. Each row posts to the ordinary app_booking_release route, whose ownership check
admits an admin, so there is still one code path that removes a booking.
#}
{% extends 'htmx_modal.html.twig' %}
{% block title %}{{ resource.label }} verwalten{% endblock %}
{% block content %}
<p class="pb-4 text-sm text-gray-500">
{{ resource.type.label }} · {{ date|format_datetime(pattern: 'EEEE, d. MMMM y') }}
{%- if resource.group %} · {{ resource.group.label }}{% endif %}
</p>
{% if bookings is empty %}
<p class="text-sm text-gray-500">In diesem Zeitraum liegt hier keine Buchung.</p>
{% else %}
<ul class="divide-y divide-gray-200 border-y border-gray-200">
{% for booking in bookings %}
<li class="flex items-center justify-between gap-4 py-3">
<div>
<div class="font-semibold">{{ booking.user.name }}</div>
<div class="text-sm text-gray-500">
{{ booking.timeRange }}
{% if booking.partOfSeries %}
<span class="badge bg-gray-100 text-gray-600 ml-1">wiederkehrend</span>
{% endif %}
</div>
</div>
{# Same as the edit modal: opens the shared confirmation modal over this one.
Confirming repaints the board out of band and removes both. #}
<button type="button" class="button button--small button--warning"
hx-get="{{ path('app_booking_release', { id: booking.id }) }}"
hx-target="body"
hx-swap="beforeend">
Freigeben
</button>
</li>
{% endfor %}
</ul>
{% endif %}
<div class="flex justify-end pt-6">
<button type="button" class="button button--neutral" {{ stimulus_action('modal', 'close') }}>
Schließen
</button>
</div>
{% endblock %}
-136
View File
@@ -1,136 +0,0 @@
{#
Every booking, not just yours. The point of the screen is the "Freigeben" button on somebody
else's row: when a colleague calls in sick, their desk has to go back into the pool.
Filtering is a plain GET form -- the house rule hands navigation to the browser. The two
destructive actions open the shared confirmation modal via htmx and then navigate back here,
which is what ?return=admin says; without it a confirmed release swaps board fragments this
page does not have.
The appointment table is paginated; the series table is not, because a company has a handful of
standing bookings and cutting them across pages would only hide them.
#}
{% extends 'layout.html.twig' %}
{% block title %}Verwaltung · 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">Verwaltung</h1>
<p class="subline">Buchungen aller Mitarbeitenden.</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.type) }}
{{ form_row(form.user) }}
</div>
<div class="flex justify-end gap-2 pt-2">
<a class="button button--small button--neutral" href="{{ path('app_admin_booking') }}">Zurücksetzen</a>
<button type="submit" class="button button--small button--primary">Filtern</button>
</div>
{{ form_end(form) }}
{% if series is not empty %}
<h2 class="pt-8 pb-4">Wiederkehrende Buchungen</h2>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<th>Mitarbeiter:in</th>
<th>Ressource</th>
<th>Gruppe</th>
<th>Wiederholung</th>
<th>Bis</th>
<th></th>
</tr>
</thead>
<tbody>
{% for entry in series %}
<tr>
<td class="font-semibold">{{ entry.user.name }}</td>
<td>{{ entry.resource.label }}</td>
<td>{{ entry.resource.group ? entry.resource.group.label : '–' }}</td>
<td>{{ entry.describe }}</td>
<td>{{ entry.endDate|format_datetime(pattern: 'd. MMMM y') }}</td>
<td class="text-right">
<button type="button" class="button button--small button--warning"
hx-get="{{ path('app_series_end', { id: entry.id, return: 'admin' }) }}"
hx-target="body"
hx-swap="beforeend">
Wiederholung beenden
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<h2 class="pt-8">Termine</h2>
<p class="subline pb-4">
{{ filter.fromDate|format_datetime(pattern: 'd. MMMM y') }} bis
{{ filter.toDate|format_datetime(pattern: 'd. MMMM y') }}
</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, 'Mitarbeiter:in', 'u.name') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Datum', 'b.date') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Zeit', 'b.startMinute') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Typ', 'r.type') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Ressource', 'r.label') }}</th>
<th>Gruppe</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for booking in pagination %}
<tr>
<td class="font-semibold">{{ booking.user.name }}</td>
<td class="whitespace-nowrap">{{ booking.date|format_datetime(pattern: 'EEE, d. MMM y') }}</td>
<td class="whitespace-nowrap">{{ booking.timeRange }}</td>
<td>{{ booking.resource.type.label }}</td>
<td>{{ booking.resource.label }}</td>
<td>{{ booking.resource.group ? booking.resource.group.label : '–' }}</td>
<td>
{% if booking.partOfSeries %}
<span class="badge bg-gray-100 text-gray-600">wiederkehrend</span>
{% endif %}
{% if booking.overridden %}
<span class="badge bg-amber-100 text-amber-800">abweichend</span>
{% endif %}
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<a class="button button--small button--neutral"
href="{{ path('app_board', { type: booking.resource.type.value, date: booking.date|date('Y-m-d') }) }}">Im Plan</a>
<button type="button" class="button button--small button--warning"
hx-get="{{ path('app_booking_release', { id: booking.id, return: 'admin' }) }}"
hx-target="body"
hx-swap="beforeend">
Freigeben
</button>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-gray-500">Keine Buchungen in diesem Zeitraum.</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% endblock %}