Files
erikandClaude Opus 5.5 4c2e04e78b Kleine Verbesserungen und Teams, Outlook Integration
- Outlook: Kalender-Abo je Konto (eigene Aufgaben, Meilensteine,
  Abwesenheiten als .ics), einzurichten unter Einstellungen
- Teams: Nachricht in einen Kanal bei neuen Aufgaben, mit Link direkt
  auf die Aufgabe; einzurichten unter Admin → Teams
- Sitzungen nach Bedarf laden: beim Start nur die aktuelle und drei
  davor, im Verlauf „Weitere laden“ in Fünferschritten
- Termin aus Mail: Wahl Aufgabe/Meilenstein, Notiz auch bei
  Meilensteinen, nur aktiv mit eingerichteter KI
- Kalender zeigt die KW, auch statt der Browser-Datumsfelder
- Pfeile bei den Abwesenheiten, kleinere Server-Korrekturen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:23:02 +01:00

431 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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" title="Voriger Monat">${icon('chevronLeft')}</button>
<strong>${esc(monatName)}</strong>
<button type="button" class="abw-rund" onclick="abwMonatWechseln(1)" aria-label="Nächster Monat" title="Nächster Monat">${icon('chevronRight')}</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(); });
}