Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,241 @@
|
||||
// Generated, do not edit. sha256:7498a98f290e59aca597eb4a57338d467525bf6c2933429d69f5e8474930a0ac
|
||||
import { aiProposalsStatus, acceptAiProposal, acceptAiProposals, rejectAiProposal, rejectAiProposals }
|
||||
from '../data/ai-proposals.js';
|
||||
import { CONFIG, PREFERENCES } from '../../shared/config.js';
|
||||
import { appSetting, timerSetting, loadAppSettings } from '../app-settings.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { assetLabelHtml } from '../ui/asset-label.js';
|
||||
import * as dialog from '../ui/dialog.js';
|
||||
import { esc } from '../ui/escape.js';
|
||||
import { fmtBase, fmtBasePrice, fmtUnit, priceFromBase, val, currencyName } from '../ui/format.js';
|
||||
import { taskCardHtml, nothingPendingHtml } from '../ui/task-card.js';
|
||||
import { priceUnitLabel } from '../../shared/units.js';
|
||||
import { localDateNl } from '../../shared/time.js';
|
||||
|
||||
const ONOMKEERBAAR = new Set(CONFIG.AI.DESTRUCTIVE_PROPOSAL_KINDS);
|
||||
|
||||
const assetVan = p => ({ asset_name: p.asset_name, asset_symbol: p.asset_symbol });
|
||||
|
||||
function detailTekst(detail) {
|
||||
if (typeof detail !== 'string' || !detail.startsWith('{')) return detail;
|
||||
try {
|
||||
const o = JSON.parse(detail);
|
||||
return o && o.key ? t(o.key, o.params) : detail;
|
||||
} catch { return detail; }
|
||||
}
|
||||
|
||||
const meta = p => ({
|
||||
kind: p.asset_kind, decimals: p.asset_decimals, unit: p.asset_unit,
|
||||
subunit: p.asset_subunit, displayUnit: p.asset_display_unit,
|
||||
});
|
||||
|
||||
const eenheidIsSymbool = p => {
|
||||
const asset = meta(p);
|
||||
const getoond = asset.displayUnit === 'subunit' && asset.subunit ? asset.subunit : asset.unit;
|
||||
return String(getoond || '').toUpperCase() === String(p.asset_symbol || '').toUpperCase();
|
||||
};
|
||||
|
||||
const koers = p => {
|
||||
const asset = meta(p);
|
||||
return `${fmtBasePrice(priceFromBase(p.fiatPrice, asset))} ${t('aiProposals.perUnit')} ${priceUnitLabel(asset)}`;
|
||||
};
|
||||
|
||||
const leesbareWaarde = (p, json) => {
|
||||
try { return toon(p, JSON.parse(json)); }
|
||||
catch { return String(json); }
|
||||
};
|
||||
|
||||
const toon = (p, v) => {
|
||||
const spec = PREFERENCES[p.scope]?.[p.key];
|
||||
if (spec?.type === 'bool') return t(Number(v) ? 'aiProposals.preferenceOn' : 'aiProposals.preferenceOff');
|
||||
return String(v);
|
||||
};
|
||||
|
||||
const huidigeVoorkeur = (p) => {
|
||||
try {
|
||||
const v = p.scope === 'timers' ? timerSetting(p.key) : appSetting(p.key);
|
||||
return v == null ? null : toon(p, v);
|
||||
} catch { return null; }
|
||||
};
|
||||
|
||||
const OMSCHRIJVING = {
|
||||
price_point: p =>
|
||||
`${t('aiProposals.kindPricePoint')}: ${assetLabelHtml(assetVan(p))} · ${val(koers(p))} `
|
||||
+ `<span class="t-muted-cell">${esc(localDateNl(p.observedAt))}</span>`,
|
||||
|
||||
account: p =>
|
||||
`${t('aiProposals.kindAccount')}: <strong>${esc(p.name)}</strong> `
|
||||
+ `<span class="t-muted-cell">${esc(t('enums.account_kind.' + p.accountKind))} · `
|
||||
+ `${esc(p.isOwnCustody ? t('aiProposals.ownCustody') : t('aiProposals.thirdParty'))}</span>`,
|
||||
|
||||
account_delete: p =>
|
||||
`${t('aiProposals.kindAccountDelete')}: <strong>${esc(p.account_name)}</strong> `
|
||||
+ `<span class="t-muted-cell">${esc(t('enums.account_kind.' + p.account_kind))}</span>`,
|
||||
|
||||
portfolio: p =>
|
||||
`${t('aiProposals.kindPortfolio')}: <strong>${esc(p.name)}</strong> `
|
||||
+ `<span class="t-muted-cell">${esc(t('aiProposals.portfolioIn'))} ${esc(currencyName(p.currency))} (${esc(p.currency)})</span><br>`
|
||||
+ `<span class="t-muted-cell">${esc(t('aiProposals.portfolioNote'))}</span>`,
|
||||
|
||||
preference: p => {
|
||||
const nu = huidigeVoorkeur(p);
|
||||
return `${t('aiProposals.kindPreference')}: <strong>${esc(p.scope)}.${esc(p.key)}</strong> `
|
||||
+ `→ ${val(leesbareWaarde(p, p.value))}`
|
||||
+ (nu === null ? '' : ` <span class="t-muted-cell">(${esc(t('aiProposals.preferenceCurrent'))} ${esc(nu)})</span>`)
|
||||
+ `<br><span class="t-muted-cell">${esc(t('aiProposals.preferenceNote'))}</span>`;
|
||||
},
|
||||
|
||||
source_choice: p =>
|
||||
`${t('aiProposals.kindSourceChoice')}: ${assetLabelHtml(assetVan(p))} · `
|
||||
+ `${esc(t('integrations.sourceCap.' + p.scope))} · <strong>${esc(p.provider_name || p.key)}</strong> `
|
||||
+ `${esc(t(p.value === 'first' ? 'aiProposals.sourceFirst' : 'aiProposals.sourceRemove'))}<br>`
|
||||
+ `<span class="t-muted-cell">${esc(t('aiProposals.sourceChoiceNote'))}</span>`,
|
||||
|
||||
valuation: p => {
|
||||
const asset = meta(p);
|
||||
const grootboek = p.ledger_balance == null ? null : Number(p.ledger_balance);
|
||||
const verschil = grootboek == null ? null : Number(p.balance) - grootboek;
|
||||
const naast = grootboek == null ? '' :
|
||||
` · ${t('aiProposals.ledger')} ${val(fmtUnit(grootboek, asset))}`
|
||||
+ (verschil === 0 ? '' : ` · ${t('aiProposals.diff')} ${verschil > 0 ? '+' : '−'}${val(fmtUnit(Math.abs(verschil), asset))}`);
|
||||
return `${t('aiProposals.kindValuation')}: ${val(fmtUnit(p.balance, asset))} `
|
||||
+ `${assetLabelHtml(assetVan(p), { withoutSymbol: eenheidIsSymbool(p) })} `
|
||||
+ `· <strong>${esc(p.account_name || '')}</strong>${naast}<br>`
|
||||
+ `<span class="t-muted-cell">${esc(localDateNl(p.observedAt))}</span>`;
|
||||
},
|
||||
|
||||
event: p => {
|
||||
const waar = [p.from_name, p.to_name].filter(Boolean).map(esc).join(' → ');
|
||||
const prijs = p.fiatPrice == null ? '' : ` · ${val(koers(p))}`;
|
||||
const bedragen = [
|
||||
p.fiatFee == null ? null : val(fmtBase(p.fiatFee)),
|
||||
p.unitFee == null ? null : val(fmtUnit(p.unitFee, meta(p))),
|
||||
].filter(Boolean);
|
||||
const kosten = bedragen.length ? ` · ${t('aiProposals.fee')} ${bedragen.join(' + ')}` : '';
|
||||
return `${esc(t('enums.event_type.' + p.eventType))}: ${val(fmtUnit(p.amount, meta(p)))} `
|
||||
+ `${assetLabelHtml(assetVan(p), { withoutSymbol: eenheidIsSymbool(p) })}${prijs}${kosten}<br>`
|
||||
+ `<span class="t-muted-cell">${waar} · ${esc(localDateNl(p.observedAt))}</span>`;
|
||||
},
|
||||
};
|
||||
|
||||
function regel(p) {
|
||||
const maak = OMSCHRIJVING[p.kind];
|
||||
const tekst = maak ? maak(p) : esc(t('aiProposals.unknownKind') + ` (${p.kind})`);
|
||||
const notitie = p.note ? `<div class="t-body-sm t-muted">${esc(p.note)}</div>` : '';
|
||||
const mislukt = p.detail ? `<div class="t-body-sm t-neg">${esc(t('aiProposals.failed'))}: ${esc(detailTekst(p.detail))}</div>` : '';
|
||||
const weg = ONOMKEERBAAR.has(p.kind);
|
||||
const waarschuwing = weg
|
||||
? `<div class="t-body-sm t-neg">${esc(t('aiProposals.irreversible'))}</div>` : '';
|
||||
const opening = p.kind === 'valuation' && p.events_before === 0
|
||||
? `<div class="t-body-sm t-neg">${esc(t('aiProposals.openingBalance'))}</div>` : '';
|
||||
return `<div class="rec-item" data-aiprop="${p.id}" data-aidanger="${weg ? 1 : 0}" data-aikind="${esc(p.kind)}">
|
||||
<div class="rec-item-row">
|
||||
<div class="rec-item-meta">${tekst}${notitie}${waarschuwing}${opening}${mislukt}</div>
|
||||
<div class="row-actions rec-hover">
|
||||
<button class="btn ${weg ? 'btn-danger' : 'btn-primary'} btn-xs" data-aiact="accept">${weg ? t('aiProposals.acceptDelete') : t('aiProposals.accept')}</button>
|
||||
<button class="btn btn-ghost btn-xs" data-aiact="reject">${t('aiProposals.reject')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function redenTekst(u) {
|
||||
if (!u.key) return u.reden;
|
||||
try { return t(u.key, u.params); } catch { return u.reden; }
|
||||
}
|
||||
|
||||
async function meldMislukt(uitkomsten) {
|
||||
const stuk = uitkomsten.filter(u => !u.ok);
|
||||
if (!stuk.length) return;
|
||||
await dialog.alert({
|
||||
title: t('aiProposals.partialTitle'),
|
||||
message: `${t('aiProposals.partialBody')}\n\n${stuk.map(u => `- ${redenTekst(u)}`).join('\n')}`,
|
||||
});
|
||||
}
|
||||
|
||||
let live = null;
|
||||
let liveGebonden = false;
|
||||
|
||||
function bindLive() {
|
||||
if (liveGebonden) return;
|
||||
liveGebonden = true;
|
||||
document.addEventListener('ai-proposals-changed', async () => {
|
||||
if (!live || !live.host || !live.host.isConnected) { live = null; return; }
|
||||
try { await renderAiProposals(live.host, live.refresh); } catch { }
|
||||
});
|
||||
}
|
||||
|
||||
export async function renderAiProposals(host, refresh) {
|
||||
live = { host, refresh };
|
||||
bindLive();
|
||||
const { enabled, pending } = await aiProposalsStatus();
|
||||
|
||||
if (!enabled) { host.innerHTML = ''; return null; }
|
||||
|
||||
if (!pending.length) {
|
||||
host.innerHTML = taskCardHtml({ title: t('aiProposals.title'), body: nothingPendingHtml() });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const teVerwijderen = pending.filter(p => ONOMKEERBAAR.has(p.kind)).length;
|
||||
const bulkUitleg = teVerwijderen
|
||||
? `<div class="t-body-sm t-neg" style="margin-bottom:0.6rem">${esc(t('aiProposals.bulkSkips').replace('{n}', teVerwijderen))}</div>`
|
||||
: '';
|
||||
|
||||
host.innerHTML = taskCardHtml({
|
||||
title: t('aiProposals.title'),
|
||||
actions: `<div class="row-actions">
|
||||
<button class="btn btn-secondary btn-xs" id="aiprop-accept-all">${t('aiProposals.acceptAll')}</button>
|
||||
<button class="btn btn-ghost btn-xs" id="aiprop-reject-all">${t('aiProposals.rejectAll')}</button>
|
||||
</div>`,
|
||||
body: `<div class="t-body-sm t-muted" style="margin-bottom:0.6rem">${t('aiProposals.intro')}</div>
|
||||
${bulkUitleg}
|
||||
${pending.map(regel).join('')}`,
|
||||
});
|
||||
|
||||
const ids = pending.filter(p => !ONOMKEERBAAR.has(p.kind)).map(p => p.id);
|
||||
const alleIds = pending.map(p => p.id);
|
||||
|
||||
host.querySelector('#aiprop-accept-all').onclick = async () => {
|
||||
if (!ids.length) {
|
||||
await dialog.alert({ title: t('aiProposals.title'), message: t('aiProposals.bulkNothing') });
|
||||
return;
|
||||
}
|
||||
if (!(await dialog.confirm({ message: t('aiProposals.acceptAllConfirm').replace('{n}', ids.length) }))) return;
|
||||
await meldMislukt(await acceptAiProposals(ids));
|
||||
if (pending.some(p => ids.includes(p.id) && p.kind === 'preference')) {
|
||||
await loadAppSettings();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
|
||||
host.querySelector('#aiprop-reject-all').onclick = async () => {
|
||||
if (!(await dialog.confirm({ message: t('aiProposals.rejectAllConfirm').replace('{n}', alleIds.length), danger: true }))) return;
|
||||
await meldMislukt(await rejectAiProposals(alleIds, t('aiProposals.rejectedByUser')));
|
||||
refresh();
|
||||
};
|
||||
|
||||
host.querySelectorAll('[data-aiprop]').forEach(rij => {
|
||||
const id = Number(rij.dataset.aiprop);
|
||||
rij.querySelectorAll('[data-aiact]').forEach(knop => knop.onclick = async () => {
|
||||
try {
|
||||
if (knop.dataset.aiact === 'accept') {
|
||||
if (rij.dataset.aidanger === '1'
|
||||
&& !(await dialog.confirm({ message: t('aiProposals.acceptDeleteConfirm'), danger: true }))) return;
|
||||
await acceptAiProposal(id);
|
||||
if (rij.dataset.aikind === 'preference') {
|
||||
await loadAppSettings();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
} else await rejectAiProposal(id, t('aiProposals.rejectedByUser'));
|
||||
} catch (e) {
|
||||
await dialog.alert({ title: t('dialog.error'), message: e.message });
|
||||
}
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
return pending.length;
|
||||
}
|
||||
Reference in new issue
Block a user