diff --git a/server.js b/server.js
index 2b97899..f99c0fa 100644
--- a/server.js
+++ b/server.js
@@ -4284,6 +4284,53 @@ initializeDatabase().then(async () => {
}
});
+ // Einzelnen Benutzer verwalten (Detailansicht von „Bestehende Benutzer").
+ app.get('/admin/users/:id', requireAdmin, async (req, res) => {
+ try {
+ const id = Number(req.params.id);
+ const user = await dbGet('SELECT id, username, is_admin, created_at FROM users WHERE id = ?', [id]);
+ if (!user) return res.status(404).send('Benutzer nicht gefunden.');
+ const anzahl = await dbGet('SELECT COUNT(*) AS c FROM bewerbungen WHERE user_id = ?', [id]);
+ const bewerbungen = await dbAll(
+ 'SELECT id, firma, stelle, status, datum FROM bewerbungen WHERE user_id = ? ORDER BY date(datum) DESC, created_at DESC LIMIT 10',
+ [id]
+ );
+ const adminCount = await dbGet('SELECT COUNT(*) AS c FROM users WHERE is_admin = 1');
+ res.render('admin-user', {
+ user,
+ currentUserId: req.user.id,
+ anzahlBewerbungen: anzahl ? anzahl.c : 0,
+ bewerbungen: bewerbungen || [],
+ adminCount: adminCount ? adminCount.c : 0,
+ hideSettings: false,
+ });
+ } catch (error) {
+ console.error('Admin user detail error:', error);
+ res.status(500).send('Serverfehler');
+ }
+ });
+
+ // Admin-Status eines Benutzers umschalten. Der letzte Admin darf nicht
+ // abgesetzt werden (sonst sperrt man sich aus).
+ app.post('/admin/users/:id/toggle-admin', requireAdmin, async (req, res) => {
+ try {
+ const id = Number(req.params.id);
+ const target = await dbGet('SELECT id, is_admin FROM users WHERE id = ?', [id]);
+ if (!target) return res.status(404).send('Benutzer nicht gefunden.');
+ if (target.is_admin) {
+ const adminCount = await dbGet('SELECT COUNT(*) AS c FROM users WHERE is_admin = 1');
+ if (adminCount && adminCount.c <= 1) return res.status(400).send('Der letzte Admin darf nicht abgesetzt werden.');
+ }
+ const neu = target.is_admin ? 0 : 1;
+ await dbRun('UPDATE users SET is_admin = ? WHERE id = ?', [neu, id]);
+ await auditLog(req.user.id, id, `admin_toggle → ${neu ? 'admin' : 'user'}`);
+ res.redirect('/admin/users/' + id);
+ } catch (error) {
+ console.error('Admin toggle-admin error:', error);
+ res.status(500).send('Serverfehler');
+ }
+ });
+
// --- Impersonation: admin "logs in as" a user --------------------------
// Best practice (Django/Flask-impersonate style): the admin's own session
// keeps its cookie token, but its user_id switches to the target and the
diff --git a/views/admin-user.ejs b/views/admin-user.ejs
new file mode 100644
index 0000000..94c0775
--- /dev/null
+++ b/views/admin-user.ejs
@@ -0,0 +1,193 @@
+
+
+
+ <%- include('partials/head') %>
+
+
+ <%- include('partials/header') %>
+
+
+
+
+
+
+ Zur Benutzerverwaltung
+
+
+ <%
+ const istIch = user.id === currentUserId;
+ const letzterAdmin = user.is_admin && adminCount <= 1;
+ %>
+
+
+
+ <%= String(user.username || '?').charAt(0).toUpperCase() %>
+
+
+
+
<%= user.username %>
+ <% if (user.is_admin) { %>
+ Admin
+ <% } else { %>
+ Benutzer
+ <% } %>
+ <% if (istIch) { %>(du) <% } %>
+
+
+ <%= anzahlBewerbungen %> <%= anzahlBewerbungen === 1 ? 'Bewerbung' : 'Bewerbungen' %> ·
+ angelegt <%= user.created_at %>
+
+
+
+
+
+
+
+ Benutzer verwalten
+
+
+
+
+
+
Admin-Status
+
+ <% if (letzterAdmin) { %>Letzter Admin – kann nicht abgesetzt werden.<% } else { %>Admins können Benutzer verwalten und andere Konten übernehmen.<% } %>
+
+
+
+
+
+
+
+
+
+
+
+
Passwort zurücksetzen
+
Alle Sitzungen dieses Benutzers werden ungültig.
+
+
+
+ Neues Passwort
+
+
+
+
+
+
+
+
+
+
+ <% if (!istIch) { %>
+
+
+
+
+
+
Benutzer löschen
+
Löscht das Konto inkl. aller Bewerbungen und Dateien. Unumkehrbar.
+
+
+
+ <% } %>
+
+
+
+
+
+ Letzte Bewerbungen
+ <% if (bewerbungen.length) { %>
+
+ <% bewerbungen.forEach(function(b){ %>
+
+
+
+
<%= b.firma %>
+
<%= b.stelle %>
+
+ <%= new Date(b.datum).toLocaleDateString('de-DE') %>
+
+ <% }); %>
+
+ <% if (anzahlBewerbungen > bewerbungen.length) { %>
+ … und <%= anzahlBewerbungen - bewerbungen.length %> weitere.
+ <% } %>
+ <% } else { %>
+ Noch keine Bewerbungen.
+ <% } %>
+
+
+
+
+ <%- include('partials/footer') %>
+
+
+
+
+
+ <%- include('partials/favorit-script') %>
+
+
\ No newline at end of file
diff --git a/views/admin.ejs b/views/admin.ejs
index 0719ab0..8bdf928 100644
--- a/views/admin.ejs
+++ b/views/admin.ejs
@@ -104,13 +104,14 @@
<% users.forEach(function(u) { %>
-
+
+
<% if (u.is_admin) { %>
@@ -128,11 +129,6 @@
diff --git a/views/bewerbung.ejs b/views/bewerbung.ejs
index 5648692..343b294 100644
--- a/views/bewerbung.ejs
+++ b/views/bewerbung.ejs
@@ -496,10 +496,30 @@
-
<% const genStatus = application.generierung_status; %>
- <% if (genStatus || (anhaenge && anhaenge.length) || (application.stellenbeschreibung && application.stellenbeschreibung.trim())) { %>
-
+ <% if (zeigeUnterlagen) { %>
+
+
+
+
+
+ Unterlagen
+
+
+
+ E-Mail
+
+
+
+
Bewerbungsunterlagen
@@ -1074,6 +1094,35 @@
} catch (e) { /* keep polling */ }
}, 4000);
}
+
+ // --- Umschalter Unterlagen <-> E-Mail-Korrespondenz (rechte Spalte) ---
+ (function () {
+ var unterlagen = document.getElementById('unterlagen');
+ var korrespondenz = document.getElementById('korrespondenz');
+ if (!unterlagen || !korrespondenz) return; // kein Toggle nötig
+ var tabs = Array.prototype.slice.call(document.querySelectorAll('#rechtsTabs [data-tab]'));
+ function setActive(name) {
+ unterlagen.classList.toggle('hidden', name !== 'unterlagen');
+ korrespondenz.classList.toggle('hidden', name !== 'korrespondenz');
+ tabs.forEach(function (t) { t.classList.toggle('active', t.getAttribute('data-tab') === name); });
+ // E-Mail-iframes nachmessen, falls sie beim Laden versteckt waren.
+ if (name === 'korrespondenz') {
+ korrespondenz.querySelectorAll('.email-html-frame').forEach(fitFrame);
+ }
+ }
+ function fromHash() {
+ var h = (location.hash || '').replace(/^#/, '');
+ return (h === 'unterlagen' || h === 'korrespondenz') ? h : 'unterlagen';
+ }
+ setActive(fromHash());
+ window.addEventListener('hashchange', function () { setActive(fromHash()); });
+ tabs.forEach(function (t) {
+ t.addEventListener('click', function () {
+ history.replaceState(null, '', '#' + t.getAttribute('data-tab'));
+ setActive(t.getAttribute('data-tab'));
+ });
+ });
+ })();
})();
// Generation form: require at least one document, and keep the