Compare commits
1 Commits
b68ed62887
...
V0.4.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d8b56e6b67 |
@@ -4,8 +4,8 @@
|
|||||||
"author": "Florian Heinz",
|
"author": "Florian Heinz",
|
||||||
"description": "Private Flask app for meals, shopping and gentle food planning",
|
"description": "Private Flask app for meals, shopping and gentle food planning",
|
||||||
"tagline": "einfach essen planen",
|
"tagline": "einfach essen planen",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"upstreamVersion": "0.3.0",
|
"upstreamVersion": "0.4.0",
|
||||||
"healthCheckPath": "/",
|
"healthCheckPath": "/",
|
||||||
"httpPort": 8000,
|
"httpPort": 8000,
|
||||||
"manifestVersion": 2,
|
"manifestVersion": 2,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Nouri ist eine kleine private Flask-App für einen Haushalt, um Essensideen, Einkäufe, vorhandene Lebensmittel und eine einfache Tages- oder Wochenplanung ruhig und alltagsnah festzuhalten.
|
Nouri ist eine kleine private Flask-App für einen Haushalt, um Essensideen, Einkäufe, vorhandene Lebensmittel und eine einfache Tages- oder Wochenplanung ruhig und alltagsnah festzuhalten.
|
||||||
|
|
||||||
## Merkmale in Version 0.3
|
## Merkmale in Version 0.4
|
||||||
|
|
||||||
- Lebensmittel und Mahlzeitenideen anlegen
|
- Lebensmittel und Mahlzeitenideen anlegen
|
||||||
- Fotos lokal hochladen
|
- Fotos lokal hochladen
|
||||||
@@ -17,6 +17,12 @@ Nouri ist eine kleine private Flask-App für einen Haushalt, um Essensideen, Ein
|
|||||||
- Profilseite und Passwortänderung
|
- Profilseite und Passwortänderung
|
||||||
- kleine Admin-Verwaltung für Nutzer
|
- kleine Admin-Verwaltung für Nutzer
|
||||||
- kompaktere mobile Navigation mit Bottom-Bar
|
- kompaktere mobile Navigation mit Bottom-Bar
|
||||||
|
- Tagesvorlagen und Wochenvorlagen
|
||||||
|
- kleine Pakete für wiederkehrende Einkaufs- oder Planungsbausteine
|
||||||
|
- sanfte Hinweise und Vorschläge aus Zuhause, Archiv und bisherigen Planungen
|
||||||
|
- globale Kategorien pro Haushalt
|
||||||
|
- „Für wen?“ direkt an Lebensmitteln und Mahlzeiten
|
||||||
|
- Mobile-Mehr-Menü als Sheet statt eigener Seite
|
||||||
|
|
||||||
## Lokal starten
|
## Lokal starten
|
||||||
|
|
||||||
@@ -39,9 +45,9 @@ Wichtige Umgebungsvariablen:
|
|||||||
- `NOURI_DATA_DIR`: Pfad für Datenbank und Uploads, z. B. `/app/data` auf Cloudron
|
- `NOURI_DATA_DIR`: Pfad für Datenbank und Uploads, z. B. `/app/data` auf Cloudron
|
||||||
- `NOURI_MAX_UPLOAD_MB`: maximales Upload-Limit in MB, Standard `5`
|
- `NOURI_MAX_UPLOAD_MB`: maximales Upload-Limit in MB, Standard `5`
|
||||||
|
|
||||||
## Migration von 0.2 auf 0.3
|
## Migration von 0.3 auf 0.4
|
||||||
|
|
||||||
Beim Start erweitert Nouri das Schema pragmatisch direkt in SQLite: Haushalt, Rollen, Aktiv-Status und Sichtbarkeit (`persönlich` oder `Für alle`) werden ergänzt. Vorhandene 0.2-Daten bleiben erhalten und werden automatisch einem gemeinsamen Haushaltskontext zugeordnet.
|
Beim Start erweitert Nouri das Schema pragmatisch direkt in SQLite weiter: Vorlagen, kleine Pakete, Kategorien pro Haushalt, Zielnutzer an Lebensmitteln und zusätzliche Einkaufs-Kontexte werden ergänzt. Vorhandene 0.3-Daten bleiben erhalten und werden weiterverwendet.
|
||||||
|
|
||||||
## Cloudron-Hinweis
|
## Cloudron-Hinweis
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ from . import db
|
|||||||
from .admin import admin_bp
|
from .admin import admin_bp
|
||||||
from .auth import auth_bp
|
from .auth import auth_bp
|
||||||
from .constants import (
|
from .constants import (
|
||||||
CATEGORIES,
|
|
||||||
DAYPARTS,
|
DAYPARTS,
|
||||||
|
DEFAULT_CATEGORIES,
|
||||||
ITEM_KIND_LABELS,
|
ITEM_KIND_LABELS,
|
||||||
ITEM_KIND_SINGULAR_LABELS,
|
ITEM_KIND_SINGULAR_LABELS,
|
||||||
ROLE_LABELS,
|
ROLE_LABELS,
|
||||||
@@ -77,7 +77,7 @@ def create_app() -> Flask:
|
|||||||
return {
|
return {
|
||||||
"item_kind_labels": ITEM_KIND_LABELS,
|
"item_kind_labels": ITEM_KIND_LABELS,
|
||||||
"item_kind_singular_labels": ITEM_KIND_SINGULAR_LABELS,
|
"item_kind_singular_labels": ITEM_KIND_SINGULAR_LABELS,
|
||||||
"category_suggestions": CATEGORIES,
|
"category_suggestions": DEFAULT_CATEGORIES,
|
||||||
"daypart_suggestions": DAYPARTS,
|
"daypart_suggestions": DAYPARTS,
|
||||||
"visibility_labels": VISIBILITY_LABELS,
|
"visibility_labels": VISIBILITY_LABELS,
|
||||||
"visibility_descriptions": VISIBILITY_DESCRIPTIONS,
|
"visibility_descriptions": VISIBILITY_DESCRIPTIONS,
|
||||||
|
|||||||
+83
-1
@@ -4,7 +4,7 @@ from flask import Blueprint, flash, g, redirect, render_template, request, url_f
|
|||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from .auth import admin_required, can_remove_last_admin, validate_admin_user_form
|
from .auth import admin_required, can_remove_last_admin, validate_admin_user_form
|
||||||
from .constants import ROLE_LABELS
|
from .constants import DEFAULT_CATEGORIES, ROLE_LABELS
|
||||||
from .db import get_db
|
from .db import get_db
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +26,18 @@ def get_household_user(user_id: int):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_household_categories():
|
||||||
|
return get_db().execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM household_categories
|
||||||
|
WHERE household_id = ?
|
||||||
|
ORDER BY is_active DESC, sort_order, LOWER(name)
|
||||||
|
""",
|
||||||
|
(g.user["household_id"],),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.get("/users")
|
@admin_bp.get("/users")
|
||||||
@admin_required
|
@admin_required
|
||||||
def user_list():
|
def user_list():
|
||||||
@@ -178,3 +190,73 @@ def user_edit(user_id: int):
|
|||||||
flash(error, "error")
|
flash(error, "error")
|
||||||
|
|
||||||
return render_template("admin/user_form.html", user=user, form_data=form_data, role_labels=ROLE_LABELS)
|
return render_template("admin/user_form.html", user=user, form_data=form_data, role_labels=ROLE_LABELS)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/categories", methods=("GET", "POST"))
|
||||||
|
@admin_required
|
||||||
|
def category_settings():
|
||||||
|
if request.method == "POST":
|
||||||
|
name = request.form.get("name", "").strip()
|
||||||
|
if not name:
|
||||||
|
flash("Bitte einen Kategorienamen eintragen.", "error")
|
||||||
|
else:
|
||||||
|
existing = get_db().execute(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM household_categories
|
||||||
|
WHERE household_id = ? AND LOWER(name) = LOWER(?)
|
||||||
|
""",
|
||||||
|
(g.user["household_id"], name),
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
get_db().execute(
|
||||||
|
"UPDATE household_categories SET is_active = 1 WHERE id = ?",
|
||||||
|
(existing["id"],),
|
||||||
|
)
|
||||||
|
flash("Die Kategorie ist wieder aktiv.", "success")
|
||||||
|
else:
|
||||||
|
sort_row = get_db().execute(
|
||||||
|
"SELECT COALESCE(MAX(sort_order), 0) AS max_sort FROM household_categories WHERE household_id = ?",
|
||||||
|
(g.user["household_id"],),
|
||||||
|
).fetchone()
|
||||||
|
get_db().execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO household_categories (household_id, name, sort_order, is_active)
|
||||||
|
VALUES (?, ?, ?, 1)
|
||||||
|
""",
|
||||||
|
(g.user["household_id"], name, int(sort_row["max_sort"]) + 10),
|
||||||
|
)
|
||||||
|
flash("Die Kategorie wurde ergänzt.", "success")
|
||||||
|
get_db().commit()
|
||||||
|
return redirect(url_for("admin.category_settings"))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"admin/categories.html",
|
||||||
|
categories=fetch_household_categories(),
|
||||||
|
default_categories=DEFAULT_CATEGORIES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.post("/categories/<int:category_id>/toggle")
|
||||||
|
@admin_required
|
||||||
|
def category_toggle(category_id: int):
|
||||||
|
category = get_db().execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM household_categories
|
||||||
|
WHERE id = ? AND household_id = ?
|
||||||
|
""",
|
||||||
|
(category_id, g.user["household_id"]),
|
||||||
|
).fetchone()
|
||||||
|
if category is None:
|
||||||
|
flash("Die Kategorie wurde nicht gefunden.", "error")
|
||||||
|
return redirect(url_for("admin.category_settings"))
|
||||||
|
|
||||||
|
new_state = 0 if category["is_active"] else 1
|
||||||
|
get_db().execute(
|
||||||
|
"UPDATE household_categories SET is_active = ? WHERE id = ?",
|
||||||
|
(new_state, category_id),
|
||||||
|
)
|
||||||
|
get_db().commit()
|
||||||
|
flash("Die Kategorie wurde aktualisiert.", "success")
|
||||||
|
return redirect(url_for("admin.category_settings"))
|
||||||
|
|||||||
+25
-2
@@ -7,7 +7,7 @@ DAYPARTS = [
|
|||||||
{"slug": "late-snack", "name": "Später Snack", "sort_order": 60},
|
{"slug": "late-snack", "name": "Später Snack", "sort_order": 60},
|
||||||
]
|
]
|
||||||
|
|
||||||
CATEGORIES = [
|
DEFAULT_CATEGORIES = [
|
||||||
"Brot & Getreide",
|
"Brot & Getreide",
|
||||||
"Milchprodukt",
|
"Milchprodukt",
|
||||||
"Obst",
|
"Obst",
|
||||||
@@ -42,7 +42,7 @@ ROLE_LABELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VISIBILITY_LABELS = {
|
VISIBILITY_LABELS = {
|
||||||
"shared": "Für alle",
|
"shared": "Gemeinsam",
|
||||||
"personal": "Persönlich",
|
"personal": "Persönlich",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,3 +50,26 @@ VISIBILITY_DESCRIPTIONS = {
|
|||||||
"shared": "Gemeinsam im Haushalt sichtbar und nutzbar.",
|
"shared": "Gemeinsam im Haushalt sichtbar und nutzbar.",
|
||||||
"personal": "Nur für dich sichtbar und planbar.",
|
"personal": "Nur für dich sichtbar und planbar.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DAY_TEMPLATE_NAME_SUGGESTIONS = [
|
||||||
|
"Ruhiger Tag",
|
||||||
|
"Einfacher Bürotag",
|
||||||
|
"Schwieriger Tag",
|
||||||
|
"Standard-Frühstückstag",
|
||||||
|
"Tag mit wenig Energie",
|
||||||
|
]
|
||||||
|
|
||||||
|
WEEK_TEMPLATE_NAME_SUGGESTIONS = [
|
||||||
|
"Standardwoche",
|
||||||
|
"Büro-Woche",
|
||||||
|
"Leichte Woche",
|
||||||
|
"Woche mit wenig Energie",
|
||||||
|
"Frühstücks-Woche",
|
||||||
|
]
|
||||||
|
|
||||||
|
ITEM_SET_NAME_SUGGESTIONS = [
|
||||||
|
"Schnelles Frühstück",
|
||||||
|
"Sicherer Snack",
|
||||||
|
"Einfaches Abendessen",
|
||||||
|
"Einkauf für zwei Tage",
|
||||||
|
]
|
||||||
|
|||||||
+65
-39
@@ -8,7 +8,7 @@ from flask import Flask, current_app, g
|
|||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from .constants import DAYPARTS
|
from .constants import DAYPARTS, DEFAULT_CATEGORIES
|
||||||
|
|
||||||
|
|
||||||
def get_db() -> sqlite3.Connection:
|
def get_db() -> sqlite3.Connection:
|
||||||
@@ -33,6 +33,14 @@ def table_columns(database: sqlite3.Connection, table_name: str) -> set[str]:
|
|||||||
return {row["name"] for row in rows}
|
return {row["name"] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def table_exists(database: sqlite3.Connection, table_name: str) -> bool:
|
||||||
|
row = database.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
|
||||||
|
(table_name,),
|
||||||
|
).fetchone()
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
def add_column_if_missing(database: sqlite3.Connection, table_name: str, definition: str) -> None:
|
def add_column_if_missing(database: sqlite3.Connection, table_name: str, definition: str) -> None:
|
||||||
column_name = definition.split()[0]
|
column_name = definition.split()[0]
|
||||||
if column_name not in table_columns(database, table_name):
|
if column_name not in table_columns(database, table_name):
|
||||||
@@ -50,31 +58,41 @@ def bootstrap_legacy_schema(database: sqlite3.Connection) -> None:
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_tables = {
|
database.execute(
|
||||||
row["name"]
|
"""
|
||||||
for row in database.execute(
|
CREATE TABLE IF NOT EXISTS household_categories (
|
||||||
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
).fetchall()
|
household_id INTEGER NOT NULL,
|
||||||
}
|
name TEXT NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 100,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (household_id, name)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
if "users" in existing_tables:
|
if table_exists(database, "users"):
|
||||||
add_column_if_missing(database, "users", "household_id INTEGER")
|
add_column_if_missing(database, "users", "household_id INTEGER")
|
||||||
add_column_if_missing(database, "users", "email TEXT")
|
add_column_if_missing(database, "users", "email TEXT")
|
||||||
add_column_if_missing(database, "users", "role TEXT NOT NULL DEFAULT 'member'")
|
add_column_if_missing(database, "users", "role TEXT NOT NULL DEFAULT 'member'")
|
||||||
add_column_if_missing(database, "users", "is_active INTEGER NOT NULL DEFAULT 1")
|
add_column_if_missing(database, "users", "is_active INTEGER NOT NULL DEFAULT 1")
|
||||||
add_column_if_missing(database, "users", "updated_at TEXT")
|
add_column_if_missing(database, "users", "updated_at TEXT")
|
||||||
|
|
||||||
if "items" in existing_tables:
|
if table_exists(database, "items"):
|
||||||
add_column_if_missing(database, "items", "household_id INTEGER")
|
add_column_if_missing(database, "items", "household_id INTEGER")
|
||||||
add_column_if_missing(database, "items", "owner_user_id INTEGER")
|
add_column_if_missing(database, "items", "owner_user_id INTEGER")
|
||||||
|
add_column_if_missing(database, "items", "target_user_id INTEGER")
|
||||||
add_column_if_missing(database, "items", "visibility TEXT NOT NULL DEFAULT 'shared'")
|
add_column_if_missing(database, "items", "visibility TEXT NOT NULL DEFAULT 'shared'")
|
||||||
|
|
||||||
if "shopping_entries" in existing_tables:
|
if table_exists(database, "shopping_entries"):
|
||||||
add_column_if_missing(database, "shopping_entries", "household_id INTEGER")
|
add_column_if_missing(database, "shopping_entries", "household_id INTEGER")
|
||||||
add_column_if_missing(database, "shopping_entries", "owner_user_id INTEGER")
|
add_column_if_missing(database, "shopping_entries", "owner_user_id INTEGER")
|
||||||
add_column_if_missing(database, "shopping_entries", "visibility TEXT NOT NULL DEFAULT 'shared'")
|
add_column_if_missing(database, "shopping_entries", "visibility TEXT NOT NULL DEFAULT 'shared'")
|
||||||
|
add_column_if_missing(database, "shopping_entries", "needed_for_date TEXT")
|
||||||
|
add_column_if_missing(database, "shopping_entries", "needed_for_daypart_id INTEGER")
|
||||||
|
|
||||||
if "plan_entries" in existing_tables:
|
if table_exists(database, "plan_entries"):
|
||||||
add_column_if_missing(database, "plan_entries", "household_id INTEGER")
|
add_column_if_missing(database, "plan_entries", "household_id INTEGER")
|
||||||
add_column_if_missing(database, "plan_entries", "owner_user_id INTEGER")
|
add_column_if_missing(database, "plan_entries", "owner_user_id INTEGER")
|
||||||
add_column_if_missing(database, "plan_entries", "visibility TEXT NOT NULL DEFAULT 'shared'")
|
add_column_if_missing(database, "plan_entries", "visibility TEXT NOT NULL DEFAULT 'shared'")
|
||||||
@@ -91,9 +109,11 @@ def ensure_default_household(database: sqlite3.Connection) -> int:
|
|||||||
"INSERT INTO households (name) VALUES (?)",
|
"INSERT INTO households (name) VALUES (?)",
|
||||||
("Unser Haushalt",),
|
("Unser Haushalt",),
|
||||||
)
|
)
|
||||||
return int(
|
return int(database.execute("SELECT id FROM households ORDER BY id LIMIT 1").fetchone()["id"])
|
||||||
database.execute("SELECT id FROM households ORDER BY id LIMIT 1").fetchone()["id"]
|
|
||||||
)
|
|
||||||
|
def household_ids(database: sqlite3.Connection) -> list[int]:
|
||||||
|
return [int(row["id"]) for row in database.execute("SELECT id FROM households ORDER BY id").fetchall()]
|
||||||
|
|
||||||
|
|
||||||
def first_user_id(database: sqlite3.Connection) -> int | None:
|
def first_user_id(database: sqlite3.Connection) -> int | None:
|
||||||
@@ -101,6 +121,18 @@ def first_user_id(database: sqlite3.Connection) -> int | None:
|
|||||||
return int(row["id"]) if row else None
|
return int(row["id"]) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def sync_default_categories(database: sqlite3.Connection) -> None:
|
||||||
|
for household_id in household_ids(database):
|
||||||
|
for sort_order, name in enumerate(DEFAULT_CATEGORIES, start=10):
|
||||||
|
database.execute(
|
||||||
|
"""
|
||||||
|
INSERT OR IGNORE INTO household_categories (household_id, name, sort_order, is_active)
|
||||||
|
VALUES (?, ?, ?, 1)
|
||||||
|
""",
|
||||||
|
(household_id, name, sort_order),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
||||||
add_column_if_missing(database, "users", "household_id INTEGER")
|
add_column_if_missing(database, "users", "household_id INTEGER")
|
||||||
add_column_if_missing(database, "users", "email TEXT")
|
add_column_if_missing(database, "users", "email TEXT")
|
||||||
@@ -113,18 +145,10 @@ def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
|||||||
"UPDATE users SET household_id = ? WHERE household_id IS NULL",
|
"UPDATE users SET household_id = ? WHERE household_id IS NULL",
|
||||||
(default_household_id,),
|
(default_household_id,),
|
||||||
)
|
)
|
||||||
database.execute(
|
database.execute("UPDATE users SET role = 'member' WHERE role IS NULL OR role = ''")
|
||||||
"UPDATE users SET role = 'member' WHERE role IS NULL OR role = ''",
|
database.execute("UPDATE users SET is_active = 1 WHERE is_active IS NULL")
|
||||||
)
|
database.execute("UPDATE users SET email = NULL WHERE TRIM(COALESCE(email, '')) = ''")
|
||||||
database.execute(
|
database.execute("UPDATE users SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP)")
|
||||||
"UPDATE users SET is_active = 1 WHERE is_active IS NULL",
|
|
||||||
)
|
|
||||||
database.execute(
|
|
||||||
"UPDATE users SET email = NULL WHERE TRIM(COALESCE(email, '')) = ''",
|
|
||||||
)
|
|
||||||
database.execute(
|
|
||||||
"UPDATE users SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP)"
|
|
||||||
)
|
|
||||||
|
|
||||||
admin_row = database.execute(
|
admin_row = database.execute(
|
||||||
"SELECT id FROM users WHERE role = 'admin' AND is_active = 1 ORDER BY id LIMIT 1"
|
"SELECT id FROM users WHERE role = 'admin' AND is_active = 1 ORDER BY id LIMIT 1"
|
||||||
@@ -132,16 +156,16 @@ def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
|||||||
if admin_row is None:
|
if admin_row is None:
|
||||||
first_id = first_user_id(database)
|
first_id = first_user_id(database)
|
||||||
if first_id is not None:
|
if first_id is not None:
|
||||||
database.execute(
|
database.execute("UPDATE users SET role = 'admin' WHERE id = ?", (first_id,))
|
||||||
"UPDATE users SET role = 'admin' WHERE id = ?",
|
|
||||||
(first_id,),
|
|
||||||
)
|
|
||||||
|
|
||||||
default_owner_id = first_user_id(database)
|
default_owner_id = first_user_id(database)
|
||||||
for table_name in ("items", "shopping_entries", "plan_entries"):
|
for table_name in ("items", "shopping_entries", "plan_entries"):
|
||||||
add_column_if_missing(database, table_name, "household_id INTEGER")
|
add_column_if_missing(database, table_name, "household_id INTEGER")
|
||||||
add_column_if_missing(database, table_name, "owner_user_id INTEGER")
|
add_column_if_missing(database, table_name, "owner_user_id INTEGER")
|
||||||
add_column_if_missing(database, table_name, "visibility TEXT NOT NULL DEFAULT 'shared'")
|
add_column_if_missing(database, table_name, "visibility TEXT NOT NULL DEFAULT 'shared'")
|
||||||
|
add_column_if_missing(database, "items", "target_user_id INTEGER")
|
||||||
|
add_column_if_missing(database, "shopping_entries", "needed_for_date TEXT")
|
||||||
|
add_column_if_missing(database, "shopping_entries", "needed_for_daypart_id INTEGER")
|
||||||
|
|
||||||
if default_owner_id is not None:
|
if default_owner_id is not None:
|
||||||
database.execute(
|
database.execute(
|
||||||
@@ -175,15 +199,11 @@ def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
|||||||
(default_household_id, default_owner_id),
|
(default_household_id, default_owner_id),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
database.execute(
|
database.execute("UPDATE items SET visibility = 'shared' WHERE visibility IS NULL OR visibility = ''")
|
||||||
"UPDATE items SET visibility = 'shared' WHERE visibility IS NULL OR visibility = ''"
|
database.execute("UPDATE shopping_entries SET visibility = 'shared' WHERE visibility IS NULL OR visibility = ''")
|
||||||
)
|
database.execute("UPDATE plan_entries SET visibility = 'shared' WHERE visibility IS NULL OR visibility = ''")
|
||||||
database.execute(
|
|
||||||
"UPDATE shopping_entries SET visibility = 'shared' WHERE visibility IS NULL OR visibility = ''"
|
sync_default_categories(database)
|
||||||
)
|
|
||||||
database.execute(
|
|
||||||
"UPDATE plan_entries SET visibility = 'shared' WHERE visibility IS NULL OR visibility = ''"
|
|
||||||
)
|
|
||||||
|
|
||||||
database.execute(
|
database.execute(
|
||||||
"""
|
"""
|
||||||
@@ -198,6 +218,12 @@ def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
|||||||
ON items (household_id, visibility, availability_state)
|
ON items (household_id, visibility, availability_state)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
database.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_items_target_user
|
||||||
|
ON items (target_user_id)
|
||||||
|
"""
|
||||||
|
)
|
||||||
database.execute(
|
database.execute(
|
||||||
"""
|
"""
|
||||||
CREATE INDEX IF NOT EXISTS idx_plan_entries_household_visibility
|
CREATE INDEX IF NOT EXISTS idx_plan_entries_household_visibility
|
||||||
|
|||||||
+1376
-320
File diff suppressed because it is too large
Load Diff
+102
-1
@@ -24,6 +24,17 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_unique
|
|||||||
ON users (email)
|
ON users (email)
|
||||||
WHERE email IS NOT NULL AND email != '';
|
WHERE email IS NOT NULL AND email != '';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS household_categories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
household_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 100,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (household_id, name),
|
||||||
|
FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS dayparts (
|
CREATE TABLE IF NOT EXISTS dayparts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
slug TEXT NOT NULL UNIQUE,
|
slug TEXT NOT NULL UNIQUE,
|
||||||
@@ -35,6 +46,7 @@ CREATE TABLE IF NOT EXISTS items (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
household_id INTEGER,
|
household_id INTEGER,
|
||||||
owner_user_id INTEGER,
|
owner_user_id INTEGER,
|
||||||
|
target_user_id INTEGER,
|
||||||
visibility TEXT NOT NULL DEFAULT 'shared',
|
visibility TEXT NOT NULL DEFAULT 'shared',
|
||||||
kind TEXT NOT NULL CHECK (kind IN ('food', 'meal')),
|
kind TEXT NOT NULL CHECK (kind IN ('food', 'meal')),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@@ -48,6 +60,7 @@ CREATE TABLE IF NOT EXISTS items (
|
|||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE,
|
FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (target_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
@@ -76,6 +89,8 @@ CREATE TABLE IF NOT EXISTS shopping_entries (
|
|||||||
item_id INTEGER NOT NULL,
|
item_id INTEGER NOT NULL,
|
||||||
added_by INTEGER,
|
added_by INTEGER,
|
||||||
checked_by INTEGER,
|
checked_by INTEGER,
|
||||||
|
needed_for_date TEXT,
|
||||||
|
needed_for_daypart_id INTEGER,
|
||||||
is_checked INTEGER NOT NULL DEFAULT 0,
|
is_checked INTEGER NOT NULL DEFAULT 0,
|
||||||
added_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
added_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
checked_at TEXT,
|
checked_at TEXT,
|
||||||
@@ -83,7 +98,8 @@ CREATE TABLE IF NOT EXISTS shopping_entries (
|
|||||||
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
|
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (added_by) REFERENCES users(id) ON DELETE SET NULL,
|
FOREIGN KEY (added_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (checked_by) REFERENCES users(id) ON DELETE SET NULL
|
FOREIGN KEY (checked_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (needed_for_daypart_id) REFERENCES dayparts(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_shopping_entries_open_item
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_shopping_entries_open_item
|
||||||
@@ -108,12 +124,88 @@ CREATE TABLE IF NOT EXISTS plan_entries (
|
|||||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS day_templates (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
household_id INTEGER NOT NULL,
|
||||||
|
owner_user_id INTEGER,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'shared',
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
last_used_at TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS day_template_entries (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
day_template_id INTEGER NOT NULL,
|
||||||
|
daypart_id INTEGER NOT NULL,
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 100,
|
||||||
|
FOREIGN KEY (day_template_id) REFERENCES day_templates(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (daypart_id) REFERENCES dayparts(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS week_templates (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
household_id INTEGER NOT NULL,
|
||||||
|
owner_user_id INTEGER,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'shared',
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
last_used_at TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS week_template_days (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
week_template_id INTEGER NOT NULL,
|
||||||
|
weekday_index INTEGER NOT NULL,
|
||||||
|
day_template_id INTEGER NOT NULL,
|
||||||
|
UNIQUE (week_template_id, weekday_index),
|
||||||
|
FOREIGN KEY (week_template_id) REFERENCES week_templates(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (day_template_id) REFERENCES day_templates(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS item_sets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
household_id INTEGER NOT NULL,
|
||||||
|
owner_user_id INTEGER,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'shared',
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
last_used_at TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS item_set_items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
item_set_id INTEGER NOT NULL,
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 100,
|
||||||
|
UNIQUE (item_set_id, item_id),
|
||||||
|
FOREIGN KEY (item_set_id) REFERENCES item_sets(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_items_kind_name
|
CREATE INDEX IF NOT EXISTS idx_items_kind_name
|
||||||
ON items (kind, name);
|
ON items (kind, name);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_items_household_visibility
|
CREATE INDEX IF NOT EXISTS idx_items_household_visibility
|
||||||
ON items (household_id, visibility, availability_state);
|
ON items (household_id, visibility, availability_state);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_items_target_user
|
||||||
|
ON items (target_user_id);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_item_dayparts_daypart_item
|
CREATE INDEX IF NOT EXISTS idx_item_dayparts_daypart_item
|
||||||
ON item_dayparts (daypart_id, item_id);
|
ON item_dayparts (daypart_id, item_id);
|
||||||
|
|
||||||
@@ -125,3 +217,12 @@ ON plan_entries (household_id, visibility, plan_date);
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_shopping_entries_household_visibility
|
CREATE INDEX IF NOT EXISTS idx_shopping_entries_household_visibility
|
||||||
ON shopping_entries (household_id, visibility, is_checked);
|
ON shopping_entries (household_id, visibility, is_checked);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_day_templates_household_visibility
|
||||||
|
ON day_templates (household_id, visibility, name);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_week_templates_household_visibility
|
||||||
|
ON week_templates (household_id, visibility, name);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_item_sets_household_visibility
|
||||||
|
ON item_sets (household_id, visibility, name);
|
||||||
|
|||||||
+192
-2
@@ -67,6 +67,10 @@ body.has-mobile-nav {
|
|||||||
padding-bottom: 6rem;
|
padding-bottom: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.sheet-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -256,6 +260,11 @@ h3,
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-backdrop[hidden],
|
||||||
|
.mobile-more-sheet[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1.2rem;
|
gap: 1.2rem;
|
||||||
@@ -576,6 +585,12 @@ h3 {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-library-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.stack-form label,
|
.stack-form label,
|
||||||
.planner-entry-form label,
|
.planner-entry-form label,
|
||||||
.filter-form label,
|
.filter-form label,
|
||||||
@@ -640,6 +655,37 @@ legend {
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.check-option[hidden],
|
||||||
|
.quick-select-card[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-select-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.7rem;
|
||||||
|
padding: 0.85rem 0.95rem;
|
||||||
|
min-width: 220px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: color-mix(in srgb, var(--surface-strong) 82%, #fff 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-select-card strong,
|
||||||
|
.template-list-card strong {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-select-card small,
|
||||||
|
.template-list-card p,
|
||||||
|
.template-list-card small,
|
||||||
|
.hint-chip,
|
||||||
|
.suggestion-card p,
|
||||||
|
.suggestion-card small,
|
||||||
|
.week-template-row p {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.inline-photo img {
|
.inline-photo img {
|
||||||
width: min(220px, 100%);
|
width: min(220px, 100%);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -739,6 +785,10 @@ legend {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-search-row {
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.quick-add-row form {
|
.quick-add-row form {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -769,6 +819,49 @@ legend {
|
|||||||
background: color-mix(in srgb, var(--surface) 88%, #fff 12%);
|
background: color-mix(in srgb, var(--surface) 88%, #fff 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-card,
|
||||||
|
.template-list-card,
|
||||||
|
.suggestion-card {
|
||||||
|
padding: 0.95rem 1rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: color-mix(in srgb, var(--surface-strong) 84%, #fff 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-list-card,
|
||||||
|
.week-template-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-template-row {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: rgba(255, 255, 255, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-chip {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: color-mix(in srgb, var(--surface-strong) 80%, #fff 20%);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.planner-entry-top {
|
.planner-entry-top {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
@@ -915,6 +1008,71 @@ legend {
|
|||||||
mask-image: url("../icons/fa/sparkles.svg");
|
mask-image: url("../icons/fa/sparkles.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-layer-group {
|
||||||
|
-webkit-mask-image: url("../icons/fa/layer-group.svg");
|
||||||
|
mask-image: url("../icons/fa/layer-group.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ellipsis {
|
||||||
|
-webkit-mask-image: url("../icons/fa/ellipsis.svg");
|
||||||
|
mask-image: url("../icons/fa/ellipsis.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 24;
|
||||||
|
background: rgba(33, 29, 28, 0.22);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-more-sheet {
|
||||||
|
position: fixed;
|
||||||
|
left: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
bottom: 5.9rem;
|
||||||
|
z-index: 25;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
backdrop-filter: blur(24px) saturate(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-head,
|
||||||
|
.mobile-sheet-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-head small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-links {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-links a {
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: color-mix(in srgb, var(--surface-strong) 86%, #fff 14%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-actions > * {
|
||||||
|
flex: 1 1 180px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
@media (max-width: 1080px) {
|
||||||
.site-header,
|
.site-header,
|
||||||
.hero,
|
.hero,
|
||||||
@@ -932,6 +1090,7 @@ legend {
|
|||||||
|
|
||||||
.stats-grid,
|
.stats-grid,
|
||||||
.two-column,
|
.two-column,
|
||||||
|
.template-library-grid,
|
||||||
.inline-form,
|
.inline-form,
|
||||||
.planner-entry-form,
|
.planner-entry-form,
|
||||||
.planner-entry-form-wide,
|
.planner-entry-form-wide,
|
||||||
@@ -1014,7 +1173,8 @@ legend {
|
|||||||
.mini-card-grid,
|
.mini-card-grid,
|
||||||
.week-mini-grid,
|
.week-mini-grid,
|
||||||
.week-overview-grid,
|
.week-overview-grid,
|
||||||
.more-link-grid {
|
.more-link-grid,
|
||||||
|
.template-library-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,7 +1239,20 @@ legend {
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-bottom-nav a.active {
|
.mobile-nav-button {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 0.28rem;
|
||||||
|
padding: 0.55rem 0.35rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-bottom-nav a.active,
|
||||||
|
.mobile-nav-button.is-open {
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@@ -1088,4 +1261,21 @@ legend {
|
|||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-profile-link {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0.35rem;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-profile-link .mobile-profile-avatar {
|
||||||
|
width: 2.15rem;
|
||||||
|
height: 2.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sheet-head,
|
||||||
|
.mobile-sheet-actions,
|
||||||
|
.week-template-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<circle cx="5" cy="12" r="2"/>
|
||||||
|
<circle cx="12" cy="12" r="2"/>
|
||||||
|
<circle cx="19" cy="12" r="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2.6 2 8l10 5.4L22 8 12 2.6Zm-7.9 8.6L2 12.4l10 5.4 10-5.4-2.1-1.2L12 15.4 4.1 11.2Zm0 4.4L2 16.8l10 5.4 10-5.4-2.1-1.2L12 19.8 4.1 15.6Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1,67 @@
|
|||||||
|
(() => {
|
||||||
|
const initMobileSheet = () => {
|
||||||
|
const sheet = document.querySelector("[data-mobile-sheet]");
|
||||||
|
const backdrop = document.querySelector("[data-mobile-sheet-backdrop]");
|
||||||
|
const openButtons = document.querySelectorAll("[data-mobile-sheet-open]");
|
||||||
|
const closeButtons = document.querySelectorAll("[data-mobile-sheet-close]");
|
||||||
|
if (!sheet || !backdrop || !openButtons.length) return;
|
||||||
|
|
||||||
|
const closeSheet = () => {
|
||||||
|
sheet.hidden = true;
|
||||||
|
backdrop.hidden = true;
|
||||||
|
document.body.classList.remove("sheet-open");
|
||||||
|
openButtons.forEach((button) => button.classList.remove("is-open"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSheet = () => {
|
||||||
|
sheet.hidden = false;
|
||||||
|
backdrop.hidden = false;
|
||||||
|
document.body.classList.add("sheet-open");
|
||||||
|
openButtons.forEach((button) => button.classList.add("is-open"));
|
||||||
|
};
|
||||||
|
|
||||||
|
openButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", openSheet);
|
||||||
|
});
|
||||||
|
closeButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", closeSheet);
|
||||||
|
});
|
||||||
|
backdrop.addEventListener("click", closeSheet);
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeSheet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sheet.querySelectorAll("a").forEach((link) => {
|
||||||
|
link.addEventListener("click", closeSheet);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initFilterInputs = () => {
|
||||||
|
document.querySelectorAll("[data-filter-input]").forEach((input) => {
|
||||||
|
const listSelector = input.getAttribute("data-filter-target");
|
||||||
|
if (!listSelector) return;
|
||||||
|
const container = document.querySelector(listSelector);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const items = Array.from(container.querySelectorAll("[data-filter-label]"));
|
||||||
|
const applyFilter = () => {
|
||||||
|
const term = input.value.trim().toLowerCase();
|
||||||
|
items.forEach((item) => {
|
||||||
|
const haystack = (item.getAttribute("data-filter-label") || "").toLowerCase();
|
||||||
|
item.hidden = Boolean(term) && !haystack.includes(term);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener("input", applyFilter);
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
initMobileSheet();
|
||||||
|
initFilterInputs();
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Kategorien | Nouri{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="page-intro">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Kategorien</p>
|
||||||
|
<h1>Kategorien global anpassen</h1>
|
||||||
|
<p class="lead">Hier pflegt ihr die Auswahl für Lebensmittel und Mahlzeiten. Bestehende Einträge bleiben auch dann erhalten, wenn eine Kategorie später pausiert wird.</p>
|
||||||
|
</div>
|
||||||
|
<a class="ghost-button" href="{{ url_for('admin.user_list') }}">Zur Nutzerverwaltung</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel compact-form-panel">
|
||||||
|
<form method="post" class="inline-form">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<label class="wide">
|
||||||
|
Neue Kategorie
|
||||||
|
<input type="text" name="name" placeholder="z. B. Süßes, Vorrat, Unterwegs">
|
||||||
|
</label>
|
||||||
|
<button type="submit">Kategorie ergänzen</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="stack-list">
|
||||||
|
{% for category in categories %}
|
||||||
|
<article class="list-row stacked-mobile">
|
||||||
|
<div>
|
||||||
|
<strong>{{ category.name }}</strong>
|
||||||
|
<p class="muted">{% if category.name in default_categories %}Teil der ruhigen Standardauswahl{% else %}Eigene Haushaltskategorie{% endif %}</p>
|
||||||
|
<div class="chip-row">
|
||||||
|
{% if category.is_active %}
|
||||||
|
<span class="chip status-home">Aktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="chip status-archived">Pausiert</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-actions">
|
||||||
|
<form method="post" action="{{ url_for('admin.category_toggle', category_id=category.id) }}">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<button class="ghost-button" type="submit">
|
||||||
|
{% if category.is_active %}Pausieren{% else %}Wieder aktivieren{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,9 +5,12 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Nutzer verwalten</p>
|
<p class="eyebrow">Nutzer verwalten</p>
|
||||||
<h1>Haushaltszugänge ruhig pflegen</h1>
|
<h1>Haushaltszugänge ruhig pflegen</h1>
|
||||||
<p class="lead">Admins können hier weitere Mitglieder anlegen, Rollen anpassen und Zugänge bei Bedarf pausieren.</p>
|
<p class="lead">Admins können hier weitere Mitglieder anlegen, Rollen anpassen, Zugänge pausieren und die gemeinsamen Kategorien pflegen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a class="button" href="{{ url_for('admin.user_create') }}">Neuen Nutzer anlegen</a>
|
||||||
|
<a class="button secondary" href="{{ url_for('admin.category_settings') }}">Kategorien</a>
|
||||||
</div>
|
</div>
|
||||||
<a class="button" href="{{ url_for('admin.user_create') }}">Neuen Nutzer anlegen</a>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="stack-list">
|
<section class="stack-list">
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
<span class="chip">{{ item.visibility_label }}</span>
|
<span class="chip">{{ item.visibility_label }}</span>
|
||||||
<span class="chip status-soft">{{ item.owner_label }}</span>
|
<span class="chip status-soft">{{ item.owner_label }}</span>
|
||||||
|
<span class="chip">{{ item.for_label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">{{ item_kind_labels[item.kind] }}{% if item.category %} · {{ item.category }}{% endif %}</p>
|
<p class="muted">{{ item_kind_labels[item.kind] }}{% if item.category %} · {{ item.category }}{% endif %}</p>
|
||||||
{% if item.dayparts %}
|
{% if item.dayparts %}
|
||||||
|
|||||||
+44
-12
@@ -9,6 +9,7 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
<script defer src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/planner.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/planner.js') }}"></script>
|
||||||
|
<script defer src="{{ url_for('static', filename='js/ui.js') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="{% if g.user %}has-mobile-nav{% endif %}">
|
<body class="{% if g.user %}has-mobile-nav{% endif %}">
|
||||||
<div class="page-shell">
|
<div class="page-shell">
|
||||||
@@ -27,11 +28,12 @@
|
|||||||
<nav class="site-nav desktop-nav">
|
<nav class="site-nav desktop-nav">
|
||||||
<a href="{{ url_for('main.dashboard') }}" class="{{ 'active' if request.endpoint == 'main.dashboard' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-sparkles"></span><span>Heute</span></span></a>
|
<a href="{{ url_for('main.dashboard') }}" class="{{ 'active' if request.endpoint == 'main.dashboard' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-sparkles"></span><span>Heute</span></span></a>
|
||||||
<a href="{{ url_for('main.shopping_list') }}" class="{{ 'active' if request.endpoint == 'main.shopping_list' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-cart-shopping"></span><span>Einkauf</span></span></a>
|
<a href="{{ url_for('main.shopping_list') }}" class="{{ 'active' if request.endpoint == 'main.shopping_list' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-cart-shopping"></span><span>Einkauf</span></span></a>
|
||||||
<a href="{{ url_for('main.home_view') }}" class="{{ 'active' if request.endpoint == 'main.home_view' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-house"></span><span>Zuhause</span></span></a>
|
|
||||||
<a href="{{ url_for('main.planner_day', date=today.isoformat()) }}" class="{{ 'active' if request.endpoint == 'main.planner_day' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-calendar"></span><span>Plan</span></span></a>
|
<a href="{{ url_for('main.planner_day', date=today.isoformat()) }}" class="{{ 'active' if request.endpoint == 'main.planner_day' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-calendar"></span><span>Plan</span></span></a>
|
||||||
|
<a href="{{ url_for('main.planner') }}" class="{{ 'active' if request.endpoint == 'main.planner' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-calendar-days"></span><span>Woche</span></span></a>
|
||||||
|
<a href="{{ url_for('main.home_view') }}" class="{{ 'active' if request.endpoint == 'main.home_view' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-house"></span><span>Zuhause</span></span></a>
|
||||||
<a href="{{ url_for('main.item_list', kind='food') }}" class="{{ 'active' if request.endpoint == 'main.item_list' and request.view_args and request.view_args.get('kind') == 'food' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-utensils"></span><span>Lebensmittel</span></span></a>
|
<a href="{{ url_for('main.item_list', kind='food') }}" class="{{ 'active' if request.endpoint == 'main.item_list' and request.view_args and request.view_args.get('kind') == 'food' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-utensils"></span><span>Lebensmittel</span></span></a>
|
||||||
<a href="{{ url_for('main.item_list', kind='meal') }}" class="{{ 'active' if request.endpoint == 'main.item_list' and request.view_args and request.view_args.get('kind') == 'meal' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-bowl-food"></span><span>Mahlzeiten</span></span></a>
|
<a href="{{ url_for('main.item_list', kind='meal') }}" class="{{ 'active' if request.endpoint == 'main.item_list' and request.view_args and request.view_args.get('kind') == 'meal' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-bowl-food"></span><span>Mahlzeiten</span></span></a>
|
||||||
<a href="{{ url_for('main.planner') }}" class="{{ 'active' if request.endpoint == 'main.planner' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-calendar-days"></span><span>Woche</span></span></a>
|
<a href="{{ url_for('main.template_library') }}" class="{{ 'active' if (request.endpoint or '').startswith('main.day_template') or (request.endpoint or '').startswith('main.week_template') or (request.endpoint or '').startswith('main.item_set') or request.endpoint == 'main.template_library' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-layer-group"></span><span>Vorlagen</span></span></a>
|
||||||
<a href="{{ url_for('main.archive_view') }}" class="{{ 'active' if request.endpoint == 'main.archive_view' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-archive"></span><span>Archiv</span></span></a>
|
<a href="{{ url_for('main.archive_view') }}" class="{{ 'active' if request.endpoint == 'main.archive_view' else '' }}"><span class="nav-link-inner"><span class="ui-icon icon-archive"></span><span>Archiv</span></span></a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
<small>{{ role_labels[g.user.role] }}</small>
|
<small>{{ role_labels[g.user.role] }}</small>
|
||||||
</a>
|
</a>
|
||||||
{% if g.user.role == 'admin' %}
|
{% if g.user.role == 'admin' %}
|
||||||
<a class="ghost-button" href="{{ url_for('admin.user_list') }}">Nutzer verwalten</a>
|
<a class="ghost-button" href="{{ url_for('admin.user_list') }}">Nutzer</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="{{ url_for('auth.logout') }}">
|
<form method="post" action="{{ url_for('auth.logout') }}">
|
||||||
{{ csrf_input() }}
|
{{ csrf_input() }}
|
||||||
@@ -50,9 +52,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="mobile-profile-link" href="{{ url_for('main.more_view') }}" aria-label="Mehr">
|
<button class="mobile-profile-link ghost-button" type="button" data-mobile-sheet-open aria-label="Mehr öffnen">
|
||||||
<span class="mobile-profile-avatar">{{ (g.user.display_name or g.user.username)[0]|upper }}</span>
|
<span class="mobile-profile-avatar">{{ (g.user.display_name or g.user.username or 'N')[:1]|upper }}</span>
|
||||||
</a>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -72,6 +74,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if g.user %}
|
{% if g.user %}
|
||||||
|
<div class="mobile-sheet-backdrop" data-mobile-sheet-backdrop hidden></div>
|
||||||
|
<aside class="mobile-more-sheet" data-mobile-sheet hidden aria-label="Mehr">
|
||||||
|
<div class="mobile-sheet-head">
|
||||||
|
<div>
|
||||||
|
<strong>{{ g.user.display_name or g.user.username }}</strong>
|
||||||
|
<small>{{ role_labels[g.user.role] }}</small>
|
||||||
|
</div>
|
||||||
|
<button class="ghost-button" type="button" data-mobile-sheet-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
<nav class="mobile-sheet-links">
|
||||||
|
<a href="{{ url_for('main.item_list', kind='food') }}">Lebensmittel</a>
|
||||||
|
<a href="{{ url_for('main.item_list', kind='meal') }}">Mahlzeiten</a>
|
||||||
|
<a href="{{ url_for('main.home_view') }}">Zuhause</a>
|
||||||
|
<a href="{{ url_for('main.archive_view') }}">Archiv</a>
|
||||||
|
<a href="{{ url_for('main.template_library') }}">Vorlagen</a>
|
||||||
|
<a href="{{ url_for('auth.profile') }}">Profil</a>
|
||||||
|
{% if g.user.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('admin.user_list') }}">Nutzerverwaltung</a>
|
||||||
|
<a href="{{ url_for('admin.category_settings') }}">Kategorien</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
<div class="mobile-sheet-actions">
|
||||||
|
<button class="ghost-button" type="button" data-theme-toggle>Modus wechseln</button>
|
||||||
|
<form method="post" action="{{ url_for('auth.logout') }}">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<button class="ghost-button" type="submit">Abmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<nav class="mobile-bottom-nav" aria-label="Mobile Navigation">
|
<nav class="mobile-bottom-nav" aria-label="Mobile Navigation">
|
||||||
<a href="{{ url_for('main.dashboard') }}" class="{{ 'active' if request.endpoint == 'main.dashboard' else '' }}">
|
<a href="{{ url_for('main.dashboard') }}" class="{{ 'active' if request.endpoint == 'main.dashboard' else '' }}">
|
||||||
<span class="ui-icon icon-sparkles"></span>
|
<span class="ui-icon icon-sparkles"></span>
|
||||||
@@ -85,14 +117,14 @@
|
|||||||
<span class="ui-icon icon-calendar"></span>
|
<span class="ui-icon icon-calendar"></span>
|
||||||
<span>Plan</span>
|
<span>Plan</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('main.home_view') }}" class="{{ 'active' if request.endpoint == 'main.home_view' else '' }}">
|
<a href="{{ url_for('main.planner') }}" class="{{ 'active' if request.endpoint == 'main.planner' else '' }}">
|
||||||
<span class="ui-icon icon-house"></span>
|
<span class="ui-icon icon-calendar-days"></span>
|
||||||
<span>Zuhause</span>
|
<span>Woche</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('main.more_view') }}" class="{{ 'active' if request.endpoint == 'main.more_view' or request.endpoint == 'auth.profile' or (request.endpoint or '').startswith('admin.') else '' }}">
|
<button type="button" class="mobile-nav-button" data-mobile-sheet-open>
|
||||||
<span class="ui-icon icon-archive"></span>
|
<span class="ui-icon icon-ellipsis"></span>
|
||||||
<span>Mehr</span>
|
<span>Mehr</span>
|
||||||
</a>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Heute</p>
|
<p class="eyebrow">Heute</p>
|
||||||
<h1>Ein ruhiger Blick auf euren Alltag</h1>
|
<h1>Ein ruhiger Blick auf euren Alltag</h1>
|
||||||
<p class="lead">Du siehst schnell, was zuhause da ist, was schon geplant wurde und was gemeinsam oder persönlich vorbereitet ist.</p>
|
<p class="lead">Du siehst schnell, was zuhause da ist, was schon geplant wurde, welche Vorlagen gut passen und wo sanfte Unterstützung hilfreich sein kann.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<a class="button" href="{{ url_for('main.planner_day', date=today.isoformat()) }}">Heutigen Tagesplan öffnen</a>
|
<a class="button" href="{{ url_for('main.planner_day', date=today.isoformat()) }}">Heutigen Tagesplan öffnen</a>
|
||||||
<a class="button secondary" href="{{ url_for('main.item_create', kind='meal') }}">Mahlzeitenidee anlegen</a>
|
<a class="button secondary" href="{{ url_for('main.template_library') }}">Vorlagen öffnen</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -31,6 +31,19 @@
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if dashboard_hints %}
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Sanfte Hinweise</h2>
|
||||||
|
</div>
|
||||||
|
<div class="hint-list">
|
||||||
|
{% for hint in dashboard_hints %}
|
||||||
|
<p class="hint-chip">{{ hint }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section class="two-column">
|
<section class="two-column">
|
||||||
<article class="panel">
|
<article class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
@@ -47,6 +60,7 @@
|
|||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
<span class="chip">{{ entry.visibility_label }}</span>
|
<span class="chip">{{ entry.visibility_label }}</span>
|
||||||
<span class="chip status-soft">{{ entry.owner_label }}</span>
|
<span class="chip status-soft">{{ entry.owner_label }}</span>
|
||||||
|
<span class="chip">{{ entry.for_label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if entry.availability_state == 'home' %}
|
{% if entry.availability_state == 'home' %}
|
||||||
@@ -72,7 +86,7 @@
|
|||||||
<div class="mini-card-body">
|
<div class="mini-card-body">
|
||||||
<strong>{{ item.name }}</strong>
|
<strong>{{ item.name }}</strong>
|
||||||
<small>{{ item_kind_labels[item.kind] }} · {{ item.visibility_label }}</small>
|
<small>{{ item_kind_labels[item.kind] }} · {{ item.visibility_label }}</small>
|
||||||
<small>{{ item.owner_label }}</small>
|
<small>{{ item.for_label }}</small>
|
||||||
{% if item.dayparts %}
|
{% if item.dayparts %}
|
||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
{% for daypart in item.dayparts %}
|
{% for daypart in item.dayparts %}
|
||||||
@@ -90,24 +104,51 @@
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="two-column">
|
||||||
<div class="panel-head">
|
<article class="panel">
|
||||||
<h2>Nächste Tage</h2>
|
<div class="panel-head">
|
||||||
<a href="{{ url_for('main.planner') }}">Wochenansicht öffnen</a>
|
<h2>Vorlagen für später</h2>
|
||||||
</div>
|
<a href="{{ url_for('main.template_library') }}">Alles ansehen</a>
|
||||||
<div class="week-mini-grid">
|
</div>
|
||||||
{% for card in week_cards %}
|
{% if day_templates or week_templates %}
|
||||||
<a class="week-mini-card" href="{{ url_for('main.planner_day', date=card.date.isoformat()) }}">
|
<div class="stack-sections">
|
||||||
<strong>{{ weekday_short_name(card.date) }} {{ card.date.strftime('%d.%m.') }}</strong>
|
{% for template in day_templates %}
|
||||||
{% if card.filled_dayparts %}
|
<a class="mini-card" href="{{ url_for('main.day_template_edit', template_id=template.id) }}">
|
||||||
<span>{{ card.planned_count }} Einträge</span>
|
<strong>{{ template.name }}</strong>
|
||||||
<small>{{ card.filled_dayparts | map(attribute='name') | join(', ') }}</small>
|
<small>Tagesvorlage · {{ template.visibility_label }}</small>
|
||||||
{% else %}
|
</a>
|
||||||
<span>Noch frei</span>
|
{% endfor %}
|
||||||
<small>sanfter Einstieg für den Tag</small>
|
{% for template in week_templates %}
|
||||||
{% endif %}
|
<a class="mini-card" href="{{ url_for('main.week_template_edit', template_id=template.id) }}">
|
||||||
</a>
|
<strong>{{ template.name }}</strong>
|
||||||
{% endfor %}
|
<small>Wochenvorlage · {{ template.visibility_label }}</small>
|
||||||
</div>
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-state">Vorlagen helfen später beim Wiederverwenden. Du kannst sie direkt aus einem Tag oder einer Woche heraus anlegen.</p>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Nächste Tage</h2>
|
||||||
|
<a href="{{ url_for('main.planner') }}">Wochenansicht öffnen</a>
|
||||||
|
</div>
|
||||||
|
<div class="week-mini-grid">
|
||||||
|
{% for card in week_cards %}
|
||||||
|
<a class="week-mini-card" href="{{ url_for('main.planner_day', date=card.date.isoformat()) }}">
|
||||||
|
<strong>{{ weekday_short_name(card.date) }} {{ card.date.strftime('%d.%m.') }}</strong>
|
||||||
|
{% if card.filled_dayparts %}
|
||||||
|
<span>{{ card.planned_count }} Einträge</span>
|
||||||
|
<small>{{ card.filled_dayparts | map(attribute='name') | join(', ') }}</small>
|
||||||
|
{% else %}
|
||||||
|
<span>Noch frei</span>
|
||||||
|
<small>sanfter Einstieg für den Tag</small>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
<span class="chip">{{ item.visibility_label }}</span>
|
<span class="chip">{{ item.visibility_label }}</span>
|
||||||
<span class="chip status-soft">{{ item.owner_label }}</span>
|
<span class="chip status-soft">{{ item.owner_label }}</span>
|
||||||
|
<span class="chip">{{ item.for_label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">{{ item_kind_labels[item.kind] }}{% if item.category %} · {{ item.category }}{% endif %}</p>
|
<p class="muted">{{ item_kind_labels[item.kind] }}{% if item.category %} · {{ item.category }}{% endif %}</p>
|
||||||
{% if item.components %}
|
{% if item.components %}
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">{{ item_kind_labels[kind] }}</p>
|
<p class="eyebrow">{{ item_kind_labels[kind] }}</p>
|
||||||
<h1>{% if item %}{{ item.name }} bearbeiten{% else %}Neue{% endif %} {{ item_kind_singular_labels[kind] }}</h1>
|
<h1>{% if item %}{{ item.name }} bearbeiten{% else %}Neue{% endif %} {{ item_kind_singular_labels[kind] }}</h1>
|
||||||
<p class="lead">Nur das Nötigste: Name, Sichtbarkeit, Bild, Tageszeiten und eine kleine Notiz, wenn sie hilft.</p>
|
<p class="lead">Nur das Nötigste: Name, Sichtbarkeit, für wen etwas gedacht ist, Bild, Tageszeiten und eine kleine Notiz.</p>
|
||||||
</div>
|
</div>
|
||||||
{% if item %}
|
{% if item %}
|
||||||
<div class="intro-pills">
|
<div class="intro-pills">
|
||||||
<span class="status-pill">{{ item.visibility_label }}</span>
|
<span class="status-pill">{{ item.visibility_label }}</span>
|
||||||
<span class="status-pill status-soft">{{ item.owner_label }}</span>
|
<span class="status-pill status-soft">{{ item.owner_label }}</span>
|
||||||
|
<span class="status-pill">{{ item.for_label }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
@@ -23,15 +24,26 @@
|
|||||||
<input type="text" name="name" value="{{ form_data.name }}" required>
|
<input type="text" name="name" value="{{ form_data.name }}" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<div class="dual-grid">
|
||||||
Sichtbarkeit
|
<label>
|
||||||
<select name="visibility">
|
Sichtbarkeit
|
||||||
{% for value, label in visibility_options %}
|
<select name="visibility">
|
||||||
<option value="{{ value }}" {% if form_data.visibility == value %}selected{% endif %}>{{ label }}</option>
|
{% for value, label in visibility_options %}
|
||||||
{% endfor %}
|
<option value="{{ value }}" {% if form_data.visibility == value %}selected{% endif %}>{{ label }}</option>
|
||||||
</select>
|
{% endfor %}
|
||||||
<small class="helper-text">{{ visibility_descriptions[form_data.visibility] }}</small>
|
</select>
|
||||||
</label>
|
<small class="helper-text">{{ visibility_descriptions[form_data.visibility] }}</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Für wen?
|
||||||
|
<select name="target_user_id">
|
||||||
|
{% for option in target_user_options %}
|
||||||
|
<option value="{{ option.value }}" {% if form_data.target_user_raw == option.value %}selected{% endif %}>{{ option.label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Kategorie
|
Kategorie
|
||||||
@@ -40,9 +52,6 @@
|
|||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<option value="{{ category }}" {% if form_data.category == category %}selected{% endif %}>{{ category }}</option>
|
<option value="{{ category }}" {% if form_data.category == category %}selected{% endif %}>{{ category }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if form_data.category and form_data.category not in categories %}
|
|
||||||
<option value="{{ form_data.category }}" selected>{{ form_data.category }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -77,20 +86,34 @@
|
|||||||
{% if kind == 'meal' %}
|
{% if kind == 'meal' %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Bestandteile der Mahlzeitenidee</legend>
|
<legend>Bestandteile der Mahlzeitenidee</legend>
|
||||||
<p class="muted">Optional: Du kannst eine Mahlzeit frei als Idee anlegen oder sie aus sichtbaren Lebensmitteln zusammenklicken.</p>
|
<p class="muted">Du kannst eine Mahlzeit frei als Idee anlegen oder sie aus sichtbaren Lebensmitteln zusammenstellen.</p>
|
||||||
|
<div class="inline-form">
|
||||||
|
<label class="wide">
|
||||||
|
Lebensmittel suchen
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="food_search"
|
||||||
|
value="{{ form_data.food_search }}"
|
||||||
|
placeholder="z. B. Reis, Banane, Joghurt"
|
||||||
|
data-filter-input
|
||||||
|
data-filter-target="#meal-components-list"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<button class="secondary" type="submit" name="form_action" value="filter_foods">Suchen</button>
|
||||||
|
</div>
|
||||||
{% if food_groups %}
|
{% if food_groups %}
|
||||||
<div class="stack-sections">
|
<div class="stack-sections" id="meal-components-list">
|
||||||
{% for group in food_groups %}
|
{% for group in food_groups %}
|
||||||
<div class="component-group">
|
<div class="component-group">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h3>{{ group["title"] }}</h3>
|
<h3>{{ group["title"] }}</h3>
|
||||||
<span>{{ group["items"]|length }} Einträge</span>
|
<span>{{ group["items"]|length }} Einträge</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox-grid">
|
<div class="checkbox-grid filterable-checkbox-group" data-filter-group>
|
||||||
{% for food in group["items"] %}
|
{% for food in group["items"] %}
|
||||||
<label class="check-option">
|
<label class="check-option" data-filter-label="{{ food.name|lower }} {{ food.category|default('', true)|lower }}">
|
||||||
<input type="checkbox" name="component_ids" value="{{ food.id }}" {% if food.id in form_data.component_ids %}checked{% endif %}>
|
<input type="checkbox" name="component_ids" value="{{ food.id }}" {% if food.id in form_data.component_ids %}checked{% endif %}>
|
||||||
<span>{{ food.name }} · {{ food.visibility_label }}</span>
|
<span>{{ food.name }} · {{ food.visibility_label }} · {{ food.for_label }}</span>
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -117,9 +140,6 @@
|
|||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<option value="{{ category }}" {% if form_data.quick_food_category == category %}selected{% endif %}>{{ category }}</option>
|
<option value="{{ category }}" {% if form_data.quick_food_category == category %}selected{% endif %}>{{ category }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if form_data.quick_food_category and form_data.quick_food_category not in categories %}
|
|
||||||
<option value="{{ form_data.quick_food_category }}" selected>{{ form_data.quick_food_category }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="wide">
|
<label class="wide">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">{{ item_kind_labels[kind] }}</p>
|
<p class="eyebrow">{{ item_kind_labels[kind] }}</p>
|
||||||
<h1>{{ item_kind_labels[kind] }}</h1>
|
<h1>{{ item_kind_labels[kind] }}</h1>
|
||||||
<p class="lead">Gemeinsame und persönliche Ideen bleiben hier ruhig sortiert und schnell wiederverwendbar.</p>
|
<p class="lead">Gemeinsame und persönliche Ideen bleiben hier ruhig sortiert, mit einem klaren Blick darauf, für wen etwas gedacht ist.</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="button" href="{{ url_for('main.item_create', kind=kind) }}">Neu anlegen</a>
|
<a class="button" href="{{ url_for('main.item_create', kind=kind) }}">Neu anlegen</a>
|
||||||
</section>
|
</section>
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
<span class="chip">{{ item.visibility_label }}</span>
|
<span class="chip">{{ item.visibility_label }}</span>
|
||||||
<span class="chip status-soft">{{ item.owner_label }}</span>
|
<span class="chip status-soft">{{ item.owner_label }}</span>
|
||||||
|
<span class="chip">{{ item.for_label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
{% if item.category %}{{ item.category }}{% else %}ohne Kategorie{% endif %}
|
{% if item.category %}{{ item.category }}{% else %}ohne Kategorie{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% if template %}Tagesvorlage bearbeiten{% else %}Neue Tagesvorlage{% endif %} | Nouri{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="page-intro">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Tagesvorlage</p>
|
||||||
|
<h1>{% if template %}{{ template.name }} bearbeiten{% else %}Tagesvorlage anlegen{% endif %}</h1>
|
||||||
|
<p class="lead">Gib der Vorlage einen Namen, den du später schnell wiedererkennst. Die Einträge bleiben bewusst einfach und alltagsnah.</p>
|
||||||
|
</div>
|
||||||
|
{% if source_date %}
|
||||||
|
<span class="status-pill">Aus {{ source_date.strftime('%d.%m.%Y') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel form-panel">
|
||||||
|
<form method="post" class="stack-form">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<label>
|
||||||
|
Name der Vorlage
|
||||||
|
<input type="text" name="name" value="{{ form_data.name }}" placeholder="{{ name_suggestions[0] }}" required>
|
||||||
|
<small class="helper-text">Zum Beispiel: Ruhiger Tag, Einfacher Bürotag oder ein ganz eigener Name.</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Beschreibung
|
||||||
|
<textarea name="description" rows="3" placeholder="Optional, wenn eine kleine Erinnerung hilfreich ist.">{{ form_data.description }}</textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Sichtbarkeit
|
||||||
|
<select name="visibility">
|
||||||
|
{% for value, label in visibility_options %}
|
||||||
|
<option value="{{ value }}" {% if form_data.visibility == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="helper-text">{{ visibility_descriptions[form_data.visibility] }}</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="chip-row">
|
||||||
|
{% for suggestion in name_suggestions %}
|
||||||
|
<span class="chip status-soft">{{ suggestion }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stack-sections">
|
||||||
|
{% for section in daypart_sections %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{ section.daypart.name }}</legend>
|
||||||
|
<div class="template-search-row">
|
||||||
|
<label class="wide">
|
||||||
|
Einträge filtern
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Nach Namen suchen"
|
||||||
|
data-filter-input
|
||||||
|
data-filter-target="#day-template-list-{{ section.daypart.id }}"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if section.quick_items %}
|
||||||
|
<div class="quick-add-row">
|
||||||
|
{% for item in section.quick_items %}
|
||||||
|
<label class="quick-select-card" data-filter-label="{{ item.name|lower }} {{ item.category|default('', true)|lower }}">
|
||||||
|
<input type="checkbox" name="daypart_{{ section.daypart.id }}_item_ids" value="{{ item.id }}" {% if item.id in section.selected_ids %}checked{% endif %}>
|
||||||
|
<span>
|
||||||
|
<strong>{{ item.name }}</strong>
|
||||||
|
<small>{{ item_kind_labels[item.kind] }} · {{ item.visibility_label }} · {{ item.for_label }}</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="checkbox-grid template-checkbox-grid" id="day-template-list-{{ section.daypart.id }}">
|
||||||
|
{% for item in section.list_items %}
|
||||||
|
<label class="check-option" data-filter-label="{{ item.name|lower }} {{ item.category|default('', true)|lower }}">
|
||||||
|
<input type="checkbox" name="daypart_{{ section.daypart.id }}_item_ids" value="{{ item.id }}" {% if item.id in section.selected_ids %}checked{% endif %}>
|
||||||
|
<span>{{ item.name }} · {{ item.visibility_label }} · {{ item.for_label }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
<a class="ghost-button" href="{{ url_for('main.template_library') }}">Zurück</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Vorlagen | Nouri{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="page-intro">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Vorlagen</p>
|
||||||
|
<h1>Bewährtes ruhig wiederverwenden</h1>
|
||||||
|
<p class="lead">Tagesvorlagen, Wochenvorlagen und kleine Pakete helfen dabei, vertraute Muster mit wenig Tipparbeit erneut zu nutzen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a class="button" href="{{ url_for('main.day_template_create') }}">Neue Tagesvorlage</a>
|
||||||
|
<a class="button secondary" href="{{ url_for('main.week_template_create') }}">Neue Wochenvorlage</a>
|
||||||
|
<a class="button secondary" href="{{ url_for('main.item_set_create') }}">Neues Paket</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel compact-form-panel">
|
||||||
|
<form method="get" class="filter-form">
|
||||||
|
<label class="wide">
|
||||||
|
Suche
|
||||||
|
<input type="text" name="q" value="{{ query }}" placeholder="Nach Namen suchen">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Sichtbarkeit
|
||||||
|
<select name="visibility">
|
||||||
|
{% for value, label in visibility_options %}
|
||||||
|
<option value="{{ value }}" {% if selected_visibility == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button type="submit">Filtern</button>
|
||||||
|
<a class="ghost-button" href="{{ url_for('main.template_library') }}">Zurücksetzen</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% if template_hints %}
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Sanfte Hinweise</h2>
|
||||||
|
</div>
|
||||||
|
<div class="hint-list">
|
||||||
|
{% for hint in template_hints %}
|
||||||
|
<p class="hint-chip">{{ hint }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="template-library-grid">
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Tagesvorlagen</h2>
|
||||||
|
<a href="{{ url_for('main.day_template_create') }}">Neu anlegen</a>
|
||||||
|
</div>
|
||||||
|
{% if day_templates %}
|
||||||
|
<div class="stack-sections">
|
||||||
|
{% for template in day_templates %}
|
||||||
|
<article class="template-list-card">
|
||||||
|
<div>
|
||||||
|
<strong>{{ template.name }}</strong>
|
||||||
|
{% if template.description %}
|
||||||
|
<p class="muted">{{ template.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="chip-row">
|
||||||
|
<span class="chip">{{ template.visibility_label }}</span>
|
||||||
|
<span class="chip status-soft">{{ template.owner_label }}</span>
|
||||||
|
{% if template.last_used_at %}
|
||||||
|
<span class="chip">Zuletzt genutzt</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-actions">
|
||||||
|
<form method="post" action="{{ url_for('main.day_template_apply', template_id=template.id) }}">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<input type="hidden" name="target_date" value="{{ today.isoformat() }}">
|
||||||
|
<button type="submit">Heute anwenden</button>
|
||||||
|
</form>
|
||||||
|
{% if template.can_edit %}
|
||||||
|
<a class="ghost-button" href="{{ url_for('main.day_template_edit', template_id=template.id) }}">Bearbeiten</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-state">Noch keine passende Tagesvorlage. Du kannst eine Vorlage direkt neu anlegen oder aus einem Tagesplan speichern.</p>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Wochenvorlagen</h2>
|
||||||
|
<a href="{{ url_for('main.week_template_create') }}">Neu anlegen</a>
|
||||||
|
</div>
|
||||||
|
{% if week_templates %}
|
||||||
|
<div class="stack-sections">
|
||||||
|
{% for template in week_templates %}
|
||||||
|
<article class="template-list-card">
|
||||||
|
<div>
|
||||||
|
<strong>{{ template.name }}</strong>
|
||||||
|
{% if template.description %}
|
||||||
|
<p class="muted">{{ template.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="chip-row">
|
||||||
|
<span class="chip">{{ template.visibility_label }}</span>
|
||||||
|
<span class="chip status-soft">{{ template.owner_label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-actions">
|
||||||
|
<form method="post" action="{{ url_for('main.week_template_apply', template_id=template.id) }}">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<input type="hidden" name="target_week" value="{{ today.isoformat() }}">
|
||||||
|
<button type="submit">Diese Woche anwenden</button>
|
||||||
|
</form>
|
||||||
|
{% if template.can_edit %}
|
||||||
|
<a class="ghost-button" href="{{ url_for('main.week_template_edit', template_id=template.id) }}">Bearbeiten</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-state">Noch keine Wochenvorlage. Eine gute Woche lässt sich später hier ganz leicht wiederverwenden.</p>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Kleine Pakete</h2>
|
||||||
|
<a href="{{ url_for('main.item_set_create') }}">Neues Paket</a>
|
||||||
|
</div>
|
||||||
|
{% if item_sets %}
|
||||||
|
<div class="stack-sections">
|
||||||
|
{% for item_set in item_sets %}
|
||||||
|
<article class="template-list-card">
|
||||||
|
<div>
|
||||||
|
<strong>{{ item_set.name }}</strong>
|
||||||
|
{% if item_set.description %}
|
||||||
|
<p class="muted">{{ item_set.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="chip-row">
|
||||||
|
<span class="chip">{{ item_set.visibility_label }}</span>
|
||||||
|
<span class="chip status-soft">{{ item_set.owner_label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-actions">
|
||||||
|
<form method="post" action="{{ url_for('main.item_set_apply', set_id=item_set.id) }}">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<button type="submit">Auf Einkaufsliste</button>
|
||||||
|
</form>
|
||||||
|
{% if item_set.can_edit %}
|
||||||
|
<a class="ghost-button" href="{{ url_for('main.item_set_edit', set_id=item_set.id) }}">Bearbeiten</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-state">Pakete eignen sich gut für kleine Bündel wie schnelles Frühstück, sicherer Snack oder Einkauf für zwei Tage.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% if item_set %}Paket bearbeiten{% else %}Neues Paket{% endif %} | Nouri{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="page-intro">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Kleines Paket</p>
|
||||||
|
<h1>{% if item_set %}{{ item_set.name }} bearbeiten{% else %}Paket anlegen{% endif %}</h1>
|
||||||
|
<p class="lead">Pakete bündeln wiederkehrende Dinge ganz leicht, zum Beispiel schnelles Frühstück, sicherer Snack oder Einkauf für zwei Tage.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel form-panel">
|
||||||
|
<form method="post" class="stack-form">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<label>
|
||||||
|
Name des Pakets
|
||||||
|
<input type="text" name="name" value="{{ form_data.name }}" placeholder="{{ name_suggestions[0] }}" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Beschreibung
|
||||||
|
<textarea name="description" rows="3" placeholder="Optional, wenn eine kleine Erinnerung hilfreich ist.">{{ form_data.description }}</textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Sichtbarkeit
|
||||||
|
<select name="visibility">
|
||||||
|
{% for value, label in visibility_options %}
|
||||||
|
<option value="{{ value }}" {% if form_data.visibility == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="helper-text">{{ visibility_descriptions[form_data.visibility] }}</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="chip-row">
|
||||||
|
{% for suggestion in name_suggestions %}
|
||||||
|
<span class="chip status-soft">{{ suggestion }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Einträge auswählen</legend>
|
||||||
|
<label>
|
||||||
|
Einträge filtern
|
||||||
|
<input type="text" placeholder="Nach Namen suchen" data-filter-input data-filter-target="#item-set-list">
|
||||||
|
</label>
|
||||||
|
<div class="stack-sections" id="item-set-list">
|
||||||
|
{% for group in item_groups %}
|
||||||
|
<div class="component-group">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h3>{{ group["title"] }}</h3>
|
||||||
|
<span>{{ group["items"]|length }} Einträge</span>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-grid">
|
||||||
|
{% for item in group["items"] %}
|
||||||
|
<label class="check-option" data-filter-label="{{ item.name|lower }} {{ item.category|default('', true)|lower }}">
|
||||||
|
<input type="checkbox" name="item_ids" value="{{ item.id }}" {% if item.id in form_data.item_ids %}checked{% endif %}>
|
||||||
|
<span>{{ item.name }} · {{ item_kind_labels[item.kind] }} · {{ item.visibility_label }} · {{ item.for_label }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
<a class="ghost-button" href="{{ url_for('main.template_library') }}">Zurück</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% if template %}Wochenvorlage bearbeiten{% else %}Neue Wochenvorlage{% endif %} | Nouri{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="page-intro">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Wochenvorlage</p>
|
||||||
|
<h1>{% if template %}{{ template.name }} bearbeiten{% else %}Wochenvorlage anlegen{% endif %}</h1>
|
||||||
|
<p class="lead">Wochenvorlagen bleiben bewusst leicht: pro Wochentag kannst du eine bestehende Tagesvorlage zuordnen oder einen aktuellen Tag als neue Vorlage übernehmen.</p>
|
||||||
|
</div>
|
||||||
|
{% if source_week %}
|
||||||
|
<span class="status-pill">Aus Woche ab {{ source_week.strftime('%d.%m.%Y') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel form-panel">
|
||||||
|
<form method="post" class="stack-form">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<input type="hidden" name="source_week" value="{{ form_data.source_week }}">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Name der Vorlage
|
||||||
|
<input type="text" name="name" value="{{ form_data.name }}" placeholder="{{ name_suggestions[0] }}" required>
|
||||||
|
<small class="helper-text">Ein Name wie Standardwoche, leichte Woche oder etwas ganz Eigenes reicht völlig aus.</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Beschreibung
|
||||||
|
<textarea name="description" rows="3" placeholder="Optional, wenn eine kleine Erinnerung hilfreich ist.">{{ form_data.description }}</textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Sichtbarkeit
|
||||||
|
<select name="visibility">
|
||||||
|
{% for value, label in visibility_options %}
|
||||||
|
<option value="{{ value }}" {% if form_data.visibility == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="helper-text">{{ visibility_descriptions[form_data.visibility] }}</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="chip-row">
|
||||||
|
{% for suggestion in name_suggestions %}
|
||||||
|
<span class="chip status-soft">{{ suggestion }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stack-sections">
|
||||||
|
{% for weekday_index in range(7) %}
|
||||||
|
<div class="week-template-row">
|
||||||
|
<div>
|
||||||
|
<strong>{{ weekday_labels[weekday_index] }}</strong>
|
||||||
|
<p class="muted">Du kannst eine vorhandene Tagesvorlage auswählen oder den aktuellen Tag aus der Quellwoche übernehmen.</p>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Tagesvorlage
|
||||||
|
<select name="weekday_{{ weekday_index }}_day_template_id">
|
||||||
|
<option value="">Noch offen</option>
|
||||||
|
{% for day_template in day_templates %}
|
||||||
|
<option value="{{ day_template.id }}" {% if form_data.selected_map.get(weekday_index) == day_template.id %}selected{% endif %}>{{ day_template.name }} · {{ day_template.visibility_label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{% if form_data.source_week %}
|
||||||
|
<label class="inline-check">
|
||||||
|
<input type="checkbox" name="weekday_{{ weekday_index }}_copy_source" value="1" {% if form_data.copy_from_source.get(weekday_index) %}checked{% endif %}>
|
||||||
|
<span>Aus Quellwoche als neue Tagesvorlage übernehmen</span>
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
<a class="ghost-button" href="{{ url_for('main.template_library') }}">Zurück</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}Mehr | Nouri{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="page-intro">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">Mehr</p>
|
|
||||||
<h1>Alles Weitere auf einen Blick</h1>
|
|
||||||
<p class="lead">Hier liegen die ruhigeren Nebenwege: Lebensmittel, Mahlzeiten, Woche, Profil und die kleinen Einstellungen.</p>
|
|
||||||
</div>
|
|
||||||
<div class="intro-pills">
|
|
||||||
<span class="status-pill">{{ g.user.display_name or g.user.username }}</span>
|
|
||||||
<span class="status-pill status-soft">{{ role_labels[g.user.role] }}</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="more-link-grid">
|
|
||||||
<a class="more-link-card" href="{{ url_for('main.item_list', kind='food') }}">
|
|
||||||
<strong>Lebensmittel</strong>
|
|
||||||
<small>Persönliche und gemeinsame Einträge ansehen</small>
|
|
||||||
</a>
|
|
||||||
<a class="more-link-card" href="{{ url_for('main.item_list', kind='meal') }}">
|
|
||||||
<strong>Mahlzeiten</strong>
|
|
||||||
<small>Mahlzeitenideen sammeln und wiederverwenden</small>
|
|
||||||
</a>
|
|
||||||
<a class="more-link-card" href="{{ url_for('main.planner') }}">
|
|
||||||
<strong>Woche</strong>
|
|
||||||
<small>Die nächsten sieben Tage im Blick behalten</small>
|
|
||||||
</a>
|
|
||||||
<a class="more-link-card" href="{{ url_for('main.archive_view') }}">
|
|
||||||
<strong>Archiv</strong>
|
|
||||||
<small>Vertraute Dinge leicht wiederfinden</small>
|
|
||||||
</a>
|
|
||||||
<a class="more-link-card" href="{{ url_for('auth.profile') }}">
|
|
||||||
<strong>Mein Profil</strong>
|
|
||||||
<small>Zugang, Name und Passwort pflegen</small>
|
|
||||||
</a>
|
|
||||||
{% if g.user.role == 'admin' %}
|
|
||||||
<a class="more-link-card" href="{{ url_for('admin.user_list') }}">
|
|
||||||
<strong>Nutzer verwalten</strong>
|
|
||||||
<small>Weitere Haushaltsmitglieder verwalten</small>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="more-actions">
|
|
||||||
<button class="ghost-button" type="button" data-theme-toggle>Modus wechseln</button>
|
|
||||||
<form method="post" action="{{ url_for('auth.logout') }}">
|
|
||||||
{{ csrf_input() }}
|
|
||||||
<button class="ghost-button" type="submit">Abmelden</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -14,6 +14,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="two-column">
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Tagesvorlagen</h2>
|
||||||
|
<a href="{{ url_for('main.day_template_create', source_date=selected_date.isoformat()) }}">Als Vorlage speichern</a>
|
||||||
|
</div>
|
||||||
|
{% if day_templates %}
|
||||||
|
<div class="stack-sections">
|
||||||
|
{% for template in day_templates %}
|
||||||
|
<form method="post" action="{{ url_for('main.day_template_apply', template_id=template.id) }}" class="inline-form template-apply-form">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<input type="hidden" name="target_date" value="{{ selected_date.isoformat() }}">
|
||||||
|
<div class="template-card">
|
||||||
|
<strong>{{ template.name }}</strong>
|
||||||
|
<small>{{ template.visibility_label }} · {{ template.owner_label }}</small>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Vorlage anwenden</button>
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-state">Wenn du einen Tag öfter wiederverwenden möchtest, kannst du ihn hier als Tagesvorlage speichern.</p>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% if day_hints %}
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Sanfte Hinweise</h2>
|
||||||
|
</div>
|
||||||
|
<div class="hint-list">
|
||||||
|
{% for hint in day_hints %}
|
||||||
|
<p class="hint-chip">{{ hint }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="planner-day-stack">
|
<section class="planner-day-stack">
|
||||||
{% for section in sections %}
|
{% for section in sections %}
|
||||||
<details class="day-tile" id="daypart-{{ section.daypart.id }}" {% if section.is_open %}open{% endif %}>
|
<details class="day-tile" id="daypart-{{ section.daypart.id }}" {% if section.is_open %}open{% endif %}>
|
||||||
@@ -33,6 +72,18 @@
|
|||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="day-tile-body">
|
<div class="day-tile-body">
|
||||||
|
{% if section.suggestions %}
|
||||||
|
<div class="suggestion-row">
|
||||||
|
{% for suggestion in section.suggestions %}
|
||||||
|
<article class="suggestion-card">
|
||||||
|
<strong>{{ suggestion.title }}</strong>
|
||||||
|
<small>{{ suggestion.reason }}</small>
|
||||||
|
<p>{{ suggestion.note }}</p>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if section.quick_items %}
|
{% if section.quick_items %}
|
||||||
<div class="quick-add-row">
|
<div class="quick-add-row">
|
||||||
{% for item in section.quick_items %}
|
{% for item in section.quick_items %}
|
||||||
@@ -44,7 +95,7 @@
|
|||||||
<input type="hidden" name="visibility" value="{{ item.visibility }}">
|
<input type="hidden" name="visibility" value="{{ item.visibility }}">
|
||||||
<button class="quick-add-button" type="submit">
|
<button class="quick-add-button" type="submit">
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
<small>{{ item_kind_labels[item.kind] }} · {{ item.visibility_label }}{% if item.availability_state == 'home' %} · zuhause{% endif %}</small>
|
<small>{{ item_kind_labels[item.kind] }} · {{ item.visibility_label }} · {{ item.for_label }}{% if item.availability_state == 'home' %} · zuhause{% endif %}</small>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -61,7 +112,7 @@
|
|||||||
<option value="">Etwas für {{ section.daypart.name }} wählen</option>
|
<option value="">Etwas für {{ section.daypart.name }} wählen</option>
|
||||||
{% for item in section.candidates %}
|
{% for item in section.candidates %}
|
||||||
<option value="{{ item.id }}" {% if section.selected_item_id == item.id %}selected{% endif %}>
|
<option value="{{ item.id }}" {% if section.selected_item_id == item.id %}selected{% endif %}>
|
||||||
{{ item.name }} · {{ item_kind_labels[item.kind] }} · {{ item.visibility_label }}{% if item.availability_state == 'home' %} · zuhause{% endif %}{% if item.dayparts and section.daypart.name not in item.dayparts %} · auch flexibel{% endif %}
|
{{ item.name }} · {{ item_kind_labels[item.kind] }} · {{ item.visibility_label }} · {{ item.for_label }}{% if item.availability_state == 'home' %} · zuhause{% endif %}{% if item.dayparts and section.daypart.name not in item.dayparts %} · auch flexibel{% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@@ -92,6 +143,7 @@
|
|||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
<span class="chip">{{ entry.visibility_label }}</span>
|
<span class="chip">{{ entry.visibility_label }}</span>
|
||||||
<span class="chip status-soft">{{ entry.owner_label }}</span>
|
<span class="chip status-soft">{{ entry.owner_label }}</span>
|
||||||
|
<span class="chip">{{ entry.for_label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if entry.can_edit %}
|
{% if entry.can_edit %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Wochenansicht</p>
|
<p class="eyebrow">Wochenansicht</p>
|
||||||
<h1>Ein sanfter Blick auf die nächsten sieben Tage</h1>
|
<h1>Ein sanfter Blick auf die nächsten sieben Tage</h1>
|
||||||
<p class="lead">Du kannst bestehende Einträge zwischen Tagen und Tageszeiten verschieben. Wenn etwas noch nicht zuhause ist, landet es dabei automatisch auf der Einkaufsliste.</p>
|
<p class="lead">Du kannst bestehende Einträge zwischen Tagen und Tageszeiten verschieben, Vorlagen anwenden und eine Woche bei Bedarf für später sichern.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="week-nav">
|
<div class="week-nav">
|
||||||
<a class="ghost-button" href="{{ url_for('main.planner', week=prev_week.isoformat()) }}">Vorige Woche</a>
|
<a class="ghost-button" href="{{ url_for('main.planner', week=prev_week.isoformat()) }}">Vorige Woche</a>
|
||||||
@@ -14,6 +14,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="two-column">
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Wochenvorlagen</h2>
|
||||||
|
<a href="{{ url_for('main.week_template_create', source_week=week_start.isoformat()) }}">Als Vorlage speichern</a>
|
||||||
|
</div>
|
||||||
|
{% if week_templates %}
|
||||||
|
<div class="stack-sections">
|
||||||
|
{% for template in week_templates %}
|
||||||
|
<form method="post" action="{{ url_for('main.week_template_apply', template_id=template.id) }}" class="inline-form template-apply-form">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<input type="hidden" name="target_week" value="{{ week_start.isoformat() }}">
|
||||||
|
<div class="template-card">
|
||||||
|
<strong>{{ template.name }}</strong>
|
||||||
|
<small>{{ template.visibility_label }} · {{ template.owner_label }}</small>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Vorlage anwenden</button>
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-state">Wenn eine Woche sich bewährt hat, kannst du sie hier später als Wochenvorlage wiederverwenden.</p>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% if week_hints %}
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Sanfte Hinweise</h2>
|
||||||
|
</div>
|
||||||
|
<div class="hint-list">
|
||||||
|
{% for hint in week_hints %}
|
||||||
|
<p class="hint-chip">{{ hint }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="week-overview-grid week-board" data-csrf-token="{{ csrf_token_value }}">
|
<section class="week-overview-grid week-board" data-csrf-token="{{ csrf_token_value }}">
|
||||||
{% for card in week_cards %}
|
{% for card in week_cards %}
|
||||||
<article class="week-card">
|
<article class="week-card">
|
||||||
@@ -51,7 +90,7 @@
|
|||||||
{% for entry in slot.entries %}
|
{% for entry in slot.entries %}
|
||||||
<article class="plan-chip draggable-plan-entry" draggable="{{ 'true' if entry.can_edit else 'false' }}" data-entry-id="{{ entry.id }}" data-move-url="{{ url_for('main.planner_move', entry_id=entry.id) }}">
|
<article class="plan-chip draggable-plan-entry" draggable="{{ 'true' if entry.can_edit else 'false' }}" data-entry-id="{{ entry.id }}" data-move-url="{{ url_for('main.planner_move', entry_id=entry.id) }}">
|
||||||
<strong>{{ entry.item_name }}</strong>
|
<strong>{{ entry.item_name }}</strong>
|
||||||
<small>{{ entry.visibility_label }} · {{ entry.owner_label }}</small>
|
<small>{{ entry.visibility_label }} · {{ entry.for_label }}</small>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Einkaufsliste</p>
|
<p class="eyebrow">Einkaufsliste</p>
|
||||||
<h1>Was noch mitkommen soll</h1>
|
<h1>Was noch mitkommen soll</h1>
|
||||||
<p class="lead">Abhaken legt Dinge automatisch unter Zuhause ab. Gemeinsame und persönliche Einträge bleiben dabei klar erkennbar.</p>
|
<p class="lead">Fehlende Lebensmittel aus einer Mahlzeit landen jetzt einzeln hier. So bleibt die Liste konkret und alltagsnah.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -35,6 +35,13 @@
|
|||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
<span class="chip">{{ entry.visibility_label }}</span>
|
<span class="chip">{{ entry.visibility_label }}</span>
|
||||||
<span class="chip status-soft">{{ entry.owner_label }}</span>
|
<span class="chip status-soft">{{ entry.owner_label }}</span>
|
||||||
|
<span class="chip">{{ entry.for_label }}</span>
|
||||||
|
{% if entry.needed_for_label %}
|
||||||
|
<span class="chip status-home">
|
||||||
|
Für {{ entry.needed_for_label }}
|
||||||
|
{% if entry.needed_daypart_name %} · {{ entry.needed_daypart_name }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
@@ -55,7 +62,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<section class="panel empty-panel">
|
<section class="panel empty-panel">
|
||||||
<h2>Die Liste ist gerade frei</h2>
|
<h2>Die Liste ist gerade frei</h2>
|
||||||
<p>Einträge aus Lebensmitteln, Mahlzeitenideen oder dem Archiv lassen sich jederzeit wieder hinzufügen.</p>
|
<p>Einträge aus Lebensmitteln, Vorlagen oder kleinen Paketen lassen sich jederzeit wieder hinzufügen.</p>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user