111 lines
6.7 KiB
JavaScript
111 lines
6.7 KiB
JavaScript
// Generated, do not edit. sha256:6eef7930e5d81c104d37fffaf0d8f5c9109485ed27fb20ffaaecdf946a306931
|
|
import { scheduleForecast, dueCreditsForAccount, dueCreditsAll, bookDueCredit, dismissDueCredit } from '../data/interest-credits.js';
|
|
import { byPeriod, bookableDues, dueKey } from './interest-order.js';
|
|
import { listSchedulesForAccount } from '../data/interest-schedules.js';
|
|
import { t } from '../i18n/index.js';
|
|
import * as dialog from '../ui/dialog.js';
|
|
import { fmtUnit, val, pct } from '../ui/format.js';
|
|
import { taskCardHtml, nothingPendingHtml } from '../ui/task-card.js';
|
|
import { esc } from '../ui/escape.js';
|
|
import { CHEVRON } from '../ui/icons.js';
|
|
import { expanders } from '../ui/expand.js';
|
|
|
|
const dueMeta = d => ({ decimals: d.payout_decimals, unit: d.payout_unit, subunit: d.payout_subunit, displayUnit: d.payout_display_unit });
|
|
|
|
const dueKnoppen = (i, kan) =>
|
|
`<button class="btn btn-primary btn-sm" data-book="${i}"${kan ? '' : ` disabled title="${esc(t('accounts.bookOldestFirst'))}"`}>${t('accounts.book')}</button>`
|
|
+ `<button class="btn btn-ghost btn-sm" data-dismiss="${i}">${t('accounts.dismiss')}</button>`;
|
|
|
|
const dueUitleg = (due, boekbaar) => due.some(d => !boekbaar.has(dueKey(d)))
|
|
? `<div class="t-body-sm t-muted mt-sm">${t('accounts.bookOldestFirst')}</div>` : '';
|
|
|
|
function wireDue(scopeEl, due, refresh) {
|
|
scopeEl.querySelectorAll('[data-book]').forEach(b => b.onclick = async () => {
|
|
const d = due[Number(b.dataset.book)];
|
|
const amt = `${fmtUnit(d.projected_sub, dueMeta(d))}`;
|
|
if (!(await dialog.confirm({ message: `${t('accounts.confirmBook')} ${amt} (${d.periodStart} → ${d.periodEnd})?` }))) return;
|
|
try { await bookDueCredit(d, d.projected_sub); refresh(); }
|
|
catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
|
|
});
|
|
scopeEl.querySelectorAll('[data-dismiss]').forEach(b => b.onclick = async () => {
|
|
const d = due[Number(b.dataset.dismiss)];
|
|
try { await dismissDueCredit(d); refresh(); }
|
|
catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
|
|
});
|
|
}
|
|
|
|
const fig = (label, value, sub = '') => `<div class="ip-fig"><div class="t-label">${label}</div>`
|
|
+ `<div class="t-value ip-fig-value">${value}</div>${sub ? `<div class="t-body-sm t-muted">${sub}</div>` : ''}</div>`;
|
|
|
|
export async function renderScheduleStatus(statusHost, accountId, refresh) {
|
|
if (!statusHost) return false;
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const scheds = (await listSchedulesForAccount(accountId)).filter(s => s.rateKind === 'fixed' && (!s.endsOn || s.endsOn >= today));
|
|
const due = byPeriod(await dueCreditsForAccount(accountId));
|
|
const boekbaar = bookableDues(due);
|
|
|
|
const prognoses = await Promise.all(scheds.map(s => scheduleForecast(s)));
|
|
const expand = await expanders('interest', { upcoming: false });
|
|
const projHtml = scheds.map((s, i) => {
|
|
const f = prognoses[i];
|
|
const m = { decimals: s.payout_decimals, unit: s.payout_unit, subunit: s.payout_subunit, displayUnit: s.payout_display_unit };
|
|
const pm = { decimals: s.principal_decimals, unit: s.principal_unit, subunit: s.principal_subunit, displayUnit: s.principal_display_unit };
|
|
const asset = s.principal_symbol === s.payout_symbol ? esc(s.payout_symbol) : `${esc(s.principal_symbol)} → ${esc(s.payout_symbol)}`;
|
|
const naam = s.note ? esc(s.note) : asset;
|
|
const termen = [
|
|
...(s.note ? [asset] : []),
|
|
pct(s.annualRate),
|
|
s.principalAmount != null ? t('accounts.projOver', { bedrag: val(fmtUnit(s.principalAmount, pm)) }) : t('interest.principalFollowsBalance'),
|
|
t('enums.interest_cadence.' + s.creditCadence),
|
|
s.endsOn ? t('accounts.projUntil', { datum: esc(s.endsOn) }) : t('interest.endsOnOpen'),
|
|
].join(' · ');
|
|
const volgende = f.upcoming[0];
|
|
const cijfers = [
|
|
fig(t('accounts.projReceived'), val(fmtUnit(f.realizedSub, m))),
|
|
...(f.termTotalSub != null ? [
|
|
fig(t('accounts.projExpectRemain'), val(fmtUnit(f.remainingSub, m)), t('accounts.projCount', { n: f.remainingCount })),
|
|
fig(t('accounts.projTermTotal'), val(fmtUnit(f.termTotalSub, m))),
|
|
] : []),
|
|
...(f.termTotalSub != null && f.sameAsset ? [
|
|
fig(t('accounts.projEndBalance', { datum: esc(s.endsOn) }), val(fmtUnit(s.principalAmount + f.termTotalSub, m)), t('accounts.projEndBalanceSub')),
|
|
] : []),
|
|
...(volgende ? [fig(t('accounts.projNext'), val(fmtUnit(volgende.projectedSub, m)), esc(volgende.periodEnd))] : []),
|
|
].join('');
|
|
const komend = f.upcoming.length > 1
|
|
? `<details class="ip-upcoming" ${expand.attr('upcoming')}><summary class="t-body-sm">${t('accounts.projAllUpcoming', { n: f.upcoming.length })}`
|
|
+ `<span class="chev-btn" aria-hidden="true">${CHEVRON}</span></summary>`
|
|
+ `<div class="ip-upcoming-list">${f.upcoming.map(p => `<div class="ip-up"><span class="t-muted">${esc(p.periodEnd)}</span>`
|
|
+ `<span class="ip-num">${val(fmtUnit(p.projectedSub, m))}</span></div>`).join('')}</div></details>`
|
|
: '';
|
|
return `<div class="ip-sched"><div class="ip-sched-head"><span class="ip-sched-name">${naam}</span>`
|
|
+ `<span class="t-body-sm t-muted">${termen}</span></div><div class="ip-figs">${cijfers}</div>${komend}</div>`;
|
|
}).join('');
|
|
|
|
const dueHtml = due.length ? `<div class="ip-due"><div class="t-label">${t('accounts.dueTitle')}</div>`
|
|
+ due.map((d, i) => `<div class="ip-due-row">`
|
|
+ `<span class="t-body-sm">${d.periodStart} → ${d.periodEnd}</span><span class="t-body-sm ip-num">${val(fmtUnit(d.projected_sub, dueMeta(d)))}</span>`
|
|
+ `<span class="ip-due-btns">${dueKnoppen(i, boekbaar.has(dueKey(d)))}</span></div>`).join('')
|
|
+ dueUitleg(due, boekbaar) + '</div>' : '';
|
|
|
|
statusHost.innerHTML = projHtml + dueHtml;
|
|
expand.bind(statusHost);
|
|
statusHost.querySelectorAll('.ip-upcoming').forEach(d => d.addEventListener('toggle', () => {
|
|
document.querySelectorAll('.ip-upcoming').forEach(o => { if (o.open !== d.open) o.open = d.open; });
|
|
}));
|
|
wireDue(statusHost, due, refresh);
|
|
return Boolean(projHtml || dueHtml);
|
|
}
|
|
|
|
export async function renderInterestWidget(host, refresh) {
|
|
const due = byPeriod(await dueCreditsAll());
|
|
const boekbaar = bookableDues(due);
|
|
const body = due.length === 0
|
|
? nothingPendingHtml()
|
|
: due.map((d, i) => `<div class="flex gap-md" style="align-items:center;flex-wrap:wrap;margin-top:0.4rem">`
|
|
+ `<span class="t-body-sm">${esc(d.account_name)} · ${d.periodStart} → ${d.periodEnd}: ${val(fmtUnit(d.projected_sub, dueMeta(d)))}</span>`
|
|
+ `${dueKnoppen(i, boekbaar.has(dueKey(d)))}</div>`).join('') + dueUitleg(due, boekbaar);
|
|
host.innerHTML = taskCardHtml({ title: t('actions.interestTitle'), body });
|
|
wireDue(host, due, refresh);
|
|
return due.length;
|
|
}
|