export class DateRangePicker { constructor({ containerId, onChange, initialStartDate = '', initialEndDate = '' }) { this.container = document.getElementById(containerId); if (!this.container) return; this.trigger = this.container.querySelector('#date-picker-trigger'); this.popover = this.container.querySelector('#date-picker-popover'); this.backdrop = this.container.querySelector('#date-picker-backdrop'); this.label = this.container.querySelector('#date-picker-label'); this.startDisplay = this.container.querySelector('#picker-start-display'); this.endDisplay = this.container.querySelector('#picker-end-display'); this.prevBtn = this.container.querySelector('#prev-month-btn'); this.nextBtn = this.container.querySelector('#next-month-btn'); this.leftMonthLabel = this.container.querySelector('#left-month-label'); this.rightMonthLabel = this.container.querySelector('#right-month-label'); this.leftMonthName = this.container.querySelector('#left-month-name'); this.rightMonthName = this.container.querySelector('#right-month-name'); this.leftYearSelect = this.container.querySelector('#left-year-select'); this.rightYearSelect = this.container.querySelector('#right-year-select'); this.leftPane = this.container.querySelector('#left-calendar-pane'); this.rightPane = this.container.querySelector('#right-calendar-pane'); this.leftDaysContainer = this.container.querySelector('#left-days-container'); this.rightDaysContainer = this.container.querySelector('#right-days-container'); this.cancelBtn = this.container.querySelector('#picker-cancel-btn'); this.applyBtn = this.container.querySelector('#picker-apply-btn'); this.onChange = onChange; // Active dates (saved) this.startDate = initialStartDate ? new Date(initialStartDate) : null; this.endDate = initialEndDate ? new Date(initialEndDate) : null; // Temporary dates (being edited) this.tempStartDate = this.startDate ? new Date(this.startDate) : null; this.tempEndDate = this.endDate ? new Date(this.endDate) : null; // Calendar view month/year const today = new Date(); if (this.endDate) { this.currentLeftMonth = this.endDate.getMonth(); this.currentLeftYear = this.endDate.getFullYear(); } else { this.currentLeftMonth = today.getMonth(); this.currentLeftYear = today.getFullYear(); } this.isOpen = false; this._init(); } _init() { this._populateYearSelects(); this._bindEvents(); this._bindDisplayInputs(); this._updateTriggerLabel(); // Listen to language changes document.addEventListener('languageChanged', () => { if (this.isOpen) { this._renderCalendars(); } this._updateTriggerLabel(); }); } _isEnglish() { if (typeof window.getLang === 'function') { const l = window.getLang(); if (l === 'id' || l === 'en') return l === 'en'; } try { const saved = localStorage.getItem('if_untan_lang'); if (saved === 'id' || saved === 'en') return saved === 'en'; } catch (e) {} const nav = (navigator.language || 'id').toLowerCase(); return nav.startsWith('en'); } _clearTime(date) { if (!date) return null; const d = new Date(date); d.setHours(0, 0, 0, 0); return d; } _datesEqual(d1, d2) { if (!d1 || !d2) return false; return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); } _getRightMonthYear() { let m = this.currentLeftMonth + 1; let y = this.currentLeftYear; if (m > 11) { m = 0; y += 1; } return { month: m, year: y }; } _navigateMonths(direction) { this.currentLeftMonth += direction; if (this.currentLeftMonth > 11) { this.currentLeftMonth = 0; this.currentLeftYear += 1; } else if (this.currentLeftMonth < 0) { this.currentLeftMonth = 11; this.currentLeftYear -= 1; } this._renderCalendars(); } _renderCalendars() { const isEn = this._isEnglish(); const monthNames = isEn ? ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] : ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember']; const dayHeaders = isEn ? ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] : ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']; if (this.leftMonthName) { this.leftMonthName.textContent = monthNames[this.currentLeftMonth]; } if (this.leftYearSelect) { this.leftYearSelect.value = this.currentLeftYear; } const rightMY = this._getRightMonthYear(); if (this.rightMonthName) { this.rightMonthName.textContent = monthNames[rightMY.month]; } if (this.rightYearSelect) { this.rightYearSelect.value = rightMY.year; } const drawHeaders = (pane) => { if (!pane) return; const dayHeadersEl = pane.querySelector('.day-headers'); if (!dayHeadersEl) return; dayHeadersEl.innerHTML = ''; dayHeaders.forEach(day => { const span = document.createElement('span'); span.textContent = day; dayHeadersEl.appendChild(span); }); }; drawHeaders(this.leftPane); drawHeaders(this.rightPane); if (this.leftDaysContainer) { this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear); } if (this.rightDaysContainer) { this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year); } this._updateDisplayInputs(); this._updatePresetSelectionVisuals(); } _populateDaysContainer(container, month, year) { container.innerHTML = ''; const firstDay = new Date(year, month, 1); const numDays = new Date(year, month + 1, 0).getDate(); let offset = (firstDay.getDay() + 6) % 7; for (let i = 0; i < offset; i++) { const cell = document.createElement('div'); cell.className = 'day-cell empty'; container.appendChild(cell); } const today = new Date(); today.setHours(23, 59, 59, 999); for (let day = 1; day <= numDays; day++) { const cell = document.createElement('div'); cell.className = 'day-cell'; cell.textContent = day; const cellDate = new Date(year, month, day); cellDate.setHours(0, 0, 0, 0); if (cellDate > today) { cell.classList.add('disabled'); } else { if (this.tempStartDate && this.tempEndDate) { const start = this._clearTime(this.tempStartDate); const end = this._clearTime(this.tempEndDate); if (this._datesEqual(cellDate, start)) { cell.classList.add('range-start'); } if (this._datesEqual(cellDate, end)) { cell.classList.add('range-end'); } if (cellDate > start && cellDate < end) { cell.classList.add('range-between'); } } else if (this.tempStartDate && this._datesEqual(cellDate, this._clearTime(this.tempStartDate))) { cell.classList.add('range-start'); cell.classList.add('range-end'); } cell.addEventListener('click', () => { this._handleDayClick(cellDate); }); } container.appendChild(cell); } } _handleDayClick(date) { if (!this.tempStartDate || (this.tempStartDate && this.tempEndDate)) { this.tempStartDate = new Date(date); this.tempEndDate = null; } else { if (date < this.tempStartDate) { this.tempStartDate = new Date(date); this.tempEndDate = null; } else { this.tempEndDate = new Date(date); } } this._renderCalendars(); } _formatDateDisplay(date) { if (!date) return ''; const d = new Date(date); const dd = String(d.getDate()).padStart(2, '0'); const mm = String(d.getMonth() + 1).padStart(2, '0'); const yyyy = d.getFullYear(); return `${dd}/${mm}/${yyyy}`; } _updateDisplayInputs() { if (this.startDisplay) { this.startDisplay.value = this._formatDateDisplay(this.tempStartDate); } if (this.endDisplay) { this.endDisplay.value = this._formatDateDisplay(this.tempEndDate); } } _calculatePreset(presetName) { const today = new Date(); today.setHours(0, 0, 0, 0); let start = null; let end = null; switch (presetName) { case 'today': start = new Date(today); end = new Date(today); break; case 'yesterday': start = new Date(today); start.setDate(today.getDate() - 1); end = new Date(start); break; case 'last7': end = new Date(today); start = new Date(today); start.setDate(today.getDate() - 6); break; case 'last14': end = new Date(today); start = new Date(today); start.setDate(today.getDate() - 13); break; case 'last30': end = new Date(today); start = new Date(today); start.setDate(today.getDate() - 29); break; case 'thisWeek': end = new Date(today); start = new Date(today); const day = today.getDay(); const diff = today.getDate() - day + (day === 0 ? -6 : 1); start.setDate(diff); break; case 'lastWeek': const temp = new Date(today); const tempDay = temp.getDay(); const tempDiff = temp.getDate() - tempDay + (tempDay === 0 ? -6 : 1); start = new Date(temp); start.setDate(tempDiff - 7); end = new Date(temp); end.setDate(tempDiff - 1); break; case 'thisMonth': start = new Date(today.getFullYear(), today.getMonth(), 1); end = new Date(today); break; case 'lastMonth': start = new Date(today.getFullYear(), today.getMonth() - 1, 1); end = new Date(today.getFullYear(), today.getMonth(), 0); break; case 'all': default: start = null; end = null; break; } return { start, end }; } _handlePresetClick(presetName) { const { start, end } = this._calculatePreset(presetName); this.tempStartDate = start; this.tempEndDate = end; const today = new Date(); if (this.tempEndDate) { this.currentLeftMonth = this.tempEndDate.getMonth(); this.currentLeftYear = this.tempEndDate.getFullYear(); } else { this.currentLeftMonth = today.getMonth(); this.currentLeftYear = today.getFullYear(); } this._renderCalendars(); } _updatePresetSelectionVisuals() { this.container.querySelectorAll('.preset-btn').forEach(btn => { btn.classList.remove('active'); }); if (!this.tempStartDate && !this.tempEndDate) { this.container.querySelector('[data-preset="all"]')?.classList.add('active'); return; } if (this.tempStartDate && this.tempEndDate) { const activePreset = ['today', 'yesterday', 'last7', 'last14', 'last30', 'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth'].find(preset => { const { start, end } = this._calculatePreset(preset); return start && end && this._datesEqual(this.tempStartDate, start) && this._datesEqual(this.tempEndDate, end); }); if (activePreset) { this.container.querySelector(`[data-preset="${activePreset}"]`)?.classList.add('active'); } } } _updateTriggerLabel() { if (!this.startDate && !this.endDate) { if (this.label) { this.label.textContent = window.tpT?.('filter_all_time') || 'Semua Waktu'; this.label.setAttribute('data-tp-i18n', 'filter_all_time'); } } else { if (this.label) { this.label.removeAttribute('data-tp-i18n'); const startFormatted = this._formatDateDisplay(this.startDate); const endFormatted = this._formatDateDisplay(this.endDate); if (this._datesEqual(this.startDate, this.endDate)) { this.label.textContent = startFormatted; } else { this.label.textContent = `${startFormatted} - ${endFormatted}`; } } } } _bindEvents() { this.trigger.addEventListener('click', (e) => { e.stopPropagation(); this.toggle(); }); this.popover.addEventListener('click', (e) => { e.stopPropagation(); }); if (this.prevBtn) { this.prevBtn.addEventListener('click', (e) => { e.stopPropagation(); this._navigateMonths(-1); }); } if (this.nextBtn) { this.nextBtn.addEventListener('click', (e) => { e.stopPropagation(); this._navigateMonths(1); }); } const presetsList = this.container.querySelector('.presets-list'); if (presetsList) { presetsList.addEventListener('click', (e) => { const btn = e.target.closest('.preset-btn'); if (!btn) return; e.stopPropagation(); const preset = btn.getAttribute('data-preset'); this._handlePresetClick(preset); }); } if (this.cancelBtn) { this.cancelBtn.addEventListener('click', (e) => { e.stopPropagation(); this._handleCancel(); }); } if (this.applyBtn) { this.applyBtn.addEventListener('click', (e) => { e.stopPropagation(); this._handleApply(); }); } if (this.backdrop) { this.backdrop.addEventListener('click', (e) => { e.stopPropagation(); this._handleCancel(); }); } document.addEventListener('click', () => { if (this.isOpen) { this._handleCancel(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen) { this._handleCancel(); } }); } _bindDisplayInputs() { const parseDate = (val) => { const m = val.trim().match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/); if (!m) return null; const d = parseInt(m[1], 10); const mon = parseInt(m[2], 10) - 1; const y = parseInt(m[3], 10); if (mon < 0 || mon > 11 || d < 1 || d > 31 || y < 1000 || y > 9999) return null; const testDate = new Date(y, mon, d); if (testDate.getFullYear() !== y || testDate.getMonth() !== mon || testDate.getDate() !== d) { return null; } return testDate; }; const setupInput = (input, isStart) => { if (!input) return; input.addEventListener('keydown', (e) => { const CTRL_KEYS = new Set(['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter']); if (!/^\d$/.test(e.key) && e.key !== '/' && e.key !== '-' && e.key !== '.' && !CTRL_KEYS.has(e.key)) { e.preventDefault(); } }); input.addEventListener('input', (e) => { if (e.inputType && e.inputType.startsWith('delete')) { return; } let val = input.value.replace(/[^\d]/g, ''); let formatted = val; if (val.length > 2) { formatted = val.substring(0, 2) + '/' + val.substring(2); } if (val.length > 4) { formatted = val.substring(0, 2) + '/' + val.substring(2, 4) + '/' + val.substring(4, 8); } input.value = formatted; const parsed = parseDate(formatted); if (parsed) { const today = new Date(); today.setHours(23, 59, 59, 999); if (parsed <= today) { if (isStart) { this.tempStartDate = parsed; if (this.tempEndDate && this.tempEndDate < this.tempStartDate) { this.tempEndDate = new Date(this.tempStartDate); } } else { this.tempEndDate = parsed; if (this.tempStartDate && this.tempStartDate > this.tempEndDate) { this.tempStartDate = new Date(this.tempEndDate); } } this.currentLeftMonth = parsed.getMonth(); this.currentLeftYear = parsed.getFullYear(); const rightMY = this._getRightMonthYear(); const isEn = this._isEnglish(); const monthNames = isEn ? ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] : ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember']; if (this.leftMonthName) this.leftMonthName.textContent = monthNames[this.currentLeftMonth]; if (this.leftYearSelect) this.leftYearSelect.value = this.currentLeftYear; if (this.rightMonthName) this.rightMonthName.textContent = monthNames[rightMY.month]; if (this.rightYearSelect) this.rightYearSelect.value = rightMY.year; if (this.leftDaysContainer) this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear); if (this.rightDaysContainer) this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year); this._updatePresetSelectionVisuals(); } } }); input.addEventListener('blur', () => { const parsed = parseDate(input.value); if (!parsed) { if (isStart) { input.value = this._formatDateDisplay(this.tempStartDate); } else { input.value = this._formatDateDisplay(this.tempEndDate); } } }); }; setupInput(this.startDisplay, true); setupInput(this.endDisplay, false); } _populateYearSelects() { if (!this.leftYearSelect || !this.rightYearSelect) return; this.leftYearSelect.innerHTML = ''; this.rightYearSelect.innerHTML = ''; const startYear = 2020; const endYear = new Date().getFullYear() + 4; for (let y = startYear; y <= endYear; y++) { const optLeft = document.createElement('option'); optLeft.value = y; optLeft.textContent = y; this.leftYearSelect.appendChild(optLeft); const optRight = document.createElement('option'); optRight.value = y; optRight.textContent = y; this.rightYearSelect.appendChild(optRight); } this.leftYearSelect.addEventListener('change', () => { this._handleYearChange(parseInt(this.leftYearSelect.value, 10), true); }); this.rightYearSelect.addEventListener('change', () => { this._handleYearChange(parseInt(this.rightYearSelect.value, 10), false); }); } _handleYearChange(year, isLeft) { if (isLeft) { this.currentLeftYear = year; } else { const rightMY = this._getRightMonthYear(); const rightMonth = rightMY.month; if (rightMonth === 0) { this.currentLeftYear = year - 1; } else { this.currentLeftYear = year; } } this._renderCalendars(); } toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } open() { document.querySelectorAll('.custom-dropdown.active').forEach(dd => { dd.classList.remove('active'); dd.querySelector('.custom-dropdown-trigger')?.setAttribute('aria-expanded', 'false'); }); this.isOpen = true; this.container.classList.add('active'); this.trigger.setAttribute('aria-expanded', 'true'); this.tempStartDate = this.startDate ? new Date(this.startDate) : null; this.tempEndDate = this.endDate ? new Date(this.endDate) : null; const today = new Date(); if (this.endDate) { this.currentLeftMonth = this.endDate.getMonth(); this.currentLeftYear = this.endDate.getFullYear(); } else { this.currentLeftMonth = today.getMonth(); this.currentLeftYear = today.getFullYear(); } this._renderCalendars(); if (window.innerWidth <= 768) { document.body.classList.add('date-picker-open'); } } close() { this.isOpen = false; this.container.classList.remove('active'); this.trigger.setAttribute('aria-expanded', 'false'); document.body.classList.remove('date-picker-open'); } _handleCancel() { this.tempStartDate = this.startDate ? new Date(this.startDate) : null; this.tempEndDate = this.endDate ? new Date(this.endDate) : null; this.close(); } _handleApply() { this.startDate = this.tempStartDate; this.endDate = this.tempEndDate; this._updateTriggerLabel(); this.close(); const startStr = this._getDateString(this.startDate); const endStr = this._getDateString(this.endDate); if (this.onChange) { this.onChange(startStr, endStr); } } _getDateString(date) { if (!date) return ''; const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } }