feat: door access codes display and admin crud

This commit is contained in:
fromme committed 2026-09-25 13:08:39 +02:00
1 parent 91419294b8
commit afa3f17d77
20 files changed
+1428

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
{#
The quick look: what to type at a door, on the phone you are holding in front of it.
One card per door with the code in force today and nothing else -- no dates, no history, no
buttons. Maintaining the codes is admin/access_codes.html.twig, which is the table this page
used to be.
The card is sized for reading, not for fitting: the code is the largest thing on the screen,
in near-black on white and in a tabular font so 1 and 7 cannot be confused. One column on a
phone, more when there is room.
#}
{% extends 'layout.html.twig' %}
{% block title %}Zugangscodes · E&P Platz{% endblock %}
{% block content %}
<a class="text-sm text-gray-500 hover:text-gray-900" href="{{ path('app_dashboard') }}">← Übersicht</a>
<div class="flex flex-wrap items-baseline justify-between gap-2 pt-2">
<div>
<h1>Zugangscodes</h1>
<p class="subline">Türen, Aufzug etc.</p>
</div>
{% if is_granted('ROLE_ADMIN') %}
<a class="button button--small button--neutral" href="{{ path('app_admin_access_code') }}">Codes verwalten</a>
{% endif %}
</div>
{% if codes is empty %}
<p class="pt-8 text-sm text-gray-500">Zurzeit gilt kein Code.</p>
{% else %}
<div class="pt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{% for code in codes %}
<div class="flex flex-col rounded-md border border-gray-200 p-6" data-access-code>
<div class="flex items-baseline justify-between gap-2">
<span class="text-lg font-bold">{{ code.label }}</span>
{% if code.visibility.isRestricted %}
<span class="badge bg-gray-100 text-gray-600 shrink-0">{{ code.visibility.label }}</span>
{% endif %}
</div>
{% if code.description %}
<p class="pt-1 text-sm text-gray-500">{{ code.description }}</p>
{% endif %}
{# mt-auto, so the code sits at the bottom of the card and the codes of a row
line up whether or not a door has a description -- grid stretches the cards
of a row to the same height, and this spends the difference above the code
rather than below it. Same device as the dashboard tiles.
tracking-widest so a long code stays readable as digits rather than as a
number, and break-all so it never overflows a narrow screen. #}
<p class="mt-auto pt-4 font-mono text-4xl font-bold tracking-widest tabular-nums break-all text-gray-900"
data-code>{{ code.code }}</p>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,37 @@
{# Creating and editing an access code. See admin/_modal_resource.html.twig for how it closes. #}
{% extends 'htmx_modal.html.twig' %}
{% block title %}
{%- if code.id -%}
{{ code.label }} bearbeiten
{%- else -%}
Code hinzufügen
{%- endif -%}
{% endblock %}
{% block content %}
{{ form_start(form, { attr: {
'hx-post': form.vars.action,
'hx-target': '#htmx-modal',
'hx-swap': 'outerHTML',
} }) }}
{{ form_errors(form) }}
<div class="space-y-4">
{{ form_row(form.label) }}
{{ form_row(form.description) }}
{{ form_row(form.code) }}
{{ form_row(form.validFrom) }}
{{ form_row(form.visibility) }}
</div>
<div class="flex justify-end gap-2 pt-6">
<button type="button" class="button button--neutral" {{ stimulus_action('modal', 'close') }}>
Abbrechen
</button>
<button type="submit" class="button button--primary">
Speichern
</button>
</div>
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,12 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block title %}Code löschen?{% endblock %}
{% block content %}
<p>
Der Code für <em>{{ code.label }}</em> (gültig ab
{{ code.validFrom|format_date(pattern: 'd.MM.y') }}) wird gelöscht.
</p>
{% endblock %}
{% block button_confirm %}Löschen{% endblock %}
+82
View File
@@ -0,0 +1,82 @@
{#
Maintaining the codes: every row there is, in a table, newest per door first.
The counterpart of access_code/index.html.twig, which shows one card per door with the code
that works today and nothing else. Everything the quick look leaves out is here -- rows the
controller marked `upcoming` or `superseded`, muted and labelled -- because this is the
screen on which a rotation is planned and checked.
„Für wen" is the visibility itself: it decides who is served the row and says so in the same
breath, so there is nothing to keep in step between the two.
#}
{% extends 'layout.html.twig' %}
{% block title %}Zugangscodes verwalten · E&P Platz{% endblock %}
{% block content %}
<a class="text-sm text-gray-500 hover:text-gray-900" href="{{ path('app_access_code') }}">← Zugangscodes</a>
<div class="flex flex-wrap items-baseline justify-between gap-2 pt-2">
<div>
<h1>Zugangscodes verwalten</h1>
</div>
<button type="button" class="button button--small button--primary"
hx-get="{{ path('app_admin_access_code_new') }}"
hx-target="body"
hx-swap="beforeend">Code hinzufügen</button>
</div>
{% if rows is empty %}
<p class="pt-8 text-sm text-gray-500">Noch nichts hinterlegt.</p>
{% else %}
<div class="pt-8 overflow-x-auto">
<table class="w-full text-left text-sm">
<thead class="border-b border-gray-200 text-gray-500">
<tr>
<th class="py-2 pr-4 font-semibold">Bezeichnung</th>
<th class="py-2 pr-4 font-semibold">Beschreibung</th>
<th class="py-2 pr-4 font-semibold">Code</th>
<th class="py-2 pr-4 font-semibold">sichtbar für</th>
<th class="py-2 pr-4 font-semibold">gültig ab</th>
<th class="py-2 font-semibold"><span class="sr-only">Aktionen</span></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for row in rows %}
{% set muted = row.upcoming or row.superseded %}
<tr data-access-code class="{{ muted ? 'text-gray-400' : '' }}">
<td class="py-2 pr-4 font-semibold">{{ row.code.label }}</td>
<td class="py-2 pr-4">{{ row.code.description }}</td>
<td class="py-2 pr-4 font-mono">{{ row.code.code }}</td>
<td class="py-2 pr-4">
{% if row.code.visibility.isRestricted %}
<span class="badge bg-gray-100 text-gray-600">{{ row.code.visibility.label }}</span>
{% else %}
{{ row.code.visibility.label }}
{% endif %}
</td>
<td class="py-2 pr-4 whitespace-nowrap">
{{ row.code.validFrom|format_date(pattern: 'd.MM.y') }}
{% if row.upcoming %}
<span class="badge bg-amber-100 text-amber-800 ml-1">noch nicht gültig</span>
{% elseif row.superseded %}
<span class="badge bg-gray-100 text-gray-600 ml-1">ersetzt</span>
{% endif %}
</td>
<td class="py-2">
<div class="flex justify-end gap-2">
<button type="button" class="button button--small button--neutral"
hx-get="{{ path('app_admin_access_code_edit', { id: row.code.id }) }}"
hx-target="body" hx-swap="beforeend">Bearbeiten</button>
<button type="button" class="button button--small button--neutral"
hx-get="{{ path('app_admin_access_code_delete', { id: row.code.id }) }}"
hx-target="body" hx-swap="beforeend">Löschen</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}
+9
View File
@@ -49,6 +49,15 @@
<p class="text-sm text-gray-500 pb-4">Alle Buchungen verwalten.</p>
</a>
<a href="{{ path('app_access_code') }}"
class="group flex flex-col rounded-md border border-gray-200 p-6 hover:border-primary hover:bg-gray-50">
<div class="flex items-center gap-3 pb-2">
{{ ux_icon('lucide:key-round', { class: 'h-6 w-6 shrink-0 text-primary' }) }}
<span class="text-lg font-bold">Zugangscodes</span>
</div>
<p class="text-sm text-gray-500 pb-4">Codes für Türen, Aufzug und Schranke.</p>
</a>
{#
The header is deliberately not navigation, so the dashboard is the only place an
admin screen can be reached from.