UI-Gesamtumbau: Du-Form, Inspector-Panel, Onboarding-Assistent, Tour, Palette
Deploy App / deploy (push) Successful in 1m1s
Deploy App / deploy (push) Successful in 1m1s
Rein an der Oberflaeche -- Berechnung, Datenmodell und API-Semantik unveraendert (103 Tests unveraendert gruen). Paket A (Fundament): - durchgehend Du-Form und echte Umlaute in allen sichtbaren Texten, inkl. API-Fehlermeldungen (vorher Mix aus Sie/Du und ae/oe/ue) - neue UI-Primitiven (ui.tsx): Button, Modal mit ESC/Fokus-Falle/Animation, Bestaetigungs-Dialog statt window.confirm, Toasts statt alert, Skeleton-Loader, EmptyState - eigene Attention-Farbe (Amber) fuer offene Entscheide, getrennt vom Akzent - Micro-Interactions mit prefers-reduced-motion-Fallback Paket B (Onboarding, Roadmap Nr. 10): - gefuehrter Plan-Assistent in 5 Schritten; Einkommen bewusst pro Person (raeumt die 9.9-AHV-Falle aus); reine Orchestrierung bestehender Endpunkte - Beispielplan mit einem Klick; Uebergaenge absichtlich offen - interaktive Tour ueber die Planansicht (localStorage, jederzeit neu startbar) - abgeleitete "Naechste Schritte"-Karte (offene Entscheide, fehlende Elemente, fehlende Pensionsphase, Ruin -> Einflussfaktoren) Paket C (Struktur): - Inspector-Panel rechts statt Modals fuer alle Einzel-Bearbeitungen; Matrix bleibt sichtbar, Zellklick wechselt den Inhalt - Phasenkopf auf vier Kern-Infos entschlackt (Rest in der 0.11-Detailansicht) - Matrix mit eigenem Scrollbereich, Koepfe beidachsig fixiert - Sidebar-Gruppen "Meine Plaene" / "Wissen"; "So rechnet FPT" statt SPEZIFIKATION; "Szenario-Profil" statt "Plan-Einstellungen" - Aktions-Icons ohne Hover sichtbar (Touch) Paket D (Extras): - Sparklines je Element-Zeile aus den 0.11-Verlaufswerten - Befehls-Palette (Ctrl/Cmd+K) - Ruin-Banner verlinkt auf die Einflussfaktoren Nebenbei: der ProfileMenu-Lint-Fehler und der Selection-Rest (9.17) sind behoben -- npm run lint laeuft erstmals fehlerfrei. SPEZIFIKATION auf 0.13: neue Kapitel 3.2.8, 3.7.6-3.7.9, 9.23, 9.24; 3.6.3 und 3.7.1 ueberarbeitet, 9.17 bereinigt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+411
-218
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
BarChart3,
|
||||
BookOpen,
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
Menu,
|
||||
PiggyBank,
|
||||
Plus,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Tornado,
|
||||
Trash2,
|
||||
X,
|
||||
@@ -27,8 +29,21 @@ 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 { 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";
|
||||
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";
|
||||
@@ -40,13 +55,31 @@ interface ScenarioDetail {
|
||||
meta: ScenarioMeta & { planName: string };
|
||||
}
|
||||
|
||||
// Die Provider (Toast, Bestätigung) müssen UM die Shell liegen, damit deren Hooks
|
||||
// innerhalb funktionieren.
|
||||
export function AppShell({ username }: { username: string }) {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<ConfirmProvider>
|
||||
<AppShellInner username={username} />
|
||||
</ConfirmProvider>
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppShellInner({ username }: { username: string }) {
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const [plans, setPlans] = useState<PlanListItem[]>([]);
|
||||
const [selectedScenarioId, setSelectedScenarioId] = useState<string | null>(null);
|
||||
const [detail, setDetail] = useState<ScenarioDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [showNewPlan, setShowNewPlan] = 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);
|
||||
@@ -54,6 +87,7 @@ export function AppShell({ username }: { username: string }) {
|
||||
const [showSensitivity, setShowSensitivity] = useState(false);
|
||||
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<string | null>(null);
|
||||
|
||||
@@ -71,7 +105,7 @@ export function AppShell({ username }: { username: string }) {
|
||||
}, []);
|
||||
|
||||
// silent = Hintergrund-Refresh ohne Loading-Umschaltung: die PlanView bleibt montiert,
|
||||
// damit die Scrollposition (z. B. nach dem Schliessen eines Popups) erhalten bleibt.
|
||||
// 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 {
|
||||
@@ -95,6 +129,18 @@ export function AppShell({ username }: { username: string }) {
|
||||
}
|
||||
}, [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);
|
||||
}
|
||||
@@ -107,8 +153,16 @@ export function AppShell({ username }: { username: string }) {
|
||||
}
|
||||
|
||||
async function handleDeletePlan(id: string) {
|
||||
if (!confirm("Diesen Plan mit ALLEN Szenarien wirklich loeschen?")) return;
|
||||
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);
|
||||
@@ -116,19 +170,61 @@ export function AppShell({ username }: { username: string }) {
|
||||
}
|
||||
|
||||
async function handleDeleteScenario(s: ScenarioMeta) {
|
||||
if (!confirm(`Szenario "${s.name}" wirklich loeschen?`)) return;
|
||||
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) {
|
||||
alert(e instanceof Error ? e.message : "Loeschen fehlgeschlagen.");
|
||||
toast("error", e instanceof Error ? e.message : "Löschen fehlgeschlagen.");
|
||||
return;
|
||||
}
|
||||
await loadPlans();
|
||||
if (selectedScenarioId === s.id) setSelectedScenarioId(null);
|
||||
}
|
||||
|
||||
async function handleCreateDemo() {
|
||||
setCreatingDemo(true);
|
||||
try {
|
||||
const sid = await createDemoPlan();
|
||||
await loadPlans();
|
||||
setShowNewPlanChooser(false);
|
||||
openScenario(sid);
|
||||
toast("success", "Beispielplan angelegt – schau dich um!");
|
||||
} 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)) ?? null;
|
||||
|
||||
// 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-home", label: "Übersicht öffnen", hint: "Aktion", run: () => { setSelectedScenarioId(null); setShowSpec(false); setShowSystemParams(false); } },
|
||||
{ id: "a-spec", label: "So rechnet FPT (Spezifikation)", hint: "Aktion", run: () => { setShowSpec(true); setSpecAnchor(null); setShowSystemParams(false); setSelectedScenarioId(null); } },
|
||||
{ id: "a-params", label: "Systemparameter", hint: "Aktion", run: () => { setShowSystemParams(true); setShowSpec(false); setSelectedScenarioId(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 sidebar = (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center gap-2 px-4 py-4">
|
||||
@@ -136,6 +232,15 @@ export function AppShell({ username }: { username: string }) {
|
||||
<PiggyBank className="h-5 w-5 text-accent-fg" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-fg">FPT</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPalette(true)}
|
||||
title="Suchen und springen (Ctrl+K)"
|
||||
className="ml-auto flex items-center gap-1 rounded-lg border border-border px-2 py-1 text-[10px] text-faint transition-colors hover:bg-surface-2 hover:text-fg"
|
||||
>
|
||||
<Search className="h-3 w-3" />
|
||||
Ctrl K
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-1 flex-col gap-1 overflow-y-auto px-3 pb-4">
|
||||
@@ -147,26 +252,26 @@ export function AppShell({ username }: { username: string }) {
|
||||
setShowSystemParams(false);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium ${
|
||||
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
||||
selectedScenarioId === null && !showSpec && !showSystemParams ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Uebersicht
|
||||
Übersicht
|
||||
</button>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between px-3">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-faint">Plaene</span>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-faint">Meine Pläne</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewPlan(true)}
|
||||
onClick={() => setShowNewPlanChooser(true)}
|
||||
aria-label="Neuen Plan erstellen"
|
||||
className="rounded-md p-1 text-accent hover:bg-accent-soft"
|
||||
className="rounded-md p-1 text-accent transition-colors hover:bg-accent-soft"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{plans.length === 0 && <p className="px-3 py-2 text-xs text-faint">Noch keine Plaene.</p>}
|
||||
{plans.length === 0 && <p className="px-3 py-2 text-xs text-faint">Noch keine Pläne.</p>}
|
||||
|
||||
{plans.map((p) => (
|
||||
<div key={p.id} className="mb-1">
|
||||
@@ -175,9 +280,9 @@ export function AppShell({ username }: { username: string }) {
|
||||
<span className="min-w-0 flex-1 truncate" title={p.name}>{p.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Plan loeschen"
|
||||
aria-label="Plan löschen"
|
||||
onClick={() => handleDeletePlan(p.id)}
|
||||
className="rounded p-0.5 text-faint opacity-0 hover:bg-danger-soft hover:text-danger group-hover:opacity-100"
|
||||
className="rounded p-0.5 text-faint opacity-60 transition-opacity hover:bg-danger-soft hover:text-danger group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -195,21 +300,7 @@ export function AppShell({ username }: { username: string }) {
|
||||
))}
|
||||
|
||||
<div className="mt-4 flex flex-col gap-1 border-t border-border pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowSystemParams(true);
|
||||
setShowSpec(false);
|
||||
setSelectedScenarioId(null);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium ${
|
||||
showSystemParams ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4 shrink-0" />
|
||||
Systemparameter
|
||||
</button>
|
||||
<span className="px-3 pb-1 text-[11px] font-semibold uppercase tracking-wide text-faint">Wissen</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -219,12 +310,27 @@ export function AppShell({ username }: { username: string }) {
|
||||
setSelectedScenarioId(null);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium ${
|
||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors ${
|
||||
showSpec ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-4 w-4 shrink-0" />
|
||||
SPEZIFIKATION
|
||||
So rechnet FPT
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowSystemParams(true);
|
||||
setShowSpec(false);
|
||||
setSelectedScenarioId(null);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors ${
|
||||
showSystemParams ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4 shrink-0" />
|
||||
Systemparameter
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -239,12 +345,12 @@ export function AppShell({ username }: { username: string }) {
|
||||
|
||||
{sidebarOpen && (
|
||||
<div className="fixed inset-0 z-40 lg:hidden">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={() => setSidebarOpen(false)} />
|
||||
<aside className="absolute left-0 top-0 h-full w-72 border-r border-border bg-surface shadow-xl">
|
||||
<div className="ui-fade absolute inset-0 bg-black/40" onClick={() => setSidebarOpen(false)} />
|
||||
<aside className="ui-slide-in absolute left-0 top-0 h-full w-72 border-r border-border bg-surface shadow-xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
aria-label="Menue schliessen"
|
||||
aria-label="Menü schliessen"
|
||||
className="absolute right-2 top-3 rounded-md p-1.5 text-faint hover:bg-surface-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -259,7 +365,7 @@ export function AppShell({ username }: { username: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
aria-label="Menue oeffnen"
|
||||
aria-label="Menü öffnen"
|
||||
className="rounded-lg border border-border p-2 text-muted lg:hidden"
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
@@ -268,10 +374,10 @@ export function AppShell({ username }: { username: string }) {
|
||||
{showSystemParams
|
||||
? "Systemparameter"
|
||||
: showSpec
|
||||
? "Spezifikation"
|
||||
? "So rechnet FPT"
|
||||
: detail
|
||||
? `${detail.meta.planName} · ${detail.meta.name}`
|
||||
: "Uebersicht"}
|
||||
: "Übersicht"}
|
||||
</h1>
|
||||
<ProfileMenu username={username} />
|
||||
</header>
|
||||
@@ -281,82 +387,64 @@ export function AppShell({ username }: { username: string }) {
|
||||
|
||||
{showSpec && <SpecView anchor={specAnchor} />}
|
||||
|
||||
{!showSpec && !showSystemParams && loading && <p className="text-sm text-muted">Laedt…</p>}
|
||||
{!showSpec && !showSystemParams && loading && <PlanSkeleton />}
|
||||
|
||||
{!showSpec && !showSystemParams && !loading && selectedScenarioId === null && (
|
||||
<DashboardHome
|
||||
username={username}
|
||||
plans={plans}
|
||||
creatingDemo={creatingDemo}
|
||||
onSelect={openScenario}
|
||||
onCreate={() => setShowNewPlan(true)}
|
||||
onCreateGuided={() => setShowWizard(true)}
|
||||
onCreateChooser={() => setShowNewPlanChooser(true)}
|
||||
onCreateDemo={handleCreateDemo}
|
||||
onDelete={handleDeletePlan}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showSpec && !showSystemParams && !loading && detail && selectedScenarioId && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCopyFrom(detail.meta)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2" data-tour="analysen">
|
||||
<Button variant="secondary" onClick={() => setCopyFrom(detail.meta)}>
|
||||
<Copy className="h-4 w-4" />
|
||||
Neues Szenario aus diesem
|
||||
</button>
|
||||
</Button>
|
||||
{!detail.meta.isBase && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteScenario(detail.meta)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-muted hover:border-danger hover:bg-danger-soft hover:text-danger"
|
||||
>
|
||||
<Button variant="danger" onClick={() => handleDeleteScenario(detail.meta)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Szenario loeschen
|
||||
</button>
|
||||
Szenario löschen
|
||||
</Button>
|
||||
)}
|
||||
{detail.plan.phases.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCharts(true)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
<Button variant="secondary" onClick={() => setShowCharts(true)}>
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
Grafiken
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMonteCarlo(true)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowMonteCarlo(true)}>
|
||||
<Dices className="h-4 w-4" />
|
||||
Monte-Carlo-Simulation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSensitivity(true)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowSensitivity(true)}>
|
||||
<Tornado className="h-4 w-4" />
|
||||
Einflussfaktoren berechnen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
Einflussfaktoren
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowPlanTraces(true)}
|
||||
title="Wie wird gerechnet? Plan-weite Grössen wie Deflatoren, AHV-Karriere und Ruinalter"
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Rechenwege
|
||||
</button>
|
||||
</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
|
||||
? "Unveraendert gegenueber der Vorlage"
|
||||
: `${diff.total} Abweichung${diff.total === 1 ? "" : "en"} gegenueber der Vorlage`}
|
||||
? "Unverändert gegenüber der Vorlage"
|
||||
: `${diff.total} Abweichung${diff.total === 1 ? "" : "en"} gegenüber der Vorlage`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -367,24 +455,54 @@ export function AppShell({ username }: { username: string }) {
|
||||
diff={diff}
|
||||
onChanged={refreshCurrent}
|
||||
onOpenSpec={openSpecAt}
|
||||
onOpenSensitivity={() => setShowSensitivity(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{showNewPlan && (
|
||||
{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();
|
||||
openScenario(sid);
|
||||
toast("success", "Plan erstellt – die Tour zeigt dir gleich die wichtigsten Stellen.");
|
||||
}}
|
||||
onClose={() => setShowWizard(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEmptyPlanDialog && (
|
||||
<PlanDialog
|
||||
onCreate={async (name, profile) => {
|
||||
const { scenario } = await api.post<{ plan: { id: string }; scenario: { id: string } }>(
|
||||
"/api/plans",
|
||||
{ name, ...profile }
|
||||
);
|
||||
setShowNewPlan(false);
|
||||
setShowEmptyPlanDialog(false);
|
||||
await loadPlans();
|
||||
openScenario(scenario.id);
|
||||
toast("success", "Plan erstellt.");
|
||||
}}
|
||||
onClose={() => setShowNewPlan(false)}
|
||||
onClose={() => setShowEmptyPlanDialog(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -432,14 +550,90 @@ export function AppShell({ username }: { username: string }) {
|
||||
setCopyFrom(null);
|
||||
await loadPlans();
|
||||
openScenario(scenarioId);
|
||||
toast("success", "Szenario erstellt.");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CommandPalette
|
||||
open={showPalette}
|
||||
onClose={() => setShowPalette(false)}
|
||||
plans={plans}
|
||||
actions={paletteActions}
|
||||
onOpenScenario={openScenario}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Breiter Dialog fuer den Analyse-Bereich (Grafiken).
|
||||
// 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,
|
||||
children,
|
||||
@@ -450,10 +644,10 @@ function ChartsDialog({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 flex items-start justify-center overflow-y-auto bg-black/40 px-4 py-8" onClick={onClose}>
|
||||
<div className="ui-fade fixed inset-0 z-40 flex items-start justify-center overflow-y-auto bg-black/40 px-4 py-8" onClick={onClose}>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex w-full max-w-5xl flex-col gap-4 rounded-2xl border border-border bg-surface p-6 shadow-xl"
|
||||
className="ui-pop flex w-full max-w-5xl flex-col gap-4 rounded-2xl border border-border bg-surface p-6 shadow-xl"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="flex items-center gap-2 text-base font-semibold text-fg">
|
||||
@@ -469,7 +663,9 @@ function ChartsDialog({
|
||||
);
|
||||
}
|
||||
|
||||
// Rekursiver Szenario-Baum: Kinder werden eingerueckt, damit Sub-Szenarien sichtbar sind.
|
||||
// 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,
|
||||
parentId,
|
||||
@@ -494,7 +690,7 @@ function ScenarioTree({
|
||||
{level.map((s) => (
|
||||
<div key={s.id}>
|
||||
<div
|
||||
className={`group flex items-center gap-1.5 rounded-lg py-1.5 pr-1.5 text-sm ${
|
||||
className={`group flex items-center gap-1.5 rounded-lg py-1.5 pr-1.5 text-sm transition-colors ${
|
||||
selectedId === s.id ? "bg-accent-soft font-medium text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
style={{ paddingLeft: `${12 + depth * 14}px` }}
|
||||
@@ -512,16 +708,16 @@ function ScenarioTree({
|
||||
aria-label="Kopie erstellen"
|
||||
title="Neues Szenario aus diesem"
|
||||
onClick={() => onCopy(s)}
|
||||
className="rounded p-0.5 text-faint opacity-0 hover:bg-accent-soft hover:text-accent group-hover:opacity-100"
|
||||
className="rounded p-0.5 text-faint opacity-60 transition-opacity hover:bg-accent-soft hover:text-accent group-hover:opacity-100"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{!s.isBase && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Szenario loeschen"
|
||||
aria-label="Szenario löschen"
|
||||
onClick={() => onDelete(s)}
|
||||
className="rounded p-0.5 text-faint opacity-0 hover:bg-danger-soft hover:text-danger group-hover:opacity-100"
|
||||
className="rounded p-0.5 text-faint opacity-60 transition-opacity hover:bg-danger-soft hover:text-danger group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -542,18 +738,24 @@ function ScenarioTree({
|
||||
);
|
||||
}
|
||||
|
||||
// Startansicht: Begruessung + Plan-Kacheln (Klick oeffnet das Basisszenario).
|
||||
// Startansicht: Begrüssung + Plan-Kacheln (Klick öffnet das Basisszenario).
|
||||
function DashboardHome({
|
||||
username,
|
||||
plans,
|
||||
creatingDemo,
|
||||
onSelect,
|
||||
onCreate,
|
||||
onCreateGuided,
|
||||
onCreateChooser,
|
||||
onCreateDemo,
|
||||
onDelete,
|
||||
}: {
|
||||
username: string;
|
||||
plans: PlanListItem[];
|
||||
creatingDemo: boolean;
|
||||
onSelect: (scenarioId: string) => void;
|
||||
onCreate: () => void;
|
||||
onCreateGuided: () => void;
|
||||
onCreateChooser: () => void;
|
||||
onCreateDemo: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -561,56 +763,71 @@ function DashboardHome({
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-fg">Willkommen, {username}</h2>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
Waehlen Sie einen Plan oder erstellen Sie einen neuen. Jeder Plan enthaelt ein Basisszenario
|
||||
und beliebig viele Varianten davon.
|
||||
Wähle einen Plan oder erstelle einen neuen. Jeder Plan enthält ein Basisszenario und beliebig
|
||||
viele Varianten davon.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{plans.map((p) => {
|
||||
const base = p.scenarios.find((s) => s.isBase) ?? p.scenarios[0];
|
||||
const others = p.scenarios.length - 1;
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="group relative flex cursor-pointer flex-col gap-2 rounded-xl border border-border bg-surface p-4 shadow-sm transition-shadow hover:shadow-md"
|
||||
onClick={() => base && onSelect(base.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-soft">
|
||||
<FolderKanban className="h-5 w-5 text-accent-soft-fg" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-fg">{p.name}</div>
|
||||
<div className="text-xs text-muted">
|
||||
Basisszenario{others > 0 ? ` + ${others} Variante${others === 1 ? "" : "n"}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Plan loeschen"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(p.id);
|
||||
}}
|
||||
className="rounded-md p-1.5 text-faint opacity-0 transition-opacity hover:bg-danger-soft hover:text-danger group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreate}
|
||||
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 hover:bg-accent-soft"
|
||||
{plans.length === 0 ? (
|
||||
<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."
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Neuer Plan
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{plans.map((p) => {
|
||||
const base = p.scenarios.find((s) => s.isBase) ?? p.scenarios[0];
|
||||
const others = p.scenarios.length - 1;
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="group relative flex cursor-pointer flex-col gap-2 rounded-xl border border-border bg-surface p-4 shadow-sm transition-shadow hover:shadow-md"
|
||||
onClick={() => base && onSelect(base.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-soft">
|
||||
<FolderKanban className="h-5 w-5 text-accent-soft-fg" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-fg">{p.name}</div>
|
||||
<div className="text-xs text-muted">
|
||||
Basisszenario{others > 0 ? ` + ${others} Variante${others === 1 ? "" : "n"}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Plan löschen"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(p.id);
|
||||
}}
|
||||
className="rounded-md p-1.5 text-faint opacity-0 transition-opacity hover:bg-danger-soft hover:text-danger group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateChooser}
|
||||
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" />
|
||||
Neuer Plan
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -627,49 +844,37 @@ function PlanDialog({
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 flex items-start justify-center overflow-y-auto bg-black/40 px-4 py-8" onClick={onClose}>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex w-full max-w-md flex-col gap-4 rounded-2xl border border-border bg-surface p-6 shadow-xl"
|
||||
>
|
||||
<h2 className="text-base font-semibold text-fg">Neuen Plan erstellen</h2>
|
||||
<p className="text-xs text-muted">
|
||||
Es wird automatisch ein <strong className="text-fg">Basisszenario</strong> angelegt. Weitere
|
||||
Szenarien entstehen spaeter als Kopien davon.
|
||||
</p>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-muted">Name des Plans</label>
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/25"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name des Plans"
|
||||
/>
|
||||
</div>
|
||||
<PlanProfileFields draft={draft} onChange={setDraft} />
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => {
|
||||
setSaving(true);
|
||||
onCreate(name.trim() || "Plan", draft);
|
||||
}}
|
||||
className="rounded-lg bg-accent px-4 py-2 text-sm font-medium text-accent-fg shadow-sm hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{saving ? "..." : "Erstellen"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-lg border border-border px-4 py-2 text-sm font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
<Modal title="Neuen Plan erstellen" onClose={onClose}>
|
||||
<p className="text-xs text-muted">
|
||||
Es wird automatisch ein <strong className="text-fg">Basisszenario</strong> angelegt. Weitere
|
||||
Szenarien entstehen später als Kopien davon.
|
||||
</p>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-muted">Name des Plans</label>
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/25"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name des Plans"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PlanProfileFields draft={draft} onChange={setDraft} />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
disabled={saving}
|
||||
onClick={() => {
|
||||
setSaving(true);
|
||||
onCreate(name.trim() || "Plan", draft);
|
||||
}}
|
||||
>
|
||||
{saving ? "…" : "Erstellen"}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -685,44 +890,32 @@ function CopyScenarioDialog({
|
||||
const [name, setName] = useState(`${source.name} – Variante`);
|
||||
const [saving, setSaving] = useState(false);
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/40 px-4" onClick={onClose}>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex w-full max-w-sm flex-col gap-3 rounded-2xl border border-border bg-surface p-6 shadow-xl"
|
||||
>
|
||||
<h2 className="text-base font-semibold text-fg">Neues Szenario</h2>
|
||||
<p className="text-xs text-muted">
|
||||
Vollstaendige Kopie von <strong className="text-fg">{source.name}</strong>. Aenderungen darin
|
||||
werden anschliessend farblich hervorgehoben.
|
||||
</p>
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/25"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name des Szenarios"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => {
|
||||
setSaving(true);
|
||||
onCreate(name.trim() || "Szenario");
|
||||
}}
|
||||
className="rounded-lg bg-accent px-4 py-2 text-sm font-medium text-accent-fg shadow-sm hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{saving ? "..." : "Erstellen"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-lg border border-border px-4 py-2 text-sm font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
<Modal title="Neues Szenario" onClose={onClose}>
|
||||
<p className="text-xs text-muted">
|
||||
Vollständige Kopie von <strong className="text-fg">{source.name}</strong>. Änderungen darin
|
||||
werden anschliessend farblich hervorgehoben.
|
||||
</p>
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/25"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name des Szenarios"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
disabled={saving}
|
||||
onClick={() => {
|
||||
setSaving(true);
|
||||
onCreate(name.trim() || "Szenario");
|
||||
}}
|
||||
>
|
||||
{saving ? "…" : "Erstellen"}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user