2448 lines
115 KiB
HTML
2448 lines
115 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{ app_name }}</title>
|
||
<style>
|
||
:root {
|
||
--color-darkgray: #5a5a5a;
|
||
--color-green: #889e33;
|
||
--color-blue: #009da5;
|
||
--color-lightgray: #bfbfbf;
|
||
--color-bg: #f5f5f5;
|
||
}
|
||
|
||
* { 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);
|
||
min-height: 100vh;
|
||
min-height: 100dvh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Header */
|
||
.header {
|
||
background: white;
|
||
padding: 1rem 2rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header h1 {
|
||
color: var(--color-blue);
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.header .subtitle {
|
||
color: var(--color-darkgray);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.bundesland-select {
|
||
padding: 0.5rem 1rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.bundesland-select:focus {
|
||
outline: none;
|
||
border-color: var(--color-blue);
|
||
}
|
||
|
||
/* Main Layout */
|
||
.main-container {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* Left Panel - List */
|
||
.list-panel {
|
||
width: 400px;
|
||
min-width: 300px;
|
||
background: white;
|
||
border-right: 1px solid var(--color-lightgray);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.list-header {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.search-row {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.search-box {
|
||
flex: 1;
|
||
padding: 0.75rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.btn-landtag {
|
||
padding: 0.5rem 0.75rem;
|
||
background: var(--color-green);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn-landtag:hover {
|
||
background: #728a2b;
|
||
}
|
||
|
||
.btn-landtag:disabled {
|
||
background: #ccc;
|
||
cursor: wait;
|
||
}
|
||
|
||
.list-filters {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-top: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-btn {
|
||
padding: 0.25rem 0.75rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 20px;
|
||
background: white;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.filter-btn.active {
|
||
background: var(--color-blue);
|
||
color: white;
|
||
border-color: var(--color-blue);
|
||
}
|
||
|
||
.list-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.list-item {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.list-item:hover {
|
||
background: var(--color-bg);
|
||
}
|
||
|
||
.list-item.active {
|
||
background: #e8f4f5;
|
||
border-left: 3px solid var(--color-blue);
|
||
}
|
||
|
||
.list-item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.list-item-id {
|
||
font-weight: bold;
|
||
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 {
|
||
font-size: 0.9rem;
|
||
font-weight: bold;
|
||
padding: 0.1rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* Neue Skala -5 bis +5 */
|
||
.score-high { background: #155724; color: white; } /* +4 bis +5: dunkelgrün */
|
||
.score-mid { background: #889e33; color: white; } /* +2 bis +3: GWÖ-grün */
|
||
.score-neutral { background: #6c757d; color: white; } /* 0 bis +1: grau */
|
||
.score-low { background: #fd7e14; color: white; } /* -1 bis -2: orange */
|
||
.score-negative { background: #dc3545; color: white; } /* -3 bis -5: rot */
|
||
.score-none { background: #e2e3e5; color: #383d41; }
|
||
|
||
.status-unchecked {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
font-size: 0.75rem;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.status-checked {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
font-size: 0.75rem;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.btn-check-now {
|
||
display: inline-block;
|
||
padding: 0.3rem 0.75rem;
|
||
background: var(--color-blue);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.btn-check-now:hover {
|
||
background: #007b82;
|
||
}
|
||
|
||
.btn-check-now:disabled {
|
||
background: #ccc;
|
||
cursor: wait;
|
||
}
|
||
|
||
.list-item-title {
|
||
font-size: 0.9rem;
|
||
color: var(--color-darkgray);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.list-item-meta {
|
||
font-size: 0.8rem;
|
||
color: #888;
|
||
}
|
||
|
||
.list-item-tags {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
margin-top: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tag {
|
||
font-size: 0.7rem;
|
||
padding: 0.1rem 0.4rem;
|
||
background: var(--color-bg);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Right Panel - Detail */
|
||
.detail-panel {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.detail-placeholder {
|
||
text-align: center;
|
||
color: #888;
|
||
padding: 4rem;
|
||
}
|
||
|
||
.detail-card {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 2rem;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.detail-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.detail-title {
|
||
font-size: 1.3rem;
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
.detail-id {
|
||
font-size: 0.9rem;
|
||
color: #888;
|
||
}
|
||
|
||
.score-display {
|
||
text-align: center;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.score-big {
|
||
font-size: 3rem;
|
||
font-weight: bold;
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
.score-label {
|
||
font-size: 0.9rem;
|
||
color: #888;
|
||
}
|
||
|
||
.matrix-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 0.5rem;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.matrix-item {
|
||
padding: 0.75rem;
|
||
background: var(--color-bg);
|
||
border-radius: 4px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.matrix-label {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.matrix-rating {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.rating-pos { color: var(--color-green); }
|
||
.rating-neg { color: #dc3545; }
|
||
.rating-neutral { color: #888; }
|
||
|
||
/* Matrix Table (5x5 Grid wie im PDF) */
|
||
.matrix-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 0.5rem 0;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.matrix-table th, .matrix-table td {
|
||
border: 1px solid var(--color-lightgray);
|
||
padding: 0.4rem 0.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.matrix-table thead th {
|
||
background: var(--color-blue);
|
||
color: white;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.matrix-table tbody th {
|
||
background: var(--color-bg);
|
||
text-align: left;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.matrix-table .positive {
|
||
background: var(--color-green);
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.matrix-table .negative {
|
||
background: #dc3545;
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.matrix-table .neutral {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
/* Themen Tags in Detail */
|
||
.themen-tags .tag {
|
||
display: inline-block;
|
||
margin-right: 0.5rem;
|
||
margin-bottom: 0.25rem;
|
||
padding: 0.25rem 0.5rem;
|
||
background: var(--color-blue);
|
||
color: white;
|
||
border-radius: 3px;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
/* Kernpunkte Liste */
|
||
.kernpunkte-list {
|
||
margin-left: 1.5rem;
|
||
}
|
||
|
||
.kernpunkte-list li {
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 1.1rem;
|
||
color: var(--color-darkgray);
|
||
margin: 1.5rem 0 0.75rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.text-block {
|
||
background: var(--color-bg);
|
||
padding: 1rem;
|
||
border-radius: 4px;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.strength-list, .weakness-list {
|
||
list-style: none;
|
||
}
|
||
|
||
.strength-list li::before {
|
||
content: "✓ ";
|
||
color: var(--color-green);
|
||
}
|
||
|
||
.weakness-list li::before {
|
||
content: "✗ ";
|
||
color: #dc3545;
|
||
}
|
||
|
||
.btn-pdf {
|
||
display: inline-block;
|
||
padding: 0.75rem 1.5rem;
|
||
background: var(--color-blue);
|
||
color: white;
|
||
text-decoration: none;
|
||
border-radius: 4px;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.btn-pdf:hover {
|
||
background: #007b82;
|
||
}
|
||
|
||
/* Upload Tab */
|
||
.upload-section {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 2rem;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.tab-btn {
|
||
padding: 0.5rem 1rem;
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
color: var(--color-darkgray);
|
||
}
|
||
|
||
.tab-btn.active {
|
||
border-bottom-color: var(--color-blue);
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
min-height: 200px;
|
||
padding: 1rem;
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
font-family: inherit;
|
||
resize: vertical;
|
||
}
|
||
|
||
.file-drop {
|
||
border: 2px dashed var(--color-lightgray);
|
||
border-radius: 8px;
|
||
padding: 3rem;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.file-drop:hover {
|
||
border-color: var(--color-blue);
|
||
}
|
||
|
||
.btn-analyze {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 1rem;
|
||
background: var(--color-green);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.btn-analyze:hover {
|
||
background: #728a2b;
|
||
}
|
||
|
||
.btn-analyze:disabled {
|
||
background: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Mode Toggle */
|
||
.mode-toggle {
|
||
display: flex;
|
||
background: var(--color-bg);
|
||
border-radius: 4px;
|
||
padding: 0.25rem;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 0.5rem 1rem;
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
color: var(--color-darkgray);
|
||
}
|
||
|
||
.mode-btn.active {
|
||
background: white;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.hamburger-dropdown {
|
||
display: none;
|
||
position: absolute;
|
||
right: 0;
|
||
top: 100%;
|
||
background: white;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
min-width: 180px;
|
||
z-index: 100;
|
||
padding: 0.3rem 0;
|
||
}
|
||
.hamburger-dropdown.open { display: block; }
|
||
.hamburger-dropdown a,
|
||
.hamburger-dropdown button {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 0.5rem 1rem;
|
||
text-align: left;
|
||
text-decoration: none;
|
||
color: var(--color-darkgray);
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
}
|
||
.hamburger-dropdown a:hover,
|
||
.hamburger-dropdown button:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
/* Loading */
|
||
.loading {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.spinner {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: 3px solid var(--color-lightgray);
|
||
border-top-color: var(--color-blue);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Mobile Back-to-List Button (nur Mobile sichtbar) */
|
||
.btn-back-mobile {
|
||
display: none;
|
||
padding: 0.5rem 0.75rem;
|
||
background: var(--color-bg);
|
||
border: 1px solid var(--color-lightgray);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
margin-bottom: 1rem;
|
||
font-size: 0.85rem;
|
||
color: var(--color-darkgray);
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 900px) {
|
||
.header {
|
||
padding: 0.75rem 1rem;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.header .subtitle {
|
||
font-size: 0.8rem;
|
||
flex-basis: 100%;
|
||
order: 10;
|
||
}
|
||
|
||
.mode-toggle {
|
||
margin-left: auto;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 0.4rem 0.6rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.main-container {
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.list-panel {
|
||
width: 100%;
|
||
min-width: 0;
|
||
border-right: none;
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.list-content {
|
||
max-height: 50vh;
|
||
}
|
||
|
||
.detail-panel {
|
||
padding: 1rem;
|
||
overflow: visible;
|
||
flex: none;
|
||
}
|
||
|
||
.btn-back-mobile {
|
||
display: inline-block;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.header h1 { font-size: 1.1rem; }
|
||
.header .subtitle { display: none; }
|
||
.detail-card { padding: 1rem; }
|
||
.matrix-table { font-size: 0.7rem; }
|
||
.matrix-table th, .matrix-table td { padding: 0.25rem 0.3rem; }
|
||
.matrix-grid { grid-template-columns: 1fr; }
|
||
.upload-section { padding: 1rem; }
|
||
.file-drop { padding: 1.5rem; }
|
||
.score-big { font-size: 2.2rem; }
|
||
.detail-header { flex-wrap: wrap; gap: 0.5rem; }
|
||
}
|
||
|
||
/* Stats Bar */
|
||
.stats-bar {
|
||
display: flex;
|
||
gap: 2rem;
|
||
padding: 1rem;
|
||
background: var(--color-bg);
|
||
border-bottom: 1px solid var(--color-lightgray);
|
||
}
|
||
|
||
.stat {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.5rem;
|
||
font-weight: bold;
|
||
color: var(--color-blue);
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.8rem;
|
||
color: #888;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="header">
|
||
<h1>{{ app_name }}</h1>
|
||
<span class="subtitle">Gemeinwohl-Matrix 2.0 für Gemeinden</span>
|
||
<select class="bundesland-select" id="bundesland-select" onchange="changeBundesland(this.value)">
|
||
{% for bl in bundeslaender %}
|
||
<option value="{{ bl.code }}" {% if bl.active %}{% else %}disabled{% endif %}>
|
||
{{ bl.name }}{% if not bl.active %} (bald){% endif %}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
<div class="mode-toggle">
|
||
<button class="mode-btn active" onclick="showMode('browse')">📋 Durchsuchen</button>
|
||
<button class="mode-btn" onclick="showMode('bookmarks')">⭐ Merkliste</button>
|
||
<button class="mode-btn" onclick="showMode('tags')">🏷️ Tags</button>
|
||
<button class="mode-btn" onclick="showMode('upload')">📤 Prüfen</button>
|
||
<div class="hamburger-wrapper" style="position:relative;display:inline-block;">
|
||
<button class="mode-btn" onclick="document.getElementById('hamburger-menu').classList.toggle('open')" style="font-size:1.2rem;padding:0.3rem 0.6rem;">☰</button>
|
||
<div id="hamburger-menu" class="hamburger-dropdown">
|
||
<a href="/auswertungen">📈 Auswertungen</a>
|
||
<a href="/quellen">📚 Quellen</a>
|
||
<a href="/methodik">🔍 Methodik</a>
|
||
<hr style="margin:0.3rem 0;border:none;border-top:1px solid #eee;">
|
||
<button onclick="event.stopPropagation();document.getElementById('queue-panel').style.display='block';document.getElementById('hamburger-menu').classList.remove('open');loadQueuePanel();">📊 Queue</button>
|
||
<button onclick="event.stopPropagation();document.getElementById('batch-panel').style.display='block';document.getElementById('hamburger-menu').classList.remove('open');">📦 Batch-Analyse</button>
|
||
<hr style="margin:0.3rem 0;border:none;border-top:1px solid #eee;">
|
||
<button id="auth-btn" onclick="event.stopPropagation();">🔑 Anmelden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-container" id="browse-mode">
|
||
<!-- Left: List -->
|
||
<aside class="list-panel">
|
||
<div class="list-header">
|
||
<!--
|
||
#16: zwei klar getrennte Suchfelder. Das erste filtert
|
||
in der DB der bereits geprüften Anträge (Live, debounced).
|
||
Das zweite triggert per Enter oder Button eine Live-
|
||
Anfrage gegen den Landtag-Adapter. Beide schreiben in
|
||
dieselbe Liste, unterscheiden sich aber visuell und
|
||
semantisch klar.
|
||
-->
|
||
<div class="search-row" style="flex-direction: column; gap: 0.4rem;">
|
||
<div style="display: flex; gap: 0.4rem; width: 100%;">
|
||
<input type="text" class="search-box" id="search-input"
|
||
placeholder="📊 Suche in geprüften Anträgen (DB)…"
|
||
oninput="debounceSearch(this.value)"
|
||
style="flex: 1;">
|
||
</div>
|
||
<div style="display: flex; gap: 0.4rem; width: 100%;">
|
||
<input type="text" class="search-box" id="landtag-search-input"
|
||
placeholder="🏛️ Im Landtag suchen (live)…"
|
||
onkeydown="if(event.key==='Enter')searchLandtag()"
|
||
style="flex: 1;">
|
||
<button class="btn-landtag" id="btn-landtag" onclick="searchLandtag()">🔍 Suchen</button>
|
||
</div>
|
||
</div>
|
||
<div class="list-filters">
|
||
<button class="filter-btn active" data-filter="all" onclick="setScoreFilter('all', this)">Alle</button>
|
||
<button class="filter-btn" data-filter="high" onclick="setScoreFilter('high', this)">8-10</button>
|
||
<button class="filter-btn" data-filter="mid" onclick="setScoreFilter('mid', this)">5-7</button>
|
||
<button class="filter-btn" data-filter="low" onclick="setScoreFilter('low', this)">0-4</button>
|
||
<select id="partei-filter" onchange="setParteiFilter(this.value)" style="padding: 0.25rem 0.5rem; border-radius: 20px; border: 1px solid var(--color-lightgray); font-size: 0.8rem; cursor: pointer;">
|
||
<option value="">Alle Parteien</option>
|
||
</select>
|
||
<select id="sort-select" onchange="setSortOrder(this.value)" style="padding: 0.25rem 0.5rem; border-radius: 20px; border: 1px solid var(--color-lightgray); font-size: 0.8rem; cursor: pointer;">
|
||
<option value="score-desc">↓ GWÖ-Score</option>
|
||
<option value="score-asc">↑ GWÖ-Score</option>
|
||
<option value="date-desc">↓ Datum</option>
|
||
<option value="date-asc">↑ Datum</option>
|
||
<option value="nr-desc">↓ Nummer</option>
|
||
<option value="title-asc">A-Z Titel</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<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 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 id="partei-stats" style="font-size: 0.8rem; display: flex; gap: 0.75rem; flex-wrap: wrap;"></span>
|
||
</div>
|
||
<div class="list-content" id="list-content">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Lade Bewertungen...</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Right: Detail -->
|
||
<main class="detail-panel" id="detail-panel">
|
||
<div class="detail-placeholder">
|
||
<p>👈 Wähle einen Antrag aus der Liste</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Bookmarks / Merkliste Mode -->
|
||
<div class="main-container" id="bookmarks-mode" style="display: none;">
|
||
<div class="list-panel" style="max-width: 100%;">
|
||
<h2 style="color: var(--color-blue); margin-bottom: 1rem;">⭐ Merkliste</h2>
|
||
<div id="bookmarks-content">
|
||
<p style="color: #888;">Lade Merkliste...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tags Mode -->
|
||
<div class="main-container" id="tags-mode" style="display: none;">
|
||
<aside class="list-panel" style="width: 100%; max-width: 400px;">
|
||
<div class="list-header">
|
||
<h3 style="margin-bottom: 0.5rem;">🏷️ Filter nach Tags</h3>
|
||
<p style="font-size: 0.85rem; color: #666; margin-bottom: 1rem;">Klicke auf Tags um zu filtern. Mehrfachauswahl zeigt Schnittmenge.</p>
|
||
<input type="text" class="search-box" id="tag-search-input" placeholder="Tags durchsuchen..." oninput="filterTagCloud(this.value)">
|
||
<div id="active-tags" style="margin-top: 0.75rem; display: flex; flex-wrap: wrap; gap: 0.5rem;"></div>
|
||
<button onclick="clearTagFilters()" style="margin-top: 0.75rem; padding: 0.5rem 1rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; display: none;" id="clear-tags-btn">✕ Filter zurücksetzen</button>
|
||
</div>
|
||
<div class="list-content" id="tag-cloud" style="padding: 1rem;">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Lade Tags...</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
<main class="detail-panel" id="tag-results-panel">
|
||
<div class="detail-card">
|
||
<h3 style="margin-bottom: 1rem;">📋 Gefilterte Anträge</h3>
|
||
<div id="tag-results-list">
|
||
<p style="color: #888;">Wähle Tags aus der Wolke um Anträge zu filtern.</p>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Upload Mode -->
|
||
<div class="main-container" id="upload-mode" style="display: none;">
|
||
<div class="detail-panel">
|
||
<div class="upload-section">
|
||
<h2 style="margin-bottom: 1rem; color: var(--color-blue);">Neuen Antrag prüfen</h2>
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" onclick="showTab('text')">Text eingeben</button>
|
||
<button class="tab-btn" onclick="showTab('file')">PDF hochladen</button>
|
||
</div>
|
||
|
||
<div id="tab-text">
|
||
<textarea id="antrag-text" placeholder="Antragstext hier einfügen..."></textarea>
|
||
</div>
|
||
|
||
<div id="tab-file" style="display: none;">
|
||
<div class="file-drop" onclick="document.getElementById('file-input').click()">
|
||
<p>📄 PDF hier ablegen oder klicken</p>
|
||
<input type="file" id="file-input" accept=".pdf" style="display: none" onchange="handleFile(this)">
|
||
<p id="file-name" style="margin-top: 0.5rem; color: var(--color-blue);"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 1rem;">
|
||
<label>Bundesland:</label>
|
||
<select id="bundesland" style="padding: 0.5rem; margin-left: 0.5rem;">
|
||
<option value="" disabled selected>— Bundesland wählen —</option>
|
||
{% 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 %}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<button class="btn-analyze" id="analyze-btn" onclick="startAnalysis()">
|
||
🔍 GWÖ-Analyse starten
|
||
</button>
|
||
|
||
<div id="analysis-status" style="display: none;">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span id="status-text">Analyse läuft...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="analysis-result" style="display: none; margin-top: 1rem;"></div>
|
||
|
||
<!-- Batch-Analyse -->
|
||
<div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 2px solid var(--color-lightgray);">
|
||
<h2 style="margin-bottom: 0.5rem; color: var(--color-blue);">📦 Batch-Analyse</h2>
|
||
<p style="font-size: 0.85rem; color: #666; margin-bottom: 1rem;">
|
||
Analysiert automatisch die neuesten ungeprüften Anträge eines Bundeslandes.
|
||
</p>
|
||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||
<select id="batch-bundesland" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
|
||
{% for bl in bundeslaender if bl.code != 'ALL' and bl.active %}
|
||
<option value="{{ bl.code }}">{{ bl.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<select id="batch-limit" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
|
||
<option value="5">5 Anträge</option>
|
||
<option value="10" selected>10 Anträge</option>
|
||
<option value="20">20 Anträge</option>
|
||
<option value="50">50 Anträge</option>
|
||
</select>
|
||
<button id="batch-btn" onclick="startBatch()"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung"'}
|
||
style="padding:0.4rem 1rem;background:var(--color-green);color:white;border:none;border-radius:4px;cursor:pointer;">
|
||
🚀 Batch starten
|
||
</button>
|
||
</div>
|
||
<div id="batch-status" style="margin-top:0.75rem;font-size:0.85rem;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let allAssessments = [];
|
||
let currentScoreFilter = 'all';
|
||
let currentParteiFilter = '';
|
||
let currentBundesland = 'ALL';
|
||
let searchTimeout = null;
|
||
let isSearching = false;
|
||
let selectedTags = new Set();
|
||
let allTags = {};
|
||
let currentUser = null; // #43: Auth-State
|
||
let currentSort = localStorage.getItem('sortOrder') || 'score-desc';
|
||
|
||
// #43: Auth prüfen beim Load. Steuert ob "Jetzt prüfen" aktiv ist.
|
||
async function initAuth() {
|
||
try {
|
||
const resp = await fetch('/api/auth/me');
|
||
const data = await resp.json();
|
||
currentUser = data.authenticated ? data : null;
|
||
} catch { currentUser = null; }
|
||
updateAuthUI();
|
||
}
|
||
function updateAuthUI() {
|
||
const authBtn = document.getElementById('auth-btn');
|
||
if (!authBtn) return;
|
||
if (currentUser) {
|
||
authBtn.textContent = '✓ Angemeldet (Logout)';
|
||
authBtn.classList.add('logged-in');
|
||
authBtn.style.color = '#889e33';
|
||
authBtn.onclick = () => {
|
||
// Cookie löschen + Keycloak-Logout
|
||
document.cookie = 'access_token=; Max-Age=0; path=/; secure; samesite=lax';
|
||
currentUser = null;
|
||
updateAuthUI();
|
||
loadAssessments(); // Liste neu rendern (Buttons deaktivieren)
|
||
};
|
||
// Bestehende Liste neu rendern damit Buttons aktiv werden
|
||
if (allAssessments.length > 0) renderList(sortAssessments(allAssessments));
|
||
} else {
|
||
authBtn.textContent = '🔑 Anmelden';
|
||
authBtn.style.color = '';
|
||
authBtn.classList.remove('logged-in');
|
||
authBtn.onclick = async () => {
|
||
const resp = await fetch(`/api/auth/login-url?redirect=${encodeURIComponent(window.location.pathname)}`);
|
||
const data = await resp.json();
|
||
if (data.url) window.location.href = data.url;
|
||
};
|
||
}
|
||
}
|
||
|
||
// Matrix-Feld-Info-Modal
|
||
function showFieldInfo(field, aspect, rating) {
|
||
const labels = {{ matrix_labels | tojson }};
|
||
const explains = {{ matrix_explanations | tojson }};
|
||
const rowNames = {'A':'Ausgelagerte Betriebe, Lieferant:innen','B':'Finanzpartner:innen, Steuerzahler:innen','C':'Politische Führung, Verwaltung','D':'Bürger:innen und Wirtschaft','E':'Staat, Gesellschaft und Natur'};
|
||
const colNames = {1:'Menschenwürde',2:'Solidarität',3:'Ökologische Nachhaltigkeit',4:'Soziale Gerechtigkeit',5:'Transparenz & Mitbestimmung'};
|
||
const row = field[0], col = parseInt(field[1]);
|
||
const ratingText = rating >= 4 ? '++ stark fördernd' : rating >= 1 ? '+ fördernd' : rating === 0 ? '○ neutral / nicht bewertet' : rating >= -3 ? '− widersprechend' : '−− stark widersprechend';
|
||
const ratingColor = rating > 0 ? '#889e33' : rating < 0 ? '#dc3545' : '#888';
|
||
|
||
const html = `
|
||
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:300;display:flex;justify-content:center;align-items:center;" onclick="if(event.target===this)this.remove()">
|
||
<div style="background:white;border-radius:8px;padding:1.5rem;max-width:500px;width:90%;box-shadow:0 8px 24px rgba(0,0,0,0.2);">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||
<h3 style="color:var(--color-blue);margin:0;">${field}: ${labels[field] || field}</h3>
|
||
<button onclick="this.closest('[style*=fixed]').remove()" style="background:none;border:none;font-size:1.2rem;cursor:pointer;">✕</button>
|
||
</div>
|
||
<div style="font-size:0.85rem;color:#666;margin-bottom:0.75rem;">
|
||
<strong>Zeile ${row}:</strong> ${rowNames[row] || ''}<br>
|
||
<strong>Spalte ${col}:</strong> ${colNames[col] || ''}
|
||
</div>
|
||
<div style="background:#f8f9fa;padding:0.75rem;border-radius:6px;margin-bottom:0.75rem;font-size:0.9rem;">
|
||
<strong>Was bedeutet das für Bürger:innen?</strong><br>
|
||
${explains[field] || 'Keine Erklärung verfügbar.'}
|
||
</div>
|
||
${aspect ? `<div style="background:#fffbf0;padding:0.75rem;border-radius:6px;margin-bottom:0.75rem;font-size:0.9rem;border-left:3px solid ${ratingColor};">
|
||
<strong>Bewertung dieses Antrags:</strong><br>
|
||
${aspect}<br>
|
||
<span style="color:${ratingColor};font-weight:bold;">Bewertung: ${rating} (${ratingText})</span>
|
||
</div>` : '<p style="color:#aaa;font-size:0.85rem;">Dieses Feld wird vom Antrag nicht berührt.</p>'}
|
||
</div>
|
||
</div>`;
|
||
document.body.insertAdjacentHTML('beforeend', html);
|
||
}
|
||
|
||
function showColumnInfo(col) {
|
||
const names = {1:'Menschenwürde',2:'Solidarität',3:'Ökologische Nachhaltigkeit',4:'Soziale Gerechtigkeit',5:'Transparenz & Mitbestimmung'};
|
||
const prinzipien = {1:'Rechtsstaatsprinzip',2:'Gemeinnutz',3:'Umwelt-Verantwortung',4:'Sozialstaatsprinzip',5:'Demokratie'};
|
||
const details = {
|
||
1: 'Werden Ihre Grundrechte geschützt? Sind Sie vor willkürlicher Behandlung durch Behörden sicher? Haben alle Menschen — unabhängig von Herkunft, Geschlecht oder Religion — die gleichen Rechte?',
|
||
2: 'Profitiert die Gemeinschaft? Wenn Ihre Kommune Geld ausgibt, kommt es bei den Menschen an? Werden Vereine, Nachbarschaftshilfe und ehrenamtliches Engagement gestärkt?',
|
||
3: 'Können Ihre Kinder noch saubere Luft atmen und in Seen schwimmen? Wird bei Entscheidungen an den Klimawandel gedacht? Gibt es mehr Grün in der Stadt, weniger Versiegelung?',
|
||
4: 'Kann sich jede Familie eine warme Wohnung leisten? Bekommen Kinder aus armen Familien die gleichen Bildungschancen? Gibt es bezahlbare Gesundheitsversorgung für alle?',
|
||
5: 'Können Sie als Bürger:in mitentscheiden, was in Ihrer Stadt passiert? Werden Sie rechtzeitig informiert? Sind Ratssitzungen öffentlich und verständlich?'
|
||
};
|
||
document.body.insertAdjacentHTML('beforeend', `
|
||
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:300;display:flex;justify-content:center;align-items:center;" onclick="if(event.target===this)this.remove()">
|
||
<div style="background:white;border-radius:8px;padding:1.5rem;max-width:500px;width:90%;box-shadow:0 8px 24px rgba(0,0,0,0.2);">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||
<h3 style="color:var(--color-blue);margin:0;">Wert ${col}: ${names[col]}</h3>
|
||
<button onclick="this.closest('[style*=fixed]').remove()" style="background:none;border:none;font-size:1.2rem;cursor:pointer;">✕</button>
|
||
</div>
|
||
<p style="font-size:0.85rem;color:#666;margin-bottom:0.5rem;"><strong>Staatsprinzip:</strong> ${prinzipien[col]}</p>
|
||
<div style="background:#f8f9fa;padding:0.75rem;border-radius:6px;font-size:0.9rem;">${details[col]}</div>
|
||
</div>
|
||
</div>`);
|
||
}
|
||
|
||
function showRowInfo(row) {
|
||
const names = {'A':'Ausgelagerte Betriebe, Lieferant:innen, Dienstleister:innen','B':'Finanzpartner:innen, Geldgeber:innen, Steuerzahler:innen','C':'Politische Führung, Verwaltung, Ehrenamtliche','D':'Bürger:innen und Wirtschaft','E':'Staat, Gesellschaft und Natur'};
|
||
const details = {
|
||
'A': 'Wenn Ihre Stadt einen Spielplatz baut: Wer liefert das Material? Unter welchen Bedingungen wurde es hergestellt? Wird regional eingekauft — beim Handwerker um die Ecke — oder beim billigsten Anbieter aus dem Ausland? Diese Zeile bewertet, wie verantwortungsvoll die öffentliche Hand einkauft.',
|
||
'B': 'Wohin fließen Ihre Steuergelder? Liegt das Geld der Stadt bei einer ethischen Bank oder bei einem Finanzkonzern? Werden Investitionen so getätigt, dass sie der Allgemeinheit nutzen? Hier geht es um den verantwortungsvollen Umgang mit dem Geld, das Sie als Steuerzahler:in einzahlen.',
|
||
'C': 'Wie geht Ihre Stadtverwaltung mit den eigenen Mitarbeitenden um? Gibt es faire Bezahlung, Gleichstellung, familienfreundliche Arbeitszeiten? Engagieren sich Ehrenamtliche — und werden sie dafür wertgeschätzt? Diese Zeile bewertet die interne Kultur der öffentlichen Hand.',
|
||
'D': 'Das ist die Zeile, die Sie direkt betrifft: Funktioniert der Bus? Ist die Schule gut? Bekommen Sie einen Termin beim Amt? Können Sie sich die Miete leisten? Hier wird bewertet, ob die öffentliche Daseinsvorsorge bei Ihnen ankommt — bei allen, nicht nur bei wenigen.',
|
||
'E': 'Denkt Ihre Kommune über den eigenen Tellerrand hinaus? Wird beim Straßenbau an den Klimawandel gedacht? Gibt es Partnerschaften mit strukturschwachen Regionen? Hier wird bewertet, ob Entscheidungen auch für künftige Generationen und Menschen anderswo gut sind.'
|
||
};
|
||
document.body.insertAdjacentHTML('beforeend', `
|
||
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:300;display:flex;justify-content:center;align-items:center;" onclick="if(event.target===this)this.remove()">
|
||
<div style="background:white;border-radius:8px;padding:1.5rem;max-width:500px;width:90%;box-shadow:0 8px 24px rgba(0,0,0,0.2);">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||
<h3 style="color:var(--color-blue);margin:0;">Gruppe ${row}: ${names[row]}</h3>
|
||
<button onclick="this.closest('[style*=fixed]').remove()" style="background:none;border:none;font-size:1.2rem;cursor:pointer;">✕</button>
|
||
</div>
|
||
<div style="background:#f8f9fa;padding:0.75rem;border-radius:6px;font-size:0.9rem;">${details[row]}</div>
|
||
</div>
|
||
</div>`);
|
||
}
|
||
|
||
// Sortierung (#100)
|
||
function setSortOrder(order) {
|
||
currentSort = order;
|
||
localStorage.setItem('sortOrder', order);
|
||
applyFilters();
|
||
}
|
||
function sortAssessments(items) {
|
||
const sorted = [...items];
|
||
switch (currentSort) {
|
||
case 'score-desc': return sorted.sort((a, b) => (b.gwoeScore || 0) - (a.gwoeScore || 0));
|
||
case 'score-asc': return sorted.sort((a, b) => (a.gwoeScore || 0) - (b.gwoeScore || 0));
|
||
case 'date-desc': return sorted.sort((a, b) => (b.datum || '').localeCompare(a.datum || ''));
|
||
case 'date-asc': return sorted.sort((a, b) => (a.datum || '').localeCompare(b.datum || ''));
|
||
case 'nr-desc': return sorted.sort((a, b) => {
|
||
const na = parseInt((a.drucksache || '').split('/')[1]) || 0;
|
||
const nb = parseInt((b.drucksache || '').split('/')[1]) || 0;
|
||
return nb - na;
|
||
});
|
||
case 'title-asc': return sorted.sort((a, b) => (a.title || '').localeCompare(b.title || '', 'de'));
|
||
default: return sorted;
|
||
}
|
||
}
|
||
|
||
// Hamburger-Menü schließen bei Klick außerhalb
|
||
document.addEventListener('click', (e) => {
|
||
const menu = document.getElementById('hamburger-menu');
|
||
if (menu && !e.target.closest('.hamburger-wrapper')) {
|
||
menu.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// Partei-Normalisierung (global, für Stats + Labels)
|
||
function normalizePartei(f) {
|
||
const u = f.toUpperCase();
|
||
if (u === 'AFD') return 'AfD';
|
||
if (u === 'GRÜNE' || u === 'GRUENE' || u === 'BÜNDNIS 90/DIE GRÜNEN') return 'GRÜNE';
|
||
if (u === 'DIE LINKE') return 'LINKE';
|
||
return f;
|
||
}
|
||
|
||
// 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', () => {
|
||
initAuth(); // #43: Auth-State prüfen
|
||
// Sort-Auswahl aus localStorage wiederherstellen
|
||
const savedSort = localStorage.getItem('sortOrder');
|
||
if (savedSort) {
|
||
document.getElementById('sort-select').value = savedSort;
|
||
currentSort = savedSort;
|
||
}
|
||
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();
|
||
});
|
||
|
||
function buildParteienFilter() {
|
||
// Alle Fraktionen sammeln
|
||
const parteien = new Set();
|
||
allAssessments.forEach(a => {
|
||
(a.fraktionen || []).forEach(f => parteien.add(f));
|
||
});
|
||
|
||
const select = document.getElementById('partei-filter');
|
||
select.innerHTML = '<option value="">Alle Parteien</option>';
|
||
Array.from(parteien).sort().forEach(p => {
|
||
select.innerHTML += `<option value="${p}">${p}</option>`;
|
||
});
|
||
}
|
||
|
||
function buildTagCloud() {
|
||
allTags = {};
|
||
allAssessments.forEach(a => {
|
||
(a.themen || []).forEach(tag => {
|
||
allTags[tag] = (allTags[tag] || 0) + 1;
|
||
});
|
||
});
|
||
renderTagCloud();
|
||
}
|
||
|
||
function renderTagCloud(filter = '') {
|
||
const container = document.getElementById('tag-cloud');
|
||
const filterLower = filter.toLowerCase();
|
||
|
||
// Sortiert nach Häufigkeit
|
||
const sorted = Object.entries(allTags)
|
||
.filter(([tag]) => tag.toLowerCase().includes(filterLower))
|
||
.sort((a, b) => b[1] - a[1]);
|
||
|
||
if (sorted.length === 0) {
|
||
container.innerHTML = '<p style="color: #888;">Keine Tags gefunden</p>';
|
||
return;
|
||
}
|
||
|
||
const maxCount = Math.max(...sorted.map(([, c]) => c));
|
||
|
||
container.innerHTML = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">' +
|
||
sorted.map(([tag, count]) => {
|
||
const size = 0.75 + (count / maxCount) * 0.75;
|
||
const isActive = selectedTags.has(tag);
|
||
return `<button
|
||
onclick="toggleTag('${tag.replace(/'/g, "\\'")}')"
|
||
style="
|
||
font-size: ${size}rem;
|
||
padding: 0.3rem 0.6rem;
|
||
border-radius: 20px;
|
||
border: 1px solid ${isActive ? 'var(--color-blue)' : 'var(--color-lightgray)'};
|
||
background: ${isActive ? 'var(--color-blue)' : 'white'};
|
||
color: ${isActive ? 'white' : 'var(--color-darkgray)'};
|
||
cursor: pointer;
|
||
"
|
||
title="${count} Anträge"
|
||
>${tag} <span style="font-size: 0.7rem; opacity: 0.7;">(${count})</span></button>`;
|
||
}).join('') +
|
||
'</div>';
|
||
}
|
||
|
||
function filterTagCloud(query) {
|
||
renderTagCloud(query);
|
||
}
|
||
|
||
function toggleTag(tag) {
|
||
if (selectedTags.has(tag)) {
|
||
selectedTags.delete(tag);
|
||
} else {
|
||
selectedTags.add(tag);
|
||
}
|
||
updateTagFilterUI();
|
||
filterByTags();
|
||
}
|
||
|
||
function updateTagFilterUI() {
|
||
const container = document.getElementById('active-tags');
|
||
const clearBtn = document.getElementById('clear-tags-btn');
|
||
|
||
if (selectedTags.size === 0) {
|
||
container.innerHTML = '';
|
||
clearBtn.style.display = 'none';
|
||
} else {
|
||
container.innerHTML = Array.from(selectedTags).map(tag =>
|
||
`<span style="padding: 0.25rem 0.5rem; background: var(--color-blue); color: white; border-radius: 4px; font-size: 0.85rem;">
|
||
${tag} <span style="cursor: pointer; margin-left: 0.25rem;" onclick="toggleTag('${tag.replace(/'/g, "\\'")}')">×</span>
|
||
</span>`
|
||
).join('');
|
||
clearBtn.style.display = 'block';
|
||
}
|
||
renderTagCloud(document.getElementById('tag-search-input')?.value || '');
|
||
}
|
||
|
||
function clearTagFilters() {
|
||
selectedTags.clear();
|
||
updateTagFilterUI();
|
||
filterByTags();
|
||
}
|
||
|
||
function filterByTags() {
|
||
const resultsContainer = document.getElementById('tag-results-list');
|
||
|
||
if (selectedTags.size === 0) {
|
||
resultsContainer.innerHTML = '<p style="color: #888;">Wähle Tags aus der Wolke um Anträge zu filtern.</p>';
|
||
return;
|
||
}
|
||
|
||
// Schnittmenge: Anträge müssen ALLE ausgewählten Tags haben
|
||
const filtered = allAssessments.filter(a => {
|
||
const tags = new Set(a.themen || []);
|
||
return Array.from(selectedTags).every(t => tags.has(t));
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
resultsContainer.innerHTML = '<p style="color: #888;">Keine Anträge mit dieser Tag-Kombination gefunden.</p>';
|
||
return;
|
||
}
|
||
|
||
resultsContainer.innerHTML = `
|
||
<p style="margin-bottom: 1rem; color: #666;">${filtered.length} Anträge gefunden</p>
|
||
${filtered.map(item => `
|
||
<div style="padding: 0.75rem; border-bottom: 1px solid var(--color-lightgray); cursor: pointer;" onclick="showMode('browse'); setTimeout(() => showDetail('${item.drucksache}'), 100);">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span style="font-weight: bold; color: var(--color-blue);">${item.drucksache}</span>
|
||
<span style="padding: 0.1rem 0.5rem; border-radius: 4px; font-size: 0.85rem; ${item.gwoeScore >= 8 ? 'background: #155724; color: white;' : item.gwoeScore >= 5 ? 'background: #889e33; color: white;' : 'background: #dc3545; color: white;'}">${item.gwoeScore}/10</span>
|
||
</div>
|
||
<div style="font-size: 0.9rem; margin-top: 0.25rem;">${item.title || 'Ohne Titel'}</div>
|
||
<div style="font-size: 0.8rem; color: #888;">${(item.fraktionen || []).join(', ')}</div>
|
||
</div>
|
||
`).join('')}
|
||
`;
|
||
}
|
||
|
||
async function loadAssessments() {
|
||
try {
|
||
const url = `/api/assessments?bundesland=${encodeURIComponent(currentBundesland)}`;
|
||
const resp = await fetch(url);
|
||
allAssessments = await resp.json();
|
||
updateStats();
|
||
renderList(sortAssessments(allAssessments));
|
||
buildParteienFilter();
|
||
buildTagCloud();
|
||
} catch (e) {
|
||
console.error('loadAssessments error:', e);
|
||
document.getElementById('list-content').innerHTML =
|
||
`<p style="padding: 1rem; color: #c00;">Fehler beim Laden: ${e.message}</p>`;
|
||
}
|
||
}
|
||
|
||
function updateStats() {
|
||
const checked = allAssessments.filter(a => a.status !== 'unchecked').length;
|
||
const high = allAssessments.filter(a => a.gwoeScore >= 8).length;
|
||
const avg = checked > 0 ? (allAssessments.filter(a => a.gwoeScore != null).reduce((s, a) => s + (a.gwoeScore || 0), 0) / checked).toFixed(1) : 0;
|
||
|
||
document.getElementById('stat-total').textContent = checked;
|
||
document.getElementById('stat-high').textContent = high;
|
||
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 (Normalisierung via globaler normalizePartei)
|
||
const parteiStats = {};
|
||
allAssessments.forEach(a => {
|
||
if (a.gwoeScore == null) return;
|
||
(a.fraktionen || []).forEach(f => {
|
||
const norm = normalizePartei(f);
|
||
if (!parteiStats[norm]) parteiStats[norm] = { sum: 0, count: 0 };
|
||
parteiStats[norm].sum += a.gwoeScore;
|
||
parteiStats[norm].count += 1;
|
||
});
|
||
});
|
||
|
||
// Sortiert nach Durchschnitt (absteigend)
|
||
const sorted = Object.entries(parteiStats)
|
||
.map(([partei, data]) => ({ partei, avg: data.sum / data.count, count: data.count }))
|
||
.sort((a, b) => b.avg - a.avg);
|
||
|
||
const container = document.getElementById('partei-stats');
|
||
container.innerHTML = sorted.map(p => {
|
||
const color = p.avg >= 7 ? '#889e33' : p.avg >= 4 ? '#fd7e14' : '#dc3545';
|
||
return `<span style="color: ${color};"><strong>${p.partei}</strong> ${p.avg.toFixed(1)}</span>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderList(items) {
|
||
const container = document.getElementById('list-content');
|
||
if (items.length === 0) {
|
||
container.innerHTML = '<p style="padding: 1rem; color: #888;">Keine Ergebnisse</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = items.map(item => {
|
||
const isUnchecked = item.status === 'unchecked';
|
||
// Skala 0-10
|
||
const scoreClass = isUnchecked ? 'status-unchecked' :
|
||
item.gwoeScore >= 8 ? 'score-high' :
|
||
item.gwoeScore >= 5 ? 'score-mid' :
|
||
item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
||
const fraktionen = (item.fraktionen || []).join(', ') || 'k.A.';
|
||
const themen = (item.themen || []).slice(0, 3);
|
||
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 `
|
||
<div class="list-item ${isUnchecked ? 'unchecked' : ''}" data-drucksache="${item.drucksache}" onclick="${isUnchecked ? '' : `showDetail('${item.drucksache}')`}">
|
||
<div class="list-item-header">
|
||
<span class="list-item-id">${blBadge}${item.drucksache}</span>
|
||
<span class="list-item-score ${scoreClass}">${scoreText}</span>
|
||
</div>
|
||
<div class="list-item-title">${(item.title || 'Ohne Titel').length > 80 ? (item.title.substring(0, 80) + '…') : (item.title || 'Ohne Titel')}</div>
|
||
<div class="list-item-meta">${fraktionen} · ${item.datum || ''}</div>
|
||
${isUnchecked ? `
|
||
<button class="btn-check-now"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung verfügbar" style="opacity:0.5;cursor:not-allowed;"'}
|
||
onclick="event.stopPropagation(); checkNow('${item.drucksache}', this)">
|
||
🔍 Jetzt prüfen
|
||
</button>
|
||
` : `
|
||
<div class="list-item-tags">
|
||
${themen.map(t => `<span class="tag">${t}</span>`).join('')}
|
||
</div>
|
||
`}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function debounceSearch(query) {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => performSearch(query), 300);
|
||
}
|
||
|
||
async function performSearch(query) {
|
||
if (query.length < 2) {
|
||
// Show all from DB with current filters
|
||
applyAllFilters();
|
||
return;
|
||
}
|
||
|
||
isSearching = true;
|
||
document.getElementById('list-content').innerHTML = `
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Suche...</span>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/search?q=${encodeURIComponent(query)}&bundesland=${currentBundesland}`);
|
||
let results = await resp.json();
|
||
|
||
// Score-Filter anwenden
|
||
if (currentScoreFilter !== 'all') {
|
||
results = applyScoreFilter(results, currentScoreFilter);
|
||
}
|
||
|
||
// Partei-Filter anwenden
|
||
if (currentParteiFilter) {
|
||
results = results.filter(a =>
|
||
(a.fraktionen || []).includes(currentParteiFilter)
|
||
);
|
||
}
|
||
|
||
renderList(results);
|
||
} catch (e) {
|
||
document.getElementById('list-content').innerHTML =
|
||
'<p style="padding: 1rem; color: #888;">Suchfehler</p>';
|
||
}
|
||
|
||
isSearching = false;
|
||
}
|
||
|
||
async function checkNow(drucksache, btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Prüfe...';
|
||
|
||
try {
|
||
const resp = await fetch('/api/analyze-drucksache', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}&bundesland=${currentBundesland}`
|
||
});
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'queued') {
|
||
btn.textContent = '⏳ Analysiere...';
|
||
pollAnalysis(data.job_id, drucksache, btn);
|
||
} else if (data.status === 'already_checked') {
|
||
btn.textContent = '✓ Bereits geprüft';
|
||
setTimeout(() => loadAssessments(), 500);
|
||
} else {
|
||
btn.textContent = '✗ Fehler';
|
||
btn.disabled = false;
|
||
}
|
||
} catch (e) {
|
||
btn.textContent = '✗ Fehler';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// ─── Batch-Analyse ──────────────────────────────────────────
|
||
async function startBatch() {
|
||
const bl = document.getElementById('batch-bundesland').value;
|
||
const limit = document.getElementById('batch-limit').value;
|
||
const btn = document.getElementById('batch-btn');
|
||
const status = document.getElementById('batch-status');
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Wird gestartet...';
|
||
status.innerHTML = '';
|
||
|
||
try {
|
||
const resp = await fetch('/api/batch-analyze', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `bundesland=${bl}&limit=${limit}`
|
||
});
|
||
if (resp.status === 401) {
|
||
status.innerHTML = '<span style="color:#dc3545;">🔒 Bitte zuerst anmelden.</span>';
|
||
btn.disabled = false; btn.textContent = '🚀 Batch starten';
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
if (data.status === 'batch_enqueued') {
|
||
status.innerHTML = `
|
||
<span style="color:var(--color-green);">✓ ${data.enqueued} Anträge in die Queue eingereiht</span>
|
||
${data.skipped_existing > 0 ? `<br><span style="color:#888;">${data.skipped_existing} bereits bewertet (übersprungen)</span>` : ''}
|
||
<br><span style="color:#888;">Die Analyse läuft im Hintergrund. Ergebnisse erscheinen nach und nach in der Liste.</span>
|
||
`;
|
||
// Queue-Status pollen
|
||
pollBatchQueue(status);
|
||
} else {
|
||
status.innerHTML = `<span style="color:#dc3545;">❌ ${data.detail || 'Fehler'}</span>`;
|
||
}
|
||
} catch (e) {
|
||
status.innerHTML = `<span style="color:#dc3545;">❌ ${e.message}</span>`;
|
||
}
|
||
btn.disabled = false; btn.textContent = '🚀 Batch starten';
|
||
}
|
||
|
||
async function pollBatchQueue(statusEl) {
|
||
for (let i = 0; i < 200; i++) {
|
||
await new Promise(r => setTimeout(r, 3000));
|
||
try {
|
||
const qs = await fetch('/api/queue/status').then(r => r.json());
|
||
|
||
// Job-Tabelle rendern
|
||
const jobs = qs.jobs || [];
|
||
const jobsHtml = jobs.length > 0 ? `
|
||
<table style="width:100%;font-size:0.8rem;border-collapse:collapse;margin-top:0.5rem;">
|
||
<tr style="color:#888;"><td>Drucksache</td><td>Status</td><td>Dauer</td></tr>
|
||
${jobs.map(j => {
|
||
const statusIcon = j.status === 'completed' ? '✅' : j.status === 'processing' ? '⏳' : j.status === 'failed' ? '❌' : '⏸';
|
||
const dur = j.duration ? j.duration + 's' : '';
|
||
const link = j.status === 'completed' ? `<a href="#" onclick="showMode('browse');setTimeout(()=>showDetail('${j.drucksache}'),100);return false;" style="color:var(--color-blue);">${j.drucksache}</a>` : j.drucksache;
|
||
return `<tr style="border-top:1px solid #f0f0f0;"><td>${link}</td><td>${statusIcon} ${j.status}</td><td>${dur}</td></tr>`;
|
||
}).join('')}
|
||
</table>` : '';
|
||
|
||
// Fortschrittsbalken
|
||
const completed = jobs.filter(j => j.status === 'completed').length;
|
||
const total = jobs.length;
|
||
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
||
|
||
statusEl.innerHTML = `
|
||
<div style="margin:0.5rem 0;">
|
||
<div style="background:#eee;border-radius:4px;height:8px;overflow:hidden;">
|
||
<div style="background:var(--color-green);height:100%;width:${pct}%;transition:width 0.5s;"></div>
|
||
</div>
|
||
<span style="font-size:0.8rem;color:#888;">${completed}/${total} fertig · ${qs.concurrency} Worker · ~${Math.round(qs.estimated_wait_seconds/60)} Min.</span>
|
||
</div>
|
||
${jobsHtml}
|
||
`;
|
||
|
||
if (qs.pending === 0 && completed > 0) {
|
||
statusEl.innerHTML += `<br><span style="color:var(--color-green);">✓ Alle Jobs abgeschlossen</span>`;
|
||
loadAssessments();
|
||
return;
|
||
}
|
||
} catch { break; }
|
||
}
|
||
}
|
||
|
||
// ─── Merkliste (#94) ────────────────────────────────────────
|
||
async function loadBookmarksList() {
|
||
const container = document.getElementById('bookmarks-content');
|
||
if (!currentUser) {
|
||
container.innerHTML = '<p style="color:#888;">Bitte anmelden um die Merkliste zu sehen.</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = '<p style="color:#888;">Lade...</p>';
|
||
try {
|
||
const bookmarkIds = await fetch('/api/bookmarks').then(r => r.json());
|
||
if (bookmarkIds.length === 0) {
|
||
container.innerHTML = '<p style="color:#888;">Keine gemerkten Anträge. Klicke auf "🔖 Merken" bei einem Antrag.</p>';
|
||
return;
|
||
}
|
||
// Finde die Assessment-Daten für die gebookmarkten Drucksachen
|
||
const bookmarked = allAssessments.filter(a => bookmarkIds.includes(a.drucksache));
|
||
if (bookmarked.length === 0) {
|
||
container.innerHTML = '<p style="color:#888;">Gemerkte Anträge nicht in der aktuellen Auswahl gefunden. Wechsle zu "Bundesweit".</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = bookmarked.map(item => {
|
||
const scoreClass = item.gwoeScore >= 8 ? 'score-high' : item.gwoeScore >= 5 ? 'score-mid' : item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
||
const blBadge = item.bundesland ? `<span class="bl-badge">${item.bundesland}</span>` : '';
|
||
return `
|
||
<div class="list-item" onclick="showMode('browse'); setTimeout(() => showDetail('${item.drucksache}'), 100);" style="cursor:pointer;">
|
||
<div class="list-item-header">
|
||
<span class="list-item-id">${blBadge}${item.drucksache}</span>
|
||
<span class="list-item-score ${scoreClass}">${item.gwoeScore}/10</span>
|
||
</div>
|
||
<div class="list-item-title">${(item.title || '').substring(0, 80)}${(item.title || '').length > 80 ? '…' : ''}</div>
|
||
<div class="list-item-meta">${(item.fraktionen || []).join(', ')} · ${item.datum || ''}</div>
|
||
</div>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
container.innerHTML = '<p style="color:#c00;">Fehler beim Laden der Merkliste.</p>';
|
||
}
|
||
}
|
||
|
||
// ─── Bookmarks + Comments (#94) ─────────────────────────────
|
||
async function toggleBookmark(drucksache, btn) {
|
||
const resp = await fetch('/api/bookmark', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}`
|
||
});
|
||
if (resp.ok) {
|
||
const data = await resp.json();
|
||
btn.textContent = data.bookmarked ? '⭐ Gemerkt' : '🔖 Merken';
|
||
btn.style.background = data.bookmarked ? '#fff3cd' : 'none';
|
||
}
|
||
}
|
||
|
||
async function loadBookmarkState(drucksache) {
|
||
if (!currentUser) return;
|
||
const bookmarks = await fetch('/api/bookmarks').then(r => r.json());
|
||
const btn = document.getElementById('bookmark-btn-' + drucksache.replace('/', '-'));
|
||
if (btn && bookmarks.includes(drucksache)) {
|
||
btn.textContent = '⭐ Gemerkt';
|
||
btn.style.background = '#fff3cd';
|
||
}
|
||
}
|
||
|
||
async function loadComments(drucksache) {
|
||
const container = document.getElementById('comments-' + drucksache.replace('/', '-'));
|
||
if (!container) return;
|
||
try {
|
||
const comments = await fetch(`/api/comments?drucksache=${encodeURIComponent(drucksache)}`).then(r => r.json());
|
||
// Client-seitig filtern nach Sichtbarkeit
|
||
const visible = comments.filter(c => {
|
||
if (c.visibility === 'all') return true;
|
||
if (!currentUser) return false;
|
||
if (c.visibility === 'authenticated') return true;
|
||
if (c.visibility === 'private') return c.user_id === currentUser.sub;
|
||
// group:XYZ — TODO: gegen Keycloak-Gruppen prüfen
|
||
if (c.visibility?.startsWith('group:')) return true; // Platzhalter
|
||
return false;
|
||
});
|
||
if (visible.length === 0) {
|
||
container.innerHTML = '<span style="color:#aaa;font-size:0.85rem;">Noch keine Kommentare.</span>';
|
||
return;
|
||
}
|
||
const visBadge = (v) => {
|
||
if (v === 'private') return '<span style="font-size:0.7rem;background:#f0f0f0;padding:0.1rem 0.3rem;border-radius:2px;" title="Nur für dich sichtbar">👤</span>';
|
||
if (v === 'authenticated') return '<span style="font-size:0.7rem;background:#e8f4f8;padding:0.1rem 0.3rem;border-radius:2px;" title="Nur für angemeldete Nutzer">🔒</span>';
|
||
if (v?.startsWith('group:')) return `<span style="font-size:0.7rem;background:#f0e6ff;padding:0.1rem 0.3rem;border-radius:2px;" title="Gruppe: ${v.substring(6)}">👥 ${v.substring(6)}</span>`;
|
||
return '<span style="font-size:0.7rem;background:#e8ffe8;padding:0.1rem 0.3rem;border-radius:2px;" title="Öffentlich sichtbar">🌐</span>';
|
||
};
|
||
container.innerHTML = visible.map(c => `
|
||
<div style="padding:0.4rem 0;border-bottom:1px solid #f0f0f0;font-size:0.85rem;">
|
||
<strong>${c.user_name || 'Anonym'}</strong>
|
||
${visBadge(c.visibility)}
|
||
<span style="color:#aaa;font-size:0.75rem;margin-left:0.3rem;">${new Date(c.created_at).toLocaleString('de-DE')}</span>
|
||
${currentUser && currentUser.sub === c.user_id ? `<button onclick="deleteCommentUI(${c.id},'${drucksache}')" style="float:right;background:none;border:none;color:#dc3545;cursor:pointer;font-size:0.75rem;">✕</button>` : ''}
|
||
<div style="margin-top:0.2rem;">${c.text}</div>
|
||
</div>
|
||
`).join('');
|
||
} catch { container.innerHTML = ''; }
|
||
}
|
||
|
||
async function addCommentUI(drucksache) {
|
||
const safeDrs = drucksache.replace('/', '-');
|
||
const input = document.getElementById('comment-input-' + safeDrs);
|
||
const visSelect = document.getElementById('comment-visibility-' + safeDrs);
|
||
if (!input || !input.value.trim()) return;
|
||
const visibility = visSelect ? visSelect.value : 'all';
|
||
await fetch('/api/comment', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}&text=${encodeURIComponent(input.value)}&visibility=${visibility}`
|
||
});
|
||
input.value = '';
|
||
loadComments(drucksache);
|
||
}
|
||
|
||
async function deleteCommentUI(commentId, drucksache) {
|
||
await fetch(`/api/comment/${commentId}`, {method: 'DELETE'});
|
||
loadComments(drucksache);
|
||
}
|
||
|
||
async function reAnalyze(drucksache, bundesland, btn) {
|
||
if (!currentUser) {
|
||
alert('Bitte zuerst anmelden.');
|
||
return;
|
||
}
|
||
btn.disabled = true;
|
||
btn.style.background = '#ffc107';
|
||
btn.textContent = '⏳ Lösche alte Bewertung...';
|
||
try {
|
||
// Altes Assessment löschen
|
||
const delResp = await fetch(`/api/assessment/delete?drucksache=${encodeURIComponent(drucksache)}`, {method: 'DELETE'});
|
||
if (delResp.status === 401) {
|
||
btn.textContent = '🔒 Nicht angemeldet';
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
return;
|
||
}
|
||
btn.textContent = '⏳ Analyse gestartet...';
|
||
// Neue Analyse enqueuen
|
||
const resp = await fetch('/api/analyze-drucksache', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: `drucksache=${encodeURIComponent(drucksache)}&bundesland=${encodeURIComponent(bundesland)}`
|
||
});
|
||
if (resp.status === 401) {
|
||
btn.textContent = '🔒 Nicht angemeldet';
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
if (data.status === 'queued') {
|
||
btn.textContent = '⏳ Wird analysiert...';
|
||
btn.style.background = '#009da5';
|
||
pollAnalysis(data.job_id, drucksache, btn);
|
||
} else {
|
||
btn.textContent = '❌ ' + (data.detail || 'Fehler');
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
}
|
||
} catch (e) {
|
||
btn.textContent = '❌ ' + e.message;
|
||
btn.style.background = '#dc3545';
|
||
setTimeout(() => { btn.textContent = '🔄 Neu bewerten'; btn.style.background = '#6c757d'; btn.disabled = false; }, 3000);
|
||
}
|
||
}
|
||
|
||
async function pollAnalysis(jobId, drucksache, btn) {
|
||
try {
|
||
const resp = await fetch(`/status/${jobId}`);
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'completed') {
|
||
btn.textContent = '✓ Geprüft';
|
||
btn.style.background = '#889e33'; // Green
|
||
// Update this item in current list to show as checked
|
||
const listItem = btn.closest('.list-item');
|
||
if (listItem) {
|
||
listItem.classList.remove('unchecked');
|
||
listItem.onclick = () => showDetail(drucksache);
|
||
}
|
||
// Reload assessments in background (for internal list)
|
||
loadAssessments();
|
||
// Show detail for this item
|
||
setTimeout(() => showDetail(drucksache), 500);
|
||
} else if (data.status === 'failed') {
|
||
btn.textContent = '✗ Fehlgeschlagen';
|
||
btn.disabled = false;
|
||
} else {
|
||
setTimeout(() => pollAnalysis(jobId, drucksache, btn), 2000);
|
||
}
|
||
} catch (e) {
|
||
btn.textContent = '✗ Fehler';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function changeBundesland(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 = '';
|
||
const landtagInput = document.getElementById('landtag-search-input');
|
||
if (landtagInput) landtagInput.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();
|
||
}
|
||
|
||
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;
|
||
}
|
||
// #16: Landtag-Suche zieht aus dem dedizierten Landtag-Input,
|
||
// nicht mehr aus dem DB-Suchfeld.
|
||
const query = document.getElementById('landtag-search-input').value.trim();
|
||
if (query.length < 2) {
|
||
alert('Bitte mindestens 2 Zeichen ins Landtag-Suchfeld eingeben');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('btn-landtag');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Suche...';
|
||
|
||
document.getElementById('list-content').innerHTML = `
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>Suche im Landtag ${currentBundesland}...</span>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/search-landtag?q=${encodeURIComponent(query)}&bundesland=${currentBundesland}`);
|
||
const results = await resp.json();
|
||
|
||
if (results.error) {
|
||
document.getElementById('list-content').innerHTML = `
|
||
<p style="padding: 1rem; color: #888;">${results.error}</p>
|
||
`;
|
||
} else if (results.length === 0) {
|
||
document.getElementById('list-content').innerHTML = `
|
||
<p style="padding: 1rem; color: #888;">Keine Treffer im Landtag für "${query}"</p>
|
||
`;
|
||
} else {
|
||
// Merge with checked status from DB
|
||
const checkedIds = new Set(allAssessments.map(a => a.drucksache));
|
||
const merged = results.map(r => ({
|
||
...r,
|
||
gwoeScore: checkedIds.has(r.drucksache)
|
||
? allAssessments.find(a => a.drucksache === r.drucksache)?.gwoeScore
|
||
: null,
|
||
status: checkedIds.has(r.drucksache) ? 'checked' : 'unchecked'
|
||
}));
|
||
renderList(merged);
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('list-content').innerHTML = `
|
||
<p style="padding: 1rem; color: #888;">Suchfehler: ${e.message}</p>
|
||
`;
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = '🔍 Im Landtag';
|
||
}
|
||
|
||
function filterList(query) {
|
||
const q = query.toLowerCase();
|
||
let filtered = allAssessments.filter(a =>
|
||
(a.title || '').toLowerCase().includes(q) ||
|
||
(a.drucksache || '').toLowerCase().includes(q) ||
|
||
(a.fraktionen || []).join(' ').toLowerCase().includes(q) ||
|
||
(a.themen || []).join(' ').toLowerCase().includes(q)
|
||
);
|
||
|
||
// Score-Filter anwenden
|
||
if (currentScoreFilter !== 'all') {
|
||
filtered = applyScoreFilter(filtered, currentScoreFilter);
|
||
}
|
||
|
||
// Partei-Filter anwenden
|
||
if (currentParteiFilter) {
|
||
filtered = filtered.filter(a =>
|
||
(a.fraktionen || []).includes(currentParteiFilter)
|
||
);
|
||
}
|
||
|
||
renderList(sortAssessments(filtered));
|
||
}
|
||
|
||
function setScoreFilter(filter, btn) {
|
||
currentScoreFilter = filter;
|
||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
applyAllFilters();
|
||
}
|
||
|
||
function setParteiFilter(partei) {
|
||
currentParteiFilter = partei;
|
||
applyAllFilters();
|
||
}
|
||
|
||
function applyAllFilters() {
|
||
let filtered = allAssessments;
|
||
|
||
// Score-Filter
|
||
if (currentScoreFilter !== 'all') {
|
||
filtered = applyScoreFilter(filtered, currentScoreFilter);
|
||
}
|
||
|
||
// Partei-Filter
|
||
if (currentParteiFilter) {
|
||
filtered = filtered.filter(a =>
|
||
(a.fraktionen || []).includes(currentParteiFilter)
|
||
);
|
||
}
|
||
|
||
renderList(sortAssessments(filtered));
|
||
}
|
||
|
||
function applyScoreFilter(items, filter) {
|
||
switch (filter) {
|
||
case 'high': return items.filter(a => a.gwoeScore >= 8);
|
||
case 'mid': return items.filter(a => a.gwoeScore >= 5 && a.gwoeScore < 8);
|
||
case 'low': return items.filter(a => a.gwoeScore < 5);
|
||
default: return items;
|
||
}
|
||
}
|
||
|
||
function showDetail(drucksache) {
|
||
const item = allAssessments.find(a => a.drucksache === drucksache);
|
||
if (!item) return;
|
||
|
||
// Highlight active item
|
||
document.querySelectorAll('.list-item').forEach(el => el.classList.remove('active'));
|
||
// Find and highlight the list item by drucksache
|
||
const listItem = document.querySelector(`.list-item[data-drucksache="${drucksache}"]`);
|
||
if (listItem) listItem.classList.add('active');
|
||
|
||
// Skala 0-10
|
||
const scoreClass = item.gwoeScore >= 8 ? 'score-high' :
|
||
item.gwoeScore >= 5 ? 'score-mid' :
|
||
item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
||
|
||
// Matrix als 5x5-Tabelle wie im PDF
|
||
const matrixData = {};
|
||
(item.gwoeMatrix || []).forEach(m => { matrixData[m.field] = m; });
|
||
|
||
// GWÖ-Matrix Definitionen (aus models.py)
|
||
const rowLabels = {
|
||
'A': 'Ausgelagerte Betriebe, Lieferant:innen',
|
||
'B': 'Finanzpartner:innen, Steuerzahler:innen',
|
||
'C': 'Politische Führung, Verwaltung',
|
||
'D': 'Bürger:innen und Wirtschaft',
|
||
'E': 'Staat, Gesellschaft und Natur'
|
||
};
|
||
const rowTooltips = {
|
||
'A': 'Externe Beschaffung, Lieferketten, Dienstleister:innen',
|
||
'B': 'Umgang mit öffentlichen Mitteln, Haushalt, Geldgeber:innen',
|
||
'C': 'Mandatsträger:innen, Mitarbeitende, Ehrenamtliche',
|
||
'D': 'Wirkung innerhalb der Grenzen, Daseinsvorsorge',
|
||
'E': 'Wirkung über die Grenzen hinaus, Zukunft'
|
||
};
|
||
const colLabels = {
|
||
1: 'Menschenwürde',
|
||
2: 'Solidarität',
|
||
3: 'Ökol. Nachhaltigkeit',
|
||
4: 'Soz. Gerechtigkeit',
|
||
5: 'Transparenz'
|
||
};
|
||
const colFull = {
|
||
1: 'Menschenwürde',
|
||
2: 'Solidarität',
|
||
3: 'Ökologische Nachhaltigkeit',
|
||
4: 'Soziale Gerechtigkeit',
|
||
5: 'Transparenz & Mitbestimmung'
|
||
};
|
||
const colPrinzip = {
|
||
1: 'Rechtsstaatsprinzip — Werden Grundrechte geschützt? Rechtliche Gleichstellung?',
|
||
2: 'Gemeinnutz — Wird das Gemeinwohl gefördert? Mehrwert für die Gemeinschaft?',
|
||
3: 'Umwelt-Verantwortung — Klimaschutz? Ressourcenschonung? Biodiversität?',
|
||
4: 'Sozialstaatsprinzip — Gerechte Verteilung? Daseinsvorsorge? Soziale Absicherung?',
|
||
5: 'Demokratie — Bürgerbeteiligung? Offenlegung? Demokratische Prozesse?'
|
||
};
|
||
const fieldLabels = {{ matrix_labels | tojson }};
|
||
const ratingExplain = (r) => r >= 4 ? '++ stark fördernd' : r >= 1 ? '+ fördernd' : r === 0 ? '○ neutral' : r >= -3 ? '− widersprechend' : '−− stark widersprechend';
|
||
|
||
// Bürger:innen-Erklärungen pro Feld
|
||
const fieldExplain = {{ matrix_explanations | tojson }};
|
||
|
||
let matrixTableHtml = '<table class="matrix-table"><thead><tr><th style="min-width:120px;"></th>';
|
||
for (let col = 1; col <= 5; col++) matrixTableHtml += `<th style="cursor:pointer;" onclick="showColumnInfo(${col})">${col}. ${colLabels[col]}</th>`;
|
||
matrixTableHtml += '</tr></thead><tbody>';
|
||
|
||
['A', 'B', 'C', 'D', 'E'].forEach(row => {
|
||
matrixTableHtml += `<tr><th style="cursor:pointer;text-align:left;font-size:0.8rem;" onclick="showRowInfo('${row}')">${row}: ${rowLabels[row]}</th>`;
|
||
for (let col = 1; col <= 5; col++) {
|
||
const field = `${row}${col}`;
|
||
const entry = matrixData[field];
|
||
const fullLabel = fieldLabels[field] || field;
|
||
if (entry) {
|
||
const cssClass = entry.rating > 0 ? 'positive' : (entry.rating < 0 ? 'negative' : 'neutral');
|
||
// Aspect in data-Attribut speichern statt inline JS-String
|
||
matrixTableHtml += `<td class="${cssClass}" style="cursor:pointer;" data-field="${field}" data-aspect="${(entry.aspect||'').replace(/"/g,'"')}" data-rating="${entry.rating}" onclick="showFieldInfo(this.dataset.field, this.dataset.aspect, Number(this.dataset.rating))">${entry.symbol}</td>`;
|
||
} else {
|
||
matrixTableHtml += `<td style="cursor:pointer;color:#ccc;" onclick="showFieldInfo('${field}', '', 0)">○</td>`;
|
||
}
|
||
}
|
||
matrixTableHtml += '</tr>';
|
||
});
|
||
matrixTableHtml += '</tbody></table>';
|
||
|
||
// Detail-Liste der bewerteten Felder mit vollen Beschreibungen
|
||
const matrixDetailHtml = (item.gwoeMatrix || []).map(m => `
|
||
<div class="matrix-item" title="${ratingExplain(m.rating)}">
|
||
<span class="matrix-label"><strong>${m.field}</strong> ${fieldLabels[m.field] || m.label}: <em>${m.aspect || ''}</em></span>
|
||
<span class="matrix-rating ${m.rating > 0 ? 'rating-pos' : m.rating < 0 ? 'rating-neg' : 'rating-neutral'}">${m.symbol} (${m.rating})</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
const stärkenHtml = (item.stärken || []).map(s => `<li>${s}</li>`).join('');
|
||
const schwächenHtml = (item.schwächen || []).map(s => `<li>${s}</li>`).join('');
|
||
|
||
// Verbesserungsvorschläge formatieren
|
||
const verbesserungenHtml = (item.verbesserungen || []).map(v => {
|
||
// Redline-Format: **fett** = grün/neu, ~~durchgestrichen~~ = rot/gelöscht
|
||
let vorschlag = v.vorschlag || '';
|
||
vorschlag = vorschlag.replace(/\*\*([^*]+)\*\*/g, '<span style="color: #889e33; font-weight: bold;">$1</span>');
|
||
vorschlag = vorschlag.replace(/~~([^~]+)~~/g, '<span style="color: #d00000; text-decoration: line-through;">$1</span>');
|
||
|
||
return `
|
||
<div style="margin: 0.75rem 0; padding: 0.75rem; border: 1px solid var(--color-lightgray); border-radius: 4px;">
|
||
<div style="background: #f5f5f5; padding: 0.5rem; margin-bottom: 0.5rem; font-size: 0.9rem;">
|
||
<strong>Original:</strong><br>${v.original || '-'}
|
||
</div>
|
||
<div style="background: rgba(136, 158, 51, 0.1); border-left: 3px solid #889e33; padding: 0.5rem; font-size: 0.9rem;">
|
||
<strong>Vorschlag:</strong><br>${vorschlag}
|
||
</div>
|
||
<div style="font-size: 0.85rem; color: #666; margin-top: 0.5rem; font-style: italic;">
|
||
${v.begruendung || ''}
|
||
</div>
|
||
</div>
|
||
`}).join('');
|
||
|
||
// Issue #47: Zitat-URLs zu Cite-Endpoint umschreiben für gelbes
|
||
// Highlighting. Funktioniert retroaktiv für Pre-#47-Assessments
|
||
// (statische /static/referenzen/X.pdf#page=N) und nativ für
|
||
// Post-#47 (die schon /api/wahlprogramm-cite enthalten).
|
||
// makeCiteUrl baut die Highlight-URL und hängt ds+bl an,
|
||
// damit der Server bei nicht-auffindbaren Zitaten automatisch
|
||
// eine Re-Analyse triggern kann (#47 + #60).
|
||
function makeCiteUrl(z, ds, bl) {
|
||
if (!z || !z.url) return '#';
|
||
const extra = (ds && bl) ? `&ds=${encodeURIComponent(ds)}&bl=${encodeURIComponent(bl)}` : '';
|
||
// Schon eine Cite-URL? ds/bl anhängen + #page=N.
|
||
if (z.url.includes('/api/wahlprogramm-cite')) {
|
||
const m = z.url.match(/seite=(\d+)/);
|
||
const page = m ? m[1] : '';
|
||
const base = z.url.split('#')[0];
|
||
return base + extra + (page ? '#page=' + page : '');
|
||
}
|
||
// Statische URL umschreiben
|
||
const m = z.url.match(/\/static\/referenzen\/(.+\.pdf)#page=(\d+)/);
|
||
if (m && z.text) {
|
||
const pdf = m[1];
|
||
const page = m[2];
|
||
const q = encodeURIComponent((z.text || '').substring(0, 200));
|
||
return `/api/wahlprogramm-cite?pdf=${encodeURIComponent(pdf)}&seite=${page}&q=${q}${extra}#page=${page}`;
|
||
}
|
||
return z.url;
|
||
}
|
||
|
||
const wahlprogrammHtml = (item.wahlprogrammScores || []).map(wp => {
|
||
// Zitate formatieren mit klickbaren Links + Highlighting
|
||
const zitateHtml = (wp.wahlprogramm?.zitate || []).map(z => {
|
||
const isVerified = z.verified !== false;
|
||
const borderColor = isVerified ? '#889e33' : '#ffc107';
|
||
const bgColor = isVerified ? '#f8f9fa' : '#fffbf0';
|
||
const badge = isVerified
|
||
? '<span style="font-size:0.7rem;color:#889e33;">✓ verifiziert</span>'
|
||
: '<span style="font-size:0.7rem;color:#b8860b;">~ paraphrasiert (nicht wörtlich im Programm)</span>';
|
||
return `
|
||
<div style="margin: 0.5rem 0; padding: 0.5rem; background: ${bgColor}; border-left: 3px ${isVerified ? 'solid' : 'dashed'} ${borderColor}; font-size: 0.85rem;">
|
||
<em>"${z.text}"</em><br>
|
||
${z.quelle ? `<a href="${makeCiteUrl(z, item.drucksache, item.bundesland)}" target="_blank" style="color: #009da5; font-size: 0.8rem;">
|
||
📄 ${z.quelle}
|
||
</a>` : ''}
|
||
${badge}
|
||
</div>`;
|
||
}).join('');
|
||
|
||
// Issue #63: Transparenz-Warnung bei Score > 0 ohne Zitate.
|
||
// Differenziert zwischen "Score 0 = keine Quellen" (LLM hat
|
||
// Force-Honesty befolgt) und "Score > 0 aber 0 Zitate" (LLM
|
||
// hat trotzdem geratet oder Zitate wurden von reconstruct_zitate
|
||
// verworfen). Nur bei Scores > 0 warnen, weil Score 0 schon
|
||
// selbsterklärend ist.
|
||
const wpScore = wp.wahlprogramm?.score ?? 0;
|
||
const wpZitateCount = (wp.wahlprogramm?.zitate || []).length;
|
||
const noQuotesWarning = (wpScore > 0 && wpZitateCount === 0) ? `
|
||
<div style="margin: 0.5rem 0; padding: 0.5rem; background: #fff3cd; border-left: 3px solid #ffc107; font-size: 0.8rem; color: #856404;">
|
||
⚠ Zu diesem Themenkomplex konnten keine konkreten Formulierungen im Wahlprogramm gefunden werden — Score basiert auf der allgemeinen Programmatik der Partei.
|
||
</div>
|
||
` : '';
|
||
|
||
// Labels: Antragsteller:in (aus item.fraktionen) und Landesregierung
|
||
// istAntragsteller/istRegierung aus dem LLM ist oft null — ableiten.
|
||
const isAntragsteller = (item.fraktionen || []).some(f => normalizePartei(f) === normalizePartei(wp.fraktion));
|
||
const roleLabels = [];
|
||
if (wp.istAntragsteller || isAntragsteller) roleLabels.push('<span style="background:#889e33;color:white;padding:0.1rem 0.4rem;border-radius:3px;font-size:0.75rem;">Antragsteller:in</span>');
|
||
if (wp.istRegierung) roleLabels.push('<span style="background:#009da5;color:white;padding:0.1rem 0.4rem;border-radius:3px;font-size:0.75rem;">Landesregierung</span>');
|
||
|
||
return `
|
||
<div style="margin: 0.75rem 0; padding: 0.75rem; background: var(--color-bg); border-radius: 4px;">
|
||
<strong>${wp.fraktion}</strong> ${roleLabels.join(' ')}<br>
|
||
<div style="margin: 0.5rem 0;">
|
||
Wahlprogramm: <strong>${wp.wahlprogramm?.score || '-'}/10</strong> ·
|
||
Parteiprogramm: <strong>${wp.parteiprogramm?.score || '-'}/10</strong>
|
||
</div>
|
||
${wp.wahlprogramm?.begründung ? `<div style="font-size: 0.9rem; color: #555; margin-bottom: 0.5rem;">${wp.wahlprogramm.begründung}</div>` : ''}
|
||
${noQuotesWarning}
|
||
${zitateHtml}
|
||
</div>
|
||
`}).join('');
|
||
|
||
document.getElementById('detail-panel').innerHTML = `
|
||
<div class="detail-card">
|
||
<button class="btn-back-mobile" onclick="document.querySelector('.list-panel').scrollIntoView({behavior:'smooth', block:'start'})">← Zur Liste</button>
|
||
<div class="detail-header">
|
||
<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 || ''}${item.updatedAt ? ` · Bewertet ${new Date(item.updatedAt).toLocaleDateString('de-DE')}` : ''}</div>
|
||
</div>
|
||
<div class="score-display">
|
||
<div class="score-big ${scoreClass}">${item.gwoeScore}</div>
|
||
<div class="score-label">GWÖ-Score</div>
|
||
</div>
|
||
</div>
|
||
|
||
${item.themen && item.themen.length > 0 ? `
|
||
<div class="themen-tags" style="margin-bottom: 1rem;">
|
||
${item.themen.map(t => `<span class="tag">${t}</span>`).join(' ')}
|
||
</div>
|
||
` : ''}
|
||
|
||
<h3 class="section-title">Zusammenfassung</h3>
|
||
<div class="text-block">${item.antragZusammenfassung || '-'}</div>
|
||
|
||
${item.antragKernpunkte && item.antragKernpunkte.length > 0 ? `
|
||
<h3 class="section-title">Kernpunkte</h3>
|
||
<ul class="kernpunkte-list">${item.antragKernpunkte.map(k => `<li>${k}</li>`).join('')}</ul>
|
||
` : ''}
|
||
|
||
<h3 class="section-title">GWÖ-Begründung</h3>
|
||
<div class="text-block">${item.gwoeBegründung || '-'}</div>
|
||
|
||
${item.gwoeSchwerpunkt ? `
|
||
<h3 class="section-title">GWÖ-Schwerpunkt</h3>
|
||
<div class="text-block">${item.gwoeSchwerpunkt}</div>
|
||
` : ''}
|
||
|
||
${item.gwoeMatrix && item.gwoeMatrix.length > 0 ? `
|
||
<h3 class="section-title">GWÖ-Matrix</h3>
|
||
${matrixTableHtml}
|
||
<div class="matrix-grid" style="margin-top: 0.75rem;">${matrixDetailHtml}</div>
|
||
` : ''}
|
||
|
||
${wahlprogrammHtml ? `
|
||
<h3 class="section-title">Programmtreue</h3>
|
||
${wahlprogrammHtml}
|
||
` : ''}
|
||
|
||
${stärkenHtml ? `
|
||
<h3 class="section-title">Stärken</h3>
|
||
<ul class="strength-list">${stärkenHtml}</ul>
|
||
` : ''}
|
||
|
||
${schwächenHtml ? `
|
||
<h3 class="section-title">Schwächen</h3>
|
||
<ul class="weakness-list">${schwächenHtml}</ul>
|
||
` : ''}
|
||
|
||
${verbesserungenHtml ? `
|
||
<h3 class="section-title">Verbesserungsvorschläge</h3>
|
||
${verbesserungenHtml}
|
||
` : ''}
|
||
|
||
${item.verbesserungspotenzial ? `
|
||
<h3 class="section-title">Verbesserungspotenzial</h3>
|
||
<div class="text-block">${item.verbesserungspotenzial}</div>
|
||
` : ''}
|
||
|
||
<h3 class="section-title">Empfehlung</h3>
|
||
<div class="text-block">
|
||
<strong>${item.empfehlungSymbol || ''} ${item.empfehlung || '-'}</strong>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem;">
|
||
<a href="${item.link}" target="_blank" class="btn-pdf">📄 Original-PDF</a>
|
||
<a href="/api/assessment/pdf?drucksache=${encodeURIComponent(item.drucksache)}" class="btn-pdf" style="background: var(--color-green);">📥 GWÖ-Report</a>
|
||
<button class="btn-pdf" style="background: #6c757d; border: none; cursor: pointer;"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung verfügbar" style="background:#6c757d;opacity:0.5;cursor:not-allowed;"'}
|
||
onclick="reAnalyze('${item.drucksache}', '${item.bundesland}', this)">
|
||
🔄 Neu bewerten
|
||
</button>
|
||
</div>
|
||
<div style="margin-top: 0.75rem; font-size: 0.8rem; color: #999; border-top: 1px solid #eee; padding-top: 0.5rem;">
|
||
Bewertet am ${item.updatedAt ? new Date(item.updatedAt).toLocaleDateString('de-DE') + ', ' + new Date(item.updatedAt).toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit'}) + ' Uhr' : '–'}
|
||
${item.source ? ` · Quelle: ${item.source}` : ''}
|
||
${item.model ? ` · Modell: ${item.model}` : ''}
|
||
</div>
|
||
|
||
<!-- Bookmark + Kommentare (#94) -->
|
||
<div style="margin-top: 1rem; border-top: 2px solid var(--color-lightgray); padding-top: 1rem;">
|
||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
|
||
<button id="bookmark-btn-${item.drucksache.replace('/','-')}"
|
||
style="background:none;border:1px solid #ddd;border-radius:4px;padding:0.3rem 0.8rem;cursor:pointer;font-size:0.9rem;"
|
||
onclick="toggleBookmark('${item.drucksache}', this)"
|
||
${currentUser ? '' : 'disabled title="Nur nach Anmeldung"'}>
|
||
🔖 Merken
|
||
</button>
|
||
<span style="font-size: 0.85rem; color: #888;">Kommentare:</span>
|
||
</div>
|
||
<div id="comments-${item.drucksache.replace('/','-')}" style="margin-bottom: 0.75rem;">
|
||
<span style="color:#aaa;font-size:0.85rem;">Lade Kommentare...</span>
|
||
</div>
|
||
${currentUser ? `
|
||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
|
||
<input id="comment-input-${item.drucksache.replace('/','-')}" type="text"
|
||
placeholder="Kommentar schreiben..."
|
||
style="flex:1;min-width:150px;padding:0.4rem;border:1px solid #ddd;border-radius:4px;font-size:0.85rem;"
|
||
onkeydown="if(event.key==='Enter')addCommentUI('${item.drucksache}')">
|
||
<select id="comment-visibility-${item.drucksache.replace('/','-')}"
|
||
style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;font-size:0.8rem;color:#666;">
|
||
<option value="all">🌐 Öffentlich</option>
|
||
<option value="authenticated">🔒 Angemeldete</option>
|
||
<option value="private">👤 Nur ich</option>
|
||
</select>
|
||
<button onclick="addCommentUI('${item.drucksache}')"
|
||
style="padding:0.4rem 0.8rem;background:var(--color-blue);color:white;border:none;border-radius:4px;cursor:pointer;font-size:0.85rem;">
|
||
Senden
|
||
</button>
|
||
</div>
|
||
` : '<span style="font-size:0.8rem;color:#aaa;">Anmelden um zu kommentieren</span>'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Kommentare + Bookmark-Status laden
|
||
loadComments('${item.drucksache}');
|
||
loadBookmarkState('${item.drucksache}');
|
||
|
||
// Auf Mobile: zum Detail-Panel scrollen, damit der gewählte Antrag sichtbar wird
|
||
if (window.matchMedia('(max-width: 900px)').matches) {
|
||
document.getElementById('detail-panel').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
}
|
||
|
||
// Mode Toggle
|
||
function showMode(mode) {
|
||
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
||
if (event && event.currentTarget) {
|
||
event.currentTarget.classList.add('active');
|
||
} else {
|
||
// Find and activate the right button
|
||
document.querySelectorAll('.mode-btn').forEach(b => {
|
||
if (b.textContent.includes(mode === 'browse' ? 'Durchsuchen' : mode === 'tags' ? 'Tags' : 'Prüfen')) {
|
||
b.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
document.getElementById('browse-mode').style.display = mode === 'browse' ? 'flex' : 'none';
|
||
document.getElementById('bookmarks-mode').style.display = mode === 'bookmarks' ? 'flex' : 'none';
|
||
document.getElementById('tags-mode').style.display = mode === 'tags' ? 'flex' : 'none';
|
||
document.getElementById('upload-mode').style.display = mode === 'upload' ? 'flex' : 'none';
|
||
if (mode === 'bookmarks') loadBookmarksList();
|
||
}
|
||
|
||
// Upload Tab Toggle
|
||
function showTab(tab) {
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
event.currentTarget.classList.add('active');
|
||
|
||
document.getElementById('tab-text').style.display = tab === 'text' ? 'block' : 'none';
|
||
document.getElementById('tab-file').style.display = tab === 'file' ? 'block' : 'none';
|
||
}
|
||
|
||
function handleFile(input) {
|
||
if (input.files[0]) {
|
||
document.getElementById('file-name').textContent = input.files[0].name;
|
||
}
|
||
}
|
||
|
||
async function startAnalysis() {
|
||
const btn = document.getElementById('analyze-btn');
|
||
const statusDiv = document.getElementById('analysis-status');
|
||
const resultDiv = document.getElementById('analysis-result');
|
||
|
||
const text = document.getElementById('antrag-text').value;
|
||
const file = document.getElementById('file-input').files[0];
|
||
const bundesland = document.getElementById('bundesland').value;
|
||
|
||
if (!text && !file) {
|
||
alert('Bitte Text eingeben oder PDF hochladen');
|
||
return;
|
||
}
|
||
if (!bundesland) {
|
||
alert('Bitte ein Bundesland wählen.');
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
statusDiv.style.display = 'block';
|
||
resultDiv.style.display = 'none';
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
if (text) formData.append('text', text);
|
||
if (file) formData.append('file', file);
|
||
formData.append('bundesland', bundesland);
|
||
formData.append('model', 'qwen-plus');
|
||
|
||
const resp = await fetch('/analyze', { method: 'POST', body: formData });
|
||
const data = await resp.json();
|
||
|
||
if (data.job_id) {
|
||
pollStatus(data.job_id);
|
||
} else {
|
||
throw new Error(data.detail || 'Fehler');
|
||
}
|
||
} catch (e) {
|
||
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: ${e.message}</p>`;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function pollStatus(jobId) {
|
||
const statusText = document.getElementById('status-text');
|
||
const statusDiv = document.getElementById('analysis-status');
|
||
const resultDiv = document.getElementById('analysis-result');
|
||
const btn = document.getElementById('analyze-btn');
|
||
|
||
try {
|
||
const resp = await fetch(`/status/${jobId}`);
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'completed') {
|
||
statusDiv.style.display = 'none';
|
||
resultDiv.innerHTML = `
|
||
<p style="color: var(--color-green);">✓ Analyse abgeschlossen!</p>
|
||
<a href="/result/${jobId}" class="btn-pdf" style="background: var(--color-green);">📊 Ergebnis ansehen</a>
|
||
<a href="/result/${jobId}/pdf" class="btn-pdf">📄 PDF herunterladen</a>
|
||
`;
|
||
resultDiv.style.display = 'block';
|
||
btn.disabled = false;
|
||
} else if (data.status === 'failed') {
|
||
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: Analyse fehlgeschlagen. Bitte erneut versuchen.</p>`;
|
||
btn.disabled = false;
|
||
} else {
|
||
statusText.textContent = `Analysiere... (${data.status})`;
|
||
setTimeout(() => pollStatus(jobId), 2000);
|
||
}
|
||
} catch (e) {
|
||
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: ${e.message}</p>`;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
</script>
|
||
<!-- Queue-Panel Overlay -->
|
||
<div id="queue-panel" style="display:none;position:fixed;top:0;right:0;width:400px;height:100vh;background:white;box-shadow:-4px 0 12px rgba(0,0,0,0.15);z-index:200;overflow-y:auto;padding:1.5rem;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||
<h3 style="color:var(--color-blue);margin:0;">📊 Queue</h3>
|
||
<button onclick="document.getElementById('queue-panel').style.display='none'" style="background:none;border:none;font-size:1.2rem;cursor:pointer;">✕</button>
|
||
</div>
|
||
<div id="queue-panel-content"><span style="color:#aaa;">Lade...</span></div>
|
||
</div>
|
||
|
||
<!-- Batch-Panel Overlay -->
|
||
<div id="batch-panel" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:450px;background:white;box-shadow:0 8px 24px rgba(0,0,0,0.2);z-index:200;border-radius:8px;padding:1.5rem;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||
<h3 style="color:var(--color-blue);margin:0;">📦 Batch-Analyse</h3>
|
||
<button onclick="document.getElementById('batch-panel').style.display='none'" style="background:none;border:none;font-size:1.2rem;cursor:pointer;">✕</button>
|
||
</div>
|
||
<p style="font-size:0.85rem;color:#666;margin-bottom:1rem;">Analysiert automatisch die neuesten ungeprüften Anträge.</p>
|
||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||
<select id="batch-bl-modal" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
|
||
{% for bl in bundeslaender if bl.code != 'ALL' and bl.active %}
|
||
<option value="{{ bl.code }}">{{ bl.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<select id="batch-limit-modal" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
|
||
<option value="5">5</option>
|
||
<option value="10" selected>10</option>
|
||
<option value="20">20</option>
|
||
<option value="50">50</option>
|
||
</select>
|
||
<button onclick="startBatchModal()" style="padding:0.4rem 1rem;background:var(--color-green);color:white;border:none;border-radius:4px;cursor:pointer;">🚀 Starten</button>
|
||
</div>
|
||
<div id="batch-modal-status" style="margin-top:0.75rem;font-size:0.85rem;"></div>
|
||
</div>
|
||
|
||
<script>
|
||
function loadQueuePanel() {
|
||
const el = document.getElementById('queue-panel-content');
|
||
async function refresh() {
|
||
try {
|
||
const qs = await fetch('/api/queue/status').then(r => r.json());
|
||
const jobs = qs.jobs || [];
|
||
if (jobs.length === 0 && qs.pending === 0) {
|
||
el.innerHTML = '<p style="color:#888;">Keine Aufträge in der Warteschlange.</p>';
|
||
return;
|
||
}
|
||
const completed = jobs.filter(j => j.status === 'completed').length;
|
||
const pct = jobs.length > 0 ? Math.round(completed / jobs.length * 100) : 0;
|
||
el.innerHTML = `
|
||
<div style="margin-bottom:0.5rem;font-size:0.85rem;color:#666;">
|
||
${qs.concurrency} Worker · ${qs.pending} wartend · ${qs.processed_total} verarbeitet
|
||
${qs.shutting_down ? '<br><span style="color:#dc3545;">⚠ Server wird heruntergefahren</span>' : ''}
|
||
</div>
|
||
<div style="background:#eee;border-radius:4px;height:6px;margin-bottom:0.75rem;">
|
||
<div style="background:var(--color-green);height:100%;width:${pct}%;transition:width 0.5s;border-radius:4px;"></div>
|
||
</div>
|
||
<table style="width:100%;font-size:0.8rem;border-collapse:collapse;">
|
||
${jobs.map(j => {
|
||
const icon = j.status === 'completed' ? '✅' : j.status === 'processing' ? '⏳' : j.status === 'failed' ? '❌' : '⏸';
|
||
return '<tr style="border-top:1px solid #f0f0f0;"><td>' + (j.drucksache || j.job_id.substring(0,8)) + '</td><td>' + icon + '</td><td>' + (j.duration ? j.duration + 's' : '') + '</td></tr>';
|
||
}).join('')}
|
||
</table>`;
|
||
} catch { el.innerHTML = '<span style="color:#c00;">Fehler</span>'; }
|
||
}
|
||
refresh();
|
||
// Auto-refresh solange Panel offen
|
||
const iv = setInterval(() => {
|
||
if (document.getElementById('queue-panel').style.display === 'none') { clearInterval(iv); return; }
|
||
refresh();
|
||
}, 3000);
|
||
}
|
||
|
||
async function startBatchModal() {
|
||
const bl = document.getElementById('batch-bl-modal').value;
|
||
const limit = document.getElementById('batch-limit-modal').value;
|
||
const status = document.getElementById('batch-modal-status');
|
||
status.innerHTML = '<span style="color:var(--color-blue);">⏳ Wird gestartet...</span>';
|
||
try {
|
||
const resp = await fetch('/api/batch-analyze', {
|
||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: 'bundesland=' + bl + '&limit=' + limit
|
||
});
|
||
if (resp.status === 401 || resp.status === 403) {
|
||
status.innerHTML = '<span style="color:#dc3545;">🔒 Admin-Berechtigung erforderlich.</span>';
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
status.innerHTML = '<span style="color:var(--color-green);">✓ ' + (data.enqueued || 0) + ' Anträge eingereiht. Queue-Panel öffnen um Fortschritt zu sehen.</span>';
|
||
} catch (e) {
|
||
status.innerHTML = '<span style="color:#dc3545;">❌ ' + e.message + '</span>';
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|