Modul-Review 3: Struktur, Dashboard, CSV-Export und Grafiken
Deploy App / deploy (push) Successful in 1m59s
Deploy App / deploy (push) Successful in 1m59s
Versionierung: - startet neu bei 0.1; erst eine gesetzte Hauptversion macht daraus 1.0 - Szenario-Liste zeigt die echte Version (z.B. 0.17) statt "1.x", plus neue Spalte "Phasen" - Migration 20260724120000_version_zero_start (nur der Default) Plan-Dashboard: - Kacheln anklickbar (fuehren in ihren Bereich), neue Kachel "Berichte" - Plan umbenennen ueber Stift-Symbol - Ist-Abweichung nennt das Jahr des juengsten Ist-Datensatzes und ist bei positiver Abweichung gruen statt rot Szenario-Liste: - Klick auf die Zeile oeffnet die Matrix (Matrix-Knopf entfaellt) - je Zeile Kopie (Vorlage frei waehlbar) und Loeschen - laedt nach einer Loeschung neu (zeigte vorher den alten Stand) Seitenleiste: - Szenarien wieder verschachtelt nach Herkunft - Effektive Werte / Analysen / Berichte buendig zum Knoten "Szenarien" CSV-Export (neues Modul lib/csv.ts): - vier Bloecke: Kopf, Lebensphasen, ganze Matrix (Elemente x Phasen inkl. Uebergangs-Entscheide im Klartext), Jahreswerte - mit BOM (Excel-Umlaute), Dateiname transliteriert Umlaute - vorher enthielt die Datei kein einziges finanzielles Element Grafiken: - Szenario-Waehler gilt fuer alle drei Grafiken - BUGFIX Szenario-Vergleich: WealthChart nutzte den Namen als Datenschluessel -> gleichnamige Szenarien ueberschrieben sich (Legende zeigte beide, Chart nur eine). Neu die ID; stille Deckelung auf 4 Serien entfaellt - eigene Legende mit freier Farbwahl je Serie + Erklaerung des Linienstils - Vermoegensaufteilung neu: gestapelte Flaeche ueber die Planjahre + Ring fuer die relative Aufteilung zu einem waehlbaren Zeitpunkt - alle Diagrammfarben aus neuen Theme-Tokens (--chart-1..6, --chart-grid) Doku-Drift bereinigt: Kapitel 2.1, 3.2.2-3.2.7 und 3.10 beschrieben noch den Stand vor V7 (Grundprofil am Szenario, parentPlanId, Scenario.startYear, window.confirm, drei Sidebar-Unterpunkte). Nebenbei: verstuemmelte Hex-Farbe --danger-soft (warm) repariert, deutsche Plural-/Umlautfehler in den Uebersichts-Kacheln. SPEZIFIKATION 0.31. 267 -> 275 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+53
-13
@@ -117,6 +117,8 @@ function AppShellInner({ username }: { username: string }) {
|
||||
const [showPalette, setShowPalette] = useState(false);
|
||||
// Sprungmarke in die SPEZIFIKATION, gesetzt aus einem Rechenweg heraus.
|
||||
const [specAnchor, setSpecAnchor] = useState<string | null>(null);
|
||||
// Erhoehen erzwingt ein Neuladen der Szenario-Liste (bleibt bei einer Loeschung montiert).
|
||||
const [scenarioListKey, setScenarioListKey] = useState(0);
|
||||
|
||||
// Tour (seit dem Layout-Umbau hier statt in PlanView -- sie liest die data-tour-Ziele im
|
||||
// DOM der Szenario-Ansicht). Zwei Auslöser:
|
||||
@@ -276,6 +278,9 @@ function AppShellInner({ username }: { username: string }) {
|
||||
return;
|
||||
}
|
||||
await loadPlans();
|
||||
// Die Szenario-Liste bleibt beim Löschen aus der Seitenleiste montiert -- ohne dieses
|
||||
// Signal zeigte sie den gelöschten Eintrag weiter.
|
||||
setScenarioListKey((k) => k + 1);
|
||||
if (selectedScenarioId === s.id) setSelectedScenarioId(null);
|
||||
}
|
||||
|
||||
@@ -384,7 +389,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openPlanTab(p.id, tab)}
|
||||
className={`flex w-full items-center gap-2 rounded-lg py-1.5 pl-8 pr-3 text-left text-xs font-medium transition-colors ${
|
||||
className={`flex w-full items-center gap-2 rounded-lg py-1.5 pl-3 pr-3 text-left text-xs font-medium transition-colors ${
|
||||
navHere && planNav?.tab === tab ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
@@ -452,7 +457,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void openActualsTab(p.id)}
|
||||
className={`flex w-full items-center gap-2 rounded-lg py-1.5 pl-8 pr-3 text-left text-xs font-medium transition-colors ${
|
||||
className={`flex w-full items-center gap-2 rounded-lg py-1.5 pl-3 pr-3 text-left text-xs font-medium transition-colors ${
|
||||
navHere && planNav?.tab === "actuals" ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
@@ -565,18 +570,27 @@ function AppShellInner({ username }: { username: string }) {
|
||||
|
||||
{/* Plan-Ebene: Dashboard / Szenarien / Analysen */}
|
||||
{!showSpec && !showSystemParams && planNav?.tab === "dashboard" && (
|
||||
<PlanDashboardView planId={planNav.planId} />
|
||||
<PlanDashboardView
|
||||
planId={planNav.planId}
|
||||
onNavigate={(tab) => (tab === "actuals" ? void openActualsTab(planNav.planId) : openPlanTab(planNav.planId, tab))}
|
||||
onRenamed={() => void loadPlans()}
|
||||
/>
|
||||
)}
|
||||
{!showSpec && !showSystemParams && planNav?.tab === "scenarios" && (
|
||||
<ScenarioListView
|
||||
planId={planNav.planId}
|
||||
reloadKey={scenarioListKey}
|
||||
onOpenMatrix={openScenario}
|
||||
onOpenHistory={(sid) => {
|
||||
void loadDetail(sid, true).then(() => setShowHistory(true));
|
||||
}}
|
||||
onNew={() => {
|
||||
const base = activePlan?.scenarios.find((s) => s.isBase) ?? activePlan?.scenarios[0];
|
||||
if (base) setCopyFrom({ id: base.id, planId: base.planId, planName: activePlan?.name ?? "", name: base.name, isBase: base.isBase, parentScenarioId: base.parentScenarioId } as ScenarioMeta);
|
||||
onCopyFrom={(sid) => {
|
||||
const src = activePlan?.scenarios.find((s) => s.id === sid);
|
||||
if (src) setCopyFrom(src);
|
||||
}}
|
||||
onDelete={(sid) => {
|
||||
const s = activePlan?.scenarios.find((x) => x.id === sid);
|
||||
if (s) void handleDeleteScenario(s);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -968,17 +982,43 @@ function ScenarioTree({
|
||||
onCopy: (s: ScenarioMeta) => void;
|
||||
onDelete: (s: ScenarioMeta) => void;
|
||||
}) {
|
||||
// Flache, gleichmässig eingerückte Liste: Basisszenario zuoberst, danach die Varianten.
|
||||
// Die Herkunfts-Verschachtelung wird in der Szenario-Liste (Spalte «aus …») gezeigt, nicht
|
||||
// mehr durch die Einrückung in der Seitenleiste.
|
||||
const ordered = [...scenarios].sort((a, b) => (a.isBase === b.isBase ? 0 : a.isBase ? -1 : 1));
|
||||
// Echte Verschachtelung: Ein Szenario wird unter seiner Vorlage eingerückt (Tiefe = Länge
|
||||
// der Herkunftskette). So ist in der Seitenleiste sofort sichtbar, woraus ein Szenario
|
||||
// entstanden ist -- die Szenario-Liste zeigt dasselbe zusätzlich als Spalte «aus …».
|
||||
const byId = new Map(scenarios.map((s) => [s.id, s]));
|
||||
const depthOf = (s: ScenarioMeta): number => {
|
||||
let d = 0;
|
||||
let cur = s.parentScenarioId ? byId.get(s.parentScenarioId) : undefined;
|
||||
// Deckel gegen eine (theoretisch) zyklische Kette.
|
||||
while (cur && d < 8) {
|
||||
d += 1;
|
||||
cur = cur.parentScenarioId ? byId.get(cur.parentScenarioId) : undefined;
|
||||
}
|
||||
return d;
|
||||
};
|
||||
|
||||
// Vorordnung: Basis zuoberst, Kinder direkt unter ihrer Vorlage.
|
||||
const ordered: ScenarioMeta[] = [];
|
||||
const visit = (parentId: string | null) => {
|
||||
for (const s of scenarios) {
|
||||
const pid = s.isBase ? null : s.parentScenarioId;
|
||||
if ((pid ?? null) !== parentId) continue;
|
||||
ordered.push(s);
|
||||
visit(s.id);
|
||||
}
|
||||
};
|
||||
visit(null);
|
||||
// Szenarien, deren Vorlage gelöscht wurde, hängen sonst nirgends -- ans Ende.
|
||||
for (const s of scenarios) if (!ordered.includes(s)) ordered.push(s);
|
||||
if (ordered.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ordered.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`group flex items-center gap-1.5 rounded-lg py-1.5 pr-1.5 pl-9 text-sm transition-colors ${
|
||||
style={{ paddingLeft: `${2.25 + depthOf(s) * 0.75}rem` }}
|
||||
className={`group flex items-center gap-1.5 rounded-lg py-1.5 pr-1.5 text-sm transition-colors ${
|
||||
selectedId === s.id ? "bg-accent-soft font-medium text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
@@ -1093,9 +1133,9 @@ function DashboardHome({
|
||||
|
||||
{/* Ein paar Kennzahlen pro Plan direkt in der Übersicht. */}
|
||||
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted">
|
||||
<span>{p.scenarios.length} Szenario{p.scenarios.length === 1 ? "" : "s"}</span>
|
||||
<span>{p.scenarios.length} Szenario{p.scenarios.length === 1 ? "" : "en"}</span>
|
||||
{p.startYear && <span>ab {p.startYear}</span>}
|
||||
{p._count && p._count.actuals > 0 && <span>{p._count.actuals} Ist-Datensatz{p._count.actuals === 1 ? "" : "e"}</span>}
|
||||
{p._count && p._count.actuals > 0 && <span>{p._count.actuals} {p._count.actuals === 1 ? "Ist-Datensatz" : "Ist-Datensätze"}</span>}
|
||||
{p._count && p._count.analyses > 0 && <span>{p._count.analyses} Analyse{p._count.analyses === 1 ? "" : "n"}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user