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:
1 parent
93505e0e48
commit
f4fb3c550b
13 files changed
+1214
-116
No files matched your search
+254
-33
@@ -37,6 +37,10 @@
|
||||
|
||||
// initialsManuallyEdited: gleiches Muster für das Kürzel-Feld.
|
||||
let initialsManuallyEdited = false;
|
||||
// emailManuallyEdited: wie oben, für den E-Mail-Vorschlag (emailVorschlag()).
|
||||
let emailManuallyEdited = false;
|
||||
// usernameManuallyEdited: wie oben, für den Benutzernamen ("Vorname Nachname").
|
||||
let usernameManuallyEdited = false;
|
||||
// editUserAbsencesCache: zuletzt geladene Abwesenheiten des im
|
||||
// Bearbeiten-Modal geöffneten Benutzers (für Rendern/Löschen ohne erneuten
|
||||
// Request pro Aktion).
|
||||
@@ -142,27 +146,24 @@
|
||||
const statusParts = [];
|
||||
statusParts.push(u.active ? '<span class="badge-active">Aktiv</span>' : '<span class="badge-inactive">Deaktiviert</span>');
|
||||
if(u.mustChangePassword) statusParts.push('<span class="badge-warn">Passwortwechsel erforderlich</span>');
|
||||
const toggleLabel = u.active ? 'Deaktivieren' : 'Aktivieren';
|
||||
const isSelf = currentUser && currentUser.id === u.id;
|
||||
const deleteBtn = (!u.active)
|
||||
? `<button class="danger small" ${isSelf ? 'disabled' : ''} onclick="hardDeleteUser(${u.id}, '${escJs(u.username)}')">Endgültig löschen</button>`
|
||||
: '';
|
||||
const deptLabel = (DEPARTMENTS[u.department] && DEPARTMENTS[u.department].label) || esc(u.department || '');
|
||||
return `<tr>
|
||||
<td>${esc(u.username)}</td>
|
||||
<td>${esc(u.displayName)}</td>
|
||||
<td>${esc(u.email || '')}</td>
|
||||
<td>${u.role === 'admin' ? 'Administrator' : u.role === 'kiosk' ? 'Kiosk' : 'Mitglied'}</td>
|
||||
<td>${esc(systemRolleText(u.role))}</td>
|
||||
<td>${deptLabel}</td>
|
||||
<td>${esc(u.initials || '')}</td>
|
||||
<td>${u.capacityHours ? esc(fmtCapacity(u.capacityHours)) : '<span class="hint" style="margin:0">voll</span>'}</td>
|
||||
<td>${esc(u.personId || '')}</td>
|
||||
<td>${statusParts.join('<br>')}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="secondary small" onclick="openEditUserModal(${u.id})">Bearbeiten</button>
|
||||
<button class="secondary small" onclick="resetUserPassword(${u.id}, '${escJs(u.username)}')">Passwort zurücksetzen</button>
|
||||
<button class="secondary small" ${isSelf ? 'disabled title="Sie können sich nicht selbst deaktivieren."' : ''} onclick="toggleUserActive(${u.id}, ${u.active})">${toggleLabel}</button>
|
||||
${deleteBtn}
|
||||
<span class="user-aktionen">
|
||||
<button class="secondary small" onclick="openEditUserModal(${u.id})">Bearbeiten</button>
|
||||
<button type="button" class="user-kebab" aria-haspopup="menu" aria-expanded="false"
|
||||
aria-label="Weitere Aktionen für ${esc(u.displayName || u.username)}"
|
||||
onclick="openUserActionMenu(event, ${u.id})">${PUNKTE_SVG}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -198,7 +199,8 @@
|
||||
|
||||
window.openNewUserModal = function(){
|
||||
document.getElementById('newUserUsername').value = '';
|
||||
document.getElementById('newUserDisplayName').value = '';
|
||||
document.getElementById('newUserFirstName').value = '';
|
||||
document.getElementById('newUserLastName').value = '';
|
||||
document.getElementById('newUserEmail').value = '';
|
||||
document.getElementById('newUserRole').value = 'member';
|
||||
document.getElementById('newUserDepartment').value = DEFAULT_DEPARTMENT;
|
||||
@@ -211,27 +213,66 @@
|
||||
document.getElementById('newUserError').textContent = '';
|
||||
personIdManuallyEdited = false;
|
||||
initialsManuallyEdited = false;
|
||||
emailManuallyEdited = false;
|
||||
usernameManuallyEdited = false;
|
||||
document.getElementById('newUserModal').style.display = 'flex';
|
||||
document.getElementById('newUserUsername').focus();
|
||||
document.getElementById('newUserFirstName').focus();
|
||||
};
|
||||
|
||||
window.closeNewUserModal = function(){
|
||||
document.getElementById('newUserModal').style.display = 'none';
|
||||
};
|
||||
|
||||
// onNewUserDisplayNameInput: schlägt beim Tippen des Anzeigenamens eine
|
||||
// Person-ID über personIdForName() vor (js/utils.js) — nur solange das Feld
|
||||
// nicht bereits manuell bearbeitet wurde (Kollisionsvermeidung laut Plan).
|
||||
window.onNewUserDisplayNameInput = function(){
|
||||
const name = document.getElementById('newUserDisplayName').value;
|
||||
if(!personIdManuallyEdited){
|
||||
document.getElementById('newUserPersonId').value = name.trim() ? personIdForName(name) : '';
|
||||
}
|
||||
// onNewUserNameInput: schlägt beim Tippen von Vor-/Nachname E-Mail,
|
||||
// Benutzername (= Anfang der E-Mail, z. B. m.koerner), Person-ID
|
||||
// (personIdForName()) und Kürzel vor (js/utils.js) — jedes Feld nur,
|
||||
// solange es nicht bereits manuell bearbeitet wurde.
|
||||
window.onNewUserNameInput = function(){
|
||||
const vorname = document.getElementById('newUserFirstName').value;
|
||||
const nachname = document.getElementById('newUserLastName').value;
|
||||
const name = (vorname.trim() + ' ' + nachname.trim()).trim();
|
||||
if(!initialsManuallyEdited){
|
||||
document.getElementById('newUserInitials').value = name.trim() ? initialsForName(name) : '';
|
||||
}
|
||||
personIdAusKuerzel();
|
||||
// Firmen-Adresse nach dem Schema v.nachname@rauschenberg-ing.de
|
||||
// (emailVorschlag(), js/utils.js) — wie Person-ID und Kürzel nur,
|
||||
// solange das Feld nicht von Hand geändert wurde.
|
||||
if(!emailManuallyEdited){
|
||||
document.getElementById('newUserEmail').value = emailVorschlag(vorname, nachname);
|
||||
}
|
||||
benutzernameAusEmail();
|
||||
};
|
||||
|
||||
// personIdAusKuerzel: Person-ID aus dem Kürzel ("MK" → "p_mk") — dasselbe
|
||||
// Schema, das alle Bestandskonten tragen (ihre IDs stammen aus der Zeit,
|
||||
// als die Personenliste über das Kürzel geführt wurde) und an dem ihre
|
||||
// Projektzuordnungen hängen. Vorher kam die ID aus dem vollen Namen
|
||||
// ("p_miriam_k_rner"), sodass neue und alte Konten verschieden aussahen.
|
||||
// Ist die ID schon vergeben, wird eine Ziffer angehängt (p_mk2, p_mk3 …),
|
||||
// denn person_id ist eindeutig. Folgt dem Kürzel, solange die ID nicht
|
||||
// von Hand geändert wurde.
|
||||
function personIdAusKuerzel(){
|
||||
if(personIdManuallyEdited) return;
|
||||
const kuerzel = document.getElementById('newUserInitials').value.trim();
|
||||
const feld = document.getElementById('newUserPersonId');
|
||||
if(!kuerzel){ feld.value = ''; return; }
|
||||
const basis = personIdForName(kuerzel);
|
||||
const vergeben = new Set(usersCache.map(u => u.personId).filter(Boolean));
|
||||
let id = basis, n = 2;
|
||||
while(vergeben.has(id)) id = basis + (n++);
|
||||
feld.value = id;
|
||||
}
|
||||
|
||||
// benutzernameAusEmail: Benutzername = Anfang der E-Mail-Adresse. Folgt
|
||||
// der Adresse auch dann, wenn sie von Hand geändert wird (z. B. bei einem
|
||||
// Doppelnamen) — solange der Benutzername selbst nicht von Hand geändert
|
||||
// wurde.
|
||||
function benutzernameAusEmail(){
|
||||
if(usernameManuallyEdited) return;
|
||||
const adresse = document.getElementById('newUserEmail').value.trim();
|
||||
document.getElementById('newUserUsername').value = adresse.includes('@') ? adresse.slice(0, adresse.indexOf('@')) : adresse;
|
||||
}
|
||||
window.onNewUserPwModeChange = function(){
|
||||
const manual = document.querySelector('input[name="newUserPwMode"]:checked').value === 'manual';
|
||||
document.getElementById('newUserPassword').style.display = manual ? '' : 'none';
|
||||
@@ -239,7 +280,9 @@
|
||||
|
||||
window.saveNewUser = function(){
|
||||
const username = document.getElementById('newUserUsername').value.trim();
|
||||
const displayName = document.getElementById('newUserDisplayName').value.trim();
|
||||
// Mit großem Anfangsbuchstaben gespeichert.
|
||||
const firstName = grossAnfang(document.getElementById('newUserFirstName').value);
|
||||
const lastName = grossAnfang(document.getElementById('newUserLastName').value);
|
||||
const email = document.getElementById('newUserEmail').value.trim();
|
||||
const role = document.getElementById('newUserRole').value;
|
||||
const department = document.getElementById('newUserDepartment').value;
|
||||
@@ -251,8 +294,12 @@
|
||||
const errEl = document.getElementById('newUserError');
|
||||
errEl.textContent = '';
|
||||
|
||||
if(!username || !displayName){
|
||||
errEl.textContent = 'Benutzername und Anzeigename sind erforderlich.';
|
||||
if(!firstName || !lastName){
|
||||
errEl.textContent = 'Vor- und Nachname sind erforderlich.';
|
||||
return;
|
||||
}
|
||||
if(!username){
|
||||
errEl.textContent = 'Benutzername ist erforderlich.';
|
||||
return;
|
||||
}
|
||||
if(!department){
|
||||
@@ -275,7 +322,7 @@
|
||||
errEl.textContent = 'Bitte eine Personen-Kennung vergeben — ohne sie kann der Benutzer keiner Sitzung und keinen Aufgaben zugeordnet werden.';
|
||||
return;
|
||||
}
|
||||
const body = { username, displayName, email, role, personId, department, initials, capacityHours, roles };
|
||||
const body = { username, firstName, lastName, email, role, personId, department, initials, capacityHours, roles };
|
||||
if(pwMode === 'manual'){
|
||||
const password = document.getElementById('newUserPassword').value;
|
||||
if(password.length < 8){
|
||||
@@ -307,8 +354,13 @@
|
||||
if(!u) return;
|
||||
editingUserId = id;
|
||||
document.getElementById('editUserUsernameLabel').textContent = 'Benutzername: ' + u.username + ' (nicht änderbar)';
|
||||
document.getElementById('editUserDisplayName').value = u.displayName || '';
|
||||
// Konten von vor der Umstellung tragen evtl. noch keinen Vor-/Nachnamen:
|
||||
// dann aus dem Anzeigenamen geteilt (erstes Wort / Rest).
|
||||
const teile = String(u.displayName || '').trim().split(/\s+/).filter(Boolean);
|
||||
document.getElementById('editUserFirstName').value = u.firstName || u.lastName ? (u.firstName || '') : (teile[0] || '');
|
||||
document.getElementById('editUserLastName').value = u.firstName || u.lastName ? (u.lastName || '') : teile.slice(1).join(' ');
|
||||
document.getElementById('editUserEmail').value = u.email || '';
|
||||
onEditUserNameInput();
|
||||
document.getElementById('editUserRole').value = u.role;
|
||||
document.getElementById('editUserDepartment').value = u.department || DEFAULT_DEPARTMENT;
|
||||
document.getElementById('editUserInitials').value = u.initials || '';
|
||||
@@ -346,7 +398,8 @@
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
displayName: document.getElementById('editUserDisplayName').value.trim(),
|
||||
firstName: grossAnfang(document.getElementById('editUserFirstName').value),
|
||||
lastName: grossAnfang(document.getElementById('editUserLastName').value),
|
||||
email: document.getElementById('editUserEmail').value.trim(),
|
||||
role: document.getElementById('editUserRole').value,
|
||||
department,
|
||||
@@ -447,8 +500,8 @@
|
||||
|
||||
// ---------- Zeilen-Aktionen (Passwort-Reset, Aktivieren/Deaktivieren, Löschen) ----------
|
||||
|
||||
window.resetUserPassword = function(id, username){
|
||||
if(!confirm('Passwort für "' + username + '" wirklich zurücksetzen? Bisherige Sitzungen dieses Benutzers werden beendet.')) return;
|
||||
window.resetUserPassword = function(id, username, ohneRueckfrage){
|
||||
if(!ohneRueckfrage && !confirm('Passwort für "' + username + '" wirklich zurücksetzen? Bisherige Sitzungen dieses Benutzers werden beendet.')) return;
|
||||
apiFetch('/admin/users/' + id + '/reset-password', { method: 'POST' }).then(res => {
|
||||
if(!res.ok || !res.data || !res.data.ok){
|
||||
alert((res.data && res.data.error) || ('Fehler (HTTP ' + res.status + ')'));
|
||||
@@ -459,9 +512,9 @@
|
||||
}).catch(err => alert('Verbindung zum Server fehlgeschlagen: ' + err.message));
|
||||
};
|
||||
|
||||
window.toggleUserActive = function(id, currentlyActive){
|
||||
window.toggleUserActive = function(id, currentlyActive, ohneRueckfrage){
|
||||
const goingActive = !currentlyActive;
|
||||
if(!goingActive && !confirm('Benutzer wirklich deaktivieren? Bestehende Sitzungen werden sofort beendet.')) return;
|
||||
if(!goingActive && !ohneRueckfrage && !confirm('Benutzer wirklich deaktivieren? Bestehende Sitzungen werden sofort beendet.')) return;
|
||||
apiFetch('/admin/users/' + id, { method: 'PATCH', body: JSON.stringify({ active: goingActive }) }).then(res => {
|
||||
if(!res.ok || !res.data || !res.data.ok){
|
||||
alert((res.data && res.data.error) || ('Fehler (HTTP ' + res.status + ')'));
|
||||
@@ -471,17 +524,177 @@
|
||||
}).catch(err => alert('Verbindung zum Server fehlgeschlagen: ' + err.message));
|
||||
};
|
||||
|
||||
window.hardDeleteUser = function(id, username){
|
||||
if(!confirm('Benutzer "' + username + '" wirklich ENDGÜLTIG löschen? Das kann nicht rückgängig gemacht werden.')) return;
|
||||
window.hardDeleteUser = function(id, username, ohneRueckfrage){
|
||||
if(!ohneRueckfrage && !confirm('Benutzer "' + username + '" wirklich ENDGÜLTIG löschen? Die Person wird auch aus allen Projekten und Aufgaben der aktuellen Sitzung entfernt. Das kann nicht rückgängig gemacht werden.')) return;
|
||||
apiFetch('/admin/users/' + id + '?hard=1', { method: 'DELETE' }).then(res => {
|
||||
if(!res.ok || !res.data || !res.data.ok){
|
||||
alert((res.data && res.data.error) || ('Fehler (HTTP ' + res.status + ')'));
|
||||
return;
|
||||
}
|
||||
loadAndRenderUsers();
|
||||
// Der Server hat die Person aus der aktuellen Sitzung entfernt (Projekt-
|
||||
// zuordnungen, Aufgaben, Teilnehmerkreis). Neu laden, damit Projekte
|
||||
// und Auslastung in diesem Browser sofort ohne sie dastehen und das
|
||||
// nächste Speichern nicht auf einem veralteten Stand aufsetzt.
|
||||
if(typeof load === 'function') load(true);
|
||||
}).catch(err => alert('Verbindung zum Server fehlgeschlagen: ' + err.message));
|
||||
};
|
||||
|
||||
// ---------- Drei-Punkte-Menü der Zeilen ----------
|
||||
// „Bearbeiten" bleibt als Knopf in der Zeile, die selteneren Aktionen
|
||||
// liegen hinter den drei Punkten (Eriks Wahl „C" aus dem Vergleich der
|
||||
// Menü-Varianten). Was nicht rückgängig zu machen ist — Passwort
|
||||
// zurücksetzen, Deaktivieren, Endgültig löschen — fragt direkt im Menü
|
||||
// nach, statt ein Browser-Dialogfenster zu öffnen.
|
||||
//
|
||||
// Das Menü hängt an <body> und steht `position: fixed`: Die Tabelle liegt
|
||||
// in einem quer scrollbaren Rahmen, der ein darin verankertes Menü
|
||||
// abschneiden würde. Reicht der Platz unter dem Knopf nicht, klappt es nach
|
||||
// oben auf. Scrollen oder Fenstergröße ändern schließt es.
|
||||
|
||||
const PUNKTE_SVG = '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><circle cx="3.2" cy="8" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12.8" cy="8" r="1.4"/></svg>';
|
||||
const MENU_IC = {
|
||||
schluessel: '<svg class="user-menu-ic" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5.5" cy="10.5" r="3"/><path d="M7.6 8.4L13.5 2.5M11 5l1.8 1.8M12.5 3.5l1.5 1.5"/></svg>',
|
||||
aus: '<svg class="user-menu-ic" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M4 12L12 4"/></svg>',
|
||||
an: '<svg class="user-menu-ic" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M5.5 8.2l1.8 1.8 3.4-3.6"/></svg>',
|
||||
muell: '<svg class="user-menu-ic" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 4.5h11M6 4.5V3h4v1.5M4 4.5l.7 8.5h6.6l.7-8.5"/></svg>',
|
||||
};
|
||||
|
||||
// offenesMenu: { el, knopf, zeile, u } solange ein Menü offen ist.
|
||||
let offenesMenu = null;
|
||||
|
||||
function menuEintrag(ic, text, aktion, opts){
|
||||
opts = opts || {};
|
||||
return `<button type="button" role="menuitem" class="user-menu-item${opts.gefahr ? ' is-danger' : ''}"
|
||||
data-aktion="${aktion}" ${opts.gesperrt ? 'aria-disabled="true"' : ''}>
|
||||
${MENU_IC[ic]}<span class="user-menu-text"><span>${esc(text)}</span>${opts.unter ? `<span class="user-menu-sub">${esc(opts.unter)}</span>` : ''}</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function menuInhalt(u){
|
||||
const isSelf = currentUser && currentUser.id === u.id;
|
||||
let h = menuEintrag('schluessel', 'Passwort zurücksetzen', 'passwort');
|
||||
h += u.active
|
||||
? menuEintrag('aus', 'Deaktivieren', 'deaktivieren', { gesperrt: isSelf, unter: isSelf ? 'Eigenes Konto' : '' })
|
||||
: menuEintrag('an', 'Aktivieren', 'aktivieren');
|
||||
if(!u.active){
|
||||
h += '<hr class="user-menu-trenner">'
|
||||
+ menuEintrag('muell', 'Endgültig löschen', 'loeschen', { gefahr: true, gesperrt: isSelf });
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
const RUECKFRAGEN = {
|
||||
passwort: u => ({ frage: 'Passwort von <b>' + esc(u.displayName || u.username) + '</b> zurücksetzen? Alle Sitzungen werden abgemeldet.', knopf: 'Zurücksetzen' }),
|
||||
deaktivieren: u => ({ frage: '<b>' + esc(u.displayName || u.username) + '</b> deaktivieren? Alle Sitzungen werden sofort abgemeldet.', knopf: 'Deaktivieren' }),
|
||||
loeschen: u => ({ frage: '<b>' + esc(u.displayName || u.username) + '</b> endgültig löschen? Die Person wird auch aus allen Projekten und Aufgaben der aktuellen Sitzung entfernt. Das lässt sich nicht rückgängig machen.', knopf: 'Löschen' }),
|
||||
};
|
||||
|
||||
function menuFokusziele(){
|
||||
return offenesMenu ? Array.from(offenesMenu.el.querySelectorAll('button:not([aria-disabled="true"])')) : [];
|
||||
}
|
||||
|
||||
function menuPositionieren(){
|
||||
if(!offenesMenu) return;
|
||||
const { el, knopf } = offenesMenu;
|
||||
const r = knopf.getBoundingClientRect();
|
||||
const m = el.getBoundingClientRect();
|
||||
const nachOben = (window.innerHeight - r.bottom) < m.height + 12 && r.top > m.height + 12;
|
||||
el.classList.toggle('nach-oben', nachOben);
|
||||
el.style.top = (nachOben ? r.top - m.height - 6 : r.bottom + 6) + 'px';
|
||||
el.style.left = Math.max(8, Math.min(r.right - m.width, window.innerWidth - m.width - 8)) + 'px';
|
||||
}
|
||||
|
||||
function schliesseUserMenu(fokusZurueck){
|
||||
if(!offenesMenu) return;
|
||||
const { el, knopf, zeile } = offenesMenu;
|
||||
offenesMenu = null;
|
||||
el.remove();
|
||||
knopf.setAttribute('aria-expanded', 'false');
|
||||
if(zeile) zeile.classList.remove('is-menu-open');
|
||||
if(fokusZurueck && document.contains(knopf)) knopf.focus();
|
||||
}
|
||||
|
||||
function zeigeMenuEintraege(){
|
||||
offenesMenu.el.innerHTML = menuInhalt(offenesMenu.u);
|
||||
menuPositionieren();
|
||||
}
|
||||
|
||||
function fuehreAus(aktion, u){
|
||||
schliesseUserMenu(true);
|
||||
if(aktion === 'passwort') resetUserPassword(u.id, u.username, true);
|
||||
else if(aktion === 'deaktivieren') toggleUserActive(u.id, true, true);
|
||||
else if(aktion === 'aktivieren') toggleUserActive(u.id, false, true);
|
||||
else if(aktion === 'loeschen') hardDeleteUser(u.id, u.username, true);
|
||||
}
|
||||
|
||||
// openUserActionMenu: öffnet (bzw. schließt bei erneutem Klick) das Menü
|
||||
// einer Zeile. Per Tastatur geöffnet (event.detail === 0) landet der Fokus
|
||||
// gleich auf dem ersten Eintrag.
|
||||
window.openUserActionMenu = function(event, id){
|
||||
event.stopPropagation();
|
||||
const knopf = event.currentTarget;
|
||||
if(offenesMenu && offenesMenu.knopf === knopf){ schliesseUserMenu(true); return; }
|
||||
schliesseUserMenu(false);
|
||||
const u = usersCache.find(x => x.id === id);
|
||||
if(!u) return;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'user-menu';
|
||||
el.setAttribute('role', 'menu');
|
||||
document.body.appendChild(el);
|
||||
const zeile = knopf.closest('tr');
|
||||
offenesMenu = { el, knopf, zeile, u };
|
||||
knopf.setAttribute('aria-expanded', 'true');
|
||||
if(zeile) zeile.classList.add('is-menu-open');
|
||||
zeigeMenuEintraege();
|
||||
|
||||
el.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const b = e.target.closest('button');
|
||||
if(!b || b.getAttribute('aria-disabled') === 'true') return;
|
||||
if(b.dataset.zurueck){ zeigeMenuEintraege(); const z = menuFokusziele(); if(z[0]) z[0].focus(); return; }
|
||||
if(b.dataset.ausfuehren){ fuehreAus(b.dataset.ausfuehren, u); return; }
|
||||
const aktion = b.dataset.aktion;
|
||||
if(RUECKFRAGEN[aktion]){
|
||||
const r = RUECKFRAGEN[aktion](u);
|
||||
el.innerHTML = `<div class="user-menu-confirm"><p>${r.frage}</p><div class="user-menu-confirm-row">
|
||||
<button type="button" class="secondary small" data-zurueck="1">Abbrechen</button>
|
||||
<button type="button" class="danger small" data-ausfuehren="${aktion}">${esc(r.knopf)}</button>
|
||||
</div></div>`;
|
||||
menuPositionieren();
|
||||
el.querySelector('[data-ausfuehren]').focus();
|
||||
return;
|
||||
}
|
||||
fuehreAus(aktion, u);
|
||||
});
|
||||
|
||||
if(event.detail === 0){ const z = menuFokusziele(); if(z[0]) z[0].focus(); }
|
||||
};
|
||||
|
||||
document.addEventListener('click', () => schliesseUserMenu(false));
|
||||
document.addEventListener('keydown', e => {
|
||||
if(!offenesMenu) return;
|
||||
const ziele = menuFokusziele();
|
||||
const i = ziele.indexOf(document.activeElement);
|
||||
if(e.key === 'Escape'){ e.preventDefault(); schliesseUserMenu(true); }
|
||||
else if(e.key === 'ArrowDown'){ e.preventDefault(); if(ziele.length) ziele[(i + 1) % ziele.length].focus(); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); if(ziele.length) ziele[(i - 1 + ziele.length) % ziele.length].focus(); }
|
||||
else if(e.key === 'Tab'){ schliesseUserMenu(false); }
|
||||
});
|
||||
window.addEventListener('resize', () => schliesseUserMenu(false));
|
||||
window.addEventListener('scroll', () => schliesseUserMenu(false), true);
|
||||
|
||||
// onEditUserNameInput: Im Bearbeiten-Dialog wird eine vorhandene Adresse
|
||||
// nie überschrieben; ist das E-Mail-Feld leer, steht der Vorschlag als
|
||||
// Platzhalter darin und folgt Vor-/Nachname.
|
||||
window.onEditUserNameInput = function(){
|
||||
const feld = document.getElementById('editUserEmail');
|
||||
if(!feld) return;
|
||||
feld.placeholder = emailVorschlag(document.getElementById('editUserFirstName').value,
|
||||
document.getElementById('editUserLastName').value) || 'v.nachname@' + EMAIL_DOMAIN;
|
||||
};
|
||||
|
||||
// personIdManuallyEdited wird gesetzt, sobald der Nutzer das Person-ID-Feld
|
||||
// selbst bearbeitet (nach dem Laden des DOM gebunden).
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
@@ -491,7 +704,15 @@
|
||||
}
|
||||
const initialsInput = document.getElementById('newUserInitials');
|
||||
if(initialsInput){
|
||||
initialsInput.addEventListener('input', function(){ initialsManuallyEdited = true; });
|
||||
initialsInput.addEventListener('input', function(){ initialsManuallyEdited = true; personIdAusKuerzel(); });
|
||||
}
|
||||
const emailInput = document.getElementById('newUserEmail');
|
||||
if(emailInput){
|
||||
emailInput.addEventListener('input', function(){ emailManuallyEdited = true; benutzernameAusEmail(); });
|
||||
}
|
||||
const usernameInput = document.getElementById('newUserUsername');
|
||||
if(usernameInput){
|
||||
usernameInput.addEventListener('input', function(){ usernameManuallyEdited = true; });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ const ADMIN_SECTIONS = [
|
||||
{ key: 'vorlagen', title: 'Aufgaben-Vorlagen' },
|
||||
{ key: 'planchecker', title: 'Plan-Checker-Merkmale' },
|
||||
{ key: 'betrieb', title: 'Betrieb & Sicherheit' },
|
||||
{ key: 'papierkorb', title: 'Papierkorb' },
|
||||
{ key: 'datenbank', title: 'Datenbank & Sitzungen' },
|
||||
];
|
||||
|
||||
@@ -136,6 +137,9 @@ function showAdminSection(key){
|
||||
// aendert sich auch ausserhalb dieses Browsers, und die uebrigen Masken
|
||||
// rechnen mit ihr (Wochenstunden -> Auslastung).
|
||||
if(key === 'benutzer' && typeof renderAdminUsers === 'function') renderAdminUsers();
|
||||
// Der Papierkorb ändert sich durch jedes Löschen irgendwo im Büro — ebenso
|
||||
// frisch holen.
|
||||
if(key === 'papierkorb') loadAdminPapierkorb();
|
||||
|
||||
try {
|
||||
history.replaceState(null, '', location.pathname + location.search + '#' + key);
|
||||
@@ -931,3 +935,73 @@ function loadHolidaysIntoState(){
|
||||
})
|
||||
.catch(err => console.error('Feiertage neu laden fehlgeschlagen:', err));
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Papierkorb gelöschter Projekte
|
||||
// -----------------------------
|
||||
// Ein aus der Projektliste gelöschtes Projekt legt der Server beim Speichern
|
||||
// der Sitzung als Schnappschuss in deleted_projects ab (server.js,
|
||||
// /save-week). Hier lässt es sich 30 Tage lang in die aktuelle Sitzung
|
||||
// zurückholen oder vorzeitig endgültig entfernen.
|
||||
|
||||
// loadAdminPapierkorb: holt die Liste (GET /admin/deleted-projects) und
|
||||
// rendert sie in #adminPapierkorbList.
|
||||
async function loadAdminPapierkorb(){
|
||||
const el = document.getElementById('adminPapierkorbList');
|
||||
if(!el) return;
|
||||
try {
|
||||
const r = await fetch(baseUrl() + '/admin/deleted-projects');
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if(!r.ok || !data.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
||||
if(!data.projekte.length){
|
||||
el.innerHTML = '<p class="hint">Der Papierkorb ist leer.</p>';
|
||||
return;
|
||||
}
|
||||
const tagMs = 24 * 60 * 60 * 1000;
|
||||
el.innerHTML = data.projekte.map(p => {
|
||||
const bez = ((p.nr ? p.nr + ' ' : '') + (p.name || '')).trim() || '(ohne Bezeichnung)';
|
||||
const am = new Date(p.deletedAt).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' });
|
||||
const rest = Math.max(0, Math.ceil((Date.parse(p.endgueltigAm) - Date.now()) / tagMs));
|
||||
return `<div class="template-admin-row">
|
||||
<span class="template-admin-title">${esc(bez)}
|
||||
<span class="hint" style="margin:0"> · gelöscht ${esc(am)}${p.deletedBy ? ' von ' + esc(p.deletedBy) : ''} · aus ${esc(p.weekLabel)}</span>
|
||||
</span>
|
||||
<span class="sitzung-chip">noch ${rest} ${rest === 1 ? 'Tag' : 'Tage'}</span>
|
||||
<button class="secondary small" onclick="adminProjektWiederherstellen('${esc(p.id)}')">Wiederherstellen</button>
|
||||
<button class="danger small" onclick="adminProjektEndgueltigLoeschen('${esc(p.id)}')" title="Sofort endgültig löschen">×</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch(err){
|
||||
el.innerHTML = '<p class="hint">Papierkorb konnte nicht geladen werden: ' + esc(err.message) + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// adminProjektWiederherstellen: holt ein Projekt in die aktuelle Sitzung
|
||||
// zurück. Danach wird neu geladen, damit die Projektliste das Projekt zeigt
|
||||
// und die lokale Revision der Sitzung zum Server passt (sonst meldete das
|
||||
// nächste Speichern einen Konflikt).
|
||||
async function adminProjektWiederherstellen(id){
|
||||
try {
|
||||
const r = await fetch(baseUrl() + '/admin/deleted-projects/' + encodeURIComponent(id) + '/restore', { method: 'POST' });
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if(!r.ok || !data.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
||||
load();
|
||||
loadAdminPapierkorb();
|
||||
alert('Projekt wiederhergestellt in „' + data.weekLabel + '".');
|
||||
} catch(err){
|
||||
alert('Wiederherstellen fehlgeschlagen: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// adminProjektEndgueltigLoeschen: entfernt einen Papierkorb-Eintrag sofort.
|
||||
async function adminProjektEndgueltigLoeschen(id){
|
||||
if(!confirm('Projekt endgültig löschen? Das lässt sich nicht rückgängig machen.')) return;
|
||||
try {
|
||||
const r = await fetch(baseUrl() + '/admin/deleted-projects/' + encodeURIComponent(id), { method: 'DELETE' });
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if(!r.ok || !data.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
||||
loadAdminPapierkorb();
|
||||
} catch(err){
|
||||
alert('Löschen fehlgeschlagen: ' + err.message);
|
||||
}
|
||||
}
|
||||
+100
-7
@@ -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');
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -157,7 +157,7 @@ function renderBoardRow(p, allPeople, locked, weekLabel, opts){
|
||||
// einen Ort haben — hier steht nur, was die LISTE betrifft.
|
||||
html += `<div class="board-row-actions" onclick="event.stopPropagation()">
|
||||
<button type="button" class="secondary small" onclick="openProjectPlan('${p.id}')" title="Projektbereich öffnen">Öffnen</button>
|
||||
${locked ? '' : `<button type="button" class="danger small" onclick="delProjectFromList('${p.id}')" title="Projekt aus dieser Sitzung löschen">Löschen</button>`}
|
||||
${locked || !darfProjektlisteAendern() ? '' : `<button type="button" class="danger small" onclick="delProjectFromList('${p.id}')" title="Projekt löschen (30 Tage durch Administratoren wiederherstellbar)">Löschen</button>`}
|
||||
</div>`;
|
||||
html += `</div>`; // board-row-main
|
||||
|
||||
|
||||
@@ -25,10 +25,20 @@
|
||||
|
||||
// delProjectFromList: löscht ein Projekt (per Bestätigungsdialog) in der
|
||||
// Listen-Ansicht. Parameter `id`: Projekt-ID. Keine Rückgabe; bei gesperrter
|
||||
// Sitzung abgebrochen.
|
||||
// Sitzung abgebrochen. Nur für Projektleiter und Administratoren
|
||||
// (darfProjektlisteAendern(), js/auth.js; serverseitig in /save-week
|
||||
// durchgesetzt). Der Server legt das Projekt beim Speichern in den
|
||||
// Papierkorb, aus dem es ein Administrator 30 Tage lang zurückholen kann
|
||||
// (Admin → Papierkorb, js/admin/admin.js).
|
||||
function delProjectFromList(id){
|
||||
if(guardWeekLocked()) return;
|
||||
if(!confirm("Projekt aus dieser Sitzung löschen?")) return;
|
||||
if(!darfProjektlisteAendern()){
|
||||
alert('Projekte löschen dürfen nur Projektleiter und Administratoren.');
|
||||
return;
|
||||
}
|
||||
const p = currentWeek().projects.find(x => x.id === id);
|
||||
const bez = p ? ((p.nr ? p.nr + ' ' : '') + (p.name || '')).trim() : '';
|
||||
if(!confirm('Projekt ' + (bez ? '„' + bez + '" ' : '') + 'löschen?\n\nEin Administrator kann es 30 Tage lang wiederherstellen.')) return;
|
||||
const w = currentWeek();
|
||||
w.projects = w.projects.filter(p=>p.id!==id);
|
||||
save(); renderProjects(); renderBoard();
|
||||
@@ -160,7 +170,7 @@ function renderProjects(){
|
||||
</div>
|
||||
<div class="project-card-footer">
|
||||
<button class="secondary small" onclick="openLpModal('${p.id}')" ${locked ? 'disabled' : ''}>LP-Aufwand</button>
|
||||
<button class="danger small" onclick="delProjectFromList('${p.id}')" ${locked ? 'disabled' : ''}>Löschen</button>
|
||||
<button class="danger small" onclick="delProjectFromList('${p.id}')" ${locked || !darfProjektlisteAendern() ? 'disabled' : ''}>Löschen</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
@@ -51,9 +51,15 @@ const GEWERK_LABELS = { ELT: 'Elektrotechnik', HLS: 'Heizung · Lüftung · Sani
|
||||
let wizard = null;
|
||||
|
||||
// openNewProjWizard: öffnet den Assistenten mit leerem Zustand.
|
||||
// Keine Parameter, keine Rückgabe; gesperrte Sitzungen werden abgewiesen.
|
||||
// Keine Parameter, keine Rückgabe; gesperrte Sitzungen werden abgewiesen,
|
||||
// ebenso Konten ohne Rolle Projektleiter/Administrator (die Knöpfe sind für
|
||||
// sie ohnehin ausgeblendet; /save-week weist das Anlegen mit 403 ab).
|
||||
function openNewProjWizard(){
|
||||
if(guardWeekLocked()) return;
|
||||
if(!darfProjektlisteAendern()){
|
||||
alert('Projekte anlegen dürfen nur Projektleiter und Administratoren.');
|
||||
return;
|
||||
}
|
||||
wizard = {
|
||||
schritt: 0,
|
||||
fertig: null, // angelegtes Projekt, solange der Abschluss steht
|
||||
|
||||
@@ -47,7 +47,7 @@ function renderSettingsAccount(){
|
||||
+ `<div class="settings-kv-row">
|
||||
<span class="settings-kv-key">E-Mail</span>
|
||||
<span class="settings-email-edit">
|
||||
<input type="email" id="settingsEmailInput" value="${esc(currentUser.email || '')}" placeholder="name@firma.de">
|
||||
<input type="email" id="settingsEmailInput" value="${esc(currentUser.email || '')}" placeholder="${esc(emailVorschlag(currentUser.displayName) || 'v.nachname@' + EMAIL_DOMAIN)}">
|
||||
<button class="secondary" onclick="submitEmailChange()">Speichern</button>
|
||||
<span id="settingsEmailError" class="settings-warn" hidden></span>
|
||||
</span>
|
||||
|
||||
@@ -104,6 +104,50 @@ function initialsForName(name){
|
||||
return words.slice(0, 4).map(w => w[0]).join('').toUpperCase();
|
||||
}
|
||||
|
||||
// EMAIL_DOMAIN: Domain der Firmen-Adressen, Grundlage von emailVorschlag().
|
||||
const EMAIL_DOMAIN = 'rauschenberg-ing.de';
|
||||
|
||||
// emailVorschlag: schlägt aus einem Namen die Firmen-Adresse nach dem Schema
|
||||
// „Anfangsbuchstabe Vorname . Nachname @rauschenberg-ing.de" vor, klein
|
||||
// geschrieben, Umlaute umschrieben (ä→ae, ß→ss), übrige Sonderzeichen
|
||||
// entfernt: "Miriam Körner" → "m.koerner@rauschenberg-ing.de". Vorname ist
|
||||
// das erste, Nachname das letzte Wort. Nur ein Vorschlag — bleibt
|
||||
// überschreibbar. Parameter: name (String) — oder, wenn `nachname` gegeben
|
||||
// ist, Vorname und Nachname getrennt (dann zählt der ganze Nachname, z. B.
|
||||
// "von Plum" → "vonplum"). Rückgabe: Adresse oder ''.
|
||||
function emailVorschlag(name, nachname){
|
||||
if(nachname !== undefined){
|
||||
const v = String(name || '').trim(), n = String(nachname || '').trim();
|
||||
if(!v || !n) return emailVorschlag((v + ' ' + n).trim());
|
||||
const l = emailVorschlag(v.split(/\s+/)[0] + ' ' + n.replace(/\s+/g, ''));
|
||||
return l;
|
||||
}
|
||||
const teil = s => String(s).toLowerCase()
|
||||
.replace(/ä/g,'ae').replace(/ö/g,'oe').replace(/ü/g,'ue').replace(/ß/g,'ss')
|
||||
.normalize('NFD').replace(/[\u0300-\u036f]/g,'')
|
||||
.replace(/[^a-z0-9-]/g,'');
|
||||
const woerter = String(name || '').trim().split(/\s+/).map(teil).filter(Boolean);
|
||||
if(woerter.length === 0) return '';
|
||||
const lokal = woerter.length === 1 ? woerter[0] : woerter[0][0] + '.' + woerter[woerter.length - 1];
|
||||
return lokal + '@' + EMAIL_DOMAIN;
|
||||
}
|
||||
|
||||
// benutzernameVorschlag: Benutzername eines neuen Kontos = Anfang der
|
||||
// E-Mail-Adresse, also "Anfangsbuchstabe Vorname . Nachname" in der
|
||||
// Schreibweise von emailVorschlag() ("Miriam Körner" → "m.koerner").
|
||||
// Angemeldet wird mit genau diesem Namen. Rückgabe: String oder ''.
|
||||
function benutzernameVorschlag(vorname, nachname){
|
||||
const adresse = emailVorschlag(vorname, nachname);
|
||||
return adresse ? adresse.slice(0, adresse.indexOf('@')) : '';
|
||||
}
|
||||
|
||||
// grossAnfang: erster Buchstabe groß, Leerraum zusammengefasst — für Vor-
|
||||
// und Nachnamen beim Speichern ("miriam" → "Miriam").
|
||||
function grossAnfang(s){
|
||||
s = String(s || '').trim().replace(/\s+/g, ' ');
|
||||
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
|
||||
}
|
||||
|
||||
// formatWeekDate: wandelt ein ISO-Datum (YYYY-MM-DD) in deutsches Format DD.MM.YYYY um
|
||||
// Parameter: dateString (String). Rückgabe: formatierter String oder leer.
|
||||
function formatWeekDate(dateString){
|
||||
|
||||
Reference in new issue
Block a user