gwoe-antragspruefer/app/templates/auswertungen.html
Dotty Dotter 7cf073122f Phase E (substituted): Auswertungen-Drilldown-Modal (#59)
Sachsen-Adapter (#26/#38) ist Eigensystem mit ASP.NET-Webforms-Postbacks
(__VIEWSTATE/__CALLBACKID, siehe bundeslaender.py:343-348) und braucht
HAR-Aufnahme → Blocker für autonome Bearbeitung. Phase E entsprechend
substituiert mit der Frontend-Erweiterung der Auswertungen.

- Matrix-Zellen sind jetzt klickbar (`cell-with-data`-Klasse +
  hover-outline mit Blue-Border)
- Klick öffnet ein Modal, das `/api/auswertungen/zeitreihe?
  bundesland=...&partei=...` aufruft und die Score-Entwicklung dieser
  (BL, Partei)-Kombination über alle bekannten WPs als Tabelle rendert
- ESC-Taste oder Backdrop-Klick schließt das Modal
- Schließt damit den Frontend-Loop für die in Phase C gebauten
  Backend-Endpoints

(CLAUDE.md-Sync separat — die Datei liegt im Projekt-Root außerhalb
des Webapp-Git-Repos.)

Refs: #59 (Phase E substituted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:30:10 +02:00

304 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auswertungen — {{ app_name }}</title>
<style>
:root {
--color-darkgray: #5a5a5a;
--color-green: #889e33;
--color-blue: #009da5;
--color-lightgray: #bfbfbf;
--color-bg: #f5f5f5;
--color-orange: #F7941D;
--color-red: #d00000;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Avenir', 'Segoe UI', sans-serif;
color: var(--color-darkgray);
line-height: 1.6;
background: var(--color-bg);
}
.header {
background: white;
padding: 1rem 2rem;
border-bottom: 1px solid var(--color-lightgray);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.header h1 { color: var(--color-blue); font-size: 1.3rem; }
.header nav a {
color: var(--color-blue);
text-decoration: none;
margin-right: 1rem;
font-size: 0.9rem;
}
.header nav a:hover { text-decoration: underline; }
main { max-width: 1400px; margin: 1.5rem auto; padding: 0 2rem; }
.controls {
background: white;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.controls label { font-size: 0.9rem; }
.controls select, .controls button {
padding: 0.4rem 0.7rem;
border: 1px solid var(--color-lightgray);
border-radius: 3px;
font-size: 0.9rem;
background: white;
color: var(--color-darkgray);
cursor: pointer;
}
.controls button.export {
background: var(--color-blue);
color: white;
border-color: var(--color-blue);
}
.matrix-wrap { background: white; padding: 1rem; border-radius: 4px; overflow-x: auto; }
table.matrix {
border-collapse: collapse;
width: 100%;
font-size: 0.85rem;
}
table.matrix th, table.matrix td {
border: 1px solid var(--color-lightgray);
padding: 0.4rem 0.6rem;
text-align: center;
}
table.matrix th {
background: var(--color-bg);
font-weight: 600;
color: var(--color-darkgray);
}
table.matrix th.row-header {
background: var(--color-blue);
color: white;
text-align: left;
position: sticky;
left: 0;
}
table.matrix .empty { color: var(--color-lightgray); }
table.matrix .score-high { background: rgba(136, 158, 51, 0.25); font-weight: 600; }
table.matrix .score-mid { background: rgba(247, 148, 29, 0.18); }
table.matrix .score-low { background: rgba(208, 0, 0, 0.18); font-weight: 600; }
.meta {
font-size: 0.8rem;
color: var(--color-lightgray);
margin-top: 0.6rem;
}
.empty-state {
background: white;
padding: 2rem;
text-align: center;
border-radius: 4px;
color: var(--color-lightgray);
}
table.matrix td.cell-with-data {
cursor: pointer;
}
table.matrix td.cell-with-data:hover {
outline: 2px solid var(--color-blue);
outline-offset: -2px;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-backdrop.show { display: flex; }
.modal {
background: white;
border-radius: 6px;
padding: 1.5rem 2rem;
min-width: 320px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
.modal h2 {
color: var(--color-blue);
margin-bottom: 0.8rem;
font-size: 1.1rem;
}
.modal table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
}
.modal table th, .modal table td {
padding: 0.4rem 0.8rem;
border-bottom: 1px solid var(--color-bg);
text-align: left;
font-size: 0.85rem;
}
.modal table th { background: var(--color-bg); }
.modal-close {
float: right;
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
color: var(--color-lightgray);
}
.modal-close:hover { color: var(--color-darkgray); }
</style>
</head>
<body>
<div class="header">
<h1>Auswertungen — Bundesland × Partei × Wahlperiode</h1>
<nav>
<a href="/">← zurück zur Suche</a>
<a href="/quellen">Quellen</a>
</nav>
</div>
<main>
<div class="controls">
<label>Wahlperiode:
<select id="wp-filter">
<option value="">— alle WPs —</option>
{% for wp in wahlperioden %}
<option value="{{ wp }}">{{ wp }}</option>
{% endfor %}
</select>
</label>
<button id="reload">Anwenden</button>
<button class="export" id="export-csv">CSV-Export (alle Daten)</button>
</div>
<div id="matrix-container" class="matrix-wrap">
<div class="empty-state">Lade Matrix …</div>
</div>
<div class="meta" id="meta"></div>
</main>
<!-- Drill-down modal: Zeitreihe für eine (BL, Partei)-Kombination -->
<div class="modal-backdrop" id="modal-backdrop" onclick="closeModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2 id="modal-title">Zeitreihe</h2>
<div id="modal-body">Lade …</div>
</div>
</div>
<script>
const wpFilter = document.getElementById('wp-filter');
const reloadBtn = document.getElementById('reload');
const exportBtn = document.getElementById('export-csv');
const container = document.getElementById('matrix-container');
const meta = document.getElementById('meta');
function scoreClass(avg) {
if (avg === null || avg === undefined) return '';
if (avg >= 6) return 'score-high';
if (avg >= 3) return 'score-mid';
return 'score-low';
}
async function loadMatrix() {
const wp = wpFilter.value;
const url = wp
? `/api/auswertungen/matrix?wahlperiode=${encodeURIComponent(wp)}`
: '/api/auswertungen/matrix';
container.innerHTML = '<div class="empty-state">Lade Matrix …</div>';
try {
const r = await fetch(url);
const data = await r.json();
if (!data.bundeslaender.length) {
container.innerHTML = '<div class="empty-state">Keine Assessments für diesen Filter.</div>';
meta.textContent = '';
return;
}
let html = '<table class="matrix"><thead><tr><th class="row-header">Bundesland</th>';
for (const partei of data.parteien) {
html += `<th>${partei}</th>`;
}
html += '</tr></thead><tbody>';
for (const bl of data.bundeslaender) {
html += `<tr><th class="row-header">${bl}</th>`;
for (const partei of data.parteien) {
const cell = (data.cells[bl] || {})[partei];
if (cell) {
// Click → drill-down auf die Zeitreihe für diese Zelle
html += `<td class="cell-with-data ${scoreClass(cell.avg)}" data-bl="${bl}" data-partei="${partei}" onclick="showZeitreihe(this.dataset.bl, this.dataset.partei)">${cell.avg.toFixed(1)}<br><small>n=${cell.n}</small></td>`;
} else {
html += '<td class="empty">—</td>';
}
}
html += '</tr>';
}
html += '</tbody></table>';
container.innerHTML = html;
meta.textContent = `${data.total} Assessment(s) | Filter: ${data.filter_wp || 'alle WPs'}`;
} catch (e) {
container.innerHTML = `<div class="empty-state">Fehler beim Laden: ${e}</div>`;
}
}
reloadBtn.addEventListener('click', loadMatrix);
wpFilter.addEventListener('change', loadMatrix);
exportBtn.addEventListener('click', () => {
window.location.href = '/api/auswertungen/export.csv';
});
// Zeitreihen-Modal: zeigt die Score-Entwicklung einer (BL, Partei)-
// Kombination über alle bekannten Wahlperioden hinweg.
async function showZeitreihe(bundesland, partei) {
const backdrop = document.getElementById('modal-backdrop');
const title = document.getElementById('modal-title');
const body = document.getElementById('modal-body');
title.textContent = `${bundesland} × ${partei}`;
body.innerHTML = '<p>Lade Zeitreihe …</p>';
backdrop.classList.add('show');
try {
const r = await fetch(`/api/auswertungen/zeitreihe?bundesland=${encodeURIComponent(bundesland)}&partei=${encodeURIComponent(partei)}`);
const z = await r.json();
if (!z.wahlperioden || !z.wahlperioden.length) {
body.innerHTML = '<p style="color:#888;">Keine Daten für diese Kombination.</p>';
return;
}
let html = '<table><thead><tr><th>Wahlperiode</th><th>Anträge</th><th>Ø GWÖ-Score</th></tr></thead><tbody>';
for (const row of z.wahlperioden) {
html += `<tr><td>${row.wp}</td><td>${row.n}</td><td><strong>${row.avg.toFixed(2)}</strong></td></tr>`;
}
html += '</tbody></table>';
body.innerHTML = html;
} catch (e) {
body.innerHTML = `<p style="color:#d00;">Fehler: ${e}</p>`;
}
}
function closeModal(ev) {
// Klick aufs Backdrop schließt; Klicks im Modal nicht
if (!ev || ev.target.id === 'modal-backdrop') {
document.getElementById('modal-backdrop').classList.remove('show');
} else if (!ev.target) {
document.getElementById('modal-backdrop').classList.remove('show');
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') document.getElementById('modal-backdrop').classList.remove('show');
});
loadMatrix();
</script>
</body>
</html>