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:
erikandClaude Sonnet 5 committed 2026-10-02 15:14:47 +02:00
1 parent bc30f2900b
commit db63a29365
47 files changed
+16486 -3768

No files matched your search

+186 -116
View File
@@ -1,36 +1,47 @@
// -----------------------------
// Projektübersicht — Daten laden, Speichern, Abteilung wechseln
// Projektübersicht — Daten laden, Speichern
// -----------------------------
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungen
// beim Start, speichert den kompletten Datenbestand (alle Wochen + globale
// Personenliste) und verwaltet den Wechsel zwischen Abteilungen (ELT/HLS/ARCH).
// Die Daten liegen serverseitig in einer SQLite-Datenbank und werden als
// strukturierte JSON-Objekte über die API ausgetauscht (keine CSV-Strings).
// Sie wird von `Test.html` nach `js/date-picker.js` und `js/server.js` geladen und stellt
// die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
// `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`,
// `js/board/board.js`, `js/people/people.js`, `js/projects/projects.js` und `js/load/load.js`; das Speichern
// ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced.
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt beim Start alle
// (firmenweit globalen, nicht mehr abteilungsgetrennten) Sitzungen und speichert
// den kompletten Datenbestand (alle Wochen + globale Personenliste). Die Daten
// liegen serverseitig in einer SQLite-Datenbank und werden als strukturierte
// JSON-Objekte über die API ausgetauscht (keine CSV-Strings). Sie wird von
// `App.html` nach `js/date-picker.js` und `js/server.js` geladen und stellt
// die globalen Funktionen `save()`, `syncAllToServer()`, `load()`, `seed()`
// und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`, `js/board/board.js`,
// `js/people/people.js`, `js/projects/projects.js` und `js/load/load.js`; das
// Speichern ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms
// debounced.
// save: plant einen (debounced) Sync. Standardmäßig wird die aktuell gewählte
// Woche als geändert markiert, da sich die meisten Bearbeitungen auf die
// aktuelle Sitzung beziehen. Optionen:
// { week:false } – aktuelle Woche NICHT markieren (z. B. reine Personen-Aktionen)
// { people:true } – zusätzlich die globale Personenliste als geändert markieren
function save(opts = {}){
if(opts.week !== false){
const w = currentWeek();
if(w) dirtyWeeks.add(w.label);
if(w) markWeekDirty(w.label);
}
if(opts.people) dirtyPeople = true;
scheduleSessionSync();
}
// Dirty-Tracking: merkt sich, welche Wochen bzw. ob die globale Personenliste
// seit dem letzten Sync lokal geändert wurden. Gespeichert wird nur das, was
// sich wirklich geändert hat (statt wie früher immer alle Wochen).
// Dirty-Tracking: merkt sich, welche Wochen seit dem letzten Sync lokal
// geändert wurden. Gespeichert wird nur das, was sich wirklich geändert hat
// (statt wie früher immer alle Wochen). Die globale Personenliste hat seit der
// Ablösung von people/person_roles/person_absences keinen eigenen Schreibpfad
// mehr (Projektbeteiligte werden über die Benutzerverwaltung gepflegt).
let dirtyWeeks = new Set();
let dirtyPeople = false;
// weekChangeCounter: zählt je Woche die Änderungen mit. syncAllToServer merkt
// sich den Stand beim Absenden und räumt die Dirty-Markierung nur auf, wenn er
// sich bis zur Antwort nicht verändert hat. Ohne diesen Zähler verlor eine
// Änderung, die WÄHREND eines laufenden Speichervorgangs entsteht, ihre
// Markierung: Der eingereihte Nachlauf-Sync fand dann nichts mehr zu senden,
// und das Getippte stand nur noch im Browser.
const weekChangeCounter = new Map();
// syncProblemTimer: Wiederholungsversuch nach einem fehlgeschlagenen Speichern.
let syncProblemTimer = null;
// In-Flight-Schutz für syncAllToServer: läuft bereits ein Sync, wird kein
// zweiter gestartet (sonst senden zwei Syncs dieselbe Woche mit gleicher
@@ -39,6 +50,37 @@ let dirtyPeople = false;
let syncInFlight = false;
let syncQueued = false;
// showSyncProblem: blendet unten rechts ein Band ein, wenn das Speichern
// fehlschlägt. Vorher stand der Fehler ausschließlich in der Browser-Konsole —
// der Nutzer arbeitete ahnungslos weiter, während seit Stunden nichts mehr
// gespeichert wurde. Parameter: message (Text) oder null zum Ausblenden.
function showSyncProblem(message){
let el = document.getElementById('syncProblemBar');
if(!message){
if(el) el.style.display = 'none';
return;
}
if(!el){
el = document.createElement('div');
el.id = 'syncProblemBar';
el.className = 'sync-problem-bar';
document.body.appendChild(el);
}
el.textContent = message;
el.style.display = '';
}
// scheduleSyncRetry: versucht ein fehlgeschlagenes Speichern nach 20 Sekunden
// erneut. Ohne das blieb eine Woche nach einem Netzaussetzer dauerhaft
// ungespeichert, bis der Nutzer zufällig etwas anderes bearbeitete.
function scheduleSyncRetry(){
if(syncProblemTimer || dirtyWeeks.size === 0) return;
syncProblemTimer = setTimeout(() => {
syncProblemTimer = null;
if(dirtyWeeks.size > 0) syncAllToServer();
}, 20000);
}
// scheduleSessionSync: plant den Speichervorgang für alle Wochen und Personen
// über einen 500-ms-Timer (Debounce). Bei erneutem Aufruf wird ein laufender
// Timer zurückgesetzt, damit aufeinanderfolgende Änderungen in einem Sync enden.
@@ -48,22 +90,28 @@ function scheduleSessionSync(){
}
// syncAllToServer: schreibt nur die seit dem letzten Sync geänderten Wochen
// plus – falls markiert – die globale Personenliste zum Server. Konflikte
// (409) auf einer Woche werden über handleWeekConflict() gemeldet. Fehlerhaft
// gespeicherte Wochen bleiben dirty, damit der nächste Sync sie erneut
// versucht; nur erfolgreich gespeicherte werden aus dem Dirty-Satz entfernt.
// zum Server. Konflikte (409) auf einer Woche werden über
// handleWeekConflict() gemeldet. Fehlerhaft gespeicherte Wochen bleiben dirty,
// damit der nächste Sync sie erneut versucht; nur erfolgreich gespeicherte
// werden aus dem Dirty-Satz entfernt.
function syncAllToServer(){
if(syncInFlight){ syncQueued = true; return; }
syncInFlight = true;
// Zuerst globale Personenliste speichern, falls geändert
const jobs = [];
if(dirtyPeople) jobs.push(saveGlobalPeopleToServer());
const weekLabels = Array.from(dirtyWeeks);
let problem = '';
weekLabels.forEach(label => {
const w = db.weeks.find(x => x.label === label);
// Die Planung-Sandbox steckt bewusst nicht in db.weeks (siehe
// db.sandboxWeek, oben in load()) — ohne diesen Zweig würde jede
// Sandbox-Änderung hier lautlos verworfen (dirtyWeeks.delete ohne
// jemals zu speichern), weil db.weeks.find() sie nie findet.
const w = label === SANDBOX_WEEK_LABEL ? db.sandboxWeek : db.weeks.find(x => x.label === label);
if(!w){ dirtyWeeks.delete(label); return; }
// Stand der Änderungen zum Zeitpunkt des Absendens merken (siehe
// weekChangeCounter): Nur wenn er bei der Antwort unverändert ist, sind
// wirklich alle Änderungen dieser Woche beim Server angekommen.
const sentGeneration = weekChangeCounter.get(label) || 0;
jobs.push(sendWeekToServer(w)
.then(res => {
if(res && res.ok){
@@ -71,8 +119,9 @@ function syncAllToServer(){
// Lokale Revisions-Basis aktualisieren, damit spätere eigene Saves
// nicht fälschlich als Konflikt gemeldet werden.
if(res.updatedAt) w.updatedAt = res.updatedAt;
dirtyWeeks.delete(label);
if((weekChangeCounter.get(label) || 0) === sentGeneration) dirtyWeeks.delete(label);
} else {
problem = 'Sitzung "' + w.label + '" konnte nicht gespeichert werden.';
console.error('Sitzung konnte nicht gespeichert werden:', w.label, res && res.error ? res.error : 'unbekannter Fehler');
}
})
@@ -80,8 +129,15 @@ function syncAllToServer(){
if(err && err.conflict){
dirtyWeeks.delete(label);
handleWeekConflict(w, err);
} else if(err && err.status === 401){
// Sitzung am Server abgelaufen: Anmeldefenster zeigen, statt still
// weiterzuarbeiten und alles Weitere zu verlieren.
problem = 'Nicht mehr angemeldet — bitte neu anmelden, sonst gehen Änderungen verloren.';
if(typeof showLoginOverlay === 'function') showLoginOverlay();
console.error('Speichern abgelehnt (nicht angemeldet):', w.label);
} else {
console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err);
problem = 'Speichern fehlgeschlagen (' + (err && err.message ? err.message : 'unbekannt') + ') — neuer Versuch in 20 Sekunden.';
console.error('Sitzung konnte nicht gespeichert werden:', w.label, (err && err.message) || err);
// Label bleibt dirty -> der nächste Sync versucht es erneut.
}
}));
@@ -89,6 +145,8 @@ function syncAllToServer(){
Promise.all(jobs).then(() => {
syncInFlight = false;
showSyncProblem(problem || null);
if(problem) scheduleSyncRetry();
if(syncQueued){ syncQueued = false; syncAllToServer(); }
});
}
@@ -96,12 +154,9 @@ function syncAllToServer(){
// markWeekDirty: markiert eine bestimmte Woche (label) als geändert – für
// Aktionen, die Wochen AUFßER der aktuellen betreffen (z. B. Auto-Lock).
function markWeekDirty(label){
if(typeof label === 'string' && label) dirtyWeeks.add(label);
}
// markPeopleDirty: markiert die globale Personenliste als geändert.
function markPeopleDirty(){
dirtyPeople = true;
if(typeof label !== 'string' || !label) return;
dirtyWeeks.add(label);
weekChangeCounter.set(label, (weekChangeCounter.get(label) || 0) + 1);
}
// handleWeekConflict: wird aufgerufen, wenn der Server eine zwischenzeitlich
@@ -117,16 +172,14 @@ function handleWeekConflict(week, err){
else load();
}
// reloadWeekFromServer: lädt die aktuelle Abteilung neu und ersetzt NUR die
// reloadWeekFromServer: lädt alle Sitzungen neu und ersetzt NUR die
// übergebene Woche (bzw. entfernt sie, falls sie am Server gelöscht wurde).
// Der Dirty-Zustand der konfliktbehafteten Woche wurde bereits von
// syncAllToServer entfernt; alle anderen Dirty-Änderungen bleiben erhalten.
function reloadWeekFromServer(label){
const requestedDepartment = currentDepartment;
fetch(baseUrl() + '/load-all' + departmentParam())
fetch(baseUrl() + '/load-all')
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => {
if(currentDepartment !== requestedDepartment) return;
const fresh = (Array.isArray(data.weeks) ? data.weeks : [])
.find(w => w && w.label === label);
const idx = db.weeks.findIndex(w => w.label === label);
@@ -149,93 +202,102 @@ function reloadWeekFromServer(label){
showTab(currentTab);
})
.catch(err => {
// KEIN load() als Rückfall: Das leert dirtyWeeks und ersetzt alle Wochen,
// also gingen auch die noch ungespeicherten Änderungen an ANDEREN
// Sitzungen verloren — genau das, was diese Funktion vermeiden soll.
console.error('Neu laden der Sitzung fehlgeschlagen:', err);
load();
showSyncProblem('Die Sitzung "' + label + '" konnte nicht neu geladen werden. Bitte die Seite neu laden.');
});
}
// saveGlobalPeopleToServer: speichert die globale Personenliste der aktuellen
// Abteilung über `sendPeopleToServer()` (JSON-Array). Auch eine leere Liste
// wird gespeichert, damit das Löschen der letzten Person erhalten bleibt. Bei
// Erfolg wird der Dirty-Status für Personen zurückgesetzt, bei Fehler bleibt
// er erhalten (Retry beim nächsten Sync). Rückgabe: Promise.
function saveGlobalPeopleToServer(){
const people = db.globalPeople[currentDepartment] || [];
return sendPeopleToServer(people)
.then(res => {
if(res && res.ok){
dirtyPeople = false;
console.log('Globale Personenliste gespeichert');
} else {
console.error('Globale Personenliste konnte nicht gespeichert werden:', res && res.error ? res.error : 'unbekannter Fehler');
}
// load: liest alle Sitzungen und die globale Personenliste über `/load-all`
// als JSON vom Server und baut daraus `db.weeks` auf. Sortiert nach
// `weekSortKey()`, wendet `applyAutoLock()` an und rendert die Ansicht. Kein
// Parameter, keine Rückgabe; bei Fehlern oder fehlenden Daten werden
// Beispieldaten über `seed()` geladen.
// loadAppSettings: holt die firmenweiten Einstellungen (GET /app-settings)
// und die Feiertage (GET /holidays) und legt sie in APP_SETTINGS/HOLIDAYS ab
// (js/globals.js). Beides wird zum RECHNEN gebraucht — Auslastung, Ampel,
// Terminplan aus dem Honorar —, deshalb sind die Routen für jedes angemeldete
// Konto lesbar; nur das Ändern ist Administratoren vorbehalten.
//
// Scheitert der Abruf, bleiben die Vorgaben aus js/globals.js stehen. Das ist
// Absicht: Die Anwendung rechnet dann wie vor dem Ausbau der Einstellungen
// weiter, statt mit leeren Werten falsche Zahlen zu zeigen.
// Rückgabe: Promise (wird nie abgelehnt).
function loadAppSettings(){
const settings = fetch(baseUrl() + '/app-settings')
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
.then(data => { if(data && data.settings) APP_SETTINGS = Object.assign({}, APP_SETTINGS, data.settings); })
.catch(err => { console.warn('Firmenweite Einstellungen konnten nicht geladen werden, es gelten die Vorgabewerte:', err); });
const holidays = fetch(baseUrl() + '/holidays')
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
.then(data => {
const list = Array.isArray(data.holidays) ? data.holidays : [];
HOLIDAYS = new Set(list.map(h => h.date));
HOLIDAY_NAMES = {};
list.forEach(h => { HOLIDAY_NAMES[h.date] = h.name || ''; });
// Die Bundesländer liefert dieselbe Route mit (siehe server.js); die
// Pflege-Maske im Admin-Bereich baut daraus ihre Auswahl.
if(data.states) GERMAN_STATES = data.states;
})
.catch(err => {
console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err);
// dirtyPeople bleibt true -> der nächste Sync versucht es erneut.
});
.catch(err => { console.warn('Feiertage konnten nicht geladen werden:', err); });
// Honorar-Anteile je Leistungsphase: Die Auslastung rechnet den offenen
// Wert einer Phase in Stunden um und braucht dafür deren Anteil am
// Gesamthonorar. Zusammen mit den übrigen Vorgaben geladen, also VOR
// /load-all und damit vor dem ersten renderAll().
const honorar = fetch(baseUrl() + '/honorar-config')
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
.then(data => { HONORAR_ANTEILE = (data && data.anteile) || {}; })
.catch(err => { console.warn('Honorar-Anteile konnten nicht geladen werden, die Auslastung weist sie als fehlend aus:', err); });
return Promise.all([settings, holidays, honorar]);
}
// switchDepartment: wechselt die aktive Abteilung (z.B. "ELT") und lädt deren
// Daten neu. Parameter `dept` ist der Abteilungsschlüssel aus `DEPARTMENTS`.
// Aktualisiert Tab-Markierung und Seitentitel und ruft anschließend `load()`.
// 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
// Daten der falschen Abteilung schreibt.
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
dirtyWeeks.clear();
dirtyPeople = false;
currentDepartment = dept;
saveUiState();
updateRouteUrl(dept, currentTab);
// UI-Status der Abteilungs-Tabs aktualisieren
document.querySelectorAll('.dept-tab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.dept === dept);
});
// Seitentitel anpassen
const info = DEPARTMENTS[dept];
document.title = 'Projektübersicht ' + info.label;
const titleEl = document.getElementById('pageTitle');
if(titleEl) titleEl.textContent = info.icon + ' Projektübersicht ' + info.label;
// Datenbank leeren und neu laden
db = { weeks: [], currentWeek: null, globalPeople: db.globalPeople || {} };
load();
}
// load: liest alle Sitzungen und die globale Personenliste der aktuellen
// Abteilung über `/load-all` als JSON vom Server und baut daraus `db.weeks`
// auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()` an und rendert
// die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder fehlenden
// Daten werden Beispieldaten über `seed()` geladen.
function load(){
// load: lädt die Sitzungsdaten vom Server neu. Parameter: silent (optional) —
// true für eine stille Hintergrund-Aktualisierung (js/wand/wand.js, alle 60s):
// ein Fehlschlag wird nur geloggt statt per alert() gemeldet, und der
// bisherige Datenstand bleibt stehen statt geleert zu werden. Ohne diesen
// Zweig stapelten sich auf einem unbeaufsichtigten Touch-Display bei jedem
// Fehlschlag blockierende alert()-Dialoge übereinander.
function load(silent){
// Nach einem (Neu-)Load sind die lokalen Daten serverseitig aktuell:
// laufenden Debounce abbrechen und Dirty-Zustand verwerfen.
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
dirtyWeeks.clear();
dirtyPeople = false;
if(!window.fetch){
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
seed(); return;
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';
console.log('Loading from', url);
fetch(url)
// Erst die firmenweiten Einstellungen und Feiertage, dann die Sitzungen:
// renderAll() am Ende rechnet bereits damit (Ampel-Schwellen, Balkenskala,
// Aufwandsvorlage, Arbeitstage). loadAppSettings() lehnt nie ab, ein
// Fehlschlag lässt also nur die Vorgabewerte stehen.
loadAppSettings().then(() => 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] = Array.isArray(data.people) ? data.people : [];
// Globale Personenliste laden (flaches Array, keine Abteilungstrennung)
db.globalPeople = Array.isArray(data.people) ? data.people : [];
// db.sandboxWeek: die Planung-Sandbox (SANDBOX_WEEK_LABEL) — bewusst
// NICHT Teil von db.weeks (siehe /load-all, server.js), sondern ein
// eigenes, synthetisches Wochen-ähnliches Objekt, hier und nur hier
// aufgebaut. Damit sehen renderBoard()/berichtProjekte()/
// berechneAuslastung()/die Sitzungs-PDFs/Admin→Sitzungen sie nie —
// sie iterieren alle ausschließlich db.weeks. people wird aus der
// globalen Personenliste abgeleitet (dieselbe Form wie week.people),
// damit die Beteiligten-Matrix im Projektbereich auch für einen
// Sandbox-Plan Kandidaten zur Auswahl hat.
db.sandboxWeek = {
id: 'sandbox', label: SANDBOX_WEEK_LABEL, date: '',
people: db.globalPeople.map(p => ({ id: p.id, name: p.name, fullName: p.fullName })),
projects: Array.isArray(data.sandboxPlans) ? data.sandboxPlans : [],
attendance: {}, locked: false, updatedAt: data.sandboxUpdatedAt || ''
};
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
.filter(w => w && typeof w.label === 'string')
@@ -262,23 +324,29 @@ function load(){
}
// Geschützte Tabs ('projects'/'people') dürfen nur mit Passwort angezeigt
// werden. showTab() öffnet dafür automatisch das Passwort-Modal – auch
// wenn der Tab per Deep-Link-URL (z. B. "/ELT/Projekte") gewählt wurde.
// wenn der Tab per Deep-Link-URL (z. B. "/Projekte") gewählt wurde.
renderAll();
showTab(currentTab);
})
.catch(err => {
if(currentDepartment !== requestedDepartment) return;
console.error('Laden vom Server fehlgeschlagen:', err);
if(silent) return;
const serverUrl = baseUrl();
const msg = 'Server unter ' + serverUrl + ' nicht erreichbar.\n\n' +
'Mögliche Ursachen:\n' +
'- Der Server wurde nicht gestartet\n' +
'- Der Server läuft auf einem anderen Port (Standard: 3003)\n' +
'- Firewall/Antivirus blockiert ' + serverUrl + '\n\n' +
'Es werden jetzt die Beispieldaten angezeigt.';
'Es werden keine Daten angezeigt. Bitte die Seite neu laden, sobald der Server wieder läuft.';
alert(msg);
seed(); renderAll();
});
// BEWUSST KEIN seed(): Die Beispieldaten enthalten eine Woche "KW 30 / 2026"
// und wurden früher über save() an den Server geschickt, sobald die
// Verbindung zurückkam — sie überschrieben damit die echte gleichnamige
// Sitzung. Bei einem Ladefehler bleibt der Bestand jetzt einfach leer.
db.weeks = [];
db.currentWeek = null;
renderAll();
}));
}
// seed: initialisiert Beispiel-Daten (Personen + eine Woche mit Projekten).
@@ -299,7 +367,7 @@ function seed(){
{id:personIdForName('MK'), name:"MK", fullName:"Martin Kluge", role:["Bauüberwachung"]},
{id:personIdForName('HR'), name:"HR", fullName:"Hartwig Rauch", role:["Bauüberwachung"]},
];
db.globalPeople[currentDepartment] = JSON.parse(JSON.stringify(people));
db.globalPeople = JSON.parse(JSON.stringify(people));
const wid = uid();
db.weeks = [{
@@ -315,7 +383,9 @@ function seed(){
save();
}
// demoProj: Helfer zum Erzeugen eines Projekt-Objekts (für seed/Tests)
// demoProj: Helfer zum Erzeugen eines Projekt-Objekts (für seed/Tests).
// projectUid: dauerhafte, wochenunabhängige Identität (siehe saveNewProj()
// in js/load/load.js für die Erklärung, warum sie sich von id unterscheidet).
function demoProj(nr,name,group,status,lph){
return {id:uid(), nr, name, group, status, lph: {...lph}, assign:{}, plan: null, aufwandProLP: undefined, aktuelleLP: undefined};
return {id:uid(), projectUid: uid(), nr, name, group, status, lph: {...lph}, assign:{}, plan: null, aufwandProLP: undefined, aktuelleLP: undefined};
}