2026-06-06 00:54:07 +02:00
<!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; }
2026-06-06 01:18:04 +02:00
.item-row { display: grid; grid-template-columns: 28px 1fr 72px 82px 86px; align-items: center; border-bottom: 1px solid var(--border); transition: background 0.15s; }
2026-06-06 00:54:07 +02:00
.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; }
2026-06-06 01:33:25 +02:00
.item-name .de { font-size: 10px; color: #b0aba3; display: block; margin-top: 0; font-style: italic; }
2026-06-06 00:54:07 +02:00
.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); }
2026-06-06 01:18:04 +02:00
.item-qty { padding: 0.35rem 0.4rem; }
2026-06-06 01:25:05 +02:00
.item-qty input { width: 100%; border: 1px solid var(--border); border-radius: 5px; padding: 6px 5px; font-size: 11px; font-family: var(--mono); background: var(--bg); color: var(--muted); text-align: center; outline: none; transition: border-color 0.15s, background 0.15s; cursor: text; min-height: 32px; }
2026-06-06 01:18:04 +02:00
.item-qty input:focus { border-color: var(--accent); background: white; color: var(--text); }
2026-06-06 01:25:05 +02:00
.item-qty input.modified { color: var(--accent); font-weight: 500; border-color: #f0b8ab; background: var(--accent-light); }
2026-06-06 00:54:07 +02:00
.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; }
2026-06-06 01:18:04 +02:00
.col-headers { display: grid; grid-template-columns: 28px 1fr 72px 82px 86px; background: #f0ece6; border: 1px solid var(--border); border-top: none; padding: 3px 0; }
2026-06-06 00:54:07 +02:00
.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; }
2026-06-06 01:18:04 +02:00
.item-row { grid-template-columns: 28px 1fr 64px 78px; }
2026-06-06 00:54:07 +02:00
.item-est { display: none; }
2026-06-06 01:18:04 +02:00
.col-headers { grid-template-columns: 28px 1fr 64px 78px; }
2026-06-06 00:54:07 +02:00
.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 23– 25< / strong > · 03.06– 23.06.2026< / span >
< span > Man– lør 08– 20< / 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 %}
2026-06-06 01:33:25 +02:00
{% if item.get('de') %}< span class = "de" > 🇩🇪 {{ item.de }}< / span > {% endif %}
2026-06-06 00:54:07 +02:00
{% if item.get('note') %}< span class = "note" > {{ item.note }}< / span > {% endif %}
< / div >
2026-06-06 01:18:04 +02:00
< div class = "item-qty" >
< input type = "text" value = "{{ s.get('qty') or item.qty }}" placeholder = "{{ item.qty }}"
oninput="updateQty(this)" onchange="updateQty(this)"
{% if s.get('qty') and s.get('qty') != item.qty %}class="modified"{% endif %}>
< / div >
2026-06-06 00:54:07 +02:00
< 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.06– 23.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) {
2026-06-06 01:18:04 +02:00
const row = document.querySelector(`.item-row[data-key="${key}"]`);
const qtyInput = row ? row.querySelector('.item-qty input') : null;
const qty = qtyInput ? qtyInput.value.trim() || null : null;
2026-06-06 00:54:07 +02:00
fetch('/api/price', {
method: 'POST',
headers: {'Content-Type':'application/json'},
2026-06-06 01:18:04 +02:00
body: JSON.stringify({key, price, checked, qty})
2026-06-06 00:54:07 +02:00
}).then(() => showSaved());
}
2026-06-06 01:18:04 +02:00
function updateQty(input) {
const row = input.closest('.item-row');
const key = row.dataset.key;
const defaultQty = input.placeholder;
input.classList.toggle('modified', input.value.trim() !== '' & & input.value.trim() !== defaultQty);
const price = parseFloat(row.querySelector('.item-actual input').value);
const checked = row.querySelector('input[type=checkbox]').checked ? 1 : 0;
clearTimeout(debounceTimers[key + '_qty']);
debounceTimers[key + '_qty'] = setTimeout(() => saveToServer(key, isNaN(price) ? null : price, checked), 600);
}
2026-06-06 00:54:07 +02:00
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 >