v1.8.16: Finanzen-Tab — Festpreis vs. Spot-Vergleich
Täglicher Tarif-Tracking: kWh + EPEX-Ø-Preis werden ab jetzt täglich gespeichert. Neuer Finanzen-Tab zeigt Balkendiagramm (Festpreis vs. Spot hypothetisch), Summen-Karten und Empfehlung ob flexibler Tarif sich lohnen würde. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
dfb42e6902
commit
2456f356b4
@@ -175,6 +175,7 @@
|
||||
<main>
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchTab('energy')">Energie</div>
|
||||
<div class="tab" onclick="switchTab('finance')">Finanzen</div>
|
||||
<div class="tab" onclick="switchTab('live')">Live-Daten</div>
|
||||
<div class="tab" onclick="switchTab('inverters')">Geräte</div>
|
||||
<div class="tab" onclick="switchTab('settings')">Einstellungen</div>
|
||||
@@ -185,6 +186,11 @@
|
||||
<div id="energy-content"><div class="no-data">Warte auf erste Messung…</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Finance Panel -->
|
||||
<div class="panel" id="panel-finance">
|
||||
<div id="finance-content"><div class="no-data">Lade…</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Live Panel -->
|
||||
<div class="panel" id="panel-live">
|
||||
<div id="live-content"><div class="no-data">Warte auf erste Messung...</div></div>
|
||||
@@ -430,11 +436,12 @@ function showToast(msg, type) {
|
||||
}
|
||||
|
||||
function switchTab(name) {
|
||||
["energy","live","inverters","settings"].forEach((t, i) => {
|
||||
["energy","finance","live","inverters","settings"].forEach((t, i) => {
|
||||
document.querySelectorAll(".tab")[i].classList.toggle("active", t === name);
|
||||
document.querySelectorAll(".panel")[i].classList.toggle("active", t === name);
|
||||
});
|
||||
if (name === "energy" || name === "live") startRefresh(); else stopRefresh();
|
||||
if (name === "finance") loadFinance();
|
||||
}
|
||||
|
||||
// ── Energy Dashboard ──────────────────────────────────────────
|
||||
@@ -705,6 +712,112 @@ function renderEnergy(inverters, aggregates, period, spotData) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Finance Tab ───────────────────────────────────────────────
|
||||
|
||||
async function loadFinance() {
|
||||
const el = document.getElementById("finance-content");
|
||||
let data;
|
||||
try { data = await fetchJSON(api("api/finance")); }
|
||||
catch(e) { el.innerHTML = '<div class="no-data">Fehler beim Laden</div>'; return; }
|
||||
|
||||
const { days, fixed_total_eur, spot_total_eur, savings_eur, period_start, spot_days, total_days } = data;
|
||||
|
||||
if (!days || days.length === 0) {
|
||||
el.innerHTML = `<div style="text-align:center;padding:40px;color:var(--text-dim)">
|
||||
<div style="font-size:32px;margin-bottom:12px">📊</div>
|
||||
<div style="font-weight:600;margin-bottom:6px">Noch keine Daten</div>
|
||||
<div style="font-size:13px">Der Tracker sammelt ab morgen täglich Daten.<br>Abrechnungsjahr ab ${period_start}.</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const C = { fixed: '#58a6ff', spot: '#f0883e', grid: '#30363d', green: '#4caf82', red: '#e05c5c' };
|
||||
|
||||
// ── Empfehlung ───────────────────────────────────────────────
|
||||
let empfehlung = '';
|
||||
if (spot_total_eur !== null && spot_days >= 7) {
|
||||
const lohnt = savings_eur > 0;
|
||||
const col = lohnt ? C.green : C.red;
|
||||
const icon = lohnt ? '✓' : '✗';
|
||||
empfehlung = `<div style="display:flex;align-items:center;justify-content:space-between;background:var(--surface);border:1px solid ${col};border-radius:var(--radius);padding:14px 18px;margin-bottom:16px">
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:15px;color:${col}">${icon} Flexibler Tarif würde sich ${lohnt ? 'lohnen' : 'nicht lohnen'}</div>
|
||||
<div style="font-size:12px;color:var(--text-dim);margin-top:3px">Basierend auf ${spot_days} Tagen im Abrechnungsjahr</div>
|
||||
</div>
|
||||
<div style="font-size:22px;font-weight:700;color:${col}">${lohnt ? '−' : '+'}${fEur(Math.abs(savings_eur))}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Summary-Karten ───────────────────────────────────────────
|
||||
const spotCard = spot_total_eur !== null
|
||||
? `<div class="kwh-card" style="border-top:3px solid ${C.spot}">
|
||||
<div class="kv" style="color:${C.spot}">${fEur(spot_total_eur)}</div>
|
||||
<div class="kl">Spot-Tarif (hypothetisch)<br><span style="opacity:.6">${spot_days} Tage mit Preisdaten</span></div>
|
||||
</div>`
|
||||
: `<div class="kwh-card"><div class="kv" style="color:var(--text-dim)">–</div><div class="kl">Spot-Tarif<br><span style="opacity:.6">Noch keine Daten</span></div></div>`;
|
||||
|
||||
const savCard = savings_eur !== null
|
||||
? `<div class="kwh-card" style="border-top:3px solid ${savings_eur > 0 ? C.green : C.red}">
|
||||
<div class="kv" style="color:${savings_eur > 0 ? C.green : C.red}">${savings_eur > 0 ? '−' : '+'}${fEur(Math.abs(savings_eur))}</div>
|
||||
<div class="kl">${savings_eur > 0 ? 'Ersparnis mit Spot' : 'Mehrkosten mit Spot'}<br><span style="opacity:.6">gegenüber Festpreis</span></div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const cards = `<div class="energy-kwh" style="margin-bottom:20px">
|
||||
<div class="kwh-card" style="border-top:3px solid ${C.fixed}">
|
||||
<div class="kv" style="color:${C.fixed}">${fEur(fixed_total_eur)}</div>
|
||||
<div class="kl">Festpreis-Kosten<br><span style="opacity:.6">${total_days} Tage</span></div>
|
||||
</div>
|
||||
${spotCard}${savCard}
|
||||
</div>`;
|
||||
|
||||
// ── SVG-Balkendiagramm ────────────────────────────────────────
|
||||
const W = 600, H = 180, PL = 44, PR = 12, PT = 10, PB = 28;
|
||||
const cW = W - PL - PR;
|
||||
const cH = H - PT - PB;
|
||||
const n = days.length;
|
||||
const barW = Math.max(2, Math.floor(cW / n) - 2);
|
||||
const gap = Math.max(1, Math.floor(cW / n) - barW);
|
||||
|
||||
const allVals = days.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null));
|
||||
const maxVal = Math.max(...allVals, 0.01);
|
||||
|
||||
const yTicks = 4;
|
||||
let gridLines = '', yLabels = '';
|
||||
for (let i = 0; i <= yTicks; i++) {
|
||||
const v = maxVal * i / yTicks;
|
||||
const y = PT + cH - (cH * i / yTicks);
|
||||
gridLines += `<line x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}" stroke="${C.grid}" stroke-width="0.5"/>`;
|
||||
yLabels += `<text x="${PL-4}" y="${(y+4).toFixed(1)}" text-anchor="end" font-size="9" fill="#8b949e">${fEur(v)}</text>`;
|
||||
}
|
||||
|
||||
let bars = '', xLabels = '';
|
||||
days.forEach((d, i) => {
|
||||
const x0 = PL + i * (barW + gap);
|
||||
const hFixed = cH * d.fixed_eur / maxVal;
|
||||
bars += `<rect x="${x0}" y="${(PT + cH - hFixed).toFixed(1)}" width="${barW/2}" height="${hFixed.toFixed(1)}" fill="${C.fixed}" opacity="0.85"/>`;
|
||||
if (d.spot_eur != null) {
|
||||
const hSpot = cH * d.spot_eur / maxVal;
|
||||
bars += `<rect x="${x0 + barW/2}" y="${(PT + cH - hSpot).toFixed(1)}" width="${barW/2}" height="${hSpot.toFixed(1)}" fill="${C.spot}" opacity="0.85"/>`;
|
||||
}
|
||||
if (i % Math.max(1, Math.floor(n / 8)) === 0) {
|
||||
const label = d.date.slice(5);
|
||||
xLabels += `<text x="${(x0 + barW/2).toFixed(1)}" y="${H - 4}" text-anchor="middle" font-size="9" fill="#8b949e">${label}</text>`;
|
||||
}
|
||||
});
|
||||
|
||||
const legend = `<text x="${PL}" y="${H+18}" font-size="10" fill="${C.fixed}">■ Festpreis</text>
|
||||
<text x="${PL+70}" y="${H+18}" font-size="10" fill="${C.spot}">■ Spot (hypothetisch)</text>`;
|
||||
|
||||
const chart = `<div style="margin-bottom:20px;overflow-x:auto">
|
||||
<svg viewBox="0 0 ${W} ${H+24}" style="width:100%;max-width:${W}px;display:block">
|
||||
${gridLines}${yLabels}${bars}${xLabels}${legend}
|
||||
</svg>
|
||||
</div>`;
|
||||
|
||||
el.innerHTML = empfehlung + cards + chart;
|
||||
}
|
||||
|
||||
// ── Live Data ─────────────────────────────────────────────────
|
||||
|
||||
async function refreshData() {
|
||||
|
||||
Reference in New Issue
Block a user