- Bewerbungsdetail: Toggle zwischen Bewerbungsunterlagen und E-Mail-Korrespondenz (hash-basiert), weniger Scroll auf der Seite. - Admin: Klick auf einen Nutzer öffnet neue Verwaltungsseite /admin/users/:id (Passwort, Anmelden als, Jobsuche, Admin-Status, Löschen). - Admin-Tabelle: Löschen und Jobsuche aus den Aktionen entfernt (jetzt auf der Nutzerdetailseite). Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
14 KiB
Plaintext
193 lines
14 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<%- include('partials/head') %>
|
||
</head>
|
||
<body class="min-h-screen flex flex-col transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
|
||
<%- include('partials/header') %>
|
||
|
||
<main class="flex-1 container mx-auto px-4 py-8 max-w-5xl">
|
||
<a href="/admin" class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline mb-6">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||
</svg>
|
||
Zur Benutzerverwaltung
|
||
</a>
|
||
|
||
<%
|
||
const istIch = user.id === currentUserId;
|
||
const letzterAdmin = user.is_admin && adminCount <= 1;
|
||
%>
|
||
|
||
<div class="flex flex-wrap items-center gap-4 mb-8">
|
||
<span class="grid h-14 w-14 place-items-center rounded-full text-lg font-semibold <%= user.is_admin ? 'bg-blue-600/10 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300' : 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300' %>">
|
||
<%= String(user.username || '?').charAt(0).toUpperCase() %>
|
||
</span>
|
||
<div class="min-w-0">
|
||
<div class="flex flex-wrap items-center gap-2.5">
|
||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white"><%= user.username %></h1>
|
||
<% if (user.is_admin) { %>
|
||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">Admin</span>
|
||
<% } else { %>
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">Benutzer</span>
|
||
<% } %>
|
||
<% if (istIch) { %><span class="text-xs text-gray-400">(du)</span><% } %>
|
||
</div>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||
<%= anzahlBewerbungen %> <%= anzahlBewerbungen === 1 ? 'Bewerbung' : 'Bewerbungen' %> ·
|
||
angelegt <time data-dt="<%= user.created_at %>"><%= user.created_at %></time>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
|
||
<!-- Verwaltung -->
|
||
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Benutzer verwalten</h2>
|
||
|
||
<div class="space-y-5">
|
||
<!-- Admin-Status -->
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">Admin-Status</p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||
<% if (letzterAdmin) { %>Letzter Admin – kann nicht abgesetzt werden.<% } else { %>Admins können Benutzer verwalten und andere Konten übernehmen.<% } %>
|
||
</p>
|
||
</div>
|
||
<form method="POST" action="/admin/users/<%= user.id %>/toggle-admin" class="inline">
|
||
<button type="submit" <%= letzterAdmin ? 'disabled' : '' %>
|
||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
||
<%= user.is_admin ? 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600' : 'bg-blue-600 hover:bg-blue-700 text-white' %>">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0a1.724 1.724 0 002.573 1.066c.78-.448 1.707.479 1.26 1.26a1.724 1.724 0 001.065 2.572c.921.3.921 1.603 0 1.902a1.724 1.724 0 00-1.066 2.573c.448.78-.479 1.707-1.26 1.26a1.724 1.724 0 00-2.572 1.065c-.3.921-1.603.921-1.902 0a1.724 1.724 0 00-2.573-1.066c-.78.448-1.707-.479-1.26-1.26a1.724 1.724 0 00-1.065-2.572c-.921-.3-.921-1.603 0-1.902a1.724 1.724 0 001.066-2.573c-.448-.78.479-1.707 1.26-1.26.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||
<%= user.is_admin ? 'Zum Benutzer herabstufen' : 'Zum Admin machen' %>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<hr class="border-gray-100 dark:border-gray-700">
|
||
|
||
<!-- Passwort zurücksetzen -->
|
||
<div>
|
||
<div class="flex flex-wrap items-center justify-between gap-3 mb-2">
|
||
<div>
|
||
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">Passwort zurücksetzen</p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400">Alle Sitzungen dieses Benutzers werden ungültig.</p>
|
||
</div>
|
||
<button type="button" onclick="document.getElementById('resetForm').classList.toggle('hidden')"
|
||
class="act-btn act-blue">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path></svg>
|
||
<span>Neues Passwort</span>
|
||
</button>
|
||
</div>
|
||
<form id="resetForm" method="POST" action="/admin/users/<%= user.id %>/reset-password" class="hidden flex items-end gap-3 flex-wrap rounded-lg border border-blue-200 bg-blue-50/50 p-4 dark:border-blue-500/20 dark:bg-blue-500/5">
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
Neues Passwort für <%= user.username %>
|
||
<input name="password" type="password" required
|
||
class="mt-1.5 block w-full sm:w-72 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-gray-800 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" />
|
||
</label>
|
||
<button type="submit" class="rounded-lg bg-blue-600 hover:bg-blue-700 transition-colors text-white font-medium px-4 py-2 shadow-sm">Speichern</button>
|
||
<button type="button" onclick="document.getElementById('resetForm').classList.toggle('hidden')"
|
||
class="rounded-lg px-3 py-2 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">Abbrechen</button>
|
||
</form>
|
||
</div>
|
||
|
||
<hr class="border-gray-100 dark:border-gray-700">
|
||
|
||
<!-- Anmelden als / Suchprofil -->
|
||
<div class="flex flex-wrap gap-2">
|
||
<% if (!istIch) { %>
|
||
<form method="POST" action="/admin/users/<%= user.id %>/impersonate" class="inline"
|
||
onsubmit="return confirm('Als „<%= user.username %>“ anmelden? Du siehst dann dessen Konto. Admin-Rechte sind dabei pausiert; über das Banner oben kannst du zurückwechseln.');">
|
||
<button type="submit" class="act-btn act-emerald" title="Dieses Konto übernehmen (Impersonation)">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14v7m-4-4h8"></path></svg>
|
||
<span>Anmelden als</span>
|
||
</button>
|
||
</form>
|
||
<% } %>
|
||
<a href="/jobsuche?user=<%= user.id %>" class="act-btn act-neutral" title="Suchprofil & Zeitplan dieses Benutzers verwalten">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||
<span>Jobsuche</span>
|
||
</a>
|
||
</div>
|
||
|
||
<% if (!istIch) { %>
|
||
<hr class="border-gray-100 dark:border-gray-700">
|
||
|
||
<!-- Löschen -->
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<p class="text-sm font-medium text-rose-700 dark:text-rose-400">Benutzer löschen</p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400">Löscht das Konto inkl. aller Bewerbungen und Dateien. Unumkehrbar.</p>
|
||
</div>
|
||
<form method="POST" action="/admin/users/<%= user.id %>/delete" class="inline"
|
||
onsubmit="return confirm('Benutzer „<%= user.username %>“ inkl. aller Daten wirklich löschen? Das lässt sich nicht rückgängig machen.');">
|
||
<button type="submit" class="act-btn act-danger" title="Benutzer inkl. aller Daten löschen">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||
<span>Löschen</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Letzte Bewerbungen -->
|
||
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Letzte Bewerbungen</h2>
|
||
<% if (bewerbungen.length) { %>
|
||
<ul class="divide-y divide-gray-100 dark:divide-gray-700">
|
||
<% bewerbungen.forEach(function(b){ %>
|
||
<li class="py-3 flex items-center gap-3">
|
||
<span class="inline-block w-2.5 h-2.5 rounded-full flex-shrink-0
|
||
<%= b.status === 'Einstellung' ? 'bg-green-500' :
|
||
b.status === 'Absage' ? 'bg-red-500' :
|
||
b.status === 'Vorstellungsgespräch' ? 'bg-yellow-500' :
|
||
'bg-gray-300' %>"></span>
|
||
<div class="min-w-0 flex-1">
|
||
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 truncate"><%= b.firma %></p>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 truncate"><%= b.stelle %></p>
|
||
</div>
|
||
<span class="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0"><%= new Date(b.datum).toLocaleDateString('de-DE') %></span>
|
||
</li>
|
||
<% }); %>
|
||
</ul>
|
||
<% if (anzahlBewerbungen > bewerbungen.length) { %>
|
||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-3">… und <%= anzahlBewerbungen - bewerbungen.length %> weitere.</p>
|
||
<% } %>
|
||
<% } else { %>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">Noch keine Bewerbungen.</p>
|
||
<% } %>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<%- include('partials/footer') %>
|
||
|
||
<style>
|
||
.act-btn{display:inline-flex;align-items:center;gap:.375rem;border-radius:.5rem;padding:.375rem .625rem;font-size:.75rem;font-weight:500;transition:background-color .15s,color .15s;line-height:1;}
|
||
.act-neutral{color:#4b5563}.act-neutral:hover{background:#f3f4f6}
|
||
.dark .act-neutral{color:#d1d5db}.dark .act-neutral:hover{background:rgba(255,255,255,.06)}
|
||
.act-emerald{color:#047857}.act-emerald:hover{background:#ecfdf5}
|
||
.dark .act-emerald{color:#6ee7b7}.dark .act-emerald:hover{background:rgba(16,185,129,.12)}
|
||
.act-blue{color:#2563eb}.act-blue:hover{background:#eff6ff}
|
||
.dark .act-blue{color:#93c5fd}.dark .act-blue:hover{background:rgba(59,130,246,.12)}
|
||
.act-danger{color:#e11d48}.act-danger:hover{background:#fff1f2}
|
||
.dark .act-danger{color:#fda4af}.dark .act-danger:hover{background:rgba(244,63,94,.12)}
|
||
</style>
|
||
|
||
<script>
|
||
// Relative Zeitangaben (data-dt) wie in der Admin-Übersicht formatieren.
|
||
(function () {
|
||
document.querySelectorAll('time[data-dt]').forEach(function (t) {
|
||
var s = t.getAttribute('data-dt');
|
||
if (!s) return;
|
||
var d = new Date(s.replace(' ', 'T') + 'Z');
|
||
if (isNaN(d.getTime())) return;
|
||
t.textContent = d.toLocaleDateString('de-DE') + ', ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<%- include('partials/favorit-script') %>
|
||
</body>
|
||
</html> |