2026-10-29 15:24:06 +01:00
|
|
|
// Generated, do not edit. sha256:26e9a2b26a7c0fa9e0b6979080609ba55baa351ce8440240a006def072475893
|
2026-10-29 11:57:29 +01:00
|
|
|
import { listAssets, listBookableAssets, createAsset, updateAsset, deleteAsset } from '../data/assets.js';
|
|
|
|
|
import { hasMetalAssets } from '../data/metals.js';
|
|
|
|
|
import { tableViewCfg } from '../data/preferences.js';
|
|
|
|
|
import { t } from '../i18n/index.js';
|
|
|
|
|
import * as dialog from '../ui/dialog.js';
|
|
|
|
|
import { esc } from '../ui/escape.js';
|
|
|
|
|
import { ENUMS, CONFIG, balanceCaptureOn } from '../../shared/config.js';
|
|
|
|
|
import { REIKWIJDTE, categorieVoorAsset } from '../tax/constants.js';
|
|
|
|
|
import { renderTable } from '../ui/table.js';
|
|
|
|
|
import { takeRowFocus } from '../ui/row-focus.js';
|
|
|
|
|
import * as metalProducts from './metal-products.js';
|
|
|
|
|
import { capacityCfg } from '../ui/lock.js';
|
|
|
|
|
import { baseCurrency } from '../app-settings.js';
|
|
|
|
|
|
|
|
|
|
let tableApi = null;
|
|
|
|
|
|
|
|
|
|
export async function render(el) {
|
|
|
|
|
el.innerHTML = `
|
|
|
|
|
<div id="tbl"></div>
|
|
|
|
|
<div id="mp-section"></div>`;
|
|
|
|
|
await buildTable(el);
|
|
|
|
|
if (await hasMetalAssets()) {
|
|
|
|
|
const sec = el.querySelector('#mp-section');
|
|
|
|
|
sec.innerHTML = `<div id="mp-tbl" class="mt-lg"></div>`;
|
|
|
|
|
metalProducts.mount(sec.querySelector('#mp-tbl'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function kindOpts() { return ENUMS.ASSET_KIND.map(v => ({ value: v, label: t('enums.asset_kind.' + v) })); }
|
|
|
|
|
function displayUnitOpts() { return ENUMS.ASSET_DISPLAY_UNIT.map(v => ({ value: v, label: t('enums.displayUnit.' + v) })); }
|
|
|
|
|
function taxCategorieOpts(vals = {}) {
|
|
|
|
|
const derived = categorieVoorAsset(vals);
|
|
|
|
|
return [{ value: '', label: derived
|
|
|
|
|
? t('assets.taxCategoryAutoIs', { waarde: t('enums.tax_category.' + derived) })
|
|
|
|
|
: t('assets.taxCategoryAuto') },
|
|
|
|
|
...ENUMS.TAX_CATEGORY.filter(v => v !== derived || v === vals.taxCategory)
|
|
|
|
|
.map(v => ({ value: v, label: t('enums.tax_category.' + v) }))];
|
|
|
|
|
}
|
|
|
|
|
function taxScopeOpts(vals = {}) {
|
|
|
|
|
return [{ value: '', label: t('assets.taxScopeAuto') },
|
|
|
|
|
...ENUMS.TAX_SCOPE.filter(v => v !== REIKWIJDTE.ALL || v === vals.taxScope)
|
|
|
|
|
.map(v => ({ value: v, label: t('enums.tax_scope.' + v) }))];
|
|
|
|
|
}
|
|
|
|
|
function balanceCaptureOpts() {
|
|
|
|
|
return [{ value: '', label: t('assets.taxCategoryAuto') },
|
|
|
|
|
{ value: '1', label: t('assets.balanceCaptureOn') }, { value: '0', label: t('assets.balanceCaptureOff') }];
|
|
|
|
|
}
|
|
|
|
|
const captureValue = v => (v === '' || v == null ? null : Number(v));
|
|
|
|
|
|
|
|
|
|
async function buildTable(el) {
|
|
|
|
|
const columns = [
|
|
|
|
|
{ key: 'name', label: t('assets.name'), sortable: true, text: true,
|
|
|
|
|
edit: { type: 'text', placeholder: 'Bitcoin' } },
|
|
|
|
|
{ key: 'symbol', label: t('assets.symbol'), sortable: true,
|
|
|
|
|
render: a => `<strong>${esc(a.symbol)}</strong>`,
|
|
|
|
|
edit: { type: 'text', placeholder: 'BTC' } },
|
|
|
|
|
{ key: 'kind', label: t('assets.kind'), sortable: true,
|
|
|
|
|
render: a => t('enums.asset_kind.' + a.kind),
|
|
|
|
|
group: { value: a => a.kind, label: v => t('enums.asset_kind.' + v) },
|
|
|
|
|
edit: { type: 'select', options: kindOpts, reactive: true } },
|
|
|
|
|
{ key: 'unit', label: t('assets.unit'), sortable: true,
|
|
|
|
|
render: a => `${esc(a.unit)}${a.subunit ? ` <span class="t-muted-cell">/ ${esc(a.subunit)}</span>` : ''}`,
|
|
|
|
|
edit: { type: 'text', placeholder: 'BTC / g / ozt' } },
|
|
|
|
|
{ key: 'subunit', label: t('assets.subunit'),
|
|
|
|
|
render: a => (a.subunit ? esc(a.subunit) : '-'),
|
|
|
|
|
edit: { type: 'text', placeholder: 'sat / mg' } },
|
|
|
|
|
{ key: 'displayUnit', label: t('assets.displayUnit'), sortable: true,
|
|
|
|
|
render: a => {
|
|
|
|
|
if (!a.subunit) return `<strong>${esc(a.unit)}</strong>`;
|
|
|
|
|
const subChosen = a.displayUnit === 'subunit';
|
|
|
|
|
const part = (txt, chosen) => chosen ? `<strong>${txt}</strong>` : `<span class="t-muted-cell">${txt}</span>`;
|
|
|
|
|
return `${part(esc(a.unit), !subChosen)} <span class="t-muted-cell">/</span> ${part(esc(a.subunit), subChosen)}`;
|
|
|
|
|
},
|
|
|
|
|
edit: { type: 'select', options: displayUnitOpts } },
|
|
|
|
|
{ key: 'decimals', label: t('assets.decimals'), align: 'num', sortable: true,
|
|
|
|
|
edit: { type: 'number', attrs: { min: 0, step: 1 } } },
|
|
|
|
|
{ key: 'taxCategory', label: t('assets.taxCategory'), sortable: true,
|
|
|
|
|
render: a => a.taxCategory
|
|
|
|
|
? t('enums.tax_category.' + a.taxCategory)
|
|
|
|
|
: `<span class="t-muted-cell">${categorieVoorAsset(a)
|
|
|
|
|
? t('assets.taxCategoryAutoIs', { waarde: t('enums.tax_category.' + categorieVoorAsset(a)) })
|
|
|
|
|
: t('assets.taxCategoryAuto')}</span>`,
|
|
|
|
|
group: { value: a => a.taxCategory || '', label: v => v ? t('enums.tax_category.' + v) : t('assets.taxCategoryAuto') },
|
|
|
|
|
edit: { type: 'select', options: taxCategorieOpts } },
|
|
|
|
|
{ key: 'taxScope', label: t('assets.taxScope'), sortable: true,
|
|
|
|
|
render: a => a.taxScope
|
|
|
|
|
? t('enums.tax_scope.' + a.taxScope)
|
|
|
|
|
: `<span class="t-muted-cell">${t('assets.taxScopeAuto')}</span>`,
|
|
|
|
|
group: { value: a => a.taxScope || '', label: v => v ? t('enums.tax_scope.' + v) : t('assets.taxScopeAuto') },
|
|
|
|
|
edit: { type: 'select', options: taxScopeOpts } },
|
|
|
|
|
{ key: 'balanceCapture', label: t('assets.balanceCapture'), sortable: true,
|
|
|
|
|
render: a => a.balanceCapture == null
|
|
|
|
|
? `<span class="t-muted-cell">${t(balanceCaptureOn(a) ? 'assets.balanceCaptureAutoOn' : 'assets.balanceCaptureAutoOff')}</span>`
|
|
|
|
|
: a.balanceCapture === 1
|
|
|
|
|
? `<span class="badge badge-accent">${t('assets.balanceCaptureOn')}</span>`
|
|
|
|
|
: `<span class="badge badge-neutral">${t('assets.balanceCaptureOff')}</span>`,
|
|
|
|
|
edit: { type: 'select', options: balanceCaptureOpts } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function toPayload(v) {
|
|
|
|
|
const symbol = String(v.symbol || '').trim();
|
|
|
|
|
const name = String(v.name || '').trim();
|
|
|
|
|
const unit = String(v.unit || '').trim();
|
|
|
|
|
if (!symbol || !name || !unit) throw new Error(t('assets.fillSymbolNameUnit'));
|
|
|
|
|
|
|
|
|
|
if (v.kind === 'metal') {
|
|
|
|
|
const M = CONFIG.METALS;
|
|
|
|
|
if (!M.UNITS[unit]) {
|
|
|
|
|
throw new Error(t('assets.metalUnitUnknown', { eenheden: Object.keys(M.UNITS).join(', ') }));
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
symbol, name, kind: 'metal',
|
|
|
|
|
unit, subunit: M.BASE_UNIT, decimals: M.BASE_DECIMALS,
|
|
|
|
|
displayUnit: v.displayUnit === 'subunit' ? 'subunit' : 'unit',
|
|
|
|
|
taxCategory: v.taxCategory || null,
|
|
|
|
|
taxScope: v.taxScope || null,
|
|
|
|
|
balanceCapture: captureValue(v.balanceCapture),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const decimals = parseInt(v.decimals, 10);
|
|
|
|
|
if (!Number.isInteger(decimals) || decimals < 0) throw new Error(t('assets.fillDecimals'));
|
|
|
|
|
const subunit = (v.subunit || '').trim() || null;
|
|
|
|
|
const displayUnit = (subunit && v.displayUnit === 'subunit') ? 'subunit' : 'unit';
|
|
|
|
|
return {
|
|
|
|
|
symbol, name, kind: v.kind,
|
|
|
|
|
unit, subunit, decimals, displayUnit,
|
|
|
|
|
taxCategory: v.taxCategory || null,
|
|
|
|
|
taxScope: v.taxScope || null,
|
|
|
|
|
balanceCapture: captureValue(v.balanceCapture),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-29 15:24:06 +01:00
|
|
|
const nieuw = new Set();
|
|
|
|
|
const zichtbaar = async () => {
|
|
|
|
|
const b = await listBookableAssets();
|
|
|
|
|
if (!nieuw.size) return b;
|
|
|
|
|
const ids = new Set(b.map(a => a.id));
|
|
|
|
|
return [...b, ...(await listAssets()).filter(a => nieuw.has(a.id) && !ids.has(a.id))];
|
|
|
|
|
};
|
|
|
|
|
const rijen = await zichtbaar();
|
2026-10-29 11:57:29 +01:00
|
|
|
const bookable = async () => (await listBookableAssets()).filter(r => r.symbol !== baseCurrency()).length;
|
|
|
|
|
let used = await bookable();
|
|
|
|
|
tableApi = renderTable(el.querySelector('#tbl'), {
|
|
|
|
|
columns, rows: rijen,
|
|
|
|
|
...capacityCfg('assets', () => used),
|
|
|
|
|
...(await tableViewCfg('assets', { key: 'symbol', dir: 'asc' })),
|
|
|
|
|
emptyText: t('assets.none'),
|
|
|
|
|
noneLabel: t('table.none'),
|
|
|
|
|
collapsibleGroups: true,
|
|
|
|
|
newLabel: t('table.new'),
|
|
|
|
|
newDefaults: { kind: 'crypto', decimals: '', displayUnit: 'unit', taxCategory: '', taxScope: '', balanceCapture: '' },
|
|
|
|
|
saveLabel: t('assets.save'), cancelLabel: t('assets.cancel'),
|
2026-10-29 15:24:06 +01:00
|
|
|
onCreate: async v => { const id = await createAsset(toPayload(v)); nieuw.add(id); used = await bookable(); tableApi.refresh(await zichtbaar(), { locate: id }); },
|
|
|
|
|
onSave: async (id, v) => { await updateAsset(id, toPayload(v)); tableApi.refresh(await zichtbaar()); },
|
2026-10-29 11:57:29 +01:00
|
|
|
onDelete: async id => {
|
|
|
|
|
if (!(await dialog.confirm({ message: t('assets.confirmDelete'), danger: true }))) return;
|
2026-10-29 15:24:06 +01:00
|
|
|
try { await deleteAsset(id); used = await bookable(); tableApi.refresh(await zichtbaar()); } catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
|
2026-10-29 11:57:29 +01:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
tableApi.focusRow(takeRowFocus('assets'));
|
|
|
|
|
}
|