gwoe-antragspruefer/app/templates/v2/screens/auswertungen.html
Dotty Dotter 565849bd84 feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
  (ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
  Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze

Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
  /api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie

Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00

431 lines
14 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.

{% extends "v2/base.html" %}
{% block title %}Auswertungen — GWÖ-Antragsprüfer{% endblock %}
{% set v2_active_nav = "auswertungen" %}
{% block head_extra %}
<script src="/static/chart.umd.min.js"></script>
<style>
.auswert-tabs {
display: flex;
gap: 4px;
margin-bottom: 1rem;
border-bottom: 2px solid var(--ecg-border);
padding-bottom: 0;
}
.auswert-tab {
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 6px 14px;
border: none;
background: none;
cursor: pointer;
color: var(--ecg-dark);
opacity: 0.55;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: opacity 0.1s;
}
.auswert-tab:hover { opacity: 0.85; }
.auswert-tab.active {
opacity: 1;
border-bottom-color: var(--ecg-teal);
color: var(--ecg-teal);
}
.auswert-panel { display: none; }
.auswert-panel.active { display: block; }
.controls-bar {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 1rem;
font-family: var(--font-mono);
font-size: 11px;
}
.controls-bar select {
font-family: var(--font-mono);
font-size: 11px;
padding: 5px 8px;
border: 1px solid var(--ecg-border);
border-radius: 3px;
background: var(--ecg-card-bg);
color: var(--ecg-dark);
}
.controls-bar button {
font-family: var(--font-mono);
font-size: 11px;
padding: 5px 12px;
border: 1px solid var(--ecg-border);
border-radius: 3px;
background: var(--ecg-card-bg);
color: var(--ecg-dark);
cursor: pointer;
}
.controls-bar button.primary {
background: var(--ecg-teal);
color: #fff;
border-color: var(--ecg-teal);
}
.matrix-wrap {
overflow-x: auto;
background: var(--ecg-card-bg);
border: 1px solid var(--ecg-border);
border-radius: 4px;
padding: 10px;
}
table.gwoe-matrix {
border-collapse: collapse;
font-size: 12px;
min-width: 400px;
}
table.gwoe-matrix th, table.gwoe-matrix td {
border: 1px solid var(--ecg-border);
padding: 5px 8px;
text-align: center;
white-space: nowrap;
}
table.gwoe-matrix th { background: var(--ecg-bg-subtle); font-weight: 700; }
table.gwoe-matrix th.row-h { text-align: left; }
table.gwoe-matrix .s-high { background: rgba(136,158,51,0.22); font-weight: 700; }
table.gwoe-matrix .s-mid { background: rgba(247,148,29,0.15); }
table.gwoe-matrix .s-low { background: rgba(200,0,0,0.13); font-weight: 700; }
table.gwoe-matrix .empty { color: var(--ecg-dark); opacity: 0.3; }
table.gwoe-matrix td.clickable { cursor: pointer; }
table.gwoe-matrix td.clickable:hover { background: rgba(0,157,165,0.1); }
.meta-line {
font-family: var(--font-mono);
font-size: 11px;
opacity: 0.6;
margin-top: 8px;
}
/* Modal */
.v2-modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
z-index: 500;
align-items: center;
justify-content: center;
}
.v2-modal-backdrop.show { display: flex; }
.v2-modal {
background: var(--ecg-card-bg);
border-radius: 6px;
padding: 20px 24px;
max-width: 580px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.v2-modal h2 {
font-family: var(--font-display);
font-size: 16px;
color: var(--ecg-teal);
margin: 0 0 12px;
}
.v2-modal-close {
position: absolute;
top: 12px;
right: 14px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: var(--ecg-dark);
opacity: 0.5;
}
.v2-modal-close:hover { opacity: 1; }
table.modal-table {
border-collapse: collapse;
width: 100%;
font-size: 12px;
margin-top: 8px;
}
table.modal-table th, table.modal-table td {
border: 1px solid var(--ecg-border);
padding: 4px 8px;
text-align: left;
}
table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
</style>
{% endblock %}
{% block main %}
<div style="padding:0 0 1.5rem;">
<h1 style="font-family:var(--font-display);font-size:22px;color:var(--ecg-teal);margin:0 0 4px;">Auswertungen</h1>
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.6;">
Bundesland × Partei · Thema × Fraktion · Cluster
</p>
</div>
<!-- Tabs -->
<div class="auswert-tabs" role="tablist">
<button class="auswert-tab active" role="tab" onclick="switchTab('bl-partei', this)">BL × Partei</button>
<button class="auswert-tab" role="tab" onclick="switchTab('themen', this)">Thema × Fraktion</button>
<button class="auswert-tab" role="tab" onclick="switchTab('cluster', this)">Cluster</button>
</div>
<!-- Panel 1: BL × Partei -->
<div class="auswert-panel active" id="panel-bl-partei">
<div class="controls-bar">
<label for="wp-filter">Wahlperiode:</label>
<select id="wp-filter">
<option value="">— alle WPs —</option>
{% for wp in wahlperioden %}
<option value="{{ wp }}">{{ wp }}</option>
{% endfor %}
</select>
<label for="bl-filter">Bundesland:</label>
<select id="bl-filter">
<option value="">Alle</option>
{% for code in bl_codes %}
<option value="{{ code }}">{{ code }}</option>
{% endfor %}
</select>
<button class="primary" onclick="loadBlMatrix()">Laden</button>
<button onclick="window.location.href='/api/auswertungen/export.csv'">CSV-Export</button>
</div>
<div id="bl-matrix-wrap" class="matrix-wrap">
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Matrix …</div>
</div>
<div id="bl-matrix-meta" class="meta-line"></div>
</div>
<!-- Panel 2: Thema × Fraktion -->
<div class="auswert-panel" id="panel-themen">
<div class="controls-bar">
<label for="themen-bl-filter">Bundesland:</label>
<select id="themen-bl-filter" onchange="loadThemenMatrix()">
<option value="">Alle</option>
{% for code in bl_codes %}
<option value="{{ code }}">{{ code }}</option>
{% endfor %}
</select>
</div>
<div id="themen-matrix-wrap" class="matrix-wrap">
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Wählen Sie den Tab „Thema × Fraktion".</div>
</div>
</div>
<!-- Panel 3: Cluster-Link -->
<div class="auswert-panel" id="panel-cluster">
<div class="v2-kasten outline-blue">
<h4>Cluster-Ansicht</h4>
<p style="font-size:12px;">
Die interaktive Cluster-Übersicht finden Sie unter
<a href="/v2/cluster" style="color:var(--ecg-teal);">/v2/cluster</a>.
Sie zeigt thematisch ähnliche Anträge als redaktionelle Liste und verlinkt
zur Force-Graph-Visualisierung.
</p>
<a href="/v2/cluster"
style="display:inline-block;margin-top:8px;font-family:var(--font-mono);font-size:11px;
padding:6px 14px;background:var(--ecg-teal);color:#fff;border-radius:3px;text-decoration:none;">
Zur Cluster-Ansicht →
</a>
</div>
</div>
<!-- Zeitreihen-Modal -->
<div class="v2-modal-backdrop" id="modal-backdrop" onclick="closeModal(event)">
<div class="v2-modal" onclick="event.stopPropagation()">
<button class="v2-modal-close" onclick="closeModal()">&times;</button>
<h2 id="modal-title">Zeitreihe</h2>
<div id="modal-body" style="font-size:12px;">Lade …</div>
</div>
</div>
{% endblock %}
{% block body_scripts %}
<script>
let _tabLoaded = { 'bl-partei': false, 'themen': false };
function switchTab(id, btn) {
document.querySelectorAll('.auswert-tab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.auswert-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('panel-' + id).classList.add('active');
if (id === 'themen' && !_tabLoaded.themen) {
loadThemenMatrix();
_tabLoaded.themen = true;
}
}
function scoreClass(avg) {
if (avg == null) return '';
if (avg >= 6) return 's-high';
if (avg >= 3) return 's-mid';
return 's-low';
}
async function loadBlMatrix() {
const wrap = document.getElementById('bl-matrix-wrap');
const metaEl = document.getElementById('bl-matrix-meta');
const wp = document.getElementById('wp-filter').value;
const bl = document.getElementById('bl-filter').value;
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Matrix …</div>';
metaEl.textContent = '';
let url = '/api/auswertungen/matrix';
const params = [];
if (wp) params.push('wahlperiode=' + encodeURIComponent(wp));
if (bl) params.push('bundesland=' + encodeURIComponent(bl));
if (params.length) url += '?' + params.join('&');
try {
const r = await fetch(url);
const data = await r.json();
if (!data.bundeslaender || !data.bundeslaender.length) {
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Keine Assessments für diesen Filter.</div>';
return;
}
let html = '<table class="gwoe-matrix"><thead><tr><th class="row-h">Bundesland</th>';
for (const partei of data.parteien) html += `<th>${partei}</th>`;
html += '</tr></thead><tbody>';
for (const bundesland of data.bundeslaender) {
html += `<tr><th class="row-h">${bundesland}</th>`;
for (const partei of data.parteien) {
const cell = (data.cells[bundesland] || {})[partei];
if (cell) {
html += `<td class="clickable ${scoreClass(cell.avg)}"
onclick="showZeitreihe('${bundesland.replace(/'/g,"\\'")}','${partei.replace(/'/g,"\\'")}')">
${cell.avg.toFixed(1)}<br><small>n=${cell.n}</small>
</td>`;
} else {
html += '<td class="empty">—</td>';
}
}
html += '</tr>';
}
html += '</tbody></table>';
wrap.innerHTML = html;
metaEl.textContent = `${data.total} Assessments | Filter: ${data.filter_wp || 'alle WPs'}`;
} catch (e) {
wrap.innerHTML = `<div style="color:#c00;font-family:var(--font-mono);font-size:12px;">Fehler: ${e}</div>`;
}
}
async function loadThemenMatrix() {
const wrap = document.getElementById('themen-matrix-wrap');
const bl = document.getElementById('themen-bl-filter').value;
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Themen-Matrix …</div>';
let url = '/api/auswertungen/themen-matrix';
if (bl) url += '?bundesland=' + encodeURIComponent(bl);
try {
const r = await fetch(url);
const data = await r.json();
if (!data.themen || !data.themen.length) {
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Noch zu wenige Assessments für Themen-Analyse.</div>';
return;
}
let html = '<table class="gwoe-matrix"><thead><tr><th class="row-h">Thema</th>';
for (const frak of data.fraktionen) html += `<th>${frak}</th>`;
html += '</tr></thead><tbody>';
for (const thema of data.themen) {
html += `<tr><th class="row-h">${thema}</th>`;
for (const frak of data.fraktionen) {
const cell = (data.cells[thema] || {})[frak];
if (cell) {
html += `<td class="${scoreClass(cell.avg)}" title="${thema} × ${frak}: Ø ${cell.avg}/10 (${cell.n} Anträge)">
${cell.avg.toFixed(1)}<br><small>n=${cell.n}</small>
</td>`;
} else {
html += '<td class="empty">—</td>';
}
}
html += '</tr>';
}
html += '</tbody></table>';
wrap.innerHTML = html;
} catch (e) {
wrap.innerHTML = `<div style="color:#c00;font-family:var(--font-mono);font-size:12px;">Fehler: ${e}</div>`;
}
}
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 style="font-family:var(--font-mono);font-size:12px;opacity:0.6;">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="font-family:var(--font-mono);font-size:12px;opacity:0.6;">Keine Daten für diese Kombination.</p>';
return;
}
body.innerHTML =
'<canvas id="zeitreihe-chart" style="max-height:260px;margin-bottom:1rem;"></canvas>' +
'<table class="modal-table"><thead><tr><th>Wahlperiode</th><th>Anträge</th><th>Ø GWÖ-Score</th></tr></thead><tbody>' +
z.wahlperioden.map(row => `<tr><td>${row.wp}</td><td>${row.n}</td><td><strong>${row.avg.toFixed(2)}</strong></td></tr>`).join('') +
'</tbody></table>';
if (window.Chart) {
const ctx = document.getElementById('zeitreihe-chart');
new Chart(ctx, {
type: 'line',
data: {
labels: z.wahlperioden.map(r => 'WP ' + r.wp),
datasets: [{
label: `Ø GWÖ-Score ${partei} (${bundesland})`,
data: z.wahlperioden.map(r => r.avg),
borderColor: '#009da5',
backgroundColor: 'rgba(0,157,165,0.1)',
fill: true,
tension: 0.3,
pointRadius: 5,
}]
},
options: {
responsive: true,
scales: {
y: { min: 0, max: 10, title: { display: true, text: 'GWÖ-Score' } }
},
plugins: {
tooltip: {
callbacks: {
afterLabel: (ctx) => `n=${z.wahlperioden[ctx.dataIndex].n} Anträge`
}
}
}
}
});
}
} catch (e) {
body.innerHTML = `<p style="color:#c00;font-family:var(--font-mono);font-size:12px;">Fehler: ${e}</p>`;
}
}
function closeModal(ev) {
if (!ev || ev.target.id === 'modal-backdrop') {
document.getElementById('modal-backdrop').classList.remove('show');
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') document.getElementById('modal-backdrop').classList.remove('show');
});
// Load BL-Matrix on init
loadBlMatrix();
</script>
{% endblock %}