OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
commit
eaf05f2811
280 files changed
+123975
No files matched your search
@@ -0,0 +1,246 @@
|
||||
/* =========================================================================
|
||||
* OPEN-ISP admin — Global search shortcuts + Reset Filter
|
||||
* -------------------------------------------------------------------------
|
||||
* - Tekan "/" : fokus ke kolom pencarian (baris filter / modal terbuka)
|
||||
* - Tekan "Enter" : jalankan pencarian (submit form filter)
|
||||
* - Tombol "Reset" : otomatis ditambahkan ke tiap baris filter:
|
||||
* • form filter GET -> bersihkan & muat ulang tanpa query
|
||||
* • baris filter JS -> bersihkan & trigger ulang filter (keyup/change)
|
||||
* Non-invasif: tidak mengubah view apa pun, dimuat lewat injeksi /admin.
|
||||
* ========================================================================= */
|
||||
(function () {
|
||||
'use strict';
|
||||
if (window.__adminSearchInit) return;
|
||||
window.__adminSearchInit = true;
|
||||
|
||||
var NAME_SEL = 'input[name="search"], input[name="q"], input[name="keyword"], input[type="search"]';
|
||||
var FILTER_WRAP = '.srow, .filter-bar';
|
||||
var FILTER_EXTRA = 'select[name="status"], select[name="month"], select[name="year"], select[name="area"], select[name="router_id"], select[name="package_id"], select[name="collector_id"]';
|
||||
|
||||
/* ---------- util ---------- */
|
||||
function isEditable(el) {
|
||||
if (!el) return false;
|
||||
var t = (el.tagName || '').toLowerCase();
|
||||
if (t === 'textarea' || t === 'select') return true;
|
||||
if (t === 'input') {
|
||||
var ty = (el.type || 'text').toLowerCase();
|
||||
return ['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'color', 'file', 'hidden'].indexOf(ty) === -1;
|
||||
}
|
||||
return el.isContentEditable === true;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || el.disabled || el.readOnly) return false;
|
||||
if (el.offsetParent === null) {
|
||||
var r = el.getBoundingClientRect();
|
||||
return r.width > 0 && r.height > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSearchLike(el) {
|
||||
if (!el || el.tagName !== 'INPUT') return false;
|
||||
if (el.matches('.sinput input, ' + NAME_SEL)) return true;
|
||||
if (el.closest('.sinput')) return true;
|
||||
var ph = (el.getAttribute('placeholder') || '').toLowerCase();
|
||||
if (/cari|search|telusur|temukan|find/.test(ph)) return true;
|
||||
var h = (el.getAttribute('onkeyup') || '') + (el.getAttribute('oninput') || '');
|
||||
if (h && /filter|search|cari/i.test(h)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
var mods = document.querySelectorAll('.mo.show, .modal.show');
|
||||
return mods.length ? mods[mods.length - 1] : null;
|
||||
}
|
||||
|
||||
function searchInputs() {
|
||||
var nodes = document.querySelectorAll('input');
|
||||
var out = [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (isSearchLike(nodes[i]) && isVisible(nodes[i])) out.push(nodes[i]);
|
||||
}
|
||||
var modal = openModal();
|
||||
out.sort(function (a, b) {
|
||||
function score(x) {
|
||||
if (modal && modal.contains(x)) return 0;
|
||||
if (x.closest(FILTER_WRAP)) return 1;
|
||||
if (x.closest('.sinput')) return 2;
|
||||
return 3;
|
||||
}
|
||||
return score(a) - score(b);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function firstSearch() { return searchInputs()[0] || null; }
|
||||
|
||||
function flash(el) {
|
||||
var w = el.closest('.sinput') || el.parentElement || el;
|
||||
if (!w || !w.classList) return;
|
||||
w.classList.add('admin-search-flash');
|
||||
setTimeout(function () { w.classList.remove('admin-search-flash'); }, 900);
|
||||
}
|
||||
|
||||
function submitForm(form) {
|
||||
if (!form) return;
|
||||
if (typeof form.requestSubmit === 'function') {
|
||||
try { form.requestSubmit(); return; } catch (e) { /* fallback */ }
|
||||
}
|
||||
form.submit();
|
||||
}
|
||||
|
||||
/* ---------- keyboard shortcut ---------- */
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.defaultPrevented || e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
var el = document.activeElement;
|
||||
var typing = isEditable(el);
|
||||
|
||||
// Enter di kolom pencarian -> jalankan pencarian
|
||||
if (e.key === 'Enter' && typing && el && el.tagName === 'INPUT') {
|
||||
var isSearch = !!(el.closest('.sinput') || isSearchLike(el));
|
||||
var form = el.form || el.closest('form');
|
||||
if (isSearch && form) {
|
||||
e.preventDefault();
|
||||
submitForm(form);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// "/" -> fokus kolom pencarian (hanya saat tidak sedang mengetik)
|
||||
if ((e.key === '/' || e.key === 'Slash' || e.code === 'Slash') && !typing) {
|
||||
var inp = firstSearch();
|
||||
if (inp) {
|
||||
e.preventDefault();
|
||||
inp.focus();
|
||||
try { inp.setSelectionRange(inp.value.length, inp.value.length); } catch (err) {}
|
||||
flash(inp);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
/* ---------- helpers reset ---------- */
|
||||
function baseAction(form) {
|
||||
var a = form.getAttribute('action') || location.pathname || '/';
|
||||
return (a.split('?')[0]) || location.pathname || '/';
|
||||
}
|
||||
|
||||
function clearField(el) {
|
||||
var ty = (el.type || '').toLowerCase();
|
||||
if (ty === 'hidden') return;
|
||||
if (ty === 'checkbox' || ty === 'radio') { el.checked = el.defaultChecked; return; }
|
||||
if (el.tagName === 'SELECT') {
|
||||
var idx = 0;
|
||||
for (var o = 0; o < el.options.length; o++) if (el.options[o].defaultSelected) { idx = o; break; }
|
||||
el.selectedIndex = idx;
|
||||
return;
|
||||
}
|
||||
el.value = '';
|
||||
}
|
||||
|
||||
function fireEvents(el) {
|
||||
try { el.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
|
||||
try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
|
||||
try { el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: '', code: '' })); } catch (e) {}
|
||||
}
|
||||
|
||||
function makeResetButton(scope, onClick) {
|
||||
var btn = document.createElement('a');
|
||||
btn.href = 'javascript:void(0)';
|
||||
btn.className = 'btn btn-g btn-sm';
|
||||
btn.setAttribute('data-admin-reset', '1');
|
||||
btn.setAttribute('title', 'Reset semua filter');
|
||||
btn.innerHTML = '<i class="bi bi-arrow-counterclockwise"></i> Reset';
|
||||
btn.addEventListener('click', function (ev) { ev.preventDefault(); onClick(); });
|
||||
return btn;
|
||||
}
|
||||
|
||||
/* ---------- A) form filter GET ---------- */
|
||||
function isFilterForm(f) {
|
||||
if (!f) return false;
|
||||
if (f.closest('.mo, .modal')) return false; // jangan sentuh form di modal
|
||||
if ((f.getAttribute('method') || 'get').toLowerCase() !== 'get') return false;
|
||||
if (f.matches(FILTER_WRAP)) return true;
|
||||
if (f.querySelector('.sinput, ' + NAME_SEL + ', ' + FILTER_EXTRA)) return true;
|
||||
var ins = f.querySelectorAll('input');
|
||||
for (var i = 0; i < ins.length; i++) if (isSearchLike(ins[i])) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasReset(f) {
|
||||
var ctrls = f.querySelectorAll('a, button');
|
||||
for (var i = 0; i < ctrls.length; i++) {
|
||||
var c = ctrls[i];
|
||||
if (c.hasAttribute('data-admin-reset')) continue;
|
||||
if (c.tagName === 'BUTTON' && (c.getAttribute('type') || '').toLowerCase() === 'reset') return true;
|
||||
if (c.hasAttribute('data-reset')) return true;
|
||||
if ((c.textContent || '').toLowerCase().indexOf('reset') !== -1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setupFormReset(form) {
|
||||
if (form.querySelector('[data-admin-reset]') || hasReset(form)) return;
|
||||
form.appendChild(makeResetButton(form, function () {
|
||||
var fields = form.querySelectorAll('input, select');
|
||||
for (var i = 0; i < fields.length; i++) clearField(fields[i]);
|
||||
location.href = baseAction(form);
|
||||
}));
|
||||
}
|
||||
|
||||
/* ---------- B) baris filter client-side ---------- */
|
||||
function clientScope(el) {
|
||||
return el.closest(FILTER_WRAP) || el.closest('.sinput') || null;
|
||||
}
|
||||
|
||||
function setupClientReset(scope) {
|
||||
if (!scope || scope.hasAttribute('data-admin-reset-scope')) return;
|
||||
scope.setAttribute('data-admin-reset-scope', '1');
|
||||
// jangan dobel bila sudah ada tombol reset di dalam/ sekitar
|
||||
var near = scope.querySelector('[data-admin-reset]') ||
|
||||
(scope.parentElement && scope.parentElement.querySelector('[data-admin-reset]'));
|
||||
if (near || hasReset(scope)) return;
|
||||
|
||||
var btn = makeResetButton(scope, function () {
|
||||
var fields = scope.querySelectorAll('input, select');
|
||||
for (var i = 0; i < fields.length; i++) { clearField(fields[i]); fireEvents(fields[i]); }
|
||||
});
|
||||
|
||||
if (scope.matches(FILTER_WRAP)) {
|
||||
scope.appendChild(btn);
|
||||
} else if (scope.parentElement) {
|
||||
scope.insertAdjacentElement('afterend', btn);
|
||||
} else {
|
||||
scope.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// A) form filter GET
|
||||
var forms = document.querySelectorAll('form');
|
||||
for (var i = 0; i < forms.length; i++) if (isFilterForm(forms[i])) setupFormReset(forms[i]);
|
||||
|
||||
// B) baris filter client-side (input di luar form)
|
||||
var ins = document.querySelectorAll('input');
|
||||
for (var j = 0; j < ins.length; j++) {
|
||||
var el = ins[j];
|
||||
if (!isSearchLike(el)) continue;
|
||||
if (el.closest('form')) continue; // ditangani bagian A
|
||||
if (el.closest('.mo, .modal')) continue; // jangan di modal
|
||||
var sc = clientScope(el);
|
||||
if (sc) setupClientReset(sc);
|
||||
}
|
||||
|
||||
// hint tooltip
|
||||
var inputs = searchInputs();
|
||||
for (var k = 0; k < inputs.length; k++) {
|
||||
if (!inputs[k].getAttribute('title')) inputs[k].setAttribute('title', 'Tekan "/" untuk fokus, "Enter" untuk mencari');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setup);
|
||||
} else {
|
||||
setup();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Membersihkan Service Worker yang salah terdaftar untuk area /admin.
|
||||
Service Worker yang benar HANYA untuk portal pelanggan (scope /customer/).
|
||||
File ini di-inject otomatis ke setiap halaman admin. */
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function (regs) {
|
||||
regs.forEach(function (r) {
|
||||
if (!r.scope || r.scope.indexOf('/customer/') === -1) {
|
||||
r.unregister().catch(function () {});
|
||||
}
|
||||
});
|
||||
}).catch(function () {});
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/* admin-theme-sync.js — Menjaga konsistensi tema admin (dark/light).
|
||||
PENTING: observer TIDAK boleh menulis ulang `data-theme` (bisa memicu loop). */
|
||||
(function () {
|
||||
var KEY = 'app-theme';
|
||||
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
|
||||
|
||||
// Sinkronkan atribut turunan berdasarkan data-theme saat ini (tanpa menulis data-theme).
|
||||
function syncAttrs() {
|
||||
var isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||
var bs = isLight ? 'light' : 'dark';
|
||||
if (document.documentElement.getAttribute('data-bs-theme') !== bs) document.documentElement.setAttribute('data-bs-theme', bs);
|
||||
if (document.documentElement.classList.contains('light-theme') !== isLight) document.documentElement.classList.toggle('light-theme', isLight);
|
||||
if (document.body && document.body.classList.contains('light-theme') !== isLight) document.body.classList.toggle('light-theme', isLight);
|
||||
try { if (document.documentElement.style.colorScheme !== bs) document.documentElement.style.colorScheme = bs; } catch (e) {}
|
||||
}
|
||||
|
||||
function applyTheme(t) {
|
||||
t = t || read() || 'dark';
|
||||
if (document.documentElement.getAttribute('data-theme') !== t) document.documentElement.setAttribute('data-theme', t);
|
||||
syncAttrs();
|
||||
return t;
|
||||
}
|
||||
|
||||
window.__applyAdminTheme = applyTheme;
|
||||
applyTheme();
|
||||
|
||||
// Observer hanya menyinkronkan atribut turunan (AMAN, tanpa loop).
|
||||
try { new MutationObserver(syncAttrs).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] }); } catch (e) {}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () { applyTheme(); });
|
||||
window.addEventListener('pageshow', function () { applyTheme(); });
|
||||
|
||||
if (typeof window.toggleAppTheme !== 'function') {
|
||||
window.toggleAppTheme = function () {
|
||||
var cur = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
||||
try { localStorage.setItem(KEY, cur); } catch (e) {}
|
||||
applyTheme(cur);
|
||||
if (typeof window.updateThemeToggleIcons === 'function') window.updateThemeToggleIcons(cur);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target && e.target.closest ? e.target.closest('.theme-toggle-btn') : null;
|
||||
if (!btn) return;
|
||||
if (btn.dataset.themeDelegate === '1') return;
|
||||
btn.dataset.themeDelegate = '1';
|
||||
if (!btn.hasAttribute('onclick')) { e.preventDefault(); window.toggleAppTheme(); }
|
||||
}, true);
|
||||
})();
|
||||
|
||||
// ── Dropdown Aksi (kebab menu) ──
|
||||
// Menu di-"portal" ke <body> saat terbuka, lalu diposisikan fixed relatif tombol.
|
||||
// Ini menghilangkan SEMUA pengaruh ancestor (overflow tabel/kartu, stacking, dsb)
|
||||
// sehingga menu selalu solid & tampil utuh di mana pun (termasuk baris Lunas).
|
||||
var _actOpen = null, _actMenu = null, _actHome = null;
|
||||
function actCloseAll() {
|
||||
if (_actMenu) {
|
||||
try { _actMenu.classList.remove('fixed', 'up'); _actMenu.style.left = ''; _actMenu.style.top = ''; } catch (e) {}
|
||||
try {
|
||||
if (_actHome && _actHome.parent) _actHome.parent.insertBefore(_actMenu, _actHome.next);
|
||||
else document.body.appendChild(_actMenu);
|
||||
} catch (e) {}
|
||||
}
|
||||
document.querySelectorAll('.act.open').forEach(function (x) { x.classList.remove('open'); });
|
||||
_actOpen = null; _actMenu = null; _actHome = null;
|
||||
}
|
||||
function actPlace(btn, menu) {
|
||||
menu.style.left = '0px'; menu.style.top = '0px';
|
||||
var w = menu.offsetWidth, h = menu.offsetHeight;
|
||||
var br = btn.getBoundingClientRect();
|
||||
var vw = window.innerWidth, vh = window.innerHeight, pad = 8, gap = 6;
|
||||
var left = br.right - w; // rata kanan ke tombol
|
||||
if (left + w > vw - pad) left = vw - pad - w;
|
||||
if (left < pad) left = pad;
|
||||
var top = br.bottom + gap; // default: di bawah
|
||||
if (top + h > vh - pad) top = br.top - gap - h; // tak cukup -> ke atas
|
||||
if (top < pad) top = pad;
|
||||
menu.style.left = Math.round(left) + 'px';
|
||||
menu.style.top = Math.round(top) + 'px';
|
||||
}
|
||||
window.actToggle = function (e, btn) {
|
||||
try { if (e) { e.preventDefault(); e.stopPropagation(); } } catch (_) {}
|
||||
var a = btn && btn.closest ? btn.closest('.act') : null; if (!a) return;
|
||||
var wasOpen = a.classList.contains('open');
|
||||
actCloseAll();
|
||||
if (!wasOpen) {
|
||||
a.classList.add('open');
|
||||
var menu = a.querySelector('.act-menu');
|
||||
if (!menu) return;
|
||||
_actOpen = a; _actMenu = menu;
|
||||
_actHome = { parent: menu.parentNode, next: menu.nextSibling };
|
||||
document.body.appendChild(menu); // pindah ke body
|
||||
menu.classList.add('fixed');
|
||||
actPlace(btn, menu);
|
||||
}
|
||||
};
|
||||
// klik di luar menu -> tutup; klik di dalam menu -> biarkan aksi jalan dulu, tutup setelahnya
|
||||
document.addEventListener('click', function (ev) {
|
||||
if (_actMenu && ev.target && _actMenu.contains(ev.target)) { setTimeout(actCloseAll, 0); return; }
|
||||
actCloseAll();
|
||||
});
|
||||
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') actCloseAll(); });
|
||||
window.addEventListener('scroll', actCloseAll, true);
|
||||
window.addEventListener('resize', actCloseAll);
|
||||
@@ -0,0 +1,185 @@
|
||||
/* admin-toast.js — Notifikasi bertema untuk seluruh admin.
|
||||
* - toast(...) : popup kecil (opsional, untuk info ringan)
|
||||
* - notify(...) : OVERLAY layar blur + ikon + teks (untuk hasil aksi)
|
||||
* - notifyLoading(...) : OVERLAY blur + spinner (proses berjalan)
|
||||
* Otomatis:
|
||||
* - banner flash (.alert[data-flash]) -> overlay notify
|
||||
* - window.alert() -> overlay notify
|
||||
* - submit form POST -> notifyLoading (blur + spinner)
|
||||
*/
|
||||
(function () {
|
||||
if (window.__adminNotify) return;
|
||||
window.__adminNotify = true;
|
||||
|
||||
var CSS = [
|
||||
/* ---- overlay ---- */
|
||||
'.nf-ov{position:fixed;inset:0;z-index:2147483000;display:none;align-items:center;justify-content:center;padding:20px;background:rgba(4,7,12,.5);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);opacity:0;transition:opacity .25s ease}',
|
||||
'.nf-ov.show{display:flex;opacity:1}',
|
||||
'.nf-card{background:var(--bg2,#161b22);border:1px solid var(--border,rgba(255,255,255,.14));border-radius:18px;padding:32px 26px;max-width:min(92vw,410px);width:100%;text-align:center;box-shadow:0 26px 70px rgba(0,0,0,.6);transform:scale(.9);transition:transform .3s cubic-bezier(.2,.9,.3,1.25)}',
|
||||
'.nf-ov.show .nf-card{transform:scale(1)}',
|
||||
'.nf-spin{width:56px;height:56px;margin:0 auto 16px;border:4px solid var(--pdim,rgba(173,255,47,.2));border-top-color:var(--primary,#ADFF2F);border-radius:50%;animation:nfspin .8s linear infinite}',
|
||||
'@keyframes nfspin{to{transform:rotate(360deg)}}',
|
||||
'.nf-ic{font-size:60px;line-height:1;display:block;margin:0 auto 12px;animation:nfpop .35s cubic-bezier(.2,.9,.3,1.4)}',
|
||||
'@keyframes nfpop{0%{transform:scale(.4);opacity:0}100%{transform:scale(1);opacity:1}}',
|
||||
'.nf-success{color:var(--success,#3fb950)} .nf-error{color:var(--danger,#f85149)} .nf-warning{color:var(--warning,#d29922)} .nf-info{color:var(--info,#58a6ff)}',
|
||||
'.nf-title{font-size:17px;font-weight:800;margin-bottom:7px;color:var(--text,#e6edf3)}',
|
||||
'.nf-msg{font-size:13.5px;color:var(--muted,#7d8590);line-height:1.55;max-height:46vh;overflow:auto;word-break:break-word}',
|
||||
'.nf-msg b,.nf-msg code{color:var(--text,#e6edf3)}',
|
||||
'.nf-close{margin-top:18px;background:var(--pdim,rgba(173,255,47,.14));color:var(--primary,#ADFF2F);border:1px solid var(--primary,#ADFF2F);border-radius:10px;padding:8px 18px;font-weight:700;font-size:13px;cursor:pointer;font-family:inherit}',
|
||||
'.nf-close:hover{filter:brightness(1.1)}',
|
||||
'html[data-theme="light"] .nf-card,html.light-theme .nf-card{background:#fff;border-color:#e2e8f0}',
|
||||
'html[data-theme="light"] .nf-title,html.light-theme .nf-title{color:#0f172a}',
|
||||
'html[data-theme="light"] .nf-msg,html.light-theme .nf-msg{color:#475569}',
|
||||
'html[data-theme="light"] .nf-ov,html.light-theme .nf-ov{background:rgba(226,232,240,.55)}',
|
||||
/* ---- toast kecil ---- */
|
||||
'.toast-wrap{position:fixed;top:16px;right:16px;z-index:2147482900;display:flex;flex-direction:column;gap:10px;max-width:min(92vw,430px);pointer-events:none}',
|
||||
'.toast{pointer-events:auto;display:flex;gap:11px;align-items:flex-start;background:var(--bg2,#161b22);border:1px solid var(--border,rgba(255,255,255,.14));border-left:4px solid var(--info,#58a6ff);border-radius:12px;padding:12px 14px;box-shadow:0 16px 44px rgba(0,0,0,.5);color:var(--text,#e6edf3);transform:translateX(120%);opacity:0;transition:transform .28s cubic-bezier(.2,.8,.2,1),opacity .28s;font-size:13px;line-height:1.4}',
|
||||
'.toast.show{transform:translateX(0);opacity:1}',
|
||||
'.toast-ic{font-size:18px;line-height:1.15;margin-top:1px;flex:0 0 auto}',
|
||||
'.toast-body{flex:1;min-width:0}.toast-title{font-weight:700;margin-bottom:2px}.toast-msg{opacity:.94;word-break:break-word}',
|
||||
'.toast-x{margin-left:6px;background:transparent;border:none;color:var(--muted,#7d8590);font-size:19px;line-height:1;cursor:pointer;padding:0 2px}',
|
||||
'.toast-success{border-left-color:var(--success,#3fb950)} .toast-success .toast-ic{color:var(--success,#3fb950)}',
|
||||
'.toast-error{border-left-color:var(--danger,#f85149)} .toast-error .toast-ic{color:var(--danger,#f85149)}',
|
||||
'.toast-warning{border-left-color:var(--warning,#d29922)} .toast-warning .toast-ic{color:var(--warning,#d29922)}',
|
||||
'.toast-info{border-left-color:var(--info,#58a6ff)} .toast-info .toast-ic{color:var(--info,#58a6ff)}',
|
||||
/* ---- mobile ---- */
|
||||
'@media(max-width:520px){.nf-ov{padding:14px;align-items:center}.nf-card{padding:26px 18px;border-radius:16px;max-width:100%}.nf-ic{font-size:50px}.nf-title{font-size:15.5px}.nf-msg{font-size:13px;max-height:52vh}.toast-wrap{top:10px;right:10px;left:10px;max-width:none}}'
|
||||
].join('\n');
|
||||
|
||||
function ensureCss() {
|
||||
if (document.getElementById('adminNotifyCss')) return;
|
||||
var st = document.createElement('style'); st.id = 'adminNotifyCss'; st.textContent = CSS; document.head.appendChild(st);
|
||||
}
|
||||
|
||||
var ICONS = { success: 'bi-check-circle-fill', error: 'bi-x-octagon-fill', warning: 'bi-exclamation-triangle-fill', info: 'bi-info-circle-fill' };
|
||||
|
||||
function autoDur(type, text, base) {
|
||||
var len = String(text || '').length;
|
||||
var b = base || (type === 'error' ? 2800 : type === 'warning' ? 2400 : 2000);
|
||||
return Math.min(b + Math.floor(len / 60) * 900, 12000);
|
||||
}
|
||||
|
||||
/* ================= TOAST (kecil) ================= */
|
||||
function toastWrap() {
|
||||
ensureCss();
|
||||
var w = document.getElementById('adminToastWrap');
|
||||
if (!w) { w = document.createElement('div'); w.id = 'adminToastWrap'; w.className = 'toast-wrap'; document.body.appendChild(w); }
|
||||
return w;
|
||||
}
|
||||
window.toast = function (type, title, message, opts) {
|
||||
try {
|
||||
if (type && typeof type === 'object') { opts = type; type = opts.type; title = opts.title; message = opts.message || opts.text; }
|
||||
type = String(type || 'info'); if (!ICONS[type]) type = 'info';
|
||||
opts = opts || {};
|
||||
var w = toastWrap();
|
||||
var el = document.createElement('div');
|
||||
el.className = 'toast toast-' + type;
|
||||
el.innerHTML = '<i class="bi ' + ICONS[type] + ' toast-ic"></i><div class="toast-body">' + (title ? '<div class="toast-title">' + title + '</div>' : '') + '<div class="toast-msg">' + (message == null ? '' : message) + '</div></div><button class="toast-x">×</button>';
|
||||
w.appendChild(el);
|
||||
requestAnimationFrame(function () { el.classList.add('show'); });
|
||||
var dur = opts.duration || autoDur(type, (title || '') + ' ' + (message || ''));
|
||||
var t = setTimeout(close, dur);
|
||||
function close() { el.classList.remove('show'); setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 280); }
|
||||
el.querySelector('.toast-x').addEventListener('click', function () { clearTimeout(t); close(); });
|
||||
el.addEventListener('mouseenter', function () { clearTimeout(t); });
|
||||
el.addEventListener('mouseleave', function () { t = setTimeout(close, 2200); });
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
/* ================= OVERLAY (blur + ikon) ================= */
|
||||
var _ovTimer = null;
|
||||
function ov() {
|
||||
ensureCss();
|
||||
var el = document.getElementById('nfOv');
|
||||
if (!el) {
|
||||
el = document.createElement('div'); el.id = 'nfOv'; el.className = 'nf-ov';
|
||||
el.innerHTML = '<div class="nf-card" role="alertdialog" aria-live="assertive"><div class="nf-spin"></div><i class="bi nf-ic"></i><div class="nf-title"></div><div class="nf-msg"></div><button type="button" class="nf-close">Tutup</button></div>';
|
||||
document.body.appendChild(el);
|
||||
el.querySelector('.nf-close').addEventListener('click', hideOv);
|
||||
el.addEventListener('click', function (e) { if (e.target === el) hideOv(); });
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') hideOv(); });
|
||||
}
|
||||
return el;
|
||||
}
|
||||
function hideOv() { var el = document.getElementById('nfOv'); if (!el) return; clearTimeout(_ovTimer); el.classList.remove('show'); }
|
||||
window.closeNotify = hideOv;
|
||||
|
||||
window.notifyLoading = function (text, title) {
|
||||
var el = ov();
|
||||
el.querySelector('.nf-spin').style.display = '';
|
||||
var ic = el.querySelector('.nf-ic'); ic.style.display = 'none'; ic.className = 'bi nf-ic';
|
||||
var t = el.querySelector('.nf-title'); if (title) { t.style.display = ''; t.textContent = title; } else { t.style.display = 'none'; t.textContent = ''; }
|
||||
el.querySelector('.nf-msg').innerHTML = text || '';
|
||||
el.querySelector('.nf-close').style.display = 'none';
|
||||
clearTimeout(_ovTimer);
|
||||
el.classList.add('show');
|
||||
return { loading: window.notifyLoading, done: function (ty, ti, ms) { window.notify(ty, ti, ms); }, close: hideOv };
|
||||
};
|
||||
|
||||
window.notify = function (type, title, message, opts) {
|
||||
if (type && typeof type === 'object') { opts = type; type = opts.type; title = opts.title; message = opts.message || opts.text; }
|
||||
type = String(type || 'info'); if (!ICONS[type]) type = 'info';
|
||||
opts = opts || {};
|
||||
var el = ov();
|
||||
el.querySelector('.nf-spin').style.display = 'none';
|
||||
var ic = el.querySelector('.nf-ic'); ic.style.display = ''; ic.className = 'bi ' + ICONS[type] + ' nf-ic nf-' + type;
|
||||
var t = el.querySelector('.nf-title'); if (title) { t.style.display = ''; t.textContent = title; } else { t.style.display = 'none'; t.textContent = ''; }
|
||||
el.querySelector('.nf-msg').innerHTML = (message == null ? '' : message);
|
||||
el.querySelector('.nf-close').style.display = '';
|
||||
clearTimeout(_ovTimer);
|
||||
el.classList.add('show');
|
||||
var dur = opts.duration != null ? opts.duration : autoDur(type, (title || '') + ' ' + (message || ''));
|
||||
if (dur > 0) _ovTimer = setTimeout(hideOv, dur);
|
||||
return el;
|
||||
};
|
||||
|
||||
/* ================= integrasi otomatis ================= */
|
||||
function flashType(cls) {
|
||||
if (cls.indexOf('alert-s') !== -1) return 'success';
|
||||
if (cls.indexOf('alert-d') !== -1) return 'error';
|
||||
if (cls.indexOf('alert-w') !== -1) return 'warning';
|
||||
return 'info';
|
||||
}
|
||||
function convertFlash() {
|
||||
var list = document.querySelectorAll('.alert[data-flash]');
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var a = list[i];
|
||||
var type = flashType(a.className || '');
|
||||
var html = a.innerHTML.replace(/^\s*<i[^>]*>\s*<\/i>\s*/i, '').trim();
|
||||
a.style.display = 'none';
|
||||
(function (tp, h) {
|
||||
var title = tp === 'success' ? 'Berhasil' : tp === 'error' ? 'Gagal' : tp === 'warning' ? 'Perhatian' : 'Info';
|
||||
// tampilkan overlay langsung (aksi sudah selesai setelah redirect)
|
||||
window.notify(tp, title, h);
|
||||
})(type, html);
|
||||
}
|
||||
}
|
||||
window.__adminFlashToNotify = convertFlash;
|
||||
|
||||
var _nativeAlert = window.alert;
|
||||
window.alert = function (msg) {
|
||||
try {
|
||||
var t = String(msg == null ? '' : msg);
|
||||
var low = t.toLowerCase();
|
||||
var isErr = /gagal|error|tidak |invalid|tidak valid|kosong|forbidden|ditolak|harus|wajib|salah|exception|kedaluwarsa|tidak ditemukan|not found|401|403|500/.test(low);
|
||||
var type = isErr ? 'error' : 'success';
|
||||
window.notify(type, isErr ? 'Gagal' : 'Berhasil', t.replace(/\n/g, '<br>'));
|
||||
} catch (e) { try { _nativeAlert(msg); } catch (_) { } }
|
||||
};
|
||||
|
||||
// form POST → tampilkan loading blur
|
||||
document.addEventListener('submit', function (e) {
|
||||
try {
|
||||
var f = e.target;
|
||||
if (!f || f.tagName !== 'FORM') return;
|
||||
if ((f.getAttribute('method') || 'get').toLowerCase() !== 'post') return;
|
||||
if (f.getAttribute('target') === '_blank') return;
|
||||
if (f.hasAttribute('data-no-loading')) return;
|
||||
if (e.defaultPrevented) return;
|
||||
window.notifyLoading(f.getAttribute('data-loading') || 'Menyimpan perubahan…', 'Mohon tunggu');
|
||||
} catch (_) { }
|
||||
}, false);
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', convertFlash);
|
||||
else convertFlash();
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
/* page-loader.js — Animasi saat berpindah halaman.
|
||||
- Muncul saat klik link (navigasi same-origin) / submit form.
|
||||
- Sembunyi saat halaman baru selesai dimuat atau saat PWA resume (bfcache).
|
||||
- Fallback timer: bila tak jadi navigasi (mis. AJAX), loader otomatis disembunyikan. */
|
||||
(function () {
|
||||
var bar = null, ov = null, tShow = null, tBar = null, tFallback = null, w = 0;
|
||||
|
||||
function build() {
|
||||
if (bar || !document.body) return;
|
||||
bar = document.createElement('div'); bar.id = 'pgbar';
|
||||
ov = document.createElement('div'); ov.id = 'pgload';
|
||||
ov.innerHTML = '<div class="pgbox"><div class="pgspin"></div><div class="pgtxt">Memuat…</div></div>';
|
||||
document.body.appendChild(bar);
|
||||
document.body.appendChild(ov);
|
||||
}
|
||||
|
||||
function startBar() {
|
||||
build(); if (!bar) return;
|
||||
w = 8; bar.classList.add('on'); bar.style.width = '8%';
|
||||
clearInterval(tBar);
|
||||
tBar = setInterval(function () { w = Math.min(w + Math.max(2, (90 - w) * 0.12), 90); bar.style.width = w.toFixed(1) + '%'; }, 220);
|
||||
}
|
||||
|
||||
function show(delay, fallbackMs) {
|
||||
build();
|
||||
clearTimeout(tShow); clearTimeout(tFallback);
|
||||
tShow = setTimeout(function () { startBar(); if (ov) ov.classList.add('on'); }, delay == null ? 150 : delay);
|
||||
if (fallbackMs) tFallback = setTimeout(hide, fallbackMs);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clearTimeout(tShow); clearTimeout(tFallback); clearInterval(tBar);
|
||||
if (bar) { bar.style.width = '100%'; bar.classList.remove('on'); }
|
||||
if (ov) ov.classList.remove('on');
|
||||
setTimeout(function () { if (bar) bar.style.width = '0'; }, 300);
|
||||
}
|
||||
|
||||
function samePage(u) {
|
||||
try { return u.origin === location.origin && u.pathname === location.pathname && u.search === location.search; } catch (e) { return false; }
|
||||
}
|
||||
function eligible(a) {
|
||||
if (!a) return false;
|
||||
if (a.hasAttribute('download') || a.target === '_blank' || a.hasAttribute('data-no-loader')) return false;
|
||||
var href = a.getAttribute('href');
|
||||
if (!href || href.charAt(0) === '#') return false;
|
||||
if (/^(mailto:|tel:|javascript:|blob:|data:)/i.test(href)) return false;
|
||||
var u; try { u = new URL(a.href, location.href); } catch (e) { return false; }
|
||||
if (u.origin !== location.origin) return false;
|
||||
if (samePage(u) && !u.hash) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
|
||||
if (eligible(a)) show(150, 12000); // navigasi nyata → fallback lama
|
||||
}, true);
|
||||
|
||||
document.addEventListener('submit', function (e) {
|
||||
if (e.defaultPrevented) return;
|
||||
var f = e.target;
|
||||
if (!f || f.hasAttribute('data-no-loader') || f.target === '_blank') return;
|
||||
// Bisa jadi AJAX → fallback lebih pendek; bila navigasi nyata, dokumen berganti & timer hilang
|
||||
show(120, 2500);
|
||||
}, true);
|
||||
|
||||
window.addEventListener('pageshow', hide);
|
||||
window.addEventListener('load', hide);
|
||||
document.addEventListener('DOMContentLoaded', function () { if (bar) hide(); });
|
||||
})();
|
||||
@@ -0,0 +1,63 @@
|
||||
/* service-notice-badge.js — badge mode status layanan di topbar admin.
|
||||
SELALU tampil; yang berbeda hanya label & warna (Normal / Gamas / Perbaikan Sistem).
|
||||
Data disuntik server-side via window.__serviceNotice. Non-invasif: tidak mengubah view. */
|
||||
(function () {
|
||||
var n = window.__serviceNotice;
|
||||
if (!n || !n.mode) return;
|
||||
|
||||
function labelOf() {
|
||||
if (n.badgeLabel) return n.badgeLabel;
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'Gamas';
|
||||
if (n.mode === 'PERBAIKAN') return 'Perbaikan Sistem';
|
||||
return 'Normal';
|
||||
}
|
||||
function iconOf() {
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'bi-exclamation-triangle-fill';
|
||||
if (n.mode === 'PERBAIKAN') return 'bi-tools';
|
||||
return 'bi-check-circle-fill';
|
||||
}
|
||||
function clsOf() {
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'sn-badge-danger sn-badge-pulse';
|
||||
if (n.mode === 'PERBAIKAN') return 'sn-badge-primary';
|
||||
return 'sn-badge-normal';
|
||||
}
|
||||
function titleOf() {
|
||||
if (n.mode === 'GANGGUAN_MASAL') return 'Status layanan: Gangguan Masal' + (n.estimateText ? ' \u00b7 estimasi selesai ' + n.estimateText : '');
|
||||
if (n.mode === 'PERBAIKAN') return 'Status layanan: Perbaikan Sistem' + (n.estimateText ? ' \u00b7 estimasi selesai ' + n.estimateText : '');
|
||||
return 'Status layanan: Normal (tidak ada gangguan)';
|
||||
}
|
||||
|
||||
function makeBadge() {
|
||||
var a = document.createElement('a');
|
||||
a.href = '/admin/service-notice';
|
||||
a.setAttribute('data-notice-badge', '1');
|
||||
a.className = 'sn-badge ' + clsOf();
|
||||
a.title = titleOf() + ' \u2014 klik untuk ubah';
|
||||
a.innerHTML = '<i class="bi ' + iconOf() + '"></i><span>' + labelOf() + '</span>';
|
||||
return a;
|
||||
}
|
||||
|
||||
function place() {
|
||||
var existing = document.querySelector('[data-notice-badge]');
|
||||
if (existing) { existing.replaceWith(makeBadge()); return true; }
|
||||
var target = document.querySelector('.topbar .tb-right') || document.querySelector('.tb-right');
|
||||
if (!target) return false;
|
||||
target.insertBefore(makeBadge(), target.firstChild);
|
||||
return true;
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (place()) return;
|
||||
var tries = 0;
|
||||
var t = setInterval(function () {
|
||||
if (place() || ++tries > 20) clearInterval(t);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
window.addEventListener('pageshow', function () { setTimeout(place, 100); });
|
||||
})();
|
||||
@@ -0,0 +1,61 @@
|
||||
/* theme-toggle.js — switch tema global (portal & pelanggan).
|
||||
- Sinkronkan data-theme + data-bs-theme + class .light-theme + color-scheme
|
||||
- Pakai #topbarThemeBtn / .theme-fab yang sudah ada, jika tidak buat floating.
|
||||
- MutationObserver: bila skrip lain mengubah data-theme, atribut lain ikut sinkron (live).
|
||||
- pageshow/visibilitychange: menyegarkan saat PWA resume / bfcache. */
|
||||
(function () {
|
||||
var KEY = 'landing-theme';
|
||||
function current() { return document.documentElement.getAttribute('data-theme') || 'dark'; }
|
||||
function syncIcon() {
|
||||
var isLight = current() === 'light';
|
||||
var btn = document.getElementById('topbarThemeBtn');
|
||||
if (btn) { var i = btn.querySelector('i'); if (i) i.className = isLight ? 'bi bi-moon-stars' : 'bi bi-sun'; }
|
||||
}
|
||||
function sync() {
|
||||
var isLight = current() === 'light';
|
||||
var bs = isLight ? 'light' : 'dark';
|
||||
if (document.documentElement.getAttribute('data-bs-theme') !== bs) document.documentElement.setAttribute('data-bs-theme', bs);
|
||||
if (document.documentElement.classList.contains('light-theme') !== isLight) document.documentElement.classList.toggle('light-theme', isLight);
|
||||
if (document.body && document.body.classList.contains('light-theme') !== isLight) document.body.classList.toggle('light-theme', isLight);
|
||||
try { if (document.documentElement.style.colorScheme !== bs) document.documentElement.style.colorScheme = bs; } catch (e) {}
|
||||
syncIcon();
|
||||
}
|
||||
function apply(t) {
|
||||
t = t || (function () { try { return localStorage.getItem(KEY); } catch (e) { return null; } })() || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
sync();
|
||||
return t;
|
||||
}
|
||||
window.applyTheme = apply;
|
||||
window.toggleTheme = function () {
|
||||
var cur = current() === 'dark' ? 'light' : 'dark';
|
||||
try { localStorage.setItem(KEY, cur); } catch (e) {}
|
||||
apply(cur);
|
||||
try { document.body && document.body.getBoundingClientRect(); } catch (e) {} // paksa repaint
|
||||
};
|
||||
apply();
|
||||
|
||||
// Bila skrip lain (mis. toggleDashTheme) mengubah data-theme, sinkronkan atribut lain
|
||||
try { new MutationObserver(sync).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] }); } catch (e) {}
|
||||
|
||||
function bind(el) {
|
||||
if (!el || el.dataset.themeBound || el.hasAttribute('onclick')) return;
|
||||
el.dataset.themeBound = '1';
|
||||
el.addEventListener('click', function (e) { e.preventDefault(); window.toggleTheme(); });
|
||||
}
|
||||
function mount() {
|
||||
var top = document.getElementById('topbarThemeBtn');
|
||||
if (top) { bind(top); syncIcon(); return; }
|
||||
var fab = document.querySelector('.theme-fab');
|
||||
if (fab) { bind(fab); return; }
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button'; b.className = 'theme-fab';
|
||||
b.setAttribute('aria-label', 'Ganti tema'); b.setAttribute('title', 'Mode Gelap / Terang');
|
||||
b.innerHTML = '<i class="bi bi-circle-half"></i>';
|
||||
b.addEventListener('click', window.toggleTheme);
|
||||
document.body.appendChild(b);
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount); else mount();
|
||||
window.addEventListener('pageshow', function () { apply(); });
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) sync(); });
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('app-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
if (savedTheme === 'light') {
|
||||
document.body?.classList.add('light-theme');
|
||||
}
|
||||
})();
|
||||
|
||||
function toggleAppTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
if (document.body) {
|
||||
document.body.classList.toggle('light-theme', newTheme === 'light');
|
||||
}
|
||||
localStorage.setItem('app-theme', newTheme);
|
||||
updateThemeToggleIcons(newTheme);
|
||||
}
|
||||
|
||||
function updateThemeToggleIcons(theme) {
|
||||
const icons = document.querySelectorAll('.theme-toggle-icon');
|
||||
const btns = document.querySelectorAll('.theme-toggle-btn');
|
||||
icons.forEach(icon => {
|
||||
if (theme === 'light') {
|
||||
icon.className = 'bi bi-sun-fill theme-toggle-icon';
|
||||
icon.style.color = '#f59e0b';
|
||||
} else {
|
||||
icon.className = 'bi bi-moon-stars-fill theme-toggle-icon';
|
||||
icon.style.color = '#fbbf24';
|
||||
}
|
||||
});
|
||||
btns.forEach(btn => {
|
||||
btn.setAttribute('title', theme === 'light' ? 'Ganti ke Mode Gelap (Dark)' : 'Ganti ke Mode Terang (Light)');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const theme = localStorage.getItem('app-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
if (theme === 'light') {
|
||||
document.body?.classList.add('light-theme');
|
||||
}
|
||||
updateThemeToggleIcons(theme);
|
||||
});
|
||||
Reference in new issue
Block a user