UI redesign step 7 (Benutzer-Verwaltung): Stitch style
- Header gets the same title/subtitle pair + primary "Neuer Benutzer" pill on the right that the other tabs use, with the person_add glyph. - Table shell now matches Übersicht/Provisionen: bordered white card, surface-container-high header band with label-caps column titles, zebra rows, hover highlight. - "Admin" column renders a verified_user icon for admins and a long dash otherwise instead of the previous green check. The username column shows a small primary "Sie" badge for the logged-in user. - Action column collapses to two icon-only buttons (edit / delete); delete is rendered with reduced opacity and disabled when it is the logged-in user's row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
968cb183f2
commit
ff0d7398db
2 files changed
+61
-26
No files matched your search
+32
-15
@@ -1815,22 +1815,39 @@
|
|||||||
|
|
||||||
<!-- Benutzer-Verwaltung Tab (admin only) -->
|
<!-- Benutzer-Verwaltung Tab (admin only) -->
|
||||||
<div id="users" class="tab-content">
|
<div id="users" class="tab-content">
|
||||||
<h2>Benutzer-Verwaltung</h2>
|
<div class="flex flex-col gap-lg">
|
||||||
<div style="margin-bottom: 20px;">
|
|
||||||
<button type="button" class="btn-success" onclick="openUserModal()">+ Neuer Benutzer</button>
|
<!-- Page header -->
|
||||||
|
<div class="flex justify-between items-end flex-wrap gap-md">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-headline-md text-on-background mb-xs">Benutzer-Verwaltung</h1>
|
||||||
|
<p class="text-body-sm text-on-surface-variant">Verwalten Sie die Anmeldedaten und Berechtigungen der Anwender.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="openUserModal()"
|
||||||
|
class="bg-primary-container text-on-primary hover:bg-primary transition-colors rounded-lg px-md py-sm text-body-sm flex items-center gap-sm shadow-sm">
|
||||||
|
<span class="material-symbols-outlined text-[18px]">person_add</span>
|
||||||
|
Neuer Benutzer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data table -->
|
||||||
|
<div class="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-[0_2px_4px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead class="bg-surface-container-high border-b-2 border-outline-variant">
|
||||||
|
<tr>
|
||||||
|
<th class="px-md py-sm text-label-caps text-on-surface-variant uppercase">Username</th>
|
||||||
|
<th class="px-md py-sm text-label-caps text-on-surface-variant uppercase">Anzeigename</th>
|
||||||
|
<th class="px-md py-sm text-label-caps text-on-surface-variant uppercase text-center w-[80px]">Admin</th>
|
||||||
|
<th class="px-md py-sm text-label-caps text-on-surface-variant uppercase w-[180px]">Erstellt</th>
|
||||||
|
<th class="px-md py-sm text-label-caps text-on-surface-variant uppercase text-center w-[120px]">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="usersTableBody" class="text-body-sm text-on-surface"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="data-table" style="width:100%; border-collapse: collapse;">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Anzeigename</th>
|
|
||||||
<th>Admin</th>
|
|
||||||
<th>Erstellt</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="usersTableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal: Benutzer anlegen / bearbeiten -->
|
<!-- Modal: Benutzer anlegen / bearbeiten -->
|
||||||
|
|||||||
+29
-11
@@ -3979,29 +3979,47 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const tbody = document.getElementById('usersTableBody');
|
const tbody = document.getElementById('usersTableBody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#666;">Lade...</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Lade…</td></tr>';
|
||||||
try {
|
try {
|
||||||
const users = await apiRequest('/api/users');
|
const users = await apiRequest('/api/users');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
if (!users.length) {
|
if (!users.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#666;">Keine Benutzer</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5" class="px-md py-lg text-center text-body-sm text-on-surface-variant">Keine Benutzer</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users.forEach(u => {
|
users.forEach(u => {
|
||||||
const tr = document.createElement('tr');
|
|
||||||
const isSelf = currentUser && currentUser.username === u.username;
|
const isSelf = currentUser && currentUser.username === u.username;
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = 'border-b border-outline-variant/30 even:bg-surface-container-low/50 hover:bg-surface-variant/30 transition-colors';
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${escapeHtml(u.username)}${isSelf ? ' <small style="color:#3498db;">(Sie)</small>' : ''}</td>
|
<td class="px-md py-xs">
|
||||||
<td>${escapeHtml(u.displayName || '')}</td>
|
<span class="font-semibold text-on-background">${escapeHtml(u.username)}</span>
|
||||||
<td>${u.isAdmin ? '<span style="color:#27ae60;">✓</span>' : ''}</td>
|
${isSelf ? '<span class="ml-xs text-label-caps text-primary">Sie</span>' : ''}
|
||||||
<td>${escapeHtml(formatDateTime(u.createdAt))}</td>
|
</td>
|
||||||
<td>
|
<td class="px-md py-xs text-on-surface">${escapeHtml(u.displayName || '–')}</td>
|
||||||
<button data-user-action="edit" data-uuid="${escapeHtml(u.uuid)}" class="btn-success" style="margin-right:5px;">Bearbeiten</button>
|
<td class="px-md py-xs text-center">
|
||||||
<button data-user-action="delete" data-uuid="${escapeHtml(u.uuid)}" data-username="${escapeHtml(u.username)}" class="btn-danger" ${isSelf ? 'disabled title="Eigener Account kann nicht gelöscht werden"' : ''}>Löschen</button>
|
${u.isAdmin
|
||||||
|
? '<span class="material-symbols-outlined text-primary text-[20px]" title="Administrator">verified_user</span>'
|
||||||
|
: '<span class="text-on-surface-variant">—</span>'}
|
||||||
|
</td>
|
||||||
|
<td class="px-md py-xs font-label-numeric text-label-numeric text-on-surface-variant">${escapeHtml(formatDateTime(u.createdAt))}</td>
|
||||||
|
<td class="px-md py-xs">
|
||||||
|
<div class="flex items-center justify-center gap-sm">
|
||||||
|
<button data-user-action="edit" data-uuid="${escapeHtml(u.uuid)}" title="Bearbeiten"
|
||||||
|
class="text-outline hover:text-primary transition-colors">
|
||||||
|
<span class="material-symbols-outlined text-[18px]">edit</span>
|
||||||
|
</button>
|
||||||
|
<button data-user-action="delete" data-uuid="${escapeHtml(u.uuid)}" data-username="${escapeHtml(u.username)}" title="${isSelf ? 'Eigener Account kann nicht gelöscht werden' : 'Löschen'}"
|
||||||
|
class="text-outline hover:text-error transition-colors ${isSelf ? 'opacity-40 cursor-not-allowed' : ''}"
|
||||||
|
${isSelf ? 'disabled' : ''}>
|
||||||
|
<span class="material-symbols-outlined text-[18px]">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tr.querySelectorAll('button[data-user-action]').forEach(btn => {
|
tr.querySelectorAll('button[data-user-action]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
if (btn.disabled) return;
|
||||||
const action = btn.dataset.userAction;
|
const action = btn.dataset.userAction;
|
||||||
const uuid = btn.dataset.uuid;
|
const uuid = btn.dataset.uuid;
|
||||||
if (action === 'edit') openUserModal(u);
|
if (action === 'edit') openUserModal(u);
|
||||||
@@ -4011,7 +4029,7 @@ async function loadUsers() {
|
|||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#c0392b;">Fehler beim Laden</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5" class="px-md py-lg text-center text-body-sm text-error">Fehler beim Laden</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user