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

+254 -33
View File
@@ -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; });
}
});