Szenario-Screen mit vier Kacheln, FPT-Assistent, Onboarding auf einen Knopf
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-202
@@ -3,7 +3,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
BarChart3,
|
||||
BookOpen,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
@@ -12,9 +11,6 @@ import {
|
||||
FolderKanban,
|
||||
CalendarClock,
|
||||
GitBranch,
|
||||
HelpCircle,
|
||||
History,
|
||||
Download,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
Menu,
|
||||
@@ -43,7 +39,6 @@ import { SystemParametersView } from "@/components/SystemParametersView";
|
||||
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";
|
||||
@@ -58,7 +53,6 @@ import {
|
||||
useToast,
|
||||
} from "@/components/ui";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { createDemoPlan } from "@/lib/demoplan";
|
||||
import { computeScenarioDiff } from "@/lib/diff";
|
||||
import type { PlanInput, PlanListItem, ScenarioMeta } from "@/lib/types";
|
||||
import type { PlanComputed } from "@/lib/calculations";
|
||||
@@ -95,10 +89,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
const [detail, setDetail] = useState<ScenarioDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [showNewPlanChooser, setShowNewPlanChooser] = useState(false);
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
const [showEmptyPlanDialog, setShowEmptyPlanDialog] = useState(false);
|
||||
const [creatingDemo, setCreatingDemo] = useState(false);
|
||||
const [copyFrom, setCopyFrom] = useState<ScenarioMeta | null>(null);
|
||||
const [showSpec, setShowSpec] = useState(false);
|
||||
const [showCharts, setShowCharts] = useState(false);
|
||||
@@ -284,22 +275,6 @@ function AppShellInner({ username }: { username: string }) {
|
||||
if (selectedScenarioId === s.id) setSelectedScenarioId(null);
|
||||
}
|
||||
|
||||
async function handleCreateDemo() {
|
||||
setCreatingDemo(true);
|
||||
try {
|
||||
const sid = await createDemoPlan();
|
||||
await loadPlans();
|
||||
setShowNewPlanChooser(false);
|
||||
pendingTourRef.current = true; // Tour bei jeder Plan-Erstellung (F14)
|
||||
openScenario(sid);
|
||||
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 {
|
||||
setCreatingDemo(false);
|
||||
}
|
||||
}
|
||||
|
||||
const activePlan =
|
||||
plans.find((p) => p.scenarios.some((s) => s.id === selectedScenarioId)) ??
|
||||
(planNav ? plans.find((p) => p.id === planNav.planId) ?? null : null);
|
||||
@@ -315,7 +290,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
// Aktionen der Befehls-Palette -- kontextabhängig (Analysen nur bei offenem Szenario).
|
||||
const paletteActions = useMemo<PaletteAction[]>(() => {
|
||||
const base: PaletteAction[] = [
|
||||
{ id: "a-new", label: "Neuen Plan erstellen", hint: "Aktion", run: () => setShowNewPlanChooser(true) },
|
||||
{ id: "a-new", label: "Neuen Plan erstellen", hint: "Aktion", run: () => setShowEmptyPlanDialog(true) },
|
||||
{ id: "a-home", label: "Übersicht öffnen", hint: "Aktion", run: () => { setSelectedScenarioId(null); setPlanNav(null); setShowSpec(false); setShowSystemParams(false); } },
|
||||
{ id: "a-spec", label: "So rechnet FPT (Spezifikation)", hint: "Aktion", run: () => { setShowSpec(true); setSpecAnchor(null); setShowSystemParams(false); setSelectedScenarioId(null); setPlanNav(null); } },
|
||||
{ id: "a-params", label: "Systemparameter", hint: "Aktion", run: () => { setShowSystemParams(true); setShowSpec(false); setSelectedScenarioId(null); setPlanNav(null); } },
|
||||
@@ -372,7 +347,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-faint">Meine Pläne</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewPlanChooser(true)}
|
||||
onClick={() => setShowEmptyPlanDialog(true)}
|
||||
aria-label="Neuen Plan erstellen"
|
||||
className="rounded-md p-1 text-accent transition-colors hover:bg-accent-soft"
|
||||
>
|
||||
@@ -631,73 +606,36 @@ function AppShellInner({ username }: { username: string }) {
|
||||
<DashboardHome
|
||||
username={username}
|
||||
plans={plans}
|
||||
creatingDemo={creatingDemo}
|
||||
onOpenPlan={(id) => openPlanTab(id, "dashboard")}
|
||||
onCreateGuided={() => setShowWizard(true)}
|
||||
onCreateChooser={() => setShowNewPlanChooser(true)}
|
||||
onCreateEmpty={() => setShowEmptyPlanDialog(true)}
|
||||
onCreateDemo={handleCreateDemo}
|
||||
onCreatePlan={() => setShowEmptyPlanDialog(true)}
|
||||
onDelete={handleDeletePlan}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!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="toolbar">
|
||||
{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>
|
||||
)}
|
||||
{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" />
|
||||
{diff.total === 0
|
||||
? "Unverändert gegenüber der Vorlage"
|
||||
: `${diff.total} Abweichung${diff.total === 1 ? "" : "en"} gegenüber der Vorlage`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Nur noch, was NICHT zum Szenario-Inhalt gehört: das Löschen des Szenarios
|
||||
und der Hinweis auf Abweichungen zur Vorlage. Die inhaltlichen Aktionen
|
||||
(Historie, Tour, Kopie, Rechenwege, Export) sitzen seit 0.36 als Kachel im
|
||||
Szenario-Screen -- dort, wo man sie braucht. */}
|
||||
{(!detail.meta.isBase || (diff && detail.base)) && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{!detail.meta.isBase && (
|
||||
<Button variant="danger" onClick={() => handleDeleteScenario(detail.meta)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Szenario löschen
|
||||
</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" />
|
||||
{diff.total === 0
|
||||
? "Unverändert gegenüber der Vorlage"
|
||||
: `${diff.total} Abweichung${diff.total === 1 ? "" : "en"} gegenüber der Vorlage`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PlanView
|
||||
plan={detail.plan}
|
||||
@@ -708,40 +646,18 @@ function AppShellInner({ username }: { username: string }) {
|
||||
onChanged={refreshCurrent}
|
||||
onOpenSpec={openSpecAt}
|
||||
onOpenSensitivity={() => setShowSensitivity(true)}
|
||||
onOpenHistory={() => setShowHistory(true)}
|
||||
onStartTour={() => setShowTour(true)}
|
||||
onCopyScenario={() => setCopyFrom(detail.meta)}
|
||||
onOpenTraces={() => setShowPlanTraces(true)}
|
||||
exportHref={`/api/scenarios/${detail.meta.id}/export`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{showNewPlanChooser && (
|
||||
<NewPlanChooser
|
||||
creatingDemo={creatingDemo}
|
||||
onGuided={() => {
|
||||
setShowNewPlanChooser(false);
|
||||
setShowWizard(true);
|
||||
}}
|
||||
onEmpty={() => {
|
||||
setShowNewPlanChooser(false);
|
||||
setShowEmptyPlanDialog(true);
|
||||
}}
|
||||
onDemo={handleCreateDemo}
|
||||
onClose={() => setShowNewPlanChooser(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showWizard && (
|
||||
<PlanWizard
|
||||
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.");
|
||||
}}
|
||||
onClose={() => setShowWizard(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEmptyPlanDialog && (
|
||||
<PlanDialog
|
||||
@@ -870,73 +786,6 @@ function AppShellInner({ username }: { username: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Auswahl beim Anlegen eines Plans: geführt (empfohlen), leer oder Beispielplan.
|
||||
function NewPlanChooser({
|
||||
creatingDemo,
|
||||
onGuided,
|
||||
onEmpty,
|
||||
onDemo,
|
||||
onClose,
|
||||
}: {
|
||||
creatingDemo: boolean;
|
||||
onGuided: () => void;
|
||||
onEmpty: () => void;
|
||||
onDemo: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const options = [
|
||||
{
|
||||
icon: <Sparkles className="h-5 w-5" />,
|
||||
title: "Geführt erstellen",
|
||||
badge: "Empfohlen",
|
||||
text: "Der Assistent fragt Schritt für Schritt – in fünf Minuten steht dein Grundgerüst.",
|
||||
run: onGuided,
|
||||
},
|
||||
{
|
||||
icon: <Plus className="h-5 w-5" />,
|
||||
title: "Leer starten",
|
||||
badge: null,
|
||||
text: "Nur das Grundprofil erfassen, alles Weitere selbst aufbauen – für geübte Nutzer.",
|
||||
run: onEmpty,
|
||||
},
|
||||
{
|
||||
icon: <PiggyBank className="h-5 w-5" />,
|
||||
title: creatingDemo ? "Beispielplan wird angelegt…" : "Beispielplan ansehen",
|
||||
badge: null,
|
||||
text: "Ein fertig ausgefüllter, fiktiver Plan zum Erkunden – so siehst du das Tool zuerst in Aktion.",
|
||||
run: onDemo,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Modal title="Neuer Plan" onClose={onClose}>
|
||||
<div className="flex flex-col gap-2">
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o.title}
|
||||
type="button"
|
||||
disabled={creatingDemo}
|
||||
onClick={o.run}
|
||||
className="flex items-start gap-3 rounded-xl border border-border p-3.5 text-left transition-colors hover:border-accent hover:bg-accent-soft/30 disabled:opacity-60"
|
||||
>
|
||||
<span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-accent-soft-fg">
|
||||
{o.icon}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="flex items-center gap-2 text-sm font-semibold text-fg">
|
||||
{o.title}
|
||||
{o.badge && (
|
||||
<span className="rounded bg-accent px-1.5 py-0.5 text-[9px] font-semibold uppercase text-accent-fg">{o.badge}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-muted">{o.text}</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// Breiter Dialog für den Analyse-Bereich (Grafiken).
|
||||
function ChartsDialog({
|
||||
title,
|
||||
@@ -1056,22 +905,14 @@ function ScenarioTree({
|
||||
function DashboardHome({
|
||||
username,
|
||||
plans,
|
||||
creatingDemo,
|
||||
onOpenPlan,
|
||||
onCreateGuided,
|
||||
onCreateChooser,
|
||||
onCreateDemo,
|
||||
onCreateEmpty,
|
||||
onCreatePlan,
|
||||
onDelete,
|
||||
}: {
|
||||
username: string;
|
||||
plans: PlanListItem[];
|
||||
creatingDemo: boolean;
|
||||
onOpenPlan: (planId: string) => void;
|
||||
onCreateGuided: () => void;
|
||||
onCreateChooser: () => void;
|
||||
onCreateDemo: () => void;
|
||||
onCreateEmpty: () => void;
|
||||
onCreatePlan: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -1085,20 +926,16 @@ function DashboardHome({
|
||||
</div>
|
||||
|
||||
{plans.length === 0 ? (
|
||||
/* Genau EIN Weg hinein. Bis 0.35 standen hier drei Knöpfe (geführt / Beispielplan /
|
||||
leer) -- eine Wahl, die niemand treffen kann, der das Tool noch nicht kennt. Der
|
||||
Assistent führt jetzt IM Plan statt davor; die Tour zeigt vorab den Aufbau. */
|
||||
<EmptyState
|
||||
icon={<Sparkles className="h-6 w-6" />}
|
||||
title="Deine erste Finanzplanung"
|
||||
text="Am schnellsten geht es geführt: Der Assistent fragt dich Schritt für Schritt und baut dein Grundgerüst – in rund fünf Minuten. Oder schau dir zuerst den Beispielplan an."
|
||||
text="Ein paar Angaben zu dir – danach zeigt dir eine kurze Tour, wie FPT aufgebaut ist, und der Assistent führt dich Schritt für Schritt durch die Planung."
|
||||
>
|
||||
<Button onClick={onCreateGuided}>
|
||||
<Sparkles className="h-4 w-4" /> Geführt starten
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onCreateDemo} disabled={creatingDemo}>
|
||||
<PiggyBank className="h-4 w-4" /> {creatingDemo ? "Wird angelegt…" : "Beispielplan ansehen"}
|
||||
</Button>
|
||||
{/* Dritter Weg -- sonst gaebe es "Leer starten" nur ueber das Plus in der Seitenleiste. */}
|
||||
<Button variant="ghost" onClick={onCreateEmpty}>
|
||||
<Plus className="h-4 w-4" /> Leer starten
|
||||
<Button onClick={onCreatePlan}>
|
||||
<Plus className="h-4 w-4" /> Meinen ersten Finanzplan anlegen
|
||||
</Button>
|
||||
</EmptyState>
|
||||
) : (
|
||||
@@ -1151,7 +988,7 @@ function DashboardHome({
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateChooser}
|
||||
onClick={onCreatePlan}
|
||||
className="flex min-h-20 items-center justify-center gap-2 rounded-xl border border-dashed border-accent bg-accent-soft text-sm font-medium text-accent-soft-fg transition-colors hover:bg-accent-soft"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user