Kleine fehler beseitigt
This commit is contained in:
1 parent
57e201b8fc
commit
115f0f5671
15 files changed
+215
-110
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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 = '';
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
const row = buildMilestoneRow(m, mIdx);
|
||||
insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling);
|
||||
insertAfter = row;
|
||||
insertedMilestoneIds.add(m.id);
|
||||
});
|
||||
|
||||
// Aufgabenzeile
|
||||
|
||||
+14
-11
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,19 +118,32 @@ 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;
|
||||
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 {
|
||||
// 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
@@ -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
|
||||
|
||||
@@ -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' +
|
||||
|
||||
@@ -47,14 +47,20 @@ function confirmPassword(){
|
||||
const pwdHash = CryptoJS.SHA256(pwd).toString();
|
||||
|
||||
if(pwdHash === PROTECTED_PASSWORD_HASH){
|
||||
// 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;
|
||||
document.getElementById('passwordModal').style.display='none';
|
||||
setTimeout(() => {
|
||||
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='';
|
||||
|
||||
@@ -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'.
|
||||
|
||||
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
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;;;
|
||||
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;;;
|
||||
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;;;
|
||||
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""]"
|
||||
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;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 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-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-4070;Umbau Bürogebäude NEW, MV10;"- neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;8;;;
|
||||
23-4072;S-K OGS-Kaldenkirchen Schulanbau;- Feininstallation beginnt, Fertigstellung voraussichtlich <font>01.08</font>;;;;;;;;;;;;;;;;;;;;;;;;;;70%;;Aktive Projekte;9;;;
|
||||
24-4049;BMA Remscheid;"- Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 & 16.07: nicht abgenommen</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;11;;;
|
||||
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&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;10;;;
|
||||
24-4049;BMA Remscheid;"- Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 & 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-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;;;
|
||||
25-4003;Märkische Bank Boele;- Feininstallation<div>- Ab KW30 Einbringung Tresore</div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;80%;;Aktive Projekte;13;;;
|
||||
25-4028;TenBrinke, Amprion, Wietmarschen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;14;;;
|
||||
25-4028-1;TenBrinke, Amprion, Meerbusch;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;15;;;
|
||||
25-4028-2;TenBrinke, Amprion, Gelsenkirchen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;16;;;
|
||||
26-4018;TUI Immobilien GmbH Salzgitter;;;;;;;;;;;;;;;;;;;;50%;;;;;;;;;Aktive Projekte;17;;;
|
||||
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;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;16;;;
|
||||
25-4028-2;TenBrinke, Amprion, Gelsenkirchen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;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-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-4026;Stadt Hemer, Anbau OGS Oesetalschule;"<font color=""#1a1a1a"">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font>";;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;19;;;
|
||||
26-4046;AXA Außenbeleuchtung;- Warten auf Unterlagen (Grundlagen & Vorplanung);;;;;;;;;;;;;;;;;;;10%;;;;;;;;;Aktive Projekte;20;;;
|
||||
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;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;;;
|
||||
;Swisstal;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;3;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";;
|
||||
;Plancodierung/Benennung;- Von Rieke erstellt. Info folgt ? KW31?;;;;;;;;;;;;;;;;;;;;;;;;;;;;Allgemein;0;;;
|
||||
|
||||
|
Reference in new issue
Block a user