feat: Ringdiagramm faerbt Legende und Mitte nach Budget

Eingefaerbt war bisher nur die Listen-Darstellung der Ausgaben-Anteile. In der
Kreis-Darstellung blieben die Betraege der Legende grau, und die Summe in der
Mitte sagte gar nichts ueber den Stand.

Jetzt tragen beide die Aussage: gruen im geplanten Rahmen, rot darueber. Faehrt
man ein Segment an, zeigt die Mitte dessen Betrag – dann gilt dort das Budget
dieser Kategorie, sonst das des Ganzen.

Fuer die Mitte liefert der Server jetzt auch das Geplante ueber ALLE
Ausgaben-Einheiten, nicht nur ueber die mit Ausgaben: Der Monatsplan umfasst
auch, wofuer noch nichts ausgegeben wurde.

Ohne Plan bleibt alles, wie es war. Der Donut faerbt nur, wenn ein Segment die
Angabe mitbringt – die Budget-Karte, die dieselbe Komponente benutzt, bleibt
also unberuehrt.

Der Test hat dabei gleich einen Fehler gefunden: ``_units`` ist ein Generator,
und ich lief zweimal darueber – beim zweiten Mal war er leer und das Geplante
immer 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-30 14:13:49 +02:00
co-authored by Claude Opus 5
parent 4d3b9a8207
commit 63056ae76a
5 changed files with 51 additions and 7 deletions
+6 -1
View File
@@ -160,8 +160,13 @@ function ExpenseShare({ props }) {
if ((props?.darstellung || "liste") === "kreis") {
return (
<Donut
segmente={data.rows.map((r) => ({ name: r.name, value: r.amount, color: r.color }))}
segmente={data.rows.map((r) => ({
name: r.name, value: r.amount, color: r.color,
// Ohne Plan keine Aussage – dort gibt es nichts zu überschreiten.
budget: r.planned > 0 ? (r.over ? "over" : "ok") : null,
}))}
gesamtLabel={t("cards.spent")}
gesamtBudget={data.total_planned > 0 ? (data.total_over ? "over" : "ok") : null}
fmt={(v) => formatMoney(v, data.currency)}
/>
);
+13 -4
View File
@@ -1,8 +1,11 @@
import { useState } from "react";
// Ringdiagramm für Anteile. segmente: [{ name, value, color }]
// Ringdiagramm für Anteile. segmente: [{ name, value, color, budget }]
// `budget` ist "ok", "over" oder fehlt – fehlt es, wird nichts eingefärbt.
// `gesamtBudget` gilt für die Mitte, solange kein Segment angezeigt wird.
// fmt: Formatierer für die Werte (z. B. Geldbetrag).
export default function Donut({ segmente = [], gesamtLabel = "gesamt", fmt = (v) => String(v) }) {
export default function Donut({ segmente = [], gesamtLabel = "gesamt",
gesamtBudget = null, fmt = (v) => String(v) }) {
const [aktiv, setAktiv] = useState(null);
const werte = segmente.filter((s) => s.value > 0);
@@ -29,6 +32,9 @@ export default function Donut({ segmente = [], gesamtLabel = "gesamt", fmt = (v)
// Mittelbetrag an die Stringlänge anpassen, damit ein 4-stelliger Betrag
// (z. B. „CHF 3'865.00") nicht in den Ring läuft.
const zentralText = fmt(gezeigt ? gezeigt.value : summe);
// Die Mitte zeigt beim Überfahren den Wert EINES Segments – dann gilt dessen
// Budget-Stand, sonst der des Ganzen.
const zentralBudget = gezeigt ? gezeigt.budget : gesamtBudget;
const wertGroesse = Math.max(11, Math.min(20, Math.floor(190 / Math.max(zentralText.length, 1))));
return (
@@ -45,7 +51,8 @@ export default function Donut({ segmente = [], gesamtLabel = "gesamt", fmt = (v)
onMouseEnter={() => setAktiv(b)} onMouseLeave={() => setAktiv(null)} />
))}
</g>
<text x={mitte} y={mitte - 4} textAnchor="middle" className="donut-value"
<text x={mitte} y={mitte - 4} textAnchor="middle"
className={`donut-value${zentralBudget ? ` budget-${zentralBudget}` : ""}`}
style={{ fontSize: wertGroesse }}>
{zentralText}
</text>
@@ -60,7 +67,9 @@ export default function Donut({ segmente = [], gesamtLabel = "gesamt", fmt = (v)
<li key={b.name} onMouseEnter={() => setAktiv(b)} onMouseLeave={() => setAktiv(null)}>
<span className="swatch" style={{ background: b.color }} />
<span className="legend-name">{b.name}</span>
<span className="legend-value">{fmt(b.value)} · {Math.round(b.anteil * 100)} %</span>
<span className={`legend-value${b.budget ? ` budget-${b.budget}` : ""}`}>
{fmt(b.value)} · {Math.round(b.anteil * 100)} %
</span>
</li>
))}
</ul>
+14
View File
@@ -1443,3 +1443,17 @@ tr.row-dup {
padding-top: var(--sp-2);
border-top: 1px dashed var(--border);
}
/* Budget-Stand im Ringdiagramm: grün im Rahmen, rot darüber – in der Legende
und in der Mitte. Ohne Plan bleibt beides, wie es war: Da gibt es nichts zu
überschreiten, und eine Farbe behauptete etwas. */
.legend-value.budget-ok,
.donut-value.budget-ok {
fill: var(--ok);
color: var(--ok);
}
.legend-value.budget-over,
.donut-value.budget-over {
fill: var(--danger);
color: var(--danger);
}