Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real filter that propagates through every layer (Listing, internal search, statistics, party/tag filters, upload mode), and at the same time makes the source parliament visible in every place where assessments from multiple bundesländer can be mixed. Backend - database.get_all_assessments(bundesland=None) — new optional filter, "ALL" treated as None. - database.search_assessments — bug fix: previous `if bundesland:` branch incorrectly added a `WHERE bundesland='ALL'` clause; now guarded with `bundesland and bundesland != "ALL"`. - main.list_assessments — accepts ?bundesland= query param, includes the bundesland field in the response so the frontend can render badges. - main.get_single_assessment — also includes bundesland in the response so the detail header can show the source parlament. - main.search_landtag — early HTTP 400 when bundesland is missing or "ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit request. - main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit" entry prepended to the bundesländer list (kept out of bundeslaender.py on purpose — ALL is not a real state). Both endpoints additionally expose a parlament_names map so the frontend can render the source parliament without an extra round-trip. Report (PDF + HTML) - generate_html_report / generate_pdf_report — new optional bundesland parameter. When set, the report header carries the parliament name ("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …) beside the title. Three call sites updated: run_analysis, run_drucksache_analysis, download_assessment_pdf. Frontend (templates/index.html) - Header dropdown gets the synthetic ALL entry as first option; initial currentBundesland is now 'ALL' (was 'NRW'). - localStorage persistence: changeBundesland writes, DOMContentLoaded reads and validates against the visible options. - changeBundesland resets the score / party / tag filter state, syncs the upload-mode bundesland select, disables the Landtag-Suche button + tooltip when ALL, and toggles a data-mode attribute on .list-content (used by CSS to show/hide the per-item bundesland badge). - loadAssessments now sends ?bundesland=… so the API does the actual filtering. updateStats renders an additional per-bundesland average block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list spans more than one bundesland. - renderList prepends a small "bl-badge" beside the Drucksachen-Nummer. Hidden in single-bundesland mode via CSS selector to avoid clutter. - showDetail header now shows the parliament name as its own line (.detail-parlament). - searchLandtag has an early-out alert if currentBundesland === 'ALL', saving a network round-trip. - Upload-Mode bundesland select now starts with a "— Bundesland wählen —" placeholder (no auto-default), and startAnalysis validates that a concrete bundesland was chosen. CSS - .bl-badge plus the .list-content[data-mode="single"] hide rule. - .detail-parlament for the detail header line. - .header-parlament for the PDF report header line. Resolves #8. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
87874a7a14
commit
f1867d463c
@ -200,14 +200,24 @@ async def get_assessment(drucksache: str) -> Optional[dict]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def get_all_assessments() -> list[dict]:
|
async def get_all_assessments(bundesland: str = None) -> list[dict]:
|
||||||
"""Get all assessments from database."""
|
"""Get all assessments from database, optionally filtered by Bundesland.
|
||||||
|
|
||||||
|
The special value ``"ALL"`` and ``None`` mean no filter — both behave
|
||||||
|
identically and return every row. Any other value becomes a strict
|
||||||
|
``WHERE bundesland = ?`` match.
|
||||||
|
"""
|
||||||
import json
|
import json
|
||||||
|
sql = "SELECT * FROM assessments"
|
||||||
|
params: list = []
|
||||||
|
if bundesland and bundesland != "ALL":
|
||||||
|
sql += " WHERE bundesland = ?"
|
||||||
|
params.append(bundesland)
|
||||||
|
sql += " ORDER BY gwoe_score DESC"
|
||||||
|
|
||||||
async with aiosqlite.connect(settings.db_path) as db:
|
async with aiosqlite.connect(settings.db_path) as db:
|
||||||
db.row_factory = aiosqlite.Row
|
db.row_factory = aiosqlite.Row
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(sql, params)
|
||||||
"SELECT * FROM assessments ORDER BY gwoe_score DESC"
|
|
||||||
)
|
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
results = []
|
results = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@ -293,7 +303,7 @@ async def search_assessments(query: str, bundesland: str = None, limit: int = 50
|
|||||||
"""
|
"""
|
||||||
params = [f"%{first_term}%"] * 4
|
params = [f"%{first_term}%"] * 4
|
||||||
|
|
||||||
if bundesland:
|
if bundesland and bundesland != "ALL":
|
||||||
sql += " AND bundesland = ?"
|
sql += " AND bundesland = ?"
|
||||||
params.append(bundesland)
|
params.append(bundesland)
|
||||||
|
|
||||||
|
|||||||
77
app/main.py
77
app/main.py
@ -84,13 +84,24 @@ async def startup():
|
|||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
"""Landing page with upload form."""
|
"""Landing page with upload form."""
|
||||||
|
# Frontend-Liste: synthetischer "ALL"-Eintrag (Bundesweit) zuerst, dann
|
||||||
|
# die echten Bundesländer aus der Konfig. Der "ALL"-Code ist eine reine
|
||||||
|
# Frontend/API-Konvention, kein Eintrag in bundeslaender.py.
|
||||||
|
bl_list = [{"code": "ALL", "name": "🌍 Bundesweit", "active": True}]
|
||||||
|
bl_list.extend(
|
||||||
|
{"code": bl.code, "name": bl.name, "active": bl.aktiv}
|
||||||
|
for bl in alle_bundeslaender()
|
||||||
|
)
|
||||||
|
# Map code → parlament_name, damit das Frontend ohne extra Backend-Call
|
||||||
|
# für jeden Antrag den Parlamentsnamen anzeigen kann.
|
||||||
|
parlament_names = {
|
||||||
|
bl.code: bl.parlament_name for bl in alle_bundeslaender()
|
||||||
|
}
|
||||||
return templates.TemplateResponse("index.html", {
|
return templates.TemplateResponse("index.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"app_name": settings.app_name,
|
"app_name": settings.app_name,
|
||||||
"bundeslaender": [
|
"bundeslaender": bl_list,
|
||||||
{"code": bl.code, "name": bl.name, "active": bl.aktiv}
|
"parlament_names": parlament_names,
|
||||||
for bl in alle_bundeslaender()
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -138,8 +149,8 @@ async def run_analysis(job_id: str, text: str, bundesland: str, model: str):
|
|||||||
html_path = settings.reports_dir / f"{job_id}.html"
|
html_path = settings.reports_dir / f"{job_id}.html"
|
||||||
pdf_path = settings.reports_dir / f"{job_id}.pdf"
|
pdf_path = settings.reports_dir / f"{job_id}.pdf"
|
||||||
|
|
||||||
await generate_html_report(assessment, html_path)
|
await generate_html_report(assessment, html_path, bundesland=bundesland)
|
||||||
await generate_pdf_report(assessment, pdf_path)
|
await generate_pdf_report(assessment, pdf_path, bundesland=bundesland)
|
||||||
|
|
||||||
await update_job(
|
await update_job(
|
||||||
job_id,
|
job_id,
|
||||||
@ -203,9 +214,12 @@ async def get_pdf(job_id: str):
|
|||||||
|
|
||||||
# API: Load assessments from database
|
# API: Load assessments from database
|
||||||
@app.get("/api/assessments")
|
@app.get("/api/assessments")
|
||||||
async def list_assessments():
|
async def list_assessments(bundesland: Optional[str] = None):
|
||||||
"""Return all assessments from database."""
|
"""Return assessments from database, optionally filtered by Bundesland.
|
||||||
rows = await get_all_assessments()
|
|
||||||
|
``bundesland="ALL"`` and missing parameter both mean "no filter".
|
||||||
|
"""
|
||||||
|
rows = await get_all_assessments(bundesland)
|
||||||
|
|
||||||
# Convert DB format to frontend format
|
# Convert DB format to frontend format
|
||||||
assessments = []
|
assessments = []
|
||||||
@ -216,6 +230,7 @@ async def list_assessments():
|
|||||||
"fraktionen": row.get("fraktionen", []),
|
"fraktionen": row.get("fraktionen", []),
|
||||||
"datum": row.get("datum"),
|
"datum": row.get("datum"),
|
||||||
"link": row.get("link"),
|
"link": row.get("link"),
|
||||||
|
"bundesland": row.get("bundesland"),
|
||||||
"gwoeScore": row.get("gwoe_score"),
|
"gwoeScore": row.get("gwoe_score"),
|
||||||
"gwoeBegründung": row.get("gwoe_begruendung"),
|
"gwoeBegründung": row.get("gwoe_begruendung"),
|
||||||
"gwoeMatrix": row.get("gwoe_matrix", []),
|
"gwoeMatrix": row.get("gwoe_matrix", []),
|
||||||
@ -249,6 +264,7 @@ async def get_single_assessment(drucksache: str):
|
|||||||
"fraktionen": row.get("fraktionen", []),
|
"fraktionen": row.get("fraktionen", []),
|
||||||
"datum": row.get("datum"),
|
"datum": row.get("datum"),
|
||||||
"link": row.get("link"),
|
"link": row.get("link"),
|
||||||
|
"bundesland": row.get("bundesland"),
|
||||||
"gwoeScore": row.get("gwoe_score"),
|
"gwoeScore": row.get("gwoe_score"),
|
||||||
"gwoeBegründung": row.get("gwoe_begruendung"),
|
"gwoeBegründung": row.get("gwoe_begruendung"),
|
||||||
"gwoeMatrix": row.get("gwoe_matrix", []),
|
"gwoeMatrix": row.get("gwoe_matrix", []),
|
||||||
@ -306,7 +322,11 @@ async def download_assessment_pdf(drucksache: str):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
assessment = Assessment(**assessment_data)
|
assessment = Assessment(**assessment_data)
|
||||||
await generate_pdf_report(assessment, pdf_path)
|
await generate_pdf_report(
|
||||||
|
assessment,
|
||||||
|
pdf_path,
|
||||||
|
bundesland=row.get("bundesland"),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"PDF-Generierung fehlgeschlagen: {e}")
|
raise HTTPException(status_code=500, detail=f"PDF-Generierung fehlgeschlagen: {e}")
|
||||||
|
|
||||||
@ -356,7 +376,15 @@ async def search_landtag(
|
|||||||
"""
|
"""
|
||||||
Search external parliament portal (e.g., NRW OPAL).
|
Search external parliament portal (e.g., NRW OPAL).
|
||||||
Returns results that can be analyzed with "Jetzt prüfen".
|
Returns results that can be analyzed with "Jetzt prüfen".
|
||||||
|
|
||||||
|
Requires a concrete Bundesland — the special "ALL" / Bundesweit mode
|
||||||
|
cannot pick a single Landtag adapter and is rejected with HTTP 400.
|
||||||
"""
|
"""
|
||||||
|
if not bundesland or bundesland == "ALL":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Landtag-Suche benötigt ein konkretes Bundesland",
|
||||||
|
)
|
||||||
adapter = get_adapter(bundesland)
|
adapter = get_adapter(bundesland)
|
||||||
if not adapter:
|
if not adapter:
|
||||||
return {"error": f"Bundesland {bundesland} noch nicht unterstützt"}
|
return {"error": f"Bundesland {bundesland} noch nicht unterstützt"}
|
||||||
@ -472,8 +500,8 @@ async def run_drucksache_analysis(
|
|||||||
html_path = settings.reports_dir / f"{job_id}.html"
|
html_path = settings.reports_dir / f"{job_id}.html"
|
||||||
pdf_path = settings.reports_dir / f"{job_id}.pdf"
|
pdf_path = settings.reports_dir / f"{job_id}.pdf"
|
||||||
|
|
||||||
await generate_html_report(assessment, html_path)
|
await generate_html_report(assessment, html_path, bundesland=bundesland)
|
||||||
await generate_pdf_report(assessment, pdf_path)
|
await generate_pdf_report(assessment, pdf_path, bundesland=bundesland)
|
||||||
|
|
||||||
await update_job(
|
await update_job(
|
||||||
job_id,
|
job_id,
|
||||||
@ -492,11 +520,26 @@ async def run_drucksache_analysis(
|
|||||||
# API: List available Bundesländer
|
# API: List available Bundesländer
|
||||||
@app.get("/api/bundeslaender")
|
@app.get("/api/bundeslaender")
|
||||||
async def list_bundeslaender():
|
async def list_bundeslaender():
|
||||||
"""List available bundesländer with their status."""
|
"""List available bundesländer with their status.
|
||||||
return [
|
|
||||||
{"code": bl.code, "name": bl.name, "active": bl.aktiv}
|
Includes the synthetic "ALL" / Bundesweit entry as the first item so
|
||||||
for bl in alle_bundeslaender()
|
that the frontend can render it directly. ``parlament_name`` is added
|
||||||
]
|
so the detail view can show the source parliament without an extra
|
||||||
|
backend round-trip.
|
||||||
|
"""
|
||||||
|
out = [{
|
||||||
|
"code": "ALL",
|
||||||
|
"name": "🌍 Bundesweit",
|
||||||
|
"parlament_name": None,
|
||||||
|
"active": True,
|
||||||
|
}]
|
||||||
|
out.extend({
|
||||||
|
"code": bl.code,
|
||||||
|
"name": bl.name,
|
||||||
|
"parlament_name": bl.parlament_name,
|
||||||
|
"active": bl.aktiv,
|
||||||
|
} for bl in alle_bundeslaender())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# === Quellen / Programme ===
|
# === Quellen / Programme ===
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from typing import Optional
|
|||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from .models import Assessment, MATRIX_LABELS, EMPFEHLUNG_CONFIG
|
from .models import Assessment, MATRIX_LABELS, EMPFEHLUNG_CONFIG
|
||||||
|
from .bundeslaender import BUNDESLAENDER
|
||||||
|
|
||||||
# ECOnGOOD Colors
|
# ECOnGOOD Colors
|
||||||
COLORS = {
|
COLORS = {
|
||||||
@ -93,11 +94,26 @@ def build_matrix_html(assessment: Assessment) -> str:
|
|||||||
return '\n'.join(html)
|
return '\n'.join(html)
|
||||||
|
|
||||||
|
|
||||||
async def generate_html_report(assessment: Assessment, output_path: Path) -> None:
|
async def generate_html_report(
|
||||||
"""Generate HTML report."""
|
assessment: Assessment,
|
||||||
|
output_path: Path,
|
||||||
|
bundesland: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Generate HTML report.
|
||||||
|
|
||||||
|
``bundesland`` is the optional state code (e.g. ``"NRW"``, ``"LSA"``).
|
||||||
|
When set and known in ``BUNDESLAENDER``, the resulting report carries
|
||||||
|
the parlament name in its header so the source parliament is always
|
||||||
|
visible — important since assessments from multiple bundesländer share
|
||||||
|
the same Drucksachen-ID space.
|
||||||
|
"""
|
||||||
|
|
||||||
empf_config = EMPFEHLUNG_CONFIG.get(assessment.empfehlung.value, {})
|
empf_config = EMPFEHLUNG_CONFIG.get(assessment.empfehlung.value, {})
|
||||||
|
|
||||||
|
parlament_name = ""
|
||||||
|
if bundesland and bundesland in BUNDESLAENDER:
|
||||||
|
parlament_name = BUNDESLAENDER[bundesland].parlament_name
|
||||||
|
|
||||||
html = f"""<!DOCTYPE html>
|
html = f"""<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
@ -142,6 +158,14 @@ async def generate_html_report(assessment: Assessment, output_path: Path) -> Non
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
.header-parlament {{
|
||||||
|
font-size: 9pt;
|
||||||
|
color: var(--color-blue);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}}
|
||||||
|
|
||||||
h1 {{
|
h1 {{
|
||||||
color: var(--color-darkgray);
|
color: var(--color-darkgray);
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
@ -327,6 +351,7 @@ async def generate_html_report(assessment: Assessment, output_path: Path) -> Non
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-label">GEMEINWOHL-ÖKONOMIE | ANTRAGSBEWERTUNG</div>
|
<div class="header-label">GEMEINWOHL-ÖKONOMIE | ANTRAGSBEWERTUNG</div>
|
||||||
<h1>{assessment.title}</h1>
|
<h1>{assessment.title}</h1>
|
||||||
|
{f'<div class="header-parlament">{parlament_name}</div>' if parlament_name else ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="meta-box">
|
<div class="meta-box">
|
||||||
@ -414,11 +439,19 @@ async def generate_html_report(assessment: Assessment, output_path: Path) -> Non
|
|||||||
output_path.write_text(html)
|
output_path.write_text(html)
|
||||||
|
|
||||||
|
|
||||||
async def generate_pdf_report(assessment: Assessment, output_path: Path) -> None:
|
async def generate_pdf_report(
|
||||||
"""Generate PDF report using WeasyPrint."""
|
assessment: Assessment,
|
||||||
|
output_path: Path,
|
||||||
|
bundesland: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Generate PDF report using WeasyPrint.
|
||||||
|
|
||||||
|
``bundesland`` is forwarded to ``generate_html_report`` so the source
|
||||||
|
parlament name appears in the report header.
|
||||||
|
"""
|
||||||
# First generate HTML
|
# First generate HTML
|
||||||
html_path = output_path.with_suffix('.tmp.html')
|
html_path = output_path.with_suffix('.tmp.html')
|
||||||
await generate_html_report(assessment, html_path)
|
await generate_html_report(assessment, html_path, bundesland=bundesland)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from weasyprint import HTML
|
from weasyprint import HTML
|
||||||
|
|||||||
@ -171,6 +171,31 @@
|
|||||||
color: var(--color-blue);
|
color: var(--color-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bundesland-Badge: Im Listen-Item links neben der Drucksachen-Nummer.
|
||||||
|
Im Bundesland-spezifischen Modus per data-mode="single" am Container
|
||||||
|
ausgeblendet (redundant, da alle Einträge demselben Land zugehören). */
|
||||||
|
.bl-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 6px;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--color-blue);
|
||||||
|
border: 1px solid var(--color-blue);
|
||||||
|
border-radius: 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.list-content[data-mode="single"] .bl-badge { display: none; }
|
||||||
|
|
||||||
|
/* Detail-Header: Parlament-Name unter dem Titel, vor der Drucksache-Zeile */
|
||||||
|
.detail-parlament {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.2rem 0 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.list-item-score {
|
.list-item-score {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -698,6 +723,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stats-bar" style="padding: 0.5rem 1rem; gap: 1rem; flex-wrap: wrap; align-items: center;">
|
<div class="stats-bar" style="padding: 0.5rem 1rem; gap: 1rem; flex-wrap: wrap; align-items: center;">
|
||||||
<span style="font-size: 0.8rem;"><strong id="stat-total">0</strong> geprüft · <strong id="stat-high">0</strong> vorbildlich · Ø <strong id="stat-avg">0</strong></span>
|
<span style="font-size: 0.8rem;"><strong id="stat-total">0</strong> geprüft · <strong id="stat-high">0</strong> vorbildlich · Ø <strong id="stat-avg">0</strong></span>
|
||||||
|
<span id="bundesland-stats" style="font-size: 0.8rem; color: var(--color-darkgray); display: none; gap: 0.6rem; flex-wrap: wrap;"></span>
|
||||||
<span style="color: var(--color-lightgray);">|</span>
|
<span style="color: var(--color-lightgray);">|</span>
|
||||||
<span id="partei-stats" style="font-size: 0.8rem; display: flex; gap: 0.75rem; flex-wrap: wrap;"></span>
|
<span id="partei-stats" style="font-size: 0.8rem; display: flex; gap: 0.75rem; flex-wrap: wrap;"></span>
|
||||||
</div>
|
</div>
|
||||||
@ -770,8 +796,9 @@
|
|||||||
<div style="margin-top: 1rem;">
|
<div style="margin-top: 1rem;">
|
||||||
<label>Bundesland:</label>
|
<label>Bundesland:</label>
|
||||||
<select id="bundesland" style="padding: 0.5rem; margin-left: 0.5rem;">
|
<select id="bundesland" style="padding: 0.5rem; margin-left: 0.5rem;">
|
||||||
{% for bl in bundeslaender %}
|
<option value="" disabled selected>— Bundesland wählen —</option>
|
||||||
<option value="{{ bl.code }}" {% if bl.active %}selected{% endif %} {% if not bl.active %}disabled{% endif %}>
|
{% for bl in bundeslaender if bl.code != 'ALL' %}
|
||||||
|
<option value="{{ bl.code }}" {% if not bl.active %}disabled{% endif %}>
|
||||||
{{ bl.name }}{% if not bl.active %} (bald){% endif %}
|
{{ bl.name }}{% if not bl.active %} (bald){% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -798,14 +825,39 @@
|
|||||||
let allAssessments = [];
|
let allAssessments = [];
|
||||||
let currentScoreFilter = 'all';
|
let currentScoreFilter = 'all';
|
||||||
let currentParteiFilter = '';
|
let currentParteiFilter = '';
|
||||||
let currentBundesland = 'NRW';
|
let currentBundesland = 'ALL';
|
||||||
let searchTimeout = null;
|
let searchTimeout = null;
|
||||||
let isSearching = false;
|
let isSearching = false;
|
||||||
let selectedTags = new Set();
|
let selectedTags = new Set();
|
||||||
let allTags = {};
|
let allTags = {};
|
||||||
|
|
||||||
// Load assessments on page load
|
// Map code → parlament_name, vom Backend mit dem Initial-Render geliefert.
|
||||||
|
// Wird im Detail-Header und im Listen-Item-Badge-Tooltip verwendet.
|
||||||
|
const PARLAMENT_NAMES = {{ parlament_names | tojson }};
|
||||||
|
|
||||||
|
// Load assessments on page load — localStorage-Auswahl wiederherstellen
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const saved = localStorage.getItem('selectedBundesland');
|
||||||
|
const select = document.getElementById('bundesland-select');
|
||||||
|
if (saved) {
|
||||||
|
// Validieren: existiert die Option?
|
||||||
|
const exists = Array.from(select.options).some(
|
||||||
|
o => o.value === saved && !o.disabled
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
currentBundesland = saved;
|
||||||
|
select.value = saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Modus-Klasse für CSS (Badges aus/an)
|
||||||
|
document.getElementById('list-content').dataset.mode =
|
||||||
|
(currentBundesland === 'ALL') ? 'all' : 'single';
|
||||||
|
// Landtag-Button-State für Initial-Auswahl
|
||||||
|
const btnLandtag = document.getElementById('btn-landtag');
|
||||||
|
if (currentBundesland === 'ALL') {
|
||||||
|
btnLandtag.disabled = true;
|
||||||
|
btnLandtag.title = 'Landtag-Suche nur mit konkretem Bundesland möglich';
|
||||||
|
}
|
||||||
loadAssessments();
|
loadAssessments();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -944,7 +996,8 @@
|
|||||||
|
|
||||||
async function loadAssessments() {
|
async function loadAssessments() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/assessments');
|
const url = `/api/assessments?bundesland=${encodeURIComponent(currentBundesland)}`;
|
||||||
|
const resp = await fetch(url);
|
||||||
allAssessments = await resp.json();
|
allAssessments = await resp.json();
|
||||||
updateStats();
|
updateStats();
|
||||||
renderList(allAssessments);
|
renderList(allAssessments);
|
||||||
@ -965,6 +1018,35 @@
|
|||||||
document.getElementById('stat-high').textContent = high;
|
document.getElementById('stat-high').textContent = high;
|
||||||
document.getElementById('stat-avg').textContent = avg;
|
document.getElementById('stat-avg').textContent = avg;
|
||||||
|
|
||||||
|
// Pro-Bundesland-Aufschlüsselung — nur im Bundesweit-Modus, und nur
|
||||||
|
// wenn tatsächlich mehr als ein Bundesland in der Liste vorkommt.
|
||||||
|
const blContainer = document.getElementById('bundesland-stats');
|
||||||
|
if (currentBundesland === 'ALL') {
|
||||||
|
const blStats = {};
|
||||||
|
allAssessments.forEach(a => {
|
||||||
|
if (a.gwoeScore == null || !a.bundesland) return;
|
||||||
|
if (!blStats[a.bundesland]) blStats[a.bundesland] = { sum: 0, count: 0 };
|
||||||
|
blStats[a.bundesland].sum += a.gwoeScore;
|
||||||
|
blStats[a.bundesland].count += 1;
|
||||||
|
});
|
||||||
|
const codes = Object.keys(blStats);
|
||||||
|
if (codes.length > 1) {
|
||||||
|
const sortedBl = codes
|
||||||
|
.map(c => ({ code: c, avg: blStats[c].sum / blStats[c].count, count: blStats[c].count }))
|
||||||
|
.sort((a, b) => b.avg - a.avg);
|
||||||
|
blContainer.innerHTML = sortedBl.map(b =>
|
||||||
|
`<span title="${PARLAMENT_NAMES[b.code] || b.code}">Ø <strong>${b.code}</strong> ${b.avg.toFixed(1)} <span style="color:#888">(n=${b.count})</span></span>`
|
||||||
|
).join(' · ');
|
||||||
|
blContainer.style.display = 'inline-flex';
|
||||||
|
} else {
|
||||||
|
blContainer.style.display = 'none';
|
||||||
|
blContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blContainer.style.display = 'none';
|
||||||
|
blContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Partei-Durchschnitte berechnen
|
// Partei-Durchschnitte berechnen
|
||||||
const parteiStats = {};
|
const parteiStats = {};
|
||||||
allAssessments.forEach(a => {
|
allAssessments.forEach(a => {
|
||||||
@ -1006,10 +1088,13 @@
|
|||||||
const themen = (item.themen || []).slice(0, 3);
|
const themen = (item.themen || []).slice(0, 3);
|
||||||
const scoreText = isUnchecked ? '⏳' : `${item.gwoeScore}/10`;
|
const scoreText = isUnchecked ? '⏳' : `${item.gwoeScore}/10`;
|
||||||
|
|
||||||
|
const blBadge = item.bundesland
|
||||||
|
? `<span class="bl-badge" title="${PARLAMENT_NAMES[item.bundesland] || item.bundesland}">${item.bundesland}</span>`
|
||||||
|
: '';
|
||||||
return `
|
return `
|
||||||
<div class="list-item ${isUnchecked ? 'unchecked' : ''}" data-drucksache="${item.drucksache}" onclick="${isUnchecked ? '' : `showDetail('${item.drucksache}')`}">
|
<div class="list-item ${isUnchecked ? 'unchecked' : ''}" data-drucksache="${item.drucksache}" onclick="${isUnchecked ? '' : `showDetail('${item.drucksache}')`}">
|
||||||
<div class="list-item-header">
|
<div class="list-item-header">
|
||||||
<span class="list-item-id">${item.drucksache}</span>
|
<span class="list-item-id">${blBadge}${item.drucksache}</span>
|
||||||
<span class="list-item-score ${scoreClass}">${scoreText}</span>
|
<span class="list-item-score ${scoreClass}">${scoreText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-item-title">${item.title || 'Ohne Titel'}</div>
|
<div class="list-item-title">${item.title || 'Ohne Titel'}</div>
|
||||||
@ -1133,11 +1218,53 @@
|
|||||||
|
|
||||||
function changeBundesland(code) {
|
function changeBundesland(code) {
|
||||||
currentBundesland = code;
|
currentBundesland = code;
|
||||||
|
localStorage.setItem('selectedBundesland', code);
|
||||||
|
|
||||||
|
// Filter zurücksetzen — Parteien & Tags pro Bundesland unterschiedlich,
|
||||||
|
// ein "LINKE"-Filter aus LSA würde in NRW eine leere Liste zeigen.
|
||||||
|
currentScoreFilter = 'all';
|
||||||
|
currentParteiFilter = '';
|
||||||
|
selectedTags.clear();
|
||||||
document.getElementById('search-input').value = '';
|
document.getElementById('search-input').value = '';
|
||||||
|
document.querySelectorAll('.filter-btn').forEach(b => {
|
||||||
|
b.classList.toggle('active', b.dataset.filter === 'all');
|
||||||
|
});
|
||||||
|
const parteiSelect = document.getElementById('partei-filter');
|
||||||
|
if (parteiSelect) parteiSelect.value = '';
|
||||||
|
|
||||||
|
// Upload-Mode-Dropdown synchronisieren. Bei "ALL" KEIN automatischer
|
||||||
|
// Default — der User soll im Upload-Form bewusst ein Bundesland wählen.
|
||||||
|
const uploadDropdown = document.getElementById('bundesland');
|
||||||
|
if (uploadDropdown) {
|
||||||
|
if (code === 'ALL') {
|
||||||
|
uploadDropdown.value = '';
|
||||||
|
} else {
|
||||||
|
uploadDropdown.value = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landtag-Suche-Button im Bundesweit-Modus deaktivieren
|
||||||
|
const btnLandtag = document.getElementById('btn-landtag');
|
||||||
|
if (code === 'ALL') {
|
||||||
|
btnLandtag.disabled = true;
|
||||||
|
btnLandtag.title = 'Landtag-Suche nur mit konkretem Bundesland möglich';
|
||||||
|
} else {
|
||||||
|
btnLandtag.disabled = false;
|
||||||
|
btnLandtag.title = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modus-Klasse für CSS (Badges aus/an im Single-Modus)
|
||||||
|
document.getElementById('list-content').dataset.mode =
|
||||||
|
(code === 'ALL') ? 'all' : 'single';
|
||||||
|
|
||||||
loadAssessments();
|
loadAssessments();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchLandtag() {
|
async function searchLandtag() {
|
||||||
|
if (currentBundesland === 'ALL') {
|
||||||
|
alert('Landtag-Suche ist nur mit Auswahl eines konkreten Bundeslands möglich.\nBitte oben ein Bundesland auswählen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const query = document.getElementById('search-input').value.trim();
|
const query = document.getElementById('search-input').value.trim();
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
alert('Bitte mindestens 2 Zeichen eingeben');
|
alert('Bitte mindestens 2 Zeichen eingeben');
|
||||||
@ -1378,6 +1505,7 @@
|
|||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="detail-title">${item.title || 'Ohne Titel'}</div>
|
<div class="detail-title">${item.title || 'Ohne Titel'}</div>
|
||||||
|
${item.bundesland && PARLAMENT_NAMES[item.bundesland] ? `<div class="detail-parlament">${PARLAMENT_NAMES[item.bundesland]}</div>` : ''}
|
||||||
<div class="detail-id">${item.drucksache} · ${(item.fraktionen || []).join(', ')} · ${item.datum || ''}</div>
|
<div class="detail-id">${item.drucksache} · ${(item.fraktionen || []).join(', ')} · ${item.datum || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-display">
|
<div class="score-display">
|
||||||
@ -1502,6 +1630,10 @@
|
|||||||
alert('Bitte Text eingeben oder PDF hochladen');
|
alert('Bitte Text eingeben oder PDF hochladen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!bundesland) {
|
||||||
|
alert('Bitte ein Bundesland wählen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
statusDiv.style.display = 'block';
|
statusDiv.style.display = 'block';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user