diff --git a/AGENTS.md b/AGENTS.md index 7683395..093811d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ user-facing text. │ ├── Test.html # main app (served at /) │ ├── js/ # shared modules (classic scripts, global fns) │ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment -│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, weekSortKey-Quelle +│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey-Quelle │ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme │ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays │ │ ├── csv.js # CSV-Parser/Serializer + weekSortKey, filenameForWeek @@ -147,6 +147,17 @@ docker compose up --build # Docker variant (compose file is at repo root) `_people.csv` — not just the changed week. Exception: `js/plan/plan-state.js` `savePlan()` bypasses `save()` and writes only the current week's CSV directly via `sendCsvToServer()`. +- `switchDepartment()` cancels a pending save debounce before switching, and + `load()` discards stale `/load-all` responses after a department switch + (frozen `requestedDepartment`), so late responses can never render or persist + data under the wrong department. +- Person drag-and-drop in the people tab: drag handle (`.drag-handle`) binds + `ondragstart`/`ondragend`, the `.people-sortable` container binds + `ondragover`/`ondrop`; insertion is computed by `getDragAfterElement(container, + y, selector)` (`js/projects/projects.js`, used with `.person-card` here and + `.person-list-item` in the projects tab). `reorderPeople()` updates both + `db.globalPeople` and the current week's person copy, then `save()`. No drag + handles are rendered on locked weeks. - Weeks are sorted by calendar week extracted from the label (`KW XX / YYYY`) via `weekSortKey()`, not alphabetically/by filename. - `applyAutoLock()` locks every week except the chronologically last one after diff --git a/public/js/board/board.js b/public/js/board/board.js index ad12263..3b18be7 100644 --- a/public/js/board/board.js +++ b/public/js/board/board.js @@ -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` diff --git a/public/js/csv.js b/public/js/csv.js index 013d029..d6adf65 100644 --- a/public/js/csv.js +++ b/public/js/csv.js @@ -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 = ''; diff --git a/public/js/load/load.js b/public/js/load/load.js index cc56181..1ca70b3 100644 --- a/public/js/load/load.js +++ b/public/js/load/load.js @@ -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. diff --git a/public/js/pdf.js b/public/js/pdf.js index e32f511..0915bc5 100644 --- a/public/js/pdf.js +++ b/public/js/pdf.js @@ -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 }, diff --git a/public/js/people/people.js b/public/js/people/people.js index 34397fd..321b74b 100644 --- a/public/js/people/people.js +++ b/public/js/people/people.js @@ -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 = `