Files
ti/templates/index.html

605 lines
31 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Citti Indkøbsseddel — Uge 23-25</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #f5f2ed; --card: #ffffff; --border: #e2ddd6; --text: #1a1814;
--muted: #8a857c; --accent: #c84b2f; --accent-light: #f7ede9;
--green: #2d6a4f; --green-light: #e8f4ee;
--amber: #92580a; --amber-light: #fef3e2;
--blue: #1d4e89; --blue-light: #e8f0f9;
--mono: 'DM Mono', monospace; --sans: 'DM Sans', sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--sans); background: var(--bg); color: var(--text); font-size: 15px; line-height: 1.5; padding: 1.5rem 1rem 6rem; }
.page { max-width: 860px; margin: 0 auto; }
header { display: grid; grid-template-columns: 1fr auto; align-items: start; gap: 1rem; margin-bottom: 1.5rem; padding-bottom: 1.25rem; border-bottom: 2px solid var(--text); }
.header-left h1 { font-size: 1.75rem; font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; margin-bottom: 0.4rem; }
.header-left h1 span { color: var(--accent); }
.header-meta { display: flex; flex-wrap: wrap; gap: 0.4rem 1.25rem; font-size: 12px; color: var(--muted); }
.header-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-end; }
.btn { background: var(--text); color: white; border: none; border-radius: 8px; padding: 8px 14px; font-size: 13px; font-family: var(--sans); cursor: pointer; text-decoration: none; display: inline-block; font-weight: 500; white-space: nowrap; }
.btn-cat { background: var(--accent); }
.savings-card { background: var(--text); color: white; border-radius: 10px; padding: 0.875rem 1.1rem; text-align: right; }
.savings-card .label { font-size: 10px; opacity: 0.6; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 3px; }
.savings-card .amount { font-size: 1.75rem; font-weight: 600; font-family: var(--mono); letter-spacing: -0.02em; color: #7de8b0; }
.savings-card .sublabel { font-size: 10px; opacity: 0.5; }
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 1.5rem; }
.stat { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 0.75rem; }
.stat .s-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 3px; }
.stat .s-val { font-size: 1.35rem; font-weight: 600; font-family: var(--mono); letter-spacing: -0.02em; }
.stat .s-sub { font-size: 10px; color: var(--muted); margin-top: 1px; }
.section { margin-bottom: 1.25rem; }
.section-header { display: flex; align-items: center; gap: 8px; padding: 0.55rem 0.875rem; background: var(--text); color: white; border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; }
.section-header .emoji { font-size: 15px; }
.section-header .est { margin-left: auto; font-family: var(--mono); font-size: 11px; opacity: 0.6; }
.section-header .sec-add { background: rgba(255,255,255,0.18); border: none; color: white; width: 22px; height: 22px; border-radius: 50%; font-size: 15px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: background 0.15s; }
.section-header .sec-add:hover { background: rgba(255,255,255,0.32); }
.section-header .sec-del { background: none; border: none; color: rgba(255,255,255,0.4); font-size: 16px; line-height: 1; cursor: pointer; padding: 0 2px; flex-shrink: 0; transition: color 0.15s; }
.section-header .sec-del:hover { color: rgba(255,255,255,0.9); }
.section-header.custom-sec { background: var(--accent); }
.items-table { background: var(--card); border: 1px solid var(--border); border-top: none; border-radius: 0 0 8px 8px; overflow: hidden; }
.item-row { display: grid; grid-template-columns: 28px 1fr 56px 82px 86px; align-items: center; border-bottom: 1px solid var(--border); transition: background 0.15s; }
.item-row:last-child { border-bottom: none; }
.item-row:hover { background: #faf9f7; }
.item-row.checked { opacity: 0.4; }
.item-row.checked .item-name { text-decoration: line-through; }
.item-check { display: flex; align-items: center; justify-content: center; padding: 0.75rem 0 0.75rem 0.875rem; }
.item-check input[type=checkbox] { width: 17px; height: 17px; accent-color: var(--accent); cursor: pointer; }
.item-name { padding: 0.7rem 0.5rem; font-size: 14px; }
.item-name .note { font-size: 11px; color: var(--muted); display: block; margin-top: 1px; }
.badge { display: inline-block; font-size: 9px; padding: 1px 5px; border-radius: 3px; margin-left: 5px; vertical-align: middle; font-weight: 500; }
.badge-card { background: var(--blue-light); color: var(--blue); }
.badge-kasse { background: var(--amber-light); color: var(--amber); }
.item-qty { padding: 0.7rem 0.5rem; font-size: 11px; color: var(--muted); font-family: var(--mono); text-align: center; }
.item-est { padding: 0.7rem 0.5rem; font-size: 12px; font-family: var(--mono); color: var(--muted); text-align: right; }
.item-actual { padding: 0.45rem 0.75rem 0.45rem 0.5rem; }
.item-actual input { width: 100%; border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px; font-size: 13px; font-family: var(--mono); background: var(--bg); color: var(--text); text-align: right; outline: none; transition: border-color 0.15s, background 0.15s; }
.item-actual input:focus { border-color: var(--accent); background: white; }
.item-actual input.has-value { background: var(--green-light); border-color: #a8d5bc; color: var(--green); font-weight: 500; }
.col-headers { display: grid; grid-template-columns: 28px 1fr 56px 82px 86px; background: #f0ece6; border: 1px solid var(--border); border-top: none; padding: 3px 0; }
.col-h { font-size: 9px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.07em; font-weight: 500; }
.col-h:nth-child(1) { padding-left: 0.875rem; }
.col-h:nth-child(2) { padding-left: 0.5rem; }
.col-h:nth-child(3) { text-align: center; padding: 0 0.5rem; }
.col-h:nth-child(4) { text-align: right; padding: 0 0.5rem; }
.col-h:nth-child(5) { text-align: right; padding: 0 0.75rem 0 0.5rem; }
/* Extra rows within a section */
.extra-item-row { display: grid; grid-template-columns: 28px 1fr auto auto; align-items: center; border-top: 1px dashed var(--border); background: #fdfcfb; }
.extra-item-row .item-name { font-size: 13px; color: var(--text); }
.extra-item-row .eir-price { padding: 0.65rem 0.5rem; font-size: 13px; font-family: var(--mono); font-weight: 600; color: var(--accent); text-align: right; white-space: nowrap; }
.extra-item-row .eir-del { padding: 0.65rem 0.75rem 0.65rem 0; }
.extra-item-row .eir-del button { background: none; border: none; color: var(--muted); font-size: 16px; cursor: pointer; line-height: 1; padding: 2px 4px; }
.extra-item-row .eir-del button:hover { color: var(--accent); }
/* Empty section placeholder */
.empty-sec { padding: 0.875rem 1rem; font-size: 13px; color: var(--muted); font-style: italic; border-top: 1px dashed var(--border); background: #fdfcfb; }
/* Rema */
.rema-section { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.1rem; margin-bottom: 1.25rem; }
.rema-title { font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-bottom: 0.75rem; }
.rema-items { display: flex; flex-wrap: wrap; gap: 6px; }
.rema-item { display: flex; align-items: center; gap: 6px; font-size: 13px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 5px 10px; cursor: pointer; transition: background 0.15s; }
.rema-item:hover { background: #eae6e0; }
.rema-item.checked { opacity: 0.4; text-decoration: line-through; }
.rema-item input { width: 14px; height: 14px; accent-color: var(--accent); }
/* Notes */
.notes-section { background: var(--amber-light); border: 1px solid #f0d8a0; border-radius: 8px; padding: 0.875rem 1.1rem; margin-bottom: 1.25rem; }
.notes-title { font-size: 12px; font-weight: 500; color: var(--amber); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.6rem; }
.notes-list { display: flex; flex-direction: column; gap: 3px; }
.notes-list li { font-size: 13px; color: #6b4a0a; list-style: none; padding-left: 1.25rem; position: relative; }
.notes-list li::before { content: '→'; position: absolute; left: 0; color: var(--amber); }
/* Summary */
.summary-section { background: var(--text); color: white; border-radius: 12px; padding: 1.25rem; margin-bottom: 1.25rem; }
.summary-title { font-size: 12px; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1rem; }
.summary-grid { display: grid; grid-template-columns: 1fr 90px 90px; font-size: 13px; }
.sg-h { font-size: 10px; opacity: 0.4; text-transform: uppercase; letter-spacing: 0.06em; padding: 0 0 6px; }
.sg-h.num { text-align: right; }
.sg-label { padding: 4px 0; opacity: 0.75; border-top: 1px solid rgba(255,255,255,0.08); }
.sg-est { padding: 4px 0; text-align: right; font-family: var(--mono); opacity: 0.5; border-top: 1px solid rgba(255,255,255,0.08); }
.sg-actual { padding: 4px 0; text-align: right; font-family: var(--mono); color: #7de8b0; border-top: 1px solid rgba(255,255,255,0.08); }
.sg-total { padding: 8px 0 0; font-weight: 600; font-size: 14px; border-top: 2px solid rgba(255,255,255,0.2); margin-top: 3px; }
.sg-total-est { padding: 8px 0 0; text-align: right; font-family: var(--mono); font-weight: 600; font-size: 14px; border-top: 2px solid rgba(255,255,255,0.2); margin-top: 3px; }
.sg-total-actual { padding: 8px 0 0; text-align: right; font-family: var(--mono); font-weight: 600; font-size: 14px; color: #7de8b0; border-top: 2px solid rgba(255,255,255,0.2); margin-top: 3px; }
footer { text-align: center; font-size: 11px; color: var(--muted); padding-top: 1rem; border-top: 1px solid var(--border); }
/* FAB */
.fab { position: fixed; bottom: 1.5rem; right: 1.5rem; background: var(--accent); color: white; border: none; border-radius: 28px; padding: 0 20px; height: 52px; font-size: 14px; font-weight: 600; font-family: var(--sans); cursor: pointer; box-shadow: 0 4px 16px rgba(200,75,47,0.35); display: flex; align-items: center; gap: 8px; z-index: 100; transition: transform 0.15s; }
.fab:hover { transform: scale(1.04); }
.fab .fab-icon { font-size: 20px; font-weight: 300; }
/* Modals */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; align-items: flex-end; justify-content: center; }
.modal-overlay.open { display: flex; }
.modal { background: white; border-radius: 16px 16px 0 0; padding: 1.5rem 1.25rem 2rem; width: 100%; max-width: 560px; max-height: 90vh; overflow-y: auto; }
.modal-title { font-size: 18px; font-weight: 600; margin-bottom: 1.25rem; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--muted); padding: 0; line-height: 1; flex-shrink: 0; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-bottom: 5px; }
.form-group input, .form-group textarea { width: 100%; border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; font-size: 15px; font-family: var(--sans); background: var(--bg); color: var(--text); outline: none; transition: border-color 0.15s; }
.form-group input:focus, .form-group textarea:focus { border-color: var(--accent); background: white; }
.form-group .emoji-input { font-size: 22px; width: 60px; text-align: center; padding: 8px; }
.photo-preview { margin-top: 8px; max-width: 100px; max-height: 100px; border-radius: 8px; display: none; border: 1px solid var(--border); }
.btn-submit { width: 100%; background: var(--accent); color: white; border: none; border-radius: 10px; padding: 14px; font-size: 16px; font-weight: 600; font-family: var(--sans); cursor: pointer; margin-top: 0.5rem; }
.btn-submit:active { opacity: 0.85; }
.modal-context { background: var(--accent-light); border-radius: 8px; padding: 8px 12px; font-size: 13px; color: var(--accent); font-weight: 500; margin-bottom: 1rem; }
.saving-indicator { position: fixed; top: 0.75rem; right: 0.75rem; background: var(--green); color: white; border-radius: 6px; padding: 4px 10px; font-size: 12px; font-family: var(--mono); opacity: 0; transition: opacity 0.3s; z-index: 50; pointer-events: none; }
.saving-indicator.show { opacity: 1; }
@media (max-width: 600px) {
.stats-row { grid-template-columns: repeat(2, 1fr); }
header { grid-template-columns: 1fr; }
.header-actions { flex-direction: row; align-items: center; }
.item-row { grid-template-columns: 28px 1fr 50px 78px; }
.item-est { display: none; }
.col-headers { grid-template-columns: 28px 1fr 50px 78px; }
.col-h:nth-child(4) { display: none; }
.extra-item-row { grid-template-columns: 28px 1fr auto auto; }
}
</style>
</head>
<body>
<div class="page">
<div class="saving-indicator" id="save-indicator">Gemt ✓</div>
<header>
<div class="header-left">
<h1>Citti <span>Indkøbsseddel</span></h1>
<div class="header-meta">
<span><strong>Uge 2325</strong> · 03.0623.06.2026</span>
<span>Manlør 0820</span>
<span>Langberger Weg 4, Flensburg</span>
<span>1 EUR ≈ 7,46 DKK</span>
</div>
</div>
<div class="header-actions">
<div style="display:flex;gap:8px">
<button class="btn btn-cat" onclick="openCatModal()">+ Kategori</button>
<a href="/analyse" class="btn">📊 Analyse</a>
</div>
<div class="savings-card">
<div class="label">Estimeret besparelse</div>
<div class="amount" id="total-savings">~1.800</div>
<div class="sublabel">kr. vs. dansk pris</div>
</div>
</div>
</header>
<div class="stats-row">
<div class="stat">
<div class="s-label">Aftensmadsdage</div>
<div class="s-val">~40</div>
<div class="s-sub">fra én tur</div>
</div>
<div class="stat">
<div class="s-label">Kødportioner</div>
<div class="s-val">24</div>
<div class="s-sub">à 500g i fryseren</div>
</div>
<div class="stat">
<div class="s-label">Estimeret total</div>
<div class="s-val" style="font-size:1.15rem">~4.400</div>
<div class="s-sub">kr. inkl. Rema</div>
</div>
<div class="stat">
<div class="s-label">Faktisk betalt</div>
<div class="s-val" style="font-size:1.15rem" id="stat-actual"></div>
<div class="s-sub">kr. hos Citti</div>
</div>
</div>
<!-- FASTE SEKTIONER -->
{% for sec in sections %}
<div class="section">
<div class="section-header">
<span class="emoji">{{ sec.emoji }}</span> {{ sec.title }}
<span class="est">est. ~{{ "{:,.0f}".format(sec.est).replace(",",".") }} kr.</span>
<button class="sec-add" onclick="openItemModal('{{ sec.id }}', '{{ sec.emoji }} {{ sec.title }}')" title="Tilføj vare">+</button>
</div>
<div class="col-headers">
<div class="col-h"></div>
<div class="col-h">Vare</div>
<div class="col-h">Antal</div>
<div class="col-h">Estimat</div>
<div class="col-h">Faktisk</div>
</div>
<div class="items-table" id="sec-{{ sec.id }}">
{% for item in sec.varer %}
{% set s = saved.get(item.key, {}) %}
<div class="item-row{% if s.get('checked') %} checked{% endif %}" data-key="{{ item.key }}" data-est="{{ item.est }}">
<div class="item-check"><input type="checkbox" {% if s.get('checked') %}checked{% endif %} onchange="toggleRow(this)"></div>
<div class="item-name">
{{ item.name }}
{% if item.get('badges') %}{% for b in item.badges %}{% if b == 'card' %}<span class="badge badge-card">CITTI CARD</span>{% endif %}{% if b == 'kasse' %}<span class="badge badge-kasse">Hel kasse</span>{% endif %}{% endfor %}{% endif %}
{% if item.get('note') %}<span class="note">{{ item.note }}</span>{% endif %}
</div>
<div class="item-qty">{{ item.qty }}</div>
<div class="item-est">{{ "{:.2f}".format(item.est).replace(".",",") if item.est else "—" }}</div>
<div class="item-actual">
<input type="number" placeholder="—" value="{{ s.get('price') or '' }}" oninput="updateActual(this)" step="0.01" min="0">
</div>
</div>
{% endfor %}
{% for e in by_sec.get(sec.id, []) %}
<div class="extra-item-row" id="extra-{{ e.id }}">
<div></div>
<div class="item-name">
{{ e.name }}
{% if e.note %}<span class="note">{{ e.note }}</span>{% endif %}
</div>
<div class="eir-price">{% if e.price %}{{ "{:.2f}".format(e.price).replace(".",",") }}{% else %}—{% endif %}</div>
<div class="eir-del"><button onclick="deleteExtra({{ e.id }}, this)" title="Slet">×</button></div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<!-- DYNAMISKE SEKTIONER -->
<div id="custom-sections">
{% for cs in custom_secs %}
{% set sk = "custom-" + cs.id|string %}
<div class="section" id="csec-{{ cs.id }}">
<div class="section-header custom-sec">
<span class="emoji">{{ cs.emoji }}</span> {{ cs.title }}
<span class="est">{{ by_sec.get(sk, [])|length }} vare{{ 'r' if by_sec.get(sk, [])|length != 1 else '' }}</span>
<button class="sec-add" onclick="openItemModal('{{ sk }}', '{{ cs.emoji }} {{ cs.title }}')" title="Tilføj vare">+</button>
<button class="sec-del" onclick="deleteCategory({{ cs.id }})" title="Slet kategori">×</button>
</div>
<div class="items-table" id="sec-{{ sk }}">
{% set sk_extras = by_sec.get(sk, []) %}
{% if sk_extras %}
{% for e in sk_extras %}
<div class="extra-item-row" id="extra-{{ e.id }}">
<div></div>
<div class="item-name">{{ e.name }}{% if e.note %}<span class="note">{{ e.note }}</span>{% endif %}</div>
<div class="eir-price">{% if e.price %}{{ "{:.2f}".format(e.price).replace(".",",") }}{% else %}—{% endif %}</div>
<div class="eir-del"><button onclick="deleteExtra({{ e.id }}, this)" title="Slet">×</button></div>
</div>
{% endfor %}
{% else %}
<div class="empty-sec">Ingen varer endnu — tryk + for at tilføje</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- REMA -->
<div class="rema-section">
<div class="rema-title">🛒 Køb i Rema1000 / Netto</div>
<div class="rema-items">
{% for item in ["Frisk frugt & grøntsager","Løg & hvidløg","Kartofler","Mælk, æg, smør","Brød & pålæg","Mel, gær, bagepulver","Vaniljesukker"] %}
<label class="rema-item"><input type="checkbox" onchange="this.closest('.rema-item').classList.toggle('checked')"> {{ item }}</label>
{% endfor %}
</div>
</div>
<!-- EKSTRA VARER (ingen kategori) -->
{% set loose = by_sec.get('', []) %}
{% if loose %}
<div class="section" id="sec-loose">
<div class="section-header" style="background:#5a5550">
<span class="emoji">📦</span> Diverse — uden kategori
<span class="est">{{ loose|length }} vare{{ 'r' if loose|length != 1 else '' }}</span>
</div>
<div class="items-table" id="sec-table-loose">
{% for e in loose %}
<div class="extra-item-row" id="extra-{{ e.id }}">
<div></div>
<div class="item-name">{{ e.name }}{% if e.note %}<span class="note">{{ e.note }}</span>{% endif %}</div>
<div class="eir-price">{% if e.price %}{{ "{:.2f}".format(e.price).replace(".",",") }}{% else %}—{% endif %}</div>
<div class="eir-del"><button onclick="deleteExtra({{ e.id }}, this)" title="Slet">×</button></div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- NOTER -->
<div class="notes-section">
<div class="notes-title">⚠ Noter</div>
<ul class="notes-list">
<li>Varer med "Hel kasse" sælges kun i hele kasser</li>
<li>CITTI CARD er gratis — hent ved kassen</li>
<li>Hakket oksekød: spørg i køddisken</li>
<li>Karrypasta: tjek den asiatiske afdeling</li>
<li>Husk fryseposer + marker hjemmefra — 500g portioner</li>
</ul>
</div>
<!-- SAMMENFATNING -->
<div class="summary-section">
<div class="summary-title">Prissammenfatning</div>
<div class="summary-grid">
<div class="sg-h">Kategori</div><div class="sg-h num">Estimat</div><div class="sg-h num">Faktisk</div>
{% set sec_ids = ['koed','frost','mejeri','pasta','groent','konserves','indisk','mex','drikke','hus'] %}
{% set sec_labels = ['Kød & fisk','Frostgrønt','Mejeri & frost','Pasta (frisk)','Grønt & tørvarer','Konserves','Indisk & karry','Mexicansk & asiatisk','Drikkevarer','Husholdning'] %}
{% set sec_ests = [1580,100,185,160,245,185,160,185,530,535] %}
{% for i in range(sec_ids|length) %}
<div class="sg-label">{{ sec_labels[i] }}</div>
<div class="sg-est">~{{ sec_ests[i] }}</div>
<div class="sg-actual" id="s-{{ sec_ids[i] }}"></div>
{% endfor %}
<div class="sg-total">Total Citti</div><div class="sg-total-est">~4.065</div><div class="sg-total-actual" id="s-total-citti"></div>
<div class="sg-label" style="opacity:0.5">Rema1000 supplement</div><div class="sg-est">~350</div><div class="sg-actual"></div>
<div class="sg-total">Samlet total</div><div class="sg-total-est">~4.400</div><div class="sg-total-actual" id="s-total-all"></div>
</div>
</div>
<footer>Citti Flensborg · Langberger Weg 4 · Tilbud 03.0623.06.2026</footer>
</div><!-- /page -->
<!-- FAB: tilføj item uden kategori -->
<button class="fab" onclick="openItemModal('', 'Tilføj vare')" title="Tilføj vare">
<span class="fab-icon">+</span> Tilføj vare
</button>
<!-- ITEM MODAL -->
<div class="modal-overlay" id="item-modal" onclick="if(event.target===this)closeItemModal()">
<div class="modal">
<div class="modal-title">
<span id="item-modal-title">Tilføj vare</span>
<button class="modal-close" onclick="closeItemModal()">×</button>
</div>
<div class="modal-context" id="item-modal-ctx" style="display:none"></div>
<form id="add-form" onsubmit="submitItem(event)">
<input type="hidden" name="section_key" id="item-section-key">
<div class="form-group">
<label>Varenavn *</label>
<input type="text" name="name" placeholder="fx. Parmigiano Reggiano 1kg" required autocomplete="off">
</div>
<div class="form-group">
<label>Pris (DKK)</label>
<input type="text" name="price" placeholder="fx. 89,99" inputmode="decimal">
</div>
<div class="form-group">
<label>Note</label>
<input type="text" name="note" placeholder="fx. 'billigere end DK'" autocomplete="off">
</div>
<div class="form-group">
<label>Foto</label>
<input type="file" name="photo" accept="image/*" capture="environment" onchange="previewPhoto(this)">
<img id="photo-preview" class="photo-preview" alt="preview">
</div>
<button type="submit" class="btn-submit">Tilføj vare</button>
</form>
</div>
</div>
<!-- KATEGORI MODAL -->
<div class="modal-overlay" id="cat-modal" onclick="if(event.target===this)closeCatModal()">
<div class="modal">
<div class="modal-title">
Tilføj kategori
<button class="modal-close" onclick="closeCatModal()">×</button>
</div>
<form id="cat-form" onsubmit="submitCategory(event)">
<div class="form-group" style="display:flex;gap:12px;align-items:flex-end">
<div style="flex-shrink:0">
<label>Emoji</label>
<input class="emoji-input" type="text" name="emoji" value="📦" maxlength="2">
</div>
<div style="flex:1">
<label>Kategorinavn *</label>
<input type="text" name="title" placeholder="fx. Ost & delikatesser" required autocomplete="off">
</div>
</div>
<button type="submit" class="btn-submit">Opret kategori</button>
</form>
</div>
</div>
<script>
const SEC_IDS = ['koed','frost','mejeri','pasta','groent','konserves','indisk','mex','drikke','hus'];
const DK_SAVINGS_RATIO = 0.35;
let saveTimer = null;
function showSaved() {
const el = document.getElementById('save-indicator');
el.classList.add('show');
clearTimeout(saveTimer);
saveTimer = setTimeout(() => el.classList.remove('show'), 1500);
}
function toggleRow(cb) {
const row = cb.closest('.item-row');
row.classList.toggle('checked', cb.checked);
const price = parseFloat(row.querySelector('.item-actual input').value) || null;
saveToServer(row.dataset.key, price, cb.checked ? 1 : 0);
}
let debounceTimers = {};
function updateActual(input) {
const val = parseFloat(input.value);
input.classList.toggle('has-value', !isNaN(val) && val > 0);
const row = input.closest('.item-row');
const key = row.dataset.key;
const checked = row.querySelector('input[type=checkbox]').checked ? 1 : 0;
clearTimeout(debounceTimers[key]);
debounceTimers[key] = setTimeout(() => saveToServer(key, isNaN(val) ? null : val, checked), 600);
recalcAll();
}
function saveToServer(key, price, checked) {
fetch('/api/price', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({key, price, checked})
}).then(() => showSaved());
}
function recalcAll() {
const sections = document.querySelectorAll('.items-table');
let totalActual = 0; let hasAny = false;
sections.forEach((sec, i) => {
const inputs = sec.querySelectorAll('.item-row .item-actual input');
let s = 0; let has = false;
inputs.forEach(inp => { const v = parseFloat(inp.value); if (!isNaN(v) && v > 0) { s += v; has = true; }});
const el = document.getElementById('s-' + SEC_IDS[i]);
if (el) el.textContent = has ? Math.round(s).toLocaleString('da-DK') : '—';
if (has) { totalActual += s; hasAny = true; }
});
if (hasAny) {
document.getElementById('s-total-citti').textContent = Math.round(totalActual).toLocaleString('da-DK');
document.getElementById('s-total-all').textContent = Math.round(totalActual + 350).toLocaleString('da-DK');
document.getElementById('stat-actual').textContent = Math.round(totalActual).toLocaleString('da-DK');
document.getElementById('total-savings').textContent = Math.round(totalActual * DK_SAVINGS_RATIO).toLocaleString('da-DK');
} else {
['s-total-citti','s-total-all','stat-actual'].forEach(id => document.getElementById(id).textContent = '—');
document.getElementById('total-savings').textContent = '~1.800';
}
}
// Item modal
let activeSectionKey = '';
function openItemModal(sectionKey, sectionTitle) {
activeSectionKey = sectionKey || '';
document.getElementById('item-section-key').value = activeSectionKey;
document.getElementById('item-modal-title').textContent = sectionKey ? `Tilføj til ${sectionTitle}` : 'Tilføj vare';
const ctx = document.getElementById('item-modal-ctx');
if (sectionKey) {
ctx.textContent = sectionTitle;
ctx.style.display = '';
} else {
ctx.style.display = 'none';
}
document.getElementById('item-modal').classList.add('open');
setTimeout(() => document.querySelector('#add-form input[name=name]').focus(), 100);
}
function closeItemModal() {
document.getElementById('item-modal').classList.remove('open');
document.getElementById('add-form').reset();
document.getElementById('photo-preview').style.display = 'none';
}
function previewPhoto(input) {
const p = document.getElementById('photo-preview');
if (input.files && input.files[0]) { p.src = URL.createObjectURL(input.files[0]); p.style.display = 'block'; }
}
async function submitItem(e) {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('button[type=submit]');
btn.textContent = 'Gemmer...'; btn.disabled = true;
const fd = new FormData(form);
const res = await fetch('/api/item', {method: 'POST', body: fd});
const data = await res.json();
if (data.ok) {
appendExtraToSection(data);
closeItemModal();
}
btn.textContent = 'Tilføj vare'; btn.disabled = false;
}
function appendExtraToSection(data) {
const sk = data.section_key || '';
const tableId = sk ? `sec-${sk}` : 'sec-table-loose';
let table = document.getElementById(tableId);
if (!table) {
if (sk) return; // section not rendered yet (shouldn't happen)
// create loose section
const sec = document.createElement('div');
sec.className = 'section'; sec.id = 'sec-loose';
sec.innerHTML = `<div class="section-header" style="background:#5a5550"><span class="emoji">📦</span> Diverse — uden kategori<span class="est">0 varer</span></div><div class="items-table" id="sec-table-loose"></div>`;
document.querySelector('.notes-section').before(sec);
table = document.getElementById('sec-table-loose');
}
// remove empty placeholder
const ph = table.querySelector('.empty-sec');
if (ph) ph.remove();
const priceStr = data.price ? data.price.toFixed(2).replace('.', ',') : '—';
const noteHtml = data.note ? `<span class="note">${data.note}</span>` : '';
const row = document.createElement('div');
row.className = 'extra-item-row'; row.id = `extra-${data.id}`;
row.innerHTML = `<div></div><div class="item-name">${data.name}${noteHtml}</div><div class="eir-price">${priceStr}</div><div class="eir-del"><button onclick="deleteExtra(${data.id}, this)" title="Slet">×</button></div>`;
table.appendChild(row);
}
async function deleteExtra(id, btn) {
if (!confirm('Slet denne vare?')) return;
await fetch(`/api/item/${id}`, {method: 'DELETE'});
document.getElementById(`extra-${id}`).remove();
}
// Category modal
function openCatModal() {
document.getElementById('cat-modal').classList.add('open');
setTimeout(() => document.querySelector('#cat-form input[name=title]').focus(), 100);
}
function closeCatModal() {
document.getElementById('cat-modal').classList.remove('open');
document.getElementById('cat-form').reset();
document.querySelector('#cat-form input[name=emoji]').value = '📦';
}
async function submitCategory(e) {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('button[type=submit]');
btn.textContent = 'Opretter...'; btn.disabled = true;
const emoji = form.emoji.value.trim() || '📦';
const title = form.title.value.trim();
const res = await fetch('/api/category', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({emoji, title})
});
const data = await res.json();
if (data.ok) {
appendCustomSection(data);
closeCatModal();
}
btn.textContent = 'Opret kategori'; btn.disabled = false;
}
function appendCustomSection(data) {
const container = document.getElementById('custom-sections');
const sec = document.createElement('div');
sec.className = 'section'; sec.id = `csec-${data.id}`;
sec.innerHTML = `
<div class="section-header custom-sec">
<span class="emoji">${data.emoji}</span> ${data.title}
<span class="est">0 varer</span>
<button class="sec-add" onclick="openItemModal('${data.key}', '${data.emoji} ${data.title}')" title="Tilføj vare">+</button>
<button class="sec-del" onclick="deleteCategory(${data.id})" title="Slet kategori">×</button>
</div>
<div class="items-table" id="sec-${data.key}">
<div class="empty-sec">Ingen varer endnu — tryk + for at tilføje</div>
</div>`;
container.appendChild(sec);
}
async function deleteCategory(id) {
if (!confirm('Slet kategori og alle dens varer?')) return;
await fetch(`/api/category/${id}`, {method: 'DELETE'});
document.getElementById(`csec-${id}`).remove();
}
recalcAll();
</script>
</body>
</html>