From 1dfdb59954d532864d94b0bc8090484757a617ce Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Wed, 6 May 2026 17:11:34 +0200 Subject: [PATCH] feat(#189 Phase 10.2): Empfehlungs-Konsistenz-Drilldown analog zu Heuchelei MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_empfehlungs_konsistenz_cases() liefert Antraege wo `partei` mit NEIN gestimmt hat, obwohl die GWÖ-Empfehlung "Unterstuetzen" lautete. - Endpoint GET /api/auswertungen/empfehlungs-konsistenz-cases - Frontend: Konsistenz-Bar bekommt onClick → Modal-Tabelle mit Drucksache, BL, Datum, GWÖ-Score, Empfehlung, Beschluss. Drucksachen-Link ins Detail. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/auswertungen.py | 59 +++++++++++++++ app/main.py | 18 +++++ app/templates/v2/screens/auswertungen.html | 84 +++++++++++++++++++++- 3 files changed, 160 insertions(+), 1 deletion(-) diff --git a/app/auswertungen.py b/app/auswertungen.py index 49c2f45..98e469e 100644 --- a/app/auswertungen.py +++ b/app/auswertungen.py @@ -992,6 +992,65 @@ def aggregate_empfehlungs_konsistenz( } +def get_empfehlungs_konsistenz_cases( + partei: str, + filter_bl: Optional[str] = None, + filter_wp: Optional[str] = None, + limit: int = 50, + db_path: Optional[Path] = None, +) -> dict: + """Drilldown: konkrete Antraege wo ``partei`` mit NEIN gestimmt hat, + obwohl die GWÖ-Empfehlung "Unterstuetzen" lautete. + + Quelle fuer Klick auf Empfehlungs-Konsistenz-Bar im Stimmverhalten-Tab. + """ + rows = _load_assessments_with_votes(filter_bl, filter_wp, db_path) + + path = db_path or settings.db_path + if not Path(path).exists(): + return {"partei": partei, "count": 0, "items": [], "filter": { + "bundesland": filter_bl, "wahlperiode": filter_wp, + }} + + POSITIV = {"Uneingeschränkt unterstützen", "Unterstützen mit Änderungen"} + conn = sqlite3.connect(str(path)) + try: + empfehlung_map = { + (r[0], r[1]): r[2] for r in conn.execute( + "SELECT bundesland, drucksache, empfehlung FROM assessments" + ).fetchall() + } + finally: + conn.close() + + cases = [] + for row in rows: + if partei not in row["nein"]: + continue + empfehlung = empfehlung_map.get((row["bundesland"], row["drucksache"])) + if empfehlung not in POSITIV: + continue + cases.append({ + "drucksache": row["drucksache"], + "bundesland": row["bundesland"], + "datum": row.get("datum"), + "gwoe_score": row.get("gwoe_score"), + "empfehlung": empfehlung, + "ergebnis": row.get("ergebnis"), + }) + + cases.sort(key=lambda c: (c.get("gwoe_score") or 0), reverse=True) + return { + "partei": partei, + "count": len(cases), + "items": cases[:limit], + "filter": { + "bundesland": filter_bl, + "wahlperiode": filter_wp, + }, + } + + def aggregate_stimm_index_cross_bl( filter_wp: Optional[str] = None, exclude_antragsteller: bool = True, diff --git a/app/main.py b/app/main.py index c073cb6..ca4fb7a 100644 --- a/app/main.py +++ b/app/main.py @@ -2730,6 +2730,24 @@ async def auswertungen_heuchelei_cases( ) +@app.get("/api/auswertungen/empfehlungs-konsistenz-cases") +async def auswertungen_empfehlungs_konsistenz_cases( + partei: str, + bundesland: Optional[str] = None, + wahlperiode: Optional[str] = None, + limit: int = 50, +): + """Drilldown: Anträge wo `partei` mit NEIN stimmte trotz GWÖ-Empfehlung + 'Unterstützen'. Quelle für Klick auf Empfehlungs-Konsistenz-Bar (#167).""" + from .auswertungen import get_empfehlungs_konsistenz_cases + return get_empfehlungs_konsistenz_cases( + partei=partei, + filter_bl=bundesland, + filter_wp=wahlperiode, + limit=limit, + ) + + @app.get("/api/auswertungen/stimm-index-pro-wert") async def auswertungen_stimm_index_pro_wert( bundesland: Optional[str] = None, diff --git a/app/templates/v2/screens/auswertungen.html b/app/templates/v2/screens/auswertungen.html index cb2044f..95c0cbf 100644 --- a/app/templates/v2/screens/auswertungen.html +++ b/app/templates/v2/screens/auswertungen.html @@ -1053,6 +1053,80 @@ function closeHeucheleiDrilldown() { if (modal) modal.style.display = 'none'; } +async function openKonsistenzDrilldown(partei, bl) { + let modal = document.getElementById('sv-heuchelei-modal'); + if (!modal) { + // Modal-Markup analog zu openHeucheleiDrilldown — bei Bedarf injizieren + modal = document.createElement('div'); + modal.id = 'sv-heuchelei-modal'; + modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:9999;display:flex;align-items:center;justify-content:center;padding:8px;'; + modal.innerHTML = ` +
+
+

+ +
+

+
Lade…
+
`; + document.body.appendChild(modal); + modal.addEventListener('click', (e) => { if (e.target === modal) closeHeucheleiDrilldown(); }); + } + modal.style.display = 'flex'; + document.getElementById('sv-heuchelei-modal-title').textContent = + `Empfehlungs-Konsistenz: ${partei}`; + document.getElementById('sv-heuchelei-modal-meta').textContent = + 'Anträge mit GWÖ-Empfehlung „Unterstützen", die diese Fraktion mit Nein abgelehnt hat.'; + + let url = `/api/auswertungen/empfehlungs-konsistenz-cases?partei=${encodeURIComponent(partei)}&limit=50`; + if (bl) url += '&bundesland=' + encodeURIComponent(bl); + try { + const r = await fetch(url); + const data = await r.json(); + const body = document.getElementById('sv-heuchelei-modal-body'); + if (!data.items || !data.items.length) { + body.innerHTML = '

Keine Anträge gefunden.

'; + return; + } + body.innerHTML = ` +

+ ${data.count} Treffer${data.items.length < data.count ? ` — Top ${data.items.length} angezeigt` : ''}. +

+
+ + + + + + + + + + + + + ${data.items.map(it => ` + + + + + + + + + `).join('')} + +
DrucksacheBLDatumGWÖEmpfehlungBeschluss
+ ${it.drucksache} + ${it.bundesland}${it.datum || ''}${it.gwoe_score != null ? it.gwoe_score.toFixed(1) : '—'}${it.empfehlung || '—'}${it.ergebnis || '—'}
+
`; + } catch (e) { + document.getElementById('sv-heuchelei-modal-body').textContent = 'Fehler: ' + e; + } +} + function wertHeatColor(idx) { // Wert-Spalten haben Domain -5..+5 (rating-Skala der Matrix) if (idx == null) return 'rgba(120,120,120,0.1)'; @@ -1272,15 +1346,23 @@ async function loadEmpfehlungsKonsistenz(bl) { `davon Nein gestimmt: ${f.n_nein_trotz_empfehlung}`, `davon Ja gestimmt: ${f.n_ja}`, `davon Enthaltung: ${f.n_enth}`, + '', + '↻ Klick öffnet Antragsliste', ]; } } } + }, + onClick: (evt, elements) => { + if (!elements || !elements.length) return; + const idx = elements[0].index; + const partei = filtered[idx].partei; + openKonsistenzDrilldown(partei, bl); } } }); - meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge mit GWÖ-Empfehlung „Uneingeschränkt unterstützen" oder „Unterstützen mit Änderungen".`; + meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge mit GWÖ-Empfehlung „Uneingeschränkt unterstützen" oder „Unterstützen mit Änderungen". Klick auf eine Bar zeigt die konkreten Anträge.`; } catch (e) { meta.textContent = 'Fehler: ' + e; }