Files
wtc.waiswuaschd.de/app/templates/shopping_list.html
T

1541 lines
45 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Einkaufsliste – What To Cook?</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛒</text></svg>">
<style>
:root {
--shop-bg: #FDFBF7;
--shop-card: #FFFFFF;
--shop-accent: #1E4D3B;
--shop-accent-hover: #163A2C;
--shop-border: #222222;
--shop-subtle: rgba(34, 34, 34, 0.08);
--shop-yellow: #FFF9D2;
}
* {
box-sizing: border-box;
}
body.shopping-body {
background-color: var(--creme, #FDFBF7);
background-image:
radial-gradient(rgba(30, 77, 59, 0.05) 1px, transparent 1px),
linear-gradient(to right, rgba(0,0,0,0.02) 1px, transparent 1px);
background-size: 24px 24px, 100% 100%;
min-height: 100vh;
margin: 0;
padding: 0;
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
color: var(--schwarz, #222222);
display: flex;
flex-direction: column;
overflow-x: hidden;
width: 100%;
}
/* ========================================== */
/* TOP NAVIGATION (Harmonisch & Minimalistisch)*/
/* ========================================== */
.shop-nav {
background-color: #FFFFFF;
border-bottom: 2.5px solid var(--shop-border);
padding: 10px 16px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
width: 100%;
}
.shop-nav-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--creme, #FDFBF7);
border: 2px solid var(--shop-border);
border-radius: 8px;
padding: 7px 12px;
font-size: 0.9rem;
font-weight: 700;
color: var(--shop-border);
text-decoration: none;
box-shadow: 2px 2px 0px var(--shop-border);
transition: all 0.12s ease;
cursor: pointer;
user-select: none;
}
.shop-nav-btn:hover {
background: #FFFFFF;
transform: translateY(-1px);
box-shadow: 3px 3px 0px var(--shop-border);
}
.shop-nav-btn:active {
transform: translate(1px, 1px);
box-shadow: 1px 1px 0px var(--shop-border);
}
.shop-nav-center {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.shop-nav-title {
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 1.85rem;
font-weight: 700;
color: var(--shop-accent);
margin: 0;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ========================================== */
/* HAUPTCONTAINER (Notizblock-Charakter) */
/* ========================================== */
.shop-main {
max-width: 760px;
width: 100%;
margin: 20px auto 60px auto;
padding: 0 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
.notepad-card {
background: #FFFFFF;
border: 2.5px solid var(--shop-border);
border-radius: 14px;
box-shadow: 4px 5px 0px var(--shop-border);
position: relative;
padding: 22px 20px;
width: 100%;
}
/* Rote Randlinie für subtilen Notizblock-Touch */
.notepad-card::before {
content: '';
position: absolute;
top: 0; bottom: 0; left: 34px;
width: 1.5px;
background-color: rgba(210, 155, 155, 0.35);
pointer-events: none;
}
/* Header des Blocks */
.notepad-header {
margin-bottom: 18px;
padding-bottom: 14px;
border-bottom: 2px dashed rgba(30, 77, 59, 0.25);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.notepad-title-group h2 {
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 2.2rem;
color: var(--shop-accent);
margin: 0;
line-height: 1.1;
}
.notepad-subtitle {
font-size: 0.88rem;
color: #666;
margin: 2px 0 0 0;
}
.notepad-counter {
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 1.15rem;
font-weight: 700;
color: var(--shop-accent);
background: var(--shop-yellow);
border: 1.5px solid var(--shop-border);
padding: 4px 10px;
border-radius: 8px;
box-shadow: 2px 2px 0px rgba(0,0,0,0.1);
}
/* ========================================== */
/* SCHNELLEINGABE */
/* ========================================== */
.shop-add-form {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
background: var(--shop-yellow);
border: 2px solid var(--shop-border);
border-radius: 12px;
padding: 10px;
box-shadow: 3px 3px 0px rgba(0,0,0,0.12);
}
.shop-add-top {
display: flex;
gap: 8px;
align-items: center;
}
.shop-input {
flex: 1;
min-width: 0;
border: 1.5px solid rgba(0,0,0,0.25);
border-radius: 8px;
background: #FFFFFF;
font-family: var(--font-sans);
font-size: 1rem;
padding: 9px 12px;
outline: none;
color: var(--shop-border);
transition: border-color 0.15s;
}
.shop-input:focus {
border-color: var(--shop-accent);
box-shadow: 0 0 0 2px rgba(30, 77, 59, 0.15);
}
.shop-input::placeholder {
color: #8c8260;
}
.shop-add-btn {
background: var(--shop-accent);
color: var(--creme, #FDFBF7);
border: 2px solid var(--shop-border);
border-radius: 8px;
padding: 9px 16px;
font-weight: 700;
font-size: 0.95rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
box-shadow: 2px 2px 0px var(--shop-border);
transition: all 0.12s;
}
.shop-add-btn:hover {
background: var(--shop-accent-hover);
transform: translateY(-1px);
box-shadow: 3px 3px 0px var(--shop-border);
}
.shop-add-btn:active {
transform: translate(1px, 1px);
box-shadow: 1px 1px 0px var(--shop-border);
}
.shop-add-bottom {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
color: #555;
}
.shop-cat-select {
background: #FFFFFF;
border: 1.5px solid rgba(0,0,0,0.25);
border-radius: 6px;
padding: 5px 8px;
font-size: 0.85rem;
font-weight: 600;
color: var(--shop-border);
outline: none;
cursor: pointer;
}
/* ========================================== */
/* TOOLBAR AKTIONEN */
/* ========================================== */
.shop-actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1.5px dashed rgba(0,0,0,0.15);
}
.shop-action-btn {
background: #FFFFFF;
border: 1.5px solid var(--shop-border);
border-radius: 6px;
padding: 6px 10px;
font-size: 0.82rem;
font-weight: 600;
color: #444;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 5px;
box-shadow: 1.5px 1.5px 0px rgba(0,0,0,0.1);
transition: all 0.12s;
text-decoration: none;
}
.shop-action-btn:hover {
background: var(--creme, #FDFBF7);
color: #111;
transform: translateY(-1px);
}
.shop-action-btn:active {
transform: translate(1px, 1px);
box-shadow: none;
}
.shop-action-btn.btn-wochenplan {
background: rgba(30, 77, 59, 0.08);
border-color: var(--shop-accent);
color: var(--shop-accent);
font-weight: 700;
}
.shop-action-btn.btn-wochenplan:hover {
background: rgba(30, 77, 59, 0.16);
}
/* ========================================== */
/* SUPERMARKT-BEREICHE & ZUTATEN-ITEMS */
/* ========================================== */
.category-sections-container {
display: flex;
flex-direction: column;
gap: 18px;
}
.category-group {
background: #FAF8F2;
border: 2px solid var(--shop-border);
border-radius: 10px;
padding: 10px 12px 6px 12px;
box-shadow: 2px 3px 0px rgba(0,0,0,0.08);
transition: all 0.2s;
}
.category-group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 6px;
margin-bottom: 6px;
border-bottom: 1.5px solid rgba(0,0,0,0.08);
}
.category-title-left {
display: flex;
align-items: center;
gap: 7px;
}
.category-icon {
font-size: 1.25rem;
line-height: 1;
}
.category-name {
font-family: var(--font-serif, 'Lora', serif);
font-size: 1.05rem;
font-weight: 700;
color: var(--shop-accent);
margin: 0;
}
.category-count {
background: var(--shop-yellow);
border: 1px solid rgba(0,0,0,0.2);
border-radius: 12px;
padding: 1px 7px;
font-size: 0.75rem;
font-weight: 700;
color: #555;
}
/* Items Liste */
.category-items-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
/* Einzelne Zutat-Zeile */
.shopping-item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 10px;
background: #FFFFFF;
border: 1.5px solid rgba(0,0,0,0.12);
border-radius: 8px;
transition: all 0.15s;
min-width: 0; /* Verhindert Flex-Overflow */
}
.shopping-item-row:hover {
border-color: rgba(30, 77, 59, 0.4);
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
}
.shopping-item-row.is-checked {
opacity: 0.55;
background: rgba(0, 0, 0, 0.02);
border-color: rgba(0,0,0,0.08);
}
.shopping-item-row.is-checked .item-text {
text-decoration: line-through;
color: #777;
}
.item-left {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0; /* Essenziell für sauberen Textumbruch */
cursor: pointer;
user-select: none;
}
.item-checkbox {
width: 22px;
height: 22px;
border: 2px solid var(--shop-border);
border-radius: 6px;
cursor: pointer;
accent-color: var(--shop-accent);
flex-shrink: 0;
}
.item-text-wrapper {
flex: 1;
min-width: 0; /* Zwingt Text zum Umbruch */
display: flex;
flex-direction: column;
gap: 2px;
}
.item-text {
font-size: 1rem;
font-weight: 500;
color: #222;
line-height: 1.35;
/* Radikaler Umbruch für lange Zutatentexte */
word-break: break-word;
overflow-wrap: anywhere;
white-space: normal;
}
.item-amount-badge {
font-weight: 700;
color: var(--shop-accent);
margin-right: 4px;
}
.item-sub-info {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 2px;
}
/* Schlanker Quell-Badge: Nur Rezepttitel */
.item-source-badge {
font-size: 0.72rem;
background: rgba(30, 77, 59, 0.08);
color: var(--shop-accent);
border: 1px solid rgba(30, 77, 59, 0.2);
padding: 1px 6px;
border-radius: 4px;
display: inline-flex;
align-items: center;
gap: 3px;
max-width: 240px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Klickbares Kategorie-Pill am Item */
.item-cat-pill {
font-size: 0.72rem;
background: rgba(0,0,0,0.04);
color: #666;
border: 1px dashed rgba(0,0,0,0.25);
padding: 1px 6px;
border-radius: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 3px;
transition: all 0.12s;
}
.item-cat-pill:hover {
background: var(--shop-yellow);
border-color: var(--shop-border);
color: #222;
}
.item-delete-btn {
background: transparent;
border: none;
color: #999;
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
transition: all 0.15s;
flex-shrink: 0;
}
.item-delete-btn:hover {
color: #c00;
background: rgba(200, 0, 0, 0.08);
}
/* ========================================== */
/* SEKTION: ERLEDIGTE EINTRÄGE */
/* ========================================== */
.completed-section {
margin-top: 10px;
background: #F8F7F3;
border: 1.5px dashed rgba(0,0,0,0.25);
border-radius: 10px;
padding: 12px;
}
.completed-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
}
.completed-title {
font-family: var(--font-serif, 'Lora', serif);
font-size: 0.95rem;
font-weight: 700;
color: #777;
margin: 0;
display: flex;
align-items: center;
gap: 6px;
}
/* ========================================== */
/* LEERER ZUSTAND */
/* ========================================== */
.shopping-empty-state {
text-align: center;
padding: 40px 16px;
}
.empty-icon {
font-size: 3.2rem;
margin-bottom: 10px;
}
.empty-title {
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 2.2rem;
color: var(--shop-accent);
margin: 0 0 6px 0;
}
.empty-desc {
color: #666;
font-size: 0.95rem;
max-width: 440px;
margin: 0 auto 20px auto;
line-height: 1.4;
}
/* ========================================== */
/* MODAL: BEREICHE VERWALTEN (SUPERMARKT) */
/* ========================================== */
.modal-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
padding: 16px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.modal-backdrop.active {
opacity: 1;
pointer-events: auto;
}
.modal-card {
background: #FFFDF7;
border: 2.5px solid var(--shop-border);
border-radius: 14px;
box-shadow: 6px 6px 0px var(--shop-border);
max-width: 520px;
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
transform: translateY(20px);
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.modal-backdrop.active .modal-card {
transform: translateY(0);
}
.modal-header {
background: var(--creme, #FDFBF7);
border-bottom: 2px solid var(--shop-border);
padding: 12px 18px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-family: var(--font-serif, 'Lora', serif);
font-size: 1.25rem;
font-weight: 700;
color: var(--shop-accent);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.modal-close-btn {
background: #FFFFFF;
border: 1.5px solid var(--shop-border);
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
color: #555;
transition: all 0.15s;
}
.modal-close-btn:hover {
background: #fce8e8;
color: #c00;
}
.modal-body {
padding: 16px 18px;
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
.cat-manage-list {
display: flex;
flex-direction: column;
gap: 8px;
list-style: none;
margin: 0;
padding: 0;
}
.cat-manage-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: #FFFFFF;
border: 1.5px solid var(--shop-border);
border-radius: 8px;
padding: 8px 10px;
box-shadow: 2px 2px 0px rgba(0,0,0,0.08);
}
.cat-manage-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.cat-manage-icon {
font-size: 1.25rem;
width: 28px;
text-align: center;
}
.cat-manage-name {
font-weight: 600;
font-size: 0.95rem;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cat-manage-actions {
display: flex;
align-items: center;
gap: 4px;
}
.cat-reorder-btn {
background: #FFFFFF;
border: 1px solid rgba(0,0,0,0.3);
border-radius: 4px;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.75rem;
font-weight: bold;
color: #444;
transition: all 0.12s;
}
.cat-reorder-btn:hover:not(:disabled) {
background: var(--shop-accent);
color: #FFF;
border-color: var(--shop-accent);
}
.cat-reorder-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.cat-del-btn {
background: transparent;
border: none;
color: #999;
font-size: 1.1rem;
cursor: pointer;
padding: 4px 6px;
border-radius: 4px;
transition: color 0.15s;
}
.cat-del-btn:hover {
color: #c00;
}
/* Formular Neue Kategorie */
.add-cat-form {
display: flex;
gap: 6px;
background: var(--shop-yellow);
border: 1.5px solid var(--shop-border);
border-radius: 8px;
padding: 8px;
}
.add-cat-icon-input {
width: 44px;
text-align: center;
border: 1px solid rgba(0,0,0,0.3);
border-radius: 6px;
font-size: 1.1rem;
outline: none;
}
.add-cat-name-input {
flex: 1;
border: 1px solid rgba(0,0,0,0.3);
border-radius: 6px;
padding: 6px 8px;
font-size: 0.9rem;
outline: none;
}
.add-cat-btn {
background: var(--shop-accent);
color: #FFF;
border: 1.5px solid var(--shop-border);
border-radius: 6px;
padding: 6px 12px;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
}
/* ========================================== */
/* TOAST NOTIFICATION */
/* ========================================== */
.shop-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--shop-accent);
color: var(--creme, #FDFBF7);
padding: 10px 20px;
border-radius: 30px;
font-weight: 600;
font-size: 0.92rem;
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
border: 2px solid var(--shop-border);
z-index: 9999;
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
white-space: nowrap;
}
.shop-toast.active {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* ========================================== */
/* RESPONSIVE DESIGN (MOBILE OPTIMIERT) */
/* ========================================== */
@media (max-width: 600px) {
.shop-nav {
padding: 8px 12px;
}
.shop-nav-btn {
padding: 6px 10px;
font-size: 0.82rem;
}
.shop-nav-title {
font-size: 1.45rem;
}
.shop-main {
margin: 12px auto 40px auto;
padding: 0 10px;
}
.notepad-card {
padding: 14px 12px;
border-radius: 10px;
}
.notepad-card::before {
left: 20px;
}
.notepad-header {
margin-bottom: 12px;
padding-bottom: 10px;
}
.notepad-title-group h2 {
font-size: 1.75rem;
}
.notepad-counter {
font-size: 0.95rem;
padding: 2px 8px;
}
.shop-add-form {
padding: 8px;
gap: 6px;
margin-bottom: 14px;
}
.shop-input {
font-size: 0.92rem;
padding: 7px 10px;
}
.shop-add-btn {
padding: 7px 12px;
font-size: 0.88rem;
}
.shopping-item-row {
padding: 8px;
gap: 8px;
}
.item-checkbox {
width: 20px;
height: 20px;
}
.item-text {
font-size: 0.95rem;
}
.category-name {
font-size: 0.95rem;
}
.item-source-badge {
max-width: 170px;
}
}
</style>
</head>
<body class="shopping-body">
<!-- Schlanke, stimmige Kopfzeile: Nur EIN Zurück-Button, kein doppeltes X -->
<header class="shop-nav">
<a href="/" class="shop-nav-btn" title="Zurück zum Startmenü">
<span>⬅️</span> <span>Zurück</span>
</a>
<div class="shop-nav-center">
<h1 class="shop-nav-title">🛒 Einkaufsliste</h1>
</div>
<!-- Rechts: Bereiche anpassen Button -->
<button type="button" class="shop-nav-btn" onclick="openCategoriesModal()" title="Supermarkt-Bereiche & Reihenfolge anpassen">
<span>⚙️</span> <span>Bereiche</span>
</button>
</header>
<!-- Notizblock Hauptcontainer -->
<main class="shop-main">
<div class="notepad-card">
<!-- Block-Kopf -->
<div class="notepad-header">
<div class="notepad-title-group">
<h2>Einkaufszettel</h2>
<p class="notepad-subtitle">Nach Supermarkt-Gängen sortiert • Woche {{ current_week }}</p>
</div>
<div class="notepad-counter" id="shop-counter">
<span id="checked-count">0</span> / <span id="total-count">{{ items|length }}</span> erledigt
</div>
</div>
<!-- Schnelleingabe mit optionaler Bereich-Wahl -->
<form class="shop-add-form" id="shop-add-form" onsubmit="onAddItemSubmit(event)">
<div class="shop-add-top">
<input
type="text"
id="shop-add-input"
class="shop-input"
placeholder="✏️ Zutat oder Artikel notieren (z. B. 500g Mehl)..."
autocomplete="off"
required
>
<button type="submit" class="shop-add-btn" id="shop-add-submit-btn">
<span>➕</span> <span>Hinzufügen</span>
</button>
</div>
<div class="shop-add-bottom">
<span>Bereich:</span>
<select id="shop-add-category" class="shop-cat-select" title="Supermarkt-Bereich auswählen">
<option value="">🪄 Automatisch erkennen</option>
{% for cat in categories %}
<option value="{{ cat.name }}">{{ cat.icon }} {{ cat.name }}</option>
{% endfor %}
</select>
</div>
</form>
<!-- Aktionen-Toolbar -->
<div class="shop-actions-bar">
<div>
<a href="/cookbook?open=mealplan&prompt_transfer=1" class="shop-action-btn btn-wochenplan" title="Zutaten aus dem Wochenplan abrufen">
<span>📅</span> <span>Aus Wochenplan übernehmen</span>
</a>
</div>
<div style="display: flex; gap: 6px;">
<button type="button" class="shop-action-btn" onclick="deleteCheckedItems()" title="Erledigte Einträge von der Liste entfernen">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
<button type="button" class="shop-action-btn" onclick="clearAllItems()" title="Gesamte Liste leeren">
<span>🗑️</span> <span>Alles leeren</span>
</button>
</div>
</div>
<!-- Gruppierte Ansicht nach Supermarkt-Bereichen -->
<div class="category-sections-container" id="category-sections-container">
<!-- Wird dynamisch durch renderShoppingList() befüllt -->
</div>
<!-- Bereich für Erledigte Einträge (wird am Ende angezeigt) -->
<div class="completed-section" id="completed-section" style="display: none;">
<div class="completed-header" onclick="toggleCompletedGroup()">
<h4 class="completed-title">
<span id="completed-toggle-icon">▼</span>
<span>✓ Bereits im Einkaufswagen (<span id="completed-badge-count">0</span>)</span>
</h4>
<button type="button" class="shop-action-btn" onclick="event.stopPropagation(); deleteCheckedItems();" style="padding: 2px 6px; font-size: 0.75rem;">
🧹 Löschen
</button>
</div>
<ul class="category-items-list" id="completed-items-list" style="margin-top: 8px;">
<!-- Erledigte Items -->
</ul>
</div>
<!-- Leerer Zustand -->
<div class="shopping-empty-state" id="shopping-empty-state" style="display: none;">
<div class="empty-icon">🎉</div>
<h3 class="empty-title">Alles eingekauft!</h3>
<p class="empty-desc">Deine Einkaufsliste ist momentan leer. Trage oben neue Besorgungen ein oder übernimm die Zutaten direkt aus deinem Wochenplan.</p>
<a href="/cookbook?open=mealplan&prompt_transfer=1" class="shop-action-btn btn-wochenplan" style="padding: 10px 20px; font-size: 0.95rem;">
<span>📅</span> <span>Zutaten aus Wochenplan holen</span>
</a>
</div>
</div>
</main>
<!-- ========================================== -->
<!-- MODAL: SUPERMARKT-BEREICHE VERWALTEN -->
<!-- ========================================== -->
<div id="modal-categories" class="modal-backdrop" onclick="onModalBackdropClick(event, 'modal-categories')">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title"><span>🛒</span> Supermarkt-Laufweg anpassen</h3>
<button type="button" class="modal-close-btn" onclick="closeCategoriesModal()">&times;</button>
</div>
<div class="modal-body">
<p style="font-size: 0.88rem; color: #666; margin: 0;">
Passe die Bereiche an deinen gewohnten Supermarkt an. Mit den Pfeilen <strong>▲</strong> und <strong>▼</strong> bestimmst du die Reihenfolge deiner Gänge beim Einkaufen.
</p>
<!-- Liste der Kategorien -->
<ul class="cat-manage-list" id="cat-manage-list">
<!-- Dynamisch -->
</ul>
<!-- Neuen Bereich hinzufügen -->
<div>
<h4 style="margin: 0 0 6px 0; font-size: 0.95rem; font-family: var(--font-serif); color: var(--shop-accent);">
Neuen Bereich hinzufügen
</h4>
<form class="add-cat-form" onsubmit="onAddCategorySubmit(event)">
<input type="text" id="new-cat-icon" class="add-cat-icon-input" value="🛒" maxlength="4" title="Emoji-Icon">
<input type="text" id="new-cat-name" class="add-cat-name-input" placeholder="Name des Bereichs (z. B. Drogerie)..." required>
<button type="submit" class="add-cat-btn">➕ Hinzufügen</button>
</form>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 6px;">
<button type="button" class="shop-action-btn" onclick="resetDefaultCategories()" style="font-size: 0.78rem; color: #888;">
↺ Standard-Bereiche wiederherstellen
</button>
</div>
</div>
</div>
</div>
<!-- Toast Notification -->
<div id="shop-toast" class="shop-toast">✓ Gespeichert</div>
<!-- ========================================== -->
<!-- JAVASCRIPT LOGIK -->
<!-- ========================================== -->
<script>
// Initialer State
let shoppingItems = {{ items_json|safe if items_json is defined else '[]' }};
let shoppingCategories = {{ categories_json|safe if categories_json is defined else '[]' }};
let completedExpanded = true;
function showToast(msg) {
const toast = document.getElementById('shop-toast');
toast.textContent = msg;
toast.classList.add('active');
setTimeout(() => toast.classList.remove('active'), 2500);
}
function updateCounter() {
const total = shoppingItems.length;
const checked = shoppingItems.filter(it => it.checked).length;
document.getElementById('checked-count').textContent = checked;
document.getElementById('total-count').textContent = total;
document.getElementById('completed-badge-count').textContent = checked;
const emptyState = document.getElementById('shopping-empty-state');
const catContainer = document.getElementById('category-sections-container');
const completedSec = document.getElementById('completed-section');
if (total === 0) {
emptyState.style.display = 'block';
catContainer.style.display = 'none';
completedSec.style.display = 'none';
} else {
emptyState.style.display = 'none';
catContainer.style.display = 'flex';
completedSec.style.display = checked > 0 ? 'block' : 'none';
}
}
// Rendert die Einkaufsliste gruppiert nach Supermarkt-Bereichen
function renderShoppingList() {
const container = document.getElementById('category-sections-container');
const completedList = document.getElementById('completed-items-list');
container.innerHTML = '';
completedList.innerHTML = '';
// Unfertige Items nach Kategorie gruppieren
const openItems = shoppingItems.filter(it => !it.checked);
const checkedItems = shoppingItems.filter(it => it.checked);
// Map aller bekannten Kategorien
const catMap = new Map();
shoppingCategories.forEach(cat => {
catMap.set(cat.name, {
category: cat,
items: []
});
});
// Zuweisen der offenen Items zu den Kategorien
const uncategorizedItems = [];
openItems.forEach(item => {
const catName = item.category || 'Drogerie & Sonstiges';
if (catMap.has(catName)) {
catMap.get(catName).items.push(item);
} else {
// Kategorie existiert nicht mehr in der Liste
if (!catMap.has('Sonstiges')) {
catMap.set(catName, {
category: { name: catName, icon: '📦', sort_order: 999 },
items: [item]
});
} else {
catMap.get('Sonstiges').items.push(item);
}
}
});
// Für jede Kategorie mit Items eine Gruppe rendern
let renderedCategoryCount = 0;
shoppingCategories.forEach(cat => {
const group = catMap.get(cat.name);
if (group && group.items.length > 0) {
renderedCategoryCount++;
const groupEl = document.createElement('div');
groupEl.className = 'category-group';
groupEl.id = `cat-group-${encodeURIComponent(cat.name)}`;
groupEl.innerHTML = `
<div class="category-group-header">
<div class="category-title-left">
<span class="category-icon">${escapeHtml(cat.icon || '🛒')}</span>
<h3 class="category-name">${escapeHtml(cat.name)}</h3>
</div>
<span class="category-count">${group.items.length}</span>
</div>
<ul class="category-items-list" id="cat-items-${encodeURIComponent(cat.name)}"></ul>
`;
const ul = groupEl.querySelector('.category-items-list');
group.items.forEach(item => {
ul.appendChild(createItemElement(item));
});
container.appendChild(groupEl);
}
});
// Falls es Kategorien außerhalb der Hauptliste gibt
catMap.forEach((group, catName) => {
if (!shoppingCategories.some(c => c.name === catName) && group.items.length > 0) {
const groupEl = document.createElement('div');
groupEl.className = 'category-group';
groupEl.innerHTML = `
<div class="category-group-header">
<div class="category-title-left">
<span class="category-icon">${escapeHtml(group.category.icon || '📦')}</span>
<h3 class="category-name">${escapeHtml(group.category.name)}</h3>
</div>
<span class="category-count">${group.items.length}</span>
</div>
<ul class="category-items-list"></ul>
`;
const ul = groupEl.querySelector('.category-items-list');
group.items.forEach(item => {
ul.appendChild(createItemElement(item));
});
container.appendChild(groupEl);
}
});
// Erledigte Items rendern
checkedItems.forEach(item => {
completedList.appendChild(createItemElement(item));
});
updateCounter();
}
// Erstellt das DOM-Element für ein Einkaufslisten-Item
function createItemElement(item) {
const li = document.createElement('li');
li.className = `shopping-item-row ${item.checked ? 'is-checked' : ''}`;
li.id = `shop-item-${item.id}`;
li.setAttribute('data-id', item.id);
const amountStr = (item.amount || item.unit) ? `<span class="item-amount-badge">${escapeHtml(item.amount || '')} ${escapeHtml(item.unit || '')}</span>` : '';
// Saubere Quelle: Nur Rezepttitel
const displaySource = item.display_source || cleanRecipeSource(item.source);
const sourceStr = displaySource ? `<span class="item-source-badge" title="${escapeHtml(displaySource)}">📖 ${escapeHtml(displaySource)}</span>` : '';
// Kategorie-Pill
const currentCatObj = shoppingCategories.find(c => c.name === item.category);
const catIcon = currentCatObj ? currentCatObj.icon : '🛒';
const catName = item.category || 'Sonstiges';
li.innerHTML = `
<label class="item-left">
<input
type="checkbox"
class="item-checkbox"
${item.checked ? 'checked' : ''}
onchange="toggleItem(${item.id})"
>
<div class="item-text-wrapper">
<span class="item-text">
${amountStr}${escapeHtml(item.item_name)}
</span>
<div class="item-sub-info">
${sourceStr}
<span class="item-cat-pill" onclick="promptChangeItemCategory(event, ${item.id}, '${escapeHtml(catName)}')" title="Bereich ändern">
<span>${escapeHtml(catIcon)}</span>
<span>${escapeHtml(catName)}</span>
<span style="font-size: 0.65rem; opacity: 0.7;">▼</span>
</span>
</div>
</div>
</label>
<button type="button" class="item-delete-btn" onclick="deleteSingleItem(${item.id})" title="Löschen">&times;</button>
`;
return li;
}
function cleanRecipeSource(src) {
if (!src) return '';
const m = String(src).match(/\((.+)\)$/);
if (m && m[1]) return m[1].trim();
return String(src).replace(/^Wochenplan\s*KW\s*\d+\s*/i, '').trim();
}
// ==========================================
// AKTIONEN: ITEMS HINZUFÜGEN / TOGGLE / LÖSCHEN
// ==========================================
async function onAddItemSubmit(e) {
e.preventDefault();
const input = document.getElementById('shop-add-input');
const catSelect = document.getElementById('shop-add-category');
const text = input.value.trim();
const chosenCat = catSelect.value;
if (!text) return;
const submitBtn = document.getElementById('shop-add-submit-btn');
submitBtn.disabled = true;
try {
const payload = {
item_name: text,
source: 'Manuell'
};
if (chosenCat) {
payload.category = chosenCat;
}
const res = await fetch('/api/shopping-list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.status === 'ok' && data.item) {
shoppingItems.unshift(data.item);
input.value = '';
renderShoppingList();
showToast(`✓ "${text}" hinzugefügt`);
}
} catch (err) {
alert('Fehler beim Hinzufügen: ' + err.message);
} finally {
submitBtn.disabled = false;
}
}
async function toggleItem(id) {
const it = shoppingItems.find(x => x.id === id);
if (!it) return;
// Optimistisches UI Update
it.checked = !it.checked;
renderShoppingList();
try {
const res = await fetch(`/api/shopping-list/${id}/toggle`, { method: 'POST' });
const data = await res.json();
if (data.status === 'ok' && data.item) {
it.checked = Boolean(data.item.checked);
renderShoppingList();
}
} catch (err) {
console.error(err);
}
}
async function deleteSingleItem(id) {
shoppingItems = shoppingItems.filter(x => x.id !== id);
renderShoppingList();
try {
await fetch(`/api/shopping-list/${id}`, { method: 'DELETE' });
} catch (err) {
console.error(err);
}
}
async function deleteCheckedItems() {
const hasChecked = shoppingItems.some(x => x.checked);
if (!hasChecked) {
showToast('Keine erledigten Einträge vorhanden.');
return;
}
shoppingItems = shoppingItems.filter(x => !x.checked);
renderShoppingList();
showToast('✓ Erledigte Einträge gelöscht');
try {
await fetch('/api/shopping-list/checked', { method: 'DELETE' });
} catch (err) {
console.error(err);
}
}
async function clearAllItems() {
if (shoppingItems.length === 0) return;
if (!confirm('Möchtest du wirklich die gesamte Einkaufsliste leeren?')) return;
shoppingItems = [];
renderShoppingList();
showToast('✓ Einkaufsliste geleert');
try {
await fetch('/api/shopping-list/all', { method: 'DELETE' });
} catch (err) {
console.error(err);
}
}
// Schnelles Ändern der Kategorie an einem Item
async function promptChangeItemCategory(event, itemId, currentCat) {
event.preventDefault();
event.stopPropagation();
const catNames = shoppingCategories.map(c => `${c.icon} ${c.name}`);
const optionsStr = catNames.map((c, i) => `${i + 1}. ${c}`).join('\n');
const input = prompt(`Supermarkt-Bereich auswählen:\n\n${optionsStr}\n\nNummer oder Namen eingeben:`, currentCat);
if (!input) return;
let targetCatName = '';
const num = parseInt(input.trim(), 10);
if (!isNaN(num) && num >= 1 && num <= shoppingCategories.length) {
targetCatName = shoppingCategories[num - 1].name;
} else {
const found = shoppingCategories.find(c => c.name.toLowerCase() === input.trim().toLowerCase());
targetCatName = found ? found.name : input.trim();
}
if (!targetCatName) return;
const it = shoppingItems.find(x => x.id === itemId);
if (it) {
it.category = targetCatName;
renderShoppingList();
}
try {
await fetch(`/api/shopping-list/${itemId}/category`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category: targetCatName })
});
showToast(`✓ Verschoben nach "${targetCatName}"`);
} catch (err) {
console.error(err);
}
}
function toggleCompletedGroup() {
const list = document.getElementById('completed-items-list');
const icon = document.getElementById('completed-toggle-icon');
completedExpanded = !completedExpanded;
list.style.display = completedExpanded ? 'flex' : 'none';
icon.textContent = completedExpanded ? '▼' : '▶';
}
// ==========================================
// MODAL: BEREICHE VERWALTEN (SUPERMARKT)
// ==========================================
function openCategoriesModal() {
renderCategoryManageList();
document.getElementById('modal-categories').classList.add('active');
}
function closeCategoriesModal() {
document.getElementById('modal-categories').classList.remove('active');
}
function onModalBackdropClick(e, modalId) {
if (e.target.id === modalId) {
closeCategoriesModal();
}
}
function renderCategoryManageList() {
const list = document.getElementById('cat-manage-list');
list.innerHTML = '';
shoppingCategories.forEach((cat, index) => {
const li = document.createElement('li');
li.className = 'cat-manage-row';
const isFirst = index === 0;
const isLast = index === shoppingCategories.length - 1;
li.innerHTML = `
<div class="cat-manage-info">
<span class="cat-manage-icon">${escapeHtml(cat.icon || '🛒')}</span>
<span class="cat-manage-name">${escapeHtml(cat.name)}</span>
</div>
<div class="cat-manage-actions">
<button type="button" class="cat-reorder-btn" onclick="moveCategory(${index}, -1)" ${isFirst ? 'disabled' : ''} title="Nach oben (früher im Laufweg)">▲</button>
<button type="button" class="cat-reorder-btn" onclick="moveCategory(${index}, 1)" ${isLast ? 'disabled' : ''} title="Nach unten (später im Laufweg)">▼</button>
<button type="button" class="cat-del-btn" onclick="deleteCategory(${cat.id}, '${escapeHtml(cat.name)}')" title="Bereich löschen">&times;</button>
</div>
`;
list.appendChild(li);
});
// Dropdown in Schnelleingabe aktualisieren
const select = document.getElementById('shop-add-category');
const currentVal = select.value;
select.innerHTML = '<option value="">🪄 Automatisch erkennen</option>';
shoppingCategories.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat.name;
opt.textContent = `${cat.icon} ${cat.name}`;
select.appendChild(opt);
});
select.value = currentVal;
}
async function moveCategory(index, delta) {
const newIndex = index + delta;
if (newIndex < 0 || newIndex >= shoppingCategories.length) return;
const item = shoppingCategories.splice(index, 1)[0];
shoppingCategories.splice(newIndex, 0, item);
renderCategoryManageList();
renderShoppingList();
try {
const ids = shoppingCategories.map(c => c.id);
await fetch('/api/shopping-categories/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_ids: ids })
});
showToast('✓ Reihenfolge gespeichert');
} catch (err) {
console.error(err);
}
}
async function onAddCategorySubmit(e) {
e.preventDefault();
const iconInput = document.getElementById('new-cat-icon');
const nameInput = document.getElementById('new-cat-name');
const icon = iconInput.value.trim() || '🛒';
const name = nameInput.value.trim();
if (!name) return;
try {
const res = await fetch('/api/shopping-categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, icon })
});
const data = await res.json();
if (data.status === 'ok' && data.category) {
shoppingCategories.push(data.category);
nameInput.value = '';
renderCategoryManageList();
renderShoppingList();
showToast(`✓ Bereich "${name}" hinzugefügt`);
}
} catch (err) {
alert('Fehler: ' + err.message);
}
}
async function deleteCategory(id, name) {
if (!confirm(`Möchtest du den Bereich "${name}" wirklich löschen?\nVorhandene Zutaten werden auf "Drogerie & Sonstiges" übertragen.`)) {
return;
}
shoppingCategories = shoppingCategories.filter(c => c.id !== id);
renderCategoryManageList();
renderShoppingList();
try {
await fetch(`/api/shopping-categories/${id}`, { method: 'DELETE' });
showToast(`✓ Bereich "${name}" gelöscht`);
} catch (err) {
console.error(err);
}
}
async function resetDefaultCategories() {
if (!confirm('Möchtest du die Supermarkt-Bereiche auf die Standard-Reihenfolge zurücksetzen?')) return;
try {
const res = await fetch('/api/shopping-categories/reset-defaults', { method: 'POST' });
const data = await res.json();
if (data.status === 'ok' && data.categories) {
shoppingCategories = data.categories;
renderCategoryManageList();
renderShoppingList();
showToast('✓ Standard-Bereiche wiederhergestellt');
}
} catch (err) {
alert('Fehler: ' + err.message);
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Beim Laden der Seite initial rendern
document.addEventListener('DOMContentLoaded', () => {
renderShoppingList();
});
</script>
</body>
</html>