diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index c4017d0..b4181e7 100644 --- a/groups/Tapops/css/documents.css +++ b/groups/Tapops/css/documents.css @@ -269,6 +269,7 @@ .search-container { flex: 1; + min-width: 260px; max-width: 500px; } @@ -316,7 +317,8 @@ } .date-range-group { - min-width: 320px; + min-width: 300px; + position: relative; } .control-label { @@ -347,94 +349,393 @@ pointer-events: none; } -/* --- Date Range Filter --- */ -.date-range-inputs { +/* --- Date Range Filter & Popover --- */ +.date-picker-trigger { + width: 100%; + padding: 14px 18px; + background: white; + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: inherit; + font-size: 0.9rem; + font-weight: 700; + color: var(--primary-blue); + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.date-picker-trigger:hover { + border-color: var(--secondary-gold); + background: #fffdf5; +} + +.date-picker-trigger:focus-visible { + outline: none; + border-color: var(--primary-blue); + box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); +} + +.date-picker-trigger .trigger-left-content { display: flex; align-items: center; gap: 10px; - background: #f8fafc; - border: 1px solid #e2e8f0; - padding: 4px 12px; - border-radius: 4px; - transition: all 0.2s ease; } -.date-range-inputs:focus-within { +.date-picker-trigger .trigger-arrow { + font-size: 0.8rem; + transition: transform 0.25s ease; + color: var(--primary-blue); +} + +.date-range-group.active .date-picker-trigger .trigger-arrow { + transform: rotate(180deg); +} + +.date-range-group.active .date-picker-trigger { + border-color: var(--primary-blue); + box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); +} + +/* Backdrop */ +.date-picker-backdrop { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + z-index: 40; +} + +/* Popover */ +.date-picker-popover { + position: absolute; + top: calc(100% + 6px); + right: 0; + width: 630px; + background: white; + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + display: none; + flex-direction: column; + z-index: 45; + overflow: hidden; +} + +.date-range-group.active .date-picker-popover { + display: flex; +} + +/* Top Bar */ +.date-picker-top-bar { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + background: #f8fafc; + display: flex; + justify-content: flex-end; +} + +.date-display-inputs { + display: flex; + align-items: center; + gap: 8px; +} + +.date-display-field input { + width: 110px; + padding: 6px 10px; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 600; + color: var(--primary-blue); + text-align: center; background: white; } -/* Custom dd/mm/yyyy date input */ -.custom-date-input { - position: relative; - display: flex; - align-items: center; - gap: 1px; - padding: 6px 0; -} - -.date-sep { - font-size: 0.85rem; +.date-picker-separator { + color: #94a3b8; font-weight: 700; - color: #94a3b8; - user-select: none; - padding: 0 2px; - flex-shrink: 0; } -.date-part { - border: none; - background: transparent; - font-family: inherit; - font-size: 0.85rem; - font-weight: 700; - color: var(--primary-blue); - text-align: center; - padding: 2px 4px; - border-radius: 3px; - transition: background 0.15s ease; - min-width: 0; -} - -.date-dd, .date-mm { width: 34px; } -.date-yyyy { width: 56px; } - -.date-part::placeholder { - color: #94a3b8; - font-weight: 400; -} - -.date-part:focus { - outline: none; - background: rgba(0, 49, 80, 0.07); -} - -.date-calendar-btn { - background: none; - border: none; - cursor: pointer; - color: #94a3b8; - padding: 4px 2px 4px 6px; +/* Body */ +.date-picker-body { display: flex; + background: white; +} + +/* Calendar Pane */ +.calendar-container { + flex: 1; + padding: 16px; +} + +.calendar-nav { + display: flex; + justify-content: space-between; align-items: center; - font-size: 0.9rem; - flex-shrink: 0; - transition: color 0.2s ease; -} - -.date-calendar-btn:hover { - color: var(--primary-blue); -} - -.date-hidden-picker { - position: absolute; - opacity: 0; - pointer-events: none; - width: 1px; - height: 1px; - border: none; + margin-bottom: 16px; padding: 0; - margin: 0; - overflow: hidden; +} + +.month-labels { + display: flex; + gap: 90px; + color: var(--primary-blue); + font-size: 0.95rem; +} + +.month-label { + display: flex; + align-items: center; + gap: 6px; + font-weight: 700; +} + +.year-select-wrapper { + position: relative; + display: inline-flex; + align-items: center; + border-radius: 4px; + transition: background 0.2s ease; +} + +.year-select-wrapper:hover { + background: #f1f5f9; +} + +.calendar-year-select { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background: transparent; + border: none; + padding: 2px 16px 2px 6px; + font-family: inherit; + font-size: 0.95rem; + font-weight: 700; + color: var(--primary-blue); + cursor: pointer; + outline: none; +} + +.year-select-wrapper .select-arrow { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 0.6rem; + color: var(--primary-blue); + pointer-events: none; +} + +.nav-btn { + background: transparent; + border: none; + color: #64748b; + cursor: pointer; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; +} + +.nav-btn:hover { + background: #f1f5f9; + color: var(--primary-blue); +} + +.calendar-grids { + display: flex; + gap: 20px; +} + +.calendar-grid-pane { + flex: 1; +} + +.day-headers { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; + font-size: 0.72rem; + font-weight: 700; + color: #94a3b8; + margin-bottom: 8px; +} + +.days-container { + display: grid; + grid-template-columns: repeat(7, 1fr); + row-gap: 4px; + column-gap: 2px; +} + +.day-cell { + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.82rem; + font-weight: 600; + color: #475569; + cursor: pointer; + border-radius: 4px; + transition: all 0.15s ease; + user-select: none; +} + +.day-cell:hover { + background: #f1f5f9; + color: var(--primary-blue); +} + +.day-cell.empty { + pointer-events: none; + visibility: hidden; +} + +.day-cell.disabled { + color: #cbd5e1; + pointer-events: none; +} + +.day-cell.range-start { + background: var(--primary-blue) !important; + color: white !important; + border-radius: 4px 0 0 4px; +} + +.day-cell.range-end { + background: var(--primary-blue) !important; + color: white !important; + border-radius: 0 4px 4px 0; +} + +.day-cell.range-start.range-end { + border-radius: 4px !important; +} + +.day-cell.range-between { + background: #eff6ff !important; + color: var(--primary-blue) !important; + border-radius: 0; +} + +/* Presets Pane */ +.presets-container { + width: 190px; + border-left: 1px solid var(--border-color); + padding: 16px; + background: white; + display: flex; + flex-direction: column; + gap: 12px; +} + +.presets-title { + font-size: 0.7rem; + font-weight: 800; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + display: block; +} + +.presets-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 240px; + overflow-y: auto; +} + +.preset-btn { + background: transparent; + border: none; + text-align: left; + padding: 8px 12px; + font-size: 0.8rem; + font-weight: 600; + color: #475569; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: space-between; + font-family: inherit; +} + +.preset-btn:hover { + background: #f8fafc; + color: var(--primary-blue); +} + +.preset-btn.active { + background: #eff6ff; + color: var(--primary-blue); +} + +.preset-btn.active::after { + content: '✓'; + font-weight: 700; + color: var(--primary-blue); + font-size: 0.85rem; +} + +/* Footer Action Buttons */ +.date-picker-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 12px 16px; + border-top: 1px solid var(--border-color); + background: #f8fafc; +} + +.action-btn { + padding: 8px 20px; + font-size: 0.85rem; + font-weight: 700; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; +} + +.cancel-btn { + background: white; + border: 1px solid #cbd5e1; + color: #475569; +} + +.cancel-btn:hover { + background: #f1f5f9; + border-color: #94a3b8; + color: #1e293b; +} + +.apply-btn { + background: var(--primary-blue); + border: 1px solid var(--primary-blue); + color: white; +} + +.apply-btn:hover { + background: #002238; + border-color: #002238; } .range-divider { @@ -713,68 +1014,83 @@ a.btn-action-outline:hover { .date-range-group { min-width: 0; + position: relative; } - /* Stack start/end date pickers vertically to avoid overflow */ - .date-range-inputs { - display: flex; - flex-direction: column; - gap: 6px; - border: none; - background: transparent; - padding: 0; - } - - .date-range-inputs .input-wrapper { - background: #f8fafc; - border: 1px solid #e2e8f0; - padding: 10px 12px; - border-radius: 4px; - display: flex; - align-items: center; - width: 100%; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + body.date-picker-open { overflow: hidden; } - .date-range-inputs .input-wrapper:focus-within { - background: white; - border-color: var(--primary-blue); - box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); + body.date-picker-open .date-picker-backdrop { + display: block; + z-index: 998; } - /* Custom date input fills the wrapper width */ - .custom-date-input { - width: 100%; - padding: 0; + .date-picker-popover { + /* Default mobile popover positioning inside media query is overridden below, + but let's define the base transitions or layouts. */ } - /* DD / MM stay compact; YYYY stretches to fill remaining space */ - .date-dd, .date-mm { - flex: 0 0 auto; - width: 30px; - } + @media (max-width: 768px) { + .date-picker-popover { + position: fixed; + top: 50%; + left: 50%; + right: auto; + transform: translate(-50%, -50%); + width: calc(100% - 32px); + max-width: 380px; + max-height: 90vh; + overflow-y: auto; + border-radius: 12px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1); + z-index: 999; + } - .date-yyyy { - flex: 1; - width: auto; - min-width: 44px; - text-align: left; - } + .date-picker-top-bar { + justify-content: center; + } - .date-part { - font-size: 0.82rem; - } + .calendar-grids { + gap: 0; + } - /* S/D label between pickers becomes a small inline tag */ - .range-divider { - font-size: 0.68rem; - font-weight: 800; - color: #94a3b8; - padding: 3px 10px; - text-transform: uppercase; - text-align: center; - align-self: center; + #right-calendar-pane { + display: none; + } + + .month-labels { + gap: 0; + justify-content: center; + flex: 1; + } + + #right-month-label { + display: none; + } + + .date-picker-body { + flex-direction: column; + } + + .presets-container { + width: 100%; + border-left: none; + border-top: 1px solid var(--border-color); + padding: 12px 16px; + } + + .presets-list { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; + max-height: none; + } + + .preset-btn { + padding: 6px 10px; + font-size: 0.75rem; + } } .select-wrapper, diff --git a/groups/Tapops/index.html b/groups/Tapops/index.html index 23ca707..d3d1b66 100644 --- a/groups/Tapops/index.html +++ b/groups/Tapops/index.html @@ -943,31 +943,86 @@
-
+
Periode Unggahan -
-
- - / - - / - - - + - + + + +
+ +
+
+
+
+ +
+ - +
+ +
+
+
+ +
+
+
+ +
+
+ Mei +
+ + +
+
+
+ Juni +
+ + +
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+ RENTANG WAKTU +
+ + + + + + + + + + +
+
+
+ +
diff --git a/groups/Tapops/js/i18n-tapops.js b/groups/Tapops/js/i18n-tapops.js index bf9ac7e..42f8209 100644 --- a/groups/Tapops/js/i18n-tapops.js +++ b/groups/Tapops/js/i18n-tapops.js @@ -43,6 +43,19 @@ sort_oldest: "Terlama", sort_az: "Nama A-Z", loading: "Menghubungkan ke repositori berkas...", + filter_all_time: "Semua Waktu", + presets_title: "RENTANG WAKTU", + preset_today: "Hari Ini", + preset_yesterday: "Kemarin", + preset_last_7_days: "7 Hari Terakhir", + preset_last_14_days: "14 Hari Terakhir", + preset_last_30_days: "30 Hari Terakhir", + preset_this_week: "Minggu Ini", + preset_last_week: "Minggu Lalu", + preset_this_month: "Bulan Ini", + preset_last_month: "Bulan Lalu", + btn_cancel: "Batal", + btn_update: "Perbarui", // Repository — dynamic (cards + states, used by main.js) card_open: "Buka Berkas", @@ -104,6 +117,19 @@ sort_oldest: "Oldest", sort_az: "Name A-Z", loading: "Connecting to the file repository...", + filter_all_time: "All Time", + presets_title: "DATE RANGE", + preset_today: "Today", + preset_yesterday: "Yesterday", + preset_last_7_days: "Last 7 Days", + preset_last_14_days: "Last 14 Days", + preset_last_30_days: "Last 30 Days", + preset_this_week: "This Week", + preset_last_week: "Last Week", + preset_this_month: "This Month", + preset_last_month: "Last Month", + btn_cancel: "Cancel", + btn_update: "Update", // Repository — dynamic (cards + states, used by main.js) card_open: "Open File", diff --git a/groups/Tapops/js/main.js b/groups/Tapops/js/main.js index 9d81bf3..fa1debc 100644 --- a/groups/Tapops/js/main.js +++ b/groups/Tapops/js/main.js @@ -412,6 +412,673 @@ class CustomDropdown { } } +/* =========================================== + DateRangePicker (Custom Calendar/Preset Popover) + =========================================== */ +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}`; + } +} + /* =========================================== DocumentRepository =========================================== */ @@ -429,21 +1096,6 @@ class DocumentRepository { this.statActiveEl = document.getElementById('stat-active-value'); this.statUpdatesEl = document.getElementById('stat-updates-value'); - this.dateStartParts = { - dd: document.getElementById('date-start-dd'), - mm: document.getElementById('date-start-mm'), - yyyy: document.getElementById('date-start-yyyy'), - picker: document.getElementById('date-start-picker'), - calBtn: document.getElementById('date-start-cal-btn'), - }; - this.dateEndParts = { - dd: document.getElementById('date-end-dd'), - mm: document.getElementById('date-end-mm'), - yyyy: document.getElementById('date-end-yyyy'), - picker: document.getElementById('date-end-picker'), - calBtn: document.getElementById('date-end-cal-btn'), - }; - this.cards = []; this.paginationContainer = null; this.state = { @@ -456,67 +1108,6 @@ class DocumentRepository { }; } - // Returns YYYY-MM-DD string only when dd, mm, and yyyy are all filled and valid. - _getDateString(parts) { - const dd = parts.dd?.value.trim() || ''; - const mm = parts.mm?.value.trim() || ''; - const yyyy = parts.yyyy?.value.trim() || ''; - - if (!dd || !mm || yyyy.length < 4) return ''; - - const d = parseInt(dd, 10); - const m = parseInt(mm, 10); - const y = parseInt(yyyy, 10); - - if (isNaN(d) || isNaN(m) || isNaN(y)) return ''; - if (d < 1 || d > 31 || m < 1 || m > 12 || y < 1 || y > 9999) return ''; - - return `${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`; - } - - // Wire up a set of dd/mm/yyyy text inputs: - // • numeric-only keydown guard - // • auto-advance dd→mm after 2 chars, mm→yyyy after 2 chars - // • backspace from empty field navigates back - // • calendar button opens native date picker; selection syncs to text inputs - // • calls onUpdate() on every change - _bindDateParts(parts, onUpdate) { - const { dd, mm, yyyy, picker, calBtn } = parts; - const CTRL_KEYS = new Set(['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter']); - - const setup = (input, prev, next) => { - if (!input) return; - input.addEventListener('keydown', (e) => { - if (!/^\d$/.test(e.key) && !CTRL_KEYS.has(e.key)) e.preventDefault(); - if (e.key === 'Backspace' && !input.value && prev) prev.focus(); - }); - input.addEventListener('input', () => { - input.value = input.value.replace(/\D/g, ''); - onUpdate(); - const max = parseInt(input.getAttribute('maxlength'), 10); - if (next && input.value.length >= max) next.focus(); - }); - }; - - setup(dd, null, mm); - setup(mm, dd, yyyy); - setup(yyyy, mm, null); - - // Calendar button → open native picker, then sync selection back to text inputs. - if (calBtn && picker) { - calBtn.addEventListener('click', () => { - try { picker.showPicker(); } catch { picker.click(); } - }); - picker.addEventListener('change', () => { - if (!picker.value) return; - const [y, m, d] = picker.value.split('-'); - if (dd) dd.value = d; - if (mm) mm.value = m; - if (yyyy) yyyy.value = y; - onUpdate(); - }); - } - } async init() { if (!this.gridContainer) return; @@ -662,17 +1253,20 @@ class DocumentRepository { this._applyFilters(); }); - this._bindDateParts(this.dateStartParts, () => { - this.state.startDate = this._getDateString(this.dateStartParts); - this.state.page = 1; - this._applyFilters(); - }); - - this._bindDateParts(this.dateEndParts, () => { - this.state.endDate = this._getDateString(this.dateEndParts); - this.state.page = 1; - this._applyFilters(); - }); + // Initialize Custom Date Range Picker + const datePickerGroup = document.getElementById('date-range-control-group'); + if (datePickerGroup) { + this.dateRangePicker = new DateRangePicker({ + containerId: 'date-range-control-group', + onChange: (startDate, endDate) => { + this.state.startDate = startDate; + this.state.endDate = endDate; + this.state.page = 1; + this._applyFilters(); + } + }); + window.applyTp?.(datePickerGroup); + } if (this.sortFilterWrapper) { const sortOptions = [