Files
erikandClaude Opus 5.5 b7e0bd0d92 Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb
- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan
  oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues
  Notizfeld je Aufgabe (plan_todos.notiz)
- Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile,
  "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen
- Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang
- Glas-Darstellung, Logo als Favicon
- Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und
  Prüfskript inkl. WLAN-Wächter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:16:52 +01:00

155 lines
7.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -----------------------------
// 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 = hatRecht('a.benutzer'); // Teilnehmerkreis pflegen: wie Benutzerverwaltung
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();
}