Sidebar: Szenario-Baum standardmaessig eingeklappt, gleichmaessig eingerueckt
Deploy App / deploy (push) Successful in 1m5s
Deploy App / deploy (push) Successful in 1m5s
"Szenarien" bekommt ein Chevron: der Baum ist per Default zu und wird aktiv aufgeklappt; das Label fuehrt weiterhin in die Szenario-Liste. Der Baum wird flach dargestellt -- alle Szenarien gleich eingerueckt, Basisszenario zuoberst. Die Herkunfts-Verschachtelung zeigt weiterhin die Szenario-Liste (Spalte "aus ..."). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+68
-58
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
Copy,
|
Copy,
|
||||||
Dices,
|
Dices,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -104,6 +106,8 @@ function AppShellInner({ username }: { username: string }) {
|
|||||||
const [showActuals, setShowActuals] = useState(false);
|
const [showActuals, setShowActuals] = useState(false);
|
||||||
// Plan-Ebene: Dashboard / Szenarien-Liste / Analysen. Null = kein Plan-View aktiv.
|
// Plan-Ebene: Dashboard / Szenarien-Liste / Analysen. Null = kein Plan-View aktiv.
|
||||||
const [planNav, setPlanNav] = useState<{ planId: string; tab: "dashboard" | "scenarios" | "analyses" } | null>(null);
|
const [planNav, setPlanNav] = useState<{ planId: string; tab: "dashboard" | "scenarios" | "analyses" } | null>(null);
|
||||||
|
// Welche Szenario-Bäume in der Seitenleiste aufgeklappt sind. Standard: eingeklappt.
|
||||||
|
const [expandedTrees, setExpandedTrees] = useState<Record<string, boolean>>({});
|
||||||
const [savedAnalysisId, setSavedAnalysisId] = useState<string | null>(null);
|
const [savedAnalysisId, setSavedAnalysisId] = useState<string | null>(null);
|
||||||
const [showSystemParams, setShowSystemParams] = useState(false);
|
const [showSystemParams, setShowSystemParams] = useState(false);
|
||||||
const [showPlanTraces, setShowPlanTraces] = useState(false);
|
const [showPlanTraces, setShowPlanTraces] = useState(false);
|
||||||
@@ -378,18 +382,40 @@ function AppShellInner({ username }: { username: string }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Unter-Menüpunkte, leicht eingezogen. */}
|
{/* «Szenarien»: Chevron klappt den Baum auf/zu (Standard: zu), das Label führt
|
||||||
{subItem("scenarios", "Szenarien", Layers)}
|
in die Szenario-Liste. */}
|
||||||
{/* Der Szenario-Baum bleibt darunter -- ein Klick führt direkt in die Matrix. */}
|
<div
|
||||||
<ScenarioTree
|
className={`group flex w-full items-center gap-1 rounded-lg py-1.5 pr-3 pl-1 text-xs font-medium transition-colors ${
|
||||||
scenarios={p.scenarios}
|
navHere && planNav?.tab === "scenarios" ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||||
parentId={null}
|
}`}
|
||||||
depth={1}
|
>
|
||||||
selectedId={selectedScenarioId}
|
<button
|
||||||
onSelect={openScenario}
|
type="button"
|
||||||
onCopy={setCopyFrom}
|
aria-label={expandedTrees[p.id] ? "Szenarien einklappen" : "Szenarien aufklappen"}
|
||||||
onDelete={handleDeleteScenario}
|
onClick={() => setExpandedTrees((prev) => ({ ...prev, [p.id]: !prev[p.id] }))}
|
||||||
/>
|
className="rounded p-0.5 text-faint hover:text-fg"
|
||||||
|
>
|
||||||
|
{expandedTrees[p.id] ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openPlanTab(p.id, "scenarios")}
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||||
|
>
|
||||||
|
<Layers className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
Szenarien
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Der Baum erscheint nur aufgeklappt -- alle Szenarien gleich eingerückt. */}
|
||||||
|
{expandedTrees[p.id] && (
|
||||||
|
<ScenarioTree
|
||||||
|
scenarios={p.scenarios}
|
||||||
|
selectedId={selectedScenarioId}
|
||||||
|
onSelect={openScenario}
|
||||||
|
onCopy={setCopyFrom}
|
||||||
|
onDelete={handleDeleteScenario}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openActualsForPlan(p.id)}
|
onClick={() => openActualsForPlan(p.id)}
|
||||||
@@ -863,70 +889,54 @@ function ChartsDialog({
|
|||||||
// gibt es kein Hover.
|
// gibt es kein Hover.
|
||||||
function ScenarioTree({
|
function ScenarioTree({
|
||||||
scenarios,
|
scenarios,
|
||||||
parentId,
|
|
||||||
depth,
|
|
||||||
selectedId,
|
selectedId,
|
||||||
onSelect,
|
onSelect,
|
||||||
onCopy,
|
onCopy,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
scenarios: ScenarioMeta[];
|
scenarios: ScenarioMeta[];
|
||||||
parentId: string | null;
|
|
||||||
depth: number;
|
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
onCopy: (s: ScenarioMeta) => void;
|
onCopy: (s: ScenarioMeta) => void;
|
||||||
onDelete: (s: ScenarioMeta) => void;
|
onDelete: (s: ScenarioMeta) => void;
|
||||||
}) {
|
}) {
|
||||||
const level = scenarios.filter((s) => (s.parentScenarioId ?? null) === parentId);
|
// Flache, gleichmässig eingerückte Liste: Basisszenario zuoberst, danach die Varianten.
|
||||||
if (level.length === 0) return null;
|
// Die Herkunfts-Verschachtelung wird in der Szenario-Liste (Spalte «aus …») gezeigt, nicht
|
||||||
|
// mehr durch die Einrückung in der Seitenleiste.
|
||||||
|
const ordered = [...scenarios].sort((a, b) => (a.isBase === b.isBase ? 0 : a.isBase ? -1 : 1));
|
||||||
|
if (ordered.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{level.map((s) => (
|
{ordered.map((s) => (
|
||||||
<div key={s.id}>
|
<div
|
||||||
<div
|
key={s.id}
|
||||||
className={`group flex items-center gap-1.5 rounded-lg py-1.5 pr-1.5 text-sm transition-colors ${
|
className={`group flex items-center gap-1.5 rounded-lg py-1.5 pr-1.5 pl-9 text-sm transition-colors ${
|
||||||
selectedId === s.id ? "bg-accent-soft font-medium text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
selectedId === s.id ? "bg-accent-soft font-medium text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||||
}`}
|
}`}
|
||||||
style={{ paddingLeft: `${12 + depth * 14}px` }}
|
>
|
||||||
|
<button type="button" onClick={() => onSelect(s.id)} className="flex min-w-0 flex-1 items-center gap-1.5 text-left">
|
||||||
|
{s.isBase ? <PiggyBank className="h-3.5 w-3.5 shrink-0" /> : <GitBranch className="h-3.5 w-3.5 shrink-0" />}
|
||||||
|
<span className="min-w-0 flex-1 truncate" title={s.name}>{s.name}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Kopie erstellen"
|
||||||
|
title="Neues Szenario aus diesem"
|
||||||
|
onClick={() => onCopy(s)}
|
||||||
|
className="rounded p-0.5 text-faint opacity-60 transition-opacity hover:bg-accent-soft hover:text-accent group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<button type="button" onClick={() => onSelect(s.id)} className="flex min-w-0 flex-1 items-center gap-1.5 text-left">
|
<Copy className="h-3.5 w-3.5" />
|
||||||
{s.isBase ? (
|
</button>
|
||||||
<PiggyBank className="h-3.5 w-3.5 shrink-0" />
|
{!s.isBase && (
|
||||||
) : (
|
|
||||||
<GitBranch className="h-3.5 w-3.5 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span className="min-w-0 flex-1 truncate" title={s.name}>{s.name}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Kopie erstellen"
|
aria-label="Szenario löschen"
|
||||||
title="Neues Szenario aus diesem"
|
onClick={() => onDelete(s)}
|
||||||
onClick={() => onCopy(s)}
|
className="rounded p-0.5 text-faint opacity-60 transition-opacity hover:bg-danger-soft hover:text-danger 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" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
{!s.isBase && (
|
)}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Szenario löschen"
|
|
||||||
onClick={() => onDelete(s)}
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ScenarioTree
|
|
||||||
scenarios={scenarios}
|
|
||||||
parentId={s.id}
|
|
||||||
depth={depth + 1}
|
|
||||||
selectedId={selectedId}
|
|
||||||
onSelect={onSelect}
|
|
||||||
onCopy={onCopy}
|
|
||||||
onDelete={onDelete}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user