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
-101
@@ -9,8 +9,6 @@ import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CreditCard,
|
||||
Download,
|
||||
HelpCircle,
|
||||
Home,
|
||||
Landmark,
|
||||
ListTodo,
|
||||
@@ -33,7 +31,6 @@ import {
|
||||
transitionInactive as inactiveAtTransition,
|
||||
TRANSITION_CATEGORIES,
|
||||
} from "@/lib/decisions";
|
||||
import { Tour, TOUR_DONE_KEY } from "@/components/Tour";
|
||||
import { Button, EmptyState, InspectorShell, Modal, useConfirm, useToast } from "@/components/ui";
|
||||
import { ElementDetailDialog, PhaseDetailDialog } from "@/components/DetailView";
|
||||
import {
|
||||
@@ -165,24 +162,12 @@ export function PlanView({
|
||||
const [addCategory, setAddCategory] = useState<ElementCategory | null>(null);
|
||||
const [showAddPhase, setShowAddPhase] = useState(false);
|
||||
const [reviewFromPhaseId, setReviewFromPhaseId] = useState<string | null>(null);
|
||||
const [showTour, setShowTour] = useState(false);
|
||||
// Offener Verteil-Dialog (Kapital bzw. Spar-/Verzehrquote) -- bewusst ein eigenes Popup,
|
||||
// weil beide mehrere Elemente auf einmal bearbeiten.
|
||||
const [distribute, setDistribute] = useState<{ kind: "capital" | "rates"; phaseId: string } | null>(null);
|
||||
const [valueMode, setValueMode] = useState<ValueMode>("nominal");
|
||||
|
||||
// Tour beim ersten Besuch eines Plans mit Phasen automatisch starten.
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (computed.phases.length > 0 && !localStorage.getItem(TOUR_DONE_KEY)) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- einmalige Initialisierung aus localStorage
|
||||
setShowTour(true);
|
||||
}
|
||||
} catch {
|
||||
/* localStorage nicht verfügbar */
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
// Die Tour (Start-Knopf, Auto-Start bei Plan-Erstellung, Rendering) liegt seit dem
|
||||
// Layout-Umbau in AppShell -- sie liest die data-tour-Ziele im DOM dieser Ansicht.
|
||||
// Nur-Lese-Detailansicht (Roadmap Nr. 43). Der Rechenweg wird erst beim Öffnen erzeugt.
|
||||
const [detailFor, setDetailFor] = useState<{ kind: "element"; id: string } | { kind: "phase"; id: string } | null>(null);
|
||||
|
||||
@@ -361,60 +346,19 @@ export function PlanView({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted">Anzeige</span>
|
||||
<div className="inline-flex rounded-lg border border-border bg-surface p-0.5 text-xs">
|
||||
{(["nominal", "both", "real"] as ValueMode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => changeValueMode(m)}
|
||||
className={`rounded-md px-2.5 py-1 font-medium ${
|
||||
valueMode === m ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{m === "nominal" ? "Nominal" : m === "both" ? "Beide" : "Real"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[11px] text-faint">real = kaufkraftbereinigt (Planbeginn)</span>
|
||||
|
||||
{/* Zweite Achse: Datenquelle. Erscheint nur, wenn es überhaupt Ist-Werte gibt --
|
||||
sonst wäre es ein Umschalter ohne Gegenstück. */}
|
||||
{hasActuals && (
|
||||
<>
|
||||
<span className="ml-2 text-xs text-muted">Zahlen</span>
|
||||
<div className="inline-flex rounded-lg border border-border bg-surface p-0.5 text-xs">
|
||||
{DATA_SOURCE_OPTIONS.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => setDataSource(o.value)}
|
||||
title={o.label}
|
||||
className={`rounded-md px-2.5 py-1 font-medium ${
|
||||
dataSource === o.value ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{o.short}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{dataSource === "ACTUAL" && (
|
||||
<span className="text-[11px] text-faint">Abweichung gegenüber Plan farbig</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div data-tour="timeline">
|
||||
<Timeline
|
||||
phases={computed.phases}
|
||||
persons={personAxes}
|
||||
ruinAge={computed.ruinAge}
|
||||
actualYears={actualYears}
|
||||
startYear={plan.startYear}
|
||||
/>
|
||||
</div>
|
||||
<NextSteps
|
||||
plan={plan}
|
||||
computed={computed}
|
||||
columns={columns}
|
||||
openCountFor={transitionOpenCount}
|
||||
onReview={(fromPhaseId) => setReviewFromPhaseId(fromPhaseId)}
|
||||
onAddElement={(cat) => {
|
||||
setAddCategory(cat);
|
||||
setShowAdd(true);
|
||||
}}
|
||||
onAddPhase={() => setShowAddPhase(true)}
|
||||
onOpenSensitivity={onOpenSensitivity}
|
||||
/>
|
||||
|
||||
{/* Szenario-Profil */}
|
||||
<div
|
||||
@@ -441,19 +385,64 @@ export function PlanView({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<NextSteps
|
||||
plan={plan}
|
||||
computed={computed}
|
||||
columns={columns}
|
||||
openCountFor={transitionOpenCount}
|
||||
onReview={(fromPhaseId) => setReviewFromPhaseId(fromPhaseId)}
|
||||
onAddElement={(cat) => {
|
||||
setAddCategory(cat);
|
||||
setShowAdd(true);
|
||||
}}
|
||||
onAddPhase={() => setShowAddPhase(true)}
|
||||
onOpenSensitivity={onOpenSensitivity}
|
||||
/>
|
||||
<div data-tour="timeline">
|
||||
<Timeline
|
||||
phases={computed.phases}
|
||||
persons={personAxes}
|
||||
ruinAge={computed.ruinAge}
|
||||
actualYears={actualYears}
|
||||
startYear={plan.startYear}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Anzeige-Umschalter (nominal/real, Plan/Ist) -- direkt über der Matrix, weil er nur
|
||||
deren Zahlen steuert. */}
|
||||
{hasPhases && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted">Anzeige</span>
|
||||
<div className="inline-flex rounded-lg border border-border bg-surface p-0.5 text-xs">
|
||||
{(["nominal", "both", "real"] as ValueMode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => changeValueMode(m)}
|
||||
className={`rounded-md px-2.5 py-1 font-medium ${
|
||||
valueMode === m ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{m === "nominal" ? "Nominal" : m === "both" ? "Beide" : "Real"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[11px] text-faint">real = kaufkraftbereinigt (Planbeginn)</span>
|
||||
|
||||
{/* Zweite Achse: Datenquelle. Erscheint nur, wenn es überhaupt Ist-Werte gibt --
|
||||
sonst wäre es ein Umschalter ohne Gegenstück. */}
|
||||
{hasActuals && (
|
||||
<>
|
||||
<span className="ml-2 text-xs text-muted">Zahlen</span>
|
||||
<div className="inline-flex rounded-lg border border-border bg-surface p-0.5 text-xs">
|
||||
{DATA_SOURCE_OPTIONS.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => setDataSource(o.value)}
|
||||
title={o.label}
|
||||
className={`rounded-md px-2.5 py-1 font-medium ${
|
||||
dataSource === o.value ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{o.short}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{dataSource === "ACTUAL" && (
|
||||
<span className="text-[11px] text-faint">Abweichung gegenüber Plan farbig</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasPhases && (
|
||||
<EmptyState
|
||||
@@ -479,21 +468,6 @@ export function PlanView({
|
||||
>
|
||||
<Plus className="h-4 w-4" /> Lebensphase
|
||||
</button>
|
||||
{/* CSV-Export der Matrix. Liest immer den aktuellen Stand aus der Datenbank. */}
|
||||
<a
|
||||
href={`/api/scenarios/${plan.id}/export`}
|
||||
className="ml-auto flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1.5 text-xs font-medium text-muted transition-colors hover:bg-surface-2 hover:text-fg"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" /> CSV-Export
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTour(true)}
|
||||
title="Kurze Tour durch die Ansicht"
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1.5 text-xs font-medium text-muted transition-colors hover:bg-surface-2 hover:text-fg"
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" /> Tour
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -852,8 +826,6 @@ export function PlanView({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTour && <Tour onClose={() => setShowTour(false)} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user