DB Import und Export sowie Backup und kleinere Fehlerbeseitigung
This commit is contained in:
1 parent
91cc898ec7
commit
1c176ea621
24 files changed
+1081
-96
No files matched your search
@@ -77,6 +77,15 @@
|
||||
<button class="secondary" onclick="exportPDF()">PDF Export</button>
|
||||
<button class="secondary" onclick="startShowMode()">Show Mode</button>
|
||||
</div>
|
||||
|
||||
<hr class="menu-divider">
|
||||
|
||||
<div class="menu-section">
|
||||
<div class="menu-label">Backup</div>
|
||||
<button class="secondary" onclick="exportCSV()">CSV Export</button>
|
||||
<button class="secondary" onclick="downloadBackup()">DB Backup</button>
|
||||
<button class="secondary" onclick="importDb()">DB Import</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -238,6 +247,9 @@
|
||||
<script src="/js/showmode.js?v=5"></script>
|
||||
<script src="/js/tabs-auth.js?v=5"></script>
|
||||
<script src="/js/pdf.js?v=5"></script>
|
||||
<script src="/js/csv.js?v=1"></script>
|
||||
<script src="/js/backup.js?v=1"></script>
|
||||
<script src="/js/import.js?v=1"></script>
|
||||
<script src="/js/init.js?v=5"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Datenbank-Backup (Download)
|
||||
// -----------------------------
|
||||
// Stellt downloadBackup() bereit: lädt einen konsistenten Snapshot der
|
||||
// SQLite-Datenbank über GET /backup als .db-Datei herunter (VACUUM INTO,
|
||||
// WAL-sicher). Hängt im Seitenmenü von Test.html am Button "DB Backup" und
|
||||
// nutzt baseUrl() aus js/server.js. Ergänzt das automatische Backup, das der
|
||||
// Server beim Start in sessions/backups/ ablegt.
|
||||
|
||||
// downloadBackup: fordert den DB-Snapshot an und startet den Download.
|
||||
// Kein Parameter, keine Rückgabe; Fehler werden über ein Alert gemeldet.
|
||||
async function downloadBackup(){
|
||||
try {
|
||||
const res = await fetch(baseUrl() + '/backup');
|
||||
if(!res.ok){
|
||||
let msg = 'Server HTTP ' + res.status;
|
||||
try {
|
||||
const json = await res.json();
|
||||
if(json && json.error) msg = json.error;
|
||||
} catch(e){ /* Antwort war kein JSON */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const filename = 'projektuebersicht_backup_'
|
||||
+ new Date().toISOString().slice(0, 10) + '.db';
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch(err){
|
||||
console.error('DB-Backup fehlgeschlagen:', err);
|
||||
alert('DB-Backup fehlgeschlagen: ' + (err.message || err));
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ function renderBoard(){
|
||||
const v = (p.lph[l]!==undefined && p.lph[l]!="") ? p.lph[l] : "";
|
||||
const aktiv = l === aktLp;
|
||||
const inputAttr = locked ? 'readonly tabindex="-1"' : `onchange="editLph('${p.id}','${l}',this.value)"`;
|
||||
body += `<td class="lph-cell ${locked ? 'locked-cell' : ''}${aktiv ? ' lp-aktiv' : ''}"${aktiv ? ' title="Aktive Leistungsphase"' : ''}><input type="text" value="${v}" ${inputAttr} placeholder="-">`;
|
||||
body += `<td class="lph-cell ${locked ? 'locked-cell' : ''}${aktiv ? ' lp-aktiv' : ''}"${aktiv ? ' title="Aktive Leistungsphase"' : ''}><input type="text" value="${esc(v)}" ${inputAttr} placeholder="-">`;
|
||||
if(v!=""){ const pct = Math.min(100, parseFloat(v)||0);
|
||||
body += `<div class="bar"><div style="width:${pct}%;background:${getProgressColor(pct)}"></div></div>`; }
|
||||
body += `</td>`;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — CSV/ZIP-Export
|
||||
// -----------------------------
|
||||
// Stellt exportCSV() bereit: lädt den ZIP mit den übersichtlichen CSV-Dateien
|
||||
// der aktuellen Abteilung über GET /export-csv und stößt den Download an.
|
||||
// Hängt im Seitenmenü von Test.html am Button "CSV Export" und nutzt baseUrl()
|
||||
// aus js/server.js sowie currentDepartment aus js/globals.js. Der ZIP enthält
|
||||
// pro logischer Entität eine CSV (Wochen, Personen, Projekte, …), erzeugt vom
|
||||
// Server (siehe server.js im Projekt-Root).
|
||||
|
||||
// exportCSV: fordert den CSV-Export der aktuellen Abteilung an und lädt ihn
|
||||
// als ZIP-Datei herunter. Kein Parameter, keine Rückgabe; Fehler werden über
|
||||
// ein Alert gemeldet.
|
||||
async function exportCSV(){
|
||||
try {
|
||||
const dept = currentDepartment || 'ELT';
|
||||
const res = await fetch(baseUrl() + '/export-csv?department=' + encodeURIComponent(dept));
|
||||
if(!res.ok){
|
||||
let msg = 'Server HTTP ' + res.status;
|
||||
try {
|
||||
const json = await res.json();
|
||||
if(json && json.error) msg = json.error;
|
||||
} catch(e){ /* Antwort war kein JSON */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const filename = 'Projektuebersicht_' + dept + '_'
|
||||
+ new Date().toISOString().slice(0, 10) + '.zip';
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch(err){
|
||||
console.error('CSV-Export fehlgeschlagen:', err);
|
||||
alert('CSV-Export fehlgeschlagen: ' + (err.message || err));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Datenbank-Import (Backup-Wiederherstellung)
|
||||
// -----------------------------
|
||||
// Stellt importDb() bereit: wählt über einen Datei-Dialog eine mit "DB Backup"
|
||||
// erzeugte .db-Datei aus, lädt sie als rohen Binär-Body an POST /import und
|
||||
// lädt nach erfolgreicher Wiederherstellung die Seite neu. Da der Import den
|
||||
// kompletten Datenbestand ersetzt, ist er passwortgeschützt (gleiche Logik wie
|
||||
// Wochen-Löschung über requestedAction/isAuthenticated, siehe js/tabs-auth.js
|
||||
// und js/weeks.js) und wird zusätzlich im Browser per Bestätigungsdialog
|
||||
// abgesichert.
|
||||
|
||||
// importDb: öffnet bei Bedarf das Passwort-Modal (requestedAction 'importDb')
|
||||
// und startet danach den Datei-Dialog für das Backup. Parameter `skipAuth=true`
|
||||
// überspringt die Passwortabfrage (wird von confirmPassword() genutzt).
|
||||
// Keine Rückgabe.
|
||||
function importDb(skipAuth = false){
|
||||
if(!skipAuth && !isAuthenticated){
|
||||
requestedAction = { type: 'importDb' };
|
||||
document.getElementById('passwordModal').style.display = 'flex';
|
||||
document.getElementById('passwordInput').value = '';
|
||||
document.getElementById('passwordInput').focus();
|
||||
return;
|
||||
}
|
||||
if(!confirm('Datenbank-Backup wirklich wiederherstellen?\n\nAlle aktuellen Daten werden ersetzt.')){
|
||||
requestedAction = null;
|
||||
return;
|
||||
}
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.db,application/octet-stream,application/x-sqlite3';
|
||||
input.onchange = () => {
|
||||
const file = input.files && input.files[0];
|
||||
if(!file){
|
||||
requestedAction = null;
|
||||
return;
|
||||
}
|
||||
uploadBackup(file);
|
||||
};
|
||||
input.click();
|
||||
requestedAction = null;
|
||||
}
|
||||
|
||||
// uploadBackup: lädt die gewählte .db-Datei als rohen Binär-Body an
|
||||
// POST /import und informiert über das Ergebnis. Bei Erfolg wird die Seite neu
|
||||
// geladen, damit das Frontend den wiederhergestellten Datenbestand zeigt.
|
||||
async function uploadBackup(file){
|
||||
try {
|
||||
const res = await fetch(baseUrl() + '/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
body: file
|
||||
});
|
||||
if(!res.ok){
|
||||
let msg = 'Server HTTP ' + res.status;
|
||||
try {
|
||||
const json = await res.json();
|
||||
if(json && json.error) msg = json.error;
|
||||
} catch(e){ /* Antwort war kein JSON */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
alert('Datenbank-Backup erfolgreich wiederhergestellt.\nDie Seite wird neu geladen.');
|
||||
window.location.reload();
|
||||
} catch(err){
|
||||
console.error('DB-Import fehlgeschlagen:', err);
|
||||
alert('DB-Import fehlgeschlagen: ' + (err.message || err));
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function addPerson(){
|
||||
document.getElementById('newPersonFullName').value='';
|
||||
document.getElementById('newPersonEmail').value='';
|
||||
document.querySelectorAll('input[name="newPersonRoles"]').forEach(el=>el.checked=false);
|
||||
save(); renderPeople(); renderBoard();
|
||||
save({people:true}); renderPeople(); renderBoard();
|
||||
}
|
||||
|
||||
// delPerson: entfernt eine Person aus der globalen Liste. Bereits in
|
||||
@@ -76,7 +76,7 @@ function delPerson(id){
|
||||
w.people = (w.people || []).filter(p=>p.id!==id);
|
||||
}
|
||||
|
||||
save(); renderPeople(); renderBoard();
|
||||
save({people:true}); renderPeople(); renderBoard();
|
||||
}
|
||||
|
||||
// editPerson: ändert Name/Vollname/Rolle sowohl global als auch in der
|
||||
@@ -132,7 +132,7 @@ function editPerson(id,field,val){
|
||||
delete w.attendance[id];
|
||||
}
|
||||
}
|
||||
save(); renderPeople(); renderBoard();
|
||||
save({people:true}); renderPeople(); renderBoard();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ function editPerson(id,field,val){
|
||||
const localPerson = (w.people || []).find(p => p.id === id);
|
||||
if(localPerson) localPerson[field] = val;
|
||||
}
|
||||
save(); renderPeople(); renderBoard();
|
||||
save({people:true}); renderPeople(); renderBoard();
|
||||
}
|
||||
|
||||
// togglePersonRole: fügt eine Rolle (`role`) zur Person `id` hinzu bzw. entfernt
|
||||
@@ -172,7 +172,7 @@ function togglePersonRole(id, role, checked){
|
||||
|
||||
if(globalPerson) updateRoles(globalPerson);
|
||||
if(localPerson) updateRoles(localPerson);
|
||||
save(); renderPeople(); renderBoard();
|
||||
save({people:true}); renderPeople(); renderBoard();
|
||||
}
|
||||
|
||||
// renderPeople: baut die HTML-Liste der Personen für den Personen-Tab auf
|
||||
@@ -324,7 +324,7 @@ function reorderPeople(){
|
||||
newPeople.forEach(p => { if(byId[p.id]) newWeekPeople.push(byId[p.id]); });
|
||||
if(newWeekPeople.length === w.people.length) w.people = newWeekPeople;
|
||||
}
|
||||
save();
|
||||
save({people:true});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ function addAbsence(personId){
|
||||
if(!Array.isArray(person.absences)) person.absences = [];
|
||||
person.absences.push({ id: uid(), start, end, type, note });
|
||||
person.absences.sort((a,b) => parseDateISO(a.start) - parseDateISO(b.start));
|
||||
save();
|
||||
save({week:false, people:true});
|
||||
renderPeople();
|
||||
if(currentTab === 'gantt') renderGantt();
|
||||
}
|
||||
@@ -361,7 +361,7 @@ function deleteAbsence(personId, absenceId){
|
||||
const person = people.find(p => p.id === personId);
|
||||
if(!person || !person.absences) return;
|
||||
person.absences = person.absences.filter(a => a.id !== absenceId);
|
||||
save();
|
||||
save({week:false, people:true});
|
||||
renderPeople();
|
||||
if(currentTab === 'gantt') renderGantt();
|
||||
}
|
||||
@@ -176,19 +176,43 @@ window.PlanApp = {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben
|
||||
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben.
|
||||
// In-Flight-Schutz: läuft bereits ein Save, wird kein zweiter POST mit
|
||||
// identischer Revisions-Basis abgeschickt (der würde fälschlich als 409-
|
||||
// Konflikt gemeldet). Stattdessen wird ein Folgesave eingereiht, der nach
|
||||
// Abschluss des laufenden Saves mit der dann frischen Revision nachzieht.
|
||||
let savePlanRunning = false;
|
||||
let savePlanQueued = false;
|
||||
function savePlan(){
|
||||
if(!app.week || !app.project) return;
|
||||
sendWeekToServer(app.week).catch(err => console.error('Speichern fehlgeschlagen:', err));
|
||||
if(savePlanRunning){ savePlanQueued = true; return; }
|
||||
savePlanRunning = true;
|
||||
sendWeekToServer(app.week)
|
||||
.then(res => {
|
||||
// Lokale Revisions-Basis aktualisieren, damit spätere eigene Saves
|
||||
// nicht fälschlich als Konflikt gemeldet werden.
|
||||
if(res && res.updatedAt) app.week.updatedAt = res.updatedAt;
|
||||
})
|
||||
.catch(err => {
|
||||
if(err && err.conflict){
|
||||
showToast('Konflikt: Die Sitzung wurde zwischenzeitlich geändert. Bitte Seite neu laden.');
|
||||
console.error('Speichern fehlgeschlagen (Konflikt):', err.message || err);
|
||||
} else {
|
||||
console.error('Speichern fehlgeschlagen:', err);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
savePlanRunning = false;
|
||||
if(savePlanQueued){ savePlanQueued = false; savePlan(); }
|
||||
});
|
||||
}
|
||||
|
||||
// saveGlobalPeople: schreibt die globale Personenliste der Abteilung als
|
||||
// JSON-Array auf den Server, aber nur wenn sie nicht leer ist.
|
||||
// JSON-Array auf den Server. Auch eine leere Liste wird gespeichert, damit
|
||||
// das Löschen der letzten Person erhalten bleibt.
|
||||
function saveGlobalPeople(){
|
||||
const people = db.globalPeople[app.department] || [];
|
||||
if(people.length > 0){
|
||||
sendPeopleToServer(people).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
|
||||
}
|
||||
sendPeopleToServer(people).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
|
||||
}
|
||||
|
||||
// Person in der globalen Liste finden (dort werden Abwesenheiten gepflegt)
|
||||
|
||||
+23
-3
@@ -34,14 +34,34 @@ function blobToDataUrl(blob){
|
||||
}
|
||||
|
||||
// sendWeekToServer: POSTet ein komplettes Wochen-Objekt (JSON) an
|
||||
// <baseUrl>/save-week. Parameter: week (Objekt). Rückgabe: Promise mit Server-Antwort.
|
||||
// <baseUrl>/save-week. Parameter: week (Objekt). Rückgabe: Promise mit
|
||||
// Server-Antwort. `baseUpdatedAt` übermittelt die Revision, auf der die
|
||||
// lokalen Daten beruhen; meldet der Server 409 (conflict), wird ein Error
|
||||
// mit `err.conflict = true` und `err.updatedAt` geworfen, damit der Aufrufer
|
||||
// den Konflikt anzeigen kann.
|
||||
function sendWeekToServer(week){
|
||||
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
|
||||
return fetch(baseUrl() + '/save-week',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json; charset=utf-8'},
|
||||
body:JSON.stringify({week, department:currentDepartment})
|
||||
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
|
||||
body:JSON.stringify({
|
||||
week,
|
||||
department:currentDepartment,
|
||||
baseUpdatedAt:(week && week.updatedAt) ? week.updatedAt : undefined
|
||||
})
|
||||
}).then(r=>{
|
||||
return r.json().then(json => {
|
||||
if(r.ok && json) return json;
|
||||
if(json && json.conflict){
|
||||
const err = new Error(json.error || 'Konflikt: Sitzung wurde zwischenzeitlich geändert');
|
||||
err.conflict = true;
|
||||
err.label = (week && week.label) || '';
|
||||
err.updatedAt = json.updatedAt;
|
||||
throw err;
|
||||
}
|
||||
throw new Error('Server HTTP '+r.status);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// sendPeopleToServer: POSTet die globale Personenliste einer Abteilung als
|
||||
|
||||
+151
-21
@@ -12,13 +12,33 @@
|
||||
// `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: synchronisiert die aktuellen Daten zum Server.
|
||||
// Jede Sitzung wird als eigenes JSON-Dokument gespeichert. Personen und Rollen
|
||||
// werden in der Sitzung selbst abgelegt.
|
||||
function save(){
|
||||
// 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(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).
|
||||
let dirtyWeeks = new Set();
|
||||
let dirtyPeople = false;
|
||||
|
||||
// In-Flight-Schutz für syncAllToServer: läuft bereits ein Sync, wird kein
|
||||
// zweiter gestartet (sonst senden zwei Syncs dieselbe Woche mit gleicher
|
||||
// Revisions-Basis und der zweite bekäme einen scheinbaren 409-Konflikt).
|
||||
// Stattdessen wird ein Nachlauf-Sync eingereiht und nach Abschluss ausgeführt.
|
||||
let syncInFlight = false;
|
||||
let syncQueued = false;
|
||||
|
||||
// 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.
|
||||
@@ -27,30 +47,133 @@ function scheduleSessionSync(){
|
||||
saveSyncTimer = setTimeout(() => { syncAllToServer(); saveSyncTimer = null; }, 500);
|
||||
}
|
||||
|
||||
// syncAllToServer: schreibt die gesamte Datenbank als JSON-Dokumente zum Server.
|
||||
// Zuerst wird die globale Personenliste (`save-people`) gespeichert, danach jede
|
||||
// Sitzung einzeln über `sendWeekToServer()`.
|
||||
// Gibt nichts zurück; Fehler werden nur per console.error protokolliert.
|
||||
// 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.
|
||||
function syncAllToServer(){
|
||||
// Zuerst globale Personenliste speichern
|
||||
saveGlobalPeopleToServer();
|
||||
// Dann jede Sitzung speichern
|
||||
db.weeks.forEach(w => {
|
||||
sendWeekToServer(w)
|
||||
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', w.label); })
|
||||
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err); });
|
||||
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);
|
||||
weekLabels.forEach(label => {
|
||||
const w = db.weeks.find(x => x.label === label);
|
||||
if(!w){ dirtyWeeks.delete(label); return; }
|
||||
jobs.push(sendWeekToServer(w)
|
||||
.then(res => {
|
||||
if(res && res.ok){
|
||||
console.log('Sitzung gespeichert:', w.label);
|
||||
// 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);
|
||||
} else {
|
||||
console.error('Sitzung konnte nicht gespeichert werden:', w.label, res && res.error ? res.error : 'unbekannter Fehler');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if(err && err.conflict){
|
||||
dirtyWeeks.delete(label);
|
||||
handleWeekConflict(w, err);
|
||||
} else {
|
||||
console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err);
|
||||
// Label bleibt dirty -> der nächste Sync versucht es erneut.
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Promise.all(jobs).then(() => {
|
||||
syncInFlight = false;
|
||||
if(syncQueued){ syncQueued = false; 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;
|
||||
}
|
||||
|
||||
// handleWeekConflict: wird aufgerufen, wenn der Server eine zwischenzeitlich
|
||||
// geänderte Woche meldet (409). Nur diese eine Woche wird vom Server neu
|
||||
// geladen; andere lokal geänderte Wochen (dirtyWeeks) und die Personenliste
|
||||
// bleiben unangetastet und gehen nicht verloren.
|
||||
function handleWeekConflict(week, err){
|
||||
const label = (week && week.label) || (err && err.label) || '';
|
||||
alert('Achtung: ' + label + ' wurde zwischenzeitlich von einer anderen Person '
|
||||
+ 'geändert. Ihre Änderungen an dieser Sitzung wurden nicht gespeichert; '
|
||||
+ 'die aktuellen Daten dieser Sitzung werden geladen.');
|
||||
if(label) reloadWeekFromServer(label);
|
||||
else load();
|
||||
}
|
||||
|
||||
// reloadWeekFromServer: lädt die aktuelle Abteilung 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())
|
||||
.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);
|
||||
if(idx >= 0){
|
||||
if(fresh){
|
||||
// Felder absichern (wie in load()), damit keine kaputten Daten crashen
|
||||
if(!Array.isArray(fresh.people)) fresh.people = [];
|
||||
if(!Array.isArray(fresh.projects)) fresh.projects = [];
|
||||
if(!fresh.attendance) fresh.attendance = {};
|
||||
db.weeks[idx] = fresh;
|
||||
} else {
|
||||
db.weeks.splice(idx, 1);
|
||||
}
|
||||
// Wurde die aktuelle Woche entfernt, auf die letzte verbleibende wechseln
|
||||
if(!db.weeks.some(w => w.id === db.currentWeek)){
|
||||
db.currentWeek = db.weeks.length ? db.weeks[db.weeks.length - 1].id : null;
|
||||
}
|
||||
}
|
||||
renderAll();
|
||||
showTab(currentTab);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Neu laden der Sitzung fehlgeschlagen:', err);
|
||||
load();
|
||||
});
|
||||
}
|
||||
|
||||
// saveGlobalPeopleToServer: speichert die globale Personenliste der aktuellen
|
||||
// Abteilung über `sendPeopleToServer()` (JSON-Array).
|
||||
// Leere Personenlisten werden übersprungen (kein Schreibvorgang).
|
||||
// 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] || [];
|
||||
if(people.length === 0) return;
|
||||
sendPeopleToServer(people)
|
||||
.then(res => { if(res && res.ok) console.log('Globale Personenliste gespeichert'); })
|
||||
.catch(err => { console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err); });
|
||||
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');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err);
|
||||
// dirtyPeople bleibt true -> der nächste Sync versucht es erneut.
|
||||
});
|
||||
}
|
||||
|
||||
// switchDepartment: wechselt die aktive Abteilung (z.B. "ELT") und lädt deren
|
||||
@@ -63,6 +186,8 @@ function switchDepartment(dept){
|
||||
// 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);
|
||||
@@ -89,6 +214,11 @@ function switchDepartment(dept){
|
||||
// die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder fehlenden
|
||||
// Daten werden Beispieldaten über `seed()` geladen.
|
||||
function load(){
|
||||
// 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;
|
||||
|
||||
@@ -59,6 +59,7 @@ function confirmPassword(){
|
||||
if(action?.type==='tab') showTab(action.tab, true);
|
||||
else if(action?.type==='deleteWeek') deleteWeek(true);
|
||||
else if(action?.type==='unlockWeek') unlockCurrentWeek(true);
|
||||
else if(action?.type==='importDb') importDb(true);
|
||||
}, 100);
|
||||
document.getElementById('passwordModal').style.display='none';
|
||||
} else {
|
||||
|
||||
+9
-3
@@ -31,9 +31,11 @@ function updateWeekDate(){
|
||||
// Kein Parameter; Rückgabe: das gefundene Woche-Objekt oder undefined.
|
||||
function currentWeek(){ return db.weeks.find(w=>w.id===db.currentWeek); }
|
||||
|
||||
// switchWeek: setzt die aktuell gewählte Wochen-ID (`id`), speichert und
|
||||
// rendert die Ansicht neu. Keine Rückgabe.
|
||||
function switchWeek(id){ db.currentWeek = id; save(); renderAll(); }
|
||||
// switchWeek: setzt die aktuell gewählte Wochen-ID (`id`) und rendert die
|
||||
// Ansicht neu. Es werden keine Daten geändert, daher wird nicht gespeichert
|
||||
// (eine save() würde die Woche nur als dirty markieren und unnötig senden).
|
||||
// Keine Rückgabe.
|
||||
function switchWeek(id){ db.currentWeek = id; renderAll(); }
|
||||
|
||||
// newWeek: erzeugt über zwei prompt-Dialoge eine neue leere Sitzung (Bezeichnung
|
||||
// und Datum). Die neue Woche übernimmt die globale Personenliste der Abteilung,
|
||||
@@ -82,7 +84,11 @@ function applyAutoLock(){
|
||||
const sorted = [...db.weeks].sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
|
||||
const latestId = sorted[sorted.length - 1].id;
|
||||
db.weeks.forEach(w => {
|
||||
const wasLocked = !!w.locked;
|
||||
w.locked = (w.id !== latestId);
|
||||
// Nur tatsächlich geänderte Lock-Zustände übertragen (beim bloßen Laden
|
||||
// sind die Werte identisch und es wird nichts gesendet).
|
||||
if(wasLocked !== w.locked) markWeekDirty(w.label);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user