M3: Reorder + Inspiration – Schritt 2 (UI): Planungsliste mit SortableJS, Tagesansicht /mahlzeiten/tage, Inspiration /inspiration mit 'Weitere 5' + 1-Klick-Adopt, Feature-Tests

This commit is contained in:
2026-09-19 17:09:15 +02:00
parent 52a34225fa
commit fa5894426d
9 changed files with 488 additions and 25 deletions
+8 -3
View File
@@ -1,12 +1,17 @@
{#
Mahlzeiten-Partial (eine Zeile). Wird sowohl in der Tagesansicht/Archiv
eingebunden als auch als htmx-Fragment von den CheckOff/Uncheck-Endpunkten
Mahlzeiten-Partial (eine Zeile). Wird in Planungsliste/Tagesansicht/Archiv
eingebunden und als htmx-Fragment von den CheckOff/Uncheck-Endpunkten
zurückgeliefert (htmx ersetzt per hx-swap="outerHTML" genau diese <li>).
@param MahlzeitResponse m
@param bool sortierbar optional: zeigt Drag-Handle + data-id (Planungsliste)
#}
<li class="item" id="mahlzeit-{{ m.id }}">
<li class="item" id="mahlzeit-{{ m.id }}"
{% if sortierbar is defined and sortierbar %}data-id="{{ m.id }}"{% endif %}>
<div>
{% if sortierbar is defined and sortierbar %}
<span class="drag-handle" title="Ziehen zum Sortieren">⋮</span>
{% endif %}
<strong>{{ m.name }}</strong>
{% if m.notiz != '' %}
<div class="muted">{{ m.notiz }}</div>
+38 -10
View File
@@ -7,25 +7,53 @@
<div class="row-between">
<div>
<h1 style="margin: 0;">Mahlzeiten</h1>
<div class="muted" style="margin-top: 0.2rem;">Tagesansicht nach Wochentag</div>
<div class="muted" style="margin-top: 0.2rem;">Planungsliste – per Drag &amp; Drop sortieren</div>
</div>
<div class="actions">
<a class="btn" href="{{ path('mahlzeiten_neu') }}">+ Neue Mahlzeit</a>
<a class="btn btn-link" href="{{ path('mahlzeiten_inspiration') }}">Inspiration</a>
<a class="btn btn-link" href="{{ path('mahlzeiten_tage') }}">Tagesansicht</a>
<a class="btn btn-link" href="{{ path('mahlzeiten_archiv') }}">Archiv</a>
</div>
</div>
{% if total == 0 %}
<p class="muted" style="margin-top: 1rem;">Noch keine Mahlzeiten angelegt.</p>
<p class="muted" style="margin-top: 1rem;">Noch keine Mahlzeiten in der Planung.</p>
{% else %}
{% for tag, mahlzeiten in tage %}
<h2 style="margin: 1.25rem 0 0;">{{ tag }}</h2>
<ul class="list">
{% for m in mahlzeiten %}
{{ include('mahlzeiten/_mahlzeit.html.twig', { m: m }) }}
{% endfor %}
</ul>
{% endfor %}
<ul class="list sortierbar">
{% for m in mahlzeiten %}
{{ include('mahlzeiten/_mahlzeit.html.twig', { m: m, sortierbar: true }) }}
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}
{% block javascripts %}
<script src="{{ asset('js/sortable.min.js') }}"></script>
<script>
(function () {
const liste = document.querySelector('ul.sortierbar');
if (!liste || typeof Sortable === 'undefined') { return; }
const url = '{{ path("mahlzeiten_reorder") }}';
Sortable.create(liste, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'item-drag',
onEnd: function () {
const ids = Array.prototype.map.call(
liste.querySelectorAll('li[data-id]'),
function (li) { return Number(li.getAttribute('data-id')); }
);
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: ids }),
}).then(function (antwort) {
if (!antwort.ok) { window.location.reload(); }
}).catch(function () { window.location.reload(); });
},
});
})();
</script>
{% endblock %}
@@ -0,0 +1,61 @@
{% extends 'layout/base.html.twig' %}
{% block title %}Inspiration · svc-essensplanung{% endblock %}
{% block body %}
<div class="card">
<div class="row-between">
<div>
<h1 style="margin: 0;">Inspiration</h1>
<div class="muted" style="margin-top: 0.2rem;">Gerichte aus dem Archiv – was kochst du mal wieder?</div>
</div>
<div class="actions">
<a class="btn btn-link" href="{{ path('mahlzeiten_index') }}">← Mahlzeiten</a>
</div>
</div>
{# Für "Weitere 5" / Adopt-Kontext: bisheriger Ausschluss + die IDs dieser Seite #}
{% set alleAusschluss = ausschluss + gezeigte %}
{% if total == 0 %}
<p class="muted" style="margin-top: 1rem;">
Noch keine Inspiration: Hake Mahlzeiten ab – dann erscheinen sie hier als Vorschlag.
</p>
{% else %}
<ul class="list">
{% for v in vorschlaege %}
<li class="item" id="vorschlag-{{ v.id }}">
<div>
<strong>{{ v.name }}</strong>
{% if v.notiz != '' %}
<div class="muted">{{ v.notiz }}</div>
{% endif %}
<span class="status">
{% if v.gegessenAm %}
zuletzt gegessen · {{ v.gegessenAm|date('d.m.Y') }}
{% else %}
aus dem Archiv
{% endif %}
· {{ v.auftraete }}× in der Küche
</span>
</div>
<div class="actions">
<form method="post" action="{{ path('mahlzeiten_adopt', { id: v.id }) }}">
{% for id in alleAusschluss %}
<input type="hidden" name="ausschluss[]" value="{{ id }}">
{% endfor %}
<button type="submit" class="btn">Übernehmen</button>
</form>
</div>
</li>
{% endfor %}
</ul>
{% if mehrVerfuegbar %}
<p style="margin: 1rem 0 0;">
<a class="btn btn-link"
href="{{ path('mahlzeiten_inspiration', { ausschluss: alleAusschluss|join(',') }) }}">Weitere 5 Vorschläge</a>
</p>
{% endif %}
{% endif %}
</div>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
{% extends 'layout/base.html.twig' %}
{% block title %}Tagesansicht · svc-essensplanung{% endblock %}
{% block body %}
<div class="card">
<div class="row-between">
<div>
<h1 style="margin: 0;">Tagesansicht</h1>
<div class="muted" style="margin-top: 0.2rem;">Alle Mahlzeiten nach Wochentag</div>
</div>
<div class="actions">
<a class="btn btn-link" href="{{ path('mahlzeiten_index') }}">← Mahlzeiten</a>
<a class="btn" href="{{ path('mahlzeiten_neu') }}">+ Neue Mahlzeit</a>
</div>
</div>
{% if total == 0 %}
<p class="muted" style="margin-top: 1rem;">Noch keine Mahlzeiten angelegt.</p>
{% else %}
{% for tag, mahlzeiten in tage %}
<h2 style="margin: 1.25rem 0 0;">{{ tag }}</h2>
<ul class="list">
{% for m in mahlzeiten %}
{{ include('mahlzeiten/_mahlzeit.html.twig', { m: m }) }}
{% endfor %}
</ul>
{% endfor %}
{% endif %}
</div>
{% endblock %}