Files
projektuebersicht/public/js/date-picker.js
erik 5b20adee48 All new
alle new
2026-10-01 06:46:51 +02:00

221 lines
9.6 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 - 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 = `<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">`;
['Mo','Di','Mi','Do','Fr','Sa','So'].forEach(w => html += `<div class="range-picker-weekday">${w}</div>`);
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);
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;
}