v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024): - app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs) - app/templates/v2/: base.html + 11 Screens + 8 Component-Macros - AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features (ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores) - v2 ist jetzt Default unter / — classic unter /classic - Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129) - Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle - Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie, Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze Backend-Erweiterungen: - main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout}, /api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.) - og_card.py + og_template: Open-Graph-Bilder via Playwright (#141) - wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138) - auswertungen.py: BL-Filter + get_wahlperioden Helper (#137) - auth.py: Direct-Access-Grant + Refresh-Token-Cookie Classic-Updates: - Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
3.7 KiB
HTML
119 lines
3.7 KiB
HTML
{% extends "v2/base.html" %}
|
|
|
|
{% block title %}Freischaltungen — GWÖ-Antragsprüfer{% endblock %}
|
|
|
|
{% set v2_active_nav = "admin_freischaltungen" %}
|
|
|
|
{% block main %}
|
|
<div style="padding:0 0 1.5rem;">
|
|
<h1 style="font-family:var(--font-display);font-size:22px;color:var(--ecg-teal);margin:0 0 4px;">Freischaltungen</h1>
|
|
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.6;">
|
|
Ausstehende Registrierungen · nur für Admins
|
|
</p>
|
|
</div>
|
|
|
|
<div id="loading" style="font-family:var(--font-mono);font-size:12px;opacity:0.5;padding:16px 0;">
|
|
Lade ausstehende Freischaltungen …
|
|
</div>
|
|
|
|
<div id="empty" class="v2-kasten outline-green" style="display:none;">
|
|
<h4>Keine ausstehenden Freischaltungen</h4>
|
|
<p>Alle Registrierungen wurden bereits bearbeitet.</p>
|
|
</div>
|
|
|
|
<div id="error" class="v2-kasten outline-blue" style="display:none;">
|
|
<h4>Fehler beim Laden</h4>
|
|
<p id="error-msg"></p>
|
|
</div>
|
|
|
|
<div id="table-wrap" style="display:none;">
|
|
<table class="v2-admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>User-ID</th>
|
|
<th>E-Mail</th>
|
|
<th>Name</th>
|
|
<th>Registriert</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="user-rows"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block body_scripts %}
|
|
<script>
|
|
(async function () {
|
|
const loadingEl = document.getElementById('loading');
|
|
const emptyEl = document.getElementById('empty');
|
|
const errorEl = document.getElementById('error');
|
|
const errorMsgEl = document.getElementById('error-msg');
|
|
const tableEl = document.getElementById('table-wrap');
|
|
const tbodyEl = document.getElementById('user-rows');
|
|
|
|
function showError(msg) {
|
|
loadingEl.style.display = 'none';
|
|
errorMsgEl.textContent = msg;
|
|
errorEl.style.display = '';
|
|
}
|
|
|
|
async function loadUsers() {
|
|
try {
|
|
const resp = await fetch('/api/auth/pending-users');
|
|
if (resp.status === 403) { showError('Zugriff verweigert (kein Admin).'); return; }
|
|
if (!resp.ok) { showError('HTTP ' + resp.status); return; }
|
|
const users = await resp.json();
|
|
loadingEl.style.display = 'none';
|
|
|
|
if (!users.length) { emptyEl.style.display = ''; return; }
|
|
|
|
tbodyEl.innerHTML = users.map(u => `
|
|
<tr id="row-${u.id}">
|
|
<td style="font-family:var(--font-mono);font-size:11px;opacity:0.7;">${u.id}</td>
|
|
<td>${u.email || '—'}</td>
|
|
<td>${[u.firstName, u.lastName].filter(Boolean).join(' ') || '—'}</td>
|
|
<td style="font-family:var(--font-mono);font-size:11px;">${u.createdTimestamp ? new Date(u.createdTimestamp).toLocaleDateString('de-DE') : '—'}</td>
|
|
<td>
|
|
<button class="v2-admin-btn" onclick="approveUser('${u.id}', this)">
|
|
Freischalten
|
|
</button>
|
|
</td>
|
|
</tr>`).join('');
|
|
|
|
tableEl.style.display = '';
|
|
} catch (e) {
|
|
showError(e.message);
|
|
}
|
|
}
|
|
|
|
window.approveUser = async function(userId, btn) {
|
|
btn.disabled = true;
|
|
btn.textContent = '…';
|
|
try {
|
|
const fd = new FormData();
|
|
fd.append('user_id', userId);
|
|
const resp = await fetch('/api/auth/approve-user', { method: 'POST', body: fd });
|
|
if (!resp.ok) {
|
|
const data = await resp.json().catch(() => ({}));
|
|
btn.textContent = 'Fehler: ' + (data.detail || resp.status);
|
|
btn.style.color = 'var(--ecg-blue)';
|
|
return;
|
|
}
|
|
const row = document.getElementById('row-' + userId);
|
|
if (row) {
|
|
row.style.opacity = '0.4';
|
|
row.querySelector('td:last-child').textContent = 'Freigeschaltet ✓';
|
|
}
|
|
} catch (e) {
|
|
btn.textContent = 'Fehler';
|
|
btn.style.color = 'var(--ecg-blue)';
|
|
}
|
|
};
|
|
|
|
await loadUsers();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|