/* 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 '' + row[0] + '' + row[2](va) + '' + (b ? row[2](vb) : 'isian belum valid') + ''; }).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(); } })();