Files
cookbook/app/templates/shopping_list.html
T

1607 lines
58 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="manifest" href="/manifest.json">
<meta name="theme-color" content="#1E4D3B">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="What To Cook?">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.5">
<style>
:root {
--shop-green: var(--tannengruen, #1E4D3B);
--shop-creme: var(--creme, #FAF6F0);
--shop-schwarz: var(--schwarz, #222222);
--shop-yellow: #FFF9D2;
--shop-red: #D9534F;
}
* {
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-creme);
background-image: radial-gradient(rgba(30, 77, 59, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
color: var(--shop-schwarz);
width: 100%;
}
/* Fester Container für Header + Filterleiste (kleben gemeinsam oben) */
.cb-sticky-header-container {
position: sticky;
top: 0;
z-index: 1000;
width: 100%;
background: var(--shop-green);
}
/* Live Online Badge in Navbar */
.cb-online-pill {
font-size: 0.72rem;
font-weight: 700;
padding: 2px 7px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.2);
color: var(--shop-creme);
border: 1px solid rgba(255, 255, 255, 0.35);
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
user-select: none;
}
.cb-online-pill.is-offline {
background: rgba(217, 83, 79, 0.4);
border-color: #D9534F;
color: #FFF;
}
/* Hauptinhalt Container */
.shop-main-container {
max-width: 820px;
width: 100%;
margin: 0 auto;
padding: 14px 12px 100px 12px;
}
/* Kategorie Sektionen (kompakter & dezenter) */
.shop-cat-section {
margin-bottom: 14px;
}
.shop-cat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
margin-bottom: 5px;
background: rgba(30, 77, 59, 0.08);
border-left: 3.5px solid var(--shop-green);
border-radius: 4px 6px 6px 4px;
}
.shop-cat-title {
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 1.15rem;
font-weight: 700;
color: var(--shop-green);
margin: 0;
display: flex;
align-items: center;
gap: 6px;
line-height: 1.1;
}
.shop-cat-count {
font-size: 0.74rem;
font-weight: 700;
color: rgba(34, 34, 34, 0.65);
background: rgba(255, 255, 255, 0.85);
padding: 1px 6px;
border-radius: 8px;
border: 1px solid rgba(34, 34, 34, 0.15);
}
/* Listen-Einträge: Kompakter, lesbar, volle Breite */
.shop-items-list {
display: flex;
flex-direction: column;
gap: 5px;
list-style: none;
padding: 0;
margin: 0;
}
.shop-item-row {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 11px;
background: #FFFFFF;
border: 1.5px solid rgba(34, 34, 34, 0.15);
border-radius: 8px;
box-shadow: 1px 2px 0px rgba(30, 77, 59, 0.06);
transition: background-color 0.15s, opacity 0.15s, transform 0.12s;
position: relative;
}
.shop-item-row:active {
transform: scale(0.995);
}
.shop-item-row.is-checked {
background: rgba(250, 246, 240, 0.65);
border-color: rgba(34, 34, 34, 0.1);
opacity: 0.68;
}
/* Checkbox */
.shop-checkbox-wrap {
flex-shrink: 0;
display: flex;
align-items: center;
cursor: pointer;
}
.shop-checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
width: 0;
height: 0;
}
.shop-custom-check {
width: 20px;
height: 20px;
border-radius: 5px;
border: 2px solid var(--shop-green);
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
color: transparent;
font-size: 0.85rem;
font-weight: 900;
}
.shop-item-row.is-checked .shop-custom-check {
background: var(--shop-green);
color: #FFFFFF;
border-color: var(--shop-green);
}
/* Textbereich (Klick öffnet Bearbeiten-Modal) */
.shop-item-content {
flex: 1 1 auto;
min-width: 0;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 2px;
}
.shop-item-title-row {
display: flex;
align-items: baseline;
gap: 7px;
flex-wrap: wrap;
word-break: break-word;
}
.shop-item-name {
font-size: 0.94rem;
font-weight: 600;
color: var(--shop-schwarz);
line-height: 1.25;
}
.shop-item-row.is-checked .shop-item-name {
text-decoration: line-through;
color: rgba(34, 34, 34, 0.5);
}
.shop-item-amount {
font-size: 0.84rem;
font-weight: 700;
color: var(--shop-green);
background: rgba(30, 77, 59, 0.1);
padding: 1px 6px;
border-radius: 5px;
white-space: nowrap;
}
.shop-item-row.is-checked .shop-item-amount {
color: rgba(30, 77, 59, 0.5);
background: rgba(30, 77, 59, 0.05);
}
/* Badge nur für persönliche Notiz */
.shop-item-badges {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
}
.shop-badge-note {
font-size: 0.74rem;
color: #7a5a00;
background: var(--shop-yellow);
border: 1px solid #d4c165;
padding: 1px 5px;
border-radius: 4px;
display: inline-flex;
align-items: center;
gap: 3px;
}
/* Löschen-Icon am rechten Rand */
.shop-item-delete {
flex-shrink: 0;
background: none;
border: none;
font-size: 1rem;
color: rgba(34, 34, 34, 0.35);
cursor: pointer;
padding: 5px 6px;
border-radius: 6px;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
}
.shop-item-delete:hover,
.shop-item-delete:active {
color: var(--shop-red);
background: rgba(217, 83, 79, 0.1);
}
/* Modal Form: Labels immer direkt über Input-Feld */
.modal-form-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.modal-form-grid > div {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.modal-form-grid .cb-label {
display: block;
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 1.15rem;
font-weight: 700;
color: var(--shop-schwarz);
margin: 0 0 3px 0;
text-align: left;
}
.modal-form-grid .cb-input,
.modal-form-grid .cb-select {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
font-size: 0.95rem;
}
/* Floating Action Button (FAB '+') */
.cb-fab {
position: fixed;
bottom: 24px;
right: 24px;
width: 58px;
height: 58px;
border-radius: 50%;
background: var(--shop-green);
color: var(--shop-creme);
border: 2.5px solid var(--shop-schwarz);
box-shadow: 3px 5px 12px rgba(0, 0, 0, 0.35);
font-size: 2.2rem;
font-family: var(--font-hand, 'Caveat', cursive);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 950;
transition: all 0.18s ease;
line-height: 1;
padding-bottom: 4px;
}
.cb-fab:hover {
transform: scale(1.08) translateY(-2px);
background: #143729;
}
.cb-fab:active {
transform: scale(0.95);
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.3);
}
/* Leerer Zustand */
.shop-empty-state {
text-align: center;
padding: 50px 20px;
background: rgba(255, 255, 255, 0.85);
border: 2px dashed rgba(30, 77, 59, 0.3);
border-radius: 12px;
margin: 30px auto;
max-width: 480px;
}
.shop-empty-icon {
font-size: 3.5rem;
margin-bottom: 12px;
}
.shop-empty-title {
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 2.2rem;
color: var(--shop-green);
margin: 0 0 8px 0;
}
.shop-empty-desc {
font-size: 0.98rem;
color: #555;
margin: 0 0 20px 0;
line-height: 1.4;
}
/* Modal Form Ergänzungen */
.modal-form-grid {
display: flex;
flex-direction: column;
gap: 14px;
}
.modal-btn-row {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 18px;
}
/* Toast Meldung */
.shop-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--shop-green);
color: #FFFFFF;
padding: 10px 20px;
border-radius: 30px;
font-weight: 700;
font-size: 0.95rem;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
z-index: 3000;
transition: transform 0.25s ease-out;
pointer-events: none;
}
.shop-toast.show {
transform: translateX(-50%) translateY(0);
}
/* Safe Area Inset für Mobile */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.cb-fab {
bottom: calc(24px + env(safe-area-inset-bottom));
}
.shop-main-container {
padding-bottom: calc(100px + env(safe-area-inset-bottom));
}
}
</style>
</head>
<body class="shopping-app-body">
<!-- ============================================================== -->
<!-- FIXIERTER KOPFBEREICH: HEADER & KATEGORIE-FILTERLEISTE GEMEINSAM -->
<!-- ============================================================== -->
<div class="cb-sticky-header-container">
<header class="cb-nav">
<a href="/shopping-list" class="cb-nav-brand" title="Einkaufsliste neu laden">
<span class="cb-brand-icon">🛒</span>
<h1 class="cb-nav-title">Einkaufsliste</h1>
</a>
<span id="shop-online-pill" class="cb-online-pill" title="Synchronisations-Status">● Live</span>
<!-- Desktop Buttons (auf Bildschirmen > 768px) -->
<div class="cb-nav-actions desktop-nav-actions">
<button type="button" onclick="openCategoriesModal()" class="cb-nav-btn" title="Supermarkt-Bereiche & Laufweg anpassen">
<span>🏬</span> <span>Bereiche anpassen</span>
</button>
<button type="button" onclick="openMealPlanImportModal()" class="cb-nav-btn" title="Zutaten aus Wochenplan holen">
<span>📅</span> <span>Aus Wochenplan holen</span>
</button>
<button type="button" onclick="deleteCheckedItems()" class="cb-nav-btn" title="Erledigte Artikel löschen">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
<button type="button" onclick="openDeleteStep1Modal()" class="cb-nav-btn cb-btn-danger" style="background:#b02a37; border-color:#b02a37; color:#fff;" title="Liste leeren">
<span>🗑️</span> <span>Liste leeren</span>
</button>
<a href="/" class="cb-nav-btn" title="Zur Startseite">
<span>🏠</span> <span>Startseite</span>
</a>
<a href="/cookbook" class="cb-nav-btn" title="Kochbuch & Rezepte">
<span>📖</span> <span>Kochbuch</span>
</a>
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
<!-- Mobile 3-Punkte (Kebab) Menü (auf Bildschirmen <= 768px) -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" onclick="toggleMobileMenu(event)" aria-label="Menü öffnen" title="Menü">
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<button type="button" onclick="handleMobileMenuAction(openCategoriesModal)" class="cb-mobile-item">
<span>🏬</span> <span>Bereiche anpassen</span>
</button>
<button type="button" onclick="handleMobileMenuAction(openMealPlanImportModal)" class="cb-mobile-item">
<span>📅</span> <span>Aus Wochenplan holen</span>
</button>
<button type="button" onclick="handleMobileMenuAction(deleteCheckedItems)" class="cb-mobile-item">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
<hr class="cb-mobile-divider">
<button type="button" onclick="handleMobileMenuAction(openDeleteStep1Modal)" class="cb-mobile-item" style="color: #b02a37; font-weight: 700;">
<span>🗑️</span> <span>Liste leeren</span>
</button>
<hr class="cb-mobile-divider">
<a href="/" class="cb-mobile-item">
<span>🏠</span> <span>Startseite</span>
</a>
<a href="/cookbook" class="cb-mobile-item">
<span>📖</span> <span>Kochbuch</span>
</a>
<hr class="cb-mobile-divider">
<a href="/logout" class="cb-mobile-item cb-mobile-logout">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
</div>
</header>
<!-- DAUERHAFT FIXIERTE KATEGORIEN-LEISTE -->
<nav class="cb-filter-bar no-print" id="shop-category-bar">
<div class="cb-filter-scroll-container" id="shop-category-chips">
<!-- Dynamisch befüllt mit Kategorien -->
</div>
</nav>
</div>
<!-- ========================================== -->
<!-- HAUPTINHALT: EINKAUFSLISTEN-EINTRÄGE -->
<!-- ========================================== -->
<main class="shop-main-container">
<div id="items-container">
<!-- Wird dynamisch aus JavaScript gerendert -->
</div>
<!-- Leerer Zustand -->
<div class="shop-empty-state" id="empty-state" style="display: none;">
<div class="shop-empty-icon">🎉</div>
<h2 class="shop-empty-title">Alles eingekauft!</h2>
<p class="shop-empty-desc">Deine Einkaufsliste ist leer. Tippe unten rechts auf das <strong>+</strong>, um einen neuen Eintrag zu notieren, oder übernimm Zutaten direkt aus dem Wochenplan.</p>
<button type="button" class="cb-btn cb-btn-green" onclick="openMealPlanImportModal()">
<span>📅</span> <span>Aus Wochenplan importieren</span>
</button>
</div>
</main>
<!-- Floating Action Button (FAB '+') -->
<button type="button" class="cb-fab" onclick="openAddItemModal()" title="Neuen Eintrag hinzufügen">
<span>+</span>
</button>
<!-- Toast Notification -->
<div id="shop-toast" class="shop-toast">✓ Gespeichert</div>
<!-- ========================================== -->
<!-- MODAL: EINTRAG BEARBEITEN -->
<!-- ========================================== -->
<div id="modal-edit-item" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-edit-item')">
<div class="cb-modal-card" style="max-width: 500px;">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>✏️</span> Eintrag bearbeiten</h3>
<button type="button" class="cb-modal-close" onclick="closeModal('modal-edit-item')" title="Schließen">&times;</button>
</div>
<form onsubmit="onSaveEditItem(event)">
<div class="cb-modal-body">
<input type="hidden" id="edit-item-id">
<div class="modal-form-grid">
<div>
<label class="cb-label" for="edit-item-name">Artikel / Zutat *</label>
<input type="text" id="edit-item-name" class="cb-input" required autocomplete="off">
</div>
<div>
<label class="cb-label" for="edit-item-category">Bereich im Supermarkt</label>
<select id="edit-item-category" class="cb-select"></select>
</div>
<div>
<label class="cb-label" for="edit-item-amount">Menge</label>
<input type="text" id="edit-item-amount" class="cb-input" placeholder="z. B. 2 oder 500">
</div>
<div>
<label class="cb-label" for="edit-item-unit">Einheit</label>
<input type="text" id="edit-item-unit" class="cb-input" placeholder="z. B. g, l, Bund, Dose">
</div>
<div>
<label class="cb-label" for="edit-item-note">Notiz / Extra</label>
<input type="text" id="edit-item-note" class="cb-input" placeholder="z. B. Bio, laktosefrei, im Angebot">
</div>
</div>
<div class="modal-btn-row">
<button type="button" class="cb-btn cb-btn-danger" onclick="onDeleteItemFromModal()" style="margin-right: auto; background: #b02a37; border-color: #b02a37; color: #fff;">
🗑️ Löschen
</button>
<button type="button" class="cb-btn cb-btn-secondary" onclick="closeModal('modal-edit-item')">
Schließen
</button>
<button type="submit" class="cb-btn cb-btn-green">
Speichern
</button>
</div>
</div>
</form>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: NEUER EINTRAG (FAB '+') -->
<!-- ========================================== -->
<div id="modal-add-item" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-add-item')">
<div class="cb-modal-card" style="max-width: 500px;">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>➕</span> Neuer Eintrag</h3>
<button type="button" class="cb-modal-close" onclick="closeModal('modal-add-item')" title="Schließen">&times;</button>
</div>
<form onsubmit="onSaveAddItem(event)">
<div class="cb-modal-body">
<div class="modal-form-grid">
<div>
<label class="cb-label" for="add-item-name">Artikel / Zutat *</label>
<input type="text" id="add-item-name" class="cb-input" placeholder="z. B. Gurke oder 500g Quark..." required autocomplete="off">
</div>
<div>
<label class="cb-label" for="add-item-category">Bereich im Supermarkt</label>
<select id="add-item-category" class="cb-select">
<option value="">🪄 Automatisch erkennen</option>
</select>
</div>
<div>
<label class="cb-label" for="add-item-amount">Menge</label>
<input type="text" id="add-item-amount" class="cb-input" placeholder="z. B. 2 oder 500">
</div>
<div>
<label class="cb-label" for="add-item-unit">Einheit</label>
<input type="text" id="add-item-unit" class="cb-input" placeholder="z. B. g, l, Bund, Dose">
</div>
<div>
<label class="cb-label" for="add-item-note">Notiz / Extra</label>
<input type="text" id="add-item-note" class="cb-input" placeholder="z. B. Bio, im Angebot">
</div>
</div>
<div class="modal-btn-row">
<button type="button" class="cb-btn cb-btn-secondary" onclick="closeModal('modal-add-item')">
Schließen
</button>
<button type="submit" class="cb-btn cb-btn-green">
Hinzufügen
</button>
</div>
</div>
</form>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: 2-STUFEN LÖSCHEN -->
<!-- ========================================== -->
<!-- Stufe 1: Einkaufsliste leeren? -->
<div id="modal-delete-step1" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-delete-step1')">
<div class="cb-modal-card" style="max-width: 440px;">
<div class="cb-modal-header" style="background: #b02a37;">
<h3 class="cb-modal-title"><span>🗑️</span> Einkaufsliste leeren?</h3>
<button type="button" class="cb-modal-close" onclick="closeModal('modal-delete-step1')" title="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="margin: 0 0 20px 0; font-size: 1.05rem; line-height: 1.45;">
Möchtest du wirklich alle Artikel von der Einkaufsliste entfernen?
</p>
<div class="modal-btn-row">
<button type="button" class="cb-btn cb-btn-secondary" onclick="closeModal('modal-delete-step1')">
Schließen
</button>
<button type="button" class="cb-btn cb-btn-danger" onclick="openDeleteStep2Modal()" style="background: #b02a37; border-color: #b02a37; color: #fff;">
Löschen
</button>
</div>
</div>
</div>
</div>
<!-- Stufe 2: Wirklich alles löschen? -->
<div id="modal-delete-step2" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-delete-step2')">
<div class="cb-modal-card" style="max-width: 440px; border-color: #800;">
<div class="cb-modal-header" style="background: #800;">
<h3 class="cb-modal-title"><span>⚠️</span> Wirklich alles löschen?</h3>
<button type="button" class="cb-modal-close" onclick="closeModal('modal-delete-step2')" title="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="margin: 0 0 20px 0; font-size: 1.05rem; line-height: 1.45; color: #800; font-weight: 700;">
Achtung: Alle Einträge (auch noch nicht erledigte Artikel) werden unwiderruflich gelöscht!
</p>
<div class="modal-btn-row">
<button type="button" class="cb-btn cb-btn-secondary" onclick="closeModal('modal-delete-step2')">
Abbrechen
</button>
<button type="button" class="cb-btn cb-btn-danger" onclick="confirmClearAllItems()" style="background: #800; border-color: #800; color: #fff;">
Ja, wirklich alles löschen
</button>
</div>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: WOCHENPLAN ZUTATEN IMPORTIEREN -->
<!-- ========================================== -->
<div id="modal-import-mealplan" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-import-mealplan')">
<div class="cb-modal-card" style="max-width: 580px;">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>📅</span> Zutaten aus Wochenplan holen</h3>
<button type="button" class="cb-modal-close" onclick="closeModal('modal-import-mealplan')" title="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<div id="mealplan-import-body" style="min-height: 120px;">
<div style="text-align: center; padding: 25px; color: #666;">
<span>⏳ Zutaten des aktuellen Wochenplans werden geladen...</span>
</div>
</div>
<div class="modal-btn-row">
<button type="button" class="cb-btn cb-btn-secondary" onclick="closeModal('modal-import-mealplan')">
Schließen
</button>
<button type="button" class="cb-btn cb-btn-green" id="btn-confirm-import-mealplan" onclick="confirmImportMealPlanIngredients()" disabled>
Ausgewählte übernehmen
</button>
</div>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: SUPERMARKT-BEREICHE & LAUFWEG -->
<!-- ========================================== -->
<div id="modal-categories" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-categories')">
<div class="cb-modal-card" style="max-width: 540px;">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>🏬</span> Supermarkt-Bereiche anpassen</h3>
<button type="button" class="cb-modal-close" onclick="closeModal('modal-categories')" title="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="font-size: 0.9rem; color: #555; margin: 0 0 12px 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 0 16px 0; padding: 0; display: flex; flex-direction: column; gap: 8px;"></ul>
<div style="background: var(--shop-yellow); border: 1.5px solid var(--shop-schwarz); border-radius: 8px; padding: 12px;">
<h4 style="margin: 0 0 8px 0; font-size: 0.95rem;">Neuen Bereich anlegen</h4>
<form onsubmit="onAddCategory(event)" style="display: flex; gap: 8px;">
<input type="text" id="new-cat-icon" style="width: 44px; text-align: center; font-size: 1.2rem; border: 1.5px solid #888; border-radius: 6px;" value="🛒">
<input type="text" id="new-cat-name" placeholder="z. B. Drogerie & Haushalt" class="cb-input" style="flex: 1; padding: 8px 12px;" required>
<button type="submit" class="cb-btn cb-btn-green" style="padding: 8px 14px;">Hinzufügen</button>
</form>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 14px;">
<button type="button" onclick="resetDefaultCategories()" style="background: none; border: none; font-size: 0.82rem; color: #777; cursor: pointer; text-decoration: underline;">
↺ Standard-Bereiche wiederherstellen
</button>
<button type="button" class="cb-btn cb-btn-secondary" onclick="closeModal('modal-categories')">
Fertig
</button>
</div>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- JAVASCRIPT: LOGIK, WEBSOCKET & OFFLINE-SYNC-->
<!-- ========================================== -->
<script>
// State Initialisierung aus Server oder localStorage
let allItems = [];
let allCategories = [];
let currentFilterCategory = null;
let ws = null;
let reconnectTimeout = null;
let pingInterval = null;
// Server-Daten einbinden
try {
allItems = {{ items_json | safe }};
allCategories = {{ categories_json | safe }};
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories));
} catch (e) {
console.warn("Lade Daten aus Offline-Cache:", e);
allItems = JSON.parse(localStorage.getItem('wtc_shop_items_cache') || '[]');
allCategories = JSON.parse(localStorage.getItem('wtc_shop_cats_cache') || '[]');
}
// ==========================================
// MOBILE NAVIGATION (3-PUNKTE-MENÜ)
// ==========================================
function toggleMobileMenu(e) {
if (e) e.stopPropagation();
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.toggle('is-open');
}
function handleMobileMenuAction(fn) {
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
if (fn) fn();
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-nav-wrapper')) {
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
});
// ==========================================
// 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);
if (pingInterval) clearInterval(pingInterval);
pingInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send('ping');
}
}, 25000);
};
ws.onmessage = (event) => {
if (event.data === 'pong') return;
try {
const data = JSON.parse(event.data);
if (data.action === 'items_updated') {
allItems = data.items;
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
renderShoppingList();
} else if (data.action === 'categories_updated') {
allCategories = data.categories;
if (data.items) allItems = data.items;
localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories));
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
renderCategoryFilterChips();
renderShoppingList();
}
} catch (err) {
console.error("Fehler beim Verarbeiten der WebSocket-Nachricht:", err);
}
};
ws.onclose = () => {
if (pingInterval) clearInterval(pingInterval);
updateOnlineStatus(false);
scheduleReconnect();
};
ws.onerror = () => {
if (ws) ws.close();
};
} catch (e) {
updateOnlineStatus(false);
scheduleReconnect();
}
}
function scheduleReconnect() {
if (reconnectTimeout) clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(initWebSocket, 3000);
}
function updateOnlineStatus(isOnline) {
const pill = document.getElementById('shop-online-pill');
if (!pill) return;
if (isOnline) {
pill.textContent = '● Live';
pill.classList.remove('is-offline');
} else {
pill.textContent = '⚡ Offline';
pill.classList.add('is-offline');
}
}
window.addEventListener('online', () => {
updateOnlineStatus(true);
initWebSocket();
flushOfflineQueue();
});
window.addEventListener('offline', () => {
updateOnlineStatus(false);
});
// ==========================================
// OFFLINE MUTATION QUEUE
// ==========================================
function getOfflineQueue() {
return JSON.parse(localStorage.getItem('wtc_shop_offline_queue') || '[]');
}
function pushOfflineQueue(action) {
const q = getOfflineQueue();
q.push(action);
localStorage.setItem('wtc_shop_offline_queue', JSON.stringify(q));
}
async function flushOfflineQueue() {
const q = getOfflineQueue();
if (!q.length) return;
localStorage.removeItem('wtc_shop_offline_queue');
for (const act of q) {
try {
await fetch(act.url, {
method: act.method,
headers: { 'Content-Type': 'application/json' },
body: act.body ? JSON.stringify(act.body) : undefined
});
} catch (e) {
console.warn("Fehler beim Nachholen einer Offline-Aktion:", e);
}
}
refreshDataFromServer();
}
async function refreshDataFromServer() {
try {
const [itemsRes, catsRes] = await Promise.all([
fetch('/api/shopping-list'),
fetch('/api/shopping-categories')
]);
if (itemsRes.ok) {
const d = await itemsRes.json();
allItems = d.items || [];
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
}
if (catsRes.ok) {
const d = await catsRes.json();
allCategories = d.categories || [];
localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories));
}
renderCategoryFilterChips();
renderShoppingList();
} catch (e) {
console.warn("Konnte Daten nicht vom Server aktualisieren:", e);
}
}
// ==========================================
// RENDERING: KATEGORIEN-FILTERLEISTE
// ==========================================
function renderCategoryFilterChips() {
const container = document.getElementById('shop-category-chips');
if (!container) return;
const totalItems = allItems.length;
let html = `
<button type="button" class="cb-chip ${currentFilterCategory === null ? 'active' : ''}" onclick="selectFilterCategory(null)">
Alle (${totalItems})
</button>
`;
allCategories.forEach(cat => {
const count = allItems.filter(it => it.category === cat.name).length;
if (count > 0 || currentFilterCategory === cat.name) {
const isAct = currentFilterCategory === cat.name;
html += `
<button type="button" class="cb-chip ${isAct ? 'active' : ''}" onclick="selectFilterCategory('${escapeAttr(cat.name)}')">
<span>${cat.icon || '🛒'}</span> <span>${escapeHtml(cat.name)}</span> <span>(${count})</span>
</button>
`;
}
});
// 'Sonstiges' anzeigen falls vorhanden
const uncategorizedCount = allItems.filter(it => !it.category || it.category === 'Sonstiges').length;
if (uncategorizedCount > 0 && !allCategories.some(c => c.name === 'Sonstiges')) {
const isAct = currentFilterCategory === 'Sonstiges';
html += `
<button type="button" class="cb-chip ${isAct ? 'active' : ''}" onclick="selectFilterCategory('Sonstiges')">
<span>🛒</span> <span>Sonstiges</span> <span>(${uncategorizedCount})</span>
</button>
`;
}
container.innerHTML = html;
}
function selectFilterCategory(catName) {
currentFilterCategory = catName;
renderCategoryFilterChips();
renderShoppingList();
}
// ==========================================
// RENDERING: EINKAUFSLISTEN-ITEMS (LAUFWEG)
// ==========================================
function renderShoppingList() {
const container = document.getElementById('items-container');
const emptyState = document.getElementById('empty-state');
if (!container) return;
// Filter anwenden
let filtered = allItems;
if (currentFilterCategory !== null) {
filtered = filtered.filter(it => (it.category || 'Sonstiges') === currentFilterCategory);
}
if (filtered.length === 0) {
container.innerHTML = '';
if (emptyState) emptyState.style.display = 'block';
return;
} else {
if (emptyState) emptyState.style.display = 'none';
}
renderCategorizedList(container, filtered);
}
function renderCategorizedList(container, items) {
// 1. Kategorien nach sort_order sortieren
const catOrderMap = {};
allCategories.forEach((c, idx) => {
catOrderMap[c.name] = (c.sort_order !== null && c.sort_order !== undefined) ? c.sort_order : idx;
});
// 2. Items in Gruppen aufteilen (offen vs. abgehakt)
const groups = {};
items.forEach(it => {
const cat = it.category || 'Sonstiges';
if (!groups[cat]) groups[cat] = { open: [], checked: [] };
if (it.checked) {
groups[cat].checked.push(it);
} else {
groups[cat].open.push(it);
}
});
// 3. Gruppen-Reihenfolge bestimmen
const groupNames = Object.keys(groups).sort((a, b) => {
const orderA = catOrderMap[a] !== undefined ? catOrderMap[a] : 999;
const orderB = catOrderMap[b] !== undefined ? catOrderMap[b] : 999;
return orderA - orderB;
});
let html = '';
groupNames.forEach(catName => {
const group = groups[catName];
const allInGroup = [...group.open, ...group.checked];
if (allInGroup.length === 0) return;
const catObj = allCategories.find(c => c.name === catName) || { name: catName, icon: '🛒' };
html += `
<section class="shop-cat-section">
<div class="shop-cat-header">
<h2 class="shop-cat-title">
<span>${catObj.icon || '🛒'}</span> <span>${escapeHtml(catName)}</span>
</h2>
<span class="shop-cat-count">${group.open.length} offen / ${allInGroup.length}</span>
</div>
<ul class="shop-items-list">
${allInGroup.map(item => renderItemRow(item)).join('')}
</ul>
</section>
`;
});
container.innerHTML = html;
}
function renderItemRow(item) {
const isChecked = item.checked ? 'is-checked' : '';
const amountStr = formatAmount(item.amount, item.unit);
return `
<li class="shop-item-row ${isChecked}" id="shop-row-${item.id}">
<label class="shop-checkbox-wrap" onclick="event.stopPropagation();">
<input type="checkbox" class="shop-checkbox-input" ${item.checked ? 'checked' : ''} onchange="toggleItemCheck(${item.id})">
<span class="shop-custom-check">✓</span>
</label>
<div class="shop-item-content" onclick="openEditModalById(${item.id})" title="Klicken zum Bearbeiten">
<div class="shop-item-title-row">
<span class="shop-item-name">${escapeHtml(item.item_name)}</span>
${amountStr ? `<span class="shop-item-amount">${escapeHtml(amountStr)}</span>` : ''}
</div>
${item.note ? `
<div class="shop-item-badges">
<span class="shop-badge-note">📝 ${escapeHtml(item.note)}</span>
</div>
` : ''}
</div>
<button type="button" class="shop-item-delete" onclick="deleteItemDirectly(${item.id}, event)" title="Eintrag löschen">
🗑️
</button>
</li>
`;
}
function formatAmount(amount, unit) {
if (!amount && !unit) return '';
if (amount && unit) return `${amount} ${unit}`;
return amount || unit;
}
// ==========================================
// AKTIONEN: TOGGLE, ADD, EDIT, DELETE
// ==========================================
async function toggleItemCheck(itemId) {
const item = allItems.find(it => it.id === itemId);
if (!item) return;
// Optimistic UI Update
item.checked = !item.checked;
renderShoppingList();
try {
const res = await fetch(`/api/shopping-list/${itemId}/toggle`, { method: 'POST' });
if (!res.ok) throw new Error('Fehler beim Abhaken');
} catch (err) {
pushOfflineQueue({ method: 'POST', url: `/api/shopping-list/${itemId}/toggle` });
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
}
}
async function deleteItemDirectly(itemId, event) {
if (event) event.stopPropagation();
// Optimistic remove
allItems = allItems.filter(it => it.id !== itemId);
renderShoppingList();
showToast('Eintrag gelöscht');
try {
const res = await fetch(`/api/shopping-list/${itemId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Fehler beim Löschen');
} catch (err) {
pushOfflineQueue({ method: 'DELETE', url: `/api/shopping-list/${itemId}` });
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
}
}
async function deleteCheckedItems() {
const checkedCount = allItems.filter(it => it.checked).length;
if (checkedCount === 0) {
showToast('Keine erledigten Artikel vorhanden');
return;
}
allItems = allItems.filter(it => !it.checked);
renderShoppingList();
showToast(`${checkedCount} erledigte Artikel gelöscht`);
try {
await fetch('/api/shopping-list/checked', { method: 'DELETE' });
} catch (err) {
pushOfflineQueue({ method: 'DELETE', url: '/api/shopping-list/checked' });
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
}
}
// ==========================================
// MODAL: NEUER EINTRAG (ADD)
// ==========================================
function openAddItemModal() {
populateCategorySelect('add-item-category', '');
document.getElementById('add-item-name').value = '';
document.getElementById('add-item-amount').value = '';
document.getElementById('add-item-unit').value = '';
document.getElementById('add-item-note').value = '';
openModal('modal-add-item');
setTimeout(() => document.getElementById('add-item-name').focus(), 100);
}
async function onSaveAddItem(e) {
e.preventDefault();
const name = document.getElementById('add-item-name').value.trim();
if (!name) return;
const cat = document.getElementById('add-item-category').value || null;
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();
closeModal('modal-add-item');
const payload = {
item_name: name,
category: cat,
amount: amount,
unit: unit,
note: note,
source: ''
};
try {
const res = await fetch('/api/shopping-list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
const data = await res.json();
if (data.item) {
allItems.push(data.item);
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
renderShoppingList();
renderCategoryFilterChips();
}
} else {
throw new Error('Server-Fehler');
}
} catch (err) {
// Offline Fallback
const tempItem = {
id: Date.now(),
item_name: name,
category: cat || 'Sonstiges',
amount: amount,
unit: unit,
note: note,
checked: false,
source: ''
};
allItems.push(tempItem);
pushOfflineQueue({ method: 'POST', url: '/api/shopping-list', body: payload });
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
renderShoppingList();
}
showToast('Eintrag hinzugefügt');
}
// ==========================================
// MODAL: EINTRAG BEARBEITEN (EDIT)
// ==========================================
function openEditModalById(itemId) {
const item = allItems.find(it => it.id === itemId);
if (!item) return;
document.getElementById('edit-item-id').value = item.id;
document.getElementById('edit-item-name').value = item.item_name || '';
document.getElementById('edit-item-amount').value = item.amount || '';
document.getElementById('edit-item-unit').value = item.unit || '';
document.getElementById('edit-item-note').value = item.note || '';
populateCategorySelect('edit-item-category', item.category || 'Sonstiges');
openModal('modal-edit-item');
setTimeout(() => document.getElementById('edit-item-name').focus(), 100);
}
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();
if (!name || !id) return;
const cat = document.getElementById('edit-item-category').value || 'Sonstiges';
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();
closeModal('modal-edit-item');
// Optimistic update
const item = allItems.find(it => it.id === id);
if (item) {
item.item_name = name;
item.category = cat;
item.amount = amount;
item.unit = unit;
item.note = note;
renderShoppingList();
renderCategoryFilterChips();
}
const payload = {
item_name: name,
category: cat,
amount: amount,
unit: unit,
note: note
};
try {
await fetch(`/api/shopping-list/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (err) {
pushOfflineQueue({ method: 'PUT', url: `/api/shopping-list/${id}`, body: payload });
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
}
showToast('Eintrag aktualisiert');
}
function onDeleteItemFromModal() {
const id = parseInt(document.getElementById('edit-item-id').value, 10);
if (!id) return;
closeModal('modal-edit-item');
deleteItemDirectly(id);
}
// ==========================================
// 2-STUFEN LÖSCHEN MODALS
// ==========================================
function openDeleteStep1Modal() {
openModal('modal-delete-step1');
}
function openDeleteStep2Modal() {
closeModal('modal-delete-step1');
openModal('modal-delete-step2');
}
async function confirmClearAllItems() {
closeModal('modal-delete-step2');
allItems = [];
renderShoppingList();
renderCategoryFilterChips();
showToast('Einkaufsliste komplett geleert');
try {
await fetch('/api/shopping-list/all', { method: 'DELETE' });
} catch (err) {
pushOfflineQueue({ method: 'DELETE', url: '/api/shopping-list/all' });
localStorage.setItem('wtc_shop_items_cache', JSON.stringify([]));
}
}
// ==========================================
// MODAL: WOCHENPLAN ZUTATEN IMPORTIEREN
// ==========================================
async function openMealPlanImportModal() {
openModal('modal-import-mealplan');
const body = document.getElementById('mealplan-import-body');
const confirmBtn = document.getElementById('btn-confirm-import-mealplan');
if (confirmBtn) confirmBtn.disabled = true;
body.innerHTML = `
<div style="text-align: center; padding: 25px; color: #666;">
<span>⏳ Zutaten des aktuellen Wochenplans werden geladen...</span>
</div>
`;
try {
const res = await fetch('/api/mealplan/ingredients');
if (!res.ok) throw new Error('Fehler beim Laden');
const data = await res.json();
const groups = data.groups || [];
if (groups.length === 0) {
body.innerHTML = `
<div style="text-align: center; padding: 25px; color: #666;">
<p style="font-size: 1.1rem; margin: 0 0 10px 0;">Keine Rezepte im Wochenplan gefunden.</p>
<p style="font-size: 0.9rem; margin: 0;">Plane zuerst Gerichte im Kochbuch-Wochenmenüplan ein!</p>
</div>
`;
return;
}
let html = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 700; font-size: 0.92rem;">KW ${data.week} / ${data.year}</span>
<button type="button" onclick="toggleAllMealPlanChecks(true)" style="background: none; border: none; color: var(--shop-green); font-size: 0.85rem; font-weight: 700; cursor: pointer; text-decoration: underline;">
Alle auswählen
</button>
</div>
<div style="max-height: 50vh; overflow-y: auto; display: flex; flex-direction: column; gap: 14px;">
`;
groups.forEach((g, gIdx) => {
html += `
<div style="background: rgba(30, 77, 59, 0.05); border: 1.5px solid rgba(30, 77, 59, 0.15); border-radius: 8px; padding: 10px;">
<div style="font-family: var(--font-hand); font-size: 1.25rem; font-weight: 700; color: var(--shop-green); margin-bottom: 6px;">
🍳 ${escapeHtml(g.recipe_title)}
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
${g.ingredients.map((ing, iIdx) => `
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.92rem; cursor: pointer; padding: 3px 0;">
<input type="checkbox" class="mealplan-ing-checkbox" checked
data-name="${escapeAttr(ing.item_name)}"
data-amount="${escapeAttr(ing.amount || '')}"
data-unit="${escapeAttr(ing.unit || '')}"
onchange="updateMealPlanConfirmBtn()">
<span><strong>${escapeHtml(formatAmount(ing.amount, ing.unit))}</strong> ${escapeHtml(ing.item_name)}</span>
</label>
`).join('')}
</div>
</div>
`;
});
html += `</div>`;
body.innerHTML = html;
if (confirmBtn) confirmBtn.disabled = false;
} catch (err) {
body.innerHTML = `
<div style="text-align: center; padding: 20px; color: #b00;">
Fehler beim Laden des Wochenplans. Bitte stelle sicher, dass du online bist.
</div>
`;
}
}
function toggleAllMealPlanChecks(val) {
document.querySelectorAll('.mealplan-ing-checkbox').forEach(cb => cb.checked = val);
updateMealPlanConfirmBtn();
}
function updateMealPlanConfirmBtn() {
const anyChecked = Array.from(document.querySelectorAll('.mealplan-ing-checkbox')).some(cb => cb.checked);
const btn = document.getElementById('btn-confirm-import-mealplan');
if (btn) btn.disabled = !anyChecked;
}
async function confirmImportMealPlanIngredients() {
const cbs = Array.from(document.querySelectorAll('.mealplan-ing-checkbox:checked'));
if (cbs.length === 0) return;
const itemsToAdd = cbs.map(cb => ({
item_name: cb.dataset.name,
amount: cb.dataset.amount || '',
unit: cb.dataset.unit || '',
source: '',
category: null,
note: ''
}));
closeModal('modal-import-mealplan');
try {
const res = await fetch('/api/shopping-list/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: itemsToAdd })
});
if (res.ok) {
const d = await res.json();
allItems = d.items || [];
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
renderCategoryFilterChips();
renderShoppingList();
showToast(`${itemsToAdd.length} Zutaten übernommen`);
}
} catch (err) {
showToast('Fehler beim Übernehmen');
}
}
// ==========================================
// MODAL: SUPERMARKT-BEREICHE VERWALTUNG
// ==========================================
function openCategoriesModal() {
renderManageCategoriesList();
openModal('modal-categories');
}
function renderManageCategoriesList() {
const ul = document.getElementById('cat-manage-list');
if (!ul) return;
ul.innerHTML = allCategories.map((cat, idx) => `
<li style="display: flex; align-items: center; justify-content: space-between; padding: 7px 12px; background: #FFFFFF; border: 1.5px solid rgba(34,34,34,0.15); border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 8px; font-weight: 600;">
<span style="font-size: 1.25rem;">${cat.icon || '🛒'}</span>
<span>${escapeHtml(cat.name)}</span>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<button type="button" class="cb-btn cb-btn-secondary" style="padding: 2px 7px; font-size: 0.85rem;" onclick="moveCategory(${cat.id}, -1)" ${idx === 0 ? 'disabled style="opacity:0.3;"' : ''}>▲</button>
<button type="button" class="cb-btn cb-btn-secondary" style="padding: 2px 7px; font-size: 0.85rem;" onclick="moveCategory(${cat.id}, 1)" ${idx === allCategories.length - 1 ? 'disabled style="opacity:0.3;"' : ''}>▼</button>
<button type="button" class="cb-btn cb-btn-danger" style="padding: 2px 7px; font-size: 0.85rem; background:#b02a37; border-color:#b02a37; color:#fff;" onclick="deleteCategory(${cat.id})">✕</button>
</div>
</li>
`).join('');
}
async function moveCategory(catId, dir) {
const idx = allCategories.findIndex(c => c.id === catId);
if (idx < 0) return;
const targetIdx = idx + dir;
if (targetIdx < 0 || targetIdx >= allCategories.length) return;
const [removed] = allCategories.splice(idx, 1);
allCategories.splice(targetIdx, 0, removed);
renderManageCategoriesList();
renderCategoryFilterChips();
renderShoppingList();
const orderedIds = allCategories.map(c => c.id);
try {
await fetch('/api/shopping-categories/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_ids: orderedIds })
});
} catch (e) {}
}
async function onAddCategory(e) {
e.preventDefault();
const iconInput = document.getElementById('new-cat-icon');
const nameInput = document.getElementById('new-cat-name');
const name = nameInput.value.trim();
const icon = iconInput.value.trim() || '🛒';
if (!name) return;
nameInput.value = '';
try {
const res = await fetch('/api/shopping-categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, icon: icon, sort_order: allCategories.length + 1 })
});
if (res.ok) {
const d = await res.json();
allCategories = d.categories || [];
localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories));
renderManageCategoriesList();
renderCategoryFilterChips();
showToast(`Bereich "${name}" angelegt`);
}
} catch (e) {}
}
async function deleteCategory(catId) {
if (!confirm('Diesen Supermarkt-Bereich wirklich entfernen?')) return;
try {
const res = await fetch(`/api/shopping-categories/${catId}`, { method: 'DELETE' });
if (res.ok) {
const d = await res.json();
allCategories = d.categories || [];
localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories));
renderManageCategoriesList();
renderCategoryFilterChips();
renderShoppingList();
showToast('Bereich entfernt');
}
} catch (e) {}
}
async function resetDefaultCategories() {
if (!confirm('Möchtest du wirklich alle Supermarkt-Bereiche auf den Standard-Laufweg zurücksetzen?')) return;
try {
const res = await fetch('/api/shopping-categories/reset-defaults', { method: 'POST' });
if (res.ok) {
const d = await res.json();
allCategories = d.categories || [];
localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories));
renderManageCategoriesList();
renderCategoryFilterChips();
renderShoppingList();
showToast('Standard-Bereiche wiederhergestellt');
}
} catch (e) {}
}
// ==========================================
// HELFER: MODAL OPEN / CLOSE & POPULATE
// ==========================================
function openModal(id) {
const m = document.getElementById(id);
if (m) {
m.classList.add('active');
document.body.classList.add('modal-open');
}
}
function closeModal(id) {
const m = document.getElementById(id);
if (m) {
m.classList.remove('active');
document.body.classList.remove('modal-open');
}
}
function onBackdropClick(e, id) {
if (e.target.id === id) {
closeModal(id);
}
}
function populateCategorySelect(selectId, selectedValue) {
const sel = document.getElementById(selectId);
if (!sel) return;
let html = selectId === 'add-item-category' ? '<option value="">🪄 Automatisch erkennen</option>' : '';
allCategories.forEach(cat => {
const isSel = (cat.name === selectedValue) ? 'selected' : '';
html += `<option value="${escapeAttr(cat.name)}" ${isSel}>${cat.icon || '🛒'} ${escapeHtml(cat.name)}</option>`;
});
if (!allCategories.some(c => c.name === 'Sonstiges')) {
const isSel = (selectedValue === 'Sonstiges') ? 'selected' : '';
html += `<option value="Sonstiges" ${isSel}>🛒 Sonstiges</option>`;
}
sel.innerHTML = html;
}
function showToast(msg) {
const toast = document.getElementById('shop-toast');
if (!toast) return;
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2200);
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function escapeAttr(str) {
if (!str) return '';
return String(str).replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}
// ==========================================
// SERVICE WORKER & INITIALISIERUNG
// ==========================================
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(err => console.log('SW fail:', err));
});
}
document.addEventListener('DOMContentLoaded', () => {
renderCategoryFilterChips();
renderShoppingList();
initWebSocket();
});
</script>
</body>
</html>