asd
This commit is contained in:
commit
3e4a056b14
67 files changed
+13926
No files matched your search
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Calculator page controller. Results are calculated in the browser (instant,
|
||||
* also offline) from the folded public model; printing and copying fetch the
|
||||
* official numbers from calc.php first. A logged-in admin calculates with the
|
||||
* full parameters to see "Rincian perhitungan".
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var meta = window.SERUBI_PUBLIC;
|
||||
var F = window.SerubiFormat;
|
||||
var PE = window.SerubiPublicEngine;
|
||||
if (!meta || !meta.model || !F || !PE) return;
|
||||
var model = meta.model;
|
||||
var full = meta.full && window.SerubiEngine ? meta.full : null; // admin only
|
||||
var PRODUCT_KEYS = ['giro', 'simanja', 'simpeda'];
|
||||
var STALE_HOURS = 24; // older than this: the page came from the offline copy
|
||||
var MAX_MODEL_AGE_DAYS = 30; // older than this: refuse to calculate with it
|
||||
var VERIFY_TIMEOUT_MS = 4000;
|
||||
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
var form = $('calcForm');
|
||||
var nominalEl = $('nominal');
|
||||
var display = $('hasil');
|
||||
var tape = $('tape');
|
||||
var MAX_NOMINAL = 1e15;
|
||||
var DEFAULTS = { nominal: 55000000, tenor: 6, cust: 'perorangan' };
|
||||
var SHOW_DEPOSITO = false; // perbandingan deposito disembunyikan dari hasil simulasi
|
||||
var CUST_LABEL = { perorangan: 'Perorangan', badan: 'Badan usaha' };
|
||||
var lastResult = null;
|
||||
var feedTimer = null;
|
||||
var verifiedKey = null; // inputs whose numbers were confirmed by the server
|
||||
var blocked = false;
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; });
|
||||
}
|
||||
// Shrinks an element's font (down to minPx) until its single line fits its box,
|
||||
// so very large amounts never wrap mid-number or overflow.
|
||||
function fitText(el, minPx) {
|
||||
if (!el) return;
|
||||
el.style.fontSize = '';
|
||||
var size = parseFloat(window.getComputedStyle(el).fontSize);
|
||||
for (var i = 0; i < 40 && el.scrollWidth > el.clientWidth + 1 && size > minPx; i++) {
|
||||
size = Math.max(minPx, size * 0.94);
|
||||
el.style.fontSize = size + 'px';
|
||||
}
|
||||
}
|
||||
function fitAll() { fitText($('dNet'), 18); fitText(nominalEl, 15); }
|
||||
|
||||
function radio(name) { var el = form.querySelector('input[name="' + name + '"]:checked'); return el ? el.value : null; }
|
||||
function setRadio(name, value) {
|
||||
var el = form.querySelector('input[name="' + name + '"][value="' + value + '"]');
|
||||
if (el) el.checked = true;
|
||||
}
|
||||
function line(label, value, cls) {
|
||||
return '<div' + (cls ? ' class="' + cls + '"' : '') + '><dt>' + label + '</dt><dd>' + value + '</dd></div>';
|
||||
}
|
||||
function tenorText(t) { return t + ' bulan'; }
|
||||
|
||||
function readState() {
|
||||
return { nominal: F.parseIdr(nominalEl.value), tenor: Number(radio('tenor')), cust: radio('cust') || 'perorangan' };
|
||||
}
|
||||
|
||||
function validate(state) {
|
||||
var err = $('nominalErr');
|
||||
var msg = '';
|
||||
if (state.nominal === null || state.nominal <= 0) msg = 'Masukkan nominal penempatan dana.';
|
||||
else if (state.nominal > MAX_NOMINAL) msg = 'Nominal terlalu besar. Maksimal Rp1.000 triliun.';
|
||||
err.textContent = msg;
|
||||
err.hidden = !msg;
|
||||
nominalEl.setAttribute('aria-invalid', msg ? 'true' : 'false');
|
||||
nominalEl.parentNode.classList.toggle('is-invalid', !!msg);
|
||||
return !msg;
|
||||
}
|
||||
|
||||
function note(text) {
|
||||
var el = $('dNote');
|
||||
if (!text) { el.hidden = true; el.innerHTML = ''; return; }
|
||||
el.innerHTML = '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg><span>' + text + '</span>';
|
||||
el.hidden = false;
|
||||
}
|
||||
|
||||
function ineligibleText(r) {
|
||||
if (r.reason === 'tenor') {
|
||||
return 'Jangka waktu ' + r.input.tenor + ' bulan tidak termasuk program cashback. Pilih ' +
|
||||
model.cashbackTenors.join(', ').replace(/, (\d+)$/, ', atau $1') + ' bulan.';
|
||||
}
|
||||
if (r.reason === 'nominal') {
|
||||
return 'Cashback berlaku mulai ' + F.rp(r.minNominal) + '. Kurang ' + F.rp(r.minNominal - r.input.nominal) + ' lagi.';
|
||||
}
|
||||
if (r.reason === 'spread') return 'Spread negatif dengan parameter saat ini, sehingga tidak ada cashback.';
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderEmpty() {
|
||||
display.classList.add('is-off');
|
||||
$('dNet').textContent = 'Rp0';
|
||||
['dGross', 'dMonthly'].forEach(function (id) { $(id).textContent = '—'; });
|
||||
$('dEquiv').textContent = '—';
|
||||
$('dFlag').textContent = '';
|
||||
note('');
|
||||
}
|
||||
|
||||
function render(r) {
|
||||
var ok = r.eligible;
|
||||
var t = r.input.tenor;
|
||||
display.classList.toggle('is-off', !ok);
|
||||
|
||||
// display
|
||||
$('dNet').textContent = F.rp(r.cashback.net);
|
||||
fitText($('dNet'), 18);
|
||||
$('dGross').textContent = F.rp(r.cashback.gross);
|
||||
$('dEquiv').textContent = F.pct(r.cashback.equiv) + ' p.a.';
|
||||
$('dMonthly').textContent = F.rp(r.cashback.monthly);
|
||||
$('dFlag').textContent = !ok ? 'Tanpa cashback' : (typeof r.share === 'number' ? 'Porsi nasabah ' + F.pct(r.share).replace(',00', '') : 'Mendapat cashback');
|
||||
note(ok ? '' : ineligibleText(r));
|
||||
$('mNet').textContent = F.rp(r.cashback.net);
|
||||
$('mEquiv').textContent = ok ? F.pct(r.cashback.equiv) + ' p.a.' : 'tanpa cashback';
|
||||
|
||||
// tape: head + totals
|
||||
$('tDate').textContent = new Intl.DateTimeFormat('id-ID', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date());
|
||||
$('tNominal').textContent = F.rp(r.input.nominal);
|
||||
$('tTenor').textContent = tenorText(t);
|
||||
$('tTenor2').textContent = tenorText(t);
|
||||
$('tCust').textContent = CUST_LABEL[r.input.customerType];
|
||||
$('tGross').textContent = F.rp(r.cashback.gross);
|
||||
$('tTaxRate').textContent = F.pct(r.cashback.taxRate).replace(',00', '');
|
||||
$('tTax').textContent = '−' + F.rp(r.cashback.tax);
|
||||
$('tNet').textContent = F.rp(r.cashback.net);
|
||||
$('tEquiv').textContent = F.pct(r.cashback.equiv);
|
||||
|
||||
renderComparison(r);
|
||||
if ($('calcLines')) renderCalcLines(r); // only present for a logged-in admin
|
||||
renderPenalty(r);
|
||||
}
|
||||
|
||||
function renderComparison(r) {
|
||||
var rows = r.comparison.map(function (c) { return { c: c, dep: false }; });
|
||||
if (SHOW_DEPOSITO) rows.push({ dep: true, c: { key: 'deposito', name: 'Deposito', rate: r.deposito.rate, bungaNet: r.deposito.net, cashbackNet: 0, total: r.deposito.net, totalRate: r.deposito.rate } });
|
||||
var max = Math.max.apply(null, rows.map(function (x) { return x.c.total; })) || 1;
|
||||
var bestKey = rows.reduce(function (a, b) { return b.c.total > a.c.total ? b : a; }).c.key;
|
||||
|
||||
var html = rows.map(function (x) {
|
||||
var c = x.c;
|
||||
var best = c.key === bestKey && c.total > 0;
|
||||
var cls = 'cmp-row' + (x.dep ? ' is-dep' : '') + (best ? ' is-best' : '');
|
||||
var tag = x.dep ? '<span class="cmp-tag">Pembanding</span>' : (best ? '<span class="cmp-tag">Tertinggi</span>' : '');
|
||||
var wBunga = Math.max(0, c.bungaNet) / max * 100;
|
||||
var wCb = Math.max(0, c.cashbackNet) / max * 100;
|
||||
var diff = '';
|
||||
if (SHOW_DEPOSITO && !x.dep) {
|
||||
var d = c.total - r.deposito.net;
|
||||
diff = '<p class="cmp-diff ' + (d >= 0 ? 'up' : 'down') + '">' + (d >= 0 ? '+' : '−') + F.rp(Math.abs(d)).replace('−', '') +
|
||||
(d >= 0 ? ' lebih besar' : ' lebih kecil') + ' dari deposito</p>';
|
||||
}
|
||||
return '<li class="' + cls + '">' +
|
||||
'<div class="cmp-top"><span class="cmp-name">' + esc(c.name) + tag + '</span><span class="cmp-total">' + F.rp(c.total) + '</span></div>' +
|
||||
'<div class="cmp-bar" role="img" aria-label="Bunga neto ' + F.rp(c.bungaNet) + (x.dep ? '' : ', cashback neto ' + F.rp(c.cashbackNet)) + '">' +
|
||||
'<i data-w="' + wBunga.toFixed(2) + '"></i>' + (x.dep ? '' : '<i data-w="' + wCb.toFixed(2) + '"></i>') + '</div>' +
|
||||
'<dl class="cmp-meta">' +
|
||||
'<div><dt>Rate</dt><dd>' + F.pct(c.rate) + '</dd></div>' +
|
||||
'<div><dt>Bunga neto</dt><dd>' + F.rp(c.bungaNet) + '</dd></div>' +
|
||||
(x.dep ? '<div><dt>Cashback</dt><dd>—</dd></div>' : '<div><dt>Cashback</dt><dd>' + F.rp(c.cashbackNet) + '</dd></div>') +
|
||||
'<div><dt>Total rate</dt><dd>' + F.pct(c.totalRate) + '</dd></div>' +
|
||||
'</dl>' + diff + '</li>';
|
||||
}).join('');
|
||||
$('cmp').innerHTML = html;
|
||||
// widths set through the CSSOM: allowed by the Content-Security-Policy (inline style attributes are not)
|
||||
$('cmp').querySelectorAll('.cmp-bar i[data-w]').forEach(function (i) { i.style.width = i.getAttribute('data-w') + '%'; });
|
||||
if (!document.getElementById('cmpLegend')) {
|
||||
$('cmp').insertAdjacentHTML('beforebegin', '<p class="legend" id="cmpLegend"><span><i></i>Bunga neto</span><span><i class="cb"></i>Cashback neto</span></p>');
|
||||
}
|
||||
}
|
||||
|
||||
function renderCalcLines(r) {
|
||||
var s = full.settings, f = r.fund, t = r.input.tenor;
|
||||
var html = '<div class="calc-block"><h4>Dana kelolaan</h4><dl class="lines">' +
|
||||
line('Nominal penempatan', F.rp(r.input.nominal)) +
|
||||
line('GWM ' + F.pct(s.gwm), '−' + F.rp(f.gwm), 'is-neg') +
|
||||
line('Kas ' + F.pct(s.kas), '−' + F.rp(f.kas), 'is-neg') +
|
||||
line('Dana kelolaan', F.rp(f.managed), 'is-strong') + '</dl></div>';
|
||||
|
||||
html += '<div class="calc-block"><h4>Pendapatan bank selama ' + t + ' bulan</h4><dl class="lines">' +
|
||||
line('PUAB<span class="formula">' + F.pct(s.porsi_puab).replace(',00', '') + ' dana × ' + F.pct(s.puab) + ' × ' + s.hari_sebulan + '/' + s.hari_setahun + ' × ' + t + '</span>', F.rp(f.puabIncome)) +
|
||||
line('Kredit<span class="formula">' + F.pct(s.porsi_kredit).replace(',00', '') + ' dana × ' + F.pct(s.kredit) + ' × ' + t + '/12</span>', F.rp(f.creditIncome)) +
|
||||
line('Total pendapatan', F.rp(f.income), 'is-strong') + '</dl></div>';
|
||||
|
||||
PRODUCT_KEYS.forEach(function (key) {
|
||||
var pr = r.products[key];
|
||||
html += '<div class="calc-block"><h4>Spread ' + esc(pr.name) + ' (' + F.pct(pr.rate) + ')</h4><dl class="lines">' +
|
||||
line('Bunga kotor produk', '−' + F.rp(pr.bungaGross), 'is-neg') +
|
||||
(pr.program ? line('Biaya program', '−' + F.rp(pr.program), 'is-neg') : '') +
|
||||
line('Overhead cost ' + F.pct(s.ohc) + ' × PUAB', '−' + F.rp(pr.ohc), 'is-neg') +
|
||||
line('Premi LPS ' + F.pct(s.premi_lps), '−' + F.rp(pr.lps), 'is-neg') +
|
||||
line('Spread', F.rp(pr.spread), 'is-strong') +
|
||||
line('Porsi nasabah ' + F.pct(r.share).replace(',00', ''), F.rp(pr.cashbackGross)) + '</dl></div>';
|
||||
});
|
||||
|
||||
html += '<div class="calc-block"><h4>Cashback</h4><dl class="lines">' +
|
||||
line('Rata-rata porsi nasabah 3 produk', F.rp(r.cashback.gross)) +
|
||||
line('Pajak cashback ' + F.pct(r.cashback.taxRate), '−' + F.rp(r.cashback.tax), 'is-neg') +
|
||||
line('Cashback neto', F.rp(r.cashback.net), 'is-strong') +
|
||||
line('Cashback per bulan', F.rp(r.cashback.monthly)) +
|
||||
line('Porsi bank', F.pct(r.shareBank).replace(',00', '')) + '</dl></div>';
|
||||
|
||||
html += '<div class="calc-block"><h4>Deposito pembanding</h4><dl class="lines">' +
|
||||
line('Rate deposito ' + t + ' bulan', F.pct(r.deposito.rate)) +
|
||||
line('Bunga kotor', F.rp(r.deposito.gross)) +
|
||||
line('Pajak ' + F.pct(s.pajak_deposito), '−' + F.rp(r.deposito.tax), 'is-neg') +
|
||||
line('Bunga neto', F.rp(r.deposito.net), 'is-strong') +
|
||||
line('Bunga neto per bulan', F.rp(r.deposito.monthly)) + '</dl></div>';
|
||||
$('calcLines').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderPenalty(r) {
|
||||
var p = r.penalty;
|
||||
var pinalti = typeof p.rate === 'number' ? p.rate : model.pinalti;
|
||||
$('penaltyLines').innerHTML = r.eligible ?
|
||||
line('Pengembalian cashback neto', F.rp(p.refund)) +
|
||||
line('Pajak cashback', F.rp(p.tax)) +
|
||||
line('Pinalti ' + F.pct(pinalti).replace(',00', '') + ' dari cashback neto', F.rp(p.fee)) +
|
||||
line('Total kewajiban nasabah', F.rp(p.total), 'is-strong') :
|
||||
line('Tidak ada cashback, tidak ada pinalti', 'Rp0');
|
||||
}
|
||||
|
||||
function feed() {
|
||||
clearTimeout(feedTimer);
|
||||
feedTimer = setTimeout(function () {
|
||||
tape.classList.remove('is-feeding');
|
||||
void tape.offsetWidth; // restart animation
|
||||
tape.classList.add('is-feeding');
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function syncUrl(state) {
|
||||
try {
|
||||
var u = new URL(location.href);
|
||||
u.searchParams.set('n', state.nominal || '');
|
||||
u.searchParams.set('t', state.tenor);
|
||||
u.searchParams.set('c', state.cust);
|
||||
history.replaceState(null, '', u);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function syncChips(n) {
|
||||
form.querySelectorAll('.chip').forEach(function (b) { b.setAttribute('aria-pressed', String(Number(b.dataset.amount) === n)); });
|
||||
}
|
||||
|
||||
function stateKey(st) { return st.nominal + '|' + st.tenor + '|' + st.cust; }
|
||||
|
||||
function localResult(st) {
|
||||
var input = { nominal: st.nominal, tenor: st.tenor, customerType: st.cust };
|
||||
return full ? window.SerubiEngine.calculate(full, input) : PE.calculate(model, input);
|
||||
}
|
||||
|
||||
function setVerify(ok, when) {
|
||||
var el = $('tVerify');
|
||||
if (ok === null) { el.hidden = true; el.textContent = ''; return; }
|
||||
el.hidden = false;
|
||||
el.textContent = ok
|
||||
? 'Angka terverifikasi server Bank Papua, ' + new Intl.DateTimeFormat('id-ID', { dateStyle: 'medium', timeStyle: 'short' }).format(when) + '.'
|
||||
: 'Simulasi lokal (tanpa koneksi): angka belum terverifikasi server.';
|
||||
}
|
||||
|
||||
function update(animate) {
|
||||
var state = readState();
|
||||
fitText(nominalEl, 15);
|
||||
syncChips(state.nominal);
|
||||
if (blocked || !validate(state)) { lastResult = null; renderEmpty(); return; }
|
||||
lastResult = localResult(state);
|
||||
verifiedKey = null;
|
||||
setVerify(null);
|
||||
render(lastResult);
|
||||
syncUrl(state);
|
||||
if (animate) feed();
|
||||
}
|
||||
|
||||
/*
|
||||
* Official numbers for printing/copying: asks calc.php (server engine, live
|
||||
* parameters). Without a connection the local simulation is used and marked
|
||||
* as not verified. Calls done(verified) in both cases.
|
||||
*/
|
||||
function verifyThen(done) {
|
||||
var state = readState();
|
||||
if (blocked || !lastResult || !validate(state)) { toast('Isi nominal terlebih dahulu.'); return; }
|
||||
if (verifiedKey === stateKey(state)) { done(true); return; }
|
||||
var url = meta.calcUrl + '?n=' + encodeURIComponent(state.nominal) + '&t=' + encodeURIComponent(state.tenor) + '&c=' + encodeURIComponent(state.cust);
|
||||
var timer;
|
||||
var timeout = new Promise(function (_, reject) { timer = setTimeout(function () { reject(new Error('timeout')); }, VERIFY_TIMEOUT_MS); });
|
||||
toast('Memverifikasi ke server…');
|
||||
Promise.race([
|
||||
fetch(url, { credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' } }).then(function (res) {
|
||||
return res.json().then(function (body) { if (!res.ok) throw new Error(body.error || 'HTTP ' + res.status); return body; });
|
||||
}),
|
||||
timeout
|
||||
]).then(function (r) {
|
||||
clearTimeout(timer);
|
||||
if (stateKey(readState()) !== stateKey(state)) return; // inputs changed meanwhile
|
||||
lastResult = r;
|
||||
verifiedKey = stateKey(state);
|
||||
render(r);
|
||||
setVerify(true, new Date());
|
||||
toast('');
|
||||
done(true);
|
||||
}, function () {
|
||||
clearTimeout(timer);
|
||||
setVerify(false);
|
||||
toast('');
|
||||
done(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Age of the model: an old one means this page came from the offline copy.
|
||||
function checkModelAge() {
|
||||
var made = Date.parse(model.generatedAt);
|
||||
if (isNaN(made)) return;
|
||||
var hours = (Date.now() - made) / 3600000;
|
||||
var note = $('modelNote');
|
||||
var at = Date.parse(String(model.paramsAt || '').replace(' ', 'T'));
|
||||
var per = isNaN(at) ? '' : new Intl.DateTimeFormat('id-ID', { dateStyle: 'medium', timeStyle: 'short' }).format(at);
|
||||
if (hours > MAX_MODEL_AGE_DAYS * 24) {
|
||||
blocked = true;
|
||||
note.textContent = 'Parameter tersimpan di perangkat ini sudah lebih dari ' + MAX_MODEL_AGE_DAYS + ' hari. Sambungkan ke internet lalu muat ulang halaman.';
|
||||
note.hidden = false;
|
||||
} else if (hours > STALE_HOURS || navigator.onLine === false) {
|
||||
note.textContent = 'Mode offline: memakai parameter yang tersimpan di perangkat' + (per ? ' (berlaku per ' + per + ')' : '') + '. Cetak struk dan salin ringkasan akan diverifikasi ke server saat online.';
|
||||
note.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setState(s) {
|
||||
nominalEl.value = F.num(s.nominal);
|
||||
setRadio('tenor', s.tenor);
|
||||
setRadio('cust', s.cust);
|
||||
}
|
||||
|
||||
function summaryText(r, verified) {
|
||||
var lines = [
|
||||
'Simulasi Cashback SERUBI – Bank Papua',
|
||||
'Nominal ' + F.rp(r.input.nominal) + ' · ' + tenorText(r.input.tenor) + ' · ' + CUST_LABEL[r.input.customerType],
|
||||
r.eligible ? 'Cashback neto: ' + F.rp(r.cashback.net) + ' (setara ' + F.pct(r.cashback.equiv) + ' per tahun)' : 'Tanpa cashback: ' + ineligibleText(r),
|
||||
'',
|
||||
'Total diterima selama ' + tenorText(r.input.tenor) + ':'
|
||||
];
|
||||
r.comparison.forEach(function (c) { lines.push('• ' + c.name + ': ' + F.rp(c.total) + ' (' + F.pct(c.totalRate) + ' p.a.)'); });
|
||||
if (SHOW_DEPOSITO) lines.push('• Deposito: ' + F.rp(r.deposito.net) + ' (' + F.pct(r.deposito.rate) + ' p.a.)');
|
||||
if (!verified) lines.push('', '(Simulasi lokal tanpa koneksi, belum terverifikasi server.)');
|
||||
lines.push('', '*Hasil simulasi (estimasi), bukan dokumen resmi atau penawaran yang mengikat; dapat berubah sesuai ketentuan Bank Papua.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function toast(msg) {
|
||||
var el = $('toast');
|
||||
el.textContent = msg;
|
||||
clearTimeout(toast.t);
|
||||
toast.t = setTimeout(function () { el.textContent = ''; }, 2600);
|
||||
}
|
||||
|
||||
// ---- events ----
|
||||
nominalEl.addEventListener('input', function () { F.liveFormatIdr(nominalEl); update(true); });
|
||||
var resizeTimer = null;
|
||||
window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(fitAll, 120); });
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(fitAll);
|
||||
nominalEl.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); nominalEl.blur(); display.focus({ preventScroll: true }); } });
|
||||
form.addEventListener('change', function (ev) { if (ev.target.name === 'tenor' || ev.target.name === 'cust') update(true); });
|
||||
form.addEventListener('submit', function (ev) { ev.preventDefault(); });
|
||||
form.querySelectorAll('.chip').forEach(function (b) {
|
||||
b.addEventListener('click', function () { nominalEl.value = F.num(Number(b.dataset.amount)); update(true); });
|
||||
});
|
||||
$('btnReset').addEventListener('click', function () { setState(DEFAULTS); update(true); nominalEl.focus(); });
|
||||
$('btnPrint').addEventListener('click', function () {
|
||||
verifyThen(function () {
|
||||
var d = document.querySelectorAll('.tape-details');
|
||||
var was = Array.prototype.map.call(d, function (x) { return x.open; });
|
||||
d.forEach(function (x) { x.open = true; });
|
||||
window.print();
|
||||
d.forEach(function (x, i) { x.open = was[i]; });
|
||||
});
|
||||
});
|
||||
$('btnCopy').addEventListener('click', function () {
|
||||
verifyThen(function (verified) {
|
||||
var text = summaryText(lastResult, verified);
|
||||
var done = function () { toast('Ringkasan disalin. Tempel di WhatsApp atau email.'); };
|
||||
// the browser may refuse a late clipboard write; a second tap copies at once (already verified)
|
||||
var failed = function () { toast('Belum tersalin. Tekan "Salin ringkasan" sekali lagi.'); };
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text) ? done() : failed(); });
|
||||
} else if (fallbackCopy(text)) done(); else failed();
|
||||
});
|
||||
});
|
||||
function fallbackCopy(text) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
var ok = false;
|
||||
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
||||
document.body.removeChild(ta);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// mini display on phones once the main display is out of view
|
||||
if ('IntersectionObserver' in window) {
|
||||
var mini = $('miniDisplay');
|
||||
new IntersectionObserver(function (entries) {
|
||||
var hidden = !entries[0].isIntersecting && entries[0].boundingClientRect.top < 0;
|
||||
mini.classList.toggle('is-shown', hidden && !!lastResult);
|
||||
}).observe(display);
|
||||
}
|
||||
|
||||
// details open by default on wide screens
|
||||
if ($('detailCalc') && window.matchMedia('(min-width: 900px)').matches) $('detailCalc').open = true;
|
||||
|
||||
// initial state: deep link ?n=&t=&c=
|
||||
var q = new URLSearchParams(location.search);
|
||||
var init = {
|
||||
nominal: F.parseIdr(q.get('n')) || DEFAULTS.nominal,
|
||||
tenor: model.tenors.indexOf(Number(q.get('t'))) >= 0 ? Number(q.get('t')) : DEFAULTS.tenor,
|
||||
cust: q.get('c') === 'badan' ? 'badan' : DEFAULTS.cust
|
||||
};
|
||||
$('minNominal').textContent = F.rp(model.minNominal);
|
||||
checkModelAge();
|
||||
setState(init);
|
||||
update(false);
|
||||
fitAll();
|
||||
|
||||
// Offline copy of the page and its model (sw.js): the calculator keeps working without signal.
|
||||
if ('serviceWorker' in navigator && meta.swUrl && window.isSecureContext) {
|
||||
window.addEventListener('load', function () { navigator.serviceWorker.register(meta.swUrl).catch(function () { /* optional */ }); });
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user