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

+12 -1
View File
@@ -32,7 +32,7 @@ user-facing text.
│ ├── Test.html # main app (served at /) │ ├── Test.html # main app (served at /)
│ ├── js/ # shared modules (classic scripts, global fns) │ ├── js/ # shared modules (classic scripts, global fns)
│ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment │ │ ├── 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 │ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
│ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays │ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays
│ │ ├── csv.js # CSV-Parser/Serializer + weekSortKey, filenameForWeek │ │ ├── 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` `_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 `savePlan()` bypasses `save()` and writes only the current week's CSV directly
via `sendCsvToServer()`. 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`) - Weeks are sorted by calendar week extracted from the label (`KW XX / YYYY`)
via `weekSortKey()`, not alphabetically/by filename. via `weekSortKey()`, not alphabetically/by filename.
- `applyAutoLock()` locks every week except the chronologically last one after - `applyAutoLock()` locks every week except the chronologically last one after
+2 -27
View File
@@ -52,33 +52,8 @@ function formatStatus(command, value){
document.execCommand(command, false, value || null); document.execCommand(command, false, value || null);
} }
// peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt. // peopleByRole liegt jetzt im gemeinsamen Modul js/utils.js (benötigt auch
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle // csv.js für die Projektplan-Seite, die board.js nicht lädt).
// 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;
}
// roleClass: erzeugt CSS-Klassen für eine Rolle bzw. ein Rollen-Array // roleClass: erzeugt CSS-Klassen für eine Rolle bzw. ein Rollen-Array
// (Rollenname kleingeschrieben, Leerzeichen → Bindestrich). Parameter: `role` // (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); 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 // Toleranter CSV-Zeilenparser. Versteht sauber gequotetes CSV (wie es
// das ältere Format, bei dem Felder nur ein öffnendes Anführungszeichen // csvEscape/weekToCsv erzeugen). Semikolons innerhalb eines quoted Feldes sind
// hatten und das Semikolon innerhalb eines quoted Feldes gleichzeitig // Feldinhalt und kein Trennzeichen – so bleibt weekToCsv()/parseSessionCsv()
// als Trennzeichen diente. // auch bei Status-Texten, die mit ";" enden, invers.
// parseCsvLine: zerlegt eine einzelne CSV-Zeile in ein Feld-Array // parseCsvLine: zerlegt eine einzelne CSV-Zeile in ein Feld-Array
// Parameter: line (String). Rückgabe: Array der Felder. // Parameter: line (String). Rückgabe: Array der Felder.
function parseCsvLine(line){ function parseCsvLine(line){
@@ -130,12 +130,6 @@ function parseCsvLine(line){
if(inQuotes){ if(inQuotes){
if(c === '"' && n === '"'){ if(c === '"' && n === '"'){
current += '"'; i += 2; 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)){ } else if(c === '"' && (n === ';' || n === undefined)){
result.push(current); result.push(current);
current = ''; current = '';
+1 -1
View File
@@ -6,7 +6,7 @@
// people → projects → load → gantt → …). Es stellt renderLoad(), das LP- // people → projects → load → gantt → …). Es stellt renderLoad(), das LP-
// Aufwand-Modal und das Neues-Projekt-Modal bereit. Genutzt werden LPHS/ROLES // 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/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, // Die Auslastungs-Logik (berechneAuslastung, lpActiveList, faktorFormat,
// pdfLoadAmpelColor, LOAD_BAR_MAX) wird zusätzlich von js/pdf.js für den // pdfLoadAmpelColor, LOAD_BAR_MAX) wird zusätzlich von js/pdf.js für den
// PDF-Export wiederverwendet. // 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 // 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 // die per CDN eingebundene Bibliothek pdfmake (offline nicht verfügbar). Es
// liest die aktuelle Woche über currentWeek() aus js/store.js, Personen über // 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 // formatWeekDate() aus js/utils.js sowie die Auslastungs-Berechnung
// (berechneAuslastung, lpActiveList, faktorFormat, pdfLoadAmpelColor, // (berechneAuslastung, lpActiveList, faktorFormat, pdfLoadAmpelColor,
// LOAD_BAR_MAX) aus js/load/load.js. Das Modul stellt ausschließlich exportPDF() // 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 => { 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 s = (p.assign || {})[assignKey(entry.person.id, entry.role)] || 0;
const txt = s === 1 ? 'X' : s === 2 ? '(X)' : ''; const txt = s === 1 ? 'X' : s === 2 ? '(X)' : '';
const bg = s === 1 ? PDF_ASSIGN_MAIN const bg = s === 1 ? PDF_ASSIGN_MAIN
@@ -468,17 +474,25 @@ async function exportPDF(){
try { try {
const grouped = peopleByRole(w); const grouped = peopleByRole(w);
const allPeople = []; // displayPeople: Spalten der Tabelle inkl. Platzhalter für leere Rollen –
ROLES.forEach(r => (grouped[r] || []).forEach(e => { // pdfHeaderRows erzwingt pro Rolle mindestens eine Spalte (colSpan/“–”),
// role explizit setzen, falls peopleByRole sie nicht mitliefert // deshalb müssen Zeilen, Breiten und colCount dieselbe Spaltenzahl nutzen,
allPeople.push({ person: e.person, role: e.role || r }); // 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 logo = await loadLogoPng();
const colCount = 3 + allPeople.length + LPHS.length; const colCount = 3 + displayPeople.length + LPHS.length;
const widths = pdfColumnWidths(allPeople); const widths = pdfColumnWidths(displayPeople);
// --- Deckblatt --- // --- Deckblatt ---
const cover = []; const cover = [];
@@ -521,7 +535,7 @@ async function exportPDF(){
const header = pdfHeaderRows(grouped); const header = pdfHeaderRows(grouped);
const body = [ pdfGroupTitleRow(groupName, colCount), header[0], header[1] ]; 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({ content.push({
table: { headerRows: 3, widths: widths, body: body, dontBreakRows: true }, table: { headerRows: 3, widths: widths, body: body, dontBreakRows: true },
+29 -11
View File
@@ -54,12 +54,9 @@ function addPerson(){
function delPerson(id){ 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; 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 // Erst prüfen, ob die Person in der aktuellen Sitzung noch zugeordnet ist –
if(Array.isArray(db.globalPeople[currentDepartment])){ // erst danach die globalen Listen verändern, sonst wäre die globale Löschung
db.globalPeople[currentDepartment] = db.globalPeople[currentDepartment].filter(p=>p.id!==id); // trotz Abbruch bereits durchgeführt (und würde über save() persistiert).
}
// Aus aktueller Sitzung entfernen (nur wenn nicht in Zuordnungen genutzt)
const w = currentWeek(); const w = currentWeek();
if(w){ if(w){
const inUse = w.projects.some(p => Object.keys(p.assign).some(k => k.startsWith(id + '|'))); 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.'); alert('Die Person ist in der aktuellen Sitzung noch Projekten zugeordnet. Bitte zuerst die Zuordnungen entfernen.');
return; 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); w.people = (w.people || []).filter(p=>p.id!==id);
} }
@@ -176,12 +182,14 @@ function togglePersonRole(id, role, checked){
function renderPeople(){ function renderPeople(){
const people = db.globalPeople[currentDepartment] || []; const people = db.globalPeople[currentDepartment] || [];
const locked = isWeekLocked(currentWeek()); 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=>{ people.forEach(p=>{
const isOpen = personAbsenceOpen[p.id]; const isOpen = personAbsenceOpen[p.id];
html += `<div class="person-card${isOpen ? ' open' : ''}" data-person-id="${p.id}">`; html += `<div class="person-card${isOpen ? ' open' : ''}" data-person-id="${p.id}">`;
html += `<div class="person-header" onclick="togglePersonAbsences('${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> <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.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"> <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; if(!draggedPerson || draggedPerson === targetCard) return;
const sortable = document.querySelector('.people-sortable'); const sortable = document.querySelector('.people-sortable');
if(!sortable) return; if(!sortable) return;
const afterElement = getDragAfterElement(sortable, event.clientY); const afterElement = getDragAfterElement(sortable, event.clientY, '.person-card:not([style*="opacity"])');
if(afterElement == null){ if(afterElement == null){
sortable.appendChild(draggedPerson); sortable.appendChild(draggedPerson);
} else { } else {
@@ -291,8 +299,9 @@ function handlePersonDragEnd(event){
} }
// reorderPeople: übernimmt die im DOM vorgefundene Reihenfolge der // reorderPeople: übernimmt die im DOM vorgefundene Reihenfolge der
// Personen-Karten in die globale Personenliste der Abteilung und speichert sie. // Personen-Karten in die globale Personenliste der Abteilung UND die
// Keine Parameter, keine Rückgabe; abgebrochen bei gesperrter Sitzung. // Personen-Kopie der aktuellen Woche (letztere bestimmt die Spaltenreihenfolge
// im Projektboard), dann speichern. Keine Parameter, keine Rückgabe.
function reorderPeople(){ function reorderPeople(){
if(guardWeekLocked()) return; if(guardWeekLocked()) return;
const sortable = document.querySelector('.people-sortable'); const sortable = document.querySelector('.people-sortable');
@@ -306,6 +315,15 @@ function reorderPeople(){
}); });
if(newPeople.length === people.length){ if(newPeople.length === people.length){
db.globalPeople[currentDepartment] = newPeople; 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(); save();
} }
} }
+1
View File
@@ -141,6 +141,7 @@
const row = buildMilestoneRow(m, mIdx); const row = buildMilestoneRow(m, mIdx);
insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling); insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling);
insertAfter = row; insertAfter = row;
insertedMilestoneIds.add(m.id);
}); });
// Aufgabenzeile // Aufgabenzeile
+14 -11
View File
@@ -85,19 +85,21 @@
} }
// getGanttRowDragAfterElement: ermittelt unter allen nicht gezogenen // getGanttRowDragAfterElement: ermittelt unter allen nicht gezogenen
// Zeilen jenes Element, das der übergebenen y-Position am nächsten liegt. // 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). // 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){ function getGanttRowDragAfterElement(chart, y){
const draggableElements = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR + ':not(.dragging)')]; 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 box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2; 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 }; 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 // reorderPlanRows: übernimmt die im DOM gefundene Zeilen-Reihenfolge in den
@@ -151,9 +153,11 @@
position = 'after:' + prevRow.dataset.todoId; position = 'after:' + prevRow.dataset.todoId;
} else if(nextRow && nextRow.classList.contains('gantt-todo')){ } else if(nextRow && nextRow.classList.contains('gantt-todo')){
position = 'before:' + nextRow.dataset.todoId; position = 'before:' + nextRow.dataset.todoId;
} else if(prevRow && prevRow.classList.contains('gantt-milestone-row')){ } else if((!prevRow || !prevRow.classList.contains('gantt-todo')) && (!nextRow || !nextRow.classList.contains('gantt-todo'))){
// Zwei Meilensteine hintereinander: beziehe dich auf die nächste TODO danach oder letzte davor // Weder vor noch hinter diesem Meilenstein steht direkt eine TODO –
// Suche nächste TODO nach diesem Block // 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; let nextTodoId = null;
for(let i = idx + 1; i < rows.length; i++){ for(let i = idx + 1; i < rows.length; i++){
if(rows[i].classList.contains('gantt-todo')){ nextTodoId = rows[i].dataset.todoId; break; } if(rows[i].classList.contains('gantt-todo')){ nextTodoId = rows[i].dataset.todoId; break; }
@@ -161,7 +165,6 @@
if(nextTodoId){ if(nextTodoId){
position = 'before:' + nextTodoId; position = 'before:' + nextTodoId;
} else { } else {
// Keine TODO danach -> letzte TODO davor suchen
let prevTodoId = null; let prevTodoId = null;
for(let i = idx - 1; i >= 0; i--){ for(let i = idx - 1; i >= 0; i--){
if(rows[i].classList.contains('gantt-todo')){ prevTodoId = rows[i].dataset.todoId; break; } 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.department = routeMatch ? routeMatch[1] : (app.params.get('department') || 'ELT');
app.weekLabel = app.params.get('weekLabel'); app.weekLabel = app.params.get('weekLabel');
app.projNr = app.params.get('projNr') || ''; 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 // init: Einstiegspunkt nach DOMContentLoaded. Übernimmt das Theme aus
// localStorage/System, prüft die URL-Parameter und bricht mit Fehlermeldung // 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). // editProjectField: ändert ein Feld eines Projekts (z.B. Name oder Gruppe).
// Parameter: `id` (Projekt-ID), `field` (Feldname), `val` (neuer Wert). // 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){ function editProjectField(id,field,val){
if(guardWeekLocked()) return; if(guardWeekLocked()) return;
const p = currentWeek().projects.find(x=>x.id===id); const w = currentWeek();
if(p){ p[field]=val; save(); renderProjects(); renderBoard(); } 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 // editProjectActiveLP: setzt bzw. hebt die amtierende Leistungsphase eines
@@ -59,9 +71,9 @@ function renderProjects(){
sortGroups(Object.keys(groups)).forEach(g=>{ sortGroups(Object.keys(groups)).forEach(g=>{
html += `<div class="section-title">${esc(g)}</div>`; 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=>{ 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> <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.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"> <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'; event.dataTransfer.dropEffect = 'move';
} }
// handleDrop: legt die gezogene Projekt-Zeile an der Position ab, die // handleDrop: legt die gezogene Projekt-Zeile ab. Der Handler hängt am
// `getDragAfterElement` relativ zur Mausposition ermittelt, und speichert // Gruppen-Container (`.projects-sortable`), sodass das Ablegen über der
// die neue Reihenfolge über `reorderProjects()`. // 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){ function handleDrop(event){
event.preventDefault(); event.preventDefault();
if(!draggedProject || draggedProject === event.currentTarget) return; if(!draggedProject) return;
const sortable = event.currentTarget.parentElement; const sortable = event.currentTarget;
const afterElement = getDragAfterElement(sortable, event.clientY); const sourceSortable = draggedProject.parentElement;
if(afterElement == null){ const isSameGroup = sourceSortable &&
sortable.appendChild(draggedProject); 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 { } else {
sortable.insertBefore(draggedProject, afterElement); // Gruppenwechsel: ans Ende der Zielgruppe anhängen
sortable.appendChild(draggedProject);
} }
reorderProjects(); reorderProjects();
} }
@@ -131,11 +156,14 @@ function handleDragEnd(event){
// getDragAfterElement: ermittelt das Element in `container`, vor dem die // getDragAfterElement: ermittelt das Element in `container`, vor dem die
// gezogene Zeile bei Mausposition `y` abgelegt werden soll (Suche nach dem // gezogene Zeile bei Mausposition `y` abgelegt werden soll (Suche nach dem
// Element mit der kleinsten negativen Distanz zur Mitte). Rückgabe: // Element mit der kleinsten negativen Distanz zur Mitte). Parameter:
// Ziel-Element oder undefined (→ ans Ende anfügen). Wird auch von // `selector` = CSS-Selektor der sortierbaren Elemente (standardmäßig die
// `js/people/people.js` für die Personen-Sortierung verwendet. // Projekt-Zeilen, steht aber auch der Personen-Liste für `.person-card` zur
function getDragAfterElement(container, y){ // Verfügung). Rückgabe: Ziel-Element oder undefined (→ ans Ende anfügen).
const draggableElements = [...container.querySelectorAll('.person-list-item:not([style*="opacity"])')]; // 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) => { return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect(); const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2; const offset = y - box.top - box.height / 2;
+14 -5
View File
@@ -18,6 +18,9 @@ let showModeTimer = null;
let showModeResumeTimer = null; let showModeResumeTimer = null;
// showModePaused: true, wenn der Durchlauf gerade pausiert ist // showModePaused: true, wenn der Durchlauf gerade pausiert ist
let showModePaused = false; 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 // showModeOriginal: Abteilung und Tab vor dem Start, wird beim Beenden wiederhergestellt
let showModeOriginal = { department: DEFAULT_DEPARTMENT, tab: DEFAULT_TAB }; let showModeOriginal = { department: DEFAULT_DEPARTMENT, tab: DEFAULT_TAB };
const SHOW_MODE_SLIDE_DURATION = 10000; // 10 Sekunden pro Slide const SHOW_MODE_SLIDE_DURATION = 10000; // 10 Sekunden pro Slide
@@ -62,16 +65,19 @@ function stopShowMode(){
showModePaused = false; showModePaused = false;
clearTimeout(showModeTimer); clearTimeout(showModeTimer);
clearTimeout(showModeResumeTimer); clearTimeout(showModeResumeTimer);
if(showModePollTimer){ clearInterval(showModePollTimer); showModePollTimer = null; }
const overlay = document.getElementById('showModeOverlay'); const overlay = document.getElementById('showModeOverlay');
if(overlay) overlay.style.display = 'none'; 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; currentTab = showModeOriginal.tab;
if(currentDepartment !== showModeOriginal.department){ if(currentDepartment !== showModeOriginal.department){
switchDepartment(showModeOriginal.department); switchDepartment(showModeOriginal.department);
} else { } else {
showTab(currentTab, true); showTab(currentTab, isAuthenticated);
renderAll(); renderAll();
} }
} }
@@ -155,11 +161,14 @@ function showModeNextSlide(){
// plant die nächste Folie. Parameter: view = 'board' oder 'load'. // plant die nächste Folie. Parameter: view = 'board' oder 'load'.
function waitForLoadThenRender(view){ function waitForLoadThenRender(view){
let attempts = 0; let attempts = 0;
const check = setInterval(() => { showModePollTimer = setInterval(() => {
attempts++; 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 // Wenn Wochen geladen sind oder nach Seed gefallen ist
if(db.weeks.length > 0 || attempts > 30){ if((db.weeks.length > 0 || attempts > 30) && showModeActive){
clearInterval(check); if(showModePollTimer){ clearInterval(showModePollTimer); showModePollTimer = null; }
showTab(view, true); showTab(view, true);
renderAll(); renderAll();
// Kurze Verzögerung, damit das DOM aktualisiert ist, bevor kopiert wird // Kurze Verzögerung, damit das DOM aktualisiert ist, bevor kopiert wird
+11
View File
@@ -60,6 +60,10 @@ function saveGlobalPeopleToServer(){
// Gibt nichts zurück. // Gibt nichts zurück.
function switchDepartment(dept){ function switchDepartment(dept){
if(!DEPARTMENTS[dept]) return; 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; currentDepartment = dept;
saveUiState(); saveUiState();
updateRouteUrl(dept, currentTab); updateRouteUrl(dept, currentTab);
@@ -90,11 +94,17 @@ function load(){
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.'); alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
seed(); return; 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(); const url = baseUrl() + '/load-all' + departmentParam();
console.log('Loading from', url); console.log('Loading from', url);
fetch(url) fetch(url)
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); }) .then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => { .then(data => {
if(currentDepartment !== requestedDepartment) return;
// Globale Personenliste laden // Globale Personenliste laden
db.globalPeople[currentDepartment] = parseGlobalPeopleCsv(data.people); db.globalPeople[currentDepartment] = parseGlobalPeopleCsv(data.people);
@@ -126,6 +136,7 @@ function load(){
showTab(currentTab); showTab(currentTab);
}) })
.catch(err => { .catch(err => {
if(currentDepartment !== requestedDepartment) return;
console.error('Laden vom Server fehlgeschlagen:', err); console.error('Laden vom Server fehlgeschlagen:', err);
const serverUrl = baseUrl(); const serverUrl = baseUrl();
const msg = 'Server unter ' + serverUrl + ' nicht erreichbar.\n\n' + 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(); const pwdHash = CryptoJS.SHA256(pwd).toString();
if(pwdHash === PROTECTED_PASSWORD_HASH){ if(pwdHash === PROTECTED_PASSWORD_HASH){
const action = requestedAction; // Sitzungs-Authentifizierung merken, damit Tab-Wechsel und Abteilungs-
requestedAction = null; // wechsel innerhalb dieser Sitzung kein erneutes Passwort verlangen.
document.getElementById('passwordModal').style.display='none'; 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(() => { setTimeout(() => {
const action = requestedAction;
requestedAction = null;
if(action?.type==='tab') showTab(action.tab, true); if(action?.type==='tab') showTab(action.tab, true);
else if(action?.type==='deleteWeek') deleteWeek(true); else if(action?.type==='deleteWeek') deleteWeek(true);
else if(action?.type==='unlockWeek') unlockCurrentWeek(true); else if(action?.type==='unlockWeek') unlockCurrentWeek(true);
}, 100); }, 100);
document.getElementById('passwordModal').style.display='none';
} else { } else {
alert("Falsches Passwort!"); alert("Falsches Passwort!");
document.getElementById('passwordInput').value=''; 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 // Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
// Zuordnungen auch nach einem Neuladen stabil bleiben. // Zuordnungen auch nach einem Neuladen stabil bleiben.
// Parameter: name (String). Rückgabe: String wie 'p_ab'. // Parameter: name (String). Rückgabe: String wie 'p_ab'.
+16 -16
View File
@@ -4,28 +4,28 @@ Pr.-Nr.;Bezeichnung;Status;AP;AK;ED;SK;SE;WS;ED;SP;THK;RR;MK;TW;SG;ED;SK;SP;THK;
ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
18-4007;Janusz-Korczak-Gesamtsch. Bottrop;- KW 35 NW Raum Planung <div>- KW 44 Möbel </div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;90%;;;;;Aktive Projekte;0;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";"{""LPH 5"":{""Planung"":0.6},""LPH 8"":{""Planung"":0,""Projektleitung"":0.2}}"; 18-4007;Janusz-Korczak-Gesamtsch. Bottrop;- KW 35 NW Raum Planung <div>- KW 44 Möbel </div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;90%;;;;;Aktive Projekte;0;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";"{""LPH 5"":{""Planung"":0.6},""LPH 8"":{""Planung"":0,""Projektleitung"":0.2}}";
21-4030;NSHV Köln Immobilienprüfung in Hof;- Thorsten W. klärt und übernimmt Schriftverkehr <div>- LV liegt zur Sichtung bei T.W.</div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;2;;; 21-4030;NSHV Köln Immobilienprüfung in Hof;- Thorsten W. klärt und übernimmt Schriftverkehr <div>- LV liegt zur Sichtung bei T.W.</div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;2;;;
22-4066;DSW 21, Umbau E-Mobilität;- Elt-Montagen ab <font><b>KW 33</b></font>.;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;6;;; 22-4066;DSW 21, Umbau E-Mobilität;- Elt-Montagen ab <font><b>KW 33</b></font>.;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;7;;;
24-4043;S-K ODL-Heim Sanierung Krefeld;- Vorraussichtlich LV-Abgabe XX.XX<div>- LPH5 mündlich freigegeben <div>- Neuer EG Grundriss mit diversen Änderungen kommt noch von Architekten</div></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;10;;; 24-4043;S-K ODL-Heim Sanierung Krefeld;- Vorraussichtlich LV-Abgabe XX.XX<div>- LPH5 mündlich freigegeben <div>- Neuer EG Grundriss mit diversen Änderungen kommt noch von Architekten</div></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;11;;;
21-4001;Feuerwehrgeräteh. Rodenkirchen;- KNX muß noch programmiert werden. <font>Abnahme 27.07</font> <div><font><b>- Fertigstellung Ende Juli</b></font><div><font><b>- Abnahmen laufen bis Ende KW31</b></font></div><div><font><b>- Blackbuilding Test 26.07</b></font></div></div>;X;;X;;;;X;X;;;;;;X;;;;;100%;100%;100%;;100%;100%;100%;;;Aktive Projekte;1;"{""start"":"""",""end"":"""",""milestones"":[{""id"":""mslrvh7hjau1e"",""name"":""Übergabe"",""date"":""2026-09-12"",""dependsOn"":[""mslv6h76cmx8k""],""position"":""after:mslv6h76cmx8k""}],""people"":[""p_ap"",""p_ak"",""p_sk"",""p_sg""],""progress"":""0"",""todos"":[{""id"":""msluufbp114g1"",""title"":""Test"",""start"":""2026-08-10"",""end"":""2026-08-14"",""done"":false,""dependsOn"":[],""assignees"":[""p_ap"",""p_ed""],""progress"":0},{""id"":""mslv6h76cmx8k"",""title"":""Test 2"",""start"":""2026-08-24"",""end"":""2026-09-04"",""done"":false,""dependsOn"":[""msluufbp114g1""],""assignees"":[""p_ap""],""progress"":41},{""id"":""mslw4l3qp1v18"",""title"":""Test 3"",""start"":""2026-09-14"",""end"":""2026-09-22"",""done"":false,""dependsOn"":[""m_mslrvh7hjau1e""],""assignees"":[],""progress"":0},{""id"":""msoltaa1itx48"",""title"":""Gehen"",""start"":""2026-09-23"",""end"":""2026-09-25"",""done"":false,""progress"":0,""dependsOn"":[""mslw4l3qp1v18""],""assignees"":[""p_ws""]}]}";"{""LPH 1"":{""Planung"":0.8},""LPH 2"":{""Planung"":0.8},""LPH 3"":{""Planung"":0.8},""LPH 4"":{""Planung"":0.8},""LPH 5"":{""Planung"":0.8}}";"[""LPH 3""]" 21-4001;Feuerwehrgeräteh. Rodenkirchen;- KNX muß noch programmiert werden. <font>Abnahme 27.07</font> <div><font><b>- Fertigstellung Ende Juli</b></font><div><font><b>- Abnahmen laufen bis Ende KW31</b></font></div><div><font><b>- Blackbuilding Test 26.07</b></font></div></div>;X;;X;;;;X;X;;;;;;X;;;;;100%;100%;100%;;100%;100%;100%;;;Aktive Projekte;1;"{""start"":"""",""end"":"""",""milestones"":[{""id"":""mslrvh7hjau1e"",""name"":""Übergabe"",""date"":""2026-09-12"",""dependsOn"":[""mslv6h76cmx8k""],""position"":""after:mslv6h76cmx8k""}],""people"":[""p_ap"",""p_ak"",""p_sk"",""p_sg""],""progress"":""0"",""todos"":[{""id"":""msluufbp114g1"",""title"":""Test"",""start"":""2026-08-10"",""end"":""2026-08-14"",""done"":false,""dependsOn"":[],""assignees"":[""p_ap"",""p_ed""],""progress"":0},{""id"":""mslv6h76cmx8k"",""title"":""Test 2"",""start"":""2026-08-24"",""end"":""2026-09-04"",""done"":false,""dependsOn"":[""msluufbp114g1""],""assignees"":[""p_ap""],""progress"":41},{""id"":""mslw4l3qp1v18"",""title"":""Test 3"",""start"":""2026-09-14"",""end"":""2026-09-22"",""done"":false,""dependsOn"":[""m_mslrvh7hjau1e""],""assignees"":[],""progress"":0},{""id"":""msoltaa1itx48"",""title"":""Gehen"",""start"":""2026-09-23"",""end"":""2026-09-25"",""done"":false,""progress"":0,""dependsOn"":[""mslw4l3qp1v18""],""assignees"":[""p_ws""]}]}";"{""LPH 1"":{""Planung"":0.8},""LPH 2"":{""Planung"":0.8},""LPH 3"":{""Planung"":0.8},""LPH 4"":{""Planung"":0.8},""LPH 5"":{""Planung"":0.8}}";"[""LPH 5""]"
22-4027;Sporthalle Soest;- SV und VOB Abnahme erfolgt.<div>- Nachprüfung VOB 24.07</div><div>- Keine Infos über Nachprüfung</div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;3;;;"[""LPH 5""]" 22-4027;Sporthalle Soest;- SV und VOB Abnahme erfolgt.<div>- Nachprüfung VOB 24.07</div><div>- Keine Infos über Nachprüfung</div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;3;;;"[""LPH 5""]"
22-4056;Gymnasium Hemer;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;4;;; 22-4056;Gymnasium Hemer;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;4;;;
22-4056-1;Gymnasium Hemer (Deckensanierung);- Beginn der Arbeiten vorraussichtlich KW 30 (KW 32 Kickoff);;;;;;;;;;;;;;;;;;;;;100%;;100%;100%;;;;Aktive Projekte;5;;; 22-4056-1;Gymnasium Hemer (Deckensanierung);- Beginn der Arbeiten vorraussichtlich KW 30 (KW 32 Kickoff);;;;;;;;;;;;;;;;;;;;;100%;;100%;100%;;;;Aktive Projekte;5;;;
23-4042;GWK Neufelder Straße;- Nachweis für E-30 Leitungen notwendig. Rechnungen wurden nicht anerkannt. <div><font>- 22.07 Aufschaltung BMA und Amok</font></div><div><font>- KW 31 VOB Abnahme</font></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;7;;; 23-4042;GWK Neufelder Straße;- Nachweis für E-30 Leitungen notwendig. Rechnungen wurden nicht anerkannt. <div><font>- 22.07 Aufschaltung BMA und Amok</font></div><div><font>- KW 31 VOB Abnahme</font></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;8;;;
23-4070;Umbau Bürogebäude NEW, MV10;"- neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&amp;SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;8;;; 23-4070;Umbau Bürogebäude NEW, MV10;"- neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&amp;SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;9;;;
23-4072;S-K OGS-Kaldenkirchen Schulanbau;- Feininstallation beginnt, Fertigstellung voraussichtlich <font>01.08</font>;;;;;;;;;;;;;;;;;;;;;;;;;;70%;;Aktive Projekte;9;;; 23-4072;S-K OGS-Kaldenkirchen Schulanbau;- Feininstallation beginnt, Fertigstellung voraussichtlich <font>01.08</font>;;;;;;;;;;;;;;;;;;;;;;;;;;70%;;Aktive Projekte;10;;;
24-4049;BMA Remscheid;"- Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 &amp; 16.07: nicht abgenommen</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;11;;; 24-4049;BMA Remscheid;"- Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 &amp; 16.07: nicht abgenommen</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;12;;;
24-4069;S-K Erw. GS Blankenheim Neubau;- nur backup;;;;;;;;;;;;;;;;;;;;;;;100%;100%;;;;Kleine Projekte;0;;; 24-4069;S-K Erw. GS Blankenheim Neubau;- nur backup;;;;;;;;;;;;;;;;;;;;;;;100%;100%;;;;Kleine Projekte;0;;;
24-4096;Umbau Wohngebäude Kirchhoff;- Position HA wird von uns bestimmt. <div>- Notwendiges Leerrohr für Leuchten im Vordach ist zu klären.<div><font>- Abgabe Kostenberechnung bis Ende KW29</font></div><div><font>- LV Fertig bis Ende KW 32 </font> </div></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;85%;0%;;;Aktive Projekte;12;;; 24-4096;Umbau Wohngebäude Kirchhoff;- Position HA wird von uns bestimmt. <div>- Notwendiges Leerrohr für Leuchten im Vordach ist zu klären.<div><font>- Abgabe Kostenberechnung bis Ende KW29</font></div><div><font>- LV Fertig bis Ende KW 32 </font> </div></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;85%;0%;;;Aktive Projekte;13;;;
25-4003;Märkische Bank Boele;- Feininstallation<div>- Ab KW30 Einbringung Tresore</div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;80%;;Aktive Projekte;13;;; 25-4003;Märkische Bank Boele;- Feininstallation<div>- Ab KW30 Einbringung Tresore</div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;80%;;Aktive Projekte;14;;;
25-4028;TenBrinke, Amprion, Wietmarschen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;14;;; 25-4028;TenBrinke, Amprion, Wietmarschen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;15;;;
25-4028-1;TenBrinke, Amprion, Meerbusch;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;15;;; 25-4028-1;TenBrinke, Amprion, Meerbusch;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;16;;;
25-4028-2;TenBrinke, Amprion, Gelsenkirchen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;16;;; 25-4028-2;TenBrinke, Amprion, Gelsenkirchen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;17;;;
26-4018;TUI Immobilien GmbH Salzgitter;;;;;;;;;;;;;;;;;;;;50%;;;;;;;;;Aktive Projekte;17;;; 26-4018;TUI Immobilien GmbH Salzgitter;;;;;;;;;;;;;;;;;;;;50%;;;;;;;;;Aktive Projekte;18;;;
26-4023;HA Rettungsw. Nahmer Gasst. - Los 1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;0;;; 26-4023;HA Rettungsw. Nahmer Gasst. - Los 1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;0;;;
26-4024;HA Rettungsw. Dahl Am Obergr. - Los 2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;1;;; 26-4024;HA Rettungsw. Dahl Am Obergr. - Los 2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;1;;;
26-4025;HA, SeePark Hengstey;- LPH2 erfolgt, weitere Arbeiten folgen.;;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;18;;; 26-4025;HA, SeePark Hengstey;- LPH2 erfolgt, weitere Arbeiten folgen.;;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;19;;;
26-4026;Stadt Hemer, Anbau OGS Oesetalschule;"<font color=""#1a1a1a"">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font>";;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;19;;; 26-4026;Stadt Hemer, Anbau OGS Oesetalschule;"<font color=""#1a1a1a"">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font>";;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;20;;;
26-4046;AXA Außenbeleuchtung;- Warten auf Unterlagen (Grundlagen & Vorplanung);;;;;;;;;;;;;;;;;;;10%;;;;;;;;;Aktive Projekte;20;;; 26-4046;AXA Außenbeleuchtung;- Warten auf Unterlagen (Grundlagen & Vorplanung);;;;;;;;;;;;;;;;;;;10%;;;;;;;;;Aktive Projekte;21;;;
;KFW Köln/Bonn Sicherheitskonzept;- Auftrag erhalten;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;2;;; ;KFW Köln/Bonn Sicherheitskonzept;- Auftrag erhalten;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;2;;;
;Swisstal;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;3;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";; ;Swisstal;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;3;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";;
;Plancodierung/Benennung;- Von Rieke erstellt. Info folgt ? KW31?;;;;;;;;;;;;;;;;;;;;;;;;;;;;Allgemein;0;;; ;Plancodierung/Benennung;- Von Rieke erstellt. Info folgt ? KW31?;;;;;;;;;;;;;;;;;;;;;;;;;;;;Allgemein;0;;;
1 WOCHE KW 33 / 2026 2026-08-09
4 ANWESENHEIT
5 18-4007 Janusz-Korczak-Gesamtsch. Bottrop - KW 35 NW Raum Planung <div>- KW 44 Möbel </div> X
6 21-4030 NSHV Köln Immobilienprüfung in Hof - Thorsten W. klärt und übernimmt Schriftverkehr <div>- LV liegt zur Sichtung bei T.W.</div> X
7 22-4066 DSW 21, Umbau E-Mobilität - Elt-Montagen ab <font><b>KW 33</b></font>.
8 24-4043 S-K ODL-Heim Sanierung Krefeld - Vorraussichtlich LV-Abgabe XX.XX<div>- LPH5 mündlich freigegeben <div>- Neuer EG Grundriss mit diversen Änderungen kommt noch von Architekten</div></div>
9 21-4001 Feuerwehrgeräteh. Rodenkirchen - KNX muß noch programmiert werden. <font>Abnahme 27.07</font> <div><font><b>- Fertigstellung Ende Juli</b></font><div><font><b>- Abnahmen laufen bis Ende KW31</b></font></div><div><font><b>- Blackbuilding Test 26.07</b></font></div></div> X
10 22-4027 Sporthalle Soest - SV und VOB Abnahme erfolgt.<div>- Nachprüfung VOB 24.07</div><div>- Keine Infos über Nachprüfung</div> X
11 22-4056 Gymnasium Hemer
12 22-4056-1 Gymnasium Hemer (Deckensanierung) - Beginn der Arbeiten vorraussichtlich KW 30 (KW 32 Kickoff)
13 23-4042 GWK Neufelder Straße - Nachweis für E-30 Leitungen notwendig. Rechnungen wurden nicht anerkannt. <div><font>- 22.07 Aufschaltung BMA und Amok</font></div><div><font>- KW 31 VOB Abnahme</font></div>
14 23-4070 Umbau Bürogebäude NEW, MV10 - neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&amp;SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div>
15 23-4072 S-K OGS-Kaldenkirchen Schulanbau - Feininstallation beginnt, Fertigstellung voraussichtlich <font>01.08</font>
16 24-4049 BMA Remscheid - Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 &amp; 16.07: nicht abgenommen</div>
17 24-4069 S-K Erw. GS Blankenheim Neubau - nur backup
18 24-4096 Umbau Wohngebäude Kirchhoff - Position HA wird von uns bestimmt. <div>- Notwendiges Leerrohr für Leuchten im Vordach ist zu klären.<div><font>- Abgabe Kostenberechnung bis Ende KW29</font></div><div><font>- LV Fertig bis Ende KW 32 </font> </div></div>
19 25-4003 Märkische Bank Boele - Feininstallation<div>- Ab KW30 Einbringung Tresore</div>
20 25-4028 TenBrinke, Amprion, Wietmarschen
21 25-4028-1 TenBrinke, Amprion, Meerbusch
22 25-4028-2 TenBrinke, Amprion, Gelsenkirchen
23 26-4018 TUI Immobilien GmbH Salzgitter
24 26-4023 HA Rettungsw. Nahmer Gasst. - Los 1
25 26-4024 HA Rettungsw. Dahl Am Obergr. - Los 2
26 26-4025 HA, SeePark Hengstey - LPH2 erfolgt, weitere Arbeiten folgen.
27 26-4026 Stadt Hemer, Anbau OGS Oesetalschule <font color="#1a1a1a">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font>
28 26-4046 AXA Außenbeleuchtung - Warten auf Unterlagen (Grundlagen & Vorplanung)
29 KFW Köln/Bonn Sicherheitskonzept - Auftrag erhalten
30 Swisstal
31 Plancodierung/Benennung - Von Rieke erstellt. Info folgt ? KW31?