feat(#189 Phase 10.2): Empfehlungs-Konsistenz-Drilldown analog zu Heuchelei
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
ba1f104c8e
commit
1dfdb59954
@ -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,
|
||||
|
||||
18
app/main.py
18
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,
|
||||
|
||||
@ -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 = `
|
||||
<div style="background:var(--paper);max-width:760px;width:100%;max-height:90vh;overflow:auto;border-radius:8px;padding:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<h3 id="sv-heuchelei-modal-title" style="margin:0;font-family:var(--font-display);">…</h3>
|
||||
<button onclick="closeHeucheleiDrilldown()"
|
||||
style="background:none;border:1px solid var(--ecg-border);border-radius:4px;padding:4px 12px;cursor:pointer;font-family:var(--font-mono);">✕</button>
|
||||
</div>
|
||||
<p id="sv-heuchelei-modal-meta" style="font-size:12px;opacity:0.7;margin:0 0 12px;"></p>
|
||||
<div id="sv-heuchelei-modal-body">Lade…</div>
|
||||
</div>`;
|
||||
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 = '<p style="opacity:0.7;">Keine Anträge gefunden.</p>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = `
|
||||
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.6;margin-bottom:8px;">
|
||||
${data.count} Treffer${data.items.length < data.count ? ` — Top ${data.items.length} angezeigt` : ''}.
|
||||
</p>
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:560px;">
|
||||
<thead style="background:var(--ecg-card-bg);">
|
||||
<tr>
|
||||
<th style="padding:6px 10px;text-align:left;border-bottom:1px solid var(--ecg-border);">Drucksache</th>
|
||||
<th style="padding:6px 10px;text-align:left;border-bottom:1px solid var(--ecg-border);">BL</th>
|
||||
<th style="padding:6px 10px;text-align:left;border-bottom:1px solid var(--ecg-border);">Datum</th>
|
||||
<th style="padding:6px 10px;text-align:right;border-bottom:1px solid var(--ecg-border);">GWÖ</th>
|
||||
<th style="padding:6px 10px;text-align:left;border-bottom:1px solid var(--ecg-border);">Empfehlung</th>
|
||||
<th style="padding:6px 10px;text-align:left;border-bottom:1px solid var(--ecg-border);">Beschluss</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${data.items.map(it => `
|
||||
<tr style="border-bottom:1px solid var(--hairline);">
|
||||
<td style="padding:6px 10px;font-family:var(--font-mono);">
|
||||
<a href="/antrag/${it.drucksache}?bundesland=${encodeURIComponent(it.bundesland)}"
|
||||
style="color:var(--ecg-teal);">${it.drucksache}</a>
|
||||
</td>
|
||||
<td style="padding:6px 10px;font-family:var(--font-mono);">${it.bundesland}</td>
|
||||
<td style="padding:6px 10px;font-family:var(--font-mono);">${it.datum || ''}</td>
|
||||
<td style="padding:6px 10px;text-align:right;font-weight:700;color:#2da44e;">${it.gwoe_score != null ? it.gwoe_score.toFixed(1) : '—'}</td>
|
||||
<td style="padding:6px 10px;font-style:italic;opacity:0.85;">${it.empfehlung || '—'}</td>
|
||||
<td style="padding:6px 10px;font-style:italic;opacity:0.7;">${it.ergebnis || '—'}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} 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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user