M4: PWA + Mobile-UX – Manifest/Service-Worker + Icons, Auf/Ab-Reorder, FAB, Touch-Targets + Safe-Area, Nav-Entdupidung, Heute-Highlight, Aktions-Trennung (Abhaken vs. Bearbeiten/Löschen), OIDC-Fixes + PWA-Tests
This commit is contained in:
@@ -3,49 +3,98 @@
|
||||
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 }}"
|
||||
@param MahlzeitResponse m
|
||||
@param bool sortierbar optional: zeigt Drag-Handle + Auf/Ab-Buttons + data-id (Planungsliste)
|
||||
#}
|
||||
<li class="item {{ m.status.value == 'gegessen' ? 'item-gegessen' : '' }}" id="mahlzeit-{{ m.id }}"
|
||||
{% if sortierbar is defined and sortierbar %}data-id="{{ m.id }}"{% endif %}>
|
||||
<div>
|
||||
<div class="item-main">
|
||||
{% if sortierbar is defined and sortierbar %}
|
||||
<span class="drag-handle" title="Ziehen zum Sortieren">⋮</span>
|
||||
<span class="drag-handle" title="Ziehen zum Sortieren" aria-hidden="true">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="5.5" cy="3" r="1.4"/><circle cx="10.5" cy="3" r="1.4"/>
|
||||
<circle cx="5.5" cy="8" r="1.4"/><circle cx="10.5" cy="8" r="1.4"/>
|
||||
<circle cx="5.5" cy="13" r="1.4"/><circle cx="10.5" cy="13" r="1.4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="updown">
|
||||
<button type="button" class="updown-btn" data-dir="-1" aria-label="Nach oben verschieben">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m6 15 6-6 6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="updown-btn" data-dir="1" aria-label="Nach unten verschieben">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
{% endif %}
|
||||
<strong>{{ m.name }}</strong>
|
||||
{% if m.notiz != '' %}
|
||||
<div class="muted">{{ m.notiz }}</div>
|
||||
{% endif %}
|
||||
{% if m.wochentage|length > 0 %}
|
||||
<div class="tags">
|
||||
{% for tag in m.wochentage %}
|
||||
<span class="tag">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="status {{ m.status.value == 'gegessen' ? 'status-gegessen' : '' }}">
|
||||
{{ m.status.value == 'gegessen' ? 'gegessen' : 'geplant' }}
|
||||
{% if m.status.value == 'gegessen' and m.gegessenAm %}
|
||||
· {{ m.gegessenAm|date('d.m.Y H:i') }}
|
||||
<div class="item-name">
|
||||
<strong>{{ m.name }}</strong>
|
||||
{% if m.notiz != '' %}
|
||||
<div class="notiz">{{ m.notiz }}</div>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if m.wochentage|length > 0 %}
|
||||
<div class="tags">
|
||||
{% for tag in m.wochentage %}
|
||||
<span class="tag tag-{{ tag.value }}">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="status {{ m.status.value == 'gegessen' ? 'status-gegessen' : '' }}">
|
||||
{{ m.status.value == 'gegessen' ? 'gegessen' : 'geplant' }}
|
||||
{% if m.status.value == 'gegessen' and m.gegessenAm %}
|
||||
· {{ m.gegessenAm|date('d.m.Y H:i') }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{% if m.status.value == 'geplant' %}
|
||||
<button type="button" class="btn"
|
||||
hx-post="{{ path('mahlzeiten_checkoff', { id: m.id }) }}"
|
||||
hx-target="#mahlzeit-{{ m.id }}"
|
||||
hx-swap="outerHTML">Abhaken</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-link"
|
||||
hx-post="{{ path('mahlzeiten_uncheck', { id: m.id }) }}"
|
||||
hx-target="#mahlzeit-{{ m.id }}"
|
||||
hx-swap="outerHTML">Aufheben</button>
|
||||
{% endif %}
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_bearbeiten', { id: m.id }) }}">Bearbeiten</a>
|
||||
<form method="post" action="{{ path('mahlzeiten_loeschen', { id: m.id }) }}"
|
||||
onsubmit="return confirm('Mahlzeit „{{ m.name }}“ wirklich löschen?');">
|
||||
<button type="submit" class="btn btn-danger">Löschen</button>
|
||||
</form>
|
||||
<div class="action-haupt">
|
||||
{% if m.status.value == 'geplant' %}
|
||||
<button type="button" class="btn"
|
||||
hx-post="{{ path('mahlzeiten_checkoff', { id: m.id }) }}"
|
||||
hx-target="#mahlzeit-{{ m.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M4.5 12.5 9.5 17.5 19.5 6.5"/>
|
||||
</svg>
|
||||
Abhaken
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-link"
|
||||
hx-post="{{ path('mahlzeiten_uncheck', { id: m.id }) }}"
|
||||
hx-target="#mahlzeit-{{ m.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M9 14 4 9l5-5"/>
|
||||
<path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>
|
||||
</svg>
|
||||
Aufheben
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="action-neben">
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_bearbeiten', { id: m.id }) }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 20h9"/>
|
||||
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/>
|
||||
</svg>
|
||||
Bearbeiten
|
||||
</a>
|
||||
<form method="post" action="{{ path('mahlzeiten_loeschen', { id: m.id }) }}"
|
||||
onsubmit="return confirm('Mahlzeit „{{ m.name }}“ wirklich löschen?');">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
|
||||
<path d="M10 11v6M14 11v6"/>
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<div class="row-between">
|
||||
<h1 style="margin: 0;">Archiv</h1>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Archiv</h1>
|
||||
<div class="subtitle">Alle abgehakten (gegessenen) Mahlzeiten. „Aufheben" stellt sie wieder auf „geplant" um.</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>
|
||||
|
||||
<p class="muted" style="margin-top: 0.75rem;">Alle abgehakten (gegessenen) Mahlzeiten. „Aufheben" stellt sie wieder auf „geplant" um.</p>
|
||||
|
||||
{% if mahlzeiten|length == 0 %}
|
||||
<p class="muted">Noch nichts gegessen – das Archiv ist leer.</p>
|
||||
<p class="empty">Noch nichts gegessen – das Archiv ist leer.</p>
|
||||
{% else %}
|
||||
<ul class="list">
|
||||
{% for m in mahlzeiten %}
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
|
||||
{% block title %}{{ titel }} · svc-essensplanung{% endblock %}
|
||||
|
||||
{% block fab %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h1 style="margin-top: 0;">{{ titel }}</h1>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>{{ titel }}</h1>
|
||||
<div class="subtitle">{{ mahlzeit ? 'Ändere Name, Notiz oder Wochentage.' : 'Ein Gericht, das wieder auf den Plan soll.' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if fehler %}
|
||||
<div class="flash flash-error">{{ fehler }}</div>
|
||||
@@ -17,12 +24,14 @@
|
||||
|
||||
<div class="field">
|
||||
<label for="name">Name <span class="req" title="Pflichtfeld">*</span></label>
|
||||
<input type="text" id="name" name="name" value="{{ werte.name }}" required>
|
||||
<input type="text" id="name" name="name" value="{{ werte.name }}" required
|
||||
placeholder="z. B. Spaghetti Bolognese" autofocus
|
||||
autocomplete="off" enterkeyhint="next">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="notiz">Notiz</label>
|
||||
<textarea id="notiz" name="notiz" rows="3" placeholder="z. B. mit Basilikum, scharf …">{{ werte.notiz }}</textarea>
|
||||
<textarea id="notiz" name="notiz" rows="3" placeholder="z. B. mit Basilikum, scharf …" enterkeyhint="done">{{ werte.notiz }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
@@ -39,7 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="actions" style="margin-top: 1.4rem;">
|
||||
<button type="submit" class="btn">{{ mahlzeit ? 'Speichern' : 'Anlegen' }}</button>
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_index') }}">Abbrechen</a>
|
||||
</div>
|
||||
|
||||
@@ -4,21 +4,23 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<div class="row-between">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 style="margin: 0;">Mahlzeiten</h1>
|
||||
<div class="muted" style="margin-top: 0.2rem;">Planungsliste – per Drag & Drop sortieren</div>
|
||||
<h1>Mahlzeiten</h1>
|
||||
<div class="subtitle">Planungsliste – per Drag & 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>
|
||||
<a class="btn" href="{{ path('mahlzeiten_neu') }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
Neue Mahlzeit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if total == 0 %}
|
||||
<p class="muted" style="margin-top: 1rem;">Noch keine Mahlzeiten in der Planung.</p>
|
||||
<p class="empty">Noch keine Mahlzeiten in der Planung.</p>
|
||||
{% else %}
|
||||
<ul class="list sortierbar">
|
||||
{% for m in mahlzeiten %}
|
||||
@@ -34,26 +36,46 @@
|
||||
<script>
|
||||
(function () {
|
||||
const liste = document.querySelector('ul.sortierbar');
|
||||
if (!liste || typeof Sortable === 'undefined') { return; }
|
||||
if (!liste) { 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(); });
|
||||
},
|
||||
|
||||
function speichern() {
|
||||
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(); });
|
||||
}
|
||||
|
||||
// Auf/Ab-Buttons (mobil): Element mit Nachbar tauschen, dann speichern.
|
||||
liste.addEventListener('click', function (ereignis) {
|
||||
const knopf = ereignis.target.closest('.updown-btn');
|
||||
if (!knopf) { return; }
|
||||
const li = knopf.closest('li[data-id]');
|
||||
if (!li) { return; }
|
||||
const ziel = knopf.getAttribute('data-dir') === '1'
|
||||
? li.nextElementSibling
|
||||
: li.previousElementSibling;
|
||||
if (!ziel || !ziel.matches('li[data-id]')) { return; }
|
||||
if (knopf.getAttribute('data-dir') === '1') { li.after(ziel); } else { li.before(ziel); }
|
||||
speichern();
|
||||
});
|
||||
|
||||
// Drag & Drop (Desktop)
|
||||
if (typeof Sortable !== 'undefined') {
|
||||
Sortable.create(liste, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'item-drag',
|
||||
onEnd: speichern,
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<div class="row-between">
|
||||
<div class="page-head">
|
||||
<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>
|
||||
<h1>Inspiration</h1>
|
||||
<div class="subtitle">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>
|
||||
@@ -18,40 +18,45 @@
|
||||
{% set alleAusschluss = ausschluss + gezeigte %}
|
||||
|
||||
{% if total == 0 %}
|
||||
<p class="muted" style="margin-top: 1rem;">
|
||||
<p class="empty">
|
||||
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>
|
||||
<div class="item-name">
|
||||
<strong>{{ v.name }}</strong>
|
||||
{% if v.notiz != '' %}
|
||||
<div class="muted">{{ v.notiz }}</div>
|
||||
<div class="notiz">{{ v.notiz }}</div>
|
||||
{% endif %}
|
||||
<span class="status">
|
||||
<div class="vorschlag-meta">
|
||||
{% if v.gegessenAm %}
|
||||
zuletzt gegessen · {{ v.gegessenAm|date('d.m.Y') }}
|
||||
{% else %}
|
||||
aus dem Archiv
|
||||
{% endif %}
|
||||
· {{ v.auftraete }}× in der Küche
|
||||
</span>
|
||||
· <b>{{ v.auftraete }}</b>× in der Küche
|
||||
</div>
|
||||
</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>
|
||||
<button type="submit" class="btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M20 6 9 17l-5-5"/>
|
||||
</svg>
|
||||
Übernehmen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if mehrVerfuegbar %}
|
||||
<p style="margin: 1rem 0 0;">
|
||||
<p class="muted" style="margin: 1.1rem 0 0; text-align: center;">
|
||||
<a class="btn btn-link"
|
||||
href="{{ path('mahlzeiten_inspiration', { ausschluss: alleAusschluss|join(',') }) }}">Weitere 5 Vorschläge</a>
|
||||
</p>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<div class="row-between">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 style="margin: 0;">Tagesansicht</h1>
|
||||
<div class="muted" style="margin-top: 0.2rem;">Alle Mahlzeiten nach Wochentag</div>
|
||||
<h1>Tagesansicht</h1>
|
||||
<div class="subtitle">Alle Mahlzeiten nach Wochentag</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_index') }}">← Mahlzeiten</a>
|
||||
@@ -16,10 +16,10 @@
|
||||
</div>
|
||||
|
||||
{% if total == 0 %}
|
||||
<p class="muted" style="margin-top: 1rem;">Noch keine Mahlzeiten angelegt.</p>
|
||||
<p class="empty">Noch keine Mahlzeiten angelegt.</p>
|
||||
{% else %}
|
||||
{% for tag, mahlzeiten in tage %}
|
||||
<h2 style="margin: 1.25rem 0 0;">{{ tag }}</h2>
|
||||
<h2 data-count="{{ mahlzeiten|length }}" {% if tag == heute %}data-heute="1"{% endif %}>{{ tag }}</h2>
|
||||
<ul class="list">
|
||||
{% for m in mahlzeiten %}
|
||||
{{ include('mahlzeiten/_mahlzeit.html.twig', { m: m }) }}
|
||||
|
||||
Reference in New Issue
Block a user