125 lines
5.3 KiB
JavaScript
125 lines
5.3 KiB
JavaScript
/*!
|
|
* Tectum
|
|
* Copyright © 2026 Machiel Heinen.
|
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
|
* Zie LICENTIE.md.
|
|
*/
|
|
// ============================================================
|
|
// cost/cost-settings.js — CostSettings: venster "Kosten & tijden". Bouwt
|
|
// het formulier automatisch uit COST_DEFAULTS (elk item een invoerveld),
|
|
// opslaan via CostModel.saveSettings (localStorage). Exporteren/importeren
|
|
// als JSON-bestand; "Standaard" zet alle velden terug op cost-defaults.js.
|
|
// Alleen UI; de rekenregels staan in CostModel.
|
|
// ============================================================
|
|
|
|
window.CostSettings = (() => {
|
|
let overlay = null;
|
|
|
|
const esc = UI.esc;
|
|
|
|
function build() {
|
|
overlay = document.createElement('div');
|
|
overlay.id = 'cost-settings-overlay';
|
|
overlay.innerHTML = `
|
|
<div id="cost-settings-modal">
|
|
<div class="modal-header">
|
|
<h2>Kosten & tijden</h2>
|
|
<button type="button" class="modal-close-x" data-cs="cancel">×</button>
|
|
</div>
|
|
<div class="cs-body"></div>
|
|
<div class="material-info">Bedragen excl. btw. Opslaan bewaart in deze browser én downloadt een JSON-bestand; een gewijzigd veld is gemarkeerd. Standaardwaarden staan in js/cost/cost-defaults.js.</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="io-btn" data-cs="reset">Standaard</button>
|
|
<button type="button" class="io-btn" data-cs="export">Exporteren…</button>
|
|
<button type="button" class="io-btn" data-cs="import">Importeren…</button>
|
|
<span style="flex:1"></span>
|
|
<button type="button" class="io-btn" data-cs="cancel">Annuleren</button>
|
|
<button type="button" class="material-toggle-btn active" data-cs="save" style="flex:none; padding:6px 18px;">Opslaan</button>
|
|
</div>
|
|
<input type="file" accept=".json,application/json" style="display:none">
|
|
</div>`;
|
|
document.body.appendChild(overlay);
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) return close();
|
|
const btn = e.target.closest('[data-cs]');
|
|
if (!btn) return;
|
|
const a = btn.dataset.cs;
|
|
if (a === 'cancel') close();
|
|
else if (a === 'save') { // ook als JSON-bestand (om te delen/bewaren)
|
|
const v = readForm();
|
|
CostModel.saveSettings(v);
|
|
download(CostModel.exportJson(v));
|
|
close();
|
|
}
|
|
else if (a === 'reset') fill(CostModel.defaultValues());
|
|
else if (a === 'export') download(CostModel.exportJson(readForm()));
|
|
else if (a === 'import') overlay.querySelector('input[type=file]').click();
|
|
});
|
|
overlay.addEventListener('input', markChanged);
|
|
overlay.querySelector('input[type=file]').addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
e.target.value = '';
|
|
if (!file) return;
|
|
file.text().then(txt => fill(CostModel.importJson(JSON.parse(txt))))
|
|
.catch(err => alert('Importeren mislukt: ' + err.message));
|
|
});
|
|
}
|
|
|
|
function renderForm() {
|
|
const body = overlay.querySelector('.cs-body');
|
|
body.innerHTML = Object.entries(window.COST_DEFAULTS).map(([g, grp]) => `
|
|
<section class="cs-group">
|
|
<h3>${esc(grp.label)}</h3>
|
|
${Object.entries(grp.items).map(([k, it]) => `
|
|
<label class="cs-row">
|
|
<span class="cs-label">${esc(it.label)}</span>
|
|
${it.options
|
|
? `<select data-g="${g}" data-k="${k}">${Object.entries(it.options).map(([v, t]) => `<option value="${esc(v)}">${esc(t)}</option>`).join('')}</select>`
|
|
: `<input type="number" step="any" data-g="${g}" data-k="${k}">`}
|
|
<span class="cs-unit">${esc(it.unit || '')}</span>
|
|
</label>`).join('')}
|
|
</section>`).join('');
|
|
}
|
|
|
|
function fill(values) {
|
|
overlay.querySelectorAll('[data-g]').forEach(el => { el.value = values[el.dataset.g][el.dataset.k]; });
|
|
markChanged();
|
|
}
|
|
function readForm() {
|
|
const v = CostModel.getSettings();
|
|
overlay.querySelectorAll('[data-g]').forEach(el => {
|
|
const it = window.COST_DEFAULTS[el.dataset.g].items[el.dataset.k];
|
|
const val = it.options ? el.value : parseFloat(el.value);
|
|
if (it.options || isFinite(val)) v[el.dataset.g][el.dataset.k] = val;
|
|
});
|
|
return v;
|
|
}
|
|
// Afwijkend van de standaard: gemarkeerd.
|
|
function markChanged() {
|
|
const def = CostModel.defaultValues();
|
|
overlay.querySelectorAll('[data-g]').forEach(el => {
|
|
const d = def[el.dataset.g][el.dataset.k];
|
|
el.closest('.cs-row').classList.toggle('changed', typeof d === 'number' ? parseFloat(el.value) !== d : el.value !== d);
|
|
});
|
|
}
|
|
|
|
function download(obj) {
|
|
const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = `tectum-kosten-${new Date().toISOString().slice(0, 10)}.json`;
|
|
a.click();
|
|
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
|
}
|
|
|
|
function open() {
|
|
if (!overlay) build();
|
|
renderForm(); // opnieuw: nieuwe items in cost-defaults.js verschijnen vanzelf
|
|
fill(CostModel.getSettings());
|
|
overlay.classList.add('open');
|
|
}
|
|
function close() { if (overlay) overlay.classList.remove('open'); }
|
|
|
|
return { open, close };
|
|
})();
|