diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index 19ae003..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,52 +349,393 @@ pointer-events: none; } -/* --- Date Range Filter --- */ -.date-range-inputs { - 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 Range Filter & Popover --- */ +.date-picker-trigger { + width: 100%; + padding: 14px 18px; background: white; -} - -.date-range-inputs .input-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.date-range-inputs input[type="date"] { - border: none; - background: transparent; - padding: 8px 0; + border: 1px solid var(--border-color); + border-radius: 4px; font-family: inherit; - font-size: 0.85rem; + font-size: 0.9rem; font-weight: 700; color: var(--primary-blue); cursor: pointer; - min-width: 120px; + 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-range-inputs input[type="date"]:focus { +.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; +} + +.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; +} + +.date-picker-separator { + color: #94a3b8; + font-weight: 700; +} + +/* 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; + margin-bottom: 16px; + padding: 0; +} + +.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; } -.date-range-inputs input[type="date"]::-webkit-calendar-picker-indicator { - cursor: pointer; - opacity: 0.6; - transition: opacity 0.2s; +.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; } -.date-range-inputs input[type="date"]::-webkit-calendar-picker-indicator:hover { - opacity: 1; +.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 { @@ -671,56 +1014,83 @@ a.btn-action-outline:hover { .date-range-group { min-width: 0; + position: relative; } - .date-range-inputs { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 8px; - border: none; - background: transparent; - padding: 0; + body.date-picker-open { + overflow: hidden; } - .date-range-inputs .input-wrapper { - background: #f8fafc; - border: 1px solid #e2e8f0; - padding: 10px 14px; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + body.date-picker-open .date-picker-backdrop { + display: block; + z-index: 998; } - .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); + .date-picker-popover { + /* Default mobile popover positioning inside media query is overridden below, + but let's define the base transitions or layouts. */ } - .date-range-inputs input[type="date"] { - width: 100%; - border: none; - background: transparent; - padding: 0; - font-family: inherit; - font-size: 0.82rem; - font-weight: 700; - color: var(--primary-blue); - min-width: 0; - flex: 1; - cursor: pointer; - } + @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; + } - .range-divider { - font-size: 0.75rem; - font-weight: 800; - color: #94a3b8; - padding: 0 4px; - text-transform: uppercase; + .date-picker-top-bar { + justify-content: center; + } + + .calendar-grids { + gap: 0; + } + + #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, @@ -765,6 +1135,119 @@ a.btn-action-outline:hover { } } +/* =========================================== + Responsive — Phone (≤480px) + =========================================== */ + +@media (max-width: 480px) { + .date-dd, .date-mm { + width: 26px; + } + + .date-yyyy { + min-width: 38px; + } + + .date-calendar-btn { + padding-left: 4px; + font-size: 0.82rem; + } +} + +/* =========================================== + Pagination + =========================================== */ + +.doc-pagination { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32px 0 0; + gap: 16px; + flex-wrap: wrap; +} + +.pagination-info { + font-size: 0.75rem; + font-weight: 800; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.pagination-nav { + display: flex; + align-items: center; + gap: 6px; +} + +.pagination-btn { + min-width: 40px; + height: 40px; + padding: 0 10px; + border: 1px solid #e2e8f0; + background: white; + color: var(--primary-blue); + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-family: inherit; +} + +.pagination-btn:hover:not(.disabled):not(.active) { + border-color: var(--secondary-gold); + background: #fffdf5; +} + +.pagination-btn.active { + background: var(--primary-blue); + border-color: var(--primary-blue); + color: white; +} + +.pagination-btn.disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.pagination-ellipsis { + min-width: 32px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: #94a3b8; + font-weight: 700; + font-size: 1rem; + letter-spacing: 1px; +} + +@media (max-width: 768px) { + .doc-pagination { + flex-direction: column; + align-items: center; + gap: 12px; + } +} + +@media (max-width: 480px) { + .pagination-btn { + min-width: 36px; + height: 36px; + font-size: 0.8rem; + } + + .pagination-nav { + gap: 4px; + } +} + /* =========================================== Responsive — Phone (≤480px) =========================================== */ diff --git a/groups/Tapops/index.html b/groups/Tapops/index.html index 9e27431..d3d1b66 100644 --- a/groups/Tapops/index.html +++ b/groups/Tapops/index.html @@ -943,15 +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 9896cc8..fa1debc 100644 --- a/groups/Tapops/js/main.js +++ b/groups/Tapops/js/main.js @@ -412,17 +412,684 @@ 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 =========================================== */ class DocumentRepository { + static PAGE_SIZE = 6; + constructor() { this.gridContainer = document.getElementById('document-grid'); this.filterWrapper = document.getElementById('tag-filter-wrapper'); this.mobileFilterWrapper = document.getElementById('mobile-tag-filter-wrapper'); this.searchInput = document.getElementById('doc-search'); - this.dateStartInput = document.getElementById('date-start'); - this.dateEndInput = document.getElementById('date-end'); this.sortFilterWrapper = document.getElementById('sort-filter-wrapper'); // Hero stat values (filled from real API data after fetch). @@ -430,18 +1097,22 @@ class DocumentRepository { this.statUpdatesEl = document.getElementById('stat-updates-value'); this.cards = []; + this.paginationContainer = null; this.state = { filter: 'all', // 'all' or a specific tag string search: '', startDate: '', endDate: '', sort: 'newest', + page: 1, }; } + async init() { if (!this.gridContainer) return; this._bindControls(); + this._initPagination(); try { const items = await DirectusClient.fetchPublished('berkas'); this._renderCards(items); @@ -456,6 +1127,14 @@ class DocumentRepository { } } + _initPagination() { + const pag = document.createElement('div'); + pag.id = 'doc-pagination'; + pag.className = 'doc-pagination'; + this.gridContainer.insertAdjacentElement('afterend', pag); + this.paginationContainer = pag; + } + // Days within which an upload counts as a "recent update". static RECENT_DAYS = 30; @@ -551,6 +1230,7 @@ class DocumentRepository { */ _setFilter(key) { this.state.filter = key; + this.state.page = 1; // Sync buttons. if (this._filterBtnGroup) { @@ -567,23 +1247,26 @@ class DocumentRepository { } _bindControls() { - // Tag filter buttons are built dynamically after API fetch - // (see _buildTagFilters). Only bind search/date/sort here. - this.searchInput?.addEventListener('input', (e) => { this.state.search = e.target.value.toLowerCase().trim(); + this.state.page = 1; this._applyFilters(); }); - this.dateStartInput?.addEventListener('change', (e) => { - this.state.startDate = e.target.value; - this._applyFilters(); - }); - - this.dateEndInput?.addEventListener('change', (e) => { - this.state.endDate = e.target.value; - 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 = [ @@ -599,6 +1282,7 @@ class DocumentRepository { prefixIcon: 'fas fa-sort-amount-down', onChange: (value) => { this.state.sort = value; + this.state.page = 1; this._applySort(); this._applyFilters(); } @@ -668,19 +1352,159 @@ class DocumentRepository { } _applyFilters() { - this.cards.forEach(({ view, element }) => { - if (this._cardMatches(view)) { - element.style.display = 'flex'; + const matched = this.cards.filter(({ view }) => this._cardMatches(view)); + const total = matched.length; + + // Handle zero-results state when cards exist but none match the filter. + this._toggleNoResultsState(total === 0 && this.cards.length > 0); + + if (total === 0) { + this.cards.forEach(({ element }) => { element.style.display = 'none'; }); + this._renderPagination(0, 0); + return; + } + + const totalPages = Math.ceil(total / DocumentRepository.PAGE_SIZE); + + // Clamp page to valid range after filter changes. + this.state.page = Math.min(Math.max(this.state.page, 1), totalPages); + + const start = (this.state.page - 1) * DocumentRepository.PAGE_SIZE; + const pageSet = new Set( + matched.slice(start, start + DocumentRepository.PAGE_SIZE).map(c => c.element) + ); + + this.cards.forEach(({ element }) => { + const show = pageSet.has(element); + element.style.display = show ? 'flex' : 'none'; + if (show) { setTimeout(() => { element.style.opacity = '1'; element.style.transform = 'translateY(0)'; }, 10); } else { - element.style.display = 'none'; element.style.opacity = '0'; element.style.transform = 'translateY(10px)'; } }); + + this._renderPagination(totalPages, total); + } + + _toggleNoResultsState(show) { + const existing = this.gridContainer.querySelector('.repo-no-results'); + if (show && !existing) { + const wrapper = document.createElement('div'); + wrapper.className = 'repo-no-results'; + Object.assign(wrapper.style, { + gridColumn: '1 / -1', + textAlign: 'center', + padding: '60px 20px', + color: '#64748b', + fontWeight: '700', + fontSize: '1rem', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '15px', + }); + const icon = document.createElement('i'); + icon.className = 'fas fa-search'; + Object.assign(icon.style, { fontSize: '3rem', color: '#cbd5e1' }); + const span = document.createElement('span'); + span.setAttribute('data-tp-i18n', 'state_no_results'); + span.textContent = (window.tpT?.('state_no_results')) || 'Tidak ada berkas yang cocok dengan filter saat ini.'; + wrapper.appendChild(icon); + wrapper.appendChild(span); + this.gridContainer.appendChild(wrapper); + } else if (!show && existing) { + existing.remove(); + } + } + + _renderPagination(totalPages, total) { + if (!this.paginationContainer) return; + this.paginationContainer.innerHTML = ''; + if (total === 0 || totalPages <= 1) return; + + const current = this.state.page; + const start = (current - 1) * DocumentRepository.PAGE_SIZE + 1; + const end = Math.min(current * DocumentRepository.PAGE_SIZE, total); + + const info = document.createElement('span'); + info.className = 'pagination-info'; + info.textContent = `${start}–${end} dari ${total} berkas`; + this.paginationContainer.appendChild(info); + + const nav = document.createElement('nav'); + nav.className = 'pagination-nav'; + nav.setAttribute('aria-label', 'Navigasi halaman dokumen'); + + const prev = document.createElement('button'); + prev.className = 'pagination-btn' + (current <= 1 ? ' disabled' : ''); + prev.disabled = current <= 1; + prev.setAttribute('aria-label', 'Halaman sebelumnya'); + const prevIcon = document.createElement('i'); + prevIcon.className = 'fas fa-chevron-left'; + prev.appendChild(prevIcon); + prev.addEventListener('click', () => { + if (this.state.page > 1) { this.state.page--; this._applyFilters(); this._scrollToGrid(); } + }); + nav.appendChild(prev); + + this._paginationRange(current, totalPages).forEach(p => { + if (p === '...') { + const el = document.createElement('span'); + el.className = 'pagination-ellipsis'; + el.setAttribute('aria-hidden', 'true'); + el.textContent = '…'; + nav.appendChild(el); + } else { + const btn = document.createElement('button'); + btn.className = 'pagination-btn pagination-number' + (p === current ? ' active' : ''); + btn.textContent = p; + btn.setAttribute('aria-label', `Halaman ${p}`); + if (p === current) btn.setAttribute('aria-current', 'page'); + btn.addEventListener('click', () => { + this.state.page = p; this._applyFilters(); this._scrollToGrid(); + }); + nav.appendChild(btn); + } + }); + + const next = document.createElement('button'); + next.className = 'pagination-btn' + (current >= totalPages ? ' disabled' : ''); + next.disabled = current >= totalPages; + next.setAttribute('aria-label', 'Halaman berikutnya'); + const nextIcon = document.createElement('i'); + nextIcon.className = 'fas fa-chevron-right'; + next.appendChild(nextIcon); + next.addEventListener('click', () => { + if (this.state.page < totalPages) { this.state.page++; this._applyFilters(); this._scrollToGrid(); } + }); + nav.appendChild(next); + + this.paginationContainer.appendChild(nav); + } + + // Returns page numbers and '...' ellipsis placeholders for the given range. + _paginationRange(current, total) { + if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1); + + const pages = [1]; + const lo = Math.max(2, current - 1); + const hi = Math.min(total - 1, current + 1); + + if (lo > 2) pages.push('...'); + for (let i = lo; i <= hi; i++) pages.push(i); + if (hi < total - 1) pages.push('...'); + pages.push(total); + + return pages; + } + + _scrollToGrid() { + document.getElementById('repository')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } _cardMatches(view) {