222 lines
10 KiB
JavaScript
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();
|
|
}
|
|
})();
|