Kleine fehler beseitigt

This commit is contained in:
erik committed 2026-08-14 11:47:19 +02:00
1 parent 57e201b8fc
commit 115f0f5671
15 files changed
+220 -115

No files matched your search

+2 -27
View File
@@ -52,33 +52,8 @@ function formatStatus(command, value){
document.execCommand(command, false, value || null);
}
// peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt.
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle
// Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung.
function peopleByRole(target){
const m = {}; ROLES.forEach(r=>m[r]=[]);
const people = [];
if(Array.isArray(target)){
target.forEach(w => { if(w && Array.isArray(w.people)) people.push(...w.people); });
} else if(target && Array.isArray(target.people)){
people.push(...target.people);
} else if(Array.isArray(currentWeek()?.people)){
people.push(...currentWeek().people);
}
const seen = new Set();
people.forEach(p=>{
const personRoles = Array.isArray(p.role) ? p.role : [p.role];
personRoles.forEach(role=>{
if(!m[role]) m[role]=[];
const key = `${p.id}|${role}`;
if(!seen.has(key)){
seen.add(key);
m[role].push({person:p, role});
}
});
});
return m;
}
// peopleByRole liegt jetzt im gemeinsamen Modul js/utils.js (benötigt auch
// csv.js für die Projektplan-Seite, die board.js nicht lädt).
// roleClass: erzeugt CSS-Klassen für eine Rolle bzw. ein Rollen-Array
// (Rollenname kleingeschrieben, Leerzeichen → Bindestrich). Parameter: `role`
+4 -10
View File
@@ -112,10 +112,10 @@ function parseCsvText(text){
return text.replace(/^[\ufeff\u00EF\u00BB\u00BF]+/,'').split(/\r?\n/).filter(line => line.trim() !== '').map(parseCsvLine);
}
// Toleranter CSV-Zeilenparser. Versteht sowohl sauberes CSV als auch
// das ältere Format, bei dem Felder nur ein öffnendes Anführungszeichen
// hatten und das Semikolon innerhalb eines quoted Feldes gleichzeitig
// als Trennzeichen diente.
// Toleranter CSV-Zeilenparser. Versteht sauber gequotetes CSV (wie es
// csvEscape/weekToCsv erzeugen). Semikolons innerhalb eines quoted Feldes sind
// Feldinhalt und kein Trennzeichen – so bleibt weekToCsv()/parseSessionCsv()
// auch bei Status-Texten, die mit ";" enden, invers.
// parseCsvLine: zerlegt eine einzelne CSV-Zeile in ein Feld-Array
// Parameter: line (String). Rückgabe: Array der Felder.
function parseCsvLine(line){
@@ -130,12 +130,6 @@ function parseCsvLine(line){
if(inQuotes){
if(c === '"' && n === '"'){
current += '"'; i += 2;
} else if(c === ';' && n === '"'){
// Altes Trennzeichen \";\\" innerhalb eines quoted Feldes
result.push(current);
current = '';
inQuotes = false;
i += 1;
} else if(c === '"' && (n === ';' || n === undefined)){
result.push(current);
current = '';
+1 -1
View File
@@ -6,7 +6,7 @@
// people → projects → load → gantt → …). Es stellt renderLoad(), das LP-
// Aufwand-Modal und das Neues-Projekt-Modal bereit. Genutzt werden LPHS/ROLES
// aus js/globals.js, currentWeek()/save() aus js/store.js, guardWeekLocked()
// aus js/weeks.js sowie peopleByRole()/assignKey() aus js/board/board.js/js/utils.js.
// aus js/weeks.js sowie peopleByRole()/assignKey() aus js/utils.js.
// Die Auslastungs-Logik (berechneAuslastung, lpActiveList, faktorFormat,
// pdfLoadAmpelColor, LOAD_BAR_MAX) wird zusätzlich von js/pdf.js für den
// PDF-Export wiederverwendet.
+24 -10
View File
@@ -8,7 +8,7 @@
// vor js/init.js); exportPDF() hängt am Export-Button von Test.html und nutzt
// die per CDN eingebundene Bibliothek pdfmake (offline nicht verfügbar). Es
// liest die aktuelle Woche über currentWeek() aus js/store.js, Personen über
// peopleByRole() aus js/board/board.js, Helfer wie assignKey()/sortGroups()/
// peopleByRole() aus js/utils.js, Helfer wie assignKey()/sortGroups()/
// formatWeekDate() aus js/utils.js sowie die Auslastungs-Berechnung
// (berechneAuslastung, lpActiveList, faktorFormat, pdfLoadAmpelColor,
// LOAD_BAR_MAX) aus js/load/load.js. Das Modul stellt ausschließlich exportPDF()
@@ -272,6 +272,12 @@ function pdfProjectRow(p, allPeople, alt){
];
allPeople.forEach(entry => {
// Platzhalter-Einträge für leere Rollen (person === null) → leere Zelle,
// damit Spaltenzahl mit der Kopfzeile übereinstimmt.
if(!entry.person){
row.push({ text: '', style: 'tdCenter', fillColor: base || '#FFFFFF' });
return;
}
const s = (p.assign || {})[assignKey(entry.person.id, entry.role)] || 0;
const txt = s === 1 ? 'X' : s === 2 ? '(X)' : '';
const bg = s === 1 ? PDF_ASSIGN_MAIN
@@ -468,17 +474,25 @@ async function exportPDF(){
try {
const grouped = peopleByRole(w);
const allPeople = [];
ROLES.forEach(r => (grouped[r] || []).forEach(e => {
// role explizit setzen, falls peopleByRole sie nicht mitliefert
allPeople.push({ person: e.person, role: e.role || r });
}));
// displayPeople: Spalten der Tabelle inkl. Platzhalter für leere Rollen –
// pdfHeaderRows erzwingt pro Rolle mindestens eine Spalte (colSpan/“–”),
// deshalb müssen Zeilen, Breiten und colCount dieselbe Spaltenzahl nutzen,
// sonst wirft pdfmake bei einer leeren Rolle einen Layout-Fehler.
const displayPeople = [];
ROLES.forEach(r => {
const list = grouped[r] || [];
if(list.length === 0){
displayPeople.push({ person: null, role: r });
} else {
list.forEach(e => displayPeople.push({ person: e.person, role: e.role || r }));
}
});
pdfCheckWidth(allPeople);
pdfCheckWidth(displayPeople);
const logo = await loadLogoPng();
const colCount = 3 + allPeople.length + LPHS.length;
const widths = pdfColumnWidths(allPeople);
const colCount = 3 + displayPeople.length + LPHS.length;
const widths = pdfColumnWidths(displayPeople);
// --- Deckblatt ---
const cover = [];
@@ -521,7 +535,7 @@ async function exportPDF(){
const header = pdfHeaderRows(grouped);
const body = [ pdfGroupTitleRow(groupName, colCount), header[0], header[1] ];
projects.forEach((p, i) => body.push(pdfProjectRow(p, allPeople, i % 2 === 0)));
projects.forEach((p, i) => body.push(pdfProjectRow(p, displayPeople, i % 2 === 0)));
content.push({
table: { headerRows: 3, widths: widths, body: body, dontBreakRows: true },
+29 -11
View File
@@ -54,12 +54,9 @@ function addPerson(){
function delPerson(id){
if(!confirm("Person global löschen? Sie bleibt in bereits existierenden Sitzungen sichtbar, wird aber in neuen Sitzungen nicht mehr angeboten.")) return;
// Aus globaler Liste entfernen
if(Array.isArray(db.globalPeople[currentDepartment])){
db.globalPeople[currentDepartment] = db.globalPeople[currentDepartment].filter(p=>p.id!==id);
}
// Aus aktueller Sitzung entfernen (nur wenn nicht in Zuordnungen genutzt)
// Erst prüfen, ob die Person in der aktuellen Sitzung noch zugeordnet ist –
// erst danach die globalen Listen verändern, sonst wäre die globale Löschung
// trotz Abbruch bereits durchgeführt (und würde über save() persistiert).
const w = currentWeek();
if(w){
const inUse = w.projects.some(p => Object.keys(p.assign).some(k => k.startsWith(id + '|')));
@@ -67,6 +64,15 @@ function delPerson(id){
alert('Die Person ist in der aktuellen Sitzung noch Projekten zugeordnet. Bitte zuerst die Zuordnungen entfernen.');
return;
}
}
// Aus globaler Liste entfernen
if(Array.isArray(db.globalPeople[currentDepartment])){
db.globalPeople[currentDepartment] = db.globalPeople[currentDepartment].filter(p=>p.id!==id);
}
// Aus aktueller Sitzung entfernen
if(w){
w.people = (w.people || []).filter(p=>p.id!==id);
}
@@ -176,12 +182,14 @@ function togglePersonRole(id, role, checked){
function renderPeople(){
const people = db.globalPeople[currentDepartment] || [];
const locked = isWeekLocked(currentWeek());
let html = `<div class="people-sortable">`;
let html = `<div class="people-sortable"
ondragover="handlePersonDragOver(event)"
ondrop="handlePersonDrop(event)">`;
people.forEach(p=>{
const isOpen = personAbsenceOpen[p.id];
html += `<div class="person-card${isOpen ? ' open' : ''}" data-person-id="${p.id}">`;
html += `<div class="person-header" onclick="togglePersonAbsences('${p.id}')">
<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px" onclick="event.stopPropagation()" ondragstart="handlePersonDragStart(event)" draggable="true">⋮⋮</span>
${locked ? '' : `<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px" onclick="event.stopPropagation()" ondragstart="handlePersonDragStart(event)" ondragend="handlePersonDragEnd(event)" draggable="true">⋮⋮</span>`}
<span class="person-toggle">${isOpen ? '▼' : '▶'}</span>
<input type="text" value="${esc(p.name)}" onclick="event.stopPropagation()" onchange="editPerson('${p.id}','name',this.value)" style="width:80px;" placeholder="Kürzel">
<input type="text" value="${esc(p.fullName||p.name)}" onclick="event.stopPropagation()" onchange="editPerson('${p.id}','fullName',this.value)" style="width:180px;" placeholder="Voller Name">
@@ -273,7 +281,7 @@ function handlePersonDrop(event){
if(!draggedPerson || draggedPerson === targetCard) return;
const sortable = document.querySelector('.people-sortable');
if(!sortable) return;
const afterElement = getDragAfterElement(sortable, event.clientY);
const afterElement = getDragAfterElement(sortable, event.clientY, '.person-card:not([style*="opacity"])');
if(afterElement == null){
sortable.appendChild(draggedPerson);
} else {
@@ -291,8 +299,9 @@ function handlePersonDragEnd(event){
}
// reorderPeople: übernimmt die im DOM vorgefundene Reihenfolge der
// Personen-Karten in die globale Personenliste der Abteilung und speichert sie.
// Keine Parameter, keine Rückgabe; abgebrochen bei gesperrter Sitzung.
// Personen-Karten in die globale Personenliste der Abteilung UND die
// Personen-Kopie der aktuellen Woche (letztere bestimmt die Spaltenreihenfolge
// im Projektboard), dann speichern. Keine Parameter, keine Rückgabe.
function reorderPeople(){
if(guardWeekLocked()) return;
const sortable = document.querySelector('.people-sortable');
@@ -306,6 +315,15 @@ function reorderPeople(){
});
if(newPeople.length === people.length){
db.globalPeople[currentDepartment] = newPeople;
// Wochen-Kopie auf dieselbe Reihenfolge bringen, damit das Board sie nutzt
const w = currentWeek();
if(w && Array.isArray(w.people)){
const byId = {};
w.people.forEach(p => { byId[p.id] = p; });
const newWeekPeople = [];
newPeople.forEach(p => { if(byId[p.id]) newWeekPeople.push(byId[p.id]); });
if(newWeekPeople.length === w.people.length) w.people = newWeekPeople;
}
save();
}
}
+1
View File
@@ -141,6 +141,7 @@
const row = buildMilestoneRow(m, mIdx);
insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling);
insertAfter = row;
insertedMilestoneIds.add(m.id);
});
// Aufgabenzeile
+14 -11
View File
@@ -85,19 +85,21 @@
}
// getGanttRowDragAfterElement: ermittelt unter allen nicht gezogenen
// Zeilen jenes Element, das der übergebenen y-Position am nächsten liegt.
// Rückgabe: Element oder undefined (dann wäre die Zeile am Chart-Ende).
// Zeilen jenes Element, das der übergebenen y-Position am nächsten liegt
// und dessen Mitte unterhalb des Cursors beginnt. Rückgabe: Element oder
// null, wenn keine Zeile mehr unter dem Cursor liegt (Curser unterhalb der
// letzten Zeile → die gezogene Zeile gehört ans Chart-Ende).
function getGanttRowDragAfterElement(chart, y){
const draggableElements = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR + ':not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const closest = draggableElements.reduce((acc, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if(offset < 0 && offset > closest.offset){
if(offset < 0 && offset > acc.offset){
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
return acc;
}, { offset: Number.NEGATIVE_INFINITY });
return closest.element || null;
}
// reorderPlanRows: übernimmt die im DOM gefundene Zeilen-Reihenfolge in den
@@ -151,9 +153,11 @@
position = 'after:' + prevRow.dataset.todoId;
} else if(nextRow && nextRow.classList.contains('gantt-todo')){
position = 'before:' + nextRow.dataset.todoId;
} else if(prevRow && prevRow.classList.contains('gantt-milestone-row')){
// Zwei Meilensteine hintereinander: beziehe dich auf die nächste TODO danach oder letzte davor
// Suche nächste TODO nach diesem Block
} else if((!prevRow || !prevRow.classList.contains('gantt-todo')) && (!nextRow || !nextRow.classList.contains('gantt-todo'))){
// Weder vor noch hinter diesem Meilenstein steht direkt eine TODO –
// das ist der Fall für einen führenden Meilenstein-Block (vor der
// ersten TODO) oder mehrere Meilensteine hintereinander. Beziehe dich
// auf die nächste TODO danach oder die letzte TODO davor.
let nextTodoId = null;
for(let i = idx + 1; i < rows.length; i++){
if(rows[i].classList.contains('gantt-todo')){ nextTodoId = rows[i].dataset.todoId; break; }
@@ -161,7 +165,6 @@
if(nextTodoId){
position = 'before:' + nextTodoId;
} else {
// Keine TODO danach -> letzte TODO davor suchen
let prevTodoId = null;
for(let i = idx - 1; i >= 0; i--){
if(rows[i].classList.contains('gantt-todo')){ prevTodoId = rows[i].dataset.todoId; break; }
+6 -1
View File
@@ -41,7 +41,12 @@ window.PlanApp = {
app.department = routeMatch ? routeMatch[1] : (app.params.get('department') || 'ELT');
app.weekLabel = app.params.get('weekLabel');
app.projNr = app.params.get('projNr') || '';
app.projName = routeMatch && routeMatch[2] ? decodeURIComponent(routeMatch[2]) : (app.params.get('projName') || '');
let projNameFromRoute = '';
if(routeMatch && routeMatch[2]){
try { projNameFromRoute = decodeURIComponent(routeMatch[2]); }
catch(e) { projNameFromRoute = app.params.get('projName') || ''; }
}
app.projName = projNameFromRoute || (app.params.get('projName') || '');
// init: Einstiegspunkt nach DOMContentLoaded. Übernimmt das Theme aus
// localStorage/System, prüft die URL-Parameter und bricht mit Fehlermeldung
+47 -19
View File
@@ -24,11 +24,23 @@ function delProjectFromList(id){
// editProjectField: ändert ein Feld eines Projekts (z.B. Name oder Gruppe).
// Parameter: `id` (Projekt-ID), `field` (Feldname), `val` (neuer Wert).
// Keine Rückgabe.
// Beim Wechsel der Gruppe wird das Projekt ans Ende der Zielgruppe angehängt
// (neue `order` = höchste Order der Zielgruppe + 1), damit es nicht anhand
// seiner alten `order` in der Liste landet. Keine Rückgabe.
function editProjectField(id,field,val){
if(guardWeekLocked()) return;
const p = currentWeek().projects.find(x=>x.id===id);
if(p){ p[field]=val; save(); renderProjects(); renderBoard(); }
const w = currentWeek();
const p = w.projects.find(x=>x.id===id);
if(!p) return;
if(field === 'group' && val && val !== p.group){
p.group = val;
const targetProjects = w.projects.filter(x => x.id !== id && x.group === val);
const maxOrder = targetProjects.reduce((m,x)=>Math.max(m, x.order || 0), -1);
p.order = maxOrder + 1;
} else {
p[field] = val;
}
save(); renderProjects(); renderBoard();
}
// editProjectActiveLP: setzt bzw. hebt die amtierende Leistungsphase eines
@@ -59,9 +71,9 @@ function renderProjects(){
sortGroups(Object.keys(groups)).forEach(g=>{
html += `<div class="section-title">${esc(g)}</div>`;
html += `<div class="projects-sortable" data-group="${g}">`;
html += `<div class="projects-sortable" data-group="${g}" ondragover="handleDragOver(event)" ondrop="handleDrop(event)">`;
groups[g].forEach(p=>{
html += `<div class="person-list-item" draggable="true" data-project-id="${p.id}" ondragstart="handleDragStart(event)" ondragover="handleDragOver(event)" ondrop="handleDrop(event)" ondragend="handleDragEnd(event)">
html += `<div class="person-list-item" draggable="true" data-project-id="${p.id}" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)">
<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>
<input type="text" value="${esc(p.nr)}" onchange="editProjectField('${p.id}','nr',this.value)" style="width:100px" placeholder="Proj-Nr">
<input type="text" value="${esc(p.name)}" onchange="editProjectField('${p.id}','name',this.value)" style="width:200px" placeholder="Bezeichnung">
@@ -106,18 +118,31 @@ function handleDragOver(event){
event.dataTransfer.dropEffect = 'move';
}
// handleDrop: legt die gezogene Projekt-Zeile an der Position ab, die
// `getDragAfterElement` relativ zur Mausposition ermittelt, und speichert
// die neue Reihenfolge über `reorderProjects()`.
// handleDrop: legt die gezogene Projekt-Zeile ab. Der Handler hängt am
// Gruppen-Container (`.projects-sortable`), sodass das Ablegen über der
// gesamten Gruppe inklusive leerer Flächen möglich ist. Beim Wechsel zwischen
// Gruppen (z. B. "Kleine Projekte" → "Aktive Projekte") wird das Projekt ans
// Ende der Zielgruppe angehängt; innerhalb derselben Gruppe wird die Position
// per `getDragAfterElement` relativ zur Mausposition bestimmt. Danach speichert
// `reorderProjects()` die neue Reihenfolge.
function handleDrop(event){
event.preventDefault();
if(!draggedProject || draggedProject === event.currentTarget) return;
const sortable = event.currentTarget.parentElement;
const afterElement = getDragAfterElement(sortable, event.clientY);
if(afterElement == null){
sortable.appendChild(draggedProject);
if(!draggedProject) return;
const sortable = event.currentTarget;
const sourceSortable = draggedProject.parentElement;
const isSameGroup = sourceSortable &&
sourceSortable.dataset && sortable.dataset &&
sourceSortable.dataset.group === sortable.dataset.group;
if(isSameGroup){
const afterElement = getDragAfterElement(sortable, event.clientY);
if(afterElement == null){
sortable.appendChild(draggedProject);
} else {
sortable.insertBefore(draggedProject, afterElement);
}
} else {
sortable.insertBefore(draggedProject, afterElement);
// Gruppenwechsel: ans Ende der Zielgruppe anhängen
sortable.appendChild(draggedProject);
}
reorderProjects();
}
@@ -131,11 +156,14 @@ function handleDragEnd(event){
// getDragAfterElement: ermittelt das Element in `container`, vor dem die
// gezogene Zeile bei Mausposition `y` abgelegt werden soll (Suche nach dem
// Element mit der kleinsten negativen Distanz zur Mitte). Rückgabe:
// Ziel-Element oder undefined (→ ans Ende anfügen). Wird auch von
// `js/people/people.js` für die Personen-Sortierung verwendet.
function getDragAfterElement(container, y){
const draggableElements = [...container.querySelectorAll('.person-list-item:not([style*="opacity"])')];
// Element mit der kleinsten negativen Distanz zur Mitte). Parameter:
// `selector` = CSS-Selektor der sortierbaren Elemente (standardmäßig die
// Projekt-Zeilen, steht aber auch der Personen-Liste für `.person-card` zur
// Verfügung). Rückgabe: Ziel-Element oder undefined (→ ans Ende anfügen).
// Wird auch von `js/people/people.js` für die Personen-Sortierung verwendet.
function getDragAfterElement(container, y, selector){
const sel = selector || '.person-list-item:not([style*="opacity"])';
const draggableElements = [...container.querySelectorAll(sel)];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
+14 -5
View File
@@ -18,6 +18,9 @@ let showModeTimer = null;
let showModeResumeTimer = null;
// showModePaused: true, wenn der Durchlauf gerade pausiert ist
let showModePaused = false;
// showModePollTimer: Timer-Handle des Poll-Intervalls in waitForLoadThenRender
// (wird in stopShowMode mit aufgeräumt, sonst rendert es nach dem Beenden weiter)
let showModePollTimer = null;
// showModeOriginal: Abteilung und Tab vor dem Start, wird beim Beenden wiederhergestellt
let showModeOriginal = { department: DEFAULT_DEPARTMENT, tab: DEFAULT_TAB };
const SHOW_MODE_SLIDE_DURATION = 10000; // 10 Sekunden pro Slide
@@ -62,16 +65,19 @@ function stopShowMode(){
showModePaused = false;
clearTimeout(showModeTimer);
clearTimeout(showModeResumeTimer);
if(showModePollTimer){ clearInterval(showModePollTimer); showModePollTimer = null; }
const overlay = document.getElementById('showModeOverlay');
if(overlay) overlay.style.display = 'none';
// Ursprünglichen Zustand wiederherstellen
// Ursprünglichen Zustand wiederherstellen. Geschützte Tabs ('projects'/
// 'people') werden nur ohne Passwort angezeigt, wenn bereits authentifiziert –
// sonst öffnet showTab (ohne skipAuth) das Passwort-Modal.
currentTab = showModeOriginal.tab;
if(currentDepartment !== showModeOriginal.department){
switchDepartment(showModeOriginal.department);
} else {
showTab(currentTab, true);
showTab(currentTab, isAuthenticated);
renderAll();
}
}
@@ -155,11 +161,14 @@ function showModeNextSlide(){
// plant die nächste Folie. Parameter: view = 'board' oder 'load'.
function waitForLoadThenRender(view){
let attempts = 0;
const check = setInterval(() => {
showModePollTimer = setInterval(() => {
attempts++;
// Poll nur fortsetzen, solange der Show Mode noch läuft (sonst würde nach
// stopShowMode der gerade wiederhergestellte Zustand überschrieben).
if(!showModeActive && showModePollTimer){ clearInterval(showModePollTimer); showModePollTimer = null; return; }
// Wenn Wochen geladen sind oder nach Seed gefallen ist
if(db.weeks.length > 0 || attempts > 30){
clearInterval(check);
if((db.weeks.length > 0 || attempts > 30) && showModeActive){
if(showModePollTimer){ clearInterval(showModePollTimer); showModePollTimer = null; }
showTab(view, true);
renderAll();
// Kurze Verzögerung, damit das DOM aktualisiert ist, bevor kopiert wird
+11
View File
@@ -60,6 +60,10 @@ function saveGlobalPeopleToServer(){
// Gibt nichts zurück.
function switchDepartment(dept){
if(!DEPARTMENTS[dept]) return;
// Laufenden Speicher-Debounce der alten Abteilung verwerfen, damit dessen
// Sync nicht nach dem Reset mit dem neuen currentDepartment feuert und die
// CSV der falschen Abteilung schreibt.
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
currentDepartment = dept;
saveUiState();
updateRouteUrl(dept, currentTab);
@@ -90,11 +94,17 @@ function load(){
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
seed(); return;
}
// Abteilung einfrieren, für die diese Anfrage gestartet wurde. Antworten,
// die nach einem zwischenzeitlichen Abteilungswechsel eintreffen, werden
// verworfen (Stale-Response), sonst würden Daten der alten Abteilung unter
// der neuen Abteilung angezeigt und gespeichert.
const requestedDepartment = currentDepartment;
const url = baseUrl() + '/load-all' + departmentParam();
console.log('Loading from', url);
fetch(url)
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => {
if(currentDepartment !== requestedDepartment) return;
// Globale Personenliste laden
db.globalPeople[currentDepartment] = parseGlobalPeopleCsv(data.people);
@@ -126,6 +136,7 @@ function load(){
showTab(currentTab);
})
.catch(err => {
if(currentDepartment !== requestedDepartment) return;
console.error('Laden vom Server fehlgeschlagen:', err);
const serverUrl = baseUrl();
const msg = 'Server unter ' + serverUrl + ' nicht erreichbar.\n\n' +
+9 -3
View File
@@ -47,14 +47,20 @@ function confirmPassword(){
const pwdHash = CryptoJS.SHA256(pwd).toString();
if(pwdHash === PROTECTED_PASSWORD_HASH){
const action = requestedAction;
requestedAction = null;
document.getElementById('passwordModal').style.display='none';
// Sitzungs-Authentifizierung merken, damit Tab-Wechsel und Abteilungs-
// wechsel innerhalb dieser Sitzung kein erneutes Passwort verlangen.
isAuthenticated = true;
// Nur die zuletzt angeforderte Aktion ausführen (nicht eine evtl. bereits
// durch closePasswordModal verworfenen Aktion erneut starten) – daher die
// Aktion hier erneut über requestedAction prüfen statt sie festzuhalten.
setTimeout(() => {
const action = requestedAction;
requestedAction = null;
if(action?.type==='tab') showTab(action.tab, true);
else if(action?.type==='deleteWeek') deleteWeek(true);
else if(action?.type==='unlockWeek') unlockCurrentWeek(true);
}, 100);
document.getElementById('passwordModal').style.display='none';
} else {
alert("Falsches Passwort!");
document.getElementById('passwordInput').value='';
+30
View File
@@ -35,6 +35,36 @@ function sortGroups(arr){
});
}
// peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt.
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle
// Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung.
// Liegt im gemeinsamen Modul, weil auch csv.js (weekToCsv für savePlan auf der
// Projektplan-Seite) und pdf.js darauf zugreifen.
function peopleByRole(target){
const m = {}; ROLES.forEach(r=>m[r]=[]);
const people = [];
if(Array.isArray(target)){
target.forEach(w => { if(w && Array.isArray(w.people)) people.push(...w.people); });
} else if(target && Array.isArray(target.people)){
people.push(...target.people);
} else if(Array.isArray(currentWeek()?.people)){
people.push(...currentWeek().people);
}
const seen = new Set();
people.forEach(p=>{
const personRoles = Array.isArray(p.role) ? p.role : [p.role];
personRoles.forEach(role=>{
if(!m[role]) m[role]=[];
const key = `${p.id}|${role}`;
if(!seen.has(key)){
seen.add(key);
m[role].push({person:p, role});
}
});
});
return m;
}
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
// Zuordnungen auch nach einem Neuladen stabil bleiben.
// Parameter: name (String). Rückgabe: String wie 'p_ab'.