Files
IT-Nexus/demo-benutzerverwaltung.html

750 lines
34 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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Benutzerverwaltung Design Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #3fa3a3;
--primary-dark: #008487;
--bg: #0f172a;
--bg2: #1e293b;
--bg3: #334155;
--text: #f1f5f9;
--text2: #94a3b8;
--text3: #64748b;
--border: #334155;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--radius: 10px;
--shadow: 0 4px 12px rgba(0,0,0,0.4);
}
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
/* ── Demo Selector Bar ── */
.demo-bar {
background: #0a1628;
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex; align-items: center; gap: 16px;
position: sticky; top: 0; z-index: 100;
}
.demo-bar-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--text3); }
.demo-tabs { display: flex; gap: 6px; }
.demo-tab {
padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500;
border: 1px solid var(--border); background: transparent; color: var(--text2);
cursor: pointer; transition: all 0.15s;
}
.demo-tab:hover { border-color: var(--primary); color: var(--primary); }
.demo-tab.active { background: var(--primary); border-color: var(--primary); color: #fff; }
.demo-tab.recommended::after { content: ' ★'; font-size: 11px; }
/* ── Page Container ── */
.page { display: none; padding: 28px 32px; max-width: 1300px; margin: 0 auto; }
.page.visible { display: block; }
/* ── Page Header ── */
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.page-title { font-size: 22px; font-weight: 700; }
.btn-group { display: flex; gap: 8px; }
.btn {
padding: 9px 18px; border-radius: 8px; font-size: 13px; font-weight: 500;
border: none; cursor: pointer; transition: all 0.15s; font-family: inherit;
}
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { background: var(--bg3); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--bg2); border-color: var(--primary); }
/* ── Stats Cards ── */
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 20px; }
.stat-card {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
padding: 16px 20px; display: flex; align-items: center; gap: 14px;
transition: border-color 0.15s;
}
.stat-card:hover { border-color: var(--primary); }
.stat-icon {
width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center;
justify-content: center; font-size: 18px; flex-shrink: 0;
}
.stat-icon.blue { background: rgba(59,130,246,0.15); }
.stat-icon.green { background: rgba(16,185,129,0.15); }
.stat-icon.red { background: rgba(239,68,68,0.15); }
.stat-icon.yellow { background: rgba(245,158,11,0.15); }
.stat-num { font-size: 24px; font-weight: 700; line-height: 1; }
.stat-label { font-size: 12px; color: var(--text2); margin-top: 3px; }
/* ── Filter Bar ── */
.filter-bar { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.search-input {
background: var(--bg2); border: 1px solid var(--border); color: var(--text);
border-radius: 8px; padding: 9px 14px; font-size: 13px; font-family: inherit;
outline: none; width: 280px; transition: border-color 0.15s;
}
.search-input:focus { border-color: var(--primary); }
.filter-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 500;
border: 1px solid var(--border); background: var(--bg2); color: var(--text2);
cursor: pointer; transition: all 0.12s; white-space: nowrap;
}
.chip:hover { border-color: var(--primary); color: var(--primary); }
.chip.active { background: var(--primary); border-color: var(--primary); color: #fff; }
.chip.active-red { background: var(--danger); border-color: var(--danger); color: #fff; }
.chip.active-green { background: var(--success); border-color: var(--success); color: #fff; }
.chip-sep { width: 1px; height: 24px; background: var(--border); margin: 0 2px; }
/* ── Table ── */
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
table { width: 100%; border-collapse: collapse; }
thead th {
background: var(--bg3); text-align: left; padding: 11px 16px;
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px;
color: var(--text3); border-bottom: 1px solid var(--border); white-space: nowrap;
}
tbody tr { border-bottom: 1px solid rgba(51,65,85,0.5); transition: background 0.12s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: rgba(63,163,163,0.04); }
td { padding: 12px 16px; font-size: 13px; vertical-align: middle; }
/* ── Avatar ── */
.user-cell { display: flex; align-items: center; gap: 10px; }
.avatar {
width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0;
color: #fff;
}
.user-name { font-weight: 500; font-size: 13px; }
.user-username { font-size: 11px; color: var(--text3); }
/* ── Badges ── */
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.4px;
}
.badge-dot { width: 6px; height: 6px; border-radius: 50%; }
.badge-active { background: rgba(16,185,129,0.15); color: var(--success); }
.badge-inactive { background: rgba(239,68,68,0.1); color: var(--danger); }
.role-badge {
padding: 3px 9px; border-radius: 6px; font-size: 11px; font-weight: 600; display: inline-flex; align-items: center; gap: 4px;
}
/* ── Action buttons (old style for Option A) ── */
.actions-old { display: flex; gap: 6px; }
.btn-sm { padding: 5px 12px; border-radius: 6px; font-size: 11px; font-weight: 600; border: none; cursor: pointer; font-family: inherit; }
.btn-sm-edit { background: rgba(63,163,163,0.15); color: var(--primary); }
.btn-sm-edit:hover { background: rgba(63,163,163,0.25); }
.btn-sm-toggle { background: rgba(148,163,184,0.1); color: var(--text2); }
.btn-sm-toggle:hover { background: rgba(148,163,184,0.2); }
.btn-sm-del { background: rgba(239,68,68,0.1); color: var(--danger); }
.btn-sm-del:hover { background: rgba(239,68,68,0.2); }
/* ── 3-dot menu (Option B+C) ── */
.menu-wrap { position: relative; display: inline-block; }
.menu-btn {
width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border);
background: transparent; color: var(--text2); cursor: pointer; font-size: 18px;
display: flex; align-items: center; justify-content: center; transition: all 0.12s;
line-height: 1;
}
.menu-btn:hover { background: var(--bg3); color: var(--text); border-color: var(--primary); }
.dropdown {
position: absolute; right: 0; top: calc(100% + 4px); background: var(--bg2);
border: 1px solid var(--border); border-radius: 8px; min-width: 160px;
box-shadow: var(--shadow); z-index: 50; overflow: hidden; display: none;
}
.dropdown.open { display: block; }
.dropdown-item {
padding: 9px 14px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px;
transition: background 0.1s; color: var(--text);
}
.dropdown-item:hover { background: var(--bg3); }
.dropdown-item.danger { color: var(--danger); }
.dropdown-item.danger:hover { background: rgba(239,68,68,0.1); }
.dropdown-sep { height: 1px; background: var(--border); margin: 3px 0; }
/* Inline role dropdown */
.role-select {
background: var(--bg3); border: 1px solid var(--border); color: var(--text);
border-radius: 6px; padding: 4px 8px; font-size: 12px; font-family: inherit;
cursor: pointer; outline: none; transition: border-color 0.12s;
}
.role-select:focus { border-color: var(--primary); }
/* ── Relative time ── */
.time-abs { font-size: 12px; color: var(--text2); }
.time-rel { font-size: 11px; color: var(--text3); }
.time-never { color: var(--text3); font-style: italic; }
/* ── Slideover (Option C) ── */
.slideover-layout { display: flex; gap: 0; position: relative; }
.table-area { flex: 1; min-width: 0; transition: margin-right 0.3s; }
.table-area.panel-open { margin-right: 0; }
.slideover {
width: 360px; flex-shrink: 0; background: var(--bg2); border: 1px solid var(--border);
border-radius: var(--radius); margin-left: 16px;
animation: slideIn 0.25s ease;
overflow-y: auto; max-height: calc(100vh - 200px);
}
@keyframes slideIn { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } }
.panel-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
.panel-avatar { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 700; color: #fff; flex-shrink: 0; }
.panel-name { font-weight: 700; font-size: 15px; }
.panel-email { font-size: 12px; color: var(--text2); }
.panel-close { margin-left: auto; background: none; border: none; color: var(--text2); font-size: 20px; cursor: pointer; padding: 4px; border-radius: 6px; }
.panel-close:hover { background: var(--bg3); color: var(--text); }
.panel-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
.panel-section:last-child { border-bottom: none; }
.panel-section-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--text3); margin-bottom: 12px; }
.panel-field { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 13px; }
.panel-field:last-child { margin-bottom: 0; }
.panel-field-label { color: var(--text2); }
.panel-actions { padding: 14px 20px; display: flex; gap: 8px; flex-direction: column; }
.btn-full { width: 100%; padding: 9px; text-align: center; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; border: none; font-family: inherit; transition: all 0.12s; }
.btn-full-primary { background: var(--primary); color: #fff; }
.btn-full-primary:hover { background: var(--primary-dark); }
.btn-full-warn { background: rgba(245,158,11,0.12); color: var(--warning); border: 1px solid rgba(245,158,11,0.3); }
.btn-full-warn:hover { background: rgba(245,158,11,0.2); }
.btn-full-danger { background: rgba(239,68,68,0.1); color: var(--danger); border: 1px solid rgba(239,68,68,0.25); }
.btn-full-danger:hover { background: rgba(239,68,68,0.18); }
/* Row clickable for C */
.clickable-row { cursor: pointer; }
.clickable-row.selected-row { background: rgba(63,163,163,0.08) !important; }
.clickable-row.selected-row td { border-left: 2px solid var(--primary); }
.clickable-row.selected-row td:not(:first-child) { border-left: none; }
/* ── Last login ── */
td .last-login { display: flex; flex-direction: column; }
/* ── Tooltip style note ── */
.design-note {
background: rgba(63,163,163,0.08); border: 1px solid rgba(63,163,163,0.2);
border-radius: 8px; padding: 10px 16px; margin-bottom: 16px;
font-size: 12px; color: var(--text2); line-height: 1.6;
}
.design-note strong { color: var(--primary); }
.combo-badge {
background: var(--primary); color: #fff; font-size: 10px; font-weight: 700;
padding: 2px 8px; border-radius: 20px; margin-left: 8px; text-transform: uppercase;
}
</style>
</head>
<body>
<!-- Demo Switcher Bar -->
<div class="demo-bar">
<span class="demo-bar-label">Design-Option:</span>
<div class="demo-tabs">
<button class="demo-tab" onclick="show('a')">A Stats + Filter-Chips</button>
<button class="demo-tab" onclick="show('b')">B Avatar + 3-Punkte-Menü</button>
<button class="demo-tab" onclick="show('c')">C Slideover-Panel</button>
<button class="demo-tab recommended active" onclick="show('combo')">Kombiniert (Empfehlung)</button>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
OPTION A Stats + Filter Chips
════════════════════════════════════════════════════════ -->
<div id="page-a" class="page">
<div class="page-header">
<h1 class="page-title">Benutzerverwaltung</h1>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Option A:</strong> Statistik-Kacheln oben + Filter-Chips statt nur Textsuche. Tabelle bleibt gleich. Kleinster Aufwand, sofortiger Informationsgewinn.
</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon blue">👥</div>
<div><div class="stat-num">14</div><div class="stat-label">Benutzer gesamt</div></div>
</div>
<div class="stat-card">
<div class="stat-icon green"></div>
<div><div class="stat-num">11</div><div class="stat-label">Aktiv</div></div>
</div>
<div class="stat-card">
<div class="stat-icon red">🚫</div>
<div><div class="stat-num">3</div><div class="stat-label">Deaktiviert</div></div>
</div>
<div class="stat-card">
<div class="stat-icon yellow">⏱️</div>
<div><div class="stat-num">4</div><div class="stat-label">Noch nie eingeloggt</div></div>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…" oninput="filterA(this.value)">
<div class="chip-sep"></div>
<div class="filter-chips" id="role-chips-a">
<span class="chip active" onclick="chipClick(this,'a')">Alle Rollen</span>
<span class="chip" onclick="chipClick(this,'a')">👑 Super Admin</span>
<span class="chip" onclick="chipClick(this,'a')">🛡️ Admin</span>
<span class="chip" onclick="chipClick(this,'a')">🎧 Support</span>
<span class="chip" onclick="chipClick(this,'a')">🔧 Bearbeiter</span>
<span class="chip" onclick="chipClick(this,'a')">👤 Benutzer</span>
</div>
<div class="chip-sep"></div>
<span class="chip active-green active" onclick="statusChip(this,'aktiv')">● Aktiv</span>
<span class="chip" onclick="statusChip(this,'inaktiv')">● Inaktiv</span>
</div>
<!-- Table (same as current, just with search) -->
<div class="card">
<table>
<thead>
<tr>
<th>Benutzername</th>
<th>E-Mail</th>
<th>Name</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="tbody-a"></tbody>
</table>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
OPTION B Avatar + 3-Punkte-Menü
════════════════════════════════════════════════════════ -->
<div id="page-b" class="page">
<div class="page-header">
<h1 class="page-title">Benutzerverwaltung</h1>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Option B:</strong> Avatar-Initialen in der ersten Spalte + Aktionen als 3-Punkte-Menü (spart viel Platz, besonders bei langen Buttons). Rolle inline per Dropdown änderbar.
</div>
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…">
</div>
<div class="card">
<table>
<thead>
<tr>
<th>Benutzer</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
<th style="width:50px">···</th>
</tr>
</thead>
<tbody id="tbody-b"></tbody>
</table>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
OPTION C Slideover-Panel
════════════════════════════════════════════════════════ -->
<div id="page-c" class="page">
<div class="page-header">
<h1 class="page-title">Benutzerverwaltung</h1>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Option C:</strong> Klick auf eine Zeile öffnet ein Detailpanel rechts. Tabelle bleibt links sichtbar. Kein Modal-Overlay. → <em>Klick auf eine Zeile zum Testen!</em>
</div>
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…">
</div>
<div class="slideover-layout" id="c-layout">
<div class="table-area card" id="c-table-area">
<table>
<thead>
<tr>
<th>Benutzer</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
</tr>
</thead>
<tbody id="tbody-c"></tbody>
</table>
</div>
<div class="slideover" id="c-panel" style="display:none"></div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
KOMBINATION (Empfehlung): Stats + Filter + Avatar + 3-Punkte + Slideover
════════════════════════════════════════════════════════ -->
<div id="page-combo" class="page visible">
<div class="page-header">
<div>
<h1 class="page-title">Benutzerverwaltung <span class="combo-badge">Empfehlung</span></h1>
</div>
<div class="btn-group">
<button class="btn btn-secondary">☁️ Aus Azure importieren</button>
<button class="btn btn-primary">+ Neuer Benutzer</button>
</div>
</div>
<div class="design-note">
<strong>Kombination aus A + B + C:</strong> Stats-Kacheln · Filter-Chips · Avatar-Initialen · 3-Punkte-Menü · Slideover-Panel bei Klick auf eine Zeile. → <em>Klick auf eine Zeile öffnet das Detail-Panel!</em>
</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon blue">👥</div>
<div><div class="stat-num">14</div><div class="stat-label">Benutzer gesamt</div></div>
</div>
<div class="stat-card">
<div class="stat-icon green"></div>
<div><div class="stat-num">11</div><div class="stat-label">Aktiv</div></div>
</div>
<div class="stat-card">
<div class="stat-icon red">🚫</div>
<div><div class="stat-num">3</div><div class="stat-label">Deaktiviert</div></div>
</div>
<div class="stat-card">
<div class="stat-icon yellow">⏱️</div>
<div><div class="stat-num">4</div><div class="stat-label">Noch nie eingeloggt</div></div>
</div>
</div>
<!-- Filter -->
<div class="filter-bar">
<input class="search-input" type="text" placeholder="🔍 Suche nach Name, E-Mail, Rolle…">
<div class="chip-sep"></div>
<div class="filter-chips">
<span class="chip active" onclick="chipClick(this,'combo')">Alle Rollen</span>
<span class="chip" onclick="chipClick(this,'combo')">👑 Super Admin</span>
<span class="chip" onclick="chipClick(this,'combo')">🛡️ Admin</span>
<span class="chip" onclick="chipClick(this,'combo')">🎧 Support</span>
<span class="chip" onclick="chipClick(this,'combo')">👤 Benutzer</span>
</div>
<div class="chip-sep"></div>
<span class="chip active-green active">● Aktiv</span>
<span class="chip">● Inaktiv</span>
</div>
<div class="slideover-layout" id="combo-layout">
<div class="table-area card" id="combo-table-area">
<table>
<thead>
<tr>
<th>Benutzer</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
<th style="width:50px">···</th>
</tr>
</thead>
<tbody id="tbody-combo"></tbody>
</table>
</div>
<div class="slideover" id="combo-panel" style="display:none"></div>
</div>
</div>
<script>
// ── Mock Data ──
const ROLE_META = {
super_admin: { icon: '👑', color: '#ef4444', label: 'Super Admin', avatarBg: '#7f1d1d' },
admin: { icon: '🛡️', color: '#f97316', label: 'Admin', avatarBg: '#7c2d12' },
support: { icon: '🎧', color: '#3b82f6', label: 'IT-Support', avatarBg: '#1e3a8a' },
bearbeiter: { icon: '🔧', color: '#8b5cf6', label: 'Bearbeiter', avatarBg: '#4c1d95' },
benutzer: { icon: '👤', color: '#64748b', label: 'Benutzer', avatarBg: '#1e293b' },
hr_personal: { icon: '🧑‍💼', color: '#10b981', label: 'HR', avatarBg: '#064e3b' },
buchhaltung: { icon: '💶', color: '#a78bfa', label: 'Buchhaltung', avatarBg: '#3b0764' },
};
const AVATAR_COLORS = ['#1d4ed8','#0f766e','#b45309','#be185d','#7c3aed','#0369a1','#15803d','#c2410c'];
function avatarColor(name) {
let h = 0; for (let c of name) h = (h * 31 + c.charCodeAt(0)) & 0xffff;
return AVATAR_COLORS[h % AVATAR_COLORS.length];
}
function initials(u) {
if (u.first_name && u.last_name) return (u.first_name[0]+u.last_name[0]).toUpperCase();
return u.username.slice(0,2).toUpperCase();
}
const USERS = [
{ id:1, username:'gruessing', email:'gruessing@cereda-systems.de', first_name:'Simon', last_name:'Grüssing', role:'super_admin', active:true, last_login:'2026-05-27T14:32:00' },
{ id:2, username:'smueller', email:'smueller@cereda-systems.de', first_name:'Stefan', last_name:'Müller', role:'admin', active:true, last_login:'2026-05-26T09:11:00' },
{ id:3, username:'jschmidt', email:'jschmidt@cereda-systems.de', first_name:'Julia', last_name:'Schmidt', role:'support', active:true, last_login:'2026-05-27T08:45:00' },
{ id:4, username:'tbauer', email:'tbauer@cereda-systems.de', first_name:'Thomas', last_name:'Bauer', role:'support', active:true, last_login:'2026-05-25T16:20:00' },
{ id:5, username:'mweber', email:'mweber@cereda-systems.de', first_name:'Maria', last_name:'Weber', role:'bearbeiter', active:true, last_login:'2026-05-22T11:00:00' },
{ id:6, username:'fkoch', email:'fkoch@cereda-systems.de', first_name:'Felix', last_name:'Koch', role:'benutzer', active:true, last_login:'2026-05-20T09:00:00' },
{ id:7, username:'krichter', email:'krichter@cereda-systems.de', first_name:'Klaus', last_name:'Richter', role:'benutzer', active:true, last_login:'2026-05-15T14:00:00' },
{ id:8, username:'aschneider', email:'aschneider@cereda-systems.de', first_name:'Anna', last_name:'Schneider', role:'hr_personal', active:true, last_login:'2026-05-24T10:30:00' },
{ id:9, username:'benders', email:'benders@cereda-systems.de', first_name:'Björn', last_name:'Enders', role:'benutzer', active:true, last_login:'2026-05-19T07:55:00' },
{ id:10, username:'mfischer', email:'mfischer@cereda-systems.de', first_name:'Markus', last_name:'Fischer', role:'buchhaltung', active:true, last_login:'2026-05-23T13:00:00' },
{ id:11, username:'lbraun', email:'lbraun@cereda-systems.de', first_name:'Lena', last_name:'Braun', role:'benutzer', active:true, last_login:null },
{ id:12, username:'konferenzraum', email:'konferenzraum@cereda-systems.de', first_name:null, last_name:null, role:'benutzer', active:true, last_login:null },
{ id:13, username:'poolfahrzeug', email:'poolfahrzeug@cereda-systems.de', first_name:null, last_name:null, role:'benutzer', active:false, last_login:null },
{ id:14, username:'einkauf', email:'einkauf@cereda-systems.de', first_name:null, last_name:null, role:'benutzer', active:false, last_login:null },
];
function relativeTime(iso) {
if (!iso) return '<span class="time-never">Nie</span>';
const diff = Date.now() - new Date(iso).getTime();
const mins = Math.floor(diff/60000), hrs = Math.floor(mins/60), days = Math.floor(hrs/24);
let rel = days > 0 ? `vor ${days} Tag${days>1?'en':''}` : hrs > 0 ? `vor ${hrs} Std.` : mins > 0 ? `vor ${mins} Min.` : 'gerade eben';
const abs = new Date(iso).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'});
return `<div class="last-login"><span class="time-abs">${abs}</span><span class="time-rel">${rel}</span></div>`;
}
function roleBadge(role) {
const m = ROLE_META[role] || { icon:'🔵', color:'#64748b', label: role };
return `<span class="role-badge" style="background:${m.color}18;color:${m.color};">${m.icon} ${m.label}</span>`;
}
function statusBadge(active) {
return active
? `<span class="badge badge-active"><span class="badge-dot" style="background:var(--success)"></span>Aktiv</span>`
: `<span class="badge badge-inactive"><span class="badge-dot" style="background:var(--danger)"></span>Inaktiv</span>`;
}
function avatarHtml(u, size=34, fontSize=12) {
const bg = avatarColor(u.username);
const ini = initials(u);
return `<div class="avatar" style="width:${size}px;height:${size}px;background:${bg};font-size:${fontSize}px">${ini}</div>`;
}
// ── Option A ──
function renderA() {
const rows = USERS.map(u => `
<tr>
<td>${u.username}</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>${u.first_name || u.last_name ? (u.first_name||'') + ' ' + (u.last_name||'') : ''}</td>
<td>${roleBadge(u.role)}</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
<td>
<div class="actions-old">
<button class="btn-sm btn-sm-edit">Bearbeiten</button>
<button class="btn-sm btn-sm-toggle">${u.active?'Deaktivieren':'Aktivieren'}</button>
<button class="btn-sm btn-sm-del">Löschen</button>
</div>
</td>
</tr>
`).join('');
document.getElementById('tbody-a').innerHTML = rows;
}
// ── Option B ──
function renderB() {
const rows = USERS.map(u => `
<tr>
<td>
<div class="user-cell">
${avatarHtml(u)}
<div>
<div class="user-name">${u.first_name || u.last_name ? (u.first_name||'')+' '+(u.last_name||'') : u.username}</div>
<div class="user-username">@${u.username}</div>
</div>
</div>
</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>
<select class="role-select" title="Rolle direkt ändern">
${Object.entries(ROLE_META).map(([k,m]) => `<option value="${k}" ${k===u.role?'selected':''}>${m.icon} ${m.label}</option>`).join('')}
</select>
</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
<td>
<div class="menu-wrap" onclick="event.stopPropagation()">
<button class="menu-btn" onclick="toggleMenu(this)">⋯</button>
<div class="dropdown">
<div class="dropdown-item">✏️ Bearbeiten</div>
<div class="dropdown-item">${u.active ? '🚫 Deaktivieren' : '✅ Aktivieren'}</div>
<div class="dropdown-item">🔑 Passwort zurücksetzen</div>
<div class="dropdown-sep"></div>
<div class="dropdown-item danger">🗑️ Löschen</div>
</div>
</div>
</td>
</tr>
`).join('');
document.getElementById('tbody-b').innerHTML = rows;
}
// ── Option C Slideover ──
let selectedC = null;
function renderC() {
const rows = USERS.map((u,i) => `
<tr class="clickable-row" id="crow-${i}" onclick="openPanel('c', ${i})">
<td>
<div class="user-cell">
${avatarHtml(u)}
<div>
<div class="user-name">${u.first_name || u.last_name ? (u.first_name||'')+' '+(u.last_name||'') : u.username}</div>
<div class="user-username">@${u.username}</div>
</div>
</div>
</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>${roleBadge(u.role)}</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
</tr>
`).join('');
document.getElementById('tbody-c').innerHTML = rows;
}
// ── Combo ──
let selectedCombo = null;
function renderCombo() {
const rows = USERS.map((u,i) => `
<tr class="clickable-row" id="combrow-${i}" onclick="openPanel('combo', ${i})">
<td>
<div class="user-cell">
${avatarHtml(u)}
<div>
<div class="user-name">${u.first_name || u.last_name ? (u.first_name||'')+' '+(u.last_name||'') : u.username}</div>
<div class="user-username">@${u.username}</div>
</div>
</div>
</td>
<td style="color:var(--text2);font-size:12px">${u.email}</td>
<td>
<select class="role-select" onclick="event.stopPropagation()" title="Rolle direkt ändern">
${Object.entries(ROLE_META).map(([k,m]) => `<option value="${k}" ${k===u.role?'selected':''}>${m.icon} ${m.label}</option>`).join('')}
</select>
</td>
<td>${statusBadge(u.active)}</td>
<td>${relativeTime(u.last_login)}</td>
<td>
<div class="menu-wrap" onclick="event.stopPropagation()">
<button class="menu-btn" onclick="toggleMenu(this)">⋯</button>
<div class="dropdown">
<div class="dropdown-item">✏️ Bearbeiten</div>
<div class="dropdown-item">${u.active ? '🚫 Deaktivieren' : '✅ Aktivieren'}</div>
<div class="dropdown-item">🔑 Passwort zurücksetzen</div>
<div class="dropdown-sep"></div>
<div class="dropdown-item danger">🗑️ Löschen</div>
</div>
</div>
</td>
</tr>
`).join('');
document.getElementById('tbody-combo').innerHTML = rows;
}
// ── Panel Content ──
function panelHtml(u, prefix) {
const m = ROLE_META[u.role] || {};
const displayName = u.first_name || u.last_name ? `${u.first_name||''} ${u.last_name||''}`.trim() : u.username;
return `
<div class="panel-header">
${avatarHtml(u, 48, 18)}
<div>
<div class="panel-name">${displayName}</div>
<div class="panel-email">${u.email}</div>
</div>
<button class="panel-close" onclick="closePanel('${prefix}')">×</button>
</div>
<div class="panel-section">
<div class="panel-section-label">Account</div>
<div class="panel-field"><span class="panel-field-label">Benutzername</span><span>@${u.username}</span></div>
<div class="panel-field"><span class="panel-field-label">Rolle</span>${roleBadge(u.role)}</div>
<div class="panel-field"><span class="panel-field-label">Status</span>${statusBadge(u.active)}</div>
</div>
<div class="panel-section">
<div class="panel-section-label">Aktivität</div>
<div class="panel-field"><span class="panel-field-label">Letzter Login</span>${u.last_login ? new Date(u.last_login).toLocaleString('de-DE') : '<span style="color:var(--text3);font-style:italic">Noch nie</span>'}</div>
<div class="panel-field"><span class="panel-field-label">Konto-ID</span><span style="color:var(--text3)">#${u.id}</span></div>
</div>
<div class="panel-actions">
<button class="btn-full btn-full-primary">✏️ Bearbeiten</button>
<button class="btn-full btn-full-warn">${u.active ? '🚫 Deaktivieren' : '✅ Aktivieren'}</button>
<button class="btn-full btn-full-danger">🗑️ Löschen</button>
</div>
`;
}
function openPanel(prefix, idx) {
const u = USERS[idx];
const panel = document.getElementById(`${prefix}-panel`);
panel.innerHTML = panelHtml(u, prefix);
panel.style.display = 'block';
// highlight row
document.querySelectorAll(`#tbody-${prefix} tr`).forEach((r,i) => r.classList.toggle('selected-row', i===idx));
}
function closePanel(prefix) {
document.getElementById(`${prefix}-panel`).style.display = 'none';
document.querySelectorAll(`#tbody-${prefix} tr`).forEach(r => r.classList.remove('selected-row'));
}
// ── Menu toggle ──
function toggleMenu(btn) {
const dd = btn.nextElementSibling;
const wasOpen = dd.classList.contains('open');
// close all
document.querySelectorAll('.dropdown.open').forEach(d => d.classList.remove('open'));
if (!wasOpen) dd.classList.add('open');
}
document.addEventListener('click', () => {
document.querySelectorAll('.dropdown.open').forEach(d => d.classList.remove('open'));
});
// ── Filter chips ──
function chipClick(el, page) {
el.closest('.filter-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
el.classList.add('active');
}
// ── Page switcher ──
function show(id) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('visible'));
document.getElementById('page-' + id).classList.add('visible');
document.querySelectorAll('.demo-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
}
// ── Init ──
renderA();
renderB();
renderC();
renderCombo();
</script>
</body>
</html>