Projektleiter-Rolle, Papierkorb, neue Login-Seite und Benutzerverwaltung

- Neue Rolle "Projektleiter": Projekte anlegen und löschen nur noch für
  Projektleiter und Administratoren (serverseitig in /save-week geprüft)
- Papierkorb: gelöschte Projekte 30 Tage durch Admins wiederherstellbar
- Benutzerverwaltung: Aktionen im Drei-Punkte-Menü mit Rückfrage im Menü,
  getrennte Felder für Vor- und Nachname, E-Mail-Vorschlag
  v.nachname@rauschenberg-ing.de, Benutzername = Anfang der E-Mail,
  Person-ID aus dem Kürzel
- Endgültiges Löschen eines Benutzers entfernt ihn auch aus den Projekten
  und Aufgaben der aktuellen Sitzung; Dateien bleiben als
  "Gelöschter Benutzer"
- Login-Seite neu (Design "Kalenderwoche") mit Logo als Wasserzeichen;
  Anmeldung ohne Groß-/Kleinschreibung, auch mit voller E-Mail-Adresse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-10-22 09:25:33 +02:00
1 parent 93505e0e48
commit f4fb3c550b
13 files changed
+1214 -116

No files matched your search

+100 -7
View File
@@ -33,12 +33,30 @@ let currentUser = null;
function renderUserBar(){
const info = document.getElementById('userBarInfo');
if(info && currentUser){
const roleLabel = currentUser.role === 'admin' ? 'Administrator'
: currentUser.role === 'kiosk' ? 'Kiosk' : 'Mitglied';
const roleLabel = systemRolleText(currentUser.role);
info.textContent = (currentUser.displayName || currentUser.username) + ' (' + roleLabel + ')';
}
const navAdminSection = document.getElementById('navAdminSection');
if(navAdminSection) navAdminSection.style.display = (currentUser && currentUser.role === 'admin') ? '' : 'none';
document.querySelectorAll('.nur-projektleitung').forEach(el => { el.hidden = !darfProjektlisteAendern(); });
}
// systemRolleText: Anzeigename einer Systemrolle (users.role). Auch von der
// Benutzerverwaltung (js/admin-users.js) genutzt.
function systemRolleText(role){
return role === 'admin' ? 'Administrator'
: role === 'projektleiter' ? 'Projektleiter'
: role === 'kiosk' ? 'Kiosk' : 'Mitglied';
}
// darfProjektlisteAendern: ob das angemeldete Konto Projekte anlegen und aus
// der Projektliste löschen darf (Projektleiter und Administratoren). Reines
// UI-Gate — dieselbe Regel setzt /save-week serverseitig durch
// (darfProjektlisteAendern() in server.js), dort mit 403. Knöpfe zum
// Anlegen tragen die Klasse .nur-projektleitung und werden von
// renderUserBar() ausgeblendet.
function darfProjektlisteAendern(){
return !!currentUser && (currentUser.role === 'admin' || currentUser.role === 'projektleiter');
}
// logout: beendet die Server-Session und lädt die Seite neu (zeigt danach
@@ -48,9 +66,63 @@ async function logout(){
window.location.reload();
}
// showLoginOverlay / hideLoginOverlay: blenden das blockierende Login-Overlay ein/aus.
// showLoginOverlay / hideLoginOverlay: blenden die blockierende Login-Seite ein/aus.
function showLoginOverlay(){
renderLoginWoche();
document.getElementById('loginOverlay').style.display = 'flex';
const feld = document.getElementById('loginUsername');
if(feld && !feld.value) feld.focus();
}
// renderLoginWoche: füllt die rechte Seite der Login-Seite — laufende
// Kalenderwoche (getWeekNumber(), js/utils.js), Jahr der ISO-Woche, die
// Arbeitstage Mo-Fr mit dem heutigen Tag markiert und das Datum.
function renderLoginWoche(){
const heute = new Date();
const kw = getWeekNumber(heute);
// ISO-Jahr: das Jahr des Donnerstags dieser Woche (Ende Dezember/Anfang
// Januar weicht es vom Kalenderjahr ab, z. B. KW 01 / 2027 am 30.12.2026).
const donnerstag = new Date(heute); donnerstag.setDate(heute.getDate() + 3 - ((heute.getDay() + 6) % 7));
const zahl = document.getElementById('loginKwZahl');
if(!zahl) return;
zahl.textContent = String(kw).padStart(2, '0');
document.getElementById('loginKwJahr').textContent = donnerstag.getFullYear();
document.getElementById('loginDatum').textContent =
heute.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const montag = new Date(heute); montag.setDate(heute.getDate() - ((heute.getDay() + 6) % 7));
document.getElementById('loginTage').innerHTML = ['Mo', 'Di', 'Mi', 'Do', 'Fr'].map((n, i) => {
const d = new Date(montag); d.setDate(montag.getDate() + i);
const istHeute = d.toDateString() === heute.toDateString();
return '<li' + (istHeute ? ' class="ist-heute" aria-current="date"' : '') + '><span>' + n + '</span><b>' + d.getDate() + '</b></li>';
}).join('');
}
// verdrahteLoginHilfen: „Anzeigen" für das Passwort und der Hinweis auf
// eine aktive Feststelltaste. Einmalig.
let loginHilfenVerdrahtet = false;
function verdrahteLoginHilfen(){
if(loginHilfenVerdrahtet) return;
loginHilfenVerdrahtet = true;
const pw = document.getElementById('loginPassword');
const toggle = document.getElementById('loginPwToggle');
const caps = document.getElementById('loginCaps');
if(toggle) toggle.addEventListener('click', () => {
const sichtbar = pw.type === 'text';
pw.type = sichtbar ? 'password' : 'text';
toggle.textContent = sichtbar ? 'Anzeigen' : 'Verbergen';
toggle.setAttribute('aria-pressed', String(!sichtbar));
pw.focus();
});
const capsPruefen = e => { if(caps && e.getModifierState) caps.hidden = !e.getModifierState('CapsLock'); };
pw.addEventListener('keydown', capsPruefen);
pw.addEventListener('keyup', capsPruefen);
pw.addEventListener('blur', () => { if(caps) caps.hidden = true; });
['loginUsername', 'loginPassword'].forEach(id => {
document.getElementById(id).addEventListener('input', e => {
e.target.closest('.login-feld').classList.remove('hat-fehler');
document.getElementById('loginError').hidden = true;
});
});
}
function hideLoginOverlay(){
document.getElementById('loginOverlay').style.display = 'none';
@@ -174,24 +246,45 @@ async function checkAuthAndInit(onAuthenticated){
return;
}
showLoginOverlay();
verdrahteLoginHilfen();
const form = document.getElementById('loginForm');
const errEl = document.getElementById('loginError');
const btn = document.getElementById('loginSubmit');
const zeigeFehler = (text, feldId) => {
errEl.textContent = text;
errEl.hidden = false;
if(feldId){
const feld = document.getElementById(feldId);
feld.closest('.login-feld').classList.add('hat-fehler');
feld.focus();
}
};
form.onsubmit = async function(ev){
ev.preventDefault();
errEl.textContent = '';
const username = document.getElementById('loginUsername').value;
errEl.hidden = true;
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
if(!username) return zeigeFehler('Bitte den Benutzernamen eingeben, z. B. m.mustermann.', 'loginUsername');
if(!password) return zeigeFehler('Bitte das Passwort eingeben.', 'loginPassword');
btn.disabled = true; btn.classList.add('laedt');
try {
const r = await fetch(baseUrl() + '/login', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ username, password })
});
const d = await r.json();
if(!r.ok || !d.ok){ errEl.textContent = d.error || 'Anmeldung fehlgeschlagen.'; return; }
if(!r.ok || !d.ok){
zeigeFehler(d.error || 'Anmeldung fehlgeschlagen.', 'loginPassword');
document.getElementById('loginPassword').select();
return;
}
currentUser = d.user;
document.getElementById('loginPassword').value = '';
afterAuthenticated(onAuthenticated);
} catch(err){
errEl.textContent = 'Verbindung zum Server fehlgeschlagen.';
zeigeFehler('Der Server ist nicht erreichbar. Bitte später erneut versuchen.');
} finally {
btn.disabled = false; btn.classList.remove('laedt');
}
};
}