From 6c7c1f01c9e517ffffca3c0f02dec66b0acca786 Mon Sep 17 00:00:00 2001 From: Florian Heinz Date: Mon, 13 Apr 2026 14:57:36 +0200 Subject: [PATCH] Add week planner entry editing popups --- nouri/main.py | 11 ++++- nouri/static/css/styles.css | 73 +++++++++++++++++++++++++++++++ nouri/static/js/planner.js | 69 +++++++++++++++++++++++++++++ nouri/templates/planner/week.html | 53 +++++++++++++++++++++- 4 files changed, 204 insertions(+), 2 deletions(-) diff --git a/nouri/main.py b/nouri/main.py index 5221731..c2ba3ac 100644 --- a/nouri/main.py +++ b/nouri/main.py @@ -3815,6 +3815,7 @@ def planner(): week_hints=build_week_hints(week_start), upcoming_entries=fetch_upcoming_shopping_needs(limit=8), household_settings=get_household_settings(), + visibility_options=VISIBILITY_FORM_OPTIONS, ) @@ -3941,6 +3942,7 @@ def planner_generated_meal(): @login_required def planner_update(entry_id: int): selected_date = parse_plan_date(request.form.get("plan_date")) + return_week = request.form.get("return_week", "").strip() entry = get_db().execute( f""" SELECT plan_entries.*, @@ -3960,19 +3962,24 @@ def planner_update(entry_id: int): ensure_can_edit(describe_record(dict(entry)), "Diesen Planeintrag kannst du gerade nicht bearbeiten.") except PermissionError as exc: flash(str(exc), "error") + if return_week: + return redirect(url_for("main.planner", week=return_week)) return redirect(url_for("main.planner_day", date=selected_date.isoformat())) visibility = normalize_visibility(request.form.get("visibility"), entry["visibility"]) note = request.form.get("note", "").strip() update_plan_entry(entry_id, visibility=visibility, note=note) flash("Der Planeintrag wurde angepasst.", "success") + if return_week: + return redirect(url_for("main.planner", week=return_week)) return redirect(url_for("main.planner_day", date=selected_date.isoformat(), daypart_id=entry["daypart_id"])) @main_bp.post("/planner//remove") @login_required def planner_remove(entry_id: int): - selected_date = request.args.get("date", "") + selected_date = request.args.get("date", "") or request.form.get("plan_date", "").strip() + return_week = request.form.get("return_week", "").strip() entry = get_db().execute( f""" SELECT plan_entries.*, @@ -3994,6 +4001,8 @@ def planner_remove(entry_id: int): flash("Der Planeintrag wurde entfernt.", "info") except PermissionError as exc: flash(str(exc), "error") + if return_week: + return redirect(url_for("main.planner", week=return_week)) if selected_date: return redirect(url_for("main.planner_day", date=selected_date)) return redirect(url_for("main.planner")) diff --git a/nouri/static/css/styles.css b/nouri/static/css/styles.css index 20956d0..88307ce 100644 --- a/nouri/static/css/styles.css +++ b/nouri/static/css/styles.css @@ -1583,6 +1583,17 @@ legend { opacity: 0.8; } +.plan-chip.is-editable { + cursor: pointer; +} + +.plan-chip.is-editable:hover { + border-color: color-mix(in srgb, var(--accent) 34%, var(--line) 66%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + 0 10px 22px rgba(94, 68, 49, 0.12); +} + .plan-chip:active { cursor: grabbing; } @@ -1602,6 +1613,55 @@ legend { padding: 0.55rem 0.85rem; } +.week-entry-dialog { + padding: 0; + border: 0; + background: transparent; + max-width: min(34rem, calc(100vw - 2rem)); + width: min(34rem, calc(100vw - 2rem)); +} + +.week-entry-dialog::backdrop { + background: rgba(29, 22, 19, 0.54); + backdrop-filter: blur(6px); +} + +.week-entry-dialog-card { + display: grid; + gap: 1rem; + padding: 1.1rem; + border-radius: 22px; + border: 1px solid var(--line); + background: color-mix(in srgb, var(--surface) 98%, #fff 2%); + box-shadow: var(--shadow); +} + +.week-entry-dialog-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.85rem; +} + +.week-entry-dialog-head h3 { + margin: 0 0 0.2rem; + font-size: 1.25rem; +} + +.week-entry-dialog-head p { + margin: 0; + color: var(--muted); +} + +.week-entry-dialog-actions { + display: flex; + justify-content: flex-start; +} + +.week-entry-remove-form { + margin: 0; +} + .plan-chip small, .week-slot-empty { color: var(--muted); @@ -1687,6 +1747,13 @@ legend { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); } +[data-theme="dark"] .plan-chip.is-editable:hover { + border-color: rgba(243, 177, 125, 0.3); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.06), + 0 12px 26px rgba(0, 0, 0, 0.24); +} + [data-theme="dark"] .week-slot-copy { background: rgba(255, 255, 255, 0.03); border-color: rgba(243, 177, 125, 0.12); @@ -1701,6 +1768,12 @@ legend { border-color: rgba(243, 177, 125, 0.16); } +[data-theme="dark"] .week-entry-dialog-card { + background: rgba(43, 37, 35, 0.98); + border-color: rgba(243, 177, 125, 0.14); + box-shadow: 0 24px 50px rgba(0, 0, 0, 0.34); +} + .flash-stack { display: grid; gap: 0.7rem; diff --git a/nouri/static/js/planner.js b/nouri/static/js/planner.js index 077c23d..50660bf 100644 --- a/nouri/static/js/planner.js +++ b/nouri/static/js/planner.js @@ -95,12 +95,14 @@ } const result = await response.json(); + rememberScroll(); if (result.redirect_url) { window.location.href = result.redirect_url; } else { window.location.reload(); } } catch (_error) { + rememberScroll(); window.location.reload(); } }); @@ -196,6 +198,72 @@ }); }; + const initWeekEntryDialogs = () => { + const board = document.querySelector(".week-board"); + if (!board) return; + + const openDialog = (trigger) => { + const dialogId = trigger.getAttribute("data-week-entry-dialog-id"); + if (!dialogId) return; + const dialog = document.getElementById(dialogId); + if (!(dialog instanceof HTMLDialogElement)) return; + if (!dialog.open) { + dialog.showModal(); + } + }; + + board.querySelectorAll("[data-week-entry-open]").forEach((entry) => { + entry.addEventListener("click", (event) => { + if (event.target instanceof Element && event.target.closest("button, a, input, select, textarea, label, form")) { + return; + } + openDialog(entry); + }); + + entry.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + openDialog(entry); + }); + }); + + document.querySelectorAll(".week-entry-dialog").forEach((dialog) => { + if (!(dialog instanceof HTMLDialogElement)) return; + + dialog.addEventListener("click", (event) => { + const rect = dialog.getBoundingClientRect(); + const clickedInside = + rect.top <= event.clientY && + event.clientY <= rect.top + rect.height && + rect.left <= event.clientX && + event.clientX <= rect.left + rect.width; + if (!clickedInside) { + dialog.close(); + } + }); + }); + + document.querySelectorAll("[data-week-entry-close]").forEach((button) => { + button.addEventListener("click", () => { + const dialog = button.closest(".week-entry-dialog"); + if (dialog instanceof HTMLDialogElement) { + dialog.close(); + } + }); + }); + + document.querySelectorAll(".js-week-entry-submit").forEach((form) => { + form.addEventListener("submit", async (event) => { + event.preventDefault(); + try { + await postAndRefreshInPlace(form); + } catch (_error) { + window.location.reload(); + } + }); + }); + }; + const syncActionContainerVisibility = (container) => { if (!(container instanceof HTMLElement)) return; const hasVisibleButtons = Array.from(container.querySelectorAll("button")).some((button) => { @@ -285,6 +353,7 @@ initWeekDragAndDrop(); initWeekCopyForward(); initWeekSlotPicker(); + initWeekEntryDialogs(); initDaySnackReveal(); initWeekSnackReveal(); }); diff --git a/nouri/templates/planner/week.html b/nouri/templates/planner/week.html index c4c284a..ad24b6d 100644 --- a/nouri/templates/planner/week.html +++ b/nouri/templates/planner/week.html @@ -213,10 +213,61 @@ {% if slot.entries %}
{% for entry in slot.entries %} -
+
{{ entry.item_name }} {{ entry.visibility_label }} · {{ entry.for_label }}
+ {% if entry.can_edit %} + +
+
+
+

{{ entry.item_name }}

+

{{ slot.daypart.name }} · {{ weekday_name(card.date) }}, {{ card.date.strftime('%d.%m.%Y') }}

+
+ +
+
+ {{ csrf_input() }} + + + + +
+ +
+
+
+ {{ csrf_input() }} + + + +
+
+
+ {% endif %} {% endfor %}