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

246 lines
11 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.
// -----------------------------
// Projektübersicht — Datumsbereich-Picker
// Ein Kalender-Popup für Von+Bis: erster Klick setzt das Startdatum,
// zweiter Klick das Enddatum (bei Klick vor dem Start wird getauscht).
// Schreibt ISO-Werte in zwei (versteckte) Inputs und aktualisiert optional
// ein Anzeige-Element. Verwendung: openRangePicker({ anchor, startInputId, endInputId, displayId, onApply })
// -----------------------------
// Diese Datei wird nach js/utils.js von beiden HTML-Einstiegspunkten geladen
// und stellt den Kalender-Picker (Einzeldatum "openDatePicker" und Bereich
// "openRangePicker") bereit. Der Zustand des offenen Popups liegt in der
// Modulvariablen rangePickerState. Genutzt wird der Picker v. a. von
// js/load/load.js (LP-Bereiche), js/people/people.js (Abwesenheiten) und den
// Plan-Modulen unter js/plan/* (z. B. GANTT-Datumsangaben in plan-modals.js).
// rangePickerState: Zustand des aktuell geöffneten Picker-Popups (Optionen,
// Start-/Enddatum, Ansichtsmonat); null solange kein Popup offen ist.
let rangePickerState = null;
// openDatePicker: öffnet den Kalender zur Auswahl eines einzelnen Datums
// Parameter: opts mit inputId, optional anchor, displayId, onApply. Rückgabe: keine.
function openDatePicker(opts){
closeRangePicker();
const input = document.getElementById(opts.inputId);
if(!input) return;
const preValue = parseDateISO(input.value);
const today = new Date();
rangePickerState = {
opts: Object.assign({}, opts, { mode: 'single' }),
start: preValue,
end: preValue,
mode: 'single',
// Ansichtsmonat: vorhandener Wert, sonst heutiger Monat
viewYear: (preValue || today).getFullYear(),
viewMonth: (preValue || today).getMonth()
};
const pop = document.createElement('div');
pop.id = 'rangePickerPopup';
pop.className = 'range-picker';
document.body.appendChild(pop);
const anchorRect = opts.anchor ? opts.anchor.getBoundingClientRect() : { left: window.innerWidth/2 - 140, bottom: window.innerHeight/2 - 160 };
pop.style.left = Math.max(8, Math.min(anchorRect.left, window.innerWidth - 332)) + 'px';
pop.style.top = Math.min(anchorRect.bottom + 6, window.innerHeight - 340) + 'px';
renderRangePicker();
setTimeout(() => {
document.addEventListener('mousedown', rangePickerOutsideClick);
}, 0);
}
// openRangePicker: öffnet den Kalender zur Auswahl eines Von-Bis-Datumsbereichs
// (oder eines Einzeldatums bei mode:'single'). Parameter: opts mit startInputId,
// endInputId, optional anchor, displayId, mode, onApply. Rückgabe: keine.
function openRangePicker(opts){
closeRangePicker();
const isSingle = (opts.mode || 'range') === 'single';
const startInput = document.getElementById(isSingle ? (opts.inputId || opts.startInputId) : opts.startInputId);
const endInput = isSingle ? null : document.getElementById(opts.endInputId);
if(!startInput || (!isSingle && !endInput)) return;
const preStart = parseDateISO(startInput.value);
const preEnd = isSingle ? preStart : parseDateISO(endInput.value);
const today = new Date();
rangePickerState = {
opts: Object.assign({}, opts, { mode: isSingle ? 'single' : 'range' }),
start: preStart,
end: preEnd,
mode: isSingle ? 'single' : 'range',
// Ansichtsmonat: vorhandener Start, sonst heutiger Monat
viewYear: (preStart || today).getFullYear(),
viewMonth: (preStart || today).getMonth()
};
const pop = document.createElement('div');
pop.id = 'rangePickerPopup';
pop.className = 'range-picker';
document.body.appendChild(pop);
// Position: unterhalb des Ankers, innerhalb des Viewports halten
const anchorRect = opts.anchor ? opts.anchor.getBoundingClientRect() : { left: window.innerWidth/2 - 140, bottom: window.innerHeight/2 - 160 };
pop.style.left = Math.max(8, Math.min(anchorRect.left, window.innerWidth - 332)) + 'px';
pop.style.top = Math.min(anchorRect.bottom + 6, window.innerHeight - 340) + 'px';
renderRangePicker();
// Klick außerhalb schließt das Popup (ohne Übernahme)
setTimeout(() => {
document.addEventListener('mousedown', rangePickerOutsideClick);
}, 0);
}
// rangePickerOutsideClick: schließt das Popup, wenn außerhalb davon geklickt wird
// Parameter: e (Mousedown-Event). Rückgabe: keine.
function rangePickerOutsideClick(e){
const pop = document.getElementById('rangePickerPopup');
if(pop && !pop.contains(e.target)) closeRangePicker();
}
// closeRangePicker: entfernt das Popup wieder und setzt rangePickerState zurück
// Parameter: keine. Rückgabe: keine.
function closeRangePicker(){
const pop = document.getElementById('rangePickerPopup');
if(pop) pop.remove();
document.removeEventListener('mousedown', rangePickerOutsideClick);
rangePickerState = null;
}
// rangePickerNav: blättert im Kalender um einen Monat vor/zurück und rendert neu
// Parameter: delta (Number, -1 oder 1). Rückgabe: keine.
function rangePickerNav(delta){
if(!rangePickerState) return;
let m = rangePickerState.viewMonth + delta;
let y = rangePickerState.viewYear;
if(m < 0){ m = 11; y--; }
if(m > 11){ m = 0; y++; }
rangePickerState.viewMonth = m;
rangePickerState.viewYear = y;
renderRangePicker();
}
// rangePickerSelect: übernimmt einen Klick auf einen Kalendertag; im Bereichsmodus
// wird beim ersten Klick der Start und beim zweiten das Ende (ggf. getauscht) gesetzt
// Parameter: iso (String, ausgewähltes Datum). Rückgabe: keine.
function rangePickerSelect(iso){
if(!rangePickerState) return;
const d = parseDateISO(iso);
if(!d) return;
const st = rangePickerState;
if(st.mode === 'single'){
st.start = d;
st.end = d;
applyRangePicker();
return;
}
if(!st.start || (st.start && st.end)){
// Neuen Bereich beginnen
st.start = d;
st.end = null;
} else {
// Zweiter Klick: Ende setzen, bei Bedarf tauschen
if(d < st.start){ st.end = st.start; st.start = d; }
else st.end = d;
applyRangePicker();
return;
}
renderRangePicker();
}
// applyRangePicker: schreibt die gewählten Daten in die (versteckten) Inputs bzw.
// das Anzeige-Element, schließt das Popup und ruft den onApply-Callback auf
// Parameter: keine. Rückgabe: keine.
function applyRangePicker(){
if(!rangePickerState) return;
const { opts, start, end } = rangePickerState;
const isSingle = (opts.mode || 'range') === 'single';
const startInput = document.getElementById(isSingle ? (opts.inputId || opts.startInputId) : opts.startInputId);
const endInput = isSingle ? null : document.getElementById(opts.endInputId);
const selected = start || end;
if(startInput) startInput.value = isSingle ? (selected ? formatDateISO(selected) : '') : (start ? formatDateISO(start) : '');
if(endInput) endInput.value = end ? formatDateISO(end) : (start ? formatDateISO(start) : '');
// Für umgeleitete native Datumsfelder (siehe unten): deren onchange/
// oninput-Handler sollen wie bei einer Eingabe von Hand laufen.
[startInput, endInput].forEach(el => {
if(el && el.type === 'date'){
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
});
if(opts.displayId){
const disp = document.getElementById(opts.displayId);
if(disp){
const from = start ? formatDateDE(start) : '';
const to = end ? formatDateDE(end) : from;
disp.value = isSingle ? (from || '') : (from ? (from + ' – ' + to) : '');
}
}
const cb = opts.onApply;
closeRangePicker();
if(typeof cb === 'function') cb();
}
// renderRangePicker: baut die Kalender-HTML für den aktuellen Ansichtsmonat und
// zeigt Start/Ende-Auswahl markiert an. Parameter: keine. Rückgabe: keine.
function renderRangePicker(){
const pop = document.getElementById('rangePickerPopup');
if(!pop || !rangePickerState) return;
const st = rangePickerState;
const monthNames = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const first = new Date(st.viewYear, st.viewMonth, 1);
// Wochentag des Monatsersten, Montag = 0
const firstWeekday = (first.getDay() + 6) % 7;
const daysInMonth = new Date(st.viewYear, st.viewMonth + 1, 0).getDate();
const today = new Date();
const todayMid = new Date(today.getFullYear(), today.getMonth(), today.getDate());
let hint;
if(st.mode === 'single') hint = 'Datum wählen';
else if(!st.start || (st.start && st.end)) hint = 'Startdatum wählen';
else hint = 'Enddatum wählen (Start: ' + formatDateDE(st.start) + ')';
let html = `<div class="range-picker-head">
<button type="button" class="secondary small" onclick="rangePickerNav(-1)">${icon('chevronLeft')}</button>
<span class="range-picker-title">${monthNames[st.viewMonth]} ${st.viewYear}</span>
<button type="button" class="secondary small" onclick="rangePickerNav(1)">${icon('chevronRight')}</button>
</div>
<div class="range-picker-hint">${hint}</div>
<div class="range-picker-grid">`;
// Erste Spalte: Kalenderwoche (Eriks Wunsch — im Büro wird in KW gedacht).
html += '<div class="range-picker-weekday range-picker-kw-kopf">KW</div>';
['Mo','Di','Mi','Do','Fr','Sa','So'].forEach(w => html += `<div class="range-picker-weekday">${w}</div>`);
const kwZelle = d => `<div class="range-picker-kw">${getWeekNumber(d)}</div>`;
if(firstWeekday > 0) html += kwZelle(first);
for(let i = 0; i < firstWeekday; i++) html += '<div></div>';
for(let day = 1; day <= daysInMonth; day++){
const d = new Date(st.viewYear, st.viewMonth, day);
if(d.getDay() === 1) html += kwZelle(d);
const iso = formatDateISO(d);
const classes = ['range-picker-day'];
if(d.getDay() === 0 || d.getDay() === 6) classes.push('weekend');
if(d.getTime() === todayMid.getTime()) classes.push('today');
if(st.start && d.getTime() === st.start.getTime()) classes.push('sel-start');
if(st.end && d.getTime() === st.end.getTime()) classes.push('sel-end');
if(st.start && st.end && d > st.start && d < st.end) classes.push('in-range');
html += `<div class="${classes.join(' ')}" onclick="rangePickerSelect('${iso}')">${day}</div>`;
}
html += '</div>';
html += `<div class="range-picker-footer">
<button type="button" class="secondary small" onclick="closeRangePicker()">Abbrechen</button>
${st.mode !== 'single' && st.start && !st.end ? '<button type="button" class="small" onclick="rangePickerState.end = rangePickerState.start; applyRangePicker()">Nur dieser Tag</button>' : ''}
</div>`;
pop.innerHTML = html;
}
// Native Datumsfelder (<input type="date">) öffnen statt des Browser-
// Kalenders (kennt keine KW) denselben Kalender wie der Rest der App. Das
// Feld selbst bleibt — Tippen funktioniert weiter, der Picker schreibt den
// ISO-Wert hinein und löst input/change aus. Ausnahme: data-nativ.
document.addEventListener('click', e => {
const input = e.target && e.target.closest ? e.target.closest('input[type="date"]') : null;
if(!input || input.disabled || input.readOnly || input.hasAttribute('data-nativ')) return;
e.preventDefault();
if(!input.id) input.id = 'datum_' + Math.random().toString(36).slice(2, 9);
openDatePicker({ anchor: input, inputId: input.id });
}, true);