Files
cookbook/app/templates/shopping_list.html
T

1940 lines
64 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, viewport-fit=cover">
<title>Einkaufsliste – What To Cook?</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.2">
<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-green: #1E4D3B;
--shop-green-dark: #15382B;
--shop-green-light: #28634C;
--shop-bg: #FDFBF7;
--shop-card: #FFFFFF;
--shop-border: #222222;
--shop-yellow: #FFF9D2;
--shop-red: #D9534F;
--shop-font-serif: 'Lora', Georgia, serif;
--shop-font-hand: 'Caveat', cursive, sans-serif;
--shop-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body.shopping-app-body {
margin: 0;
padding: 0;
min-height: 100vh;
min-height: 100dvh;
background-color: var(--shop-bg);
background-image:
radial-gradient(rgba(30, 77, 59, 0.04) 1px, transparent 1px);
background-size: 20px 20px;
font-family: var(--shop-font-sans);
color: var(--shop-border);
display: flex;
flex-direction: column;
overflow-x: hidden;
width: 100%;
}
/* ========================================== */
/* FIXED HEADER (GRÜNTON WIE BEIM KOCHBUCH) */
/* ========================================== */
.shop-header {
position: sticky;
top: 0;
z-index: 500;
background: var(--shop-green);
color: #FFFFFF;
border-bottom: 2.5px solid var(--shop-border);
box-shadow: 0 4px 14px rgba(0,0,0,0.18);
width: 100%;
}
.shop-header-inner {
max-width: 900px;
margin: 0 auto;
padding: 10px 14px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.shop-header-left {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.shop-header-title {
font-family: var(--shop-font-hand);
font-size: 1.9rem;
font-weight: 700;
color: #FFFFFF;
margin: 0;
line-height: 1;
white-space: nowrap;
text-shadow: 1px 1px 0px rgba(0,0,0,0.3);
}
.shop-online-pill {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.72rem;
font-weight: 700;
padding: 2px 7px;
border-radius: 10px;
background: rgba(255,255,255,0.2);
color: #FFFFFF;
border: 1px solid rgba(255,255,255,0.3);
}
.shop-online-pill.is-offline {
background: #D9534F;
border-color: #b00;
}
.shop-header-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
/* Header Icon Button */
.hdr-btn {
background: rgba(255, 255, 255, 0.15);
border: 1.5px solid rgba(255, 255, 255, 0.35);
color: #FFFFFF;
border-radius: 8px;
width: 38px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.15rem;
cursor: pointer;
transition: all 0.12s ease;
position: relative;
}
.hdr-btn:hover {
background: rgba(255, 255, 255, 0.28);
transform: translateY(-1px);
}
.hdr-btn:active {
transform: translate(1px, 1px);
}
.hdr-btn.is-active {
background: #FFFFFF;
color: var(--shop-green);
border-color: #FFFFFF;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
.hdr-btn-danger:hover {
background: #D9534F;
border-color: #D9534F;
}
/* ========================================== */
/* KEBAB DROPDOWN MENÜ */
/* ========================================== */
.kebab-wrapper {
position: relative;
}
.kebab-menu {
position: absolute;
top: 46px;
right: 0;
background: #FFFFFF;
border: 2px solid var(--shop-border);
border-radius: 12px;
box-shadow: 4px 6px 0px var(--shop-border);
width: 230px;
display: none;
flex-direction: column;
z-index: 1000;
overflow: hidden;
animation: fadeIn 0.12s ease;
}
.kebab-menu.show {
display: flex;
}
.kebab-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 14px;
font-size: 0.92rem;
font-weight: 600;
color: var(--shop-border);
text-decoration: none;
background: transparent;
border: none;
border-bottom: 1px solid rgba(0,0,0,0.08);
cursor: pointer;
text-align: left;
transition: background 0.12s;
}
.kebab-item:last-child {
border-bottom: none;
}
.kebab-item:hover {
background: rgba(30, 77, 59, 0.08);
color: var(--shop-green);
}
.kebab-item.text-danger:hover {
background: #fdeaea;
color: #b00;
}
/* ========================================== */
/* FILTERLEISTE (KATEGORIE-CHIPS) */
/* ========================================== */
.filter-bar {
background: #F4EFE6;
border-bottom: 1.5px solid rgba(34,34,34,0.15);
padding: 8px 12px;
display: none;
overflow-x: auto;
white-space: nowrap;
gap: 8px;
align-items: center;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.filter-bar::-webkit-scrollbar {
display: none;
}
.filter-bar.show {
display: flex;
}
.filter-chip {
background: #FFFFFF;
border: 1.5px solid var(--shop-border);
border-radius: 20px;
padding: 5px 12px;
font-size: 0.82rem;
font-weight: 600;
color: var(--shop-border);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 5px;
box-shadow: 1px 2px 0px rgba(0,0,0,0.08);
transition: all 0.12s;
flex-shrink: 0;
}
.filter-chip.active {
background: var(--shop-green);
color: #FFFFFF;
border-color: var(--shop-border);
box-shadow: 2px 2px 0px var(--shop-border);
}
/* ========================================== */
/* LISTEN-CONTAINER & FULL-WIDTH ITEMS */
/* ========================================== */
.shop-content {
max-width: 900px;
width: 100%;
margin: 0 auto;
padding: 12px 0 100px 0; /* Unten Platz für FAB */
flex: 1;
}
.category-group {
margin-bottom: 14px;
}
.category-header {
background: #EFEBE4;
border-top: 1.5px solid rgba(34,34,34,0.15);
border-bottom: 1.5px solid rgba(34,34,34,0.15);
padding: 7px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.category-header-title {
font-family: var(--shop-font-serif);
font-size: 1.05rem;
font-weight: 700;
color: var(--shop-green);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.category-header-count {
font-size: 0.75rem;
font-weight: 700;
background: var(--shop-yellow);
border: 1px solid rgba(0,0,0,0.25);
border-radius: 10px;
padding: 1px 7px;
}
/* Die Items-Liste */
.items-list {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
/* Jede Zeile: Full-Width über den Bildschirm */
.item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
background: #FFFFFF;
border-bottom: 1px solid rgba(0,0,0,0.08);
transition: background 0.12s, opacity 0.15s;
width: 100%;
box-sizing: border-box;
}
.item-row:hover {
background: #FAF8F4;
}
/* Abgehakte Zeile */
.item-row.is-checked {
background: #F7F5F0;
opacity: 0.55;
}
.item-row.is-checked .item-name {
text-decoration: line-through;
color: #777;
}
/* Checkbox links */
.item-check-btn {
background: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.custom-checkbox {
width: 24px;
height: 24px;
border: 2px solid var(--shop-border);
border-radius: 6px;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.item-row.is-checked .custom-checkbox {
background: var(--shop-green);
border-color: var(--shop-green);
}
.custom-checkbox-icon {
color: #FFFFFF;
font-size: 0.88rem;
font-weight: 900;
display: none;
}
.item-row.is-checked .custom-checkbox-icon {
display: block;
}
/* Textbereich Mitte: Klick öffnet Edit-Popup! */
.item-body {
flex: 1;
min-width: 0;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 2px;
}
.item-main-text {
font-size: 1.05rem;
line-height: 1.35;
color: #111;
word-break: break-word;
overflow-wrap: anywhere;
}
.item-amount {
font-weight: 700;
color: var(--shop-green);
margin-right: 4px;
}
.item-badges-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 2px;
}
.item-note-badge {
font-size: 0.72rem;
font-weight: 600;
background: var(--shop-yellow);
border: 1px solid rgba(180, 150, 0, 0.4);
color: #6a5300;
padding: 1px 6px;
border-radius: 4px;
}
.item-source-badge {
font-size: 0.72rem;
background: rgba(30, 77, 59, 0.08);
color: var(--shop-green);
border: 1px solid rgba(30, 77, 59, 0.2);
padding: 1px 6px;
border-radius: 4px;
max-width: 240px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Löschen Icon rechts */
.item-del-btn {
background: transparent;
border: none;
color: #aaa;
font-size: 1.25rem;
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
transition: all 0.12s;
flex-shrink: 0;
}
.item-del-btn:hover {
color: var(--shop-red);
background: rgba(217, 83, 79, 0.1);
}
/* ========================================== */
/* SEKTION: ERLEDIGT (BEI A-Z SORTIERUNG) */
/* ========================================== */
.completed-section {
margin-top: 18px;
border-top: 2px dashed rgba(0,0,0,0.15);
}
.completed-header {
background: #ECE8E0;
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.completed-title {
font-family: var(--shop-font-serif);
font-size: 0.95rem;
font-weight: 700;
color: #666;
margin: 0;
display: flex;
align-items: center;
gap: 6px;
}
/* ========================================== */
/* FLOATING ACTION BUTTON (FAB '+') */
/* ========================================== */
.fab-add {
position: fixed;
bottom: 24px;
right: 20px;
width: 58px;
height: 58px;
border-radius: 50%;
background: var(--shop-green);
color: #FFFFFF;
border: 2.5px solid var(--shop-border);
box-shadow: 4px 5px 0px var(--shop-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: 700;
cursor: pointer;
z-index: 450;
transition: all 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.fab-add:hover {
background: var(--shop-green-dark);
transform: scale(1.08) translateY(-2px);
box-shadow: 5px 7px 0px var(--shop-border);
}
.fab-add:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px var(--shop-border);
}
/* ========================================== */
/* MODALS & POPUPS */
/* ========================================== */
.modal-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.6);
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: 16px;
box-shadow: 5px 6px 0px var(--shop-border);
max-width: 480px;
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(--shop-green);
color: #FFFFFF;
padding: 12px 18px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid var(--shop-border);
}
.modal-title {
font-family: var(--shop-font-serif);
font-size: 1.2rem;
font-weight: 700;
margin: 0;
color: #FFFFFF;
display: flex;
align-items: center;
gap: 8px;
}
.modal-close-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255,255,255,0.4);
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: #FFFFFF;
transition: all 0.12s;
}
.modal-close-btn:hover {
background: #D9534F;
border-color: #D9534F;
}
.modal-body {
padding: 18px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.form-label {
font-size: 0.88rem;
font-weight: 700;
color: #333;
}
.form-input, .form-select, .form-textarea {
border: 1.5px solid var(--shop-border);
border-radius: 8px;
padding: 9px 12px;
font-family: var(--shop-font-sans);
font-size: 1rem;
outline: none;
background: #FFFFFF;
transition: border-color 0.15s;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
border-color: var(--shop-green);
box-shadow: 0 0 0 2px rgba(30, 77, 59, 0.2);
}
.form-row-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.modal-footer {
padding: 12px 18px;
background: #FAF8F4;
border-top: 1.5px solid rgba(0,0,0,0.1);
display: flex;
justify-content: flex-end;
gap: 8px;
}
.btn-modal {
border: 2px solid var(--shop-border);
border-radius: 8px;
padding: 8px 16px;
font-size: 0.92rem;
font-weight: 700;
cursor: pointer;
box-shadow: 2px 2px 0px var(--shop-border);
transition: all 0.12s;
}
.btn-modal:active {
transform: translate(1px, 1px);
box-shadow: 1px 1px 0px var(--shop-border);
}
.btn-modal-primary {
background: var(--shop-green);
color: #FFFFFF;
}
.btn-modal-primary:hover {
background: var(--shop-green-dark);
}
.btn-modal-secondary {
background: #FFFFFF;
color: #444;
}
.btn-modal-secondary:hover {
background: #f0f0f0;
}
.btn-modal-danger {
background: #D9534F;
color: #FFFFFF;
border-color: var(--shop-border);
}
.btn-modal-danger:hover {
background: #c9302c;
}
/* Leerer Zustand */
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-state-icon {
font-size: 3.5rem;
margin-bottom: 8px;
}
.empty-state-title {
font-family: var(--shop-font-hand);
font-size: 2.3rem;
color: var(--shop-green);
margin: 0 0 6px 0;
}
.empty-state-desc {
color: #666;
font-size: 0.95rem;
max-width: 360px;
margin: 0 auto 20px auto;
}
/* Toast */
.shop-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--shop-green);
color: #FFFFFF;
padding: 10px 22px;
border-radius: 30px;
font-weight: 600;
font-size: 0.92rem;
box-shadow: 0 6px 18px rgba(0,0,0,0.3);
border: 2px solid var(--shop-border);
z-index: 9999;
opacity: 0;
transition: all 0.25s 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;
}
</style>
</head>
<body class="shopping-app-body">
<!-- ========================================== -->
<!-- FIXED HEADER IM GRÜNTON (WIE BEIM KOCHBUCH)-->
<!-- ========================================== -->
<header class="shop-header">
<div class="shop-header-inner">
<div class="shop-header-left">
<h1 class="shop-header-title">🛒 Einkaufsliste</h1>
<span class="shop-online-pill" id="online-indicator" title="Live-Sync Status">
<span id="online-dot">●</span>
<span id="online-text">Live</span>
</span>
</div>
<div class="shop-header-actions">
<!-- Sortierungs-Toggle (Laufweg vs. A-Z) -->
<button type="button" class="hdr-btn" id="btn-toggle-sort" onclick="toggleSortMode()" title="Sortierung umschalten (Nach Bereichen / Alphabetisch)">
<span id="sort-icon">⇅</span>
</button>
<!-- Filter einblenden / ausblenden -->
<button type="button" class="hdr-btn" id="btn-toggle-filter" onclick="toggleFilterBar()" title="Kategorie-Filter einblenden">
<span>🏷️</span>
</button>
<!-- Liste komplett leeren (2-Stufen Sicherheitsabfrage) -->
<button type="button" class="hdr-btn hdr-btn-danger" onclick="openDeleteStep1Modal()" title="Liste komplett leeren">
<span>🗑️</span>
</button>
<!-- Kebab-Menü für Links & Verwaltung -->
<div class="kebab-wrapper">
<button type="button" class="hdr-btn" onclick="toggleKebabMenu(event)" title="Menü öffnen">
<span>⋮</span>
</button>
<div class="kebab-menu" id="kebab-menu">
<a href="/" class="kebab-item">
<span>⬅️</span> <span>Startmenü</span>
</a>
<a href="/cookbook" class="kebab-item">
<span>📖</span> <span>Zum Kochbuch</span>
</a>
<button type="button" class="kebab-item" onclick="closeKebabMenu(); openCategoriesModal();">
<span>⚙️</span> <span>Bereiche / Laufweg</span>
</button>
<button type="button" class="kebab-item" onclick="closeKebabMenu(); openMealPlanImportModal();">
<span>📅</span> <span>Aus Wochenplan holen</span>
</button>
<button type="button" class="kebab-item text-danger" onclick="closeKebabMenu(); deleteCheckedItems();">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
</div>
</div>
</div>
</div>
<!-- Einblendbare Filterleiste nach Kategorien -->
<div class="filter-bar" id="filter-bar">
<!-- Wird dynamisch mit renderFilterChips() befüllt -->
</div>
</header>
<!-- ========================================== -->
<!-- LISTEN-CONTAINER -->
<!-- ========================================== -->
<main class="shop-content" id="shop-content">
<div id="items-container">
<!-- Dynamisch -->
</div>
<!-- Leerer Zustand -->
<div class="empty-state" id="empty-state" style="display: none;">
<div class="empty-state-icon">🎉</div>
<h2 class="empty-state-title">Alles eingekauft!</h2>
<p class="empty-state-desc">Deine Einkaufsliste ist leer. Tippe unten rechts auf das <strong>+</strong>, um einen neuen Eintrag zu notieren, oder hole Zutaten aus dem Wochenplan.</p>
<button type="button" class="btn-modal btn-modal-primary" onclick="openMealPlanImportModal()">
<span>📅</span> <span>Aus Wochenplan importieren</span>
</button>
</div>
</main>
<!-- ========================================== -->
<!-- FLOATING ACTION BUTTON (FAB '+') -->
<!-- ========================================== -->
<button type="button" class="fab-add" onclick="openAddItemModal()" title="Neuen Eintrag hinzufügen">
<span>+</span>
</button>
<!-- ========================================== -->
<!-- MODAL: ITEM EDITIEREN -->
<!-- ========================================== -->
<div id="modal-edit-item" class="modal-backdrop" onclick="onBackdropClick(event, 'modal-edit-item')">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title"><span>✏️</span> Eintrag bearbeiten</h3>
<button type="button" class="modal-close-btn" onclick="closeModal('modal-edit-item')">&times;</button>
</div>
<form onsubmit="onSaveEditItem(event)">
<div class="modal-body">
<input type="hidden" id="edit-item-id">
<div class="form-group">
<label class="form-label" for="edit-item-name">Artikel / Zutat *</label>
<input type="text" id="edit-item-name" class="form-input" required autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="edit-item-category">Bereich im Supermarkt</label>
<select id="edit-item-category" class="form-select"></select>
</div>
<div class="form-row-2">
<div class="form-group">
<label class="form-label" for="edit-item-amount">Menge</label>
<input type="text" id="edit-item-amount" class="form-input" placeholder="z. B. 2 oder 500">
</div>
<div class="form-group">
<label class="form-label" for="edit-item-unit">Einheit</label>
<input type="text" id="edit-item-unit" class="form-input" placeholder="z. B. g, l, Bund, Dose">
</div>
</div>
<div class="form-group">
<label class="form-label" for="edit-item-note">Notiz / Extra</label>
<input type="text" id="edit-item-note" class="form-input" placeholder="z. B. Bio, laktosefrei, nur Angebot">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal btn-modal-danger" onclick="onDeleteItemFromModal()" style="margin-right: auto;">
🗑️ Löschen
</button>
<button type="button" class="btn-modal btn-modal-secondary" onclick="closeModal('modal-edit-item')">
Schließen
</button>
<button type="submit" class="btn-modal btn-modal-primary">
Speichern
</button>
</div>
</form>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: ITEM HINZUFÜGEN (VIA FAB '+') -->
<!-- ========================================== -->
<div id="modal-add-item" class="modal-backdrop" onclick="onBackdropClick(event, 'modal-add-item')">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title"><span>➕</span> Neuer Eintrag</h3>
<button type="button" class="modal-close-btn" onclick="closeModal('modal-add-item')">&times;</button>
</div>
<form onsubmit="onSaveAddItem(event)">
<div class="modal-body">
<div class="form-group">
<label class="form-label" for="add-item-name">Artikel / Zutat *</label>
<input type="text" id="add-item-name" class="form-input" placeholder="z. B. Gurke oder 500g Quark..." required autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="add-item-category">Bereich im Supermarkt</label>
<select id="add-item-category" class="form-select">
<option value="">🪄 Automatisch erkennen</option>
</select>
</div>
<div class="form-row-2">
<div class="form-group">
<label class="form-label" for="add-item-amount">Menge</label>
<input type="text" id="add-item-amount" class="form-input" placeholder="z. B. 2 oder 500">
</div>
<div class="form-group">
<label class="form-label" for="add-item-unit">Einheit</label>
<input type="text" id="add-item-unit" class="form-input" placeholder="z. B. g, l, Bund, Dose">
</div>
</div>
<div class="form-group">
<label class="form-label" for="add-item-note">Notiz / Extra</label>
<input type="text" id="add-item-note" class="form-input" placeholder="z. B. Bio, im Angebot">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal btn-modal-secondary" onclick="closeModal('modal-add-item')">
Schließen
</button>
<button type="submit" class="btn-modal btn-modal-primary">
Hinzufügen
</button>
</div>
</form>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: 2-STUFEN LÖSCH-SICHERHEITSABFRAGE -->
<!-- ========================================== -->
<!-- Stufe 1 -->
<div id="modal-delete-step1" class="modal-backdrop" onclick="onBackdropClick(event, 'modal-delete-step1')">
<div class="modal-card" style="max-width: 420px;">
<div class="modal-header" style="background: #D9534F;">
<h3 class="modal-title"><span>🗑️</span> Einkaufsliste leeren?</h3>
<button type="button" class="modal-close-btn" onclick="closeModal('modal-delete-step1')">&times;</button>
</div>
<div class="modal-body">
<p style="margin: 0; font-size: 1rem; line-height: 1.4;">
Möchtest du wirklich alle Artikel von der Einkaufsliste entfernen?
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal btn-modal-secondary" onclick="closeModal('modal-delete-step1')">
Schließen
</button>
<button type="button" class="btn-modal btn-modal-danger" onclick="openDeleteStep2Modal()">
Löschen
</button>
</div>
</div>
</div>
<!-- Stufe 2 (Doppelte Bestätigung) -->
<div id="modal-delete-step2" class="modal-backdrop" onclick="onBackdropClick(event, 'modal-delete-step2')">
<div class="modal-card" style="max-width: 420px; border-color: #b00;">
<div class="modal-header" style="background: #b00;">
<h3 class="modal-title"><span>⚠️</span> Wirklich alles löschen?</h3>
<button type="button" class="modal-close-btn" onclick="closeModal('modal-delete-step2')">&times;</button>
</div>
<div class="modal-body">
<p style="margin: 0; font-size: 1rem; line-height: 1.4; color: #b00; font-weight: 600;">
Achtung: Alle Einträge (auch noch nicht eingekaufte Artikel) werden unwiderruflich gelöscht!
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal btn-modal-secondary" onclick="closeModal('modal-delete-step2')">
Abbrechen
</button>
<button type="button" class="btn-modal btn-modal-danger" onclick="confirmClearAllItems()">
Ja, wirklich alles löschen
</button>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: WOCHENPLAN ZUTATEN IMPORTIEREN -->
<!-- ========================================== -->
<div id="modal-import-mealplan" class="modal-backdrop" onclick="onBackdropClick(event, 'modal-import-mealplan')">
<div class="modal-card" style="max-width: 540px;">
<div class="modal-header">
<h3 class="modal-title"><span>📅</span> Zutaten aus Wochenplan holen</h3>
<button type="button" class="modal-close-btn" onclick="closeModal('modal-import-mealplan')">&times;</button>
</div>
<div class="modal-body" id="mealplan-import-body">
<div style="text-align: center; padding: 20px; color: #666;">
<span>⏳ Zutaten des Wochenplans werden geladen...</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal btn-modal-secondary" onclick="closeModal('modal-import-mealplan')">
Schließen
</button>
<button type="button" class="btn-modal btn-modal-primary" id="btn-confirm-import-mealplan" onclick="confirmImportMealPlanIngredients()" disabled>
Ausgewählte übernehmen
</button>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: SUPERMARKT-BEREICHE & LAUFWEG -->
<!-- ========================================== -->
<div id="modal-categories" class="modal-backdrop" onclick="onBackdropClick(event, 'modal-categories')">
<div class="modal-card" style="max-width: 520px;">
<div class="modal-header">
<h3 class="modal-title"><span>⚙️</span> Supermarkt-Laufweg anpassen</h3>
<button type="button" class="modal-close-btn" onclick="closeModal('modal-categories')">&times;</button>
</div>
<div class="modal-body">
<p style="font-size: 0.88rem; color: #666; margin: 0;">
Ordne die Bereiche mit <strong>▲</strong> und <strong>▼</strong> so an, wie du durch deinen Supermarkt gehst.
</p>
<ul id="cat-manage-list" style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px;"></ul>
<div style="margin-top: 10px; background: var(--shop-yellow); border: 1.5px solid var(--shop-border); border-radius: 8px; padding: 8px;">
<h4 style="margin: 0 0 6px 0; font-size: 0.9rem;">Neuen Bereich anlegen</h4>
<form onsubmit="onAddCategory(event)" style="display: flex; gap: 6px;">
<input type="text" id="new-cat-icon" style="width: 44px; text-align: center; font-size: 1.1rem; border: 1px solid #999; border-radius: 6px;" value="🛒">
<input type="text" id="new-cat-name" placeholder="Bereichsname..." style="flex: 1; padding: 6px 8px; border: 1px solid #999; border-radius: 6px;" required>
<button type="submit" class="btn-modal btn-modal-primary" style="padding: 6px 12px; font-size: 0.85rem;">Hinzufügen</button>
</form>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 4px;">
<button type="button" onclick="resetDefaultCategories()" style="background: none; border: none; font-size: 0.78rem; color: #777; cursor: pointer; text-decoration: underline;">
↺ Standard-Bereiche wiederherstellen
</button>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div id="shop-toast" class="shop-toast">✓ Gespeichert</div>
<!-- ========================================== -->
<!-- JAVASCRIPT: LOGIK, WEBSOCKET & OFFLINE-SYNC-->
<!-- ========================================== -->
<script>
// State Initialisierung aus Server oder localStorage
let shoppingItems = {{ items_json|safe if items_json is defined else '[]' }};
let shoppingCategories = {{ categories_json|safe if categories_json is defined else '[]' }};
// Sortierung: 'category' (Laufweg) oder 'alphabetical' (A-Z)
let sortMode = localStorage.getItem('wtc_shop_sort_mode') || 'category';
let activeFilterCategory = null;
let completedExpanded = true;
let ws = null;
let reconnectTimeout = null;
// Lokaler Cache
function saveLocalCache() {
try {
localStorage.setItem('wtc_shopping_items', JSON.stringify(shoppingItems));
localStorage.setItem('wtc_shopping_categories', JSON.stringify(shoppingCategories));
} catch (e) {}
}
function loadLocalCache() {
try {
const cachedItems = localStorage.getItem('wtc_shopping_items');
if (cachedItems) shoppingItems = JSON.parse(cachedItems);
const cachedCats = localStorage.getItem('wtc_shopping_categories');
if (cachedCats) shoppingCategories = JSON.parse(cachedCats);
} catch (e) {}
}
// Toast
function showToast(msg) {
const toast = document.getElementById('shop-toast');
toast.textContent = msg;
toast.classList.add('active');
setTimeout(() => toast.classList.remove('active'), 2500);
}
// ==========================================
// WEBSOCKET & LIVE SYNCHRONISATION
// ==========================================
function initWebSocket() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/shopping-list`;
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
updateOnlineStatus(true);
flushOfflineQueue();
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.action === 'items_updated' && data.items) {
shoppingItems = data.items;
saveLocalCache();
renderShoppingList();
} else if (data.action === 'categories_updated' && data.categories) {
shoppingCategories = data.categories;
if (data.items) shoppingItems = data.items;
saveLocalCache();
updateCategorySelects();
renderFilterChips();
renderShoppingList();
}
} catch (err) {}
};
ws.onclose = () => {
updateOnlineStatus(false);
scheduleReconnect();
};
ws.onerror = () => {
updateOnlineStatus(false);
};
} catch (e) {
updateOnlineStatus(false);
scheduleReconnect();
}
}
function scheduleReconnect() {
if (reconnectTimeout) clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(initWebSocket, 3000);
}
function updateOnlineStatus(isOnline) {
const ind = document.getElementById('online-indicator');
const text = document.getElementById('online-text');
if (isOnline && navigator.onLine) {
ind.classList.remove('is-offline');
text.textContent = 'Live';
} else {
ind.classList.add('is-offline');
text.textContent = 'Offline';
}
}
window.addEventListener('online', () => {
updateOnlineStatus(true);
initWebSocket();
flushOfflineQueue();
});
window.addEventListener('offline', () => {
updateOnlineStatus(false);
});
// Offline Queue
function getOfflineQueue() {
try {
return JSON.parse(localStorage.getItem('wtc_offline_queue') || '[]');
} catch (e) {
return [];
}
}
function pushOfflineQueue(action) {
const q = getOfflineQueue();
q.push(action);
localStorage.setItem('wtc_offline_queue', JSON.stringify(q));
}
async function flushOfflineQueue() {
const q = getOfflineQueue();
if (q.length === 0) return;
localStorage.removeItem('wtc_offline_queue');
for (const item of q) {
try {
await fetch(item.url, {
method: item.method,
headers: { 'Content-Type': 'application/json' },
body: item.body ? JSON.stringify(item.body) : undefined
});
} catch (e) {
pushOfflineQueue(item);
break;
}
}
// Frischen Stand holen
try {
const res = await fetch('/api/shopping-list');
const data = await res.json();
if (data.status === 'ok') {
shoppingItems = data.items;
shoppingCategories = data.categories;
saveLocalCache();
renderShoppingList();
}
} catch (e) {}
}
// ==========================================
// RENDERING: ITEMS LISTE
// ==========================================
function renderShoppingList() {
const container = document.getElementById('items-container');
const emptyState = document.getElementById('empty-state');
container.innerHTML = '';
if (shoppingItems.length === 0) {
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
// Filtern
let displayItems = shoppingItems;
if (activeFilterCategory) {
displayItems = displayItems.filter(it => (it.category || 'Drogerie & Sonstiges') === activeFilterCategory);
}
if (sortMode === 'alphabetical') {
renderAlphabeticalView(container, displayItems);
} else {
renderCategoryView(container, displayItems);
}
}
// 1. Ansicht: Nach Kategorien (Laufweg)
function renderCategoryView(container, items) {
const openItems = items.filter(it => !it.checked);
const checkedItems = items.filter(it => it.checked);
// Map aller Kategorien
const catMap = new Map();
shoppingCategories.forEach(c => {
catMap.set(c.name, { category: c, open: [], checked: [] });
});
items.forEach(it => {
const catName = it.category || 'Drogerie & Sonstiges';
if (!catMap.has(catName)) {
catMap.set(catName, {
category: { name: catName, icon: '📦', sort_order: 999 },
open: [],
checked: []
});
}
const group = catMap.get(catName);
if (it.checked) group.checked.push(it);
else group.open.push(it);
});
// Sortierte Kategorien durchlaufen
shoppingCategories.forEach(cat => {
const group = catMap.get(cat.name);
if (group && (group.open.length > 0 || group.checked.length > 0)) {
renderCategoryBlock(container, group.category, group.open, group.checked);
}
});
// Evtl. benutzerdefinierte Rest-Kategorien
catMap.forEach((group, catName) => {
if (!shoppingCategories.some(c => c.name === catName) && (group.open.length > 0 || group.checked.length > 0)) {
renderCategoryBlock(container, group.category, group.open, group.checked);
}
});
}
function renderCategoryBlock(container, cat, openItems, checkedItems) {
const groupEl = document.createElement('div');
groupEl.className = 'category-group';
const totalCount = openItems.length;
groupEl.innerHTML = `
<div class="category-header">
<h3 class="category-header-title">
<span>${escapeHtml(cat.icon || '🛒')}</span>
<span>${escapeHtml(cat.name)}</span>
</h3>
<span class="category-header-count">${totalCount}</span>
</div>
<ul class="items-list"></ul>
`;
const ul = groupEl.querySelector('.items-list');
// Zuerst offene Items
openItems.forEach(it => ul.appendChild(createItemRowElement(it)));
// Dann abgehakte Items (ans Ende der Kategorie!)
checkedItems.forEach(it => ul.appendChild(createItemRowElement(it)));
container.appendChild(groupEl);
}
// 2. Ansicht: Alphabetisch (A-Z)
function renderAlphabeticalView(container, items) {
const openItems = items.filter(it => !it.checked);
const checkedItems = items.filter(it => it.checked);
openItems.sort((a, b) => a.item_name.localeCompare(b.item_name, 'de'));
checkedItems.sort((a, b) => a.item_name.localeCompare(b.item_name, 'de'));
const ul = document.createElement('ul');
ul.className = 'items-list';
openItems.forEach(it => ul.appendChild(createItemRowElement(it)));
container.appendChild(ul);
// Erledigte Items am Ende gesammelt
if (checkedItems.length > 0) {
const compSection = document.createElement('div');
compSection.className = 'completed-section';
compSection.innerHTML = `
<div class="completed-header" onclick="toggleCompletedGroup()">
<h4 class="completed-title">
<span id="completed-chevron">${completedExpanded ? '▼' : '▶'}</span>
<span>✓ Erledigt (${checkedItems.length})</span>
</h4>
</div>
<ul class="items-list" id="completed-ul" style="${completedExpanded ? '' : 'display: none;'}"></ul>
`;
const compUl = compSection.querySelector('#completed-ul');
checkedItems.forEach(it => compUl.appendChild(createItemRowElement(it)));
container.appendChild(compSection);
}
}
// Einzelnes Item Element
function createItemRowElement(item) {
const li = document.createElement('li');
li.className = `item-row ${item.checked ? 'is-checked' : ''}`;
li.id = `item-row-${item.id}`;
// Menge / Einheit
const amountStr = (item.amount || item.unit) ? `<span class="item-amount">${escapeHtml(item.amount || '')} ${escapeHtml(item.unit || '')}</span>` : '';
// Quelle & Notiz Badges
let badgesHtml = '';
if (item.note) {
badgesHtml += `<span class="item-note-badge">📝 ${escapeHtml(item.note)}</span>`;
}
const displaySource = item.display_source || cleanRecipeSource(item.source);
if (displaySource) {
badgesHtml += `<span class="item-source-badge" title="${escapeHtml(displaySource)}">📖 ${escapeHtml(displaySource)}</span>`;
}
li.innerHTML = `
<button type="button" class="item-check-btn" onclick="toggleItem(${item.id})" aria-label="Abhaken">
<div class="custom-checkbox">
<span class="custom-checkbox-icon">✓</span>
</div>
</button>
<div class="item-body" onclick="openEditItemModal(${item.id})">
<div class="item-main-text">
${amountStr}<span class="item-name">${escapeHtml(item.item_name)}</span>
</div>
${badgesHtml ? `<div class="item-badges-line">${badgesHtml}</div>` : ''}
</div>
<button type="button" class="item-del-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: TOGGLE / LÖSCHEN / FILTER / SORT
// ==========================================
async function toggleItem(id) {
const it = shoppingItems.find(x => x.id === id);
if (!it) return;
// Optimistisches Update sofort
it.checked = !it.checked;
renderShoppingList();
saveLocalCache();
const url = `/api/shopping-list/${id}/toggle`;
try {
const res = await fetch(url, { method: 'POST' });
const data = await res.json();
if (data.status === 'ok' && data.item) {
it.checked = Boolean(data.item.checked);
renderShoppingList();
}
} catch (err) {
pushOfflineQueue({ url, method: 'POST' });
}
}
async function deleteSingleItem(id) {
shoppingItems = shoppingItems.filter(x => x.id !== id);
renderShoppingList();
saveLocalCache();
const url = `/api/shopping-list/${id}`;
try {
await fetch(url, { method: 'DELETE' });
} catch (err) {
pushOfflineQueue({ url, method: 'DELETE' });
}
}
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();
saveLocalCache();
showToast('✓ Erledigte Einträge gelöscht');
const url = '/api/shopping-list/checked';
try {
await fetch(url, { method: 'DELETE' });
} catch (err) {
pushOfflineQueue({ url, method: 'DELETE' });
}
}
// 2-Stufen Sicherheitsabfrage fürs Leeren der gesamten Liste
function openDeleteStep1Modal() {
if (shoppingItems.length === 0) {
showToast('Die Einkaufsliste ist bereits leer.');
return;
}
openModal('modal-delete-step1');
}
function openDeleteStep2Modal() {
closeModal('modal-delete-step1');
openModal('modal-delete-step2');
}
async function confirmClearAllItems() {
closeModal('modal-delete-step2');
shoppingItems = [];
renderShoppingList();
saveLocalCache();
showToast('✓ Einkaufsliste komplett geleert');
const url = '/api/shopping-list/all';
try {
await fetch(url, { method: 'DELETE' });
} catch (err) {
pushOfflineQueue({ url, method: 'DELETE' });
}
}
// Sortierungs-Toggle
function toggleSortMode() {
sortMode = (sortMode === 'category') ? 'alphabetical' : 'category';
localStorage.setItem('wtc_shop_sort_mode', sortMode);
const btn = document.getElementById('btn-toggle-sort');
if (sortMode === 'alphabetical') {
btn.classList.add('is-active');
showToast('Sortierung: Alphabetisch (A–Z)');
} else {
btn.classList.remove('is-active');
showToast('Sortierung: Nach Supermarkt-Gängen');
}
renderShoppingList();
}
// Filterleiste einblenden
function toggleFilterBar() {
const bar = document.getElementById('filter-bar');
const btn = document.getElementById('btn-toggle-filter');
const isVisible = bar.classList.toggle('show');
btn.classList.toggle('is-active', isVisible || activeFilterCategory !== null);
if (isVisible) renderFilterChips();
}
function renderFilterChips() {
const bar = document.getElementById('filter-bar');
bar.innerHTML = '';
// Chip "Alle"
const allChip = document.createElement('button');
allChip.type = 'button';
allChip.className = `filter-chip ${activeFilterCategory === null ? 'active' : ''}`;
allChip.textContent = `Alle (${shoppingItems.length})`;
allChip.onclick = () => {
activeFilterCategory = null;
renderFilterChips();
renderShoppingList();
};
bar.appendChild(allChip);
shoppingCategories.forEach(cat => {
const count = shoppingItems.filter(it => (it.category || 'Drogerie & Sonstiges') === cat.name).length;
if (count > 0 || activeFilterCategory === cat.name) {
const chip = document.createElement('button');
chip.type = 'button';
chip.className = `filter-chip ${activeFilterCategory === cat.name ? 'active' : ''}`;
chip.innerHTML = `<span>${escapeHtml(cat.icon || '🛒')}</span> <span>${escapeHtml(cat.name)} (${count})</span>`;
chip.onclick = () => {
activeFilterCategory = (activeFilterCategory === cat.name) ? null : cat.name;
renderFilterChips();
renderShoppingList();
};
bar.appendChild(chip);
}
});
}
function toggleCompletedGroup() {
completedExpanded = !completedExpanded;
const compUl = document.getElementById('completed-ul');
const chevron = document.getElementById('completed-chevron');
if (compUl) compUl.style.display = completedExpanded ? '' : 'none';
if (chevron) chevron.textContent = completedExpanded ? '▼' : '▶';
}
// Kebab Menü
function toggleKebabMenu(e) {
e.stopPropagation();
const menu = document.getElementById('kebab-menu');
menu.classList.toggle('show');
}
function closeKebabMenu() {
document.getElementById('kebab-menu').classList.remove('show');
}
document.addEventListener('click', () => closeKebabMenu());
// ==========================================
// MODAL: ITEM EDITIEREN
// ==========================================
function openEditItemModal(id) {
const it = shoppingItems.find(x => x.id === id);
if (!it) return;
document.getElementById('edit-item-id').value = it.id;
document.getElementById('edit-item-name').value = it.item_name || '';
document.getElementById('edit-item-amount').value = it.amount || '';
document.getElementById('edit-item-unit').value = it.unit || '';
document.getElementById('edit-item-note').value = it.note || '';
const catSelect = document.getElementById('edit-item-category');
catSelect.innerHTML = '';
shoppingCategories.forEach(c => {
const opt = document.createElement('option');
opt.value = c.name;
opt.textContent = `${c.icon} ${c.name}`;
if (c.name === it.category) opt.selected = true;
catSelect.appendChild(opt);
});
openModal('modal-edit-item');
}
async function onSaveEditItem(e) {
e.preventDefault();
const id = parseInt(document.getElementById('edit-item-id').value, 10);
const name = document.getElementById('edit-item-name').value.trim();
const category = document.getElementById('edit-item-category').value;
const amount = document.getElementById('edit-item-amount').value.trim();
const unit = document.getElementById('edit-item-unit').value.trim();
const note = document.getElementById('edit-item-note').value.trim();
if (!name) return;
const it = shoppingItems.find(x => x.id === id);
if (it) {
it.item_name = name;
it.category = category;
it.amount = amount;
it.unit = unit;
it.note = note;
renderShoppingList();
saveLocalCache();
}
closeModal('modal-edit-item');
showToast('✓ Eintrag aktualisiert');
const url = `/api/shopping-list/${id}`;
const payload = { item_name: name, category, amount, unit, note };
try {
await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (err) {
pushOfflineQueue({ url, method: 'PUT', body: payload });
}
}
function onDeleteItemFromModal() {
const id = parseInt(document.getElementById('edit-item-id').value, 10);
closeModal('modal-edit-item');
deleteSingleItem(id);
}
// ==========================================
// MODAL: ITEM HINZUFÜGEN (VIA FAB '+')
// ==========================================
function openAddItemModal() {
document.getElementById('add-item-name').value = '';
document.getElementById('add-item-amount').value = '';
document.getElementById('add-item-unit').value = '';
document.getElementById('add-item-note').value = '';
const catSelect = document.getElementById('add-item-category');
catSelect.innerHTML = '<option value="">🪄 Automatisch erkennen</option>';
shoppingCategories.forEach(c => {
const opt = document.createElement('option');
opt.value = c.name;
opt.textContent = `${c.icon} ${c.name}`;
catSelect.appendChild(opt);
});
openModal('modal-add-item');
setTimeout(() => document.getElementById('add-item-name').focus(), 150);
}
async function onSaveAddItem(e) {
e.preventDefault();
const name = document.getElementById('add-item-name').value.trim();
const category = document.getElementById('add-item-category').value;
const amount = document.getElementById('add-item-amount').value.trim();
const unit = document.getElementById('add-item-unit').value.trim();
const note = document.getElementById('add-item-note').value.trim();
if (!name) return;
closeModal('modal-add-item');
const payload = {
item_name: name,
amount,
unit,
note,
source: 'Manuell'
};
if (category) payload.category = category;
// Optimistisch anlegen
const tempId = -Date.now();
const tempItem = {
id: tempId,
item_name: name,
amount,
unit,
note,
category: category || 'Drogerie & Sonstiges',
checked: false,
source: 'Manuell',
display_source: ''
};
shoppingItems.unshift(tempItem);
renderShoppingList();
saveLocalCache();
showToast(`✓ "${name}" hinzugefügt`);
const url = '/api/shopping-list';
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.status === 'ok' && data.item) {
const idx = shoppingItems.findIndex(x => x.id === tempId);
if (idx !== -1) shoppingItems[idx] = data.item;
renderShoppingList();
saveLocalCache();
}
} catch (err) {
pushOfflineQueue({ url, method: 'POST', body: payload });
}
}
// ==========================================
// MODAL: WOCHENPLAN IMPORT
// ==========================================
let mealPlanIngredientGroups = [];
async function openMealPlanImportModal() {
openModal('modal-import-mealplan');
const body = document.getElementById('mealplan-import-body');
const btn = document.getElementById('btn-confirm-import-mealplan');
btn.disabled = true;
body.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">⏳ Lade Zutaten des aktuellen Wochenplans...</div>';
try {
const res = await fetch('/api/mealplan/ingredients');
const data = await res.json();
if (data.status === 'ok' && data.groups && data.groups.length > 0) {
mealPlanIngredientGroups = data.groups;
renderMealPlanImportGroups(data.groups);
btn.disabled = false;
} else {
body.innerHTML = `
<div style="text-align: center; padding: 30px 10px;">
<div style="font-size: 2.2rem; margin-bottom: 8px;">🍽️</div>
<h4 style="margin: 0 0 6px 0; font-family: var(--shop-font-serif);">Noch keine Rezepte im Wochenplan</h4>
<p style="color: #666; font-size: 0.9rem; margin: 0 0 16px 0;">Plane zuerst deine Mahlzeiten im Kochbuch, um die Zutaten hier zu übernehmen.</p>
<a href="/cookbook?open=mealplan" class="btn-modal btn-modal-primary" style="text-decoration: none; display: inline-block;">
Zum Wochenmenüplan
</a>
</div>
`;
}
} catch (err) {
body.innerHTML = `<div style="color: #b00; padding: 14px;">Fehler beim Laden: ${escapeHtml(err.message)}</div>`;
}
}
function renderMealPlanImportGroups(groups) {
const body = document.getElementById('mealplan-import-body');
body.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #ddd; padding-bottom: 6px;">
<span style="font-size: 0.88rem; font-weight: 600; color: #555;">Zutaten auswählen:</span>
<button type="button" onclick="toggleAllMealPlanCheckboxes()" style="background: none; border: none; font-size: 0.82rem; color: var(--shop-green); font-weight: 700; cursor: pointer;">
Alle / Keine
</button>
</div>
`;
groups.forEach((group, gIdx) => {
const gDiv = document.createElement('div');
gDiv.style.marginBottom = '14px';
gDiv.innerHTML = `
<h4 style="margin: 0 0 6px 0; font-size: 0.95rem; font-family: var(--shop-font-serif); color: var(--shop-green);">
📖 ${escapeHtml(group.recipe_title)} <span style="font-size: 0.8rem; font-weight: normal; color: #666;">(${escapeHtml(group.slot_label)})</span>
</h4>
<div style="display: flex; flex-direction: column; gap: 4px; padding-left: 6px;" id="mpi-group-${gIdx}"></div>
`;
const inner = gDiv.querySelector(`#mpi-group-${gIdx}`);
group.items.forEach((it, iIdx) => {
const label = document.createElement('label');
label.style.display = 'flex';
label.style.alignItems = 'center';
label.style.gap = '8px';
label.style.fontSize = '0.92rem';
label.style.cursor = 'pointer';
label.innerHTML = `
<input type="checkbox" class="mpi-cb" checked data-name="${escapeHtml(it.name)}" data-amount="${escapeHtml(it.amount)}" data-unit="${escapeHtml(it.unit)}" data-recipe="${escapeHtml(group.recipe_title)}" style="width: 18px; height: 18px; accent-color: var(--shop-green);">
<span>${it.amount || it.unit ? `<strong>${escapeHtml(it.amount)} ${escapeHtml(it.unit)}</strong> ` : ''}${escapeHtml(it.name)}</span>
`;
inner.appendChild(label);
});
body.appendChild(gDiv);
});
}
function toggleAllMealPlanCheckboxes() {
const cbs = document.querySelectorAll('.mpi-cb');
const anyUnchecked = Array.from(cbs).some(cb => !cb.checked);
cbs.forEach(cb => cb.checked = anyUnchecked);
}
async function confirmImportMealPlanIngredients() {
const checkedBoxes = Array.from(document.querySelectorAll('.mpi-cb:checked'));
if (checkedBoxes.length === 0) {
alert('Bitte wähle mindestens eine Zutat aus.');
return;
}
const items = checkedBoxes.map(cb => ({
item_name: cb.getAttribute('data-name'),
amount: cb.getAttribute('data-amount') || '',
unit: cb.getAttribute('data-unit') || '',
source: cb.getAttribute('data-recipe') || 'Wochenplan'
}));
closeModal('modal-import-mealplan');
showToast(`⏳ ${items.length} Zutaten werden übernommen...`);
try {
const res = await fetch('/api/shopping-list/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items })
});
const data = await res.json();
if (data.status === 'ok') {
showToast(`✓ ${data.added_count} Zutaten erfolgreich hinzugefügt!`);
}
} catch (err) {
alert('Fehler beim Importieren: ' + err.message);
}
}
// ==========================================
// MODAL: SUPERMARKT-BEREICHE VERWALTEN
// ==========================================
function openCategoriesModal() {
renderCategoryManageList();
openModal('modal-categories');
}
function renderCategoryManageList() {
const list = document.getElementById('cat-manage-list');
list.innerHTML = '';
shoppingCategories.forEach((cat, index) => {
const li = document.createElement('li');
li.style.display = 'flex';
li.style.alignItems = 'center';
li.style.justifyContent = 'space-between';
li.style.background = '#FFFFFF';
li.style.border = '1.5px solid var(--shop-border)';
li.style.borderRadius = '8px';
li.style.padding = '8px 10px';
const isFirst = index === 0;
const isLast = index === shoppingCategories.length - 1;
li.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;">
<span style="font-size: 1.2rem;">${escapeHtml(cat.icon || '🛒')}</span>
<span style="font-weight: 600; font-size: 0.95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(cat.name)}</span>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<button type="button" onclick="moveCategory(${index}, -1)" ${isFirst ? 'disabled' : ''} style="width: 26px; height: 26px; font-size: 0.75rem; font-weight: bold; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #fff;">▲</button>
<button type="button" onclick="moveCategory(${index}, 1)" ${isLast ? 'disabled' : ''} style="width: 26px; height: 26px; font-size: 0.75rem; font-weight: bold; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #fff;">▼</button>
<button type="button" onclick="deleteCategory(${cat.id}, '${escapeHtml(cat.name)}')" style="border: none; background: transparent; color: #999; font-size: 1.2rem; cursor: pointer; padding: 2px 6px;">&times;</button>
</div>
`;
list.appendChild(li);
});
}
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();
saveLocalCache();
const ids = shoppingCategories.map(c => c.id);
try {
await fetch('/api/shopping-categories/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_ids: ids })
});
} catch (err) {}
}
async function onAddCategory(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();
saveLocalCache();
showToast(`✓ Bereich "${name}" angelegt`);
}
} catch (err) {
alert(err.message);
}
}
async function deleteCategory(id, name) {
if (!confirm(`Bereich "${name}" wirklich löschen?\nVorhandene Artikel werden auf "Drogerie & Sonstiges" übertragen.`)) return;
shoppingCategories = shoppingCategories.filter(c => c.id !== id);
renderCategoryManageList();
renderShoppingList();
saveLocalCache();
try {
await fetch(`/api/shopping-categories/${id}`, { method: 'DELETE' });
showToast(`✓ Bereich "${name}" gelöscht`);
} catch (err) {}
}
async function resetDefaultCategories() {
if (!confirm('Auf Standard-Supermarkt-Bereiche 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();
saveLocalCache();
showToast('✓ Standard-Bereiche wiederhergestellt');
}
} catch (err) {
alert(err.message);
}
}
// Modal Hilfsfunktionen
function openModal(id) {
document.getElementById(id).classList.add('active');
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
}
function onBackdropClick(e, id) {
if (e.target.id === id) closeModal(id);
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Initialisierung beim Laden
document.addEventListener('DOMContentLoaded', () => {
// Sortierungs-Button initial stylen
if (sortMode === 'alphabetical') {
document.getElementById('btn-toggle-sort').classList.add('is-active');
}
renderShoppingList();
initWebSocket();
});
</script>
</body>
</html>