Files
2026-10-20 13:16:38 +09:00

222 lines
10 KiB
JavaScript

/* Admin page: tier editors, unsaved-change bar, and live impact preview. */
(function () {
'use strict';
var F = window.SerubiFormat;
var E = window.SerubiEngine;
var saved = window.SERUBI_PARAMS;
var form = document.getElementById('paramForm');
var $ = function (id) { return document.getElementById(id); };
var clone = function (o) { return JSON.parse(JSON.stringify(o)); };
// ---- thousands formatting for every Rupiah input ----
document.addEventListener('input', function (ev) {
var el = ev.target;
if (el.matches && el.matches('.is-idr input, #pvNominal')) F.liveFormatIdr(el);
});
// ---- tier editors (product tiers and deposito matrix share the row logic) ----
function tierRows(editor) { return Array.prototype.slice.call(editor.querySelectorAll('tbody .tier-row')); }
function refreshTiers(editor) {
var rows = tierRows(editor);
var prev = 0;
rows.forEach(function (row, i) {
var last = i === rows.length - 1;
var maxInput = row.querySelector('.t-max');
var wrap = row.querySelector('.tier-max-wrap');
var from = row.querySelector('.tier-from');
if (last) {
from.textContent = rows.length === 1 ? 'Semua nominal' : 'Di atas Rp' + F.num(prev);
wrap.hidden = true;
maxInput.value = '';
} else {
from.textContent = i === 0 ? 'Sampai dengan' : 'Rp' + F.num(prev + 1) + ' s.d.';
wrap.hidden = false;
prev = F.parseIdr(maxInput.value) || prev;
}
// renumber names: prefix[i][field] or bands[i] / rates[i][tenor]
row.querySelectorAll('input[name]').forEach(function (inp) {
inp.name = inp.name.replace(/^(\w+(?:\[[a-z_]\w*\])?)\[\d+\]/, '$1[' + i + ']');
});
var del = row.querySelector('.tier-del');
if (del) { del.disabled = rows.length === 1; del.setAttribute('aria-label', 'Hapus baris ' + (i + 1)); }
});
}
document.querySelectorAll('[data-tiers], #depoMatrix').forEach(function (ed) {
var editor = ed.id === 'depoMatrix' ? ed.closest('.panel') : ed;
refreshTiers(editor);
editor.addEventListener('input', function (ev) { if (ev.target.classList.contains('t-max')) refreshTiers(editor); });
editor.addEventListener('click', function (ev) {
var del = ev.target.closest('.tier-del');
var add = ev.target.closest('.tier-add');
if (del) {
var rows = tierRows(editor);
if (rows.length > 1) { del.closest('.tier-row').remove(); refreshTiers(editor); markDirty(); }
} else if (add) {
var rowsA = tierRows(editor);
var lastRow = rowsA[rowsA.length - 1];
var copy = lastRow.cloneNode(true);
copy.querySelectorAll('input').forEach(function (i) { i.value = ''; i.removeAttribute('aria-invalid'); });
// the new row goes before the open-ended last row, keeping "di atas" at the bottom
lastRow.parentNode.insertBefore(copy, lastRow);
copy.querySelectorAll('.t-rate').forEach(function (inp, k) {
var src = lastRow.querySelectorAll('.t-rate')[k];
inp.value = src ? src.value : '';
});
refreshTiers(editor);
var first = copy.querySelector('.t-max');
if (first) first.focus();
markDirty();
}
});
});
// ---- cashback tiers ----
var cbTable = $('cbTable');
function refreshCb() {
var rows = cbTable.querySelectorAll('tbody .cb-row');
rows.forEach(function (row, i) {
var share = F.parsePct(row.querySelector('.cb-share').value);
row.querySelector('.cb-bank').textContent = share === null || isNaN(share) ? '—' : F.pctInput(1 - share) + '%';
row.querySelectorAll('input[name]').forEach(function (inp) { inp.name = inp.name.replace(/^cb\[\d+\]/, 'cb[' + i + ']'); });
row.querySelector('.cb-del').disabled = rows.length === 1;
});
}
if (cbTable) {
refreshCb();
cbTable.addEventListener('input', refreshCb);
cbTable.addEventListener('click', function (ev) {
var del = ev.target.closest('.cb-del');
if (del && cbTable.querySelectorAll('tbody .cb-row').length > 1) { del.closest('.cb-row').remove(); refreshCb(); markDirty(); }
});
document.querySelector('.cb-add').addEventListener('click', function () {
var rows = cbTable.querySelectorAll('tbody .cb-row');
var copy = rows[rows.length - 1].cloneNode(true);
copy.querySelectorAll('input').forEach(function (i) { i.value = ''; });
cbTable.querySelector('tbody').appendChild(copy);
refreshCb();
copy.querySelector('.cb-min').focus();
markDirty();
});
}
// ---- unsaved changes ----
var dirty = false;
var savebar = $('savebar');
function markDirty() {
if (!form) return;
dirty = true;
savebar.hidden = false;
document.body.classList.add('has-savebar');
preview();
}
if (form) {
form.addEventListener('input', markDirty);
form.addEventListener('change', markDirty);
form.addEventListener('submit', function () {
dirty = false;
form.querySelectorAll('button[type=submit]').forEach(function (b) { b.disabled = true; b.setAttribute('aria-busy', 'true'); });
$('btnSave').disabled = true;
});
$('btnSave').addEventListener('click', function () { form.requestSubmit ? form.requestSubmit() : form.submit(); });
$('btnDiscard').addEventListener('click', function () { dirty = false; location.href = location.pathname + location.search; });
window.addEventListener('beforeunload', function (ev) { if (dirty) { ev.preventDefault(); ev.returnValue = ''; } });
document.addEventListener('serubi:dirty', markDirty);
}
// ---- draft params from the form ----
function readTierEditor(editor, rateSelector) {
return tierRows(editor).map(function (row, i, all) {
return { max: i === all.length - 1 ? null : F.parseIdr(row.querySelector('.t-max').value), rate: F.parsePct(row.querySelector(rateSelector).value) };
});
}
function draftParams() {
var p = clone(saved);
if (!form) return p;
var kind = form.dataset.kind;
var bad = false;
if (kind === 'settings') {
form.querySelectorAll('[data-setting]').forEach(function (inp) {
var u = inp.dataset.unit, v;
if (u === 'pct') v = F.parsePct(inp.value);
else if (u === 'idr') v = F.parseIdr(inp.value);
else v = parseFloat(String(inp.value).replace(',', '.'));
if (v === null || isNaN(v)) bad = true; else p.settings[inp.dataset.setting] = v;
});
} else if (kind === 'products') {
form.querySelectorAll('[data-tiers]').forEach(function (ed) { p.productRates[ed.dataset.tiers] = readTierEditor(ed, '.t-rate'); });
} else if (kind === 'deposito') {
var panel = $('depoMatrix').closest('.panel');
p.tenors.forEach(function (t) { p.depositoRates[t] = readTierEditor(panel, '.t-rate[data-tenor="' + t + '"]'); });
} else if (kind === 'cashback') {
p.cashbackTiers = Array.prototype.map.call(cbTable.querySelectorAll('tbody .cb-row'), function (row) {
return { min: F.parseIdr(row.querySelector('.cb-min').value), nasabah: F.parsePct(row.querySelector('.cb-share').value) };
}).filter(function (t) { return t.min !== null && t.nasabah !== null && !isNaN(t.nasabah); });
p.cashbackTenors = Array.prototype.filter.call(form.querySelectorAll('[data-cbtenor]'), function (c) { return c.checked; })
.map(function (c) { return Number(c.dataset.cbtenor); });
}
return bad ? null : p;
}
// ---- warning: a higher nominal tier with a lower rate is usually a typing mistake ----
var PRODUCT_NAMES = { giro: 'Giro Umum', simanja: 'SIMANJA', simpeda: 'SIMPEDA' };
function descending(tiers) {
for (var i = 1; i < (tiers || []).length; i++) {
if (Number(tiers[i].rate) < Number(tiers[i - 1].rate)) return i;
}
return 0;
}
function tierWarnings(p) {
var out = [];
Object.keys(PRODUCT_NAMES).forEach(function (k) {
var i = descending(p.productRates[k]);
if (i) out.push('Rate ' + PRODUCT_NAMES[k] + ' baris ' + (i + 1) + ' lebih kecil dari baris ' + i + '.');
});
p.tenors.forEach(function (t) {
var i = descending(p.depositoRates[t]);
if (i) out.push('Rate deposito ' + t + ' bulan baris ' + (i + 1) + ' lebih kecil dari baris ' + i + '.');
});
return out;
}
// ---- preview ----
var pvRows = $('pvRows');
function preview() {
if (!pvRows) return;
var n = F.parseIdr($('pvNominal').value) || 0;
var t = Number($('pvTenor').value);
var input = { nominal: n, tenor: t, customerType: 'perorangan' };
var a = E.calculate(saved, input);
var dp = draftParams();
var b = dp ? E.calculate(dp, input) : null;
var by = function (r, k) { return r.comparison.filter(function (c) { return c.key === k; })[0]; };
var rows = [
['Cashback neto', function (r) { return r.cashback.net; }, F.rp],
['Setara p.a.', function (r) { return r.cashback.equiv; }, F.pct],
['Total Giro', function (r) { return by(r, 'giro').total; }, F.rp],
['Total SIMANJA', function (r) { return by(r, 'simanja').total; }, F.rp],
['Total SIMPEDA', function (r) { return by(r, 'simpeda').total; }, F.rp],
['Deposito neto', function (r) { return r.deposito.net; }, F.rp],
['Rate deposito', function (r) { return r.deposito.rate; }, F.pct]
];
pvRows.innerHTML = rows.map(function (row) {
var va = row[1](a), vb = b ? row[1](b) : null;
var changed = b && Math.abs(va - vb) > 1e-9;
var cls = changed ? (vb > va ? ' class="up"' : ' class="down"') : '';
return '<tr' + (changed ? ' class="is-changed"' : '') + '><th scope="row">' + row[0] + '</th><td>' + row[2](va) + '</td><td' + cls + '>' +
(b ? row[2](vb) : '<span class="muted">isian belum valid</span>') + '</td></tr>';
}).join('');
var warn = $('pvWarn');
var list = dp ? tierWarnings(dp) : [];
warn.hidden = !list.length;
warn.querySelector('span').textContent = list.length ? 'Periksa kembali: ' + list.join(' ') + ' Simpan tetap diizinkan bila memang disengaja.' : '';
}
if (pvRows) {
$('pvNominal').addEventListener('input', preview);
$('pvTenor').addEventListener('change', preview);
preview();
}
})();