forked from izu/student-web-if-development-kit
fix(tapops): validasi input tahun dan tambah pagination dokumen #1
+559
-76
@@ -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)
|
||||
=========================================== */
|
||||
|
||||
@@ -943,15 +943,86 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-controls">
|
||||
<div class="control-group date-range-group">
|
||||
<div class="control-group date-range-group" id="date-range-control-group">
|
||||
<span class="control-label" data-tp-i18n="label_period">Periode Unggahan</span>
|
||||
<div class="date-range-inputs">
|
||||
<div class="input-wrapper">
|
||||
<input type="date" id="date-start" placeholder="Mulai">
|
||||
<button type="button" id="date-picker-trigger" class="date-picker-trigger">
|
||||
<div class="trigger-left-content">
|
||||
<i class="far fa-calendar-alt"></i>
|
||||
<span id="date-picker-label" data-tp-i18n="filter_all_time">Semua Waktu</span>
|
||||
</div>
|
||||
<span class="range-divider" data-tp-i18n="range_divider">s/d</span>
|
||||
<div class="input-wrapper">
|
||||
<input type="date" id="date-end" placeholder="Selesai">
|
||||
<i class="fas fa-chevron-down trigger-arrow"></i>
|
||||
</button>
|
||||
|
||||
<div id="date-picker-backdrop" class="date-picker-backdrop"></div>
|
||||
|
||||
<div id="date-picker-popover" class="date-picker-popover">
|
||||
<div class="date-picker-top-bar">
|
||||
<div class="date-display-inputs">
|
||||
<div class="date-display-field">
|
||||
<input type="text" id="picker-start-display" placeholder="DD/MM/YYYY">
|
||||
</div>
|
||||
<span class="date-picker-separator">-</span>
|
||||
<div class="date-display-field">
|
||||
<input type="text" id="picker-end-display" placeholder="DD/MM/YYYY">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-picker-body">
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-nav">
|
||||
<button type="button" id="prev-month-btn" class="nav-btn"><i class="fas fa-chevron-left"></i></button>
|
||||
<div class="month-labels">
|
||||
<div id="left-month-label" class="month-label">
|
||||
<span id="left-month-name">Mei</span>
|
||||
<div class="year-select-wrapper">
|
||||
<select id="left-year-select" class="calendar-year-select"></select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="right-month-label" class="month-label">
|
||||
<span id="right-month-name">Juni</span>
|
||||
<div class="year-select-wrapper">
|
||||
<select id="right-year-select" class="calendar-year-select"></select>
|
||||
<i class="fas fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="next-month-btn" class="nav-btn"><i class="fas fa-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="calendar-grids">
|
||||
<div class="calendar-grid-pane" id="left-calendar-pane">
|
||||
<div class="day-headers"></div>
|
||||
<div class="days-container" id="left-days-container"></div>
|
||||
</div>
|
||||
<div class="calendar-grid-pane" id="right-calendar-pane">
|
||||
<div class="day-headers"></div>
|
||||
<div class="days-container" id="right-days-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="presets-container">
|
||||
<span class="presets-title" data-tp-i18n="presets_title">RENTANG WAKTU</span>
|
||||
<div class="presets-list">
|
||||
<button type="button" class="preset-btn" data-preset="all" data-tp-i18n="filter_all_time">Semua Waktu</button>
|
||||
<button type="button" class="preset-btn" data-preset="today" data-tp-i18n="preset_today">Hari Ini</button>
|
||||
<button type="button" class="preset-btn" data-preset="yesterday" data-tp-i18n="preset_yesterday">Kemarin</button>
|
||||
<button type="button" class="preset-btn" data-preset="last7" data-tp-i18n="preset_last_7_days">7 Hari Terakhir</button>
|
||||
<button type="button" class="preset-btn" data-preset="last14" data-tp-i18n="preset_last_14_days">14 Hari Terakhir</button>
|
||||
<button type="button" class="preset-btn" data-preset="last30" data-tp-i18n="preset_last_30_days">30 Hari Terakhir</button>
|
||||
<button type="button" class="preset-btn" data-preset="thisWeek" data-tp-i18n="preset_this_week">Minggu Ini</button>
|
||||
<button type="button" class="preset-btn" data-preset="lastWeek" data-tp-i18n="preset_last_week">Minggu Lalu</button>
|
||||
<button type="button" class="preset-btn" data-preset="thisMonth" data-tp-i18n="preset_this_month">Bulan Ini</button>
|
||||
<button type="button" class="preset-btn" data-preset="lastMonth" data-tp-i18n="preset_last_month">Bulan Lalu</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-picker-footer">
|
||||
<button type="button" id="picker-cancel-btn" class="action-btn cancel-btn" data-tp-i18n="btn_cancel">Batal</button>
|
||||
<button type="button" id="picker-apply-btn" class="action-btn apply-btn" data-tp-i18n="btn_update">Perbarui</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
+842
-18
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user