feat: PDF-Generierung auf v3-Layout, Werkstatt-Link im Admin, Share nur bei Login

Drei Aufgaben in einem Schwung:

1. Werkstatt-Link im Admin
   admin_stand bekommt eine Sektion 'Design-Werkstaetten' mit Link auf
   /v2/scorecard-werkstatt — damit Admins den Live-Editor finden ohne
   die URL kennen zu muessen.

2. Share-Block nur fuer angemeldete User
   Der ganze Share-Block (Kopieren, Threads, Mastodon, LinkedIn,
   Instagram, E-Mail, Scorecard, Stock-Bild) bekommt id=v2-share-block
   und wird per initAuth() display:none/block geschaltet — analog zum
   Comment-Form. Default im Markup: display:none, damit Gaeste ihn
   nicht waehrend des Auth-Roundtrips kurz sehen. Funktioniert in v2
   und v3 (gleicher JS-Handler via super-Inheritance).

3. PDF-Layout = v3-Layout
   Neues Template v3/pdf/antrag_pdf.html (single column, A4) reused die
   Visuallogik aus der Online-Detailseite:
   - Score-Hero-Block mit Farb-Tint
   - Matrix 5×5 mit Achsen-Labels (Werte oben, Berührungsgruppen links)
   - Programm-Treue pro Fraktion mit Begruendung + Zitaten + Fallback-
     Hinweis bei fehlenden Zitaten
   - Verbesserungsvorschlaege mit Redline-Format
   - Abstimmungsergebnis (best-effort via get_plenum_votes) inkl.
     Konsistenz-Hinweis

   Online-spezifisches gestrichen: Merken-Button, Vote-treffend, Share,
   Kommentare, News-Box, Reanalyze, Historie, Modals.

   NEU im PDF: 'Schwerpunkte erklaert'-Sektion direkt unter der Matrix.
   Listet die Top-4 positiven und Top-4 negativen Matrix-Felder mit
   ihrem LLM-generierten label + aspect — Ersatz fuer den interaktiven
   Klick, der im PDF nicht funktioniert.

   report.generate_html_report_v3() neu, generate_pdf_report() ruft
   diese statt der alten Inline-HTML-Variante. Alte generate_html_report
   bleibt als Fallback erhalten.

   WeasyPrint rendert mit @page A4, Footer mit Drucksache + Branding +
   Seitenzahl 'Seite X von Y'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 13:48:19 +02:00
parent 8ae2b92313
commit 79e7937d51
5 changed files with 877 additions and 12 deletions

View File

@ -14,11 +14,21 @@ from html import escape as _e
from pathlib import Path
from typing import Optional
from jinja2 import Environment, FileSystemLoader, select_autoescape
logger = logging.getLogger(__name__)
from .models import Assessment, MATRIX_LABELS, EMPFEHLUNG_CONFIG
from .bundeslaender import BUNDESLAENDER
# Eigene Jinja-Env fuer PDF-Templates (separat von Starlette templates,
# weil report.py auch von Hintergrund-Jobs ohne FastAPI-Request laufen muss).
_TEMPLATE_DIR = Path(__file__).parent / "templates"
_pdf_jinja = Environment(
loader=FileSystemLoader(str(_TEMPLATE_DIR)),
autoescape=select_autoescape(["html"]),
)
# ECOnGOOD Colors
COLORS = {
"darkgray": "#5a5a5a",
@ -512,6 +522,90 @@ async def generate_html_report(
output_path.write_text(html)
async def generate_html_report_v3(
assessment: Assessment,
output_path: Path,
bundesland: Optional[str] = None,
) -> None:
"""Render Antrags-PDF im neuen v3-Layout (single column, A4 portrait).
Reuses die v3-Layout-Logik (Score-Hero, Matrix mit Achsen-Labels,
Programm-Treue, Verbesserungen) und ergaenzt sie um die im PDF
notwendigen Adaptionen:
- Kein interaktiver Matrix-Klick "Schwerpunkte erklaert"-Sektion
listet die Top-3 positiven und Top-3 negativen Felder mit ihren
LLM-generierten label/aspect-Texten unter der Matrix.
- Plenum-Votes werden best-effort geladen, inkl. Konsistenz-Hinweis
(Mehrheit deckt sich / gegen GWOE-Empfehlung).
- Online-Elemente (Share, Vote-treffend, Kommentare, News, Modals)
sind im Template gar nicht erst angelegt.
Template: app/templates/v3/pdf/antrag_pdf.html
"""
matrix_lookup = {e.field: {"rating": e.rating} for e in assessment.gwoe_matrix}
# Schwerpunkt-Felder mit Erklaerung: Top + Bottom Ratings.
sorted_matrix = sorted(
assessment.gwoe_matrix, key=lambda e: e.rating, reverse=True
)
matrix_top = [
{"field": e.field, "label": e.label, "aspect": e.aspect, "rating": e.rating}
for e in sorted_matrix if e.rating > 0
][:4]
matrix_bottom = [
{"field": e.field, "label": e.label, "aspect": e.aspect, "rating": e.rating}
for e in sorted(sorted_matrix, key=lambda e: e.rating) if e.rating < 0
][:4]
# Score-Color (gleich wie Scorecard)
s = assessment.gwoe_score
if s >= 8: score_color = "#1a7f37"
elif s >= 5: score_color = "#bf6c10"
else: score_color = "#9a2a2a"
parlament_name = ""
if bundesland and bundesland in BUNDESLAENDER:
parlament_name = BUNDESLAENDER[bundesland].parlament_name
# Plenum-Votes best-effort (Hintergrund-Job kann ohne DB-Pfad laufen,
# in dem Fall einfach keine Votes anzeigen).
plenum_votes: list[dict] = []
konsistenz_state: Optional[str] = None
konsistenz_decisive: Optional[str] = None
try:
from .database import get_plenum_votes
plenum_votes = await get_plenum_votes(
bundesland or "NRW", assessment.drucksache,
)
if plenum_votes:
from .marker import consistency_state, decisive_outcome
konsistenz_state = consistency_state(
assessment.empfehlung.value, plenum_votes,
)
konsistenz_decisive = decisive_outcome(plenum_votes)
except Exception as exc:
logger.warning(
"Plenum-Votes fuer PDF nicht ladbar (drucksache=%s): %s",
assessment.drucksache, exc,
)
template = _pdf_jinja.get_template("v3/pdf/antrag_pdf.html")
html = template.render(
assessment=assessment,
matrix_lookup=matrix_lookup,
matrix_top=matrix_top,
matrix_bottom=matrix_bottom,
score_color=score_color,
parlament_name=parlament_name,
bundesland=bundesland or "",
plenum_votes=plenum_votes,
konsistenz_state=konsistenz_state,
konsistenz_decisive=konsistenz_decisive,
)
output_path.write_text(html)
async def generate_pdf_report(
assessment: Assessment,
output_path: Path,
@ -535,9 +629,10 @@ async def generate_pdf_report(
``bundesland`` is forwarded to ``generate_html_report`` so the source
parlament name appears in the report header.
"""
# Step 1 — render the report itself
# Step 1 — render the report itself, neues v3-Layout (single column,
# Score-Hero, Matrix mit Achsen-Labels, Schwerpunkte-erklaert).
html_path = output_path.with_suffix('.tmp.html')
await generate_html_report(assessment, html_path, bundesland=bundesland)
await generate_html_report_v3(assessment, html_path, bundesland=bundesland)
try:
from weasyprint import HTML

View File

@ -180,6 +180,16 @@
<p id="alt-views-sample" style="font-family:var(--font-mono);font-size:12px;line-height:1.8;">
Lade aktuelles Beispiel …
</p>
<h3 class="v2-h3" style="margin-top:24px;">Design-Werkstätten</h3>
<p style="font-family:var(--font-mono);font-size:12px;opacity:0.75;line-height:1.6;">
Live-Editoren für Layout-Iteration ohne Server-Redeploy:
</p>
<ul style="font-family:var(--font-mono);font-size:12px;line-height:1.8;">
<li><a href="/v2/scorecard-werkstatt">Scorecard-Werkstatt</a>
Live-Vorschau aller Card-Formate (Portrait / Square / OG) mit
CSS-Editor und Embed-Link-Generator.</li>
</ul>
</div>
{% endblock %}

View File

@ -484,8 +484,9 @@
</div>
</div>
{# ── Share-Block (analog v1) ───────────────────────────────────── #}
<div style="margin-top:20px;">
{# ── Share-Block (analog v1) — nur fuer angemeldete User sichtbar,
wird in initAuth() ein-/ausgeblendet (display:none default). ── #}
<div id="v2-share-block" style="margin-top:20px;display:none;">
<div style="font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:8px;">Teilen</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button onclick="v2DetailShareCopy()"
@ -725,14 +726,17 @@ window.v2ShowMatrixFieldInfo = function(field) {
currentUser = (data && data.authenticated) ? data : null;
} catch (_) { currentUser = null; }
var form = document.getElementById('v2-comment-form');
var loginHint = document.getElementById('v2-comment-login-hint');
var form = document.getElementById('v2-comment-form');
var loginHint = document.getElementById('v2-comment-login-hint');
var shareBlock = document.getElementById('v2-share-block');
if (currentUser) {
if (form) form.style.display = 'block';
if (loginHint) loginHint.style.display = 'none';
if (form) form.style.display = 'block';
if (loginHint) loginHint.style.display = 'none';
if (shareBlock) shareBlock.style.display = ''; /* default-display, nicht 'block' (v3-rest-block ist flex/grid je nach Klasse) */
} else {
if (form) form.style.display = 'none';
if (loginHint) loginHint.style.display = 'block';
if (form) form.style.display = 'none';
if (loginHint) loginHint.style.display = 'block';
if (shareBlock) shareBlock.style.display = 'none';
}
loadComments();
loadVotes();

View File

@ -0,0 +1,756 @@
{# ─────────────────────────────────────────────────────────────────────
v3/pdf/antrag_pdf.html — GWÖ-Antragsprüfung als druckbares PDF.
Wiederverwendet die v3-Layout-Logik (Single Column, Sektions-Reihen-
folge), kappt aber alle Online-Elemente:
raus: Merken, Vote-treffend, Share-Block, News-Box, Kommentare,
Modals, Reanalyze, Historie, Cluster
drin: Header, Titel, Themen, Zusammenfassung+Kernpunkte, Bewertung,
Stärken/Schwächen, Matrix mit Achsen-Labels, NEU: Erklärung
der Schwerpunkt-Felder, Programm-Treue, Verbesserungen,
Abstimmungsergebnis.
Wird von report.py via Jinja gerendert; WeasyPrint produziert A4 PDF.
───────────────────────────────────────────────────────────────────── #}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>GWÖ-Antragsprüfung — {{ assessment.title }}</title>
<style>
@page {
size: A4;
margin: 18mm 16mm 22mm 16mm;
@bottom-left {
content: "{{ assessment.drucksache }}";
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
color: #888;
}
@bottom-center {
content: "gwoe.toppyr.de · GWÖ-Antragsprüfer · automatische Gemeinwohl-Bilanzierung";
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
color: #888;
}
@bottom-right {
content: "Seite " counter(page) " von " counter(pages);
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
color: #888;
}
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { font-family: 'Avenir Next', 'Avenir', 'Helvetica Neue', Arial, sans-serif; }
body {
color: #1f1f1f;
font-size: 10pt;
line-height: 1.5;
}
/* ── Header ── */
.pdf-header {
border-bottom: 2px solid #009DA5;
padding-bottom: 8pt;
margin-bottom: 12pt;
}
.pdf-kicker {
font-family: 'Source Code Pro', monospace;
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #009DA5;
margin-bottom: 4pt;
}
.pdf-title {
font-size: 18pt;
font-weight: 700;
line-height: 1.18;
color: #1f1f1f;
margin: 0 0 6pt;
}
.pdf-meta {
font-family: 'Source Code Pro', monospace;
font-size: 9pt;
color: #555;
line-height: 1.5;
}
.pdf-meta .pill {
display: inline-block;
padding: 1pt 6pt;
background: rgba(136, 158, 51, 0.18);
color: #44570a;
border-radius: 2pt;
font-weight: 700;
margin-left: 4pt;
}
.pdf-themen {
margin-top: 6pt;
}
.pdf-theme-chip {
display: inline-block;
padding: 1pt 6pt;
border: 0.5pt solid #cccfb8;
border-radius: 2pt;
background: #fff;
color: #444;
font-family: 'Source Code Pro', monospace;
font-size: 8.5pt;
margin-right: 4pt;
}
/* ── Sektionen ── */
.pdf-section {
margin: 14pt 0 0;
page-break-inside: avoid;
}
.pdf-h2 {
font-family: 'Source Code Pro', monospace;
font-size: 9pt;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #009DA5;
margin: 0 0 6pt;
}
p { margin: 0 0 6pt; }
/* ── Zusammenfassung + Kernpunkte ── */
.pdf-prose {
font-size: 10pt;
line-height: 1.55;
}
.pdf-kernpunkte {
margin-top: 8pt;
padding-top: 6pt;
border-top: 0.5pt dashed #ccc;
}
.pdf-kernpunkte-label {
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #009DA5;
margin-bottom: 4pt;
font-weight: 700;
}
.pdf-kernpunkte ul {
margin: 0;
padding-left: 14pt;
}
.pdf-kernpunkte li {
margin-bottom: 2pt;
}
/* ── Bewertungs-Block ── */
.pdf-bewertung {
background:
{% if score_color == '#1a7f37' %}#e8eed1
{% elif score_color == '#bf6c10' %}#f4e6cf
{% else %}#f1dcda{% endif %};
border-left: 4pt solid {{ score_color }};
border-radius: 2pt;
padding: 12pt 14pt;
display: flex;
align-items: center;
gap: 16pt;
}
.pdf-score-num {
font-family: 'Source Code Pro', monospace;
font-size: 36pt;
font-weight: 700;
line-height: 1;
color: {{ score_color }};
flex-shrink: 0;
}
.pdf-score-num .slash { font-size: 16pt; opacity: 0.5; }
.pdf-score-side { flex: 1; }
.pdf-score-label {
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #555;
margin-bottom: 3pt;
}
.pdf-verdict {
font-size: 14pt;
font-weight: 700;
color: {{ score_color }};
line-height: 1.15;
}
.pdf-bewertung-body {
font-size: 9.5pt;
line-height: 1.55;
color: #333;
margin-top: 8pt;
}
/* ── Stärken / Schwächen ── */
.pdf-werte-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10pt;
}
.pdf-werte-card {
border-left: 3pt solid;
padding: 8pt 10pt;
border-radius: 2pt;
}
.pdf-werte-card.staerke { border-left-color: #889E33; background: rgba(136, 158, 51, 0.08); }
.pdf-werte-card.schwaeche { border-left-color: #009DA5; background: rgba(0, 157, 165, 0.08); }
.pdf-werte-card h4 {
font-size: 9pt;
margin-bottom: 4pt;
color: #1f1f1f;
font-weight: 700;
}
.pdf-werte-card p {
font-size: 9.5pt;
margin: 0;
line-height: 1.5;
}
.pdf-werte-card ul {
margin: 0;
padding-left: 14pt;
font-size: 9.5pt;
}
/* ── Matrix mit Achsen ── */
.pdf-matrix-block {
page-break-inside: avoid;
}
.pdf-matrix-grid {
display: grid;
grid-template-columns: 90pt repeat(5, 60pt);
grid-template-rows: 24pt repeat(5, 60pt);
gap: 2pt;
}
.pdf-matrix-grid .col-label {
font-family: 'Source Code Pro', monospace;
font-size: 7pt;
font-weight: 700;
color: #009DA5;
text-transform: uppercase;
letter-spacing: 0.04em;
display: flex;
align-items: end;
justify-content: center;
padding-bottom: 3pt;
text-align: center;
line-height: 1.1;
}
.pdf-matrix-grid .row-label {
font-family: 'Source Code Pro', monospace;
font-size: 7.5pt;
font-weight: 700;
color: #009DA5;
text-transform: uppercase;
letter-spacing: 0.03em;
display: flex;
align-items: center;
padding-right: 5pt;
line-height: 1.15;
}
.pdf-matrix-grid .cell {
border-radius: 1pt;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Source Code Pro', monospace;
font-size: 13pt;
font-weight: 700;
}
.cell.r-pp { background: #889E33; color: #fff; }
.cell.r-p { background: #cddaa1; color: #44570a; }
.cell.r-0 { background: #b8b8b2; color: #4a4a44; }
.cell.r-n { background: #efc9c3; color: #931515; }
.cell.r-nn { background: #9A2A2A; color: #fff; }
.pdf-matrix-legend {
margin-top: 6pt;
display: flex;
flex-wrap: wrap;
gap: 4pt 14pt;
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
color: #444;
}
.pdf-matrix-legend .legend-item {
display: inline-flex;
align-items: center;
gap: 4pt;
}
.pdf-matrix-legend .swatch {
width: 12pt; height: 12pt; border-radius: 1pt; flex-shrink: 0;
}
/* ── Schwerpunkt-Felder-Erklaerung (Ersatz fuer Klick im PDF) ── */
.pdf-schwerpunkt-list {
margin-top: 8pt;
}
.pdf-schwerpunkt-item {
border-left: 2pt solid;
padding: 6pt 10pt;
margin-bottom: 5pt;
background: #fafaf7;
border-radius: 1pt;
}
.pdf-schwerpunkt-item.pos { border-left-color: #889E33; }
.pdf-schwerpunkt-item.neg { border-left-color: #9A2A2A; }
.pdf-schwerpunkt-item.neu { border-left-color: #b8b8b2; }
.pdf-schwerpunkt-head {
display: flex;
align-items: baseline;
gap: 8pt;
margin-bottom: 3pt;
}
.pdf-schwerpunkt-code {
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
font-weight: 700;
padding: 1pt 5pt;
border-radius: 2pt;
color: #fff;
background: #009DA5;
}
.pdf-schwerpunkt-rating {
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
font-weight: 700;
color: #555;
}
.pdf-schwerpunkt-label {
font-size: 9.5pt;
font-weight: 700;
color: #1f1f1f;
}
.pdf-schwerpunkt-aspect {
font-size: 9pt;
line-height: 1.5;
color: #333;
}
/* ── Programm-Treue ── */
.pdf-fraktion {
border: 0.5pt solid #ddd;
border-radius: 2pt;
padding: 8pt 10pt;
margin-bottom: 8pt;
page-break-inside: avoid;
background: #fafaf7;
}
.pdf-fraktion-head {
display: flex;
align-items: baseline;
gap: 6pt;
flex-wrap: wrap;
margin-bottom: 5pt;
}
.pdf-fraktion-name {
font-size: 11pt;
font-weight: 700;
}
.pdf-pill {
font-family: 'Source Code Pro', monospace;
font-size: 7pt;
text-transform: uppercase;
padding: 1pt 5pt;
border-radius: 1pt;
}
.pdf-pill.antrag { background: rgba(247, 148, 29, 0.18); color: #bf6c10; }
.pdf-pill.reg { background: rgba(0, 157, 165, 0.18); color: #1e6a90; }
.pdf-prog {
margin-top: 5pt;
padding-top: 5pt;
border-top: 0.5pt dashed #ccc;
}
.pdf-prog:first-of-type { border-top: none; padding-top: 0; }
.pdf-prog-row {
display: flex;
align-items: baseline;
gap: 8pt;
margin-bottom: 3pt;
}
.pdf-prog-label {
font-family: 'Source Code Pro', monospace;
font-size: 8.5pt;
font-weight: 700;
text-transform: uppercase;
color: #0d6f76;
letter-spacing: 0.04em;
}
.pdf-prog-spacer { flex: 1; }
.pdf-prog-score {
font-family: 'Source Code Pro', monospace;
font-size: 8.5pt;
font-weight: 700;
padding: 1pt 5pt;
border-radius: 2pt;
}
.pdf-prog-score.good { background: #cddaa1; color: #44570a; }
.pdf-prog-score.mid { background: #fff3cd; color: #7d5a00; }
.pdf-prog-score.low { background: #efc9c3; color: #a00000; }
.pdf-prog-text {
font-size: 9.5pt;
line-height: 1.5;
color: #333;
margin-bottom: 3pt;
}
.pdf-prog-zitat {
border-left: 2pt solid #009DA5;
background: #f3f8f8;
padding: 4pt 8pt;
margin: 3pt 0;
font-size: 9pt;
line-height: 1.4;
color: #333;
}
.pdf-prog-zitat .src {
display: block;
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
color: #555;
margin-top: 2pt;
}
.pdf-no-zitate {
font-size: 8.5pt;
line-height: 1.5;
color: #777;
font-style: italic;
background: #f5f5f5;
border-left: 2pt dashed #ccc;
padding: 4pt 8pt;
border-radius: 0 2pt 2pt 0;
}
/* ── Verbesserungsvorschlaege (Redline) ── */
.pdf-verbesserung {
margin-bottom: 10pt;
padding: 6pt 0;
page-break-inside: avoid;
}
.pdf-verbesserung-num {
font-family: 'Source Code Pro', monospace;
font-size: 8pt;
color: #555;
margin-bottom: 4pt;
}
.pdf-redline {
font-size: 9.5pt;
line-height: 1.5;
background: #fafaf7;
padding: 6pt 8pt;
border-radius: 2pt;
}
.pdf-redline .ins { background: #cddaa1; padding: 0 2pt; }
.pdf-redline .del { background: #efc9c3; text-decoration: line-through; padding: 0 2pt; }
.pdf-redline-original { font-size: 9pt; color: #555; margin-bottom: 4pt; }
.pdf-verbesserung-begr {
font-size: 8.5pt;
color: #555;
line-height: 1.5;
margin-top: 4pt;
font-style: italic;
}
/* ── Abstimmungsergebnis ── */
.pdf-vote-card {
border: 0.5pt solid #ddd;
border-radius: 2pt;
padding: 6pt 8pt;
margin-bottom: 5pt;
background: #fafaf7;
page-break-inside: avoid;
}
.pdf-vote-head {
font-size: 10pt;
font-weight: 700;
margin-bottom: 4pt;
}
.pdf-vote-pills {
display: flex;
flex-wrap: wrap;
gap: 6pt 12pt;
font-family: 'Source Code Pro', monospace;
font-size: 8.5pt;
}
.pdf-vote-side { font-weight: 700; }
.pdf-vote-pill {
display: inline-block;
padding: 1pt 5pt;
border-radius: 1pt;
margin-right: 2pt;
}
.pdf-vote-pill.ja { background: rgba(45, 164, 78, 0.15); color: #1a7f37; }
.pdf-vote-pill.nein { background: rgba(207, 34, 46, 0.15); color: #a40e26; }
.pdf-vote-pill.enth { background: rgba(110, 119, 129, 0.15); color: #57606a; }
.pdf-konsistenz {
font-size: 9pt;
padding: 5pt 8pt;
margin-bottom: 5pt;
border-left: 3pt solid;
border-radius: 2pt;
}
.pdf-konsistenz.conflict {
background: rgba(207, 34, 46, 0.07);
border-left-color: #cf222e;
}
.pdf-konsistenz.match {
background: rgba(45, 164, 78, 0.07);
border-left-color: #2da44e;
}
</style>
</head>
<body>
<!-- ─── Header + Title ─────────────────────────────────────────────── -->
<div class="pdf-header">
<div class="pdf-kicker">GWÖ-Antragsprüfung
{% if parlament_name %} · {{ parlament_name }}{% endif %}
</div>
<h1 class="pdf-title">{{ assessment.title }}</h1>
<div class="pdf-meta">
Drucksache <strong>{{ assessment.drucksache }}</strong>
{% if assessment.datum %} · eingebracht {{ assessment.datum }}{% endif %}
{% if assessment.fraktionen %}
— Antragsteller:
{% for f in assessment.fraktionen %}<span class="pill">{{ f }}</span>{% endfor %}
{% endif %}
</div>
{% if assessment.themen %}
<div class="pdf-themen">
{% for t in assessment.themen %}<span class="pdf-theme-chip">{{ t }}</span>{% endfor %}
</div>
{% endif %}
</div>
<!-- ─── Zusammenfassung + Kernpunkte ────────────────────────────────── -->
{% if assessment.antrag_zusammenfassung or assessment.antrag_kernpunkte %}
<div class="pdf-section">
<h2 class="pdf-h2">Zusammenfassung</h2>
{% if assessment.antrag_zusammenfassung %}
<p class="pdf-prose">{{ assessment.antrag_zusammenfassung }}</p>
{% endif %}
{% if assessment.antrag_kernpunkte %}
<div class="pdf-kernpunkte">
<div class="pdf-kernpunkte-label">Kernforderungen</div>
<ul>
{% for kp in assessment.antrag_kernpunkte %}<li>{{ kp }}</li>{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
<!-- ─── Bewertungs-Block ────────────────────────────────────────────── -->
<div class="pdf-section">
<h2 class="pdf-h2">Bewertung</h2>
<div class="pdf-bewertung">
<div class="pdf-score-num">{{ "%.1f"|format(assessment.gwoe_score) }}<span class="slash">/10</span></div>
<div class="pdf-score-side">
<div class="pdf-score-label">Gemeinwohl-Score</div>
<div class="pdf-verdict">{{ assessment.empfehlung.value }}</div>
</div>
</div>
{% if assessment.gwoe_begruendung %}
<p class="pdf-bewertung-body">{{ assessment.gwoe_begruendung }}</p>
{% endif %}
</div>
<!-- ─── Stärken / Schwächen ────────────────────────────────────────── -->
{% if assessment.staerken or assessment.schwaechen %}
<div class="pdf-section">
<h2 class="pdf-h2">Stärken &amp; Schwächen</h2>
<div class="pdf-werte-grid">
{% if assessment.staerken %}
<div class="pdf-werte-card staerke">
<h4>Stärken</h4>
<ul>{% for s in assessment.staerken %}<li>{{ s }}</li>{% endfor %}</ul>
</div>
{% endif %}
{% if assessment.schwaechen %}
<div class="pdf-werte-card schwaeche">
<h4>Schwächen</h4>
<ul>{% for s in assessment.schwaechen %}<li>{{ s }}</li>{% endfor %}</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- ─── Matrix 5×5 mit Achsen-Labels ───────────────────────────────── -->
<div class="pdf-section pdf-matrix-block">
<h2 class="pdf-h2">GWÖ-Matrix 5×5</h2>
<div class="pdf-matrix-grid">
<div></div>
<div class="col-label">Würde</div>
<div class="col-label">Solida­rität</div>
<div class="col-label">Nach­haltig­keit</div>
<div class="col-label">Gerech­tigkeit</div>
<div class="col-label">Trans­parenz</div>
{% set rows = [
('A', 'Lieferant:­innen'),
('B', 'Finanzen'),
('C', 'Verwal­tung'),
('D', 'Bürger:­innen'),
('E', 'Gesell­schaft & Natur'),
] %}
{% for r, r_label in rows %}
<div class="row-label">{{ r }} · {{ r_label }}</div>
{% for c in ['1','2','3','4','5'] %}
{% set cell = matrix_lookup.get(r ~ c, {}) %}
{% set rt = cell.get('rating', 0) %}
<div class="cell {% if rt >= 4 %}r-pp{% elif rt >= 1 %}r-p{% elif rt == 0 %}r-0{% elif rt <= -4 %}r-nn{% else %}r-n{% endif %}">{% if rt >= 4 %}++{% elif rt >= 1 %}+{% elif rt == 0 %}·{% elif rt <= -4 %}{% else %}{% endif %}</div>
{% endfor %}
{% endfor %}
</div>
<div class="pdf-matrix-legend">
<span class="legend-item"><span class="swatch" style="background:#889E33;"></span>++ stark fördernd</span>
<span class="legend-item"><span class="swatch" style="background:#cddaa1;"></span>+ fördernd</span>
<span class="legend-item"><span class="swatch" style="background:#b8b8b2;"></span>○ neutral</span>
<span class="legend-item"><span class="swatch" style="background:#efc9c3;"></span> widersprechend</span>
<span class="legend-item"><span class="swatch" style="background:#9A2A2A;"></span> stark widersprechend</span>
</div>
</div>
<!-- ─── Schwerpunkt-Felder erklärt (Ersatz für Matrix-Klick im PDF) ── -->
{% if matrix_top or matrix_bottom %}
<div class="pdf-section">
<h2 class="pdf-h2">Schwerpunkte erklärt</h2>
<p style="font-size:9pt;color:#555;margin-bottom:6pt;">
Die wichtigsten positiv und negativ wirkenden Bewertungsfelder mit der jeweiligen Begründung.
</p>
<div class="pdf-schwerpunkt-list">
{% for cell in matrix_top %}
<div class="pdf-schwerpunkt-item pos">
<div class="pdf-schwerpunkt-head">
<span class="pdf-schwerpunkt-code">{{ cell.field }}</span>
<span class="pdf-schwerpunkt-label">{{ cell.label }}</span>
<span class="pdf-schwerpunkt-rating">Bewertung: {% if cell.rating > 0 %}+{% endif %}{{ cell.rating }}</span>
</div>
{% if cell.aspect %}<div class="pdf-schwerpunkt-aspect">{{ cell.aspect }}</div>{% endif %}
</div>
{% endfor %}
{% for cell in matrix_bottom %}
<div class="pdf-schwerpunkt-item neg">
<div class="pdf-schwerpunkt-head">
<span class="pdf-schwerpunkt-code">{{ cell.field }}</span>
<span class="pdf-schwerpunkt-label">{{ cell.label }}</span>
<span class="pdf-schwerpunkt-rating">Bewertung: {{ cell.rating }}</span>
</div>
{% if cell.aspect %}<div class="pdf-schwerpunkt-aspect">{{ cell.aspect }}</div>{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- ─── Programm-Treue pro Fraktion ─────────────────────────────────── -->
{% if assessment.wahlprogramm_scores %}
<div class="pdf-section">
<h2 class="pdf-h2">Programm-Treue pro Fraktion</h2>
{% if assessment.fehlende_programme %}
<p style="font-size:9pt;color:#555;margin-bottom:6pt;">
<strong>Hinweis:</strong> Für folgende Parteien lag kein Wahl-/Parteiprogramm vor:
{{ assessment.fehlende_programme | join(", ") }}.
</p>
{% endif %}
{% for fs in assessment.wahlprogramm_scores %}
<div class="pdf-fraktion">
<div class="pdf-fraktion-head">
<span class="pdf-fraktion-name">{{ fs.fraktion }}</span>
{% if fs.ist_antragsteller %}<span class="pdf-pill antrag">Antragsteller:in</span>{% endif %}
{% if fs.ist_regierung %}<span class="pdf-pill reg">Regierungsfraktion</span>{% endif %}
</div>
{% for prog_key, prog_label in [("wahlprogramm","Wahlprogramm"), ("parteiprogramm","Parteiprogramm")] %}
{% set p = fs[prog_key] %}
{% set p_score = p.score | float %}
<div class="pdf-prog">
<div class="pdf-prog-row">
<span class="pdf-prog-label">{{ prog_label }}</span>
<span class="pdf-prog-spacer"></span>
<span class="pdf-prog-score {% if p_score >= 7 %}good{% elif p_score >= 4 %}mid{% else %}low{% endif %}">{{ "%.0f"|format(p_score) }}/10</span>
</div>
{% if p.begruendung %}<p class="pdf-prog-text">{{ p.begruendung }}</p>{% endif %}
{% if p.zitate %}
{% for z in p.zitate %}
<div class="pdf-prog-zitat">
„{{ z.text }}"
<span class="src">{{ z.quelle }}</span>
</div>
{% endfor %}
{% else %}
<div class="pdf-no-zitate">
Keine wörtlich passenden Stellen im {{ prog_label }} gefunden — Bewertung beruht auf inhaltlicher Auslegung.
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- ─── Verbesserungsvorschläge (Redline-Diff) ─────────────────────── -->
{% if assessment.verbesserungen %}
<div class="pdf-section">
<h2 class="pdf-h2">Verbesserungsvorschläge</h2>
{% for v in assessment.verbesserungen %}
<div class="pdf-verbesserung">
{% if assessment.verbesserungen | length > 1 %}
<div class="pdf-verbesserung-num">Vorschlag {{ loop.index }} von {{ assessment.verbesserungen | length }}</div>
{% endif %}
{% if v.original %}
<div class="pdf-redline-original"><em>Original:</em> {{ v.original }}</div>
{% endif %}
{% if v.vorschlag %}
<div class="pdf-redline">{{ v.vorschlag | safe }}</div>
{% endif %}
{% if v.begruendung %}
<div class="pdf-verbesserung-begr">Begründung: {{ v.begruendung }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- ─── Abstimmungsergebnis (falls vorhanden) ──────────────────────── -->
{% if plenum_votes %}
<div class="pdf-section">
<h2 class="pdf-h2">Abstimmungsergebnis</h2>
{% if konsistenz_state %}
<div class="pdf-konsistenz {{ konsistenz_state }}">
<strong>{% if konsistenz_state == 'conflict' %}Mehrheit gegen GWÖ-Empfehlung{% else %}Mehrheit deckt sich mit GWÖ-Empfehlung{% endif %}</strong>
— Empfohlen: <em>{{ assessment.empfehlung.value }}</em>; Beschluss: <em>{{ konsistenz_decisive | capitalize }}</em>.
</div>
{% endif %}
{% for v in plenum_votes %}
<div class="pdf-vote-card">
<div class="pdf-vote-head">{{ v.ergebnis | capitalize }}{% if v.einstimmig %} · einstimmig{% endif %}{% if v.quelle_protokoll %} · {{ v.quelle_protokoll }}{% endif %}</div>
<div class="pdf-vote-pills">
{% if v.fraktionen_ja %}
<div><span class="pdf-vote-side" style="color:#1a7f37;">Ja:</span>
{% for f in v.fraktionen_ja %}<span class="pdf-vote-pill ja">{{ f }}</span>{% endfor %}
</div>
{% endif %}
{% if v.fraktionen_nein %}
<div><span class="pdf-vote-side" style="color:#a40e26;">Nein:</span>
{% for f in v.fraktionen_nein %}<span class="pdf-vote-pill nein">{{ f }}</span>{% endfor %}
</div>
{% endif %}
{% if v.fraktionen_enthaltung %}
<div><span class="pdf-vote-side" style="color:#57606a;">Enth.:</span>
{% for f in v.fraktionen_enthaltung %}<span class="pdf-vote-pill enth">{{ f }}</span>{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
</body>
</html>

View File

@ -365,8 +365,8 @@
</div>
</div>
{# Teilen #}
<div class="v3-rest-block v3-rest-divider-top">
{# Teilen — nur fuer angemeldete User; initAuth() blendet via #v2-share-block ein. #}
<div id="v2-share-block" class="v3-rest-block v3-rest-divider-top" style="display:none;">
<h3 class="v3-h3">Teilen</h3>
<div class="v3-share-buttons">
<button onclick="v2DetailShareCopy()" class="v3-action-btn">📋 Kopieren</button>