Split calculator into tabs and use dynamic spread

Reorganize the single-scroll page into three purpose tabs (Rekenmodel,
Live dag-strategie, Historie) with the Parameters panel as a persistent
sidebar. Tab state is persisted in localStorage and charts are resized on
activation to avoid 0px canvases in hidden panels; print CSS keeps the full
report.

Also replace the legacy fixed EUR 0,15 spread in the scenario comparator and
payback chart with the dynamic market spread (peakPrice - refPrice), matching
the rest of the tool, and make the related labels reflect it live.

Add .claude/launch.json for local static-preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 14:19:15 +02:00
parent 3f67c537a8
commit 99f08d3f2c
2 changed files with 104 additions and 19 deletions

11
.claude/launch.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "static",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "4567"],
"port": 4567
}
]
}

View File

@ -128,9 +128,19 @@
.chart-box { position: relative; height: 260px; }
.chart-box-tall { position: relative; height: 320px; }
/* Tab bar */
.tab-btn { color: rgb(82 82 91); }
.dark .tab-btn { color: rgb(161 161 170); }
.tab-btn:not([aria-selected="true"]):hover { background: rgb(244 244 245); }
.dark .tab-btn:not([aria-selected="true"]):hover { background: rgb(39 39 42); }
.tab-btn[aria-selected="true"] { background: rgb(16 185 129); color: #fff; }
/* Print friendly */
@media print {
.no-print { display: none !important; }
/* Print the full report regardless of the active tab */
[role="tablist"] { display: none !important; }
[role="tabpanel"][hidden] { display: block !important; }
html, body { background: #ffffff !important; color: #000000 !important; }
.dark, .dark * { color: #000 !important; }
.dark .bg-zinc-900, .dark .bg-zinc-950, .dark .bg-zinc-800\/40 { background: #fff !important; }
@ -323,9 +333,21 @@
</div>
</aside>
<!-- Live results -->
<!-- Tab container -->
<div class="space-y-6 min-w-0">
<nav role="tablist" aria-label="Weergaven" class="no-print flex flex-wrap gap-1 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900/40 p-1 shadow-sm">
<button id="tab-btn-model" type="button" role="tab" aria-controls="tab-model" aria-selected="true" data-tab="model"
class="tab-btn flex-1 min-w-max rounded-lg px-3 py-1.5 text-sm font-medium transition-colors">Rekenmodel</button>
<button id="tab-btn-day" type="button" role="tab" aria-controls="tab-day" aria-selected="false" data-tab="day"
class="tab-btn flex-1 min-w-max rounded-lg px-3 py-1.5 text-sm font-medium transition-colors">Live dag-strategie</button>
<button id="tab-btn-history" type="button" role="tab" aria-controls="tab-history" aria-selected="false" data-tab="history"
class="tab-btn flex-1 min-w-max rounded-lg px-3 py-1.5 text-sm font-medium transition-colors">Historie</button>
</nav>
<!-- TAB: Rekenmodel -->
<div id="tab-model" role="tabpanel" aria-labelledby="tab-btn-model" class="space-y-6">
<!-- Headline cards -->
<div class="grid sm:grid-cols-3 gap-4">
<div class="rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900/40 shadow-sm p-4">
@ -455,9 +477,9 @@
<div class="rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900/40 shadow-sm p-4 break-inside-avoid">
<div class="flex items-baseline justify-between mb-2">
<h3 class="text-sm font-semibold flex items-center">Terugverdiend van de aanschaf
<span class="help-icon" data-tip="Hoeveel van de aanschafprijs verdien je terug over de hele levensduur bij een prijsverschil van EUR 0,15? Surplus is winst boven aanschafprijs.">?</span>
<span class="help-icon" data-tip="Hoeveel van de aanschafprijs verdien je terug over de hele levensduur bij de ingestelde spread (hoge minus lage marktprijs)? Surplus is winst boven aanschafprijs.">?</span>
</h3>
<span class="text-[11px] text-zinc-500 dark:text-zinc-400">over levensduur, verschil EUR 0,15</span>
<span class="text-[11px] text-zinc-500 dark:text-zinc-400" id="payback-spread-label">over levensduur</span>
</div>
<div class="chart-box"><canvas id="chart-payback"></canvas></div>
</div>
@ -472,20 +494,23 @@
<div class="chart-box"><canvas id="chart-waterfall"></canvas></div>
</div>
</div>
</div>
</section>
<!-- SCENARIO VERGELIJKER -->
<section class="space-y-3">
<div class="flex items-baseline justify-between">
<h2 class="text-lg font-semibold">Scenario vergelijker</h2>
<p class="text-xs text-zinc-500 dark:text-zinc-400">Bij spread EUR 0,15 per kWh doorzet</p>
</div>
<div class="grid md:grid-cols-3 gap-4" id="scenarios"></div>
</section>
<!-- SCENARIO VERGELIJKER -->
<section class="space-y-3">
<div class="flex items-baseline justify-between">
<h2 class="text-lg font-semibold">Scenario vergelijker</h2>
<p class="text-xs text-zinc-500 dark:text-zinc-400" id="scenario-spread-label">Bij ingestelde spread per kWh doorzet</p>
</div>
<div class="grid md:grid-cols-3 gap-4" id="scenarios"></div>
</section>
<!-- EPEX DAG STRATEGIE -->
<section class="space-y-3 no-print">
</div><!-- /#tab-model -->
<!-- TAB: Live dag-strategie -->
<div id="tab-day" role="tabpanel" aria-labelledby="tab-btn-day" class="space-y-6" hidden>
<!-- EPEX DAG STRATEGIE -->
<section class="space-y-3 no-print">
<div class="flex flex-wrap items-baseline justify-between gap-2">
<div>
<h2 class="text-lg font-semibold">EPEX dag-strategie</h2>
@ -551,8 +576,13 @@
</div>
</section>
<!-- HISTORICAL ANALYSIS -->
<section class="space-y-3 no-print">
</div><!-- /#tab-day -->
<!-- TAB: Historie -->
<div id="tab-history" role="tabpanel" aria-labelledby="tab-btn-history" class="space-y-6" hidden>
<!-- HISTORICAL ANALYSIS -->
<section class="space-y-3 no-print">
<div class="flex flex-wrap items-baseline justify-between gap-2">
<div>
<h2 class="text-lg font-semibold">30-dagen historische analyse</h2>
@ -602,6 +632,11 @@
</div>
</section>
</div><!-- /#tab-history -->
</div><!-- /tab container -->
</section>
<footer class="pt-8 pb-4 text-center text-xs text-zinc-500 dark:text-zinc-400 no-print">
Gegevens worden lokaal opgeslagen in je browser. Geen server, geen tracking.
</footer>
@ -1167,7 +1202,9 @@
function renderPaybackChart() {
// Donut: lifetime revenue vs aanschafprijs.
const spread = 0.15;
const spread = state.peakPrice - state.refPrice;
const paybackLabel = document.getElementById('payback-spread-label');
if (paybackLabel) paybackLabel.textContent = 'over levensduur, spread ' + fmtEur(spread, 3);
const perCycle = profitPerCycle(state, spread);
const lifetimeRev = Math.max(0, perCycle * state.cycles);
const aanschaf = state.price;
@ -1257,7 +1294,9 @@
];
function renderScenarios() {
const spread = 0.15; // EUR / kWh doorzet, vast
const spread = state.peakPrice - state.refPrice; // dynamische marktspread (hoog - laag)
const spreadLabel = document.getElementById('scenario-spread-label');
if (spreadLabel) spreadLabel.textContent = 'Bij spread ' + fmtEur(spread, 3) + ' per kWh doorzet (hoog - laag)';
const parent = document.getElementById('scenarios');
const cards = SCENARIOS.map(sc => {
const s = Object.assign({}, state, { rte: sc.rte, cycles: sc.cycles });
@ -1394,6 +1433,40 @@
});
}
/* ============================================================
* Tabs
* ============================================================ */
const TAB_KEY = 'thuisbatterij_tab';
const TAB_NAMES = ['model', 'day', 'history'];
function activateTab(name) {
if (!TAB_NAMES.includes(name)) name = 'model';
for (const t of TAB_NAMES) {
const panel = document.getElementById('tab-' + t);
const btn = document.getElementById('tab-btn-' + t);
const active = t === name;
if (panel) panel.hidden = !active;
if (btn) btn.setAttribute('aria-selected', active ? 'true' : 'false');
}
try { localStorage.setItem(TAB_KEY, name); } catch (e) { /* ignore */ }
// Charts created inside a hidden panel render at 0px; resize once visible.
requestAnimationFrame(() => {
for (const k of Object.keys(charts)) {
if (charts[k] && charts[k].resize) charts[k].resize();
}
});
}
function bindTabs() {
document.querySelectorAll('[role="tab"]').forEach(btn => {
btn.addEventListener('click', () => activateTab(btn.dataset.tab));
});
let initial = 'model';
try { initial = localStorage.getItem(TAB_KEY) || 'model'; } catch (e) { /* ignore */ }
activateTab(initial);
}
/* ============================================================
* EPEX dag-strategie via EasyEnergy
* ============================================================ */
@ -1767,6 +1840,7 @@
bindInputs();
bindTheme();
bindReset();
bindTabs();
document.getElementById('fetch-day').addEventListener('click', handleFetchDay);
document.getElementById('fetch-history').addEventListener('click', handleFetchHistory);
renderAll();