// ----------------------------- // 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.js (LP-Bereiche), js/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 - 296)) + '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 - 296)) + '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) : ''); 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 = `
${monthNames[st.viewMonth]} ${st.viewYear}
${hint}
`; ['Mo','Di','Mi','Do','Fr','Sa','So'].forEach(w => html += `
${w}
`); for(let i = 0; i < firstWeekday; i++) html += '
'; for(let day = 1; day <= daysInMonth; day++){ const d = new Date(st.viewYear, st.viewMonth, day); 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 += `
${day}
`; } html += '
'; html += ``; pop.innerHTML = html; }