Wandtafel-Dashboard, Dateibereinigung und Auslastungs-Vereinfachung
Neue Wandtafel (Touch-Dashboard für Kiosk-Anzeige): Auslastung/Besprochen je Gewerk, Aufgaben-Kennzahlen, Ereignis-Feed nach Art gruppiert, vollständige Projektliste zum Scrollen, automatischer Rücksprung nach Untätigkeit mit sichtbarem Countdown. Eigene Kiosk-Rolle ohne Fachrolle und ohne Schreibrechte. Auslastung: Möglichkeit zum Wechsel der ausgewerteten Sitzung entfernt. Aufräumen nicht mehr benötigter Dateien, README auf aktuellen Stand gebracht. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bc30f2900b
commit
db63a29365
47 files changed
+16486
-3768
No files matched your search
+93
-27
@@ -32,8 +32,8 @@
|
||||
}
|
||||
|
||||
// onGanttTodoMouseDown: startet das Zeilen-Ziehen, wenn der Greif-Handler
|
||||
// (⋮⋮) einer TODO-/Meilenstein-Zeile gedrückt wird, und merkt sich Zeile
|
||||
// und Startposition in app.todoDrag.
|
||||
// (Icon "grip", .gantt-todo-drag) einer TODO-/Meilenstein-Zeile gedrückt
|
||||
// wird, und merkt sich Zeile und Startposition in app.todoDrag.
|
||||
function onGanttTodoMouseDown(e){
|
||||
if(app.isLocked()) return;
|
||||
const handle = e.target.closest('.gantt-todo-drag');
|
||||
@@ -113,7 +113,11 @@
|
||||
const plan = app.ensurePlanLocal();
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
|
||||
const todoIds = todos.map(t => t.id);
|
||||
// Nur terminierte Aufgaben (Start UND Ende gesetzt) haben überhaupt eine
|
||||
// eigene GANTT-Zeile (siehe plan-build-gantt.js) und damit einen
|
||||
// Ziehgriff — Aufgaben ohne Termin bleiben an ihrer Stelle
|
||||
// in plan.todos stehen, sie können hier gar nicht bewegt worden sein.
|
||||
const scheduledIds = new Set(todos.filter(t => t.start && t.end).map(t => t.id));
|
||||
|
||||
// Alle beweglichen Zeilen in aktueller DOM-Reihenfolge
|
||||
const rows = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR)];
|
||||
@@ -125,17 +129,19 @@
|
||||
// Position (before:/after:) sind eigene Zeilen. Meilensteine ohne Position
|
||||
// liegen in der Projektzeile und werden hier nicht umsortiert, deshalb nur
|
||||
// die positionierten zählen.
|
||||
if(todoRowIds.length !== todoIds.length) return;
|
||||
if(todoRowIds.length !== scheduledIds.size) return;
|
||||
const positionedIds = milestones.filter(m => m.position && /^(before|after):/.test(m.position)).map(m => m.id);
|
||||
if(milestoneRowIds.length !== positionedIds.length) return;
|
||||
|
||||
// TODOs neu ordnen
|
||||
const newTodos = [];
|
||||
todoRowIds.forEach(id => {
|
||||
const t = todos.find(x => x.id === id);
|
||||
if(t) newTodos.push(t);
|
||||
// TODOs neu ordnen: nur die Positionen der terminierten Aufgaben werden
|
||||
// gemäß der neuen DOM-Reihenfolge neu belegt (todoRowIds); Aufgaben ohne
|
||||
// Termin behalten exakt ihren bisherigen Platz in der Liste.
|
||||
let nextRowIdIdx = 0;
|
||||
plan.todos = todos.map(t => {
|
||||
if(!scheduledIds.has(t.id)) return t;
|
||||
const id = todoRowIds[nextRowIdIdx++];
|
||||
return todos.find(x => x.id === id) || t;
|
||||
});
|
||||
plan.todos = newTodos;
|
||||
|
||||
// Meilenstein-Positionen neu berechnen
|
||||
// Regeln:
|
||||
@@ -382,9 +388,12 @@
|
||||
const totalDays = daysBetween(rangeStart, rangeEnd) + 1;
|
||||
const dayOffset = Math.max(0, Math.min(totalDays - 1, Math.round(x / rect.width * totalDays)));
|
||||
const date = formatDateISO(addDays(rangeStart, dayOffset));
|
||||
openTodoModal(null);
|
||||
document.getElementById('todoEditStart').value = date;
|
||||
document.getElementById('todoEditEnd').value = date;
|
||||
// Über prefill statt nachträglich in die versteckten Felder schreiben:
|
||||
// openTodoModal() setzt damit auch die sichtbare Zeitraum-Anzeige, das
|
||||
// Dauer-Kennzeichen und den Vergleichsstand für "ungespeicherte
|
||||
// Änderungen" (sonst stand im Feld "Start – Ende wählen…", und Esc fragte
|
||||
// grundlos nach dem Verwerfen).
|
||||
openTodoModal(null, { start: date, end: date });
|
||||
}
|
||||
|
||||
// updateProjectDates: setzt Start und Ende des Projekt-Plans und speichert.
|
||||
@@ -415,23 +424,80 @@
|
||||
}
|
||||
}
|
||||
|
||||
// updateVacationDates: schreibt die verschobenen Urlaubsdaten sowohl in die
|
||||
// lokale Sitzungs-Kopie der Person als auch in die globale Personenliste
|
||||
// und speichert beides.
|
||||
// updateVacationDates: übernimmt die per Drag verschobenen Urlaubsdaten
|
||||
// sofort optimistisch in lokale Sitzungs-Kopie und globale Personenliste
|
||||
// (damit das GANTT unmittelbar danach die neue Position zeichnet), und
|
||||
// persistiert sie asynchron über die /users/:id/absences-Endpunkte — es
|
||||
// gibt seit der Ablösung von people/person_roles/person_absences keinen
|
||||
// /save-people-Schreibpfad und kein PATCH auf Abwesenheiten mehr, daher:
|
||||
// alte Abwesenheit löschen, neue mit den verschobenen Daten anlegen (dabei
|
||||
// vergibt der Server eine neue ID, die hier übernommen wird). Kein
|
||||
// Benutzerkonto für die Person gefunden -> klare Fehlermeldung statt
|
||||
// stillem Fehlschlag.
|
||||
function updateVacationDates(personId, absenceId, start, end){
|
||||
const localPerson = (app.week.people || []).find(p => p.id === personId);
|
||||
if(localPerson && localPerson.absences){
|
||||
const a = localPerson.absences.find(x => x.id === absenceId);
|
||||
if(a){ a.start = start; a.end = end; }
|
||||
}
|
||||
const globalPeople = db.globalPeople[app.department] || [];
|
||||
const localAbsence = localPerson && Array.isArray(localPerson.absences)
|
||||
? localPerson.absences.find(x => x.id === absenceId) : null;
|
||||
const globalPeople = db.globalPeople || [];
|
||||
const globalPerson = globalPeople.find(p => p.id === personId);
|
||||
if(globalPerson && globalPerson.absences){
|
||||
const a = globalPerson.absences.find(x => x.id === absenceId);
|
||||
if(a){ a.start = start; a.end = end; }
|
||||
}
|
||||
app.savePlan();
|
||||
app.saveGlobalPeople();
|
||||
const globalAbsence = globalPerson && Array.isArray(globalPerson.absences)
|
||||
? globalPerson.absences.find(x => x.id === absenceId) : null;
|
||||
if(!globalAbsence) return;
|
||||
const type = globalAbsence.type || '';
|
||||
const note = globalAbsence.note || '';
|
||||
// Ursprungstermine merken: Scheitert das Speichern (Netz, fehlende
|
||||
// Berechtigung), wird die optimistisch verschobene Anzeige wieder auf den
|
||||
// Server-Stand zurückgesetzt, statt etwas zu zeigen, das dort nicht gilt.
|
||||
const vorher = { start: globalAbsence.start, end: globalAbsence.end };
|
||||
const zuruecksetzen = () => {
|
||||
if(localAbsence){ localAbsence.start = vorher.start; localAbsence.end = vorher.end; }
|
||||
globalAbsence.start = vorher.start; globalAbsence.end = vorher.end;
|
||||
renderPlanGantt();
|
||||
};
|
||||
|
||||
if(localAbsence){ localAbsence.start = start; localAbsence.end = end; }
|
||||
globalAbsence.start = start; globalAbsence.end = end;
|
||||
|
||||
app.getUserIdForPersonId(personId).then(userId => {
|
||||
if(!userId){
|
||||
app.showToast('Für diese Person existiert kein Benutzerkonto – Abwesenheit konnte nicht gespeichert werden.');
|
||||
zuruecksetzen();
|
||||
return;
|
||||
}
|
||||
// Ergebnis des Löschens prüfen, BEVOR neu angelegt wird: Schlug das
|
||||
// Anlegen danach fehl (Netz, 400), war die alte Abwesenheit bereits weg
|
||||
// — die Anzeige zeigte sie aber weiter, und nach dem nächsten Neuladen
|
||||
// fehlte sie ersatzlos.
|
||||
return fetch(baseUrl() + '/users/' + userId + '/absences/' + encodeURIComponent(absenceId), { method: 'DELETE' })
|
||||
.then(r => {
|
||||
if(!r.ok){
|
||||
// 403 heißt: Der Eintrag stammt von jemand anderem (siehe
|
||||
// DELETE /users/:id/absences/:absenceId in server.js).
|
||||
return r.json().catch(() => ({})).then(json => {
|
||||
throw new Error((json && json.error) || ('Alte Abwesenheit konnte nicht entfernt werden (HTTP ' + r.status + ')'));
|
||||
});
|
||||
}
|
||||
return fetch(baseUrl() + '/users/' + userId + '/absences', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json; charset=utf-8'},
|
||||
body: JSON.stringify({ start, end, type, note })
|
||||
});
|
||||
})
|
||||
.then(r => r.json().then(json => ({ ok: r.ok, json }))).then(({ ok, json }) => {
|
||||
if(!ok || !json || !json.ok){
|
||||
app.showToast('Abwesenheit konnte nicht gespeichert werden: ' + ((json && json.error) || 'unbekannter Fehler')
|
||||
+ ' — bitte die Seite neu laden.');
|
||||
return;
|
||||
}
|
||||
globalAbsence.id = json.absence.id;
|
||||
if(localAbsence) localAbsence.id = json.absence.id;
|
||||
renderPlanGantt();
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Abwesenheit verschieben fehlgeschlagen:', err);
|
||||
app.showToast('Abwesenheit konnte nicht verschoben werden: ' + (err.message || err));
|
||||
zuruecksetzen();
|
||||
});
|
||||
}
|
||||
|
||||
// Drag-Funktionalität erst nach geladenem DOM registrieren
|
||||
|
||||
Reference in new issue
Block a user