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:
oxpoe committed 2026-10-09 05:51:21 +07:00
commit eaf05f2811
280 files changed
+123975

No files matched your search

+246
View File
@@ -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();
}
})();
+12
View File
@@ -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 () {});
}
+104
View File
@@ -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);
+185
View File
@@ -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">&times;</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();
})();
+70
View File
@@ -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(); });
})();
+63
View File
@@ -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); });
})();
+61
View File
@@ -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(); });
})();
+44
View File
@@ -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);
});