v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
@@ -0,0 +1,124 @@
|
||||
/*!
|
||||
* 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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user