Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb

- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan
  oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues
  Notizfeld je Aufgabe (plan_todos.notiz)
- Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile,
  "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen
- Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang
- Glas-Darstellung, Logo als Favicon
- Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und
  Prüfskript inkl. WLAN-Wächter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-10-29 14:16:52 +01:00
1 parent f4fb3c550b
commit b7e0bd0d92
56 files changed
+12265 -2974

No files matched your search

+430
View File
@@ -0,0 +1,430 @@
// js/abwesenheit/abwesenheit.js — Bereich „Abwesenheiten“ (Tab abwesenheit,
// Eriks Wahl „Entwurf A“): oben die Schnellerfassung, darunter die Liste der
// gewählten Person, unten die Team-Zeitleiste eines Monats.
//
// Rechte (Rollen & Rechte, Gruppe „Team“): team.abw (für mich),
// team.abwProjekte, team.abwGewerk, team.abwAlle. Der Server rechnet daraus
// je Person `darfAbw` (loadGlobalPeople(req.user)) und schickt Abwesenheiten
// ANDERER Personen nur als „abwesend“ ohne Art und Notiz (`verdeckt`) — Art
// und Notiz sieht nur die Person selbst und der Administrator. Verdeckte Einträge lassen sich
// deshalb nicht ändern (die Art ist unbekannt), nur löschen. Diese Datei
// verlässt sich darauf, entscheidet selbst also nichts über Rechte.
//
// Daten: db.globalPeople (aus /load-all bzw. GET /personen). Nach jedem
// Speichern lädt personenNachladen() (js/store.js) die Liste neu; andere
// Browser bekommen das Live-Ereignis {typ:'personen'}.
const ABW_ARTEN = ['Urlaub', 'Krank', 'Fortbildung', 'Sonstiges'];
const ABW_KLASSE = { Urlaub: 'abw-u', Krank: 'abw-k', Fortbildung: 'abw-f', Sonstiges: 'abw-s' };
let abwMonat = null; // Date: erster Tag des angezeigten Monats
let abwFilter = 'Alle'; // Gewerk-Filter der Zeitleiste
let abwFuer = null; // userId der Person, für die eingetragen wird
let abwBearbeiten = null; // id der Abwesenheit, die gerade geändert wird
let abwVergangen = false; // Liste: vergangene statt kommende
let abwLoeschFrage = null; // id, deren Löschen gerade bestätigt werden soll
let abwFehler = '';
let abwEntwurf = { start: '', end: '', type: 'Urlaub', note: '' };
// darfIrgendeineAbwesenheit: ob die Rolle überhaupt Abwesenheiten eintragen
// darf (Menüpunkt sichtbar). Rückgabe: Boolean.
function darfIrgendeineAbwesenheit(){
return ['team.abw', 'team.abwProjekte', 'team.abwGewerk', 'team.abwAlle'].some(r => hatRecht(r));
}
function abwPersonen(){ return Array.isArray(db.globalPeople) ? db.globalPeople : []; }
function abwIch(){ return abwPersonen().find(p => currentUser && p.userId === currentUser.id) || null; }
function abwPerson(userId){ return abwPersonen().find(p => p.userId === userId) || null; }
function abwName(p){ return p ? (p.fullName || p.name || '') : ''; }
// abwDatum: "2026-11-02" -> "02.11." (mit Jahr, wenn mitJahr). Rückgabe: String.
function abwDatum(iso, mitJahr){
const d = parseDateISO(iso);
if(!d) return '';
const s = String(d.getDate()).padStart(2, '0') + '.' + String(d.getMonth() + 1).padStart(2, '0') + '.';
return mitJahr ? s + d.getFullYear() : s;
}
// abwZeitraum: lesbarer Zeitraum einer Abwesenheit.
function abwZeitraum(a){
if(a.start === a.end) return abwDatum(a.start, true);
const gleichesJahr = a.start.slice(0, 4) === a.end.slice(0, 4);
return abwDatum(a.start, !gleichesJahr) + ' – ' + abwDatum(a.end, true);
}
function abwArbeitstage(start, end){
const a = parseDateISO(start), b = parseDateISO(end);
return (a && b) ? workdaysBetween(a, b) : 0;
}
function abwUeberlappt(a1, e1, a2, e2){ return a1 <= e2 && a2 <= e1; }
// ---------- Rendern ----------
// Gestaltung „Variante 2 im Stil der Projektübersicht“ (Eriks Wahl): dieselben
// Bausteine wie die Projektliste — Titelzeile mit Legende (.legend), Glas-
// Karten (.board-list), Zeilen (.board-row/.board-row-main, .board-nr,
// .board-name, .board-status-line, .phase-pill, .board-row-actions) und
// Kürzel-Kreise in Fachrollen-Farbe (.board-avatar.role-*). Links „Neue
// Abwesenheit“, rechts die Liste der gewählten Person, darunter das Team je
// Gewerk in einer eigenen Karte.
const ABW_WOCHENTAG = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
const ABW_WOCHENTAG_LANG = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
// abwRollen: alle Fachrollen der Person in der festen Reihenfolge von ROLES
// (die gespeicherte Reihenfolge ist je Konto zufällig). Rückgabe: Array.
function abwRollen(p){
const eigene = Array.isArray(p && p.role) ? p.role : [];
return ROLES.filter(r => eigene.includes(r)).concat(eigene.filter(r => !ROLES.includes(r)));
}
// abwKreis: Kürzel-Kreis wie in der Projektliste, Farbe nach der ersten
// Fachrolle der Person (Reihenfolge ROLES). Rückgabe: HTML.
function abwKreis(p){
if(!p) return '';
const rollen = abwRollen(p);
const rolle = rollen.length ? roleClass(rollen[0]) : '';
return `<span class="board-avatar ${rolle} state-1 abw-kreis" title="${esc(abwName(p))}">${esc(p.name || '')}</span>`;
}
function renderAbwesenheiten(){
const el = document.getElementById('abwesenheit');
if(!el) return;
if(!darfIrgendeineAbwesenheit()){
el.innerHTML = '<div class="section-title">Abwesenheiten</div><p class="hint">Deine Rolle darf keine Abwesenheiten eintragen.</p>';
return;
}
if(!abwMonat){ const h = new Date(); abwMonat = new Date(h.getFullYear(), h.getMonth(), 1); }
if(abwFuer === null || !abwPerson(abwFuer) || !abwPerson(abwFuer).darfAbw) abwFuer = abwStandardFuer();
const erlaubte = abwPersonen().filter(p => p.darfAbw);
el.innerHTML = `
<div class="abw-zwei">
<div class="abw-spalte">${abwFuer !== null ? abwFormularHtml(erlaubte) : abwOhnePersonHtml()}</div>
<div class="abw-spalte">${abwFuer !== null ? abwListeHtml() : ''}</div>
</div>
${abwTeamHtml()}`;
}
// abwStandardFuer: wer in der Erfassung vorausgewählt ist — immer die Person,
// die die Seite öffnet; hat ihr Konto keine Person (bzw. kein Recht für sich),
// die erste Person, für die sie eintragen darf. Rückgabe: userId oder null.
function abwStandardFuer(){
const ich = abwIch();
if(ich && ich.darfAbw) return ich.userId;
const erste = abwPersonen().filter(p => p.darfAbw).sort((a, b) => abwName(a).localeCompare(abwName(b), 'de'))[0];
return erste ? erste.userId : null;
}
// abwBeimOeffnen: beim Betreten des Bereichs (showTab) — Erfassung leer, für
// die eigene Person.
function abwBeimOeffnen(){
abwFuer = abwStandardFuer(); abwBearbeiten = null; abwLoeschFrage = null; abwFehler = '';
abwEntwurf = { start: '', end: '', type: 'Urlaub', note: '' };
}
// abwOhnePersonHtml: Platzhalter-Karte, wenn niemand wählbar ist.
function abwOhnePersonHtml(){
return `<section class="board-list abw-karte"><div class="section-title abw-karten-titel">Neue Abwesenheit</div><p class="hint" style="margin:0">Für dein Konto lässt sich keine Abwesenheit eintragen: Es ist keiner Person zugeordnet (Person-ID fehlt). Ein Administrator kann das in der Benutzerverwaltung nachtragen.</p></section>`;
}
// abwFormularHtml: „Neue Abwesenheit“ (bzw. „Abwesenheit ändern“) als Karte.
function abwFormularHtml(erlaubte){
const p = abwPerson(abwFuer);
const e = abwEntwurf;
const ich = abwIch();
const istIch = p === ich;
const fuerAuswahl = erlaubte.length > 1 && !abwBearbeiten
? `<select id="abwFuer" class="abw-fuer-wahl" onchange="abwFuerWaehlen(this.value)" aria-label="Für wen">
${erlaubte.slice().sort((a, b) => (a === ich ? -1 : b === ich ? 1 : abwName(a).localeCompare(abwName(b), 'de')))
.map(x => `<option value="${x.userId}" ${x.userId === abwFuer ? 'selected' : ''}>${esc(x === ich ? 'Für mich' : abwName(x))}</option>`).join('')}
</select>`
: '';
const kachel = (titel, iso, unter) => {
const d = parseDateISO(iso);
return `<button type="button" class="abw-datum" onclick="abwZeitraumOeffnen(this)">
<small>${titel}</small>
<b>${d ? abwDatum(iso) : '–'}</b>
<span>${d ? ABW_WOCHENTAG_LANG[d.getDay()] + (unter || '') : 'Datum wählen'}</span>
</button>`;
};
const tage = e.start ? abwArbeitstage(e.start, e.end) : 0;
const warnungen = e.start ? abwWarnungen(p, e.start, e.end) : [];
return `<section class="board-list abw-karte" aria-label="Abwesenheit eintragen">
<div class="section-title abw-karten-titel">${abwBearbeiten ? 'Abwesenheit ändern' : 'Neue Abwesenheit'}</div>
<div class="abw-fuer">
${abwKreis(p)}
<span><b>${istIch ? 'Für mich' : 'Für ' + esc(abwName(p))}</b>${istIch ? ' · ' + esc(abwName(p)) : (p && p.gewerk ? ' · ' + esc(p.gewerk) : '')}</span>
${fuerAuswahl}
</div>
<div class="abw-datumzeile">
${kachel('Von', e.start, '')}
<span class="abw-pfeil" aria-hidden="true">→</span>
${kachel('Bis', e.end || e.start, e.start ? ' · ' + tage + (tage === 1 ? ' Arbeitstag' : ' Arbeitstage') : '')}
</div>
<input type="hidden" id="abwStart" value="${esc(e.start)}">
<input type="hidden" id="abwEnde" value="${esc(e.end)}">
<div class="abw-arten" role="group" aria-label="Art">
${ABW_ARTEN.map(a => `<button type="button" class="${ABW_KLASSE[a]}${a === e.type ? ' ist-an' : ''}" aria-pressed="${a === e.type}" onclick="abwArtWaehlen('${a}')"><i></i>${a}</button>`).join('')}
</div>
<div class="abw-unten">
<input type="text" id="abwNotiz" class="abw-notiz" placeholder="Notiz (optional)" value="${esc(e.note)}" oninput="abwEntwurf.note = this.value">
${abwBearbeiten ? '<button type="button" class="secondary small" onclick="abwAbbrechen()">Abbrechen</button>' : ''}
<button type="button" onclick="abwSpeichern()">${abwBearbeiten ? 'Speichern' : 'Eintragen'}</button>
</div>
${abwFehler ? `<div class="abw-fehler" role="alert">${esc(abwFehler)}</div>` : ''}
${warnungen.map(w => `<div class="abw-warnung">${w}</div>`).join('')}
</section>`;
}
// abwWarnungen: Hinweise zum gewählten Zeitraum — Überschneidung mit einer
// eigenen Abwesenheit der Person, ihre offenen Aufgaben im Zeitraum und
// Kollegen desselben Gewerks, die ebenfalls fehlen. Rückgabe: HTML-Strings.
function abwWarnungen(p, start, end){
if(!p) return [];
const w = [];
const doppelt = (p.absences || []).filter(a => a.id !== abwBearbeiten && abwUeberlappt(start, end, a.start, a.end));
if(doppelt.length) w.push('Überschneidet sich mit ' + doppelt.map(a => esc((a.verdeckt ? 'abwesend' : a.type) + ' ' + abwZeitraum(a))).join(', ') + '.');
const wk = typeof currentWeek === 'function' ? currentWeek() : null;
const aufgaben = [];
((wk && wk.projects) || []).forEach(pr => {
((pr.plan && pr.plan.todos) || []).forEach(t => {
if(t.done || !Array.isArray(t.assignees) || !t.assignees.includes(p.id)) return;
const tEnde = t.end || t.start;
if(!tEnde) return;
if(abwUeberlappt(start, end, t.start || tEnde, tEnde)) aufgaben.push(`„${esc(t.title || 'Aufgabe')}“ (${esc(pr.name || '')}, ${abwDatum(tEnde)})`);
});
});
if(aufgaben.length){
const zuviel = aufgaben.length - 3;
w.push(`In diesen Zeitraum ${aufgaben.length === 1 ? 'fällt 1 offene Aufgabe' : 'fallen ' + aufgaben.length + ' offene Aufgaben'}: ${aufgaben.slice(0, 3).join(', ')}${zuviel > 0 ? ' und ' + zuviel + ' weitere' : ''}.`);
}
if(p.gewerk){
const andere = abwPersonen().filter(x => x !== p && x.gewerk === p.gewerk
&& (x.absences || []).some(a => abwUeberlappt(start, end, a.start, a.end)));
if(andere.length) w.push(`${andere.map(x => esc(abwName(x))).join(', ')} (${esc(p.gewerk)}) ${andere.length === 1 ? 'ist' : 'sind'} in dieser Zeit ebenfalls weg.`);
}
return w;
}
// abwListeHtml: Abwesenheiten der gewählten Person als Zeilen wie in der
// Projektliste — Startdatum in der Nummernspalte, Kalenderwoche als Kapsel.
function abwListeHtml(){
const p = abwPerson(abwFuer);
if(!p) return '';
const heute = heuteIso();
const alle = (p.absences || []).slice().sort((a, b) => a.start.localeCompare(b.start));
const kommend = alle.filter(a => a.end >= heute);
const vergangen = alle.filter(a => a.end < heute).reverse();
const liste = abwVergangen ? vergangen : kommend;
const titel = p === abwIch() ? 'Meine Abwesenheiten' : 'Abwesenheiten von ' + abwName(p);
const zeilen = liste.map(a => {
const tage = abwArbeitstage(a.start, a.end);
const ds = parseDateISO(a.start), de = parseDateISO(a.end);
const kwA = getWeekNumber(ds), kwE = getWeekNumber(de);
const zr = ABW_WOCHENTAG[ds.getDay()] + ' ' + abwDatum(a.start, a.start.slice(0, 4) !== a.end.slice(0, 4))
+ (a.start === a.end ? a.start.slice(0, 4) : ' – ' + ABW_WOCHENTAG[de.getDay()] + ' ' + abwDatum(a.end, true));
const fragt = abwLoeschFrage === a.id;
const verdeckt = !!a.verdeckt;
const aendern = verdeckt ? '' : ` onclick="abwAendern('${esc(a.id)}')"`;
return `<div class="board-row abw-zeile${abwBearbeiten === a.id ? ' ist-aktiv' : ''} ${verdeckt ? 'abw-verdeckt-zeile' : (ABW_KLASSE[a.type] || 'abw-s')}">
<div class="board-row-main"${aendern}>
<span class="board-nr">${abwDatum(a.start)}</span>
<div class="board-name-wrap">
<span class="board-name abw-zeile-name"><i class="abw-punkt"></i>${verdeckt ? 'abwesend' : esc(a.type)}<span class="abw-tag">${tage} ${tage === 1 ? 'Arbeitstag' : 'Arbeitstage'}</span></span>
<div class="board-status-line">${esc(zr)}${!verdeckt && a.note ? ' · ' + esc(a.note) : ''}</div>
</div>
<div class="board-row-actions${fragt ? ' abw-fragt' : ''}" onclick="event.stopPropagation()">
${verdeckt ? '' : `<button type="button" class="secondary small" onclick="abwAendern('${esc(a.id)}')">Ändern</button>`}
<button type="button" class="danger small" onclick="abwLoeschen('${esc(a.id)}')">${fragt ? 'Wirklich löschen?' : 'Löschen'}</button>
</div>
<span class="phase-pill">KW ${kwA}${kwE !== kwA ? '–' + kwE : ''}</span>
</div>
</div>`;
}).join('');
return `<section class="board-list abw-listen-karte">
<div class="abw-listen-kopf">
<div class="section-title abw-karten-titel">${esc(titel)}</div>
<span class="abw-reiter" role="group">
<button type="button" class="${abwVergangen ? '' : 'ist-an'}" onclick="abwVergangenZeigen(false)">Kommende ${kommend.length}</button>
<button type="button" class="${abwVergangen ? 'ist-an' : ''}" onclick="abwVergangenZeigen(true)">Vergangene ${vergangen.length}</button>
</span>
</div>
<div class="abw-listen-zeilen">${zeilen || `<div class="abw-leer">${abwVergangen ? 'Keine vergangenen Abwesenheiten.' : 'Keine kommenden Abwesenheiten.'}</div>`}</div>
</section>`;
}
// abwTeamHtml: Kopf des Team-Monats und je Gewerk eine Karte (wie die
// Projektgruppen), Personen ohne Gewerk zuletzt.
function abwTeamHtml(){
const monatName = abwMonat.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
const gewerke = Object.keys(DEPARTMENTS);
const sichtbar = abwFilter === 'Alle' ? gewerke.concat(['']) : [abwFilter];
const karten = sichtbar.map(g => {
const ps = abwPersonen().filter(p => (p.gewerk || '') === g);
return ps.length ? abwGewerkKarteHtml(g, ps) : '';
}).join('');
return `<div class="bericht-titelzeile abw-team-kopf">
<div class="section-title" style="margin:0">Team im ${esc(abwMonat.toLocaleDateString('de-DE', { month: 'long' }))}</div>
<span class="abw-team-steuer">
<span class="abw-monat">
<button type="button" class="abw-rund" onclick="abwMonatWechseln(-1)" aria-label="Voriger Monat">‹</button>
<strong>${esc(monatName)}</strong>
<button type="button" class="abw-rund" onclick="abwMonatWechseln(1)" aria-label="Nächster Monat">›</button>
<button type="button" class="secondary small" onclick="abwMonatWechseln(0)">Heute</button>
</span>
<span class="abw-reiter" role="group" aria-label="Gewerk">${['Alle'].concat(gewerke).map(g => `<button type="button" class="${g === abwFilter ? 'ist-an' : ''}" onclick="abwFilterSetzen('${g}')">${g}</button>`).join('')}</span>
</span>
</div>
${karten || '<p class="hint">Keine Personen in diesem Gewerk.</p>'}`;
}
// abwGewerkKarteHtml: eine Karte je Gewerk — Tageskopf, dann eine Zeile je
// Person mit Kürzel-Kreis und je Abwesenheit ein Balken.
function abwGewerkKarteHtml(gewerk, personen){
const jahr = abwMonat.getFullYear(), monat = abwMonat.getMonth();
const tage = new Date(jahr, monat + 1, 0).getDate();
const heute = heuteIso();
const ich = abwIch();
const iso = d => jahr + '-' + String(monat + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0');
const ersterIso = iso(1), letzterIso = iso(tage);
const sortiert = personen.slice().sort((a, b) => (a === ich ? -1 : b === ich ? 1 : abwName(a).localeCompare(abwName(b), 'de')));
const darfIrgendwen = sortiert.some(p => p.darfAbw);
let h = `<div class="abw-tl-wer abw-tl-kopf"><small>${sortiert.length} ${sortiert.length === 1 ? 'Person' : 'Personen'}</small></div>`;
for(let d = 1; d <= tage; d++){
const dt = new Date(jahr, monat, d);
h += `<div class="abw-tl-tag${!isWorkday(dt) ? ' ist-frei' : ''}${iso(d) === heute ? ' ist-heute' : ''}"><small>${ABW_WOCHENTAG[dt.getDay()]}</small><b>${d}</b></div>`;
}
sortiert.forEach(p => {
const sichtbar = (p.absences || []).filter(a => abwUeberlappt(a.start, a.end, ersterIso, letzterIso));
let tageImMonat = 0;
if(p.darfAbw) sichtbar.forEach(a => {
tageImMonat += abwArbeitstage(a.start < ersterIso ? ersterIso : a.start, a.end > letzterIso ? letzterIso : a.end);
});
const rolle = abwRollen(p).join(', ');
const meta = p === ich ? 'Ich' + (rolle ? ' · ' + rolle : '') : (p.darfAbw ? (rolle || 'ohne Fachrolle') : 'nur „abwesend“');
const waehlen = p.darfAbw ? ` ist-klickbar" role="button" tabindex="0" title="Abwesenheiten von ${esc(abwName(p))} anzeigen und eintragen" onclick="abwPersonAusTabelle(${p.userId})" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();abwPersonAusTabelle(${p.userId})}` : '';
h += `<div class="abw-tl-wer${p.userId === abwFuer ? ' ist-gewaehlt' : ''}${waehlen}">
${abwKreis(p)}
<span class="abw-tl-wer-text"><span class="abw-tl-wer-name">${esc(abwName(p))}</span>
<span class="abw-tl-wer-meta" title="${esc(meta)}">${esc(meta)}${tageImMonat ? ' · ' + tageImMonat + (tageImMonat === 1 ? ' Tag' : ' Tage') + ' im ' + abwMonat.toLocaleDateString('de-DE', { month: 'short' }).replace('.', '') + '.' : ''}</span></span>
</div>`;
for(let d = 1; d <= tage; d++){
const dt = new Date(jahr, monat, d);
const tagIso = iso(d);
let balken = '';
sichtbar.forEach(a => {
const beginn = a.start < ersterIso ? ersterIso : a.start;
if(beginn !== tagIso) return;
const ende = a.end > letzterIso ? letzterIso : a.end;
const len = Number(ende.slice(8)) - d + 1;
const verdeckt = a.verdeckt || !p.darfAbw;
const text = verdeckt ? 'abwesend' : a.type;
const titel = abwName(p) + ': ' + text + ' ' + abwZeitraum(a) + (!verdeckt && a.note ? ' — ' + a.note : '');
balken += `<span class="abw-balken ${verdeckt ? 'abw-verdeckt' : (ABW_KLASSE[a.type] || 'abw-s')}" style="width:calc(${len * 100}% - 4px)" title="${esc(titel)}"
${!verdeckt ? `onclick="event.stopPropagation(); abwAendernFuer(${p.userId}, '${esc(a.id)}')"` : ''}>${len > 1 && !verdeckt ? esc(text) : ''}</span>`;
});
const klick = p.darfAbw ? ` onclick="abwTagGewaehlt(${p.userId}, '${tagIso}')"` : '';
h += `<div class="abw-tl-zelle${!isWorkday(dt) ? ' ist-frei' : ''}${tagIso === heute ? ' ist-heute' : ''}${p.darfAbw ? ' ist-klickbar' : ''}"${klick}>${balken}</div>`;
}
});
return `<div class="abw-gewerk-kopf"><div class="section-title" style="margin:0">${esc(gewerk || 'Ohne Gewerk')}</div>${darfIrgendwen ? '' : '<small>nur Ansicht</small>'}</div>
<div class="board-list abw-tl-karte">
<div class="abw-tl" style="grid-template-columns: 230px repeat(${tage}, minmax(22px, 1fr))">${h}</div>
</div>`;
}
// ---------- Bedienung ----------
function abwFuerWaehlen(userId){
abwFuer = Number(userId);
abwLoeschFrage = null; abwFehler = '';
renderAbwesenheiten();
}
// abwPersonAusTabelle: Klick auf eine Person in der Team-Tabelle — sie wird
// oben in Erfassung und Liste gewählt (eine laufende Änderung wird verworfen).
// Die Seite bleibt, wo sie ist (Eriks Wunsch, kein Springen nach oben).
function abwPersonAusTabelle(userId){
abwBearbeiten = null;
abwEntwurf = { start: '', end: '', type: abwEntwurf.type || 'Urlaub', note: '' };
abwFuerWaehlen(userId);
}
// abwZeitraumOeffnen: Von- oder Bis-Kachel öffnet den Bereichs-Kalender.
function abwZeitraumOeffnen(anker){
openRangePicker({ anchor: anker, startInputId: 'abwStart', endInputId: 'abwEnde', onApply: abwZeitraumGewaehlt });
}
function abwArtWaehlen(art){ abwEntwurf.type = art; renderAbwesenheiten(); }
function abwZeitraumGewaehlt(){
abwEntwurf.start = document.getElementById('abwStart').value;
abwEntwurf.end = document.getElementById('abwEnde').value || abwEntwurf.start;
abwFehler = '';
renderAbwesenheiten();
}
function abwVergangenZeigen(v){ abwVergangen = !!v; abwLoeschFrage = null; renderAbwesenheiten(); }
function abwFilterSetzen(g){ abwFilter = g; renderAbwesenheiten(); }
function abwMonatWechseln(n){
if(n === 0){ const h = new Date(); abwMonat = new Date(h.getFullYear(), h.getMonth(), 1); }
else abwMonat = new Date(abwMonat.getFullYear(), abwMonat.getMonth() + n, 1);
renderAbwesenheiten();
}
// abwTagGewaehlt: Klick auf einen leeren Tag der Zeitleiste — Schnellerfassung
// für diese Person und diesen Tag vorbelegen.
function abwTagGewaehlt(userId, tagIso){
abwFuer = userId; abwBearbeiten = null; abwLoeschFrage = null; abwFehler = '';
abwEntwurf = { start: tagIso, end: tagIso, type: abwEntwurf.type || 'Urlaub', note: '' };
renderAbwesenheiten();
const f = document.querySelector('#abwesenheit .abw-datum');
if(f){ f.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
}
function abwAendernFuer(userId, id){ abwFuer = userId; abwAendern(id); }
function abwAendern(id){
const p = abwPerson(abwFuer);
const a = p && (p.absences || []).find(x => x.id === id);
if(!a || a.verdeckt) return;
abwBearbeiten = id; abwLoeschFrage = null; abwFehler = '';
abwEntwurf = { start: a.start, end: a.end, type: ABW_ARTEN.includes(a.type) ? a.type : 'Sonstiges', note: a.note || '' };
renderAbwesenheiten();
const f = document.querySelector('#abwesenheit .abw-datum');
if(f) f.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
function abwAbbrechen(){
abwBearbeiten = null; abwFehler = ''; abwFuer = abwStandardFuer();
abwEntwurf = { start: '', end: '', type: 'Urlaub', note: '' };
renderAbwesenheiten();
}
// abwAnfrage: Aufruf der Abwesenheits-Routen. Rückgabe: Promise<{ok, json}>.
function abwAnfrage(methode, pfad, daten){
return fetch(baseUrl() + pfad, {
method: methode,
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: daten ? JSON.stringify(daten) : undefined
}).then(r => r.json().catch(() => ({})).then(json => ({ ok: r.ok && json && json.ok, json })));
}
function abwSpeichern(){
const e = abwEntwurf;
e.note = ((document.getElementById('abwNotiz') || {}).value || '').trim();
if(!e.start){ abwFehler = 'Bitte einen Zeitraum wählen.'; renderAbwesenheiten(); return; }
const pfad = '/users/' + abwFuer + '/absences' + (abwBearbeiten ? '/' + encodeURIComponent(abwBearbeiten) : '');
abwAnfrage(abwBearbeiten ? 'PATCH' : 'POST', pfad, { start: e.start, end: e.end || e.start, type: e.type, note: e.note })
.then(({ ok, json }) => {
if(!ok){ abwFehler = (json && json.error) || 'Die Abwesenheit konnte nicht gespeichert werden.'; renderAbwesenheiten(); return; }
abwBearbeiten = null; abwFehler = ''; abwFuer = abwStandardFuer();
abwEntwurf = { start: '', end: '', type: 'Urlaub', note: '' };
return personenNachladen().then(renderAbwesenheiten);
})
.catch(() => { abwFehler = 'Keine Verbindung zum Server.'; renderAbwesenheiten(); });
}
// abwLoeschen: zweistufig im Knopf („Löschen“ → „Wirklich löschen?“).
function abwLoeschen(id){
if(abwLoeschFrage !== id){ abwLoeschFrage = id; renderAbwesenheiten(); return; }
abwLoeschFrage = null;
abwAnfrage('DELETE', '/users/' + abwFuer + '/absences/' + encodeURIComponent(id))
.then(({ ok, json }) => {
if(!ok){ abwFehler = (json && json.error) || 'Die Abwesenheit konnte nicht gelöscht werden.'; renderAbwesenheiten(); return; }
if(abwBearbeiten === id) abwAbbrechen();
return personenNachladen().then(renderAbwesenheiten);
})
.catch(() => { abwFehler = 'Keine Verbindung zum Server.'; renderAbwesenheiten(); });
}