Admin Dashboard und Hilfeseite

This commit is contained in:
Erik Thiele
2026-06-20 16:40:02 +02:00
parent ab1a36dee1
commit e18414ab79
12 changed files with 929 additions and 32 deletions

View File

@@ -33,7 +33,7 @@
<a class="btn btn-outline-secondary me-2" href="javascript:void(0)"
onclick="var n=prompt('Name des neuen Standorts:'); if(n&&n.trim()) location.href='/add-site?name='+encodeURIComponent(n.trim().toLowerCase());"
title="Neuen Standort anlegen"
style="display:inline-flex;align-items:center;justify-content:center;min-width:2.5rem;min-height:2.5rem;"><i class="ti ti-plus"></i></a>
style="display:inline-flex;align-items:center;justify-content:center;min-width:2.5rem;min-height:2.5rem;padding:0;"><i class="ti ti-plus"></i></a>
<!-- Dark-Mode-Toggle (persistiert in localStorage("signage-theme")) -->
<div>
@@ -50,7 +50,9 @@
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="/change-password"><i class="ti ti-key me-1"></i>Passwort ändern</a>
<a class="dropdown-item" href="/admin/help"><i class="ti ti-help me-1"></i>Hilfe</a>
{% if current_user.is_admin %}
<a class="dropdown-item" href="/admin/dashboard"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</a>
<a class="dropdown-item" href="/admin/users"><i class="ti ti-users me-1"></i>Userverwaltung</a>
{% endif %}
<div class="dropdown-divider"></div>

View File

@@ -166,6 +166,23 @@
box-shadow: 0 0 0 0.2rem rgba(218, 0, 45, 0.25);
}
.dropdown-item:hover,
.dropdown-item:focus {
background-color: #e9ecef !important;
color: inherit !important;
}
.dropdown-item.active,
.dropdown-item:active {
background-color: #e9ecef !important;
color: inherit !important;
}
[data-bs-theme="dark"] .dropdown-item:hover,
[data-bs-theme="dark"] .dropdown-item:focus,
[data-bs-theme="dark"] .dropdown-item.active,
[data-bs-theme="dark"] .dropdown-item:active {
background-color: #3a3f45 !important;
}
.dropdown-item.active,
.dropdown-item:active {
background-color: var(--ccm-primary);

View File

@@ -73,24 +73,6 @@
color: var(--ccm-primary);
}
/* ─── Dropdown-Hover (grauer Hintergrund) ─── */
.dropdown-item:hover,
.dropdown-item:focus {
background-color: #e9ecef !important;
color: inherit !important;
}
.dropdown-item.active,
.dropdown-item:active {
background-color: #e9ecef !important;
color: inherit !important;
}
[data-bs-theme="dark"] .dropdown-item:hover,
[data-bs-theme="dark"] .dropdown-item:focus,
[data-bs-theme="dark"] .dropdown-item.active,
[data-bs-theme="dark"] .dropdown-item:active {
background-color: #3a3f45 !important;
}
/* ─── Drag-Handle für SortableJS ─── */
.drag-handle {
cursor: grab;

View File

@@ -0,0 +1,206 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>CANCOM Simple Signage Admin Dashboard</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script>
(() => {
const stored = window.localStorage.getItem("signage-theme");
const theme = stored === "dark" || stored === "light" ? stored : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
})();
</script>
<link rel="stylesheet"
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<script defer
src="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
{% include "_styles.html" %}
</head>
<body>
<div class="page">
{% set brand_subtitle = "Admin Dashboard" %}
{% set site_dropdown_url_prefix = "/admin/" %}
{% include "_header.html" %}
<div class="navbar-expand-md">
<div class="collapse navbar-collapse show">
<div class="navbar navbar-dark nav-surface">
<div class="container-xl">
<div class="d-flex flex-wrap gap-2 py-2">
<a class="btn btn-white" href="/admin/{{ current_site }}"><i class="ti ti-arrow-left me-1"></i>Zurück</a>
<a class="btn btn-white active" href="/admin/dashboard"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</a>
<a class="btn btn-white" href="/admin/users"><i class="ti ti-users me-1"></i>Userverwaltung</a>
<a class="btn btn-white" href="/admin/help"><i class="ti ti-help me-1"></i>Hilfe</a>
</div>
</div>
</div>
</div>
</div>
<div class="page-wrapper">
<div class="container-xl mt-4">
<div class="row g-3">
<div class="col-6 col-lg">
<div class="card card-sm">
<div class="card-body p-3 pb-0">
<div class="d-flex gap-3">
<div class="text-secondary mt-1"><i class="ti ti-building-community" style="font-size:2.2rem;"></i></div>
<div class="flex-grow-1">
<div class="fs-1 fw-bold d-flex align-items-center gap-2">{{ site_count }}{% if site_trend > 0 %}<span class="text-green d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-up"></i>+{{ site_trend }}</span>{% elif site_trend < 0 %}<span class="text-red d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-down"></i>{{ site_trend }}</span>{% endif %}</div>
<div class="text-secondary small">Standorte</div>
</div>
</div>
<div id="spark-sites" class="mx-n3" style="height:36px;"></div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card card-sm">
<div class="card-body p-3 pb-0">
<div class="d-flex gap-3">
<div class="text-secondary mt-1"><i class="ti ti-devices" style="font-size:2.2rem;"></i></div>
<div class="flex-grow-1">
<div class="fs-1 fw-bold d-flex align-items-center gap-2">{{ screen_count }}{% if screen_trend > 0 %}<span class="text-green d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-up"></i>+{{ screen_trend }}</span>{% elif screen_trend < 0 %}<span class="text-red d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-down"></i>{{ screen_trend }}</span>{% endif %}</div>
<div class="text-secondary small">Screens</div>
</div>
</div>
<div id="spark-screens" class="mx-n3" style="height:36px;"></div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card card-sm">
<div class="card-body p-3 pb-0">
<div class="d-flex gap-3">
<div class="text-secondary mt-1"><i class="ti ti-users" style="font-size:2.2rem;"></i></div>
<div class="flex-grow-1">
<div class="fs-1 fw-bold d-flex align-items-center gap-2">{{ user_count }}{% if user_trend > 0 %}<span class="text-green d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-up"></i>+{{ user_trend }}</span>{% elif user_trend < 0 %}<span class="text-red d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-down"></i>{{ user_trend }}</span>{% endif %}</div>
<div class="text-secondary small">User</div>
</div>
</div>
<div id="spark-users" class="mx-n3" style="height:36px;"></div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card card-sm">
<div class="card-body p-3 pb-0">
<div class="d-flex gap-3">
<div class="text-secondary mt-1"><i class="ti ti-shield" style="font-size:2.2rem;"></i></div>
<div class="flex-grow-1">
<div class="fs-1 fw-bold d-flex align-items-center gap-2">{{ admin_count }}{% if admin_trend > 0 %}<span class="text-green d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-up"></i>+{{ admin_trend }}</span>{% elif admin_trend < 0 %}<span class="text-red d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-down"></i>{{ admin_trend }}</span>{% endif %}</div>
<div class="text-secondary small">Admins</div>
</div>
</div>
<div id="spark-admins" class="mx-n3" style="height:36px;"></div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card card-sm">
<div class="card-body p-3 pb-0">
<div class="d-flex gap-3">
<div class="text-secondary mt-1"><i class="ti ti-star" style="font-size:2.2rem;"></i></div>
<div class="flex-grow-1">
<div class="fs-1 fw-bold d-flex align-items-center gap-2">{{ superuser_count }}{% if superuser_trend > 0 %}<span class="text-green d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-up"></i>+{{ superuser_trend }}</span>{% elif superuser_trend < 0 %}<span class="text-red d-flex align-items-center gap-1" style="font-size:0.8rem;"><i class="ti ti-trending-down"></i>{{ superuser_trend }}</span>{% endif %}</div>
<div class="text-secondary small">Superuser</div>
</div>
</div>
<div id="spark-superusers" class="mx-n3" style="height:36px;"></div>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-history me-1"></i>Verlauf</h3>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for entry in history %}
<div class="list-group-item d-flex align-items-center gap-3 py-2">
<span class="d-flex align-items-center">
{% if entry.action == "site_created" %}
<span class="avatar avatar-xs bg-green" data-bs-toggle="tooltip" title="Standort angelegt"><i class="ti ti-building-community text-white"></i></span>
{% elif entry.action == "site_deleted" %}
<span class="avatar avatar-xs bg-red" data-bs-toggle="tooltip" title="Standort gelöscht"><i class="ti ti-building-community text-white"></i></span>
{% elif entry.action == "screen_created" %}
<span class="avatar avatar-xs bg-green" data-bs-toggle="tooltip" title="Screen angelegt"><i class="ti ti-devices text-white"></i></span>
{% elif entry.action == "screen_deleted" %}
<span class="avatar avatar-xs bg-red" data-bs-toggle="tooltip" title="Screen gelöscht"><i class="ti ti-devices text-white"></i></span>
{% elif entry.action == "user_created" %}
<span class="avatar avatar-xs bg-green" data-bs-toggle="tooltip" title="User angelegt"><i class="ti ti-user-plus text-white"></i></span>
{% elif entry.action == "user_deleted" %}
<span class="avatar avatar-xs bg-red" data-bs-toggle="tooltip" title="User gelöscht"><i class="ti ti-user-minus text-white"></i></span>
{% elif entry.action == "user_updated" %}
<span class="avatar avatar-xs bg-blue" data-bs-toggle="tooltip" title="User bearbeitet"><i class="ti ti-user-edit text-white"></i></span>
{% elif entry.action == "user_password_reset" %}
<span class="avatar avatar-xs bg-warning" data-bs-toggle="tooltip" title="Passwort zurückgesetzt"><i class="ti ti-key text-white"></i></span>
{% elif entry.action == "password_changed" %}
<span class="avatar avatar-xs bg-purple" data-bs-toggle="tooltip" title="Passwort geändert"><i class="ti ti-key text-white"></i></span>
{% elif entry.action == "background_uploaded" %}
<span class="avatar avatar-xs bg-blue" data-bs-toggle="tooltip" title="Hintergrund hochgeladen"><i class="ti ti-photo text-white"></i></span>
{% elif entry.action == "background_deleted" %}
<span class="avatar avatar-xs bg-yellow" data-bs-toggle="tooltip" title="Hintergrund zurückgesetzt"><i class="ti ti-photo-off text-white"></i></span>
{% elif entry.action == "customer_added" %}
<span class="avatar avatar-xs bg-teal" data-bs-toggle="tooltip" title="Kunde hinzugefügt"><i class="ti ti-users text-white"></i></span>
{% elif entry.action == "user_login" %}
<span class="avatar avatar-xs bg-lime" data-bs-toggle="tooltip" title="Anmeldung"><i class="ti ti-login text-white"></i></span>
{% elif entry.action == "user_logout" %}
<span class="avatar avatar-xs bg-secondary" data-bs-toggle="tooltip" title="Abmeldung"><i class="ti ti-logout text-white"></i></span>
{% else %}
<span class="avatar avatar-xs bg-secondary" data-bs-toggle="tooltip" title="{{ entry.action }}"><i class="ti ti-info-circle text-white"></i></span>
{% endif %}
</span>
<span class="text-secondary small text-nowrap" style="min-width:8.5rem;">{{ entry.timestamp }}</span>
<span class="flex-grow-1">{{ entry.detail }}</span>
<span class="text-secondary small text-nowrap">{{ entry.user }}</span>
</div>
{% else %}
<div class="list-group-item text-center text-muted py-4">
<i class="ti ti-inbox ti-lg d-block mb-1"></i>
Noch keine Aktivitäten aufgezeichnet
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% include "_footer.html" %}
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
if (!window.ApexCharts) return;
function spark(id, data, color) {
new ApexCharts(document.getElementById(id), {
chart: { type: "area", height: 32, width: "100%", sparkline: { enabled: true }, animations: { enabled: false } },
series: [{ data: data }],
stroke: { width: 1.5, curve: "straight" },
fill: { opacity: 0.25 },
colors: [color],
tooltip: { enabled: false }
}).render();
}
spark("spark-sites", {{ site_spark | tojson }}, "var(--ccm-primary, #da002d)");
spark("spark-screens", {{ screen_spark | tojson }}, "var(--ccm-primary, #da002d)");
spark("spark-users", {{ user_spark | tojson }}, "var(--ccm-primary, #da002d)");
spark("spark-admins", {{ admin_spark | tojson }}, "var(--ccm-primary, #da002d)");
spark("spark-superusers", {{ superuser_spark | tojson }}, "var(--ccm-primary, #da002d)");
});
</script>
</body>
</html>

216
templates/help.html Normal file
View File

@@ -0,0 +1,216 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>CANCOM Simple Signage Hilfe</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script>
(() => {
const stored = window.localStorage.getItem("signage-theme");
const theme = stored === "dark" || stored === "light" ? stored : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
})();
</script>
<link rel="stylesheet"
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<script defer
src="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js"></script>
{% include "_styles.html" %}
</head>
<body>
<div class="page">
{% set brand_subtitle = "Hilfe" %}
{% set site_dropdown_url_prefix = "/admin/" %}
{% include "_header.html" %}
<div class="navbar-expand-md">
<div class="collapse navbar-collapse show">
<div class="navbar navbar-dark nav-surface">
<div class="container-xl">
<div class="d-flex flex-wrap gap-2 py-2">
<a class="btn btn-white" href="/admin/{{ current_site }}"><i class="ti ti-arrow-left me-1"></i>Zurück</a>
<a class="btn btn-white active" href="/admin/help"><i class="ti ti-help me-1"></i>Hilfe</a>
</div>
</div>
</div>
</div>
</div>
<div class="page-wrapper">
<div class="container-xl mt-4">
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-building-community me-1"></i>Standort anlegen</h3></div>
<div class="card-body">
<p>Ein Standort (Site) gruppiert mehrere Screens, z.&thinsp;B. <code>stuttgart</code> oder <code>karlsruhe</code>.</p>
<ol>
<li><strong>+ Button</strong> im Header (neben dem Standort-Dropdown) klicken</li>
<li>Namen des neuen Standorts eingeben (z.&thinsp;B. <code>berlin</code>)</li>
<li>Der Standort wird sofort angelegt und im Dropdown sichtbar</li>
</ol>
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Nur Admins können Standorte anlegen oder löschen (Löschen über den Button im Standort-Dashboard).</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-devices me-1"></i>Screen hinzufügen</h3></div>
<div class="card-body">
<p>Ein Screen ist ein einzelner Player (z.&thinsp;B. <code>lobby</code>, <code>casino</code>), der eine Playlist abspielt.</p>
<ol>
<li>Im Admin-Bereich des Standorts (<code>/admin/&lt;standort&gt;</code>) das <strong>+</strong>-Symbol oben rechts klicken</li>
<li>Namen des Screens eingeben (z.&thinsp;B. <code>lobby</code>)</li>
<li>Der Screen erscheint als neue Card mit Tabs: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien</li>
</ol>
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Der Screen-Name wird automatisch in Kleinbuchstaben umgewandelt.</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-list me-1"></i>Playlist verwalten</h3></div>
<div class="card-body">
<p>Jeder Screen hat eine eigene Playlist mit Medien und URLs.</p>
<ul>
<li><strong>Medien hochladen:</strong> Im Tab "Medien" eine Datei auswählen und hochladen. Unterstützt werden Bilder (JPG, PNG) und Videos (MP4).</li>
<li><strong>URL hinzufügen:</strong> Im Tab "Playlist" eine URL eingeben und optional den Zoom-Faktor anpassen.</li>
<li><strong>Sortieren:</strong> Per Drag & Drop die Reihenfolge der Playlist-Einträge ändern (Ziehgriff links).</li>
<li><strong>Deaktivieren:</strong> Mit der Checkbox einzelne Einträge temporär ausblenden.</li>
<li><strong>Löschen:</strong> Über den roten Papierkorb-Button.</li>
</ul>
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Änderungen an der Playlist werden automatisch gespeichert. Der Player lädt die Seite automatisch neu (via Hash-Prüfung).</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div>
<div class="card-body">
<p>Die Priority-Playlist ist <strong>global</strong> und wird auf <em>allen</em> Playern eines Standorts eingeblendet zwischen den normalen Playlist-Einträgen.</p>
<ul>
<li>Aufruf über <code>/admin/&lt;standort&gt;/priority</code></li>
<li>Globale Medien und URLs verwalten, ebenfalls per Drag & Drop</li>
<li>Priority muss in der Konfiguration aktiviert sein (<code>config.json → priority.enabled</code>)</li>
</ul>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-external-link me-1"></i>Action Button (Custom-URL)</h3></div>
<div class="card-body">
<p>Der Action Button ist ein konfigurierbarer Button im Player, der eine frei wählbare URL öffnet.</p>
<ul>
<li><strong>Konfiguration:</strong> Im Tab <strong>Aktionen</strong> der Screen-Card</li>
<li><strong>URL:</strong> Beliebige Webadresse (z.&thinsp;B. Intranet-Seite, Dashboard)</li>
<li><strong>Label:</strong> Text, der auf dem Button angezeigt wird (z.&thinsp;B. "Infos")</li>
<li><strong>Position:</strong> 9 mögliche Positionen (oben/mitte/unten × links/mitte/rechts)</li>
<li><strong>Ziel:</strong>
<ul>
<li><code>overlay</code> URL wird in einem iframe-Overlay geöffnet, Zurück-Button schließt es</li>
<li><code>redirect</code> direkte Weiterleitung zur URL</li>
</ul>
</li>
</ul>
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Der Button erscheint nur, wenn "Aktiviert" eingeschaltet ist. Bei Positionen unten weicht er automatisch 16px über dem Newsticker aus.</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-robot me-1"></i>Digital Voice Agent</h3></div>
<div class="card-body">
<p>Der Digital Voice Agent zeigt einen Button im Player, der eine Typewriter-Animation mit wechselnden mehrsprachigen Texten anzeigt.</p>
<ul>
<li><strong>Globale URL:</strong> Die Ziel-URL wird in <code>config.json</code> (Feld <code>voice_agent_url</code>) gesetzt und ist im Admin sichtbar, aber nicht änderbar</li>
<li><strong>Konfiguration pro Screen:</strong> Im Tab <strong>Digital Voice Agent</strong> der Screen-Card</li>
<li><strong>Label:</strong> Button-Beschriftung (Default: "Digitaler Assistent")</li>
<li><strong>Position:</strong> Wie Action Button 9 Positionen wählbar</li>
<li><strong>Bild:</strong> Optional kann über dem Button ein Bild (dva.png) angezeigt werden (Beta-Feature)</li>
<li><strong>Typewriter-Tagline:</strong> 11 Phrasen in 5 Sprachen wechseln alle 2,5 Sekunden mit Buchstaben-für-Buchstaben-Effekt</li>
<li><strong>Animierte Balken:</strong> 5 rote Audiobalken neben dem Button signalisieren Aktivität</li>
</ul>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-settings me-1"></i>Screen-Einstellungen</h3></div>
<div class="card-body">
<p>Im Tab <strong>Einstellungen</strong> können pro Screen konfiguriert werden:</p>
<ul>
<li><strong>Intervall:</strong> Anzeigedauer pro Playlist-Eintrag in Sekunden</li>
<li><strong>Bilder / Videos anzeigen:</strong> Jeweils ein-/ausschaltbar</li>
<li><strong>Newsticker:</strong> Text (max. 200 Zeichen) und Aktivierung des Laufbands</li>
<li><strong>Stay on First:</strong> Wenn aktiviert, bleibt der Player auf dem ersten Playlist-Element stehen (kein Durchlauf)</li>
</ul>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-users me-1"></i>Willkommensseite</h3></div>
<div class="card-body">
<p>Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.</p>
<ul>
<li>Aufruf über <code>/willkommen?site=&lt;standort&gt;</code> (oder Menüpunkt)</li>
<li>Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Lobby-Playlist gesetzt</li>
<li>Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Tab "Medien" im Standort-Dashboard)</li>
<li>Im Hochformat (Portrait) werden die Logos untereinander angezeigt</li>
</ul>
</div>
</div>
{% if current_user.is_admin %}
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-shield me-1"></i>User-Verwaltung (Admin)</h3></div>
<div class="card-body">
<p>Admins können über <code>/admin/users</code> die User-Verwaltung aufrufen.</p>
<ul>
<li><strong>User anlegen:</strong> E-Mail, Passwort, Rolle (Admin/Superuser/User) und Standort-Zugriff festlegen</li>
<li><strong>User bearbeiten:</strong> E-Mail, Rolle, Standorte, Profilfelder (Vorname, Nachname, Abteilung) ändern</li>
<li><strong>User löschen:</strong> Der letzte Admin kann nicht gelöscht werden</li>
<li><strong>Passwort-Reset:</strong> Setzt ein temporäres Passwort, der User muss es beim nächsten Login ändern</li>
<li><strong>Rollen:</strong>
<ul>
<li><strong>Admin</strong> Zugriff auf alle Standorte + Userverwaltung</li>
<li><strong>Superuser</strong> Zugriff auf alle Standorte, keine Userverwaltung</li>
<li><strong>User</strong> Zugriff nur auf zugewiesene Standorte</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</h3></div>
<div class="card-body">
<p>Das Admin Dashboard (<code>/admin/dashboard</code>) bietet eine Übersicht über das gesamte System:</p>
<ul>
<li><strong>Statistik-Cards:</strong> Anzahl Standorte, Screens, User, Admins und Superuser</li>
<li><strong>Sparkline:</strong> Kleine Liniendiagramme zeigen den Verlauf der letzten Änderungen</li>
<li><strong>Trend:</strong> Pfeil nach oben/unten mit Netto-Änderung aus den letzten 30 Aktionen</li>
<li><strong>Aktivitätsverlauf:</strong> Alle Erstell-, Lösch-, Änderungsaktionen sowie An-/Abmeldungen werden protokolliert</li>
</ul>
</div>
</div>
{% endif %}
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-moon me-1"></i>Dark Mode</h3></div>
<div class="card-body">
<p>Der Dark Mode kann über den Mond-Button im Header umgeschaltet werden. Die Einstellung wird im Browser gespeichert (localStorage) und bleibt auch nach Seitenwechsel erhalten.</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-key me-1"></i>Passwort ändern</h3></div>
<div class="card-body">
<p>Das Passwort kann über das User-Menü (oben rechts) → <strong>Passwort ändern</strong> geändert werden. Nach einem Admin-Reset muss der User beim ersten Login ein neues Passwort vergeben.</p>
</div>
</div>
</div>
</div>
{% include "_footer.html" %}
</div>
</body>
</html>

View File

@@ -32,8 +32,10 @@
<div class="container-xl">
<div class="d-flex flex-wrap gap-2 py-2">
<a class="btn btn-white" href="/admin/{{ current_site }}"><i class="ti ti-arrow-left me-1"></i>Zurück</a>
<a class="btn btn-white" href="/admin/dashboard"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</a>
<a class="btn btn-white" href="/admin/users"><i class="ti ti-users me-1"></i>Userliste</a>
<a class="btn btn-white active" href="/admin/users/create"><i class="ti ti-user-plus me-1"></i>Neu anlegen</a>
<a class="btn btn-white" href="/admin/help"><i class="ti ti-help me-1"></i>Hilfe</a>
</div>
</div>
</div>
@@ -53,6 +55,16 @@
<label class="form-label">E-Mail (Username)</label>
<input type="email" name="email" class="form-control" placeholder="user@example.com" required>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Vorname</label>
<input type="text" name="first_name" class="form-control" placeholder="Vorname">
</div>
<div class="col-md-6">
<label class="form-label">Nachname</label>
<input type="text" name="last_name" class="form-control" placeholder="Nachname">
</div>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input type="password" name="password" class="form-control" placeholder="Passwort" minlength="6" required>
@@ -83,6 +95,14 @@
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Abteilung</label>
<input type="text" name="department" class="form-control" placeholder="z. B. Marketing">
</div>
<div class="mb-3">
<label class="form-label">Bemerkung</label>
<textarea name="notes" class="form-control" rows="2" placeholder="Interne Notiz"></textarea>
</div>
<button type="submit" class="btn btn-primary">User anlegen</button>
<a href="/admin/users" class="btn btn-outline-secondary ms-2">Abbrechen</a>
</form>

View File

@@ -32,8 +32,10 @@
<div class="container-xl">
<div class="d-flex flex-wrap gap-2 py-2">
<a class="btn btn-white" href="/admin"><i class="ti ti-arrow-left me-1"></i>Zurück</a>
<a class="btn btn-white" href="/admin/dashboard"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</a>
<a class="btn btn-white" href="/admin/users"><i class="ti ti-users me-1"></i>Userliste</a>
<a class="btn btn-white active" href="#"><i class="ti ti-edit me-1"></i>Bearbeiten</a>
<a class="btn btn-white" href="/admin/help"><i class="ti ti-help me-1"></i>Hilfe</a>
</div>
</div>
</div>
@@ -57,6 +59,16 @@
<label class="form-label">E-Mail (Username)</label>
<input type="email" name="email" class="form-control" value="{{ user.email }}" required>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Vorname</label>
<input type="text" name="first_name" class="form-control" value="{{ user.first_name }}">
</div>
<div class="col-md-6">
<label class="form-label">Nachname</label>
<input type="text" name="last_name" class="form-control" value="{{ user.last_name }}">
</div>
</div>
<div class="mb-3">
<label class="form-label">Rolle</label>
<select name="role" class="form-select" id="role-select">
@@ -91,6 +103,14 @@
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Abteilung</label>
<input type="text" name="department" class="form-control" value="{{ user.department }}">
</div>
<div class="mb-3">
<label class="form-label">Bemerkung</label>
<textarea name="notes" class="form-control" rows="2">{{ user.notes }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Speichern</button>
<a href="/admin/users" class="btn btn-outline-secondary ms-2">Abbrechen</a>
</form>

View File

@@ -36,8 +36,10 @@
<div class="container-xl">
<div class="d-flex flex-wrap gap-2 py-2">
<a class="btn btn-white" href="/admin/{{ current_site }}"><i class="ti ti-arrow-left me-1"></i>Zurück</a>
<a class="btn btn-white" href="/admin/dashboard"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</a>
<a class="btn btn-white active" href="/admin/users"><i class="ti ti-users me-1"></i>Userliste</a>
<a class="btn btn-white" href="/admin/users/create"><i class="ti ti-user-plus me-1"></i>Neu anlegen</a>
<a class="btn btn-white" href="/admin/help"><i class="ti ti-help me-1"></i>Hilfe</a>
</div>
</div>
</div>
@@ -69,9 +71,11 @@
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>Name</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Standorte</th>
<th>Abteilung</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
@@ -79,6 +83,7 @@
<tbody>
{% for u in users %}
<tr>
<td class="text-nowrap">{{ u.last_name }}{% if u.last_name and u.first_name %}, {% endif %}{{ u.first_name }}</td>
<td class="text-nowrap">{{ u.email }}</td>
<td>
{% if u.role == "admin" %}
@@ -100,6 +105,7 @@
{% endfor %}
{% endif %}
</td>
<td class="text-muted small">{{ u.department }}</td>
<td>
{% if u.must_change_password %}
<span class="badge bg-warning text-white">Passwort-Änderung erforderlich</span>
@@ -107,7 +113,7 @@
<span class="badge bg-green text-green-fg">Aktiv</span>
{% endif %}
</td>
<td>
<td class="text-nowrap">
<div class="d-flex gap-1">
{% if u.email != current_user.email %}
<form action="/admin/users/delete/{{ u.email }}" method="post"