Modul-Review 2: Onboarding-Umbau, Tour, Assistenten-Redesign
Deploy App / deploy (push) Successful in 1m10s
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:
+73
-41
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user