Files
ti/templates/index.html
Henrik Jess Nielsen 1a3476c4d3
Some checks failed
deploy / deploy (push) Has been cancelled
Add Flask shopping app with Nomad deployment
Flask app serving the Citti shopping list with SQLite persistence,
per-category item addition, dynamic custom categories, photo upload,
and a post-trip price analysis page.

- Port 9756, DATA_DIR env var for persistent volume in Nomad
- Gitea Actions pipeline builds Docker image and deploys via Nomad API
- Makefile targets: run, install, build, push

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 00:54:07 +02:00

605 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="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>