Files
ti/templates/analyse.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

285 lines
12 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">
<title>Citti Analyse — 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; --green: #2d6a4f; --green-light: #e8f4ee;
--amber: #92580a; --amber-light: #fef3e2; --blue: #1d4e89; --blue-light: #e8f0f9;
--red-light: #fde8e3;
--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 4rem; }
.page { max-width: 860px; margin: 0 auto; }
header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; padding-bottom: 1.25rem; border-bottom: 2px solid var(--text); }
h1 { font-size: 1.75rem; font-weight: 600; letter-spacing: -0.03em; }
h1 span { color: var(--accent); }
.btn-back { 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; }
.kpi-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 1.5rem; }
.kpi { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1rem; }
.kpi-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px; }
.kpi-val { font-size: 1.75rem; font-weight: 600; font-family: var(--mono); letter-spacing: -0.02em; }
.kpi-val.green { color: var(--green); }
.kpi-val.red { color: var(--accent); }
.kpi-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
/* Category table */
.card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 1.5rem; }
.card-title { background: var(--text); color: white; padding: 0.6rem 1rem; font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { padding: 6px 12px; font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 500; text-align: left; background: #f0ece6; border-bottom: 1px solid var(--border); }
th.num { text-align: right; }
td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
td.num { text-align: right; font-family: var(--mono); }
tr:last-child td { border-bottom: none; }
tr.total-row td { font-weight: 600; background: #faf9f7; border-top: 2px solid var(--border); }
.diff-pos { color: var(--accent); font-weight: 500; }
.diff-neg { color: var(--green); font-weight: 500; }
.no-data { color: var(--muted); font-style: italic; font-size: 12px; }
/* Bar chart */
.bar-wrap { padding: 1rem; }
.bar-row { display: grid; grid-template-columns: 140px 1fr 70px; align-items: center; gap: 10px; margin-bottom: 8px; }
.bar-label { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-track { background: #e8f4ee; border-radius: 4px; height: 10px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; background: var(--green); transition: width 0.6s ease; }
.bar-fill.over { background: var(--accent); }
.bar-val { font-size: 11px; font-family: var(--mono); color: var(--muted); text-align: right; }
/* Item details */
.items-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; padding: 1rem; }
.item-card { border: 1px solid var(--border); border-radius: 8px; padding: 0.75rem; background: var(--bg); }
.item-card-name { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
.item-card-prices { display: flex; gap: 12px; font-size: 12px; }
.item-card-est { color: var(--muted); font-family: var(--mono); }
.item-card-actual { color: var(--green); font-family: var(--mono); font-weight: 600; }
.item-card-diff { font-size: 11px; margin-top: 2px; }
/* Extra items grid */
.extras-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; padding: 1rem; }
.extra-card { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: white; }
.extra-card img { width: 100%; height: 120px; object-fit: cover; display: block; }
.extra-card-no-photo { width: 100%; height: 120px; background: var(--bg); display: flex; align-items: center; justify-content: center; font-size: 40px; }
.extra-card-body { padding: 0.6rem 0.75rem; }
.extra-card-name { font-size: 13px; font-weight: 500; }
.extra-card-price { font-size: 16px; font-weight: 600; font-family: var(--mono); color: var(--accent); margin-top: 2px; }
.extra-card-meta { font-size: 11px; color: var(--muted); margin-top: 2px; }
.empty-state { padding: 2rem; text-align: center; color: var(--muted); font-size: 14px; }
@media (max-width: 600px) {
.kpi-row { grid-template-columns: repeat(2, 1fr); }
.bar-row { grid-template-columns: 90px 1fr 60px; }
}
</style>
</head>
<body>
<div class="page">
<header>
<div>
<h1>Citti <span>Analyse</span></h1>
<div style="font-size:12px;color:var(--muted);margin-top:4px">Uge 2325 · 03.0623.06.2026</div>
</div>
<a href="/" class="btn-back">← Indkøbsseddel</a>
</header>
{% set total_actual = total_actual or 0 %}
{% set total_est = total_est or 4065 %}
{% set diff = total_actual - total_est if total_actual else 0 %}
{% set pct = ((diff / total_est) * 100) if total_est else 0 %}
<div class="kpi-row">
<div class="kpi">
<div class="kpi-label">Faktisk betalt (Citti)</div>
{% if total_actual %}
<div class="kpi-val">{{ "{:,.0f}".format(total_actual).replace(",",".") }}</div>
<div class="kpi-sub">kr. registreret</div>
{% else %}
<div class="kpi-val" style="color:var(--muted)"></div>
<div class="kpi-sub">ingen priser indtastet endnu</div>
{% endif %}
</div>
<div class="kpi">
<div class="kpi-label">Estimeret</div>
<div class="kpi-val">{{ "{:,.0f}".format(total_est).replace(",",".") }}</div>
<div class="kpi-sub">kr. planlagt</div>
</div>
<div class="kpi">
<div class="kpi-label">Difference</div>
{% if total_actual %}
<div class="kpi-val {% if diff > 0 %}red{% else %}green{% endif %}">
{% if diff > 0 %}+{% endif %}{{ "{:,.0f}".format(diff).replace(",",".") }}
</div>
<div class="kpi-sub">kr. ({{ "{:+.1f}".format(pct) }}%)</div>
{% else %}
<div class="kpi-val" style="color:var(--muted)"></div>
<div class="kpi-sub">-</div>
{% endif %}
</div>
</div>
<!-- KATEGORI OVERSIGT -->
<div class="card">
<div class="card-title">Kategorioversigt</div>
<table>
<thead>
<tr>
<th>Kategori</th>
<th class="num">Estimat</th>
<th class="num">Faktisk</th>
<th class="num">Difference</th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr>
<td>{{ r.emoji }} {{ r.title }}</td>
<td class="num">{{ "{:,.0f}".format(r.est).replace(",",".") }}</td>
<td class="num">{% if r.actual %}{{ "{:,.0f}".format(r.actual).replace(",",".") }}{% else %}<span class="no-data"></span>{% endif %}</td>
<td class="num">
{% if r.diff is not none %}
<span class="{{ 'diff-pos' if r.diff > 0 else 'diff-neg' }}">
{% if r.diff > 0 %}+{% endif %}{{ "{:,.0f}".format(r.diff).replace(",",".") }}
</span>
{% else %}
<span class="no-data"></span>
{% endif %}
</td>
</tr>
{% endfor %}
<tr class="total-row">
<td>Total Citti</td>
<td class="num">{{ "{:,.0f}".format(total_est).replace(",",".") }}</td>
<td class="num">{% if total_actual %}{{ "{:,.0f}".format(total_actual).replace(",",".") }}{% else %}—{% endif %}</td>
<td class="num">
{% if total_actual %}
<span class="{{ 'diff-pos' if diff > 0 else 'diff-neg' }}">
{% if diff > 0 %}+{% endif %}{{ "{:,.0f}".format(diff).replace(",",".") }}
</span>
{% else %}—{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
<!-- STABDIAGRAM -->
{% if total_actual %}
<div class="card">
<div class="card-title">Faktisk vs. estimat per kategori</div>
<div class="bar-wrap">
{% for r in rows %}
{% if r.actual %}
{% set pct_bar = [((r.actual / r.est) * 100), 100]|min if r.est else 0 %}
<div class="bar-row">
<div class="bar-label" title="{{ r.title }}">{{ r.emoji }} {{ r.title.split('·')[0].strip() }}</div>
<div class="bar-track">
<div class="bar-fill {% if r.actual > r.est %}over{% endif %}" style="width:{{ pct_bar }}%"></div>
</div>
<div class="bar-val">{{ "{:,.0f}".format(r.actual).replace(",",".") }} / {{ "{:,.0f}".format(r.est).replace(",",".") }}</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
<!-- VARE-DETALJER -->
<div class="card">
<div class="card-title">Alle varer — est. vs. faktisk</div>
{% set has_items = saved|length > 0 %}
{% if has_items %}
<div class="items-grid">
{% for sec in sections %}
{% for item in sec.varer %}
{% set s = saved.get(item.key, {}) %}
{% if s.get('price') %}
{% set diff_item = s.price - item.est %}
<div class="item-card">
<div class="item-card-name">{{ item.name }}</div>
<div class="item-card-prices">
<span class="item-card-est">est. {{ "{:.0f}".format(item.est) }}</span>
<span class="item-card-actual">→ {{ "{:.0f}".format(s.price) }} kr.</span>
</div>
<div class="item-card-diff {% if diff_item > 2 %}diff-pos{% elif diff_item < -2 %}diff-neg{% endif %}">
{% if diff_item > 0.5 %}+{{ "{:.0f}".format(diff_item) }} kr. dyrere{% elif diff_item < -0.5 %}{{ "{:.0f}".format(diff_item) }} kr. billigere{% else %}som estimat{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
{% else %}
<div class="empty-state">Ingen priser registreret endnu.</div>
{% endif %}
</div>
<!-- EKSTRA VARER -->
<div class="card">
<div class="card-title">Ekstra varer tilføjet i butikken ({{ extras|length }})</div>
{% if extras %}
<div class="extras-grid">
{% for e in extras %}
<div class="extra-card">
{% if e.photo %}
<img src="/uploads/{{ e.photo }}" alt="{{ e.name }}">
{% else %}
<div class="extra-card-no-photo">📦</div>
{% endif %}
<div class="extra-card-body">
<div class="extra-card-name">{{ e.name }}</div>
{% if e.price %}<div class="extra-card-price">{{ "{:.2f}".format(e.price).replace(".",",") }} kr.</div>{% endif %}
<div class="extra-card-meta">
{% if e.category %}{{ e.category }}{% endif %}
{% if e.note %} · {{ e.note }}{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">Ingen ekstra varer tilføjet.</div>
{% endif %}
</div>
{% if extras %}
<div class="card">
<div class="card-title">Ekstra varer — prissammenfatning</div>
<table>
<thead>
<tr><th>Vare</th><th>Kategori</th><th class="num">Pris</th><th>Note</th></tr>
</thead>
<tbody>
{% set extra_total = namespace(v=0) %}
{% for e in extras %}
<tr>
<td>{{ e.name }}</td>
<td style="color:var(--muted);font-size:12px">{{ e.category or '—' }}</td>
<td class="num">{% if e.price %}{{ "{:.2f}".format(e.price).replace(".",",") }}{% else %}—{% endif %}</td>
<td style="color:var(--muted);font-size:12px">{{ e.note or '' }}</td>
</tr>
{% if e.price %}{% set extra_total.v = extra_total.v + e.price %}{% endif %}
{% endfor %}
{% if extra_total.v > 0 %}
<tr class="total-row">
<td colspan="2">Total ekstra</td>
<td class="num">{{ "{:.2f}".format(extra_total.v).replace(".",",") }}</td>
<td></td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% endif %}
</div>
</body>
</html>