"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { BarChart3, ChevronDown, ChevronRight, Copy, FileText, FileSpreadsheet, FolderKanban, CalendarClock, GitBranch, Layers, LayoutDashboard, Menu, Pin, PinOff, PiggyBank, Plus, Search, SlidersHorizontal, Sparkles, Trash2, X, } from "lucide-react"; import { PlanView } from "@/components/PlanView"; import { Dashboard } from "@/components/Dashboard"; import { MonteCarloDialog } from "@/components/MonteCarloDialog"; import { SensitivityDialog } from "@/components/SensitivityDialog"; import { LiveSimDialog } from "@/components/LiveSimDialog"; import { ActualsDialog } from "@/components/ActualsDialog"; import { PlanDashboardView, ScenarioListView, AnalysesView } from "@/components/PlanViews"; import { SavedAnalysisView } from "@/components/SavedAnalysisView"; import { ReportsView } from "@/components/ReportsView"; import { buildViews } from "@/lib/dataview"; import type { ActualsSetInput, ElementOrigin } from "@/lib/actuals"; import { VersionHistoryDialog } from "@/components/VersionHistoryDialog"; import { SpecView } from "@/components/SpecView"; import { SystemParametersView } from "@/components/SystemParametersView"; import { PlanTraceDialog } from "@/components/DetailView"; import { computePlan } from "@/lib/calculations"; import { ProfileMenu } from "@/components/ProfileMenu"; import { Tour, TOUR_DONE_KEY } from "@/components/Tour";import { CommandPalette, type PaletteAction } from "@/components/CommandPalette"; import { PlanProfileFields, emptyProfileDraft, type ProfileDraft } from "@/components/PlanProfileFields"; import { Button, ConfirmProvider, EmptyState, Modal, PlanSkeleton, ToastProvider, useConfirm, useToast, } from "@/components/ui"; // Angepinnte Menueleiste: eine Arbeitsgewohnheit des Benutzers, keine Eigenschaft des Plans. const RAIL_PINNED_KEY = "fpt.rail.pinned"; import { api } from "@/lib/api-client"; import { computeScenarioDiff } from "@/lib/diff"; import type { PlanInput, PlanListItem, ScenarioMeta } from "@/lib/types"; import type { PlanComputed } from "@/lib/calculations"; interface ScenarioDetail { plan: PlanInput; // das Szenario selbst (berechenbare Einheit) computed: PlanComputed; base: PlanInput | null; // Eltern-Szenario als Vergleichsbasis meta: ScenarioMeta & { planName: string }; // Effektive Werte des PLANS plus die Element-Herkunft aller Szenarien -- daraus entsteht // im Browser der zweite Rechenlauf (siehe lib/dataview.ts). actuals: ActualsSetInput[]; elementOrigins: ElementOrigin[]; } // Die Provider (Toast, Bestätigung) müssen UM die Shell liegen, damit deren Hooks // innerhalb funktionieren. export function AppShell({ username }: { username: string }) { return ( ); } function AppShellInner({ username }: { username: string }) { const toast = useToast(); const confirm = useConfirm(); const [plans, setPlans] = useState([]); const [selectedScenarioId, setSelectedScenarioId] = useState(null); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(false); const [showEmptyPlanDialog, setShowEmptyPlanDialog] = useState(false); const [copyFrom, setCopyFrom] = useState(null); const [showSpec, setShowSpec] = useState(false); const [showCharts, setShowCharts] = useState(false); const [showMonteCarlo, setShowMonteCarlo] = useState(false); const [showSensitivity, setShowSensitivity] = useState(false); const [showLiveSim, setShowLiveSim] = useState(false); const [showHistory, setShowHistory] = useState(false); const [showActuals, setShowActuals] = useState(false); // Plan-Ebene: Dashboard / Szenarien-Liste / Analysen. Null = kein Plan-View aktiv. const [planNav, setPlanNav] = useState<{ planId: string; tab: "dashboard" | "scenarios" | "actuals" | "analyses" | "reports" } | null>(null); const [savedAnalysisId, setSavedAnalysisId] = useState(null); const [showSystemParams, setShowSystemParams] = useState(false); const [showPlanTraces, setShowPlanTraces] = useState(false); const [showPalette, setShowPalette] = useState(false); // Sprungmarke in die SPEZIFIKATION, gesetzt aus einem Rechenweg heraus. const [specAnchor, setSpecAnchor] = useState(null); // Erhoehen erzwingt ein Neuladen der Szenario-Liste (bleibt bei einer Loeschung montiert). const [scenarioListKey, setScenarioListKey] = useState(0); // Menueleiste links: eingeklappt auf eine Icon-Schiene (56 px) statt 240 px. // // Beim Ueberfahren faehrt sie aus und SCHIEBT den Inhalt -- sie legt sich nicht darueber. // Der erste Anlauf (0.42) liess sie als Overlay ausfahren, um im Ruhezustand keinen Platz // zu kosten; dabei verschwand aber jedes Mal ein Streifen des Inhalts, und das las sich wie // ein Fehler. Aufgeklappt sieht es damit genau so aus wie angepinnt, und das Anpinnen // entscheidet nur noch, ob es so BLEIBT. Die Wahl gehoert zum Benutzer und ueberlebt die // Sitzung. const [railPinned, setRailPinned] = useState(false); const [railHover, setRailHover] = useState(false); const [planMenuOpen, setPlanMenuOpen] = useState(false); useEffect(() => { try { // eslint-disable-next-line react-hooks/set-state-in-effect -- einmalige Initialisierung if (localStorage.getItem(RAIL_PINNED_KEY) === "1") setRailPinned(true); } catch { /* localStorage nicht verfuegbar */ } }, []); function toggleRailPin() { setRailPinned((v) => { try { localStorage.setItem(RAIL_PINNED_KEY, v ? "0" : "1"); } catch { /* ignorieren */ } return !v; }); } const railWide = railPinned || railHover; // 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>(new Set()); useEffect(() => { // Bis 0.39 stand hier zusätzlich `plan.phases.length === 0`: Die alte Spotlight-Tour // beleuchtete echte DOM-Ziele in der Matrix und hatte ohne Phasen nichts zu zeigen. Die // Tour ist seit 0.36 eine Attrappe mit erfundenen Zahlen -- sie braucht nichts, und sie // gehört genau an den Anfang, wo man noch gar nicht weiss, wie der Plan aufgebaut ist. if (!detail) 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); setShowSystemParams(false); setSelectedScenarioId(null); setPlanNav(null); } const loadPlans = useCallback(async () => { const data = await api.get<{ plans: PlanListItem[] }>("/api/plans"); setPlans(data.plans); return data.plans; }, []); // silent = Hintergrund-Refresh ohne Loading-Umschaltung: die PlanView bleibt montiert, // damit die Scrollposition (z. B. nach dem Schliessen eines Panels) erhalten bleibt. const loadDetail = useCallback(async (scenarioId: string, silent = false) => { if (!silent) setLoading(true); try { const d = await api.get(`/api/scenarios/${scenarioId}`); setDetail(d); return d; } finally { if (!silent) setLoading(false); } }, []); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- asynchroner Datenabruf beim Mount loadPlans().finally(() => setLoading(false)); }, [loadPlans]); useEffect(() => { if (selectedScenarioId) { // eslint-disable-next-line react-hooks/set-state-in-effect -- asynchroner Datenabruf loadDetail(selectedScenarioId); } // Kein `else setDetail(null)`: In der Plan-Ebene (planNav) wird `detail` fürs Basisszenario // geladen, damit die Werkzeuge starten können -- das darf hier nicht weggeräumt werden. }, [selectedScenarioId, loadDetail]); // Cmd/Ctrl+K öffnet die Befehls-Palette. useEffect(() => { function onKey(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setShowPalette((v) => !v); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); function refreshCurrent() { if (selectedScenarioId) loadDetail(selectedScenarioId, true); } function openScenario(id: string) { setSelectedScenarioId(id); setPlanNav(null); setShowSpec(false); setShowSystemParams(false); setSidebarOpen(false); } // Plan-Ebene öffnen (Dashboard / Szenarien / Analysen). Räumt die Szenario- und Wissens- // Ansichten weg -- es kann immer nur eine Hauptansicht aktiv sein. function openPlanTab(planId: string, tab: "dashboard" | "scenarios" | "actuals" | "analyses" | "reports") { setPlanNav({ planId, tab }); setSelectedScenarioId(null); setShowSpec(false); setShowSystemParams(false); setSidebarOpen(false); } // Ein Analysewerkzeug aus der Analysen-Ansicht heraus starten. Die Werkzeuge arbeiten auf // einem Szenario -- ohne ausgewähltes nehmen wir das Basisszenario; im Werkzeug selbst // lässt sich Szenario/Version weiterhin umstellen. async function launchTool(planId: string, kind: "CHART" | "MONTE_CARLO" | "SENSITIVITY" | "LIVESIM") { const plan = plans.find((p) => p.id === planId); const baseId = plan?.scenarios.find((s) => s.isBase)?.id ?? plan?.scenarios[0]?.id; if (!baseId) return; if (detail?.meta.id !== baseId) await loadDetail(baseId, true); if (kind === "CHART") setShowCharts(true); else if (kind === "MONTE_CARLO") setShowMonteCarlo(true); else if (kind === "SENSITIVITY") setShowSensitivity(true); else setShowLiveSim(true); } // Effektive-Werte-Ansicht (Plan-Ebene, eingebettet). Lädt das Basisszenario, das der // eingebettete Dialog als Ausgangspunkt braucht, und öffnet dann den Tab. async function openActualsTab(planId: string) { const plan = plans.find((p) => p.id === planId); const baseId = plan?.scenarios.find((s) => s.isBase)?.id ?? plan?.scenarios[0]?.id; if (baseId && detail?.meta.id !== baseId) await loadDetail(baseId, true); openPlanTab(planId, "actuals"); } async function handleDeletePlan(id: string) { const plan = plans.find((p) => p.id === id); const ok = await confirm({ title: "Plan löschen?", message: `«${plan?.name ?? "Plan"}» wird mit allen Szenarien unwiderruflich gelöscht.`, confirmLabel: "Endgültig löschen", danger: true, }); if (!ok) return; await api.delete(`/api/plans/${id}`); toast("success", "Plan gelöscht."); const rest = await loadPlans(); if (!rest.some((p) => p.scenarios.some((s) => s.id === selectedScenarioId))) { setSelectedScenarioId(null); } } async function handleDeleteScenario(s: ScenarioMeta) { const ok = await confirm({ title: "Szenario löschen?", message: `«${s.name}» wird unwiderruflich gelöscht. Untergeordnete Szenarien bleiben bestehen.`, confirmLabel: "Endgültig löschen", danger: true, }); if (!ok) return; try { await api.delete(`/api/scenarios/${s.id}`); toast("success", "Szenario gelöscht."); } catch (e) { toast("error", e instanceof Error ? e.message : "Löschen fehlgeschlagen."); return; } await loadPlans(); // Die Szenario-Liste bleibt beim Löschen aus der Seitenleiste montiert -- ohne dieses // Signal zeigte sie den gelöschten Eintrag weiter. setScenarioListKey((k) => k + 1); if (selectedScenarioId === s.id) setSelectedScenarioId(null); } const activePlan = plans.find((p) => p.scenarios.some((s) => s.id === selectedScenarioId)) ?? (planNav ? plans.find((p) => p.id === planNav.planId) ?? null : null); // Plan-Sicht und Ist-Sicht in einem Zug. Ohne erfasste Ist-Werte bleibt `actual` null und // die Oberflaeche verhaelt sich exakt wie bisher. const views = useMemo( () => detail ? buildViews(detail.plan, detail.actuals ?? [], detail.elementOrigins ?? []) : null, [detail] ); // Aktionen der Befehls-Palette -- kontextabhängig (Analysen nur bei offenem Szenario). const paletteActions = useMemo(() => { const base: PaletteAction[] = [ { 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); } }, ]; if (detail && selectedScenarioId) { base.unshift( { id: "a-charts", label: "Grafiken öffnen", hint: "Aktion", run: () => setShowCharts(true) }, { id: "a-mc", label: "Monte-Carlo-Simulation", hint: "Aktion", run: () => setShowMonteCarlo(true) }, { id: "a-sens", label: "Einflussfaktoren berechnen", hint: "Aktion", run: () => setShowSensitivity(true) }, { id: "a-traces", label: "Rechenwege anzeigen", hint: "Aktion", run: () => setShowPlanTraces(true) }, { id: "a-copy", label: "Neues Szenario aus diesem", hint: "Aktion", run: () => setCopyFrom(detail.meta) } ); } return base; }, [detail, selectedScenarioId]); const diff = detail ? computeScenarioDiff(detail.plan, detail.base) : null; return (
{/* Linke Leiste. Eingeklappt eine Icon-Schiene; beim Ueberfahren faehrt sie als Overlay aus (kostet keinen Platz), angepinnt bleibt sie stehen und schiebt den Inhalt. Icon-only braucht zwingend Tooltip UND aria-label -- sonst ist die Schiene fuer Screenreader und fuer Neulinge gleichermassen stumm. */} {/* Linke Leiste. EINE Komponente in zwei Breiten: Die Symbole bleiben an Ort und Stelle, rechts daneben faehrt die Beschriftung aus. Ein Wechsel der Darstellung (Icons hier, Baum dort) waere kein Uebergang, sondern ein Sprung -- und genau das hat sich falsch angefuehlt. */}
setRailHover(true)} onMouseLeave={() => { setRailHover(false); setPlanMenuOpen(false); }} > setPlanMenuOpen((o) => !o)} onNewPlan={() => { setPlanMenuOpen(false); setShowEmptyPlanDialog(true); }} onDeletePlan={handleDeletePlan} onHome={() => { setSelectedScenarioId(null); setPlanNav(null); setShowSpec(false); setShowSystemParams(false); }} onOpenTab={(planId, tab) => { setPlanMenuOpen(false); if (tab === "actuals") void openActualsTab(planId); else openPlanTab(planId, tab); }} onOpenScenario={openScenario} onCopyScenario={setCopyFrom} onDeleteScenario={handleDeleteScenario} onSpec={() => { setShowSpec(true); setSpecAnchor(null); setShowSystemParams(false); setSelectedScenarioId(null); setPlanNav(null); }} onParams={() => { setShowSystemParams(true); setShowSpec(false); setSelectedScenarioId(null); setPlanNav(null); }} onSearch={() => setShowPalette(true)} />
{sidebarOpen && (
setSidebarOpen(false)} />
)}

{showSystemParams ? "Systemparameter" : showSpec ? "So rechnet FPT" : planNav ? `${activePlan?.name ?? "Plan"} · ${ { dashboard: "Dashboard", scenarios: "Szenarien", actuals: "Effektive Werte", analyses: "Analysen", reports: "Berichte" }[ planNav.tab ] }` : selectedScenarioId && detail ? `${detail.meta.planName} · ${detail.meta.name}` : "Übersicht"}

{showSystemParams && } {showSpec && } {!showSpec && !showSystemParams && loading && !planNav && } {/* Plan-Ebene: Dashboard / Szenarien / Analysen */} {!showSpec && !showSystemParams && planNav?.tab === "dashboard" && ( (tab === "actuals" ? void openActualsTab(planNav.planId) : openPlanTab(planNav.planId, tab))} onRenamed={() => void loadPlans()} /> )} {!showSpec && !showSystemParams && planNav?.tab === "scenarios" && ( { void loadDetail(sid, true).then(() => setShowHistory(true)); }} onCopyFrom={(sid) => { const src = activePlan?.scenarios.find((s) => s.id === sid); if (src) setCopyFrom(src); }} onDelete={(sid) => { const s = activePlan?.scenarios.find((x) => x.id === sid); if (s) void handleDeleteScenario(s); }} /> )} {!showSpec && !showSystemParams && planNav?.tab === "actuals" && activePlan && ( detail && detail.meta.planId === planNav.planId ? ( {}} onChanged={refreshCurrent} /> ) : (

Wird geladen…

) )} {!showSpec && !showSystemParams && planNav?.tab === "analyses" && ( void launchTool(planNav.planId, kind)} onOpenSaved={(id) => setSavedAnalysisId(id)} /> )} {!showSpec && !showSystemParams && planNav?.tab === "reports" && activePlan && ( ({ id: s.id, name: s.name, isBase: s.isBase }))} hasActuals={(activePlan._count?.actuals ?? 0) > 0} /> )} {!showSpec && !showSystemParams && !loading && !planNav && selectedScenarioId === null && ( openPlanTab(id, "dashboard")} onCreatePlan={() => setShowEmptyPlanDialog(true)} onDelete={handleDeletePlan} /> )} {!showSpec && !showSystemParams && !loading && detail && selectedScenarioId && (
{/* 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)) && (
{!detail.meta.isBase && ( )} {diff && detail.base && ( {diff.total === 0 ? "Unverändert gegenüber der Vorlage" : `${diff.total} Abweichung${diff.total === 1 ? "" : "en"} gegenüber der Vorlage`} )}
)} setShowSensitivity(true)} onOpenHistory={() => setShowHistory(true)} onStartTour={() => setShowTour(true)} onCopyScenario={() => setCopyFrom(detail.meta)} onOpenTraces={() => setShowPlanTraces(true)} exportHref={`/api/scenarios/${detail.meta.id}/export`} />
)}
{showEmptyPlanDialog && ( { const { scenario } = await api.post<{ plan: { id: string }; scenario: { id: string } }>( "/api/plans", { name, ...profile } ); setShowEmptyPlanDialog(false); await loadPlans(); pendingTourRef.current = true; // Tour bei jeder Plan-Erstellung (F14) openScenario(scenario.id); toast("success", "Plan erstellt – die Tour zeigt dir gleich die wichtigsten Stellen."); }} onClose={() => setShowEmptyPlanDialog(false)} /> )} {showCharts && detail && ( setShowCharts(false)} title={`Grafiken · ${detail.meta.name}`}> s.id !== detail.meta.id)} actuals={detail.actuals ?? []} origins={detail.elementOrigins ?? []} /> )} {showMonteCarlo && detail && ( setShowMonteCarlo(false)} /> )} {showSensitivity && detail && ( setShowSensitivity(false)} /> )} {showLiveSim && detail && ( setShowLiveSim(false)} /> )} {savedAnalysisId && planNav && ( setSavedAnalysisId(null)} /> )} {showActuals && detail && activePlan && ( setShowActuals(false)} onChanged={refreshCurrent} /> )} {showHistory && detail && ( setShowHistory(false)} onRestored={refreshCurrent} /> )} {showPlanTraces && detail && ( setShowPlanTraces(false)} onOpenSpec={openSpecAt} /> )} {copyFrom && ( setCopyFrom(null)} onCreate={async (name) => { const { scenarioId } = await api.post<{ scenarioId: string }>( `/api/scenarios/${copyFrom.id}/copy`, { name } ); setCopyFrom(null); await loadPlans(); openScenario(scenarioId); toast("success", "Szenario erstellt."); }} /> )} setShowPalette(false)} plans={plans} actions={paletteActions} onOpenScenario={openScenario} /> {showTour && setShowTour(false)} />}
); } // Breiter Dialog für den Analyse-Bereich (Grafiken). function ChartsDialog({ title, children, onClose, }: { title: string; children: React.ReactNode; onClose: () => void; }) { return (
e.stopPropagation()} className="ui-pop flex w-full max-w-5xl flex-col gap-4 rounded-2xl border border-border bg-surface p-6 shadow-xl" >

{title}

{children}
); } // Rekursiver Szenario-Baum: Kinder werden eingerückt, damit Sub-Szenarien sichtbar sind. // Die Aktions-Icons sind immer leicht sichtbar (nicht nur bei Hover) -- auf Touch-Geräten // gibt es kein Hover. function ScenarioTree({ scenarios, selectedId, onSelect, onCopy, onDelete, }: { scenarios: ScenarioMeta[]; selectedId: string | null; onSelect: (id: string) => void; onCopy: (s: ScenarioMeta) => void; onDelete: (s: ScenarioMeta) => void; }) { // Echte Verschachtelung: Ein Szenario wird unter seiner Vorlage eingerückt (Tiefe = Länge // der Herkunftskette). So ist in der Seitenleiste sofort sichtbar, woraus ein Szenario // entstanden ist -- die Szenario-Liste zeigt dasselbe zusätzlich als Spalte «aus …». const byId = new Map(scenarios.map((s) => [s.id, s])); const depthOf = (s: ScenarioMeta): number => { let d = 0; let cur = s.parentScenarioId ? byId.get(s.parentScenarioId) : undefined; // Deckel gegen eine (theoretisch) zyklische Kette. while (cur && d < 8) { d += 1; cur = cur.parentScenarioId ? byId.get(cur.parentScenarioId) : undefined; } return d; }; // Vorordnung: Basis zuoberst, Kinder direkt unter ihrer Vorlage. const ordered: ScenarioMeta[] = []; const visit = (parentId: string | null) => { for (const s of scenarios) { const pid = s.isBase ? null : s.parentScenarioId; if ((pid ?? null) !== parentId) continue; ordered.push(s); visit(s.id); } }; visit(null); // Szenarien, deren Vorlage gelöscht wurde, hängen sonst nirgends -- ans Ende. for (const s of scenarios) if (!ordered.includes(s)) ordered.push(s); if (ordered.length === 0) return null; return ( <> {ordered.map((s) => (
{!s.isBase && ( )}
))} ); } // Startansicht: Begrüssung + Plan-Kacheln (Klick öffnet das Basisszenario). function DashboardHome({ username, plans, onOpenPlan, onCreatePlan, onDelete, }: { username: string; plans: PlanListItem[]; onOpenPlan: (planId: string) => void; onCreatePlan: () => void; onDelete: (id: string) => void; }) { return (

Willkommen, {username}

Wähle einen Plan oder erstelle einen neuen. Jeder Plan enthält ein Basisszenario und beliebig viele Varianten davon.

{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. */ } title="Deine erste Finanzplanung" 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." > ) : (
{plans.map((p) => { const others = p.scenarios.length - 1; const persons = (p.persons ?? []) .map((pe) => pe.name || (pe.role === "PERSON_A" ? "Person A" : "Person B")) .join(" · "); return (
onOpenPlan(p.id)} >
{p.name}
{persons || "Basisszenario"} {others > 0 ? ` · +${others} Variante${others === 1 ? "" : "n"}` : ""}
{/* Ein paar Kennzahlen pro Plan direkt in der Übersicht. */}
{p.scenarios.length} Szenario{p.scenarios.length === 1 ? "" : "en"} {p.startYear && ab {p.startYear}} {p._count && p._count.actuals > 0 && {p._count.actuals} {p._count.actuals === 1 ? "Ist-Datensatz" : "Ist-Datensätze"}} {p._count && p._count.analyses > 0 && {p._count.analyses} Analyse{p._count.analyses === 1 ? "" : "n"}}
); })}
)}
); } function PlanDialog({ onCreate, onClose, }: { onCreate: (name: string, profile: ProfileDraft) => void; onClose: () => void; }) { const [name, setName] = useState("Meine Planung"); const [draft, setDraft] = useState(emptyProfileDraft); const [saving, setSaving] = useState(false); return (

Es wird automatisch ein Basisszenario angelegt. Weitere Szenarien entstehen später als Kopien davon.

setName(e.target.value)} placeholder="Name des Plans" />
); } function CopyScenarioDialog({ source, onCreate, onClose, }: { source: ScenarioMeta; onCreate: (name: string) => void; onClose: () => void; }) { const [name, setName] = useState(`${source.name} – Variante`); const [saving, setSaving] = useState(false); return (

Vollständige Kopie von {source.name}. Änderungen darin werden anschliessend farblich hervorgehoben.

setName(e.target.value)} placeholder="Name des Szenarios" />
); } // --- Die linke Leiste ------------------------------------------------------------------------ // // EINE Komponente in zwei Breiten. Die Symbole stehen immer an derselben Stelle -- in einer // festen 3.5-rem-Spalte --, rechts daneben faehrt die Beschriftung aus. Bis 0.42 wechselte die // Leiste beim Ueberfahren die Darstellung: eingeklappt Icons, ausgefahren ein Plan-Baum. Das // war kein Uebergang, sondern ein Sprung, und man verlor jedes Mal die Orientierung. // // Der Preis ist der Verlust des Baums ALLER Plaene. Er wird nicht vermisst: Der Plan wird ueber // den Waehler oben gewechselt, und die vier Bereiche darunter beziehen sich immer auf den // AKTIVEN Plan. Statt einer Liste von Listen bleiben vier Knoepfe. function Rail({ wide, pinned, mobile, onTogglePin, plans, activeScenarioId, planNav, atHome, showSpec, showSystemParams, planMenuOpen, onTogglePlanMenu, onNewPlan, onDeletePlan, onHome, onOpenTab, onOpenScenario, onCopyScenario, onDeleteScenario, onSpec, onParams, onSearch, }: { wide: boolean; pinned: boolean; mobile?: boolean; onTogglePin: () => void; plans: PlanListItem[]; activeScenarioId: string | null; planNav: { planId: string; tab: string } | null; atHome: boolean; showSpec: boolean; showSystemParams: boolean; planMenuOpen: boolean; onTogglePlanMenu: () => void; onNewPlan: () => void; onDeletePlan: (planId: string) => void; onHome: () => void; onOpenTab: (planId: string, tab: "dashboard" | "scenarios" | "actuals" | "analyses" | "reports") => void; onOpenScenario: (scenarioId: string) => void; onCopyScenario: (meta: ScenarioMeta) => void; onDeleteScenario: (s: ScenarioMeta) => void; onSpec: () => void; onParams: () => void; onSearch: () => void; }) { // Der Szenario-Baum haengt unter «Szenarien» und braucht Text -- eingeklappt gibt es ihn // nicht. Standard: zu, sonst waere die Leiste beim Ueberfahren sofort lang. const [treeOpen, setTreeOpen] = useState(false); // Welcher Plan ist im Zugriff? Erst der offene, sonst der zuletzt gewaehlte Bereich, sonst // der einzige. Ohne diesen Bezug waeren die vier Knoepfe darunter gegenstandslos. const active = plans.find((p) => p.scenarios.some((sc) => sc.id === activeScenarioId)) ?? plans.find((p) => p.id === planNav?.planId) ?? (plans.length === 1 ? plans[0] : undefined); const initials = active ? active.name.split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase() : "–"; return ( ); } // Eine Zeile der Leiste. Das Symbol sitzt in einer FESTEN Spalte von 3.5 rem -- dadurch bleibt // es beim Ausfahren exakt an seinem Platz, und es kommt nur die Beschriftung dazu. function RailRow({ wide, icon, iconBox, label, shortLabel, onClick, active, disabled, trailing, }: { wide: boolean; icon: React.ReactNode; iconBox?: string; label: string; shortLabel?: string; onClick?: () => void; active?: boolean; disabled?: boolean; trailing?: React.ReactNode; }) { const cls = `flex w-full items-center rounded-lg pr-2 text-left transition-colors ${ disabled ? "opacity-30" : "" } ${active ? "bg-accent-soft text-accent-soft-fg" : onClick ? "text-muted hover:bg-surface-2 hover:text-fg" : "text-fg"}`; const inner = ( <> {icon} {wide && ( <> {shortLabel ?? label} {trailing} )} ); if (!onClick) return
{inner}
; return ( ); }