feat(admin): GET /admin/belege/{belegnummer}/html - HTML-Detailansicht

Rendert dieselben Daten wie der JSON-Detail-Endpunkt als schoen kategorisierte
HTML-Seite (askama, compile-time-Template). Der Handler destilliert das
DeliveryDetails-Aggregat in ein flaches View-Modell (Artikel-/Lagernamen
aufgeloest, Euro/Datum formatiert, deutsche Status-/Rollen-Labels); die
Template-Datei bleibt frei von Domain-Logik. Kategorien: Uebersicht, Kunde,
Lieferadresse, Positionen (Tabelle, veraenderte Zeilen hervorgehoben),
Geld-Gutschrift, Dienstleistungen, Notizen, Kontakte. Nutzt den bestehenden
get_delivery_details-Use-Case; admin-key-geschuetzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-07-10 00:11:05 +02:00
parent af034250e7
commit 02aa216bc6
6 changed files with 680 additions and 1 deletions

View File

@ -0,0 +1,220 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Beleg {{ belegnummer }} Lieferdetails</title>
<style>
:root {
--bg: #f4f5f7; --card: #ffffff; --ink: #1f2733; --muted: #6b7684;
--line: #e4e8ee; --accent: #2f6fed; --ok: #1f9d63; --warn: #d97706;
--danger: #d83a3a; --chip: #eef2fb;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--ink);
font: 15px/1.5 -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
padding: 24px;
}
.wrap { max-width: 1040px; margin: 0 auto; }
header.page {
display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
margin-bottom: 20px;
}
header.page h1 { font-size: 26px; margin: 0; letter-spacing: .3px; }
header.page .sub { color: var(--muted); font-size: 14px; margin-top: 2px; }
.badges { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
.badge {
display: inline-block; padding: 4px 12px; border-radius: 999px;
font-size: 13px; font-weight: 600; background: var(--chip); color: var(--accent);
}
.badge.state-completed { background: #e5f6ec; color: var(--ok); }
.badge.state-active { background: #e8f0fe; color: var(--accent); }
.badge.state-held { background: #fef3e2; color: var(--warn); }
.badge.state-canceled { background: #fce8e8; color: var(--danger); }
.badge.modified { background: #fef3e2; color: var(--warn); }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.card {
background: var(--card); border: 1px solid var(--line); border-radius: 12px;
padding: 16px 18px;
}
.card.full { grid-column: 1 / -1; }
.card h2 {
font-size: 13px; text-transform: uppercase; letter-spacing: .6px;
color: var(--muted); margin: 0 0 12px; font-weight: 700;
}
dl { display: grid; grid-template-columns: minmax(120px, 40%) 1fr; gap: 6px 14px; margin: 0; }
dt { color: var(--muted); }
dd { margin: 0; font-variant-numeric: tabular-nums; }
.addr { line-height: 1.45; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .4px; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
tr.modified td { background: #fff8ec; }
tfoot td { font-weight: 700; border-top: 2px solid var(--line); border-bottom: none; }
.pill { display: inline-block; padding: 1px 8px; border-radius: 6px; font-size: 12px; font-weight: 600; }
.pill.done { background: #e5f6ec; color: var(--ok); }
.pill.removed, .pill.held { background: #fef3e2; color: var(--warn); }
.pill.in_progress { background: #eef2fb; color: var(--accent); }
.note { border-left: 3px solid var(--line); padding: 4px 0 4px 12px; margin: 10px 0; }
.note .meta { color: var(--muted); font-size: 13px; margin-top: 2px; }
.empty { color: var(--muted); font-style: italic; }
.contact { padding: 8px 0; border-bottom: 1px solid var(--line); }
.contact:last-child { border-bottom: none; }
.contact .role { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .4px; }
.chan { color: var(--muted); font-size: 14px; }
footer.page { color: var(--muted); font-size: 12px; margin-top: 22px; text-align: right; }
@media (max-width: 720px) { .grid { grid-template-columns: 1fr; } body { padding: 14px; } }
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<div>
<h1>Beleg {{ belegnummer }}</h1>
<div class="sub">{{ customer_name }} · Tour {{ tour_date }}</div>
</div>
<div class="badges">
<span class="badge state-{{ state_class }}">{{ state_label }}</span>
{% if positions_modified %}<span class="badge modified">Positionen verändert</span>{% endif %}
</div>
</header>
<div class="grid">
<section class="card">
<h2>Übersicht</h2>
<dl>
<dt>Status</dt><dd>{{ state_label }}</dd>
<dt>Tourdatum</dt><dd>{{ tour_date }}</dd>
<dt>Position in Tour</dt><dd>{{ sort_order }}</dd>
<dt>Wunschzeit</dt><dd>{{ desired_time }}</dd>
<dt>Sondervereinbarung</dt><dd>{{ special_agreements }}</dd>
<dt>Vorausbezahlt</dt><dd>{{ prepaid }}</dd>
<dt>Warenwert (geliefert)</dt><dd>{{ total }}</dd>
</dl>
</section>
<section class="card">
<h2>Kunde</h2>
<dl>
<dt>Name</dt><dd>{{ customer_name }}</dd>
<dt>ERP-Kundennr.</dt><dd>{{ erp_customer_id }}</dd>
</dl>
<div class="addr" style="margin-top:10px">
{% for line in customer_address_lines %}{{ line }}<br>{% endfor %}
</div>
{% if !customer_contacts.is_empty() %}
<div style="margin-top:12px">
{% for c in customer_contacts %}
<div class="contact">
<strong>{{ c.name }}</strong>
{% if !c.phone.is_empty() %}<div class="chan">☎ {{ c.phone }}</div>{% endif %}
{% if !c.email.is_empty() %}<div class="chan">✉ {{ c.email }}</div>{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
</section>
<section class="card">
<h2>Lieferadresse</h2>
<div class="addr">
{% for line in delivery_address_lines %}{{ line }}<br>{% endfor %}
</div>
</section>
<section class="card">
<h2>Geld-Gutschrift</h2>
{% if has_credit %}
<dl>
<dt>Betrag</dt><dd>{{ credit_amount }}</dd>
<dt>Grund</dt><dd>{{ credit_reason }}</dd>
</dl>
{% else %}
<div class="empty">Keine Geld-Gutschrift</div>
{% endif %}
</section>
<section class="card full">
<h2>Positionen</h2>
<table>
<thead>
<tr>
<th>Art.-Nr.</th><th>Bezeichnung</th><th>Lager</th>
<th class="num">Soll</th><th class="num">Gescannt</th>
<th class="num">Gutschr.</th><th class="num">Geliefert</th>
<th class="num">Einzelpreis</th><th class="num">Summe</th><th>Status</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<tr class="{% if it.modified %}modified{% endif %}">
<td>{{ it.article_number }}</td>
<td>{{ it.article_name }}{% if !it.held_reason.is_empty() %}<br><span class="chan">{{ it.held_reason }}</span>{% endif %}</td>
<td>{{ it.warehouse }}</td>
<td class="num">{{ it.required }}</td>
<td class="num">{{ it.scanned }}</td>
<td class="num">{{ it.credited }}</td>
<td class="num">{{ it.delivered }}</td>
<td class="num">{{ it.unit_price }}</td>
<td class="num">{{ it.line_total }}</td>
<td><span class="pill {{ it.status_class }}">{{ it.status_label }}</span></td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr><td colspan="8" class="num">Warenwert (geliefert)</td><td class="num">{{ total }}</td><td></td></tr>
</tfoot>
</table>
</section>
<section class="card">
<h2>Dienstleistungen</h2>
{% if services.is_empty() %}
<div class="empty">Keine Dienstleistungen gesetzt</div>
{% else %}
<dl>
{% for s in services %}<dt>{{ s.name }}</dt><dd>{{ s.value }}</dd>{% endfor %}
</dl>
{% endif %}
</section>
<section class="card">
<h2>Notizen</h2>
{% if notes.is_empty() %}
<div class="empty">Keine Notizen</div>
{% else %}
{% for n in notes %}
<div class="note">
<div>{{ n.text }}</div>
<div class="meta">
#{{ n.author }} · {{ n.created_at }}
{% if n.credit_note %} · Gutschrift-Notiz{% endif %}
{% if n.image %} · 📎 Bild{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</section>
<section class="card full">
<h2>Kontakte</h2>
{% if contacts.is_empty() %}
<div class="empty">Keine Kontaktquellen</div>
{% else %}
{% for c in contacts %}
<div class="contact">
<div class="role">{{ c.role }}</div>
<strong>{{ c.name }}</strong>
{% for ch in c.channels %}<div class="chan">{{ ch.kind }}: {{ ch.value }}</div>{% endfor %}
</div>
{% endfor %}
{% endif %}
</section>
</div>
<footer class="page">Erzeugt am {{ generated_at }} · Holzleitner Backend</footer>
</div>
</body>
</html>