Modul-Review 2: Onboarding-Umbau, Tour, Assistenten-Redesign
Deploy App / deploy (push) Successful in 1m10s

Ergebnis der zweiten Test- und Review-Runde (Modul "Onboarding & Plan-Erstellung"):

Layout:
- Szenario-Ansicht neu geordnet: schlanke Funktions-Leiste oben (Historie,
  Tour, Neues Szenario, Rechenwege, CSV-Export, Loeschen, Diff-Badge), dann
  Naechste Schritte -> Grundprofil -> Zeitachse -> Anzeige -> Matrix
- Grafiken/Effektive Werte/Live-Sim/MC/Einflussfaktoren aus der Leiste
  entfernt (laufen ueber die eigenen Menuepunkte)
- Tour nach AppShell gehoben; startet neu bei JEDER Plan-Erstellung (F14)

Assistent:
- durchgehend Du-Form im Einzelmodus
- Schritt "Vorsorge & Vermoegen" nur noch Bestandswerte
- neuer Schritt "Sparen & Verteilen": Sparquote (Netto - Ausgaben) auf
  3a/Wertschriften/Amortisation/Tilgung verteilen, Rest bleibt Cash; PK
  separat mit Hinweis "vor Netto, reduziert Sparquote nicht"
- Immobilie: Wertsteigerung + Zins-in-Ausgaben-Schalter; Lohn 1% Default

Grosse Saeule 3a (Selbststaendige ohne PK):
- Schalter am 3a-Element + im Assistenten hebt Obergrenze 7258 -> 36288
- neue Konstante PILLAR_3A_MAX_SELF_EMPLOYED (2026-Wert zu verifizieren)
- neues Feld selfEmployed3a

Nebenbei: POST /plans liefert lesbare Fehlermeldung statt zod-Objekt.

Kein Eingriff in den Rechenkern; 267 Tests unveraendert. SPEZIFIKATION 0.28.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 21:57:56 +02:00
parent 2bb243b896
commit 52c48a9956
8 changed files with 448 additions and 192 deletions
+73 -41
View File
@@ -1,19 +1,20 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
BarChart3,
BookOpen,
ChevronDown,
ChevronRight,
Copy,
Dices,
FileText,
FileSpreadsheet,
FolderKanban,
CalendarClock,
GitBranch,
HelpCircle,
History,
Download,
Layers,
LayoutDashboard,
Menu,
@@ -22,7 +23,6 @@ import {
Search,
SlidersHorizontal,
Sparkles,
Tornado,
Trash2,
X,
} from "lucide-react";
@@ -44,6 +44,7 @@ import { PlanTraceDialog } from "@/components/DetailView";
import { computePlan } from "@/lib/calculations";
import { ProfileMenu } from "@/components/ProfileMenu";
import { PlanWizard } from "@/components/PlanWizard";
import { Tour, TOUR_DONE_KEY } from "@/components/Tour";
import { CommandPalette, type PaletteAction } from "@/components/CommandPalette";
import { PlanProfileFields, emptyProfileDraft, type ProfileDraft } from "@/components/PlanProfileFields";
import {
@@ -117,6 +118,33 @@ function AppShellInner({ username }: { username: string }) {
// Sprungmarke in die SPEZIFIKATION, gesetzt aus einem Rechenweg heraus.
const [specAnchor, setSpecAnchor] = useState<string | null>(null);
// Tour (seit dem Layout-Umbau hier statt in PlanView -- sie liest die data-tour-Ziele im
// DOM der Szenario-Ansicht). Zwei Auslöser:
// * pendingTourRef: nach JEDER Plan-Erstellung genau einmal erzwingen (Roadmap-Feedback F14).
// * Erst-Öffnen eines Plans mit Phasen, solange die Tour noch nie beendet wurde.
const [showTour, setShowTour] = useState(false);
const pendingTourRef = useRef(false);
const autoTourCheckedRef = useRef<Set<string>>(new Set());
useEffect(() => {
if (!detail || detail.plan.phases.length === 0) return;
const sid = detail.meta.id;
if (pendingTourRef.current) {
pendingTourRef.current = false;
autoTourCheckedRef.current.add(sid);
setShowTour(true);
return;
}
if (autoTourCheckedRef.current.has(sid)) return; // pro Szenario nur einmal, nicht bei Refresh
autoTourCheckedRef.current.add(sid);
try {
// eslint-disable-next-line react-hooks/set-state-in-effect -- einmalige Initialisierung aus localStorage
if (!localStorage.getItem(TOUR_DONE_KEY)) setShowTour(true);
} catch {
/* localStorage nicht verfügbar */
}
}, [detail]);
function openSpecAt(anchor: string) {
setSpecAnchor(anchor);
setShowSpec(true);
@@ -257,8 +285,9 @@ function AppShellInner({ username }: { username: string }) {
const sid = await createDemoPlan();
await loadPlans();
setShowNewPlanChooser(false);
pendingTourRef.current = true; // Tour bei jeder Plan-Erstellung (F14)
openScenario(sid);
toast("success", "Beispielplan angelegt schau dich um!");
toast("success", "Beispielplan angelegt die Tour zeigt dir gleich die wichtigsten Stellen.");
} catch (e) {
toast("error", e instanceof Error ? e.message : "Beispielplan konnte nicht angelegt werden.");
} finally {
@@ -597,53 +626,52 @@ function AppShellInner({ username }: { username: string }) {
{!showSpec && !showSystemParams && !loading && detail && selectedScenarioId && (
<div className="flex flex-col gap-6">
{/* Funktions-Buttons der Szenario-Ansicht. Grafiken/Effektive Werte/Live-Sim/
Monte-Carlo/Einflussfaktoren sind hier bewusst NICHT mehr -- sie laufen über
die eigenen Menüpunkte (Analysen / Effektive Werte). Hier bleiben die reinen
Szenario-Aktionen plus Tour und CSV-Export. */}
<div className="flex flex-wrap items-center gap-2" data-tour="analysen">
{detail.plan.phases.length > 0 && (
<Button variant="secondary" onClick={() => setShowHistory(true)}>
<History className="h-4 w-4" />
Änderungshistorie
</Button>
)}
{detail.plan.phases.length > 0 && (
<Button variant="secondary" onClick={() => setShowTour(true)} title="Kurze Tour durch die Ansicht">
<HelpCircle className="h-4 w-4" />
Tour
</Button>
)}
<Button variant="secondary" onClick={() => setCopyFrom(detail.meta)}>
<Copy className="h-4 w-4" />
Neues Szenario aus diesem
</Button>
{detail.plan.phases.length > 0 && (
<Button
variant="secondary"
onClick={() => setShowPlanTraces(true)}
title="Wie wird gerechnet? Plan-weite Grössen wie Deflatoren, AHV-Karriere und Ruinalter"
>
<BookOpen className="h-4 w-4" />
Rechenwege
</Button>
)}
{detail.plan.phases.length > 0 && (
<a
href={`/api/scenarios/${detail.meta.id}/export`}
className="inline-flex items-center justify-center gap-1.5 rounded-lg border border-border bg-surface px-4 py-2 text-sm font-medium text-muted transition-all duration-150 hover:bg-surface-2 hover:text-fg"
>
<Download className="h-4 w-4" />
CSV-Export
</a>
)}
{!detail.meta.isBase && (
<Button variant="danger" onClick={() => handleDeleteScenario(detail.meta)}>
<Trash2 className="h-4 w-4" />
Szenario löschen
</Button>
)}
{detail.plan.phases.length > 0 && (
<>
<Button variant="secondary" onClick={() => setShowCharts(true)}>
<BarChart3 className="h-4 w-4" />
Grafiken
</Button>
<Button variant="secondary" onClick={() => setShowActuals(true)}>
<CalendarClock className="h-4 w-4" />
Effektive Werte
</Button>
<Button variant="secondary" onClick={() => setShowHistory(true)}>
<History className="h-4 w-4" />
Änderungshistorie
</Button>
<Button variant="secondary" onClick={() => setShowLiveSim(true)}>
<SlidersHorizontal className="h-4 w-4" />
Live-Simulation
</Button>
<Button variant="secondary" onClick={() => setShowMonteCarlo(true)}>
<Dices className="h-4 w-4" />
Monte-Carlo-Simulation
</Button>
<Button variant="secondary" onClick={() => setShowSensitivity(true)}>
<Tornado className="h-4 w-4" />
Einflussfaktoren
</Button>
<Button
variant="secondary"
onClick={() => setShowPlanTraces(true)}
title="Wie wird gerechnet? Plan-weite Grössen wie Deflatoren, AHV-Karriere und Ruinalter"
>
<BookOpen className="h-4 w-4" />
Rechenwege
</Button>
</>
)}
{diff && detail.base && (
<span className="ml-auto flex items-center gap-1.5 rounded-lg border border-diff bg-diff-soft px-3 py-1.5 text-xs font-medium text-diff">
<GitBranch className="h-3.5 w-3.5" />
@@ -690,6 +718,7 @@ function AppShellInner({ username }: { username: string }) {
onCreated={async (sid) => {
setShowWizard(false);
await loadPlans();
pendingTourRef.current = true; // Tour bei jeder Plan-Erstellung (F14)
openScenario(sid);
toast("success", "Plan erstellt die Tour zeigt dir gleich die wichtigsten Stellen.");
}}
@@ -706,8 +735,9 @@ function AppShellInner({ username }: { username: string }) {
);
setShowEmptyPlanDialog(false);
await loadPlans();
pendingTourRef.current = true; // Tour bei jeder Plan-Erstellung (F14)
openScenario(scenario.id);
toast("success", "Plan erstellt.");
toast("success", "Plan erstellt die Tour zeigt dir gleich die wichtigsten Stellen.");
}}
onClose={() => setShowEmptyPlanDialog(false)}
/>
@@ -817,6 +847,8 @@ function AppShellInner({ username }: { username: string }) {
actions={paletteActions}
onOpenScenario={openScenario}
/>
{showTour && <Tour onClose={() => setShowTour(false)} />}
</div>
);
}