Files
jobbi-bewerbung/views/admin-user.ejs
T
thomasandClaude 1c913b2b28 Admin: pro-Nutzer-Rechte für Suchlauf-Start und Zeitplan
- Neue users-Spalten darf_suchlauf_starten / darf_zeitplan_aendern
  (Default 0 = deaktiviert); idempotente ALTER-Migration für Bestands-DBs.
- Admin-Detailseite: zwei Toggles (Erlaubt/Gesperrt) je Nutzer.
- /jobsuche: ohne Recht ist „Jetzt suchen" ausgeblendet und der Zeitplan
  (aktiv/Tage/Uhrzeit) read-only; Speichern und manueller Start sind
  serverseitig abgesichert. Admin (fremd) ist nie beschränkt.
- Default-Zeitplan bleibt Mo–Fr 17:00 (suchprofil-Defaults).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 08:47:48 +02:00

229 lines
16 KiB
Plaintext
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>
<%- 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">
<!-- Suchläufe starten -->
<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">Suchläufe selber starten</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Manueller „Jetzt suchen"-Knopf auf der Jobsuche. Der automatische Zeitplan läuft davon unberührt.</p>
</div>
<form method="POST" action="/admin/users/<%= user.id %>/toggle-suchlauf" class="inline">
<button type="submit"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors
<%= user.darf_suchlauf_starten ? 'bg-green-600 hover:bg-green-700 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600' %>">
<span class="inline-block w-2 h-2 rounded-full <%= user.darf_suchlauf_starten ? 'bg-white' : 'bg-gray-400' %>"></span>
<%= user.darf_suchlauf_starten ? 'Erlaubt' : 'Gesperrt' %>
</button>
</form>
</div>
<hr class="border-gray-100 dark:border-gray-700">
<!-- Zeitplan ändern -->
<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">Zeitplan ändern dürfen</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Wochentage, Uhrzeit und „aktiv". Standard: MoFr 17:00, verwaltet vom Administrator.</p>
</div>
<form method="POST" action="/admin/users/<%= user.id %>/toggle-zeitplan" class="inline">
<button type="submit"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors
<%= user.darf_zeitplan_aendern ? 'bg-green-600 hover:bg-green-700 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600' %>">
<span class="inline-block w-2 h-2 rounded-full <%= user.darf_zeitplan_aendern ? 'bg-white' : 'bg-gray-400' %>"></span>
<%= user.darf_zeitplan_aendern ? 'Erlaubt' : 'Gesperrt' %>
</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>