feat(#106): UI-Block 'Abstimmungsergebnis' auf Antrag-Detail

Antrag-Detail-Endpoint liest plenum_votes via get_plenum_votes() und
reicht sie an antrag_detail.html durch.

Block rendert pro Plenum-Abstimmung eine Karte:
- Ergebnis (angenommen/abgelehnt/...) farb-kodiert
- 'einstimmig'-Annotation falls gesetzt
- Quelle (Protokoll-ID, mit URL als Tooltip)
- Fraktions-Chips fuer Ja/Nein/Enthaltung

Mehrfach-Abstimmungen einer Drucksache (Ueberweisung + finale
Beschlussfassung) erzeugen mehrere Karten — chronologisch via
parsed_at DESC im Repository sortiert.

Block erscheint nur, wenn Eintraege existieren (kein leerer Header).
This commit is contained in:
Dotty Dotter 2026-04-28 08:04:32 +02:00
parent e26607854f
commit 7e0f0117e6
2 changed files with 54 additions and 0 deletions

View File

@ -304,6 +304,13 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona
except Exception:
logger.exception("Fehler beim Laden von Abstimmungsverhalten für %s", drucksache)
antrag["abstimmungsverhalten"] = None
# #106 Phase 2: fraktions-aggregierte Plenum-Abstimmungen aus Plenarprotokollen
try:
from .database import get_plenum_votes as _gpv
antrag["plenum_votes"] = await _gpv(antrag.get("bundesland") or "NRW", drucksache)
except Exception:
logger.exception("Fehler beim Laden plenum_vote_results für %s", drucksache)
antrag["plenum_votes"] = []
from .models import MATRIX_LABELS
return templates.TemplateResponse("v2/screens/antrag_detail.html", {
"request": request,

View File

@ -264,6 +264,53 @@
{% endfor %}
{% endif %}{# abstimmungsverhalten #}
{# ── Fraktions-aggregierte Plenum-Abstimmung aus Plenarprotokoll (#106) ── #}
{% if antrag.plenum_votes %}
<h3 class="v2-h3" style="margin-top:24px;">Abstimmungsergebnis</h3>
{% set ergebnis_color = {
"angenommen": "#2da44e",
"abgelehnt": "#cf222e",
"überwiesen": "#0969da",
"zurückgezogen": "#8250df",
"bestätigt": "#2da44e",
"sammel": "#0969da",
} %}
{% for v in antrag.plenum_votes %}
<div style="border:1px solid var(--hairline);border-radius:6px;padding:12px 14px;margin-bottom:10px;background:var(--paper);">
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;">
<span style="font-family:var(--font-display);font-size:14px;font-weight:700;color:{{ ergebnis_color.get(v.ergebnis, '#6e7781') }};">
{{ v.ergebnis | capitalize }}{% if v.einstimmig %} · einstimmig{% endif %}
</span>
<span style="font-family:var(--font-mono);font-size:10px;opacity:0.6;" title="{% if v.quelle_url %}{{ v.quelle_url }}{% endif %}">
{{ v.quelle_protokoll }}{% if v.quelle_url %} ↗{% endif %}
</span>
</div>
{% if v.fraktionen_ja or v.fraktionen_nein or v.fraktionen_enthaltung %}
<div style="display:flex;flex-wrap:wrap;gap:12px;font-family:var(--font-mono);font-size:11px;">
{% if v.fraktionen_ja %}
<div><span style="color:#2da44e;font-weight:700;">Ja:</span>
{% for f in v.fraktionen_ja %}<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#2da44e 15%,transparent);color:#1a7f37;border-radius:3px;margin-right:3px;">{{ f }}</span>{% endfor %}
</div>
{% endif %}
{% if v.fraktionen_nein %}
<div><span style="color:#cf222e;font-weight:700;">Nein:</span>
{% for f in v.fraktionen_nein %}<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#cf222e 15%,transparent);color:#a40e26;border-radius:3px;margin-right:3px;">{{ f }}</span>{% endfor %}
</div>
{% endif %}
{% if v.fraktionen_enthaltung %}
<div><span style="color:#6e7781;font-weight:700;">Enth.:</span>
{% for f in v.fraktionen_enthaltung %}<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#6e7781 15%,transparent);color:#57606a;border-radius:3px;margin-right:3px;">{{ f }}</span>{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
<div style="font-family:var(--font-mono);font-size:10px;opacity:0.5;margin-top:-4px;margin-bottom:8px;">
Quelle: Plenarprotokoll · automatisch extrahiert
</div>
{% endif %}{# plenum_votes #}
{% if antrag.matrix %}
<h3 class="v2-h3">Matrix 2.0 · 25 Felder</h3>
{{ matrix_mini(antrag.matrix) }}