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:
1 parent
f4fb3c550b
commit
b7e0bd0d92
56 files changed
+12265
-2974
No files matched your search
@@ -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(); });
|
||||
}
|
||||
Reference in new issue
Block a user