Neue Wandtafel (Touch-Dashboard für Kiosk-Anzeige): Auslastung/Besprochen je Gewerk, Aufgaben-Kennzahlen, Ereignis-Feed nach Art gruppiert, vollständige Projektliste zum Scrollen, automatischer Rücksprung nach Untätigkeit mit sichtbarem Countdown. Eigene Kiosk-Rolle ohne Fachrolle und ohne Schreibrechte. Auslastung: Möglichkeit zum Wechsel der ausgewerteten Sitzung entfernt. Aufräumen nicht mehr benötigter Dateien, README auf aktuellen Stand gebracht. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
155 lines
7.9 KiB
JavaScript
155 lines
7.9 KiB
JavaScript
// -----------------------------
|
||
// Projektübersicht — Personen-Ansicht (global, NUR LESEND)
|
||
// -----------------------------
|
||
// Diese Datei rendert den Personen-Tab (Tab "Projektbeteiligte" in `App.html`)
|
||
// als reine Anzeige. Seit der Ablösung von people/person_roles/person_absences
|
||
// (server.js) werden Projektbeteiligte ausschließlich in der zentralen
|
||
// Benutzerverwaltung (Bereich „Admin“) gepflegt; Anlegen/Bearbeiten/Löschen/
|
||
// Drag-and-Drop-Sortierung/Abwesenheits-Formulare gibt es hier bewusst nicht
|
||
// mehr. EINE Ausnahme ist geblieben bzw. wieder dazugekommen: der
|
||
// Teilnehmerkreis der aktuellen Sitzung (`currentWeek().people`). Er entsteht
|
||
// nur beim Anlegen einer neuen Sitzung und wird beim Duplizieren
|
||
// unverändert übernommen — ohne die Schaltflächen hier ("Zur Sitzung
|
||
// hinzufügen"/"Aus Sitzung entfernen") wäre ein neu angelegter Benutzer in
|
||
// keiner laufenden Sitzung zuordenbar, weder im Board noch in der
|
||
// Auslastung noch in der Anwesenheit. Die Personendaten selbst kommen unverändert über `db.globalPeople`
|
||
// (befüllt via `GET /load-all`, serverseitig jetzt aus `users`/`user_roles`/
|
||
// `user_absences` zusammengesetzt, firmenweit ohne Abteilungstrennung). Sie
|
||
// wird von `App.html` nach `js/board/board.js` (vor `js/projects/projects.js`)
|
||
// geladen und stellt die globale Funktion `renderPeople()` bereit, die u.a.
|
||
// von `js/tabs-auth.js` (`showTab()`) und `js/init.js` aufgerufen wird.
|
||
|
||
// renderPeople: baut die schreibgeschützte Liste (`.people-readonly`, eine
|
||
// Bubble-Card mit den Personen als Listenzeilen) der Personen für den
|
||
// Personen-Tab auf (Zeilen mit Kürzel, Vollname, E-Mail, Rollen,
|
||
// aufklappbaren Abwesenheiten). Kein Formular, keine Lösch-/
|
||
// Bearbeiten-Aktionen, kein Drag-Handle. Zeigt zusätzlich einen Hinweis auf
|
||
// die zentrale Benutzerverwaltung (Link nur für Admins sichtbar). Keine
|
||
// Parameter, keine Rückgabe.
|
||
function renderPeople(){
|
||
const people = db.globalPeople || [];
|
||
const isAdmin = typeof currentUser !== 'undefined' && !!currentUser && currentUser.role === 'admin';
|
||
const w = currentWeek();
|
||
const inWeek = new Set((w && Array.isArray(w.people) ? w.people : []).map(p => p.id));
|
||
const locked = !w || isWeekLocked(w);
|
||
|
||
let html = `<div class="hint" style="margin-bottom:12px">Projektbeteiligte werden zentral in der Benutzerverwaltung gepflegt.`
|
||
+ (isAdmin ? ` <a href="/Admin#benutzer" onclick="showAdminSection('benutzer');return false;">Zur Benutzerverwaltung</a>` : '')
|
||
+ `</div>`;
|
||
html += `<div class="hint" style="margin-bottom:12px">Wer in der aktuellen Sitzung`
|
||
+ (w ? ` <b>${esc(w.label)}</b>` : '')
|
||
+ ` mitwirkt, erscheint als Spalte im Projektboard, in der Auslastung und in der Anwesenheit.`
|
||
+ (locked ? ` Die Sitzung ist gesperrt — der Kreis kann nicht geändert werden.` : '')
|
||
+ `</div>`;
|
||
|
||
if(people.length === 0){
|
||
html += '<div class="hint">Keine Personen vorhanden.</div>';
|
||
document.getElementById('peopleList').innerHTML = html;
|
||
return;
|
||
}
|
||
|
||
html += `<div class="people-readonly">`;
|
||
people.forEach(p => {
|
||
const isOpen = personAbsenceOpen[p.id];
|
||
const roles = Array.isArray(p.role) ? p.role : [];
|
||
const dabei = inWeek.has(p.id);
|
||
// Die Schaltfläche liegt IM Kopfbereich, der das Aufklappen auslöst —
|
||
// stopPropagation verhindert, dass ein Klick darauf zusätzlich die
|
||
// Abwesenheiten auf- oder zuklappt.
|
||
const weekAction = locked ? '' : (dabei
|
||
? `<button type="button" class="secondary small" onclick="event.stopPropagation(); removePersonFromCurrentWeek('${esc(p.id)}')">Aus Sitzung entfernen</button>`
|
||
: `<button type="button" class="small" onclick="event.stopPropagation(); addPersonToCurrentWeek('${esc(p.id)}')">Zur Sitzung hinzufügen</button>`);
|
||
html += `<div class="person-card${isOpen ? ' open' : ''}" data-person-id="${p.id}">`;
|
||
html += `<div class="person-header" onclick="togglePersonAbsences('${esc(p.id)}')">
|
||
<span class="person-toggle">${icon('chevronRight')}</span>
|
||
<strong>${esc(p.name)}</strong>
|
||
<span>${esc(p.fullName || '')}</span>
|
||
<span class="hint" style="margin:0">${esc(p.email || '')}</span>
|
||
<span>${roles.length ? roles.map(r => esc(r)).join(', ') : '<span class="hint" style="margin:0">Keine Rolle</span>'}</span>
|
||
<span class="person-week-state">${dabei ? 'in dieser Sitzung' : '<span class="hint" style="margin:0">nicht in dieser Sitzung</span>'}</span>
|
||
${weekAction}
|
||
</div>`;
|
||
|
||
html += `<div class="person-absences" style="display:${isOpen ? 'block' : 'none'}">`;
|
||
const absences = Array.isArray(p.absences) ? p.absences : [];
|
||
if(absences.length > 0){
|
||
html += `<div class="absence-list">`;
|
||
absences.forEach(a => {
|
||
html += `<div class="absence-chip">
|
||
<span>${esc(a.type || 'Abwesenheit')}: ${formatDateDE(parseDateISO(a.start))}–${formatDateDE(parseDateISO(a.end))}</span>
|
||
${a.note ? `<small>${esc(a.note)}</small>` : ''}
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
} else {
|
||
html += `<div class="hint">Keine Abwesenheiten hinterlegt.</div>`;
|
||
}
|
||
html += `</div></div>`;
|
||
});
|
||
html += `</div>`;
|
||
document.getElementById('peopleList').innerHTML = html;
|
||
}
|
||
|
||
// personAbsenceOpen: UI-Zustand, welche Personen-Karte ihre Abwesenheiten
|
||
// aufgeklappt darstellt (Key = Personen-ID, Wert = boolean). Reine Anzeige-
|
||
// Steuerung, wird nicht persistiert.
|
||
let personAbsenceOpen = {};
|
||
|
||
// togglePersonAbsences: klappt die Abwesenheits-Liste einer Person auf bzw. zu
|
||
// (Umschalten des Eintrags in `personAbsenceOpen`) und rendert neu.
|
||
function togglePersonAbsences(personId){
|
||
personAbsenceOpen[personId] = !personAbsenceOpen[personId];
|
||
renderPeople();
|
||
}
|
||
|
||
// addPersonToCurrentWeek: nimmt eine Person aus der globalen Liste in den
|
||
// Teilnehmerkreis der aktuellen Sitzung auf. Nötig, weil der Kreis sonst nur
|
||
// beim Anlegen einer neuen Sitzung entsteht: Ein später angelegter Benutzer
|
||
// tauchte in laufenden (und in allen daraus duplizierten) Sitzungen nirgends
|
||
// auf. Geschrieben wird über den normalen save()-Weg — `week.people` ist Teil
|
||
// des Wochen-Datensatzes, den /save-week ohnehin schreibt.
|
||
// Parameter: personId (String). Keine Rückgabe.
|
||
function addPersonToCurrentWeek(personId){
|
||
if(guardWeekLocked()) return;
|
||
const w = currentWeek();
|
||
if(!w) return;
|
||
if(!Array.isArray(w.people)) w.people = [];
|
||
if(w.people.some(p => p.id === personId)) return;
|
||
const person = (db.globalPeople || []).find(p => p.id === personId);
|
||
if(!person){ alert('Diese Person ist nicht mehr in der Benutzerverwaltung vorhanden.'); return; }
|
||
// Tiefe Kopie: Der Teilnehmerkreis friert Name und Rollen zum Zeitpunkt der
|
||
// Sitzung ein (genauso wie newWeek() es tut) und darf sich später nicht
|
||
// rückwirkend mit der globalen Liste ändern.
|
||
w.people.push(JSON.parse(JSON.stringify(person)));
|
||
save();
|
||
renderAll();
|
||
}
|
||
|
||
// removePersonFromCurrentWeek: nimmt eine Person wieder aus dem
|
||
// Teilnehmerkreis der aktuellen Sitzung heraus. Verweigert wird das, solange
|
||
// sie noch einem Projekt dieser Sitzung zugeordnet ist — sonst bliebe eine
|
||
// Zuordnung ohne zugehörige Spalte zurück. Parameter: personId (String).
|
||
function removePersonFromCurrentWeek(personId){
|
||
if(guardWeekLocked()) return;
|
||
const w = currentWeek();
|
||
if(!w || !Array.isArray(w.people)) return;
|
||
const person = w.people.find(p => p.id === personId);
|
||
if(!person) return;
|
||
|
||
const zugeordnet = (w.projects || []).filter(proj => {
|
||
const assign = proj.assign || {};
|
||
return Object.keys(assign).some(key => key === personId || key.indexOf(personId + '|') === 0);
|
||
});
|
||
if(zugeordnet.length > 0){
|
||
alert('"' + (person.fullName || person.name) + '" ist in dieser Sitzung noch ' + zugeordnet.length
|
||
+ ' Projekt(en) zugeordnet:\n\n' + zugeordnet.map(p => '· ' + (p.name || p.nr)).join('\n')
|
||
+ '\n\nBitte zuerst die Zuordnungen im Projektboard entfernen.');
|
||
return;
|
||
}
|
||
|
||
w.people = w.people.filter(p => p.id !== personId);
|
||
if(w.attendance) delete w.attendance[personId];
|
||
save();
|
||
renderAll();
|
||
}
|