gwoe-antragspruefer/app/templates/v2/screens/auswertungen.html

428 lines
14 KiB
HTML
Raw Normal View History

{% 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>
<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">
<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;
}
}
// Bei BL-Wechsel aktive Panels neu laden
window.addEventListener('v2-bl-changed', function () {
var activePanel = document.querySelector('.auswert-panel.active');
if (!activePanel) return;
if (activePanel.id === 'panel-bl-partei') loadBlMatrix();
if (activePanel.id === 'panel-themen') loadThemenMatrix();
});
document.addEventListener('DOMContentLoaded', function () { loadBlMatrix(); });
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 blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
const bl = (blRaw === 'ALL') ? '' : blRaw;
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 blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
const bl = (blRaw === 'ALL') ? '' : blRaw;
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 %}