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>
221 lines
8.4 KiB
HTML
221 lines
8.4 KiB
HTML
<!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>
|