Release Nouri 1.3.3 with shopping articles
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
"author": "Florian Heinz",
|
||||
"description": "Private Flask app for meals, shopping and gentle food planning",
|
||||
"tagline": "einfach essen planen",
|
||||
"version": "1.3.2",
|
||||
"upstreamVersion": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"upstreamVersion": "1.3.3",
|
||||
"healthCheckPath": "/",
|
||||
"httpPort": 8000,
|
||||
"manifestVersion": 2,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Nouri 1.3.3
|
||||
|
||||
Nouri 1.3.3 erweitert die Einkaufsliste um freie Einkaufsartikel, ohne die bestehende Lebensmittel- und Rezeptlogik umzubauen. Der Fokus liegt darauf, Alltagsdinge wie Drogerie, Haushalt oder Garten genauso schnell auf die Liste setzen zu können wie Lebensmittel.
|
||||
|
||||
## Neu in 1.3.3
|
||||
|
||||
- Die Einkaufssuche kann jetzt Lebensmittel und allgemeine Artikel finden.
|
||||
- Neue Artikel können direkt aus dem Suchbegriff angelegt werden:
|
||||
- `Als Lebensmittel anlegen`
|
||||
- `Als Einkaufsartikel anlegen`
|
||||
- Einkaufsartikel wie `Blumenerde`, `Deo`, `Insektenschutz` oder `Sonnencreme` werden intern gespeichert.
|
||||
- Reine Einkaufsartikel bleiben aus Mahlzeiten, Rezeptvorschlägen und Lebensmittel-Details heraus.
|
||||
- Nicht vorhandene Lebensmittel können aus der Einkaufsliste heraus schnell als unsortiertes Lebensmittel angelegt werden.
|
||||
|
||||
## Einkaufsliste
|
||||
|
||||
- Bereits angelegte Einkaufsartikel erscheinen bei späteren Suchen wieder als Treffer.
|
||||
- Einkaufshinweise wie `TK`, `Dose`, `frisch` oder andere kurze Notizen funktionieren weiterhin.
|
||||
- Derselbe Artikel kann mit unterschiedlichen Einkaufshinweisen mehrfach auf der Liste stehen.
|
||||
- Einkaufsartikel werden auf der Liste als `Einkaufsartikel` markiert und nutzen ein Einkaufswagen-Symbol.
|
||||
- Beim Abhaken eines Einkaufsartikels wird er als eingekauft markiert, ohne ihn als zuhause vorhandenes Lebensmittel zu behandeln.
|
||||
|
||||
## Daten und Migration
|
||||
|
||||
- Das Items-Schema unterstützt jetzt zusätzlich den internen Typ `shopping`.
|
||||
- Bestehende Datenbanken werden beim Start migriert, damit der neue Typ auch bei Updates funktioniert.
|
||||
- Der Index für Items nach Typ und Name wird bei Schema-Upgrades sauber wieder angelegt.
|
||||
|
||||
## Betrieb
|
||||
|
||||
- Cloudron-Version und Upstream-Version stehen jetzt auf `1.3.3`.
|
||||
- Die interne Schema-Version und der App-Version-Fallback wurden auf `1.3.3` angehoben.
|
||||
- Der Service Worker nutzt einen neuen Cache-Namen für `1.3.3`.
|
||||
|
||||
## Upgrade-Hinweis
|
||||
|
||||
- Bestehende Lebensmittel, Mahlzeitenideen und Einkaufseinträge bleiben erhalten.
|
||||
- Nach dem Update können freie Einkaufsartikel direkt unter `Einkauf` über das Suchfeld angelegt werden.
|
||||
+1
-1
@@ -93,7 +93,7 @@ def load_app_version(root_dir: Path) -> str:
|
||||
).strip()
|
||||
if manifest_version:
|
||||
return manifest_version
|
||||
return "1.3.2"
|
||||
return "1.3.3"
|
||||
|
||||
|
||||
def load_release_url() -> str:
|
||||
|
||||
+111
-1
@@ -15,7 +15,7 @@ from .constants import (
|
||||
DEFAULT_CATEGORY_BUILDERS,
|
||||
)
|
||||
|
||||
CURRENT_SCHEMA_VERSION = "1.3.2"
|
||||
CURRENT_SCHEMA_VERSION = "1.3.3"
|
||||
|
||||
ANIMAL_HINTS = (
|
||||
"huhn",
|
||||
@@ -423,6 +423,109 @@ def set_meta(database: sqlite3.Connection, key: str, value: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def item_kind_constraint_supports_shopping(database: sqlite3.Connection) -> bool:
|
||||
row = database.execute(
|
||||
"""
|
||||
SELECT sql
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = 'items'
|
||||
"""
|
||||
).fetchone()
|
||||
return bool(row and row["sql"] and "'shopping'" in row["sql"])
|
||||
|
||||
|
||||
def migrate_items_kind_constraint(database: sqlite3.Connection) -> None:
|
||||
if not table_exists(database, "items") or item_kind_constraint_supports_shopping(database):
|
||||
return
|
||||
|
||||
columns = table_columns(database, "items")
|
||||
if "kind" not in columns:
|
||||
return
|
||||
|
||||
foreign_keys_enabled = bool(database.execute("PRAGMA foreign_keys").fetchone()[0])
|
||||
if foreign_keys_enabled:
|
||||
database.execute("PRAGMA foreign_keys = OFF")
|
||||
try:
|
||||
database.execute("DROP TABLE IF EXISTS items_new")
|
||||
database.execute(
|
||||
"""
|
||||
CREATE TABLE items_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
household_id INTEGER,
|
||||
owner_user_id INTEGER,
|
||||
target_user_id INTEGER,
|
||||
visibility TEXT NOT NULL DEFAULT 'shared',
|
||||
kind TEXT NOT NULL CHECK (kind IN ('food', 'meal', 'shopping')),
|
||||
name TEXT NOT NULL,
|
||||
category TEXT,
|
||||
base_type TEXT NOT NULL DEFAULT 'neutral',
|
||||
flavor_profile TEXT NOT NULL DEFAULT 'neutral',
|
||||
suggestion_role TEXT NOT NULL DEFAULT 'base',
|
||||
suggestion_priority TEXT NOT NULL DEFAULT 'normal',
|
||||
can_be_meal_core INTEGER NOT NULL DEFAULT 0,
|
||||
meal_type TEXT,
|
||||
meal_tags TEXT NOT NULL DEFAULT '',
|
||||
energy_density TEXT NOT NULL DEFAULT 'neutral',
|
||||
note TEXT,
|
||||
photo_filename TEXT,
|
||||
availability_state TEXT NOT NULL DEFAULT 'idea' CHECK (availability_state IN ('idea', 'home', 'archived')),
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
is_quick_added INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER,
|
||||
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,
|
||||
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 (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
target_columns = [
|
||||
"id",
|
||||
"household_id",
|
||||
"owner_user_id",
|
||||
"target_user_id",
|
||||
"visibility",
|
||||
"kind",
|
||||
"name",
|
||||
"category",
|
||||
"base_type",
|
||||
"flavor_profile",
|
||||
"suggestion_role",
|
||||
"suggestion_priority",
|
||||
"can_be_meal_core",
|
||||
"meal_type",
|
||||
"meal_tags",
|
||||
"energy_density",
|
||||
"note",
|
||||
"photo_filename",
|
||||
"availability_state",
|
||||
"is_archived",
|
||||
"is_quick_added",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
copy_columns = [column for column in target_columns if column in columns]
|
||||
quoted_columns = ", ".join(copy_columns)
|
||||
database.execute(
|
||||
f"""
|
||||
INSERT INTO items_new ({quoted_columns})
|
||||
SELECT {quoted_columns}
|
||||
FROM items
|
||||
"""
|
||||
)
|
||||
database.execute("DROP TABLE items")
|
||||
database.execute("ALTER TABLE items_new RENAME TO items")
|
||||
finally:
|
||||
if foreign_keys_enabled:
|
||||
database.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
|
||||
def bootstrap_legacy_schema(database: sqlite3.Connection) -> None:
|
||||
ensure_meta_table(database)
|
||||
database.execute(
|
||||
@@ -739,6 +842,7 @@ def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
||||
add_column_if_missing(database, "items", "energy_density TEXT NOT NULL DEFAULT 'neutral'")
|
||||
add_column_if_missing(database, "items", "is_archived INTEGER NOT NULL DEFAULT 0")
|
||||
add_column_if_missing(database, "items", "is_quick_added INTEGER NOT NULL DEFAULT 0")
|
||||
migrate_items_kind_constraint(database)
|
||||
add_column_if_missing(database, "shopping_entries", "needed_for_date TEXT")
|
||||
add_column_if_missing(database, "shopping_entries", "needed_for_daypart_id INTEGER")
|
||||
add_column_if_missing(database, "shopping_entries", "shopping_note TEXT NOT NULL DEFAULT ''")
|
||||
@@ -821,6 +925,12 @@ def ensure_schema_upgrades(database: sqlite3.Connection) -> None:
|
||||
WHERE email IS NOT NULL AND email != ''
|
||||
"""
|
||||
)
|
||||
database.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_items_kind_name
|
||||
ON items (kind, name)
|
||||
"""
|
||||
)
|
||||
database.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_items_household_visibility
|
||||
|
||||
+88
-9
@@ -57,7 +57,7 @@ from .constants import (
|
||||
WEEKDAY_OPTIONS,
|
||||
WEEK_TEMPLATE_NAME_SUGGESTIONS,
|
||||
)
|
||||
from .db import get_db
|
||||
from .db import get_db, infer_food_flavor_profile, infer_food_profile
|
||||
from .images import (
|
||||
allowed_image_file,
|
||||
save_photo_with_variants,
|
||||
@@ -1005,6 +1005,10 @@ def normalize_shopping_note(value: str | None) -> str:
|
||||
return " ".join((value or "").strip().split())[:80]
|
||||
|
||||
|
||||
def normalize_new_item_name(value: str | None) -> str:
|
||||
return " ".join((value or "").strip().split())[:120]
|
||||
|
||||
|
||||
def schedule_shopping_need(
|
||||
*,
|
||||
item_id: int,
|
||||
@@ -1298,8 +1302,8 @@ def fetch_items_by_ids(item_ids: list[int]) -> list[dict]:
|
||||
return [items_by_id[item_id] for item_id in normalized_ids if item_id in items_by_id]
|
||||
|
||||
|
||||
def find_shopping_food_by_name(name: str) -> dict | None:
|
||||
normalized_name = name.strip().lower()
|
||||
def find_shopping_item_by_name(name: str) -> dict | None:
|
||||
normalized_name = normalize_new_item_name(name).lower()
|
||||
if not normalized_name:
|
||||
return None
|
||||
row = get_db().execute(
|
||||
@@ -1317,11 +1321,11 @@ def find_shopping_food_by_name(name: str) -> dict | None:
|
||||
FROM items
|
||||
LEFT JOIN users AS owner ON owner.id = items.owner_user_id
|
||||
LEFT JOIN users AS target ON target.id = items.target_user_id
|
||||
WHERE items.kind = 'food'
|
||||
WHERE items.kind IN ('food', 'shopping')
|
||||
AND items.is_archived = 0
|
||||
AND LOWER(items.name) = ?
|
||||
AND {visible_clause('items')}
|
||||
ORDER BY LOWER(items.name), items.id
|
||||
ORDER BY CASE items.kind WHEN 'food' THEN 0 ELSE 1 END, LOWER(items.name), items.id
|
||||
LIMIT 1
|
||||
""",
|
||||
[normalized_name, *visible_params()],
|
||||
@@ -1331,6 +1335,64 @@ def find_shopping_food_by_name(name: str) -> dict | None:
|
||||
return attach_builder_keys(attach_dayparts(describe_records([row])))[0]
|
||||
|
||||
|
||||
def create_shopping_search_item(name: str, kind: str) -> dict:
|
||||
normalized_name = normalize_new_item_name(name)
|
||||
if not normalized_name:
|
||||
raise ValueError("Bitte gib zuerst einen Namen ein.")
|
||||
if kind not in {"food", "shopping"}:
|
||||
raise ValueError("Bitte wähle aus, ob es ein Lebensmittel oder ein Einkaufsartikel ist.")
|
||||
|
||||
existing = find_shopping_item_by_name(normalized_name)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
if kind == "food":
|
||||
profile = infer_food_profile(normalized_name, "Unsortiert", "neutral")
|
||||
category = "Unsortiert"
|
||||
note = "Aus der Einkaufssuche angelegt. Details später ergänzen."
|
||||
is_quick_added = 1
|
||||
else:
|
||||
profile = {
|
||||
"base_type": "neutral",
|
||||
"suggestion_role": "cooking",
|
||||
"suggestion_priority": "never",
|
||||
"can_be_meal_core": 0,
|
||||
}
|
||||
category = "Einkaufsartikel"
|
||||
note = "Einkaufsartikel ohne Rezeptlogik."
|
||||
is_quick_added = 0
|
||||
|
||||
cursor = get_db().execute(
|
||||
"""
|
||||
INSERT INTO items (
|
||||
household_id, owner_user_id, visibility, kind, name, category,
|
||||
base_type, flavor_profile, suggestion_role, suggestion_priority,
|
||||
can_be_meal_core, energy_density, availability_state, note,
|
||||
is_quick_added, created_by, updated_by
|
||||
)
|
||||
VALUES (?, ?, 'shared', ?, ?, ?, ?, ?, ?, ?, ?, 'neutral', 'idea', ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
current_household_id(),
|
||||
g.user["id"],
|
||||
kind,
|
||||
normalized_name,
|
||||
category,
|
||||
profile["base_type"],
|
||||
infer_food_flavor_profile(normalized_name, category, profile["base_type"], profile["suggestion_role"]),
|
||||
profile["suggestion_role"],
|
||||
profile["suggestion_priority"],
|
||||
profile["can_be_meal_core"],
|
||||
note,
|
||||
is_quick_added,
|
||||
g.user["id"],
|
||||
g.user["id"],
|
||||
),
|
||||
)
|
||||
get_db().commit()
|
||||
return get_item(int(cursor.lastrowid))
|
||||
|
||||
|
||||
def fetch_shopping_entries():
|
||||
rows = get_db().execute(
|
||||
f"""
|
||||
@@ -4390,6 +4452,7 @@ def mark_shopping_entry_checked(entry_id: int) -> dict:
|
||||
"UPDATE shopping_entries SET is_checked = 1, checked_at = CURRENT_TIMESTAMP, checked_by = ? WHERE id = ?",
|
||||
(g.user["id"], entry_id),
|
||||
)
|
||||
if item["kind"] != "shopping":
|
||||
get_db().execute(
|
||||
"""
|
||||
UPDATE items
|
||||
@@ -4495,6 +4558,9 @@ def item_mark_bought(item_id: int):
|
||||
except (ValueError, PermissionError) as exc:
|
||||
flash(str(exc), "error")
|
||||
return redirect(request.referrer or url_for("main.shopping_list"))
|
||||
if item["kind"] == "shopping":
|
||||
flash(f"{item['name']} wurde als eingekauft markiert.", "success")
|
||||
else:
|
||||
flash(f"{item['name']} ist jetzt als Zuhause vorhanden markiert.", "success")
|
||||
return redirect(request.referrer or url_for("main.shopping_list"))
|
||||
|
||||
@@ -4528,17 +4594,24 @@ def shopping_list():
|
||||
if request.method == "POST":
|
||||
selected_item_id = request.form.get("item_id", "").strip()
|
||||
item_search = request.form.get("item_search", "").strip()
|
||||
create_as = request.form.get("create_as", "").strip()
|
||||
create_item_name = normalize_new_item_name(request.form.get("create_item_name") or item_search)
|
||||
shopping_note = normalize_shopping_note(request.form.get("shopping_note"))
|
||||
item = None
|
||||
if selected_item_id.isdigit():
|
||||
if create_as in {"food", "shopping"}:
|
||||
try:
|
||||
item = create_shopping_search_item(create_item_name, create_as)
|
||||
except ValueError as exc:
|
||||
flash(str(exc), "error")
|
||||
elif selected_item_id.isdigit():
|
||||
try:
|
||||
item = get_item(int(selected_item_id))
|
||||
except ValueError as exc:
|
||||
flash(str(exc), "error")
|
||||
elif item_search:
|
||||
item = find_shopping_food_by_name(item_search)
|
||||
item = find_shopping_item_by_name(item_search)
|
||||
if item is None:
|
||||
flash("Bitte ein Lebensmittel aus der Suche auswählen.", "error")
|
||||
flash("Bitte einen Treffer auswählen oder den Begriff als Lebensmittel bzw. Einkaufsartikel anlegen.", "error")
|
||||
else:
|
||||
flash("Bitte zuerst etwas auswählen.", "error")
|
||||
|
||||
@@ -4560,7 +4633,10 @@ def shopping_list():
|
||||
|
||||
entries = fetch_shopping_entries()
|
||||
upcoming_entries = fetch_upcoming_shopping_needs()
|
||||
addable_items = fetch_items(kind="food", include_archived=False, include_quick_added=True)
|
||||
addable_items = [
|
||||
item for item in fetch_items(include_archived=False, include_quick_added=True)
|
||||
if item["kind"] in {"food", "shopping"}
|
||||
]
|
||||
household_settings = get_household_settings()
|
||||
shopping_weekday_label = dict(WEEKDAY_OPTIONS).get(household_settings["shopping_weekday"], "gesetzt")
|
||||
return render_template(
|
||||
@@ -4581,6 +4657,9 @@ def shopping_check(entry_id: int):
|
||||
except (ValueError, PermissionError) as exc:
|
||||
flash(str(exc), "error")
|
||||
return redirect(url_with_scroll_position(url_for("main.shopping_list")))
|
||||
if item["kind"] == "shopping":
|
||||
flash(f"{item['name']} wurde als eingekauft markiert.", "success")
|
||||
else:
|
||||
flash(f"{item['name']} ist jetzt als Zuhause vorhanden markiert.", "success")
|
||||
return redirect(url_with_scroll_position(url_for("main.shopping_list")))
|
||||
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ CREATE TABLE IF NOT EXISTS items (
|
||||
owner_user_id INTEGER,
|
||||
target_user_id INTEGER,
|
||||
visibility TEXT NOT NULL DEFAULT 'shared',
|
||||
kind TEXT NOT NULL CHECK (kind IN ('food', 'meal')),
|
||||
kind TEXT NOT NULL CHECK (kind IN ('food', 'meal', 'shopping')),
|
||||
name TEXT NOT NULL,
|
||||
category TEXT,
|
||||
base_type TEXT NOT NULL DEFAULT 'neutral',
|
||||
|
||||
@@ -1491,6 +1491,31 @@ h3 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.shopping-create-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.65rem;
|
||||
border-radius: 18px;
|
||||
background: color-mix(in srgb, var(--surface-soft) 76%, transparent 24%);
|
||||
border: 1px solid color-mix(in srgb, var(--line) 78%, transparent 22%);
|
||||
}
|
||||
|
||||
.shopping-create-actions[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shopping-create-actions p {
|
||||
flex: 1 1 16rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shopping-create-actions button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.shopping-add-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
@@ -1746,6 +1771,11 @@ h3 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shopping-create-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.shopping-entry-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
|
||||
@@ -219,6 +219,25 @@
|
||||
});
|
||||
};
|
||||
|
||||
const initCreateFromSearch = () => {
|
||||
document.querySelectorAll("[data-create-from]").forEach((container) => {
|
||||
const inputSelector = container.getAttribute("data-create-from");
|
||||
if (!inputSelector) return;
|
||||
const input = document.querySelector(inputSelector);
|
||||
const hiddenName = container.querySelector("[data-create-name]");
|
||||
if (!(input instanceof HTMLInputElement) || !(hiddenName instanceof HTMLInputElement)) return;
|
||||
|
||||
const sync = () => {
|
||||
const value = input.value.trim().replace(/\s+/g, " ");
|
||||
hiddenName.value = value;
|
||||
container.hidden = value.length === 0;
|
||||
};
|
||||
|
||||
input.addEventListener("input", sync);
|
||||
sync();
|
||||
});
|
||||
};
|
||||
|
||||
const initIosPullToRefresh = () => {
|
||||
const isAppleTouchDevice = /iP(ad|hone|od)/.test(navigator.userAgent)
|
||||
|| (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
@@ -323,6 +342,7 @@
|
||||
initMobileSheet();
|
||||
initFilterInputs();
|
||||
initSelectedPreviews();
|
||||
initCreateFromSearch();
|
||||
initIosPullToRefresh();
|
||||
initDialogs();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = "nouri-v1-3-2";
|
||||
const CACHE_NAME = "nouri-v1-3-3";
|
||||
const OFFLINE_URL = "/static/pwa/offline.html";
|
||||
const STATIC_ASSETS = [
|
||||
"/static/css/styles.css",
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
<form method="post" class="shopping-add-form">
|
||||
{{ csrf_input() }}
|
||||
<label>
|
||||
Lebensmittel suchen
|
||||
Lebensmittel oder Artikel suchen
|
||||
<input
|
||||
type="text"
|
||||
name="item_search"
|
||||
placeholder="Nach Lebensmitteln suchen"
|
||||
id="shopping-item-search"
|
||||
placeholder="Nach Lebensmitteln oder Artikeln suchen"
|
||||
autocomplete="off"
|
||||
data-filter-input
|
||||
data-filter-target="#shopping-food-options"
|
||||
@@ -46,12 +47,15 @@
|
||||
'seeds': 'icon-component-seeds',
|
||||
'neutral': 'icon-component-neutral',
|
||||
}.get(item.primary_builder_key or item.base_type, 'icon-component-neutral') %}
|
||||
{% if item.kind == 'shopping' %}
|
||||
{% set item_icon_class = 'icon-cart-shopping' %}
|
||||
{% endif %}
|
||||
<button
|
||||
class="shopping-add-card"
|
||||
type="submit"
|
||||
name="item_id"
|
||||
value="{{ item.id }}"
|
||||
data-filter-label="{{ item.name|lower }} {{ item.category|default('', true)|lower }} {{ item.base_type_label|lower }} {{ item.availability_label|lower }}"
|
||||
data-filter-label="{{ item.name|lower }} {{ item.category|default('', true)|lower }} {{ item.base_type_label|lower }} {{ item.availability_label|lower }} {% if item.kind == 'shopping' %}einkaufsartikel artikel drogerie haushalt{% endif %}"
|
||||
>
|
||||
<span class="shopping-add-card-visual">
|
||||
{% if item.photo_filename %}
|
||||
@@ -69,11 +73,17 @@
|
||||
</span>
|
||||
<span class="shopping-add-card-copy">
|
||||
<strong>{{ item.name }}</strong>
|
||||
<small>{{ item.availability_label }}</small>
|
||||
<small>{% if item.kind == 'shopping' %}Einkaufsartikel{% else %}{{ item.availability_label }}{% endif %}</small>
|
||||
</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="shopping-create-actions" data-create-from="#shopping-item-search" hidden>
|
||||
<input type="hidden" name="create_item_name" data-create-name>
|
||||
<p class="muted">Kein passender Treffer? Direkt aus dem Suchbegriff anlegen:</p>
|
||||
<button class="ghost-button" type="submit" name="create_as" value="food">Als Lebensmittel anlegen</button>
|
||||
<button class="ghost-button" type="submit" name="create_as" value="shopping">Als Einkaufsartikel anlegen</button>
|
||||
</div>
|
||||
<button type="submit">Auf die Liste</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -97,6 +107,9 @@
|
||||
'seeds': 'icon-component-seeds',
|
||||
'neutral': 'icon-component-neutral',
|
||||
}.get(entry.primary_builder_key or entry.base_type, 'icon-component-neutral') %}
|
||||
{% if entry.item_kind == 'shopping' %}
|
||||
{% set entry_icon_class = 'icon-cart-shopping' %}
|
||||
{% endif %}
|
||||
<article class="shopping-entry-card">
|
||||
<div class="shopping-entry-row">
|
||||
<div
|
||||
@@ -165,6 +178,8 @@
|
||||
{% elif entry.needed_for_label %}
|
||||
Für {{ entry.needed_for_label }}
|
||||
{% if entry.needed_daypart_name %} · {{ entry.needed_daypart_name }}{% endif %}
|
||||
{% elif entry.item_kind == 'shopping' %}
|
||||
Einkaufsartikel
|
||||
{% elif entry.is_home %}
|
||||
Zuhause vorhanden
|
||||
{% else %}
|
||||
@@ -175,7 +190,7 @@
|
||||
<button class="ghost-button" type="button" data-dialog-close>Schließen</button>
|
||||
</div>
|
||||
<div class="shopping-entry-dialog-actions">
|
||||
{% if entry.can_edit %}
|
||||
{% if entry.can_edit and entry.item_kind != 'shopping' %}
|
||||
<a class="ghost-button" href="{{ url_for('main.item_edit', item_id=entry.item_id) }}">Bearbeiten</a>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('main.shopping_check', entry_id=entry.id) }}">
|
||||
@@ -201,22 +216,24 @@
|
||||
{{ csrf_input() }}
|
||||
<button class="ghost-button" type="submit">Von Einkaufsliste nehmen</button>
|
||||
</form>
|
||||
{% if entry.is_home %}
|
||||
{% if entry.item_kind != 'shopping' and entry.is_home %}
|
||||
<form method="post" action="{{ url_for('main.item_set_not_home', item_id=entry.item_id) }}">
|
||||
{{ csrf_input() }}
|
||||
<button class="ghost-button" type="submit">Nicht mehr da</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% elif entry.item_kind != 'shopping' %}
|
||||
<form method="post" action="{{ url_for('main.item_set_home', item_id=entry.item_id) }}">
|
||||
{{ csrf_input() }}
|
||||
<button class="ghost-button" type="submit">Als zuhause markieren</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if entry.item_kind != 'shopping' %}
|
||||
<form method="post" action="{{ url_for('main.item_archive', item_id=entry.item_id) }}">
|
||||
{{ csrf_input() }}
|
||||
<button class="ghost-button" type="submit">Archivieren</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
Reference in New Issue
Block a user