843 lines
39 KiB
JavaScript
843 lines
39 KiB
JavaScript
// -----------------------------
|
||
// Projektübersicht — Bereich „Admin"
|
||
// -----------------------------
|
||
// Der Bereich #admin bündelt alles, was NUR Administratoren zusteht. Er ist
|
||
// aus dem Bereich „Einstellungen" herausgelöst worden: Dort standen die
|
||
// firmenweiten Masken zunächst neben „Mein Konto", was zwei verschiedene
|
||
// Dinge vermischte — die eigenen Kontoangaben (jedes Konto) und Vorgaben, die
|
||
// die Zahlen ALLER verschieben. „Einstellungen" enthält seitdem nur noch das
|
||
// eigene Konto, alles Firmenweite liegt hier.
|
||
//
|
||
// Der Bereich zerfällt in ABSCHNITTE, zwischen denen die aufgeklappte Gruppe
|
||
// „Admin" in der Seitenleiste umschaltet (ADMIN_SECTIONS unten,
|
||
// showAdminSection()). Sichtbar ist immer genau einer; der gewählte steht im
|
||
// URL-Fragment (…/Admin#auslastung) und ist damit verlinkbar — dasselbe
|
||
// Muster wie die Bereichsleiste des Projektbereichs (js/plan/plan-sections.js).
|
||
//
|
||
// „Benutzerverwaltung" — Konten, Rollen, Wochenstunden, Abwesenheiten
|
||
// (js/admin-users.js). War bis zum Umbau eine
|
||
// eigene Seite (admin-users.html) mit nachgebauter
|
||
// Kopfzeile statt der Seitenleiste.
|
||
// „Honorar" — Stundensatz und Anteile je Leistungsphase.
|
||
// „Auslastung" — Wochenstunden einer vollen Stelle, Ampel-
|
||
// Schwellen, Balkenskala, Aufwandsvorlage.
|
||
// „Termine & Sitzungen" — Arbeitsstunden je Tag, automatische Sperre.
|
||
// „Feiertage" — arbeitsfreie Tage (eigene Tabelle).
|
||
// „Aufgaben-Vorlagen" — Standardschritte je Leistungsphase.
|
||
// „Betrieb & Sicherheit" — Backups, Sitzungsdauer, Anmeldebremse.
|
||
// „Datenbank & Sitzungen"— Backup herunterladen, Backup einspielen,
|
||
// Sitzung löschen. Diese drei standen vorher als
|
||
// nackte Menüpunkte in der Seitenleiste, wo ein
|
||
// Fehlklick genügte; hier stehen sie mit dem
|
||
// Hinweis, was sie anrichten.
|
||
//
|
||
// Honorar, Auslastung, Termine und Betrieb waren bis zum Ausbau der
|
||
// Einstellungen Konstanten im Quelltext — teils im Frontend
|
||
// (Aufwandsvorlage, Ampel-Schwellen, Balkenskala, Arbeitsstunden je Tag),
|
||
// teils im Server (Backups, Sitzungsdauer, Anmeldebremse). Sie sind nicht
|
||
// deshalb gepflegt, weil sie sich oft ändern, sondern weil sie die Zahlen
|
||
// aller Nutzer verschieben. Gelesen werden sie über GET /app-settings (jedes
|
||
// Konto — das Frontend rechnet damit), geschrieben über PUT /app-settings.
|
||
//
|
||
// Dass der Bereich für Nicht-Administratoren gar nicht erst auftaucht, ist
|
||
// wie überall in dieser App ein reines UI-Gate; die eigentliche Sperre sitzt
|
||
// serverseitig in requireRole('admin') auf PUT /app-settings,
|
||
// PUT /honorar-config, POST/DELETE /lph-templates, POST/DELETE /holidays,
|
||
// GET /backup, POST /import und POST /delete-week.
|
||
|
||
// ADMIN_SECTIONS: die Abschnitte in der Reihenfolge der Seitenleiste. Die
|
||
// EINE Stelle, an der ein Abschnitt definiert wird — Schlüssel (zugleich
|
||
// URL-Fragment) und Überschrift. Die Zuordnung zum Markup läuft über das
|
||
// Attribut data-admin-section, sowohl an der Karte in Test.html als auch am
|
||
// Unterpunkt in der Seitenleiste; ein neuer Abschnitt braucht damit nur einen
|
||
// Eintrag hier plus die Karte. `wide` hebt die Breitenbegrenzung des Rasters
|
||
// auf — die Benutzertabelle hat zehn Spalten und scrollte sonst quer.
|
||
const ADMIN_SECTIONS = [
|
||
{ key: 'benutzer', title: 'Benutzerverwaltung', wide: true },
|
||
{ key: 'honorar', title: 'Honorar' },
|
||
{ key: 'auslastung', title: 'Auslastung' },
|
||
{ key: 'termine', title: 'Termine & Sitzungen' },
|
||
{ key: 'feiertage', title: 'Feiertage' },
|
||
{ key: 'vorlagen', title: 'Aufgaben-Vorlagen' },
|
||
{ key: 'betrieb', title: 'Betrieb & Sicherheit' },
|
||
{ key: 'datenbank', title: 'Datenbank & Sitzungen' },
|
||
];
|
||
|
||
// adminSection: der gerade sichtbare Abschnitt.
|
||
let adminSection = 'benutzer';
|
||
|
||
// renderAdminArea: Einstiegspunkt des Bereichs, von showTab('admin')
|
||
// gerufen. Befüllt alle Masken und wählt danach den sichtbaren Abschnitt.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function renderAdminArea(){
|
||
if(!isAdminUser()) return;
|
||
|
||
loadAdminHonorar();
|
||
loadAdminTemplates();
|
||
// Die firmenweiten Zahlen und Feiertage stehen bereits in APP_SETTINGS bzw.
|
||
// HOLIDAYS (beim Seitenstart geladen, js/store.js) — die Masken füllen sich
|
||
// also ohne einen weiteren Serveraufruf.
|
||
renderAppSettingFields();
|
||
renderAdminHolidays();
|
||
renderAdminSitzungen();
|
||
|
||
const fromHash = (location.hash || '').replace(/^#/, '');
|
||
showAdminSection(adminSectionExists(fromHash) ? fromHash : adminSection);
|
||
}
|
||
|
||
// isAdminUser: Kurzform für die Sichtbarkeit des Bereichs.
|
||
// Rückgabe: Boolean.
|
||
function isAdminUser(){
|
||
return !!(currentUser && currentUser.role === 'admin');
|
||
}
|
||
|
||
// adminSectionExists: prüft, ob ein Schlüssel ein bekannter Abschnitt ist.
|
||
// Parameter: key. Rückgabe: Boolean.
|
||
function adminSectionExists(key){
|
||
return ADMIN_SECTIONS.some(x => x.key === key);
|
||
}
|
||
|
||
// showAdminSection: schaltet auf einen Abschnitt um — blendet die zugehörige
|
||
// Karte ein, alle anderen aus, hebt den Unterpunkt hervor und schreibt den
|
||
// Schlüssel ins URL-Fragment (teilbarer Link, ohne je Klick einen
|
||
// History-Eintrag zu erzeugen). Wird der Bereich gerade gar nicht angezeigt
|
||
// (Klick aus einem anderen Bereich heraus), wird zuerst dorthin gewechselt.
|
||
// Parameter: key. Keine Rückgabe.
|
||
function showAdminSection(key){
|
||
if(!adminSectionExists(key)) key = ADMIN_SECTIONS[0].key;
|
||
adminSection = key;
|
||
|
||
if(currentTab !== 'admin'){ showTab('admin'); return; }
|
||
|
||
ADMIN_SECTIONS.forEach(sec => {
|
||
// querySelectorAll, nicht -Single: ein Abschnitt kann grundsätzlich aus
|
||
// mehreren Karten bestehen, die zusammen ein-/ausgeblendet werden.
|
||
document.querySelectorAll('#admin [data-admin-section="' + sec.key + '"]').forEach(card => {
|
||
card.style.display = (sec.key === key) ? '' : 'none';
|
||
});
|
||
const item = document.querySelector('#adminSubnav [data-admin-section="' + sec.key + '"]');
|
||
if(item) item.classList.toggle('is-active', sec.key === key);
|
||
});
|
||
|
||
const sec = ADMIN_SECTIONS.find(x => x.key === key);
|
||
const title = document.getElementById('adminSectionTitle');
|
||
if(title && sec) title.textContent = 'Admin — ' + sec.title;
|
||
|
||
const grid = document.querySelector('#admin .settings-grid');
|
||
if(grid) grid.classList.toggle('is-wide', !!(sec && sec.wide));
|
||
|
||
// Die Benutzerliste wird beim Betreten des Abschnitts frisch geholt: Sie
|
||
// aendert sich auch ausserhalb dieses Browsers, und die uebrigen Masken
|
||
// rechnen mit ihr (Wochenstunden -> Auslastung).
|
||
if(key === 'benutzer' && typeof renderAdminUsers === 'function') renderAdminUsers();
|
||
|
||
try {
|
||
history.replaceState(null, '', location.pathname + location.search + '#' + key);
|
||
} catch(e) { /* z. B. file:// — dann bleibt die Adresse eben unverändert */ }
|
||
}
|
||
|
||
// toggleAdminNav: Kopf-Eintrag der Gruppe. Er klappt nicht nur auf und zu wie
|
||
// „Werkzeuge", sondern führt auch in den Bereich — die Unterpunkte sind hier
|
||
// Navigation innerhalb einer Seite, keine Aktionen, und ein Kopf, der beim
|
||
// Anklicken nichts zeigt, wäre irreführend. Steht man WOANDERS, führt ein
|
||
// Klick immer hinein und klappt auf; nur wer schon drin ist, klappt damit auf
|
||
// und zu. Keine Parameter, keine Rückgabe.
|
||
function toggleAdminNav(){
|
||
if(currentTab !== 'admin'){
|
||
showTab('admin'); // klappt über syncAdminNavState() auch auf
|
||
return;
|
||
}
|
||
toggleSidebarAccordion('admin');
|
||
}
|
||
|
||
// Ein von außen geändertes Fragment (Vor-/Zurück-Taste, getippter Anker)
|
||
// mitziehen — showAdminSection() selbst nutzt replaceState und löst dieses
|
||
// Ereignis daher nicht aus.
|
||
window.addEventListener('hashchange', () => {
|
||
if(currentTab !== 'admin') return;
|
||
const key = (location.hash || '').replace(/^#/, '');
|
||
if(adminSectionExists(key) && key !== adminSection) showAdminSection(key);
|
||
});
|
||
|
||
// ---------- Firmenweite Zahlen (PUT /app-settings) ----------
|
||
//
|
||
// Bedient die Abschnitte Auslastung, Termine & Sitzungen und Betrieb.
|
||
|
||
// APP_SETTING_FIELDS: Zuordnung Eingabefeld -> Einstellungs-Schlüssel, je
|
||
// Abschnitt. Eine Tabelle statt einzelner getElementById-Zeilen, damit Füllen
|
||
// und Speichern denselben Weg gehen und ein neues Feld nur hier auftaucht.
|
||
const APP_SETTING_FIELDS = {
|
||
auslastung: {
|
||
setVollzeitStunden: 'vollzeitStundenProWoche',
|
||
setSchwelleGelb: 'auslastungSchwelleGelb',
|
||
setSchwelleRot: 'auslastungSchwelleRot',
|
||
// Das frühere Feld "Maximum der Balkenskala" (auslastungBalkenMax) ist
|
||
// entfallen: Seit die Auslastung in Stunden rechnet, ist die Skala die
|
||
// Kapazität der Person — ein volles Band ist eine volle Woche. Die feste
|
||
// Vorgabe 8 hatte zur Folge, dass ein roter Balken (ab 1,0) nur zu einem
|
||
// Achtel gefüllt war; Balken und Ampel widersprachen einander.
|
||
setReferenzHonorar: 'referenzHonorar',
|
||
setVerteilWochen: 'verteilzeitraumWochen',
|
||
},
|
||
termine: {
|
||
setStundenProTag: 'arbeitsstundenProTag',
|
||
},
|
||
betrieb: {
|
||
setMaxBackups: 'maxBackups',
|
||
setSessionTage: 'sessionTage',
|
||
setLoginVersuche: 'loginMaxVersuche',
|
||
setLoginSperre: 'loginSperreMinuten',
|
||
},
|
||
};
|
||
|
||
// renderAppSettingFields: schreibt die aktuellen Werte aus APP_SETTINGS in
|
||
// alle Eingabefelder und baut die Aufwands-Matrix auf. Ein gerade
|
||
// fokussiertes Feld bleibt unangetastet, sonst spränge beim Tippen der Cursor.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function renderAppSettingFields(){
|
||
Object.values(APP_SETTING_FIELDS).forEach(group => {
|
||
Object.entries(group).forEach(([elId, key]) => {
|
||
const el = document.getElementById(elId);
|
||
if(el && document.activeElement !== el) el.value = APP_SETTINGS[key];
|
||
});
|
||
});
|
||
const autoLock = document.getElementById('setAutoLock');
|
||
if(autoLock) autoLock.checked = !!Number(APP_SETTINGS.autoLockAktiv);
|
||
renderAufwandMatrix();
|
||
}
|
||
|
||
// renderAufwandMatrix: die firmenweite Gewichtung als Tabelle Leistungsphase ×
|
||
// Rolle. „W+M" fehlt bewusst — das Aufwandsmodell kennt nur die
|
||
// Leistungsphasen (AUFWAND_LPS in js/load/load.js). Ein leeres Feld bedeutet
|
||
// „kein Eintrag"; dafür zählt die Auslastung dann den vollen Faktor 1,0.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function renderAufwandMatrix(){
|
||
const wrap = document.getElementById('setAufwandMatrix');
|
||
if(!wrap) return;
|
||
const vorlage = APP_SETTINGS.aufwandVorlage || {};
|
||
const phasen = LPHS.filter(l => l !== 'W+M');
|
||
|
||
wrap.innerHTML = `<table class="settings-matrix">
|
||
<thead><tr><th>Phase</th>${ROLES.map(r => `<th>${esc(r)}</th>`).join('')}</tr></thead>
|
||
<tbody>${phasen.map(l => `<tr>
|
||
<th>${esc(l)}</th>
|
||
${ROLES.map(r => {
|
||
const v = vorlage[l] ? vorlage[l][r] : undefined;
|
||
return `<td><input type="number" min="0" max="1" step="0.1"
|
||
data-lph="${esc(l)}" data-role="${esc(r)}"
|
||
value="${v === undefined ? '' : esc(v)}" placeholder="1,0"></td>`;
|
||
}).join('')}
|
||
</tr>`).join('')}</tbody>
|
||
</table>`;
|
||
}
|
||
|
||
// collectAufwandMatrix: liest die Matrix aus den Feldern zurück. Leere Felder
|
||
// werden weggelassen (siehe renderAufwandMatrix), 0 ist ein gültiger Wert und
|
||
// bleibt erhalten. Rückgabe: Objekt LPH -> { Rolle: Faktor }.
|
||
function collectAufwandMatrix(){
|
||
const wrap = document.getElementById('setAufwandMatrix');
|
||
const out = {};
|
||
if(!wrap) return out;
|
||
wrap.querySelectorAll('input[data-lph]').forEach(inp => {
|
||
if(inp.value.trim() === '') return;
|
||
const v = parseFloat(inp.value);
|
||
if(!Number.isFinite(v)) return;
|
||
const lph = inp.dataset.lph;
|
||
(out[lph] = out[lph] || {})[inp.dataset.role] = v;
|
||
});
|
||
return out;
|
||
}
|
||
|
||
// putAppSettings: schickt einen Teil-Satz an PUT /app-settings und übernimmt
|
||
// die Antwort (der Server liefert immer den vollständigen, geprüften Stand
|
||
// zurück) in APP_SETTINGS. Danach rechnet die Anwendung sofort damit — ein
|
||
// Neuladen ist nicht nötig, deshalb wird die Auslastung gleich neu gezeichnet,
|
||
// falls sie gerade sichtbar ist. Parameter: patch (Objekt), erfolgsText.
|
||
// Rückgabe: keine.
|
||
function putAppSettings(patch, erfolgsText){
|
||
fetch(baseUrl() + '/app-settings', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||
body: JSON.stringify(patch),
|
||
})
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Speichern fehlgeschlagen.');
|
||
APP_SETTINGS = Object.assign({}, APP_SETTINGS, json.settings || {});
|
||
renderAppSettingFields();
|
||
showAdminNote(erfolgsText);
|
||
})
|
||
.catch(err => {
|
||
console.error('Einstellungen speichern fehlgeschlagen:', err);
|
||
alert('Einstellungen konnten nicht gespeichert werden: ' + (err.message || err));
|
||
});
|
||
}
|
||
|
||
// fieldValues: sammelt die Zahlenwerte einer Gruppe aus APP_SETTING_FIELDS.
|
||
// Parameter: gruppe (Schlüssel in APP_SETTING_FIELDS). Rückgabe: Objekt.
|
||
function fieldValues(gruppe){
|
||
const patch = {};
|
||
Object.entries(APP_SETTING_FIELDS[gruppe]).forEach(([elId, key]) => {
|
||
const el = document.getElementById(elId);
|
||
if(el && el.value.trim() !== '') patch[key] = parseFloat(el.value);
|
||
});
|
||
return patch;
|
||
}
|
||
|
||
// saveAuslastungSettings: Schwellen, Skala, Vollzeit-Stunden und die
|
||
// Aufwandsvorlage in einem Zug. Keine Parameter, keine Rückgabe.
|
||
function saveAuslastungSettings(){
|
||
const patch = fieldValues('auslastung');
|
||
patch.aufwandVorlage = collectAufwandMatrix();
|
||
putAppSettings(patch, 'Auslastungs-Einstellungen gespeichert.');
|
||
}
|
||
|
||
// saveTerminSettings: Arbeitsstunden je Tag und die automatische Sperre.
|
||
// Keine Parameter, keine Rückgabe.
|
||
// juengsteWoche: die chronologisch letzte Sitzung — das ist die, die
|
||
// applyAutoLock() im Normalbetrieb offen hält und auf die db.currentWeek
|
||
// nach jedem vollständigen Neuladen zurückfällt. Rückgabe: Woche-Objekt oder
|
||
// undefined (keine Sitzungen vorhanden).
|
||
function juengsteWoche(){
|
||
return [...db.weeks].sort((a, b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label))).pop();
|
||
}
|
||
|
||
// renderAdminSitzungen: listet alle Sitzungen (js/weeks.js: db.weeks),
|
||
// neueste zuerst, mit Datum, Zustand und den passenden Aktionen.
|
||
//
|
||
// Diese Liste ist seit dem Wegfall der Sitzungsauswahl im Menüband die
|
||
// einzige Stelle, die alle Sitzungen nebeneinander zeigt — die übrige
|
||
// Oberfläche zeigt immer nur die eine offene. Vier Zustände:
|
||
// "Geöffnet" — das ist gerade db.currentWeek UND zugleich die
|
||
// jüngste Sitzung: der Normalfall, keine Aktion
|
||
// nötig.
|
||
// "Geöffnet (Korrektur)" — db.currentWeek, aber NICHT die jüngste — man
|
||
// hat eine ältere Sitzung über "Öffnen" zur
|
||
// Bearbeitung geholt. "Fertig – wieder sperren"
|
||
// sperrt sie zurück UND wechselt die Oberfläche
|
||
// wieder auf die jüngste Sitzung, statt beides
|
||
// getrennten Handgriffen zu überlassen.
|
||
// "Gesperrt" — historisch; "Entsperren" macht sie für eine
|
||
// Korrektur kurz editierbar. Ist die Auto-Sperre
|
||
// aktiv, sperrt sie beim nächsten Neuladen (oder
|
||
// beim nächsten Anlegen/Duplizieren/Löschen
|
||
// einer Sitzung — applyAutoLock() läuft dabei
|
||
// überall) automatisch wieder.
|
||
// "Entsperrt" — editierbar, aber gerade nicht die angezeigte
|
||
// Sitzung. Kommt vor, wenn eine gesperrte
|
||
// entsperrt, aber noch nicht geöffnet wurde, wenn
|
||
// man von einer Korrektur zur jüngsten
|
||
// zurückgekehrt ist (die bleibt dabei entsperrt),
|
||
// oder wenn die Auto-Sperre firmenweit aus ist.
|
||
// "Öffnen" macht sie zur angezeigten Sitzung.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function renderAdminSitzungen(){
|
||
const el = document.getElementById('adminSessionsList');
|
||
if(!el || !Array.isArray(db.weeks)) return;
|
||
|
||
const juengste = juengsteWoche();
|
||
const sortiert = [...db.weeks].sort((a, b) => weekSortKey(b.label).localeCompare(weekSortKey(a.label)));
|
||
el.innerHTML = sortiert.map(w => {
|
||
const aktuell = w.id === db.currentWeek;
|
||
const istJuengste = juengste && w.id === juengste.id;
|
||
const datum = w.date ? formatWeekDate(w.date) : '';
|
||
|
||
let chip, aktionen;
|
||
if(aktuell && istJuengste){
|
||
chip = '<span class="sitzung-chip ist-geoeffnet">Geöffnet</span>';
|
||
aktionen = '';
|
||
} else if(aktuell){
|
||
// Zur Bearbeitung geöffnete, ältere Sitzung — noch nicht wieder
|
||
// gesperrt. Ohne diesen eigenen Zustand sähe man hier "Geöffnet" ohne
|
||
// jede Aktion und hätte keinen Weg zurück außer über eine andere,
|
||
// zufällige applyAutoLock()-auslösende Handlung.
|
||
chip = '<span class="sitzung-chip ist-geoeffnet">Geöffnet (Korrektur)</span>';
|
||
aktionen = `<button class="secondary small" onclick="adminRelockWeek('${esc(w.id)}')">Fertig – wieder sperren</button>`;
|
||
} else if(w.locked){
|
||
chip = '<span class="sitzung-chip ist-gesperrt">Gesperrt</span>';
|
||
aktionen = `<button class="secondary small" onclick="adminUnlockWeek('${esc(w.id)}')">Entsperren</button>`;
|
||
} else {
|
||
chip = '<span class="sitzung-chip ist-entsperrt">Entsperrt</span>';
|
||
aktionen = `<button class="secondary small" onclick="adminOpenWeek('${esc(w.id)}')">Öffnen</button>`;
|
||
}
|
||
|
||
return `<div class="template-admin-row">
|
||
<span class="template-admin-title">${esc(w.label)}${datum ? ' · ' + esc(datum) : ''}</span>
|
||
${chip}
|
||
${aktionen}
|
||
<button class="danger small" onclick="adminDeleteWeek('${esc(w.id)}')" title="Sitzung löschen">×</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// adminUnlockWeek / adminDeleteWeek / adminOpenWeek / adminRelockWeek: dünne
|
||
// Wrapper der Sitzungsliste um die allgemeinen Funktionen aus js/weeks.js.
|
||
// Nach Entsperren/Löschen/Sperren zeichnet sich NUR diese Liste neu (nicht
|
||
// die ganze Seite) — die übrige Oberfläche zeigt ohnehin nur die eine offene
|
||
// Sitzung und ändert sich durch das Entsperren oder Löschen einer anderen
|
||
// nicht. "Öffnen" macht dagegen ausdrücklich sichtbar, was sich geändert
|
||
// hat: Es wechselt zur Sitzung UND zum Board, statt sie nur im Hintergrund
|
||
// zur aktuellen zu machen.
|
||
function adminUnlockWeek(weekId){
|
||
unlockWeek(db.weeks.find(w => w.id === weekId), renderAdminSitzungen);
|
||
}
|
||
function adminDeleteWeek(weekId){
|
||
deleteWeekById(weekId, renderAdminSitzungen);
|
||
}
|
||
function adminOpenWeek(weekId){
|
||
switchWeek(weekId);
|
||
showTab('board');
|
||
}
|
||
|
||
// adminRelockWeek: schließt eine Korrektur ab — sperrt die übergebene
|
||
// Sitzung wieder UND stellt die jüngste Sitzung als db.currentWeek wieder
|
||
// her, statt beides getrennten Schritten zu überlassen (sperren allein
|
||
// ließe die Oberfläche weiter auf der alten Sitzung stehen; nur zurück-
|
||
// wechseln ließe sie entsperrt liegen). markWeekDirty() + save({week:false})
|
||
// statt des einfachen save(), weil save() ohne Parameter db.currentWeek
|
||
// markieren würde — und das ist zu diesem Zeitpunkt schon die jüngste, NICHT
|
||
// die gerade gesperrte Sitzung (gleiches Muster wie savePlan() in
|
||
// js/plan/plan-state.js). Parameter: weekId. Keine Rückgabe.
|
||
function adminRelockWeek(weekId){
|
||
const w = db.weeks.find(x => x.id === weekId);
|
||
if(!w || w.locked) return;
|
||
w.locked = true;
|
||
markWeekDirty(w.label);
|
||
save({ week: false });
|
||
const juengste = juengsteWoche();
|
||
if(juengste) db.currentWeek = juengste.id;
|
||
renderAdminSitzungen();
|
||
}
|
||
|
||
function saveTerminSettings(){
|
||
const patch = fieldValues('termine');
|
||
const autoLock = document.getElementById('setAutoLock');
|
||
if(autoLock) patch.autoLockAktiv = autoLock.checked ? 1 : 0;
|
||
putAppSettings(patch, 'Termin-Einstellungen gespeichert.');
|
||
}
|
||
|
||
// saveBetriebSettings: Backups, Sitzungsdauer und Anmeldebremse.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function saveBetriebSettings(){
|
||
putAppSettings(fieldValues('betrieb'), 'Betriebs-Einstellungen gespeichert.');
|
||
}
|
||
|
||
|
||
// ---------- Abschnitt Honorar ----------
|
||
|
||
// loadAdminHonorar: holt Anteile und Stundensatz (GET /honorar-config, für
|
||
// jedes angemeldete Konto lesbar) und rendert die Maske.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function loadAdminHonorar(){
|
||
const fieldsEl = document.getElementById('honorarAdminFields');
|
||
if(fieldsEl) fieldsEl.innerHTML = '<span class="hint">Lade Honorar-Einstellungen…</span>';
|
||
fetch(baseUrl() + '/honorar-config')
|
||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||
.then(data => {
|
||
adminHonorar = { anteile: data.anteile || {}, stundensatz: Number(data.stundensatz) || 135 };
|
||
renderAdminHonorar();
|
||
})
|
||
.catch(err => {
|
||
console.error('Honorar-Konfiguration laden fehlgeschlagen:', err);
|
||
if(fieldsEl) fieldsEl.innerHTML = '<span class="hint">Honorar-Einstellungen konnten nicht geladen werden.</span>';
|
||
});
|
||
}
|
||
|
||
// renderAdminHonorar: ein Eingabefeld je Leistungsphase plus die laufende
|
||
// Summe. Die Summe steht dabei, weil die HOAI-Sätze eines Leistungsbildes
|
||
// zusammen 100 % ergeben — ein unvollständiger Satz fällt sonst erst auf,
|
||
// wenn ein gerechneter Terminplan zu kurz gerät.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function renderAdminHonorar(){
|
||
const fieldsEl = document.getElementById('honorarAdminFields');
|
||
if(!fieldsEl || !adminHonorar) return;
|
||
|
||
const anteile = adminHonorar.anteile || {};
|
||
fieldsEl.innerHTML = LPHS.map(l => `<label class="honorar-anteil-field">
|
||
<span>${esc(l)}</span>
|
||
<input type="number" min="0" max="100" step="0.1" data-lph="${esc(l)}"
|
||
value="${anteile[l] !== undefined ? esc(anteile[l]) : ''}" placeholder="0"
|
||
oninput="refreshAdminHonorarSum()">
|
||
</label>`).join('');
|
||
|
||
const satzEl = document.getElementById('honorarStundensatz');
|
||
if(satzEl && document.activeElement !== satzEl) satzEl.value = adminHonorar.stundensatz;
|
||
|
||
refreshAdminHonorarSum();
|
||
}
|
||
|
||
// refreshAdminHonorarSum: rechnet die Summe der eingetippten Anteile neu
|
||
// und färbt sie (grün bei 100 %, sonst Warnfarbe). Läuft bei jeder Eingabe,
|
||
// liest deshalb aus den Feldern und nicht aus adminHonorar.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function refreshAdminHonorarSum(){
|
||
const fieldsEl = document.getElementById('honorarAdminFields');
|
||
const sumEl = document.getElementById('honorarAdminSum');
|
||
if(!fieldsEl || !sumEl) return;
|
||
|
||
let summe = 0;
|
||
fieldsEl.querySelectorAll('input[data-lph]').forEach(inp => {
|
||
const v = parseFloat(inp.value);
|
||
if(Number.isFinite(v)) summe += v;
|
||
});
|
||
const ok = Math.abs(summe - 100) < 0.05;
|
||
sumEl.innerHTML = `<span class="honorar-sum ${ok ? 'ok' : 'warn'}">Summe ${summe.toFixed(1)} %</span>`
|
||
+ (ok ? '' : ' <span class="hint" style="margin:0">— die Sätze eines Leistungsbildes ergeben zusammen 100 %.</span>');
|
||
}
|
||
|
||
// saveHonorarConfig: schreibt Anteile und Stundensatz firmenweit
|
||
// (PUT /honorar-config, Admin-only). Danach wird der Zwischenspeicher des
|
||
// Honorar-Dialogs im Projektbereich verworfen, damit er nicht mit den alten
|
||
// Zahlen weiterrechnet. Keine Parameter, keine Rückgabe.
|
||
function saveHonorarConfig(){
|
||
const fieldsEl = document.getElementById('honorarAdminFields');
|
||
const anteile = {};
|
||
if(fieldsEl){
|
||
fieldsEl.querySelectorAll('input[data-lph]').forEach(inp => {
|
||
const v = parseFloat(inp.value);
|
||
if(Number.isFinite(v) && v > 0) anteile[inp.dataset.lph] = v;
|
||
});
|
||
}
|
||
const satzEl = document.getElementById('honorarStundensatz');
|
||
const stundensatz = satzEl ? parseFloat(satzEl.value) : undefined;
|
||
|
||
fetch(baseUrl() + '/honorar-config', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||
body: JSON.stringify({ anteile, stundensatz }),
|
||
})
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Speichern fehlgeschlagen.');
|
||
adminHonorar = { anteile: json.anteile || {}, stundensatz: Number(json.stundensatz) || 135 };
|
||
if(typeof invalidateHonorarConfig === 'function') invalidateHonorarConfig();
|
||
renderAdminHonorar();
|
||
showAdminNote('Honorar-Einstellungen gespeichert.');
|
||
})
|
||
.catch(err => {
|
||
console.error('Honorar-Konfiguration speichern fehlgeschlagen:', err);
|
||
alert('Einstellungen konnten nicht gespeichert werden: ' + (err.message || err));
|
||
});
|
||
}
|
||
|
||
|
||
// ---------- Abschnitt Feiertage ----------
|
||
|
||
// renderAdminHolidays: listet die gepflegten arbeitsfreien Tage
|
||
// chronologisch, mit Wochentag (ein Feiertag auf einem Samstag ändert an der
|
||
// Rechnung nichts — das soll man sehen) und Lösch-Knopf.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function renderAdminHolidays(){
|
||
renderHolidayYearHint();
|
||
renderHolidayStateSelect();
|
||
|
||
const el = document.getElementById('adminHolidayList');
|
||
if(!el) return;
|
||
const dates = Array.from(HOLIDAYS).sort();
|
||
if(dates.length === 0){
|
||
el.innerHTML = '<span class="hint">Noch keine arbeitsfreien Tage hinterlegt — es zählen nur Wochenenden.</span>';
|
||
return;
|
||
}
|
||
const wochentage = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
|
||
el.innerHTML = dates.map(d => {
|
||
const date = parseDateISO(d);
|
||
const tag = date ? wochentage[date.getDay()] : '';
|
||
const amWochenende = date && (date.getDay() === 0 || date.getDay() === 6);
|
||
return `<div class="template-admin-row">
|
||
<span class="template-admin-title">${esc(formatDateDE(date))} · ${esc(HOLIDAY_NAMES[d] || 'Feiertag')}</span>
|
||
<span class="hint" style="margin:0">${esc(tag)}${amWochenende ? ' — ohnehin arbeitsfrei' : ''}</span>
|
||
<button class="danger small" onclick="deleteHoliday('${esc(d)}')" title="Tag entfernen">×</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// addHoliday: legt einen arbeitsfreien Tag an (POST /holidays, Admin-only).
|
||
// Ein bereits vorhandenes Datum wird nur umbenannt — das Datum ist der
|
||
// Primärschlüssel. Keine Parameter, keine Rückgabe.
|
||
function addHoliday(){
|
||
const dateEl = document.getElementById('holidayNewDate');
|
||
const nameEl = document.getElementById('holidayNewName');
|
||
const date = dateEl ? dateEl.value : '';
|
||
if(!date){ alert('Bitte ein Datum wählen.'); return; }
|
||
|
||
fetch(baseUrl() + '/holidays', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||
body: JSON.stringify({ date, name: nameEl ? nameEl.value.trim() : '' }),
|
||
})
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Anlegen fehlgeschlagen.');
|
||
HOLIDAYS.add(json.holiday.date);
|
||
HOLIDAY_NAMES[json.holiday.date] = json.holiday.name || '';
|
||
if(nameEl) nameEl.value = '';
|
||
renderAdminHolidays();
|
||
showAdminNote('Arbeitsfreier Tag hinterlegt.');
|
||
})
|
||
.catch(err => {
|
||
console.error('Feiertag anlegen fehlgeschlagen:', err);
|
||
alert('Tag konnte nicht angelegt werden: ' + (err.message || err));
|
||
});
|
||
}
|
||
|
||
// deleteHoliday: entfernt einen arbeitsfreien Tag (DELETE, Admin-only).
|
||
// Bereits errechnete Terminpläne bleiben unberührt — sie sind nach dem
|
||
// Einfügen ganz normale Aufgaben. Parameter: date (ISO). Keine Rückgabe.
|
||
function deleteHoliday(date){
|
||
fetch(baseUrl() + '/holidays/' + encodeURIComponent(date), { method: 'DELETE' })
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Löschen fehlgeschlagen.');
|
||
HOLIDAYS.delete(date);
|
||
delete HOLIDAY_NAMES[date];
|
||
renderAdminHolidays();
|
||
})
|
||
.catch(err => {
|
||
console.error('Feiertag löschen fehlgeschlagen:', err);
|
||
alert('Tag konnte nicht entfernt werden: ' + (err.message || err));
|
||
});
|
||
}
|
||
|
||
// ---------- Abschnitt Aufgaben-Vorlagen ----------
|
||
|
||
// loadAdminTemplates: holt alle Vorlagen-Schritte (GET /lph-templates) und
|
||
// rendert die Liste der gerade gewählten Leistungsphase.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function loadAdminTemplates(){
|
||
const select = document.getElementById('adminTemplateLph');
|
||
if(select && !select.options.length){
|
||
select.innerHTML = LPHS.map(l => `<option value="${esc(l)}">${esc(l)}</option>`).join('');
|
||
}
|
||
const listEl = document.getElementById('templateAdminList');
|
||
if(listEl) listEl.innerHTML = '<span class="hint">Lade Vorlagen…</span>';
|
||
|
||
fetch(baseUrl() + '/lph-templates')
|
||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||
.then(data => {
|
||
adminTemplates = Array.isArray(data.templates) ? data.templates : [];
|
||
renderAdminTemplates();
|
||
})
|
||
.catch(err => {
|
||
console.error('Vorlagen laden fehlgeschlagen:', err);
|
||
if(listEl) listEl.innerHTML = '<span class="hint">Vorlagen konnten nicht geladen werden.</span>';
|
||
});
|
||
}
|
||
|
||
// renderAdminTemplates: zeigt die Schritte der gewählten Leistungsphase in
|
||
// ihrer gepflegten Reihenfolge, je Zeile mit Versatz/Dauer und Lösch-Knopf.
|
||
// Wird auch vom onchange der Leistungsphasen-Auswahl gerufen.
|
||
// Keine Parameter, keine Rückgabe.
|
||
function renderAdminTemplates(){
|
||
const listEl = document.getElementById('templateAdminList');
|
||
if(!listEl) return;
|
||
const select = document.getElementById('adminTemplateLph');
|
||
const lph = select ? select.value : '';
|
||
const entries = adminTemplates.filter(t => t.lph === lph);
|
||
|
||
listEl.innerHTML = entries.length === 0
|
||
? '<span class="hint">Für diese Leistungsphase ist noch kein Schritt hinterlegt.</span>'
|
||
: entries.map(t => `<div class="template-admin-row">
|
||
<span class="template-admin-title">${esc(t.title)}</span>
|
||
<span class="hint" style="margin:0">Start +${esc(t.offsetDays)} Tage · Dauer ${esc(t.durationDays)} Tage</span>
|
||
<button class="danger small" onclick="deleteTemplateEntry('${esc(t.id)}')" title="Schritt aus der Vorlage löschen">×</button>
|
||
</div>`).join('');
|
||
}
|
||
|
||
// addTemplateEntry: legt einen Schritt für die gewählte Leistungsphase an
|
||
// (POST /lph-templates, Admin-only). Keine Parameter, keine Rückgabe.
|
||
function addTemplateEntry(){
|
||
const select = document.getElementById('adminTemplateLph');
|
||
const titleEl = document.getElementById('templateNewTitle');
|
||
const offsetEl = document.getElementById('templateNewOffset');
|
||
const durationEl = document.getElementById('templateNewDuration');
|
||
const title = titleEl ? titleEl.value.trim() : '';
|
||
if(!title){ alert('Bitte eine Bezeichnung für den Schritt eingeben.'); return; }
|
||
|
||
fetch(baseUrl() + '/lph-templates', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||
body: JSON.stringify({
|
||
lph: select ? select.value : '',
|
||
title,
|
||
offsetDays: offsetEl ? parseInt(offsetEl.value, 10) || 0 : 0,
|
||
durationDays: durationEl ? parseInt(durationEl.value, 10) || 1 : 1,
|
||
})
|
||
})
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Anlegen fehlgeschlagen.');
|
||
if(titleEl) titleEl.value = '';
|
||
loadAdminTemplates();
|
||
showAdminNote('Schritt angelegt.');
|
||
})
|
||
.catch(err => {
|
||
console.error('Vorlagen-Schritt anlegen fehlgeschlagen:', err);
|
||
alert('Schritt konnte nicht angelegt werden: ' + (err.message || err));
|
||
});
|
||
}
|
||
|
||
// deleteTemplateEntry: löscht einen Vorlagen-Schritt (DELETE, Admin-only).
|
||
// Bereits in Projekte eingefügte Aufgaben bleiben unberührt — sie sind nach
|
||
// dem Einfügen ganz normale plan_todos ohne Verbindung zur Vorlage.
|
||
// Parameter: id (String). Keine Rückgabe.
|
||
function deleteTemplateEntry(id){
|
||
if(!confirm('Diesen Schritt aus der Vorlage löschen?\n\nBereits in Projekte eingefügte Aufgaben bleiben erhalten.')) return;
|
||
fetch(baseUrl() + '/lph-templates/' + encodeURIComponent(id), { method: 'DELETE' })
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Löschen fehlgeschlagen.');
|
||
loadAdminTemplates();
|
||
})
|
||
.catch(err => {
|
||
console.error('Vorlagen-Schritt löschen fehlgeschlagen:', err);
|
||
alert('Schritt konnte nicht gelöscht werden: ' + (err.message || err));
|
||
});
|
||
}
|
||
|
||
|
||
// ---------- Rückmeldung ----------
|
||
|
||
// showAdminNote: kurze Bestätigung am Fuß des Bereichs. Bewusst kein
|
||
// alert(): Ein erfolgreiches Speichern soll den Arbeitsfluss nicht anhalten,
|
||
// ein Fehlschlag dagegen schon (die Fehlerfälle oben nutzen weiter alert).
|
||
// Parameter: text (String). Keine Rückgabe.
|
||
function showAdminNote(text){
|
||
const el = document.getElementById('adminNote');
|
||
// Ein leerer Text bedeutet "nichts melden": So kann ein Speichervorgang,
|
||
// der nur nebenbei laeuft (etwa das gemerkte Bundesland beim Feiertags-
|
||
// Import), die eigentliche Rueckmeldung stehen lassen, statt sie zu
|
||
// ueberschreiben.
|
||
if(!el || !text) return;
|
||
el.textContent = text;
|
||
el.classList.add('show');
|
||
if(showAdminNote.timer) clearTimeout(showAdminNote.timer);
|
||
showAdminNote.timer = setTimeout(() => { el.classList.remove('show'); }, 2500);
|
||
}
|
||
|
||
// ---------- Gesetzliche Feiertage berechnen lassen ----------
|
||
//
|
||
// Die Tage kommen NICHT von einem Netzdienst, sondern aus dem Kalender:
|
||
// holidaysForYear() in server.js rechnet sie aus (feste Termine plus alles,
|
||
// was am Osterdatum haengt). Das spart eine Abhaengigkeit von fremder
|
||
// Verfuegbarkeit und einem Internetzugang des Servers, den es im Buero-Netz
|
||
// nicht zwingend gibt. Eingetragen werden sie erst auf Knopfdruck — sie
|
||
// verschieben jeden gerechneten Terminplan.
|
||
|
||
// renderHolidayStateSelect: fuellt Jahr und Bundesland vor. Das Bundesland
|
||
// steht als firmenweite Einstellung (feiertagBundesland), damit es nicht bei
|
||
// jedem Jahr neu gewaehlt werden muss. Keine Parameter, keine Rueckgabe.
|
||
function renderHolidayStateSelect(){
|
||
const jahrEl = document.getElementById('holidayYear');
|
||
if(jahrEl && !jahrEl.value) jahrEl.value = new Date().getFullYear();
|
||
|
||
const sel = document.getElementById('holidayState');
|
||
if(!sel) return;
|
||
const laender = Object.entries(GERMAN_STATES);
|
||
if(laender.length === 0) return; // /holidays noch nicht beantwortet
|
||
const gewaehlt = sel.value || APP_SETTINGS.feiertagBundesland || 'NW';
|
||
sel.innerHTML = laender
|
||
.map(([k, name]) => `<option value="${esc(k)}"${k === gewaehlt ? ' selected' : ''}>${esc(name)}</option>`)
|
||
.join('');
|
||
}
|
||
|
||
// renderHolidayYearHint: weist darauf hin, wenn fuer das laufende oder das
|
||
// kommende Jahr noch kein einziger Tag hinterlegt ist. Ohne diesen Hinweis
|
||
// faellt erst beim naechsten falsch gerechneten Terminplan auf, dass die
|
||
// Feiertage fehlen. Keine Parameter, keine Rueckgabe.
|
||
function renderHolidayYearHint(){
|
||
const el = document.getElementById('holidayYearHint');
|
||
if(!el) return;
|
||
const jahre = [new Date().getFullYear(), new Date().getFullYear() + 1];
|
||
const fehlend = jahre.filter(j => !Array.from(HOLIDAYS).some(d => d.startsWith(String(j))));
|
||
if(fehlend.length === 0){ el.style.display = 'none'; return; }
|
||
el.style.display = '';
|
||
el.textContent = fehlend.length === 2
|
||
? `Für ${fehlend[0]} und ${fehlend[1]} ist noch kein arbeitsfreier Tag hinterlegt.`
|
||
: `Für ${fehlend[0]} ist noch kein arbeitsfreier Tag hinterlegt.`;
|
||
}
|
||
|
||
// suggestHolidays: laesst sich die gesetzlichen Feiertage des gewaehlten
|
||
// Jahres und Bundeslandes berechnen (GET /holidays/suggest) und zeigt sie zur
|
||
// Durchsicht. Bereits hinterlegte Tage sind als solche gekennzeichnet und
|
||
// werden beim Uebernehmen nicht angetastet. Keine Parameter, keine Rueckgabe.
|
||
function suggestHolidays(){
|
||
const jahr = parseInt((document.getElementById('holidayYear') || {}).value, 10);
|
||
const land = (document.getElementById('holidayState') || {}).value || '';
|
||
const box = document.getElementById('holidaySuggestion');
|
||
if(!box) return;
|
||
if(!Number.isInteger(jahr)){ alert('Bitte ein Jahr eingeben.'); return; }
|
||
|
||
box.innerHTML = '<span class="hint">Wird berechnet…</span>';
|
||
fetch(baseUrl() + '/holidays/suggest?year=' + jahr + '&state=' + encodeURIComponent(land))
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Berechnung fehlgeschlagen.');
|
||
const neu = json.holidays.filter(h => !h.vorhanden).length;
|
||
box.innerHTML = `<div class="settings-holiday-list">`
|
||
+ json.holidays.map(h => `<div class="template-admin-row">
|
||
<span class="template-admin-title">${esc(formatDateDE(parseDateISO(h.date)))} · ${esc(h.name)}</span>
|
||
<span class="hint" style="margin:0">${h.vorhanden ? 'bereits hinterlegt' : 'neu'}</span>
|
||
</div>`).join('')
|
||
+ `</div>`
|
||
+ `<div class="toolbar" style="margin-top:10px">`
|
||
+ (neu > 0
|
||
? `<button class="secondary small" onclick="importHolidays()">${neu} Tag${neu === 1 ? '' : 'e'} übernehmen</button>`
|
||
: `<span class="hint" style="margin:0">Alle ${json.holidays.length} Tage sind bereits hinterlegt.</span>`)
|
||
+ `<button class="secondary small" onclick="document.getElementById('holidaySuggestion').innerHTML='';">Schließen</button>`
|
||
+ `</div>`;
|
||
})
|
||
.catch(err => {
|
||
console.error('Feiertage berechnen fehlgeschlagen:', err);
|
||
box.innerHTML = '<span class="hint">Feiertage konnten nicht berechnet werden: ' + esc(err.message || err) + '</span>';
|
||
});
|
||
}
|
||
|
||
// importHolidays: uebernimmt die berechneten Tage (POST /holidays/import,
|
||
// Admin-only). Bereits hinterlegte Tage bleiben unveraendert — ein von Hand
|
||
// angepasster Name soll nicht ueberschrieben werden. Das gewaehlte Bundesland
|
||
// wird als firmenweite Vorgabe gemerkt. Keine Parameter, keine Rueckgabe.
|
||
function importHolidays(){
|
||
const jahr = parseInt((document.getElementById('holidayYear') || {}).value, 10);
|
||
const land = (document.getElementById('holidayState') || {}).value || '';
|
||
|
||
fetch(baseUrl() + '/holidays/import', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||
body: JSON.stringify({ year: jahr, state: land }),
|
||
})
|
||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||
.then(({ ok, json }) => {
|
||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Übernehmen fehlgeschlagen.');
|
||
showAdminNote(json.angelegt + ' Feiertag(e) für ' + jahr + ' übernommen'
|
||
+ (json.uebersprungen ? ', ' + json.uebersprungen + ' waren schon hinterlegt.' : '.'));
|
||
// Gemerktes Bundesland nachziehen, damit der nächste Vorschlag passt.
|
||
// Ohne Rueckmeldung — die Zeile darueber gehoert dem Import.
|
||
if(land && land !== APP_SETTINGS.feiertagBundesland) putAppSettings({ feiertagBundesland: land }, '');
|
||
const box = document.getElementById('holidaySuggestion');
|
||
if(box) box.innerHTML = '';
|
||
return loadHolidaysIntoState();
|
||
})
|
||
.catch(err => {
|
||
console.error('Feiertage übernehmen fehlgeschlagen:', err);
|
||
alert('Feiertage konnten nicht übernommen werden: ' + (err.message || err));
|
||
});
|
||
}
|
||
|
||
// loadHolidaysIntoState: holt die Feiertage frisch vom Server in HOLIDAYS/
|
||
// HOLIDAY_NAMES (js/globals.js) und zeichnet die Liste neu. Nach einem Import
|
||
// sind es zu viele einzelne Tage, um sie von Hand nachzuhalten — und die
|
||
// Werktags-Rechnung (js/utils.js) muss sofort damit arbeiten.
|
||
// Rueckgabe: Promise.
|
||
function loadHolidaysIntoState(){
|
||
return fetch(baseUrl() + '/holidays')
|
||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||
.then(data => {
|
||
const liste = Array.isArray(data.holidays) ? data.holidays : [];
|
||
HOLIDAYS = new Set(liste.map(h => h.date));
|
||
HOLIDAY_NAMES = {};
|
||
liste.forEach(h => { HOLIDAY_NAMES[h.date] = h.name || ''; });
|
||
renderAdminHolidays();
|
||
})
|
||
.catch(err => console.error('Feiertage neu laden fehlgeschlagen:', err));
|
||
}
|