1940 lines
64 KiB
HTML
1940 lines
64 KiB
HTML
<!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')">×</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')">×</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')">×</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')">×</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')">×</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')">×</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">
|
||
×
|
||
</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;">×</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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// 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>
|