From edab5db3d710dc0d1cf731196921ee9ae6f29e28 Mon Sep 17 00:00:00 2001 From: Dodo Date: Thu, 4 Jun 2026 16:52:54 +0700 Subject: [PATCH 1/6] fix(tapops): batasi input tahun 4 digit dan tambah paginasi 6 dokumen per halaman - Tambah atribut max/min pada date input agar tahun dibatasi 1000-9999, plus clamping via JS sebagai fallback lintas browser - Tambah paginasi 6 dokumen per halaman dengan navigasi prev/next dan nomor halaman (ellipsis untuk banyak halaman) - Reset ke halaman 1 saat filter, sort, search, atau rentang tanggal berubah - Clamp halaman aktif ke rentang valid jika jumlah hasil berkurang setelah filter - Tambah pesan "tidak ada hasil" saat filter aktif tidak menemukan berkas - Semua DOM pagination dibangun via safe DOM API (tanpa innerHTML) --- groups/Tapops/css/documents.css | 98 +++++++++++++++++ groups/Tapops/js/main.js | 181 ++++++++++++++++++++++++++++++-- 2 files changed, 273 insertions(+), 6 deletions(-) diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index 19ae003..799ac4d 100644 --- a/groups/Tapops/css/documents.css +++ b/groups/Tapops/css/documents.css @@ -765,6 +765,104 @@ a.btn-action-outline:hover { } } +/* =========================================== + Responsive — Phone (≤480px) + =========================================== */ + +/* =========================================== + 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/js/main.js b/groups/Tapops/js/main.js index 9896cc8..1f6668f 100644 --- a/groups/Tapops/js/main.js +++ b/groups/Tapops/js/main.js @@ -416,6 +416,8 @@ class CustomDropdown { DocumentRepository =========================================== */ class DocumentRepository { + static PAGE_SIZE = 6; + constructor() { this.gridContainer = document.getElementById('document-grid'); this.filterWrapper = document.getElementById('tag-filter-wrapper'); @@ -430,18 +432,21 @@ 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 +461,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 +564,7 @@ class DocumentRepository { */ _setFilter(key) { this.state.filter = key; + this.state.page = 1; // Sync buttons. if (this._filterBtnGroup) { @@ -567,21 +581,35 @@ class DocumentRepository { } _bindControls() { - // Tag filter buttons are built dynamically after API fetch - // (see _buildTagFilters). Only bind search/date/sort here. + // Limit date inputs to a 4-digit year (max 9999). + [this.dateStartInput, this.dateEndInput].forEach(input => { + if (!input) return; + input.setAttribute('max', '9999-12-31'); + input.setAttribute('min', '1000-01-01'); + // Clamp year on change as a fallback for browsers that don't enforce max. + input.addEventListener('change', () => { + if (!input.value) return; + const [yearStr, month, day] = input.value.split('-'); + const year = parseInt(yearStr, 10); + if (year > 9999) input.value = `9999-${month}-${day}`; + }); + }); 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.state.page = 1; this._applyFilters(); }); this.dateEndInput?.addEventListener('change', (e) => { this.state.endDate = e.target.value; + this.state.page = 1; this._applyFilters(); }); @@ -599,6 +627,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 +697,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) { -- 2.52.0 From 5bb704ac88e728e093a75e4d750c04358f02194c Mon Sep 17 00:00:00 2001 From: Dodo Date: Thu, 4 Jun 2026 16:58:05 +0700 Subject: [PATCH 2/6] fix(tapops): clamp year pada input dan change event agar validasi aktif saat mengetik --- groups/Tapops/js/main.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/groups/Tapops/js/main.js b/groups/Tapops/js/main.js index 1f6668f..949cab2 100644 --- a/groups/Tapops/js/main.js +++ b/groups/Tapops/js/main.js @@ -586,13 +586,19 @@ class DocumentRepository { if (!input) return; input.setAttribute('max', '9999-12-31'); input.setAttribute('min', '1000-01-01'); - // Clamp year on change as a fallback for browsers that don't enforce max. - input.addEventListener('change', () => { + // Clamp year on both input (fires while typing) and change (fires on blur). + const clampYear = () => { if (!input.value) return; - const [yearStr, month, day] = input.value.split('-'); - const year = parseInt(yearStr, 10); - if (year > 9999) input.value = `9999-${month}-${day}`; - }); + const parts = input.value.split('-'); + if (parts.length !== 3) return; + const year = parseInt(parts[0], 10); + if (!Number.isNaN(year) && year > 9999) { + parts[0] = '9999'; + input.value = parts.join('-'); + } + }; + input.addEventListener('input', clampYear); + input.addEventListener('change', clampYear); }); this.searchInput?.addEventListener('input', (e) => { -- 2.52.0 From 9c1de91a826f0c404369ce14ae1a911799b67883 Mon Sep 17 00:00:00 2001 From: Dodo Date: Thu, 4 Jun 2026 17:06:11 +0700 Subject: [PATCH 3/6] fix(tapops): ganti input date native dengan dd/mm/yyyy text input terpisah agar tahun max 4 digit --- groups/Tapops/css/documents.css | 57 ++++++++++----------- groups/Tapops/index.html | 16 ++++-- groups/Tapops/js/main.js | 87 +++++++++++++++++++++++---------- 3 files changed, 102 insertions(+), 58 deletions(-) diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index 799ac4d..0b6e850 100644 --- a/groups/Tapops/css/documents.css +++ b/groups/Tapops/css/documents.css @@ -363,36 +363,47 @@ background: white; } -.date-range-inputs .input-wrapper { - position: relative; +/* Custom dd/mm/yyyy date input */ +.custom-date-input { display: flex; align-items: center; + gap: 1px; + padding: 6px 0; } -.date-range-inputs input[type="date"] { +.date-sep { + font-size: 0.85rem; + font-weight: 700; + color: #94a3b8; + user-select: none; + padding: 0 1px; +} + +.date-part { border: none; background: transparent; - padding: 8px 0; font-family: inherit; font-size: 0.85rem; font-weight: 700; color: var(--primary-blue); - cursor: pointer; - min-width: 120px; + text-align: center; + padding: 2px 3px; + border-radius: 3px; + transition: background 0.15s ease; + min-width: 0; } -.date-range-inputs input[type="date"]:focus { +.date-dd, .date-mm { width: 2.2ch; } +.date-yyyy { width: 4.2ch; } + +.date-part::placeholder { + color: #94a3b8; + font-weight: 400; +} + +.date-part:focus { outline: none; -} - -.date-range-inputs input[type="date"]::-webkit-calendar-picker-indicator { - cursor: pointer; - opacity: 0.6; - transition: opacity 0.2s; -} - -.date-range-inputs input[type="date"]::-webkit-calendar-picker-indicator:hover { - opacity: 1; + background: rgba(0, 49, 80, 0.07); } .range-divider { @@ -701,18 +712,8 @@ a.btn-action-outline:hover { box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); } - .date-range-inputs input[type="date"] { - width: 100%; - border: none; - background: transparent; - padding: 0; - font-family: inherit; + .date-part { font-size: 0.82rem; - font-weight: 700; - color: var(--primary-blue); - min-width: 0; - flex: 1; - cursor: pointer; } .range-divider { diff --git a/groups/Tapops/index.html b/groups/Tapops/index.html index 9e27431..898939f 100644 --- a/groups/Tapops/index.html +++ b/groups/Tapops/index.html @@ -946,12 +946,20 @@
Periode Unggahan
-
- +
+ + / + + / +
s/d -
- +
+ + / + + / +
diff --git a/groups/Tapops/js/main.js b/groups/Tapops/js/main.js index 949cab2..1633c26 100644 --- a/groups/Tapops/js/main.js +++ b/groups/Tapops/js/main.js @@ -423,14 +423,23 @@ class DocumentRepository { 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). 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'), + }; + this.dateEndParts = { + dd: document.getElementById('date-end-dd'), + mm: document.getElementById('date-end-mm'), + yyyy: document.getElementById('date-end-yyyy'), + }; + this.cards = []; this.paginationContainer = null; this.state = { @@ -443,6 +452,52 @@ 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 + // • calls onUpdate() on every change + _bindDateParts(parts, onUpdate) { + const { dd, mm, yyyy } = 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); + } + async init() { if (!this.gridContainer) return; this._bindControls(); @@ -581,40 +636,20 @@ class DocumentRepository { } _bindControls() { - // Limit date inputs to a 4-digit year (max 9999). - [this.dateStartInput, this.dateEndInput].forEach(input => { - if (!input) return; - input.setAttribute('max', '9999-12-31'); - input.setAttribute('min', '1000-01-01'); - // Clamp year on both input (fires while typing) and change (fires on blur). - const clampYear = () => { - if (!input.value) return; - const parts = input.value.split('-'); - if (parts.length !== 3) return; - const year = parseInt(parts[0], 10); - if (!Number.isNaN(year) && year > 9999) { - parts[0] = '9999'; - input.value = parts.join('-'); - } - }; - input.addEventListener('input', clampYear); - input.addEventListener('change', clampYear); - }); - 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._bindDateParts(this.dateStartParts, () => { + this.state.startDate = this._getDateString(this.dateStartParts); this.state.page = 1; this._applyFilters(); }); - this.dateEndInput?.addEventListener('change', (e) => { - this.state.endDate = e.target.value; + this._bindDateParts(this.dateEndParts, () => { + this.state.endDate = this._getDateString(this.dateEndParts); this.state.page = 1; this._applyFilters(); }); -- 2.52.0 From 46d9b1677ddea6b4d5307ba875e6a781e4814434 Mon Sep 17 00:00:00 2001 From: Dodo Date: Thu, 4 Jun 2026 17:09:33 +0700 Subject: [PATCH 4/6] fix(tapops): perlebar field tanggal dan tambah kembali tombol kalender dengan picker tersembunyi --- groups/Tapops/css/documents.css | 39 +++++++++++++++++++++++++++++---- groups/Tapops/index.html | 8 +++++++ groups/Tapops/js/main.js | 34 ++++++++++++++++++++++------ 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index 0b6e850..61bf593 100644 --- a/groups/Tapops/css/documents.css +++ b/groups/Tapops/css/documents.css @@ -365,6 +365,7 @@ /* Custom dd/mm/yyyy date input */ .custom-date-input { + position: relative; display: flex; align-items: center; gap: 1px; @@ -376,7 +377,8 @@ font-weight: 700; color: #94a3b8; user-select: none; - padding: 0 1px; + padding: 0 2px; + flex-shrink: 0; } .date-part { @@ -387,14 +389,14 @@ font-weight: 700; color: var(--primary-blue); text-align: center; - padding: 2px 3px; + padding: 2px 4px; border-radius: 3px; transition: background 0.15s ease; min-width: 0; } -.date-dd, .date-mm { width: 2.2ch; } -.date-yyyy { width: 4.2ch; } +.date-dd, .date-mm { width: 34px; } +.date-yyyy { width: 56px; } .date-part::placeholder { color: #94a3b8; @@ -406,6 +408,35 @@ background: rgba(0, 49, 80, 0.07); } +.date-calendar-btn { + background: none; + border: none; + cursor: pointer; + color: #94a3b8; + padding: 4px 2px 4px 6px; + display: flex; + 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; + padding: 0; + margin: 0; + overflow: hidden; +} + .range-divider { font-size: 0.7rem; font-weight: 800; diff --git a/groups/Tapops/index.html b/groups/Tapops/index.html index 898939f..23ca707 100644 --- a/groups/Tapops/index.html +++ b/groups/Tapops/index.html @@ -952,6 +952,10 @@ / + +
s/d
@@ -960,6 +964,10 @@ / + +
diff --git a/groups/Tapops/js/main.js b/groups/Tapops/js/main.js index 1633c26..9d81bf3 100644 --- a/groups/Tapops/js/main.js +++ b/groups/Tapops/js/main.js @@ -430,14 +430,18 @@ class DocumentRepository { 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'), + 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'), + 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 = []; @@ -474,9 +478,10 @@ class DocumentRepository { // • 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 } = parts; + 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) => { @@ -496,6 +501,21 @@ class DocumentRepository { 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() { -- 2.52.0 From b0918c1bb97ef8c74a16d9981afdffb516df2754 Mon Sep 17 00:00:00 2001 From: Dodo Date: Thu, 4 Jun 2026 17:11:23 +0700 Subject: [PATCH 5/6] fix(tapops): perbaiki overflow date input di mobile dengan layout kolom dan lebar fleksibel --- groups/Tapops/css/documents.css | 53 ++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index 61bf593..c4017d0 100644 --- a/groups/Tapops/css/documents.css +++ b/groups/Tapops/css/documents.css @@ -715,11 +715,11 @@ a.btn-action-outline:hover { min-width: 0; } + /* Stack start/end date pickers vertically to avoid overflow */ .date-range-inputs { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 8px; + display: flex; + flex-direction: column; + gap: 6px; border: none; background: transparent; padding: 0; @@ -728,13 +728,13 @@ a.btn-action-outline:hover { .date-range-inputs .input-wrapper { background: #f8fafc; border: 1px solid #e2e8f0; - padding: 10px 14px; + padding: 10px 12px; 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); + overflow: hidden; } .date-range-inputs .input-wrapper:focus-within { @@ -743,16 +743,38 @@ a.btn-action-outline:hover { box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); } + /* Custom date input fills the wrapper width */ + .custom-date-input { + width: 100%; + padding: 0; + } + + /* DD / MM stay compact; YYYY stretches to fill remaining space */ + .date-dd, .date-mm { + flex: 0 0 auto; + width: 30px; + } + + .date-yyyy { + flex: 1; + width: auto; + min-width: 44px; + text-align: left; + } + .date-part { font-size: 0.82rem; } + /* S/D label between pickers becomes a small inline tag */ .range-divider { - font-size: 0.75rem; + font-size: 0.68rem; font-weight: 800; color: #94a3b8; - padding: 0 4px; + padding: 3px 10px; text-transform: uppercase; + text-align: center; + align-self: center; } .select-wrapper, @@ -801,6 +823,21 @@ 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 =========================================== */ -- 2.52.0 From 529693cd5f4d012ac78c65acc04d2f090cf65d60 Mon Sep 17 00:00:00 2001 From: Dodo Date: Thu, 4 Jun 2026 17:28:18 +0700 Subject: [PATCH 6/6] feat: implementasi custom date range picker dengan preset dan navigasi kalender * Mengganti field input tanggal yang sudah ada dengan UI date range picker baru. * Menambahkan popover kalender untuk memilih rentang tanggal dengan navigasi bulan. * Menambahkan tombol preset untuk pilihan rentang tanggal umum secara cepat, seperti Hari Ini dan 7 Hari Terakhir. * Memperbarui string lokalisasi untuk fitur date range picker baru. * Melakukan refactor JavaScript untuk menangani pemilihan tanggal dan state management pada picker baru. --- groups/Tapops/css/documents.css | 566 +++++++++++++++++------ groups/Tapops/index.html | 101 ++++- groups/Tapops/js/i18n-tapops.js | 26 ++ groups/Tapops/js/main.js | 768 ++++++++++++++++++++++++++++---- 4 files changed, 1226 insertions(+), 235 deletions(-) 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 = [ -- 2.52.0