- Single-Page HTML-App mit allen 18 Eingabefeldern - 12 Berechnungen live (calc.js, reine Funktionen) - LocalStorage-Persistenz, Mehrfach-Projekte via Sidebar - Excel Im-/Export ueber SheetJS (vendored, MIT) - Drag&Drop + File-Picker-Import - Apple-Swiss-Styling, responsive - Vorlagen-Excel mit 3 Reitern (Eingabe/Kalkulation/Angebot), Formeln referenzieren Eingabe - openpyxl-Script fuer reproduzierbaren Template-Build - 5 Test-Szenarien validiert Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
250 lines
7.6 KiB
JavaScript
250 lines
7.6 KiB
JavaScript
/* App-Controller — UI-Bindings, State-Sync, Excel */
|
|
/* global window, document */
|
|
|
|
(() => {
|
|
const { Calc, Store, Excel } = window;
|
|
|
|
/** @type {Array} */
|
|
let projects = Store.load();
|
|
let currentId = Store.getCurrentId();
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
|
|
|
const fmtEur = (v) =>
|
|
new Intl.NumberFormat('de-DE', {
|
|
style: 'currency', currency: 'EUR', minimumFractionDigits: 2,
|
|
}).format(v || 0);
|
|
|
|
const ensureProject = () => {
|
|
if (projects.length === 0) {
|
|
const p = Store.makeDefault('Neues Projekt');
|
|
projects.push(p);
|
|
currentId = p.id;
|
|
Store.save(projects);
|
|
Store.setCurrentId(currentId);
|
|
} else if (!currentId || !projects.find((p) => p.id === currentId)) {
|
|
currentId = projects[0].id;
|
|
Store.setCurrentId(currentId);
|
|
}
|
|
};
|
|
|
|
const getCurrent = () => projects.find((p) => p.id === currentId);
|
|
|
|
const renderProjects = () => {
|
|
const nav = $('#projects');
|
|
nav.innerHTML = '';
|
|
projects.forEach((p) => {
|
|
const el = document.createElement('div');
|
|
el.className = 'project-item' + (p.id === currentId ? ' active' : '');
|
|
el.textContent = p.projectName || 'Unbenannt';
|
|
el.title = p.projectName || '';
|
|
el.addEventListener('click', () => {
|
|
currentId = p.id;
|
|
Store.setCurrentId(currentId);
|
|
renderProjects();
|
|
syncFormFromState();
|
|
recalc();
|
|
});
|
|
nav.appendChild(el);
|
|
});
|
|
};
|
|
|
|
const syncFormFromState = () => {
|
|
const p = getCurrent();
|
|
if (!p) return;
|
|
Store.FIELDS.forEach((f) => {
|
|
const el = document.querySelector(`[data-field="${f}"]`);
|
|
if (!el) return;
|
|
el.value = p[f] ?? '';
|
|
});
|
|
$('#current-project-name').textContent = p.projectName || 'Unbenanntes Projekt';
|
|
};
|
|
|
|
const syncStateFromForm = () => {
|
|
const p = getCurrent();
|
|
if (!p) return;
|
|
Store.FIELDS.forEach((f) => {
|
|
const el = document.querySelector(`[data-field="${f}"]`);
|
|
if (!el) return;
|
|
const raw = el.value;
|
|
if (el.type === 'number') {
|
|
p[f] = raw === '' ? 0 : parseFloat(raw);
|
|
} else {
|
|
p[f] = raw;
|
|
}
|
|
});
|
|
p.updatedAt = new Date().toISOString();
|
|
Store.save(projects);
|
|
$('#current-project-name').textContent = p.projectName || 'Unbenanntes Projekt';
|
|
};
|
|
|
|
const recalc = () => {
|
|
const p = getCurrent();
|
|
if (!p) return;
|
|
const r = Calc.calculate(p);
|
|
$('#r-unitNet').textContent = fmtEur(r.unitNet);
|
|
$('#r-unitGross').textContent = fmtEur(r.unitGross);
|
|
$('#r-totalGross').textContent = fmtEur(r.totalGross);
|
|
$('#r-margin').textContent = fmtEur(r.margin);
|
|
$('#r-c1').textContent = fmtEur(r.materialCost);
|
|
$('#r-c2').textContent = fmtEur(r.machineCost);
|
|
$('#r-c3').textContent = fmtEur(r.energyCost);
|
|
$('#r-c4').textContent = fmtEur(r.postCost);
|
|
$('#r-c5').textContent = fmtEur(r.totalProduction);
|
|
$('#r-c6').textContent = fmtEur(r.scrapSurcharge);
|
|
$('#r-c7').textContent = fmtEur(r.subtotalNet);
|
|
$('#r-c9').textContent = fmtEur(r.customerNet);
|
|
};
|
|
|
|
const attachFieldListeners = () => {
|
|
$$('[data-field]').forEach((el) => {
|
|
el.addEventListener('input', () => {
|
|
syncStateFromForm();
|
|
if (el.dataset.field !== 'projectName') {
|
|
recalc();
|
|
} else {
|
|
renderProjects();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const newProject = () => {
|
|
const p = Store.makeDefault('Neues Projekt ' + (projects.length + 1));
|
|
projects.push(p);
|
|
currentId = p.id;
|
|
Store.save(projects);
|
|
Store.setCurrentId(currentId);
|
|
renderProjects();
|
|
syncFormFromState();
|
|
recalc();
|
|
};
|
|
|
|
const duplicateProject = () => {
|
|
const cur = getCurrent();
|
|
if (!cur) return;
|
|
const clone = { ...cur, id: 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), projectName: (cur.projectName || 'Projekt') + ' (Kopie)', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
projects.push(clone);
|
|
currentId = clone.id;
|
|
Store.save(projects);
|
|
Store.setCurrentId(currentId);
|
|
renderProjects();
|
|
syncFormFromState();
|
|
recalc();
|
|
};
|
|
|
|
const deleteProject = () => {
|
|
if (projects.length === 0) return;
|
|
const cur = getCurrent();
|
|
const ok = confirm(`Projekt "${cur?.projectName || 'Unbenannt'}" wirklich loeschen?`);
|
|
if (!ok) return;
|
|
projects = projects.filter((p) => p.id !== currentId);
|
|
if (projects.length === 0) {
|
|
ensureProject();
|
|
} else {
|
|
currentId = projects[0].id;
|
|
Store.setCurrentId(currentId);
|
|
}
|
|
Store.save(projects);
|
|
renderProjects();
|
|
syncFormFromState();
|
|
recalc();
|
|
};
|
|
|
|
const exportCurrent = () => {
|
|
const p = getCurrent();
|
|
if (!p) return;
|
|
const r = Calc.calculate(p);
|
|
try {
|
|
Excel.exportXlsx(p, r);
|
|
} catch (e) {
|
|
alert('Export fehlgeschlagen: ' + e.message);
|
|
}
|
|
};
|
|
|
|
const importFile = async (file) => {
|
|
try {
|
|
const updates = await Excel.importXlsx(file);
|
|
const p = getCurrent();
|
|
if (!p) return;
|
|
Object.entries(updates).forEach(([k, v]) => {
|
|
if (Store.FIELDS.includes(k)) {
|
|
if (typeof p[k] === 'number' || ['materialCostPerKg','materialUsageG','printTimeH','machineRate','powerKwh','powerPrice','postMin','postRate','packagingCost','shippingCost','setupCost','scrapPct','marginPct','quantity','individualAdjustment','vatPct'].includes(k)) {
|
|
const n = parseFloat(v);
|
|
p[k] = Number.isFinite(n) ? n : 0;
|
|
} else {
|
|
p[k] = String(v ?? '');
|
|
}
|
|
}
|
|
});
|
|
p.updatedAt = new Date().toISOString();
|
|
Store.save(projects);
|
|
renderProjects();
|
|
syncFormFromState();
|
|
recalc();
|
|
$('#dropzone').classList.remove('visible');
|
|
} catch (e) {
|
|
alert('Import fehlgeschlagen: ' + e.message);
|
|
}
|
|
};
|
|
|
|
const initDragDrop = () => {
|
|
const dz = $('#dropzone');
|
|
const show = () => dz.classList.add('visible');
|
|
const hide = () => dz.classList.remove('visible');
|
|
['dragenter', 'dragover'].forEach((ev) => {
|
|
window.addEventListener(ev, (e) => {
|
|
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) {
|
|
e.preventDefault();
|
|
show();
|
|
dz.classList.add('dragover');
|
|
}
|
|
});
|
|
});
|
|
['dragleave', 'dragend'].forEach((ev) => {
|
|
dz.addEventListener(ev, () => dz.classList.remove('dragover'));
|
|
});
|
|
dz.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dz.classList.remove('dragover');
|
|
hide();
|
|
const f = e.dataTransfer?.files?.[0];
|
|
if (f) importFile(f);
|
|
});
|
|
window.addEventListener('drop', (e) => { e.preventDefault(); hide(); });
|
|
window.addEventListener('dragleave', (e) => {
|
|
if (e.clientX === 0 && e.clientY === 0) hide();
|
|
});
|
|
};
|
|
|
|
const initButtons = () => {
|
|
$('#btn-new').addEventListener('click', newProject);
|
|
$('#btn-duplicate').addEventListener('click', duplicateProject);
|
|
$('#btn-delete').addEventListener('click', deleteProject);
|
|
$('#btn-export').addEventListener('click', exportCurrent);
|
|
$('#btn-import').addEventListener('click', () => $('#file-input').click());
|
|
$('#file-input').addEventListener('change', (e) => {
|
|
const f = e.target.files?.[0];
|
|
if (f) importFile(f);
|
|
e.target.value = '';
|
|
});
|
|
};
|
|
|
|
const init = () => {
|
|
ensureProject();
|
|
renderProjects();
|
|
syncFormFromState();
|
|
attachFieldListeners();
|
|
initButtons();
|
|
initDragDrop();
|
|
recalc();
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|