feat: add admin crud for resources and rooms

This commit is contained in:
fromme committed 2026-09-17 16:26:29 +02:00
1 parent ad5d5acb00
commit 3d67942c62
19 files changed
+1059 -4

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
{#
Creating and editing a desk or parking spot. Same double duty as the booking modal: the form
targets the modal itself, so an invalid submit (422) replaces it with the errors showing, and a
valid one answers with HX-Redirect back to the list.
#}
{% extends 'htmx_modal.html.twig' %}
{% block title %}
{%- if resource.id -%}
{{ resource.label }} bearbeiten
{%- else -%}
{{ resource.type.label }} 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.group) }}
{% if form.active is defined %}
{{ form_row(form.active) }}
{% endif %}
</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,34 @@
{# Creating and editing a room or area. See admin/_modal_resource.html.twig for how it closes. #}
{% extends 'htmx_modal.html.twig' %}
{% block title %}
{%- if group.id -%}
{{ group.label }} bearbeiten
{%- else -%}
{{ group.type.groupLabel }} 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.position) }}
</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,11 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block title %}{{ group.type.groupLabel }} löschen?{% endblock %}
{% block content %}
<p>
{{ group.type.groupLabel }} <em>{{ group.label }}</em> wird gelöscht.
</p>
{% endblock %}
{% block button_confirm %}Löschen{% endblock %}
+86
View File
@@ -0,0 +1,86 @@
{#
Everything bookable, per type and room. Every write opens a modal via htmx and navigates back
here once saved -- the same shape as the sibling apps' admin lists.
Retired resources stay listed, muted, because this is the only place they can be reactivated.
Only an empty room offers "Löschen"; DeleteController refuses the rest regardless.
#}
{% extends 'layout.html.twig' %}
{% block title %}Plätze & Räume · ephub{% endblock %}
{% macro opener(url, label, variant = 'neutral') %}
<button type="button" class="button button--small button--{{ variant }}"
hx-get="{{ url }}"
hx-target="body"
hx-swap="beforeend">{{ label }}</button>
{% endmacro %}
{% macro resource_list(resources) %}
{% if resources is empty %}
<p class="py-3 text-sm text-gray-500">Noch nichts angelegt.</p>
{% else %}
<ul class="divide-y divide-gray-200 border-y border-gray-200">
{% for resource in resources %}
<li class="flex items-center justify-between gap-4 py-2" data-resource>
<span class="{{ resource.active ? '' : 'text-gray-400' }}">
{{ resource.label }}
{% if not resource.active %}
<span class="badge bg-gray-100 text-gray-600 ml-1">nicht buchbar</span>
{% endif %}
</span>
{{ _self.opener(path('app_admin_resource_edit', { id: resource.id }), 'Bearbeiten') }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
{% block content %}
<a class="text-sm text-gray-500 hover:text-gray-900" href="{{ path('app_dashboard') }}">← Übersicht</a>
<h1 class="pt-2">Plätze & Räume</h1>
<p class="subline">Was gebucht werden kann und wo es steht.</p>
{% for section in sections %}
{% set type = section.type %}
<section class="pt-8" data-type="{{ type.value }}">
<div class="flex flex-wrap items-baseline justify-between gap-2">
<h2>{{ type.plural }}</h2>
<div class="flex gap-2">
{{ _self.opener(path('app_admin_resource_group_new', { type: type.value }), type.groupLabel ~ ' hinzufügen') }}
{{ _self.opener(path('app_admin_resource_new', { type: type.value }), type.label ~ ' hinzufügen', 'primary') }}
</div>
</div>
{% for entry in section.groups %}
<div class="pt-4" data-group>
<div class="flex flex-wrap items-center justify-between gap-2 pb-1">
<h3 class="!mb-0 text-base md:text-base">
{{ entry.group.label }}
</h3>
<div class="flex gap-2">
{{ _self.opener(path('app_admin_resource_group_edit', { id: entry.group.id }), 'Bearbeiten') }}
{% if entry.resources is empty %}
{{ _self.opener(path('app_admin_resource_group_delete', { id: entry.group.id }), 'Löschen') }}
{% endif %}
</div>
</div>
{{ _self.resource_list(entry.resources) }}
</div>
{% endfor %}
{# Only when there is something to show, or when there are no groups at all -- then this
is simply the list, as for the car park. #}
{% if section.ungrouped is not empty or section.groups is empty %}
<div class="pt-4" data-group>
{% if section.groups is not empty %}
<h3 class="!mb-0 pb-1 text-base md:text-base">Ohne {{ type.groupLabel }}</h3>
{% endif %}
{{ _self.resource_list(section.ungrouped) }}
</div>
{% endif %}
</section>
{% endfor %}
{% endblock %}
+11
View File
@@ -63,6 +63,17 @@
Buchungen aller Mitarbeitenden einsehen und ggf. freigeben.
</p>
</a>
<a href="{{ path('app_admin_resource') }}"
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:layout-grid', { class: 'h-6 w-6 shrink-0 text-primary' }) }}
<span class="text-lg font-bold">Plätze &amp; Räume</span>
</div>
<p class="text-sm text-gray-500 pb-4">
Schreibtische, Parkplätze und Räume anlegen und bearbeiten.
</p>
</a>
{% endif %}
</div>