diff --git a/src/components/PlanView.tsx b/src/components/PlanView.tsx index f6324a1..5b62914 100644 --- a/src/components/PlanView.tsx +++ b/src/components/PlanView.tsx @@ -432,55 +432,6 @@ export function PlanView({ - {/* Anzeige-Umschalter (nominal/real, Plan/Ist) -- direkt über der Matrix, weil er nur - deren Zahlen steuert. */} - {hasPhases && ( -
- Anzeige -
- {(["nominal", "both", "real"] as ValueMode[]).map((m) => ( - - ))} -
- real = kaufkraftbereinigt (Planbeginn) - - {/* Zweite Achse: Datenquelle. Erscheint nur, wenn es überhaupt Ist-Werte gibt -- - sonst wäre es ein Umschalter ohne Gegenstück. */} - {hasActuals && ( - <> - Zahlen -
- {DATA_SOURCE_OPTIONS.map((o) => ( - - ))} -
- {dataSource === "ACTUAL" && ( - Abweichung gegenüber Plan farbig - )} - - )} -
- )} - {!hasPhases && ( } @@ -493,41 +444,6 @@ export function PlanView({ )} - {hasPhases && ( -
- - - {/* Alle Kategoriegruppen auf einen Schlag auf- oder zuklappen. */} - -
- )} - {computed.ruinAge !== null && (
@@ -556,8 +472,90 @@ export function PlanView({ - {columns.map((col) => col.kind === "phase" ? ( diff --git a/src/components/Tour.tsx b/src/components/Tour.tsx index 0b339fe..b2ee88c 100644 --- a/src/components/Tour.tsx +++ b/src/components/Tour.tsx @@ -1,211 +1,341 @@ "use client"; -// Interaktive Kurz-Tour über die Planansicht (Teil von Roadmap Nr. 10). +// Die Tour: eine kurze Demo statt eines Spotlights. // -// Spotlight ohne Engine-Bibliothek: Der abgedunkelte Bereich entsteht aus VIER fixed-Flächen -// um die Bounding-Box des Ziels herum (oben/unten/links/rechts). Das lässt ein «Loch» am Ziel -// frei und funktioniert -- anders als ein box-shadow-Trick -- unabhängig von z-index (sticky -// Matrix-Köpfe) und overflow (Matrix-Scrollbereich). Ziele, die es im aktuellen Plan nicht -// gibt oder die unsichtbar sind, werden übersprungen. +// Bis 0.35 legte sich die Tour über den ECHTEN Bildschirm und hob Bereiche hervor. Das hatte +// zwei Nachteile, die sich nicht beheben liessen: Auf einem frisch angelegten, leeren Plan gab +// es kaum etwas hervorzuheben -- gerade dann, wenn die Tour am nötigsten ist. Und das +// Ausschneiden der Hervorhebung kämpfte dauernd mit Scroll-Containern und Z-Ebenen. +// +// Neu zeigt die Tour einen NACHGEBAUTEN Bildschirm mit erfundenen Zahlen. Er ist bewusst +// keine echte Ansicht, sondern eine Illustration: gerade so detailliert, dass man den Aufbau +// erkennt, und so einfach, dass sie sich in einem Popup erklären lässt. Der Preis ist, dass +// die Attrappe bei UI-Änderungen nachgeführt werden muss -- dafür funktioniert die Tour ab +// der ersten Sekunde und unabhängig davon, was im Plan schon steht. -import { useEffect, useMemo, useState } from "react"; -import { Lightbulb, X } from "lucide-react"; +import { useState } from "react"; +import { ArrowLeft, ArrowRight, Sparkles, X } from "lucide-react"; import { Button } from "@/components/ui"; export const TOUR_DONE_KEY = "fpt-tour-done"; +type Focus = "none" | "settings" | "figures" | "actions" | "assistant" | "timeline" | "matrix" | "sidebar"; + interface TourStep { - target: string; // data-tour="…" title: string; text: string; + focus: Focus; } const STEPS: TourStep[] = [ { - target: "profil", - title: "Dein Grundprofil", - text: "Alter, Pensionsalter und Inflationsannahme – die Basis dieses Szenarios. Über «Bearbeiten» änderst du sie jederzeit.", + title: "Willkommen bei FPT", + text: "In zwei Minuten siehst du, wie das Tool aufgebaut ist. Alles, was hier steht, sind erfundene Beispielzahlen – dein eigener Plan ist noch leer.", + focus: "none", + }, + { + title: "Oben stellst du ein, unten siehst du das Ergebnis", + text: "Der Bildschirm hat zwei Hälften. Oben die Steuerung: Grundeinstellungen, Kennzahlen, Schnellaktionen und der Assistent. Unten die Matrix mit allen Zahlen.", + focus: "none", + }, + { + title: "Die Grundeinstellungen", + text: "Wer plant, ab wann und wie lange. Alter, Startjahr und Inflation gelten für den ganzen Plan; Planungshorizont und Pensionsalter gehören zum Szenario. Je Person führt ein Knopf in die Pensionsplanung.", + focus: "settings", + }, + { + title: "Die Kennzahlen", + text: "Was unter dem Strich herauskommt: dein Vermögen heute, das Endvermögen nominal und real – und ob das Kapital über die ganze Planung reicht.", + focus: "figures", + }, + { + title: "Der FPT-Assistent", + text: "Dein Wegweiser. Sieben Schritte führen von der Bestandsaufnahme bis zum fertigen Plan. Jeder Schritt erklärt zuerst, worum es geht, und bietet danach das passende Werkzeug an. Wann ein Schritt erledigt ist, entscheidest du.", + focus: "assistant", }, { - target: "timeline", title: "Die Zeitachse", - text: "Deine Lebensphasen im Zeitverlauf: Die Breite zeigt die Dauer, die Fähnchen die Pensionierungen – und falls es dazu kommt, den Punkt, an dem das Kapital ausgeht. Die ganze Planung denkt in diesen Phasen.", + text: "Deine Planung im Überblick: Lebensphasen, die Zeitpunkte der Pensionierung – und, falls das Geld nicht reicht, das Jahr, in dem es knapp wird.", + focus: "timeline", }, { - target: "endwealth", - title: "Dein Endvermögen", - text: "Das Ergebnis auf einen Blick: das Vermögen am Ende der Planung – nominal und real (in heutiger Kaufkraft). Reicht das Kapital nicht bis zum Schluss, steht hier stattdessen das Alter, in dem es ausgeht.", + title: "Die Matrix", + text: "Das Herz des Tools. Jede Zeile ist ein finanzielles Element, jede Spalte eine Lebensphase. Die schmalen Spalten dazwischen sind die Übergänge – dort entscheidest du, was beim Wechsel geschieht.", + focus: "matrix", }, { - target: "matrix", - title: "Die Matrix – das Herzstück", - text: "Zeilen sind deine Finanzen, Spalten deine Lebensphasen. Klick auf eine Zelle: rechts öffnet sich das Bearbeiten-Panel, die Matrix bleibt sichtbar.", + title: "Pläne und Szenarien", + text: "Links verwaltest du alles. Ein Szenario ist eine Variante deines Plans – etwa mit anderem Pensionsalter. Du kannst beliebig viele anlegen und vergleichen.", + focus: "sidebar", }, { - target: "transition", - title: "Übergänge", - text: "Zwischen zwei Phasen triffst du einmalige Entscheide – verkaufen, beziehen, verrenten. Orange heisst: noch offen. Klick auf den Spaltenkopf führt dich durch alle Entscheide.", - }, - { - target: "cash", - title: "Das Cash-Konto", - text: "Dein Ausgleichskonto: Was übrig bleibt, landet hier – was fehlt, macht es rot. Negativ = Liquiditätslücke.", - }, - { - target: "toolbar", - title: "Aktionen zum Szenario", - text: "Hier oben: die Änderungshistorie ansehen, ein neues Szenario aus diesem ableiten, die Rechenwege einsehen, die Matrix als CSV exportieren – und diese Tour jederzeit neu starten.", - }, - { - target: "menu", - title: "Analysen & Berichte", - text: "Links im Menü öffnest du die grossen Werkzeuge: unter «Analysen» die Grafiken, die Live-Simulation, Monte-Carlo und die Einflussfaktoren; unter «Berichte» erzeugst du ein PDF. «Effektive Werte» hält deine echten Zahlen gegen den Plan.", + title: "Jetzt bist du dran", + text: "Starte mit dem FPT-Assistenten oben rechts. Schritt 1 ist die Bestandsaufnahme: alles, was du heute besitzt. Von dort führt dich der Assistent weiter.", + focus: "assistant", }, ]; -interface Box { - top: number; - left: number; - width: number; - height: number; -} - export function Tour({ onClose }: { onClose: () => void }) { - // Nur Schritte, deren Ziel im DOM existiert UND sichtbar ist (z. B. die Menü-Leiste ist auf - // schmalen Screens ausgeblendet -- getClientRects() ist dann leer). - const steps = useMemo( - () => - STEPS.filter((s) => { - const el = document.querySelector(`[data-tour="${s.target}"]`); - return el !== null && el.getClientRects().length > 0; - }), - [] - ); const [index, setIndex] = useState(0); - const [rect, setRect] = useState(null); - const step = steps[index]; - - useEffect(() => { - if (!step) return; - const el = document.querySelector(`[data-tour="${step.target}"]`); - if (!el) return; - el.scrollIntoView({ block: "center", behavior: "smooth" }); - - // Die Bounding-Box jedes Frame nachführen (folgt dem sanften Scrollen und Resize), aber - // den State nur bei tatsächlicher Änderung neu setzen. - let raf = 0; - const update = () => { - const r = el.getBoundingClientRect(); - setRect((prev) => - prev && prev.top === r.top && prev.left === r.left && prev.width === r.width && prev.height === r.height - ? prev - : { top: r.top, left: r.left, width: r.width, height: r.height } - ); - raf = requestAnimationFrame(update); - }; - update(); - return () => cancelAnimationFrame(raf); - }, [step]); - - if (!step || !rect) return null; - - const pad = 6; - const hole: Box = { - top: Math.max(0, rect.top - pad), - left: Math.max(0, rect.left - pad), - width: rect.width + pad * 2, - height: rect.height + pad * 2, - }; - const cardAtBottom = rect.top + rect.height / 2 < window.innerHeight * 0.5; + const step = STEPS[index]; + const last = index === STEPS.length - 1; function finish() { try { localStorage.setItem(TOUR_DONE_KEY, "1"); } catch { - /* localStorage nicht verfügbar -> Tour erscheint beim nächsten Mal erneut */ + // Privater Modus o. Ä. -- die Tour lässt sich dann eben erneut öffnen. } onClose(); } - // Vier Abdunkel-Flächen um das Loch. pointer-events-none, damit nichts blockiert wird. - const dim = "fixed z-[45] bg-black/60 pointer-events-none"; - const belowTop = hole.top + hole.height; - const rightOfLeft = hole.left + hole.width; - return ( - <> -
-
-
-
- - {/* Pulsierender Rahmen ums Ziel. */} -
- - {/* Karte -- springt auf die dem Ziel gegenüberliegende Bildschirmhälfte. */} -
-
-
- - - -
-
{step.title}
-
- Schritt {index + 1} von {steps.length} -
-
+
+
+
+ +
+

{step.title}

+

+ Schritt {index + 1} von {STEPS.length} +

+
+ + {/* Die Attrappe. Rein illustrativ -- nichts darin ist klickbar. */} +
+
-

{step.text}

-
- -
-
- {steps.map((_, i) => ( - + +
+

{step.text}

+
+
+ {STEPS.map((_, i) => ( +
))}
-
- {index > 0 && ( - - )} - {index < steps.length - 1 ? ( - - ) : ( - - )} + + {last ? ( + + ) : ( + + )} +
+
+
+
+ ); +} + +// --- Die Attrappe --------------------------------------------------------------------------- +// +// Feste, erfundene Zahlen. Sie müssen weder stimmen noch zueinander passen -- ihr einziger +// Zweck ist, den Aufbau greifbar zu machen. + +function MockScreen({ focus }: { focus: Focus }) { + const dim = (area: Focus) => (focus === "none" || focus === area ? "opacity-100" : "opacity-20"); + const ring = (area: Focus) => (focus === area ? "ring-2 ring-accent" : ""); + + return ( +
+ {/* Seitenleiste */} +
+
Meine Pläne
+
Familie Muster
+
Szenarien
+
Basis
+
Pension mit 62
+
Effektive Werte
+
Analysen
+
Berichte
+
+ +
+ {/* Oberer Bereich */} +
+
+ +
Anna · 42 J.
+
Beat · 45 J.
+
Start 2026 · Inflation 1,5 %
+
Horizont 40 Jahre · bis 2065
+
+ Pension 65 · Ende 82 + Pensionsplanung +
+
+ + + + + +
+ Kapital reicht über die ganze Planung +
+
+ + + {["Neues Szenario", "Tour", "Änderungshistorie", "Rechenwege", "CSV-Export"].map((a) => ( +
+ {a} +
+ ))} +
+ +
+
+ + FPT-Assistent + 2 / 7 +
+ {[ + ["3. Lebensphasen", "4 Lebensphasen"], + ["4. Erwerbsjahre planen", ""], + ["5. Pensionierung planen", ""], + ].map(([t, sub]) => ( +
+ + + {t} + {sub && {sub}} + +
+ ))} + {["1. Bestandsaufnahme", "2. Eckdaten"].map((t) => ( +
+ + ✓ + + {t} +
+ ))} +
+
+ + {/* Zeitachse */} +
+
+ 2026 + 2065 +
+
+
+ Erwerbsjahre +
+
+ Übergangszeit +
+
+ Ruhestand +
+
+
+ Anna 42 · Beat 45 + Pension 65 + Anna 82 · Beat 85
+ + {/* Matrix */} +
+
- Finanzielle Elemente + {/* Die Ecke oben links trägt seit 0.36 alles, was die Matrix STEUERT: was man + hinzufügt und wie die Zahlen dargestellt werden. Vorher lagen diese Knöpfe + lose darüber und wirkten wie Aktionen der ganzen Seite. */} + +
+
+ Matrix + +
+ +
+ + +
+ +
+ {(["nominal", "both", "real"] as ValueMode[]).map((m) => ( + + ))} +
+ + {hasActuals && ( +
+ {DATA_SOURCE_OPTIONS.map((o) => ( + + ))} +
+ )} +
+ + + + {["Erwerbsjahre", "Übergang", "Ruhestand I", "Übergang", "Ruhestand II"].map((h, i) => ( + + ))} + + + + {[ + ["Lohn Anna", "96'000", "→", "—", "→", "—"], + ["Lebenshaltung", "78'000", "→", "72'000", "→", "68'000"], + ["Pensionskasse Anna", "310'000", "40 % Kapital", "Rente 18'400", "→", "18'400"], + ["Säule 3a Anna", "84'000", "Bezug mit 64", "—", "→", "—"], + ["Wertschriften", "120'000", "→", "486'000", "→", "310'000"], + ].map((row, i) => ( + + + {row.slice(1).map((c, j) => ( + + ))} + + ))} + +
+
Matrix
+
+ + + Element + + + + Phase + +
+
+ {h} + {h === "Übergang" &&
2 offen
} +
{row[0]} + {c} +
+
- + + ); +} + +function MockTile({ title, cls, children }: { title: string; cls: string; children: React.ReactNode }) { + return ( +
+
{title}
+ {children} +
+ ); +} + +function MockFigure({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
); }