gwoe-antragspruefer/app/templates/v2/screens/durchsuchen.html
Dotty Dotter 553e99d14e feat(v2): globaler BL-Selector im Header + Auth-gated Sidebar + Queue-Widget
Bundesland-Auswahl:
- Topbar: einziger BL-Selektor mit localStorage.gwoe.bl-Persistenz
- BL-Felder entfernt aus durchsuchen.html, landtag_suche.html, neu.html, auswertungen.html
- Screens hoeren auf v2-bl-changed CustomEvent + initial via window.v2GetGlobalBl()

Sichtbarkeit (Sidebar):
- Durchsuchen + Tags: immer
- Merkliste / Neuer Antrag / Landtag-Suche / Auswertungen / Export / Feed: nur eingeloggt
- Cluster + Batch-Analyse + Administration: nur Admin

Server-Side Schutz:
- _v2_template_context()-Helper liefert is_authenticated, is_admin, v2_bundeslaender
- HTML-Routen mit Depends(require_auth) bzw. require_admin
- 401/403-Browser-Requests redirecten auf /?login=1 statt JSON-Error

Queue-Widget (#149):
- Neues Component-Partial v2/components/queue_widget.html
- Statusbar unten links + Hover-Tooltip mit den letzten 20 Jobs
- 5s-Polling auf /api/queue/status, blendet sich aus wenn keine Jobs

Smoke-Test angepasst an neue Auth-Erwartungen (302 fuer auth-protected Routen).

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

324 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "v2/base.html" %}
{% from "v2/components/result_row.html" import result_row %}
{% from "v2/components/chip.html" import chip %}
{% block title %}Durchsuchen — GWÖ-Antragsprüfer{% endblock %}
{% set v2_active_nav = "durchsuchen" %}
{% block main %}
{# ── Toolbar: Suche ──────────────────────────────────────────────── #}
{# BL-Filter läuft jetzt über den globalen Selector in der Topbar. #}
<div class="v2-toolbar" id="v2-toolbar" role="toolbar" aria-label="Filter und Suche">
<input class="v2-search"
type="search"
placeholder="Anträge durchsuchen …"
aria-label="Anträge durchsuchen"
id="v2-search-input">
</div>
{# ── Score-Filter + Sortierung ───────────────────────────────────── #}
<div class="v2-toolbar" role="toolbar" aria-label="Score-Filter und Sortierung" style="border:0;padding:4px 0;margin:0 0 8px;position:static;">
<button class="v2-chip active" data-band="ALL" onclick="v2SetBand(this,'ALL')">Alle Scores</button>
<button class="v2-chip" data-band="HIGH" onclick="v2SetBand(this,'HIGH')">Score 810</button>
<button class="v2-chip" data-band="MID" onclick="v2SetBand(this,'MID')">57</button>
<button class="v2-chip" data-band="LOW" onclick="v2SetBand(this,'LOW')">04</button>
<span class="v2-toolbar-sep"></span>
<select id="v2-sort"
aria-label="Sortierung"
onchange="v2ApplySort(this.value)"
style="padding:4px 8px;border:1px solid var(--hairline);border-radius:4px;
font-family:var(--font-mono);font-size:11px;background:var(--surface);
color:var(--ecg-dark);cursor:pointer;">
<option value="score-desc">Score ↓</option>
<option value="score-asc">Score ↑</option>
<option value="date-desc">Datum ↓</option>
<option value="date-asc">Datum ↑</option>
<option value="drucksache-desc">Drs.-Nr. ↓</option>
<option value="drucksache-asc">Drs.-Nr. ↑</option>
<option value="title-asc">Titel AZ</option>
<option value="title-desc">Titel ZA</option>
</select>
</div>
{# ── Ergebnisliste ───────────────────────────────────────────────── #}
<div id="v2-results" role="list">
{% if assessments %}
{% for a in assessments %}
{{ result_row(a) }}
{% endfor %}
{% else %}
<p style="font-family:var(--font-mono);font-size:13px;color:var(--ecg-dark);opacity:0.6;margin-top:32px;">
Noch keine Bewertungen in der Datenbank.
</p>
{% endif %}
</div>{# #v2-results #}
{# ── Empty-State ─────────────────────────────────────────────────── #}
<div id="v2-empty-state" class="v2-kasten outline-green" style="display:none;margin-top:32px;">
<h4>Keine Ergebnisse</h4>
<p>Die aktuelle Filterauswahl liefert keine Treffer.
<button onclick="v2ResetFilters()"
style="background:none;border:none;color:var(--ecg-green);cursor:pointer;font-weight:900;text-decoration:underline;font-size:inherit;font-family:inherit;">
Filter zurücksetzen
</button>
</p>
</div>
{% endblock %}
{% block body_scripts %}
<script>
/* ── v2 Listenfilter ─────────────────────────────────────────────── */
(function () {
var activeBl = 'ALL';
var activeBand = 'ALL';
function getRows() {
return document.querySelectorAll('#v2-results .v2-result-row');
}
function getScore(row) {
var cell = row.querySelector('.v2-score-cell');
return cell ? parseFloat(cell.textContent) || 0 : 0;
}
function getBl(row) {
var state = row.querySelector('.v2-r-state');
return state ? state.textContent.trim().split('·')[0].trim() : '';
}
function applyFilters() {
var q = (document.getElementById('v2-search-input').value || '').toLowerCase().trim();
var rows = getRows();
var visible = 0;
rows.forEach(function (row) {
var score = getScore(row);
var bl = getBl(row);
var text = row.textContent.toLowerCase();
var blOk = (activeBl === 'ALL') || (bl === activeBl);
var bandOk = (activeBand === 'ALL') ||
(activeBand === 'HIGH' && score >= 8) ||
(activeBand === 'MID' && score >= 5 && score < 8) ||
(activeBand === 'LOW' && score < 5);
var qOk = !q || text.includes(q);
var show = blOk && bandOk && qOk;
row.style.display = show ? '' : 'none';
if (show) visible++;
});
var empty = document.getElementById('v2-empty-state');
if (empty) empty.style.display = (visible === 0) ? 'block' : 'none';
}
/* BL-Filter: globaler Selector in der Topbar */
window.addEventListener('v2-bl-changed', function (e) {
activeBl = (e.detail && e.detail.bl) ? e.detail.bl : 'ALL';
applyFilters();
});
window.v2SetBand = function (btn, band) {
activeBand = band;
document.querySelectorAll('[data-band]').forEach(function (b) {
b.classList.toggle('active', b.dataset.band === band);
});
applyFilters();
};
window.v2ResetFilters = function () {
document.getElementById('v2-search-input').value = '';
// BL auf ALL zurücksetzen: globalen Selector aktualisieren
var sel = document.getElementById('v2-global-bl');
if (sel) sel.value = 'ALL';
window.v2SetGlobalBl && window.v2SetGlobalBl('ALL');
v2SetBand(null, 'ALL');
};
document.addEventListener('DOMContentLoaded', function () {
var input = document.getElementById('v2-search-input');
if (input) input.addEventListener('input', applyFilters);
// Gespeicherten BL-Wert beim Laden anwenden
activeBl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
applyFilters();
});
})();
/* ── Sort ────────────────────────────────────────────────────────── */
(function () {
var SORT_KEY = 'gwoe.v2-sort';
function getRowVal(row, field) {
if (field === 'score') {
var cell = row.querySelector('.v2-score-cell');
return cell ? parseFloat(cell.textContent) || 0 : 0;
}
if (field === 'title') {
var titleEl = row.querySelector('.v2-r-title');
return titleEl ? titleEl.textContent.trim().toLowerCase() : '';
}
if (field === 'date') {
var dateEl = row.querySelector('.v2-r-date');
return dateEl ? dateEl.textContent.trim() : '';
}
if (field === 'drucksache') {
var stateEl = row.querySelector('.v2-r-state');
return stateEl ? stateEl.textContent.trim() : '';
}
return '';
}
function sortRows(sortVal) {
var parts = sortVal.split('-');
var field = parts.slice(0, -1).join('-');
var dir = parts[parts.length - 1]; // 'asc' or 'desc'
var container = document.getElementById('v2-results');
if (!container) return;
var rows = Array.from(container.querySelectorAll('.v2-result-row'));
rows.sort(function(a, b) {
var va = getRowVal(a, field);
var vb = getRowVal(b, field);
var cmp;
if (typeof va === 'number' && typeof vb === 'number') {
cmp = va - vb;
} else {
cmp = String(va).localeCompare(String(vb), 'de');
}
return dir === 'asc' ? cmp : -cmp;
});
rows.forEach(function(row) { container.appendChild(row); });
}
window.v2ApplySort = function(val) {
try { localStorage.setItem(SORT_KEY, val); } catch (_) {}
sortRows(val);
};
document.addEventListener('DOMContentLoaded', function() {
var savedSort;
try { savedSort = localStorage.getItem(SORT_KEY); } catch (_) {}
var sortSelect = document.getElementById('v2-sort');
if (savedSort && sortSelect) {
sortSelect.value = savedSort;
sortRows(savedSort);
}
});
})();
/* ── Keyboard Shortcuts (#116 port to v2) ────────────────────────── */
(function () {
document.addEventListener('keydown', function (e) {
// Nicht in Input-Feldern auslösen
if (e.target.matches('input, textarea, select')) return;
var rows = Array.from(document.querySelectorAll('#v2-results .v2-result-row'));
var active = document.querySelector('#v2-results .v2-result-row.v2-kb-active');
var idx = active ? rows.indexOf(active) : -1;
switch (e.key) {
case 'j': // nächster Eintrag
e.preventDefault();
if (!rows.length) break;
idx = Math.min(idx + 1, rows.length - 1);
if (active) active.classList.remove('v2-kb-active');
rows[idx].classList.add('v2-kb-active');
rows[idx].scrollIntoView({ block: 'nearest' });
break;
case 'k': // vorheriger Eintrag
e.preventDefault();
if (!rows.length) break;
idx = Math.max(idx - 1, 0);
if (active) active.classList.remove('v2-kb-active');
rows[idx].classList.add('v2-kb-active');
rows[idx].scrollIntoView({ block: 'nearest' });
break;
case 'Enter': // Eintrag öffnen
if (active) {
e.preventDefault();
var href = active.getAttribute('href');
if (href) window.location.href = href;
}
break;
case '/': // Suche fokussieren
e.preventDefault();
var searchInput = document.getElementById('v2-search-input');
if (searchInput) searchInput.focus();
break;
case '?': // Shortcuts-Hilfe
e.preventDefault();
var modal = document.getElementById('v2-kb-help-modal');
if (modal) {
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
}
break;
case 'Escape': // Hilfe-Modal schließen (auf Detailseiten: history.back() — dort eigener Handler)
var helpModal = document.getElementById('v2-kb-help-modal');
if (helpModal && helpModal.style.display === 'flex') {
e.preventDefault();
helpModal.style.display = 'none';
}
break;
}
});
})();
</script>
{# ── Keyboard-Hilfe-Modal ───────────────────────────────────────── #}
<div id="v2-kb-help-modal"
role="dialog" aria-modal="true" aria-label="Tastenkürzel"
style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);
z-index:9000;align-items:center;justify-content:center;"
onclick="if(event.target===this)this.style.display='none'">
<div style="background:var(--ecg-card-bg);border:1px solid var(--ecg-border);
border-radius:8px;padding:28px 32px;min-width:280px;max-width:400px;
font-family:var(--font-mono);font-size:13px;color:var(--ecg-dark);">
<div style="font-family:var(--font-display);font-size:15px;font-weight:900;
color:var(--ecg-teal);margin-bottom:16px;letter-spacing:0.03em;">
Tastenkürzel
</div>
<table style="width:100%;border-collapse:collapse;line-height:1.9;">
<tr><td style="width:50px;"><kbd>j</kbd></td><td>Nächster Antrag</td></tr>
<tr><td><kbd>k</kbd></td><td>Vorheriger Antrag</td></tr>
<tr><td><kbd>Enter</kbd></td><td>Antrag öffnen</td></tr>
<tr><td><kbd>Esc</kbd></td><td>Detail schließen / zurück</td></tr>
<tr><td><kbd>/</kbd></td><td>Suche fokussieren</td></tr>
<tr><td><kbd>?</kbd></td><td>Diese Hilfe</td></tr>
</table>
<button onclick="document.getElementById('v2-kb-help-modal').style.display='none'"
style="margin-top:18px;font-family:var(--font-mono);font-size:11px;
background:none;border:1px solid var(--ecg-border);border-radius:4px;
padding:5px 14px;cursor:pointer;color:var(--ecg-dark);">
Schließen
</button>
</div>
</div>
<style>
.v2-kb-active {
outline: 2px solid var(--ecg-teal);
outline-offset: -2px;
border-radius: 4px;
}
kbd {
display: inline-block;
font-family: var(--font-mono);
font-size: 11px;
background: var(--ecg-border);
border: 1px solid color-mix(in srgb, var(--ecg-border) 60%, #000);
border-radius: 3px;
padding: 1px 5px;
color: var(--ecg-dark);
}
</style>
{% endblock %}