From ff862ed0308507bb5af0a8c5eceeca60f26cee87 Mon Sep 17 00:00:00 2001 From: kelle Date: Mon, 17 Aug 2026 21:47:28 +0200 Subject: [PATCH] Menueleiste als eine Komponente in zwei Breiten, Kartenansicht wieder erreichbar Co-Authored-By: Claude Opus 5 --- SPEZIFIKATION.md | 56 ++- src/components/AppShell.tsx | 683 ++++++++++++++++++---------------- src/components/PlanView.tsx | 40 +- src/components/ReviewTile.tsx | 5 +- 4 files changed, 441 insertions(+), 343 deletions(-) diff --git a/SPEZIFIKATION.md b/SPEZIFIKATION.md index 7802ae3..72cfbb1 100644 --- a/SPEZIFIKATION.md +++ b/SPEZIFIKATION.md @@ -4,10 +4,10 @@ | | | |---|---| | **Dokument** | Funktionale und Technische Spezifikation FPT | -| **Version** | 0.42 | +| **Version** | 0.42.1 | | **Datum** | 2026-07-25 | | **Status** | Lebendes Dokument | -| **Codestand** | Arbeitsstand nach `113616c` inkl. Kartenansicht (Branch `main`) | +| **Codestand** | Arbeitsstand nach `f41c361` inkl. Memo 2026081702 (Branch `main`) | | **Ersetzt** | `FDD_TDD_FPT.docx` (v1–v5) im Ordner `Info Dateien` – diese sind ab Version 0.1 dieses Dokuments obsolet | | **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` | @@ -17,6 +17,7 @@ | Version | Datum | Autor | Änderung | |---|---|---|---| +| 0.42.1 | 2026-08-17 | Claude (Opus 5) | **Memo 2026081702** – vier Befunde aus der ersten Testrunde der neuen Oberfläche. (1) **Die Kartenansicht war überhaupt nicht erreichbar** – der Umschalter dauerhaft ausgegraut. Ursache: Die Bedingung lautete `forceMatrix = !!diff`, aber `computeScenarioDiff` liefert **immer** ein Objekt (bei Gleichheit ein leeres). Auch `diff.total > 0` wäre falsch gewesen: Ein Kind-Szenario weicht nach der ersten Änderung immer von seiner Vorlage ab, Karten wären dort für immer gesperrt. Die Diff-Markierungen sind eine **Verzierung** der Matrix, kein Modus. Neu erzwingt nur die **Ist-Ansicht** die Matrix (das ist ein Modus, in den man bewusst wechselt); bei Abweichungen zur Vorlage steht in der Kartenansicht ein Hinweis mit dem Weg dorthin. (2) **Die Menüleiste wechselte beim Überfahren die Darstellung** – eingeklappt Icons, ausgefahren ein Plan-Baum. Das war kein Übergang, sondern ein Sprung. Neu ist es **eine Komponente in zwei Breiten**: Die Symbole sitzen in einer festen 3.5-rem-Spalte und bleiben exakt an ihrem Platz, rechts daneben fährt nur die Beschriftung aus. Damit entfällt der Baum aller Pläne; er wird nicht vermisst, weil der Plan über den Wähler oben gewechselt wird und die vier Bereiche sich immer auf den **aktiven** Plan beziehen. Plan anlegen und löschen sind in den Wähler gewandert, der Szenario-Baum hängt aufklappbar unter «Szenarien». (3) **Die Matrix schob sich vor die ausgefahrene Leiste.** Ihre sticky Köpfe tragen `z-40` und konkurrierten damit global; die Ecke oben links gewann. Die Matrix ist neu ein eigener **Stapel-Kontext** (`relative z-0`) – ihre Köpfe stapeln sich nur noch untereinander. (4) **Die Kachel «Offene Punkte» zog sich über den ganzen Bildschirm**, auch mit einem einzigen Eintrag: ein `h-full` aus der Zeit, als die vier Kacheln in einem Raster gleich hoch sein mussten. In der rechten Leiste ist das falsch – sie ist jetzt so hoch wie ihr Inhalt. | | 0.42 | 2026-08-17 | Claude (Opus 5) | **Kartenansicht und Drei-Spalten-Shell** (neues Kapitel 3.15). Der grösste Eingriff in die Oberfläche seit 0.36 – und er behebt eine Ursache statt Symptome. Ausgemessen war die Lage eindeutig: Der Phasenkopf trug 220 px an Kennzahlen, dauerhaft fixiert; von 675 px Matrixhöhe blieben 430 px zum Arbeiten. Fünf Phasen brauchten 1760 px Breite, sichtbar waren mit offenem Panel 1024 px. Und das Bearbeitungs-Panel (`fixed right-0`, 416 px) legte sich ausgerechnet über die Spalte, zu der man gerade gescrollt hatte. (1) **Eine Karte je Lebensphase** ersetzt die Matrix als Arbeitsansicht. Sie erzählt die Phase als Zeitablauf: **Eintritt** (der Übergang aus der Vorphase; bei Phase 1 der Bestand), **Laufend** (die Elemente mit ihren Annahmen und die Quote), **Ergebnis** (Vermögen und Cash am Phasenende). Horizontales Scrollen entfällt vollständig. Der Übergang gehört bewusst zur FOLGEphase – dort wird das frei werdende Kapital verteilt, konsistent mit 0.41. (2) **Bearbeitet wird IN der Karte.** Ein Klick klappt die Zeile auf, die Felder stehen darunter. Damit kann nichts mehr verdecken, woran man arbeitet – das Panel bleibt nur für die Matrix. Ein Knopf im Kartenkopf klappt **alle** Zeilen auf oder zu; bei acht Elementen ist das der Unterschied zwischen einmal durchsehen und acht Klicks. (3) **Die Zeitachse wird zum Navigator**: Das Segment der offenen Karte ist hervorgehoben, ein Klick springt hin. Dazu Pfeilknöpfe unten und die **Pfeiltasten** (ausser in Eingabefeldern – sonst springt die Karte beim Tippen weg). (4) **Die Matrix bleibt als Vergleichsansicht.** Umschalter oben rechts; die Wahl gehört zum Benutzer und überlebt die Sitzung. Ein Szenario-Diff oder eine Ist-Ansicht **erzwingt** die Matrix: Vergleich über alle Phasen kann eine einzelne Karte prinzipiell nicht leisten, und stillschweigend die falsche Darstellung anzubieten wäre schlechter als der Wechsel. (5) **Drei-Spalten-Shell.** Die Menüleiste links ist eingeklappt eine **Icon-Schiene** (56 statt 256 px); beim Überfahren fährt sie als Overlay aus, ein **Pin** hält sie breit. Die vier Kacheln wandern von oben in eine **rechte Leiste** mit aufklappbaren Abschnitten. Das ist der entscheidende Tausch: Sie kosteten oben **vertikalen** Platz, rechts kosten sie horizontalen – und Höhe ist auf einem Breitbildschirm die knappe Grösse. Eingeklappt bleibt die Zahl der offenen Punkte am Knopf sichtbar. Zusammen: statt 1184 × 574 rund **1328 × 804** für den Inhalt, also +12 % Breite und **+40 % Höhe**. (6) Der Anzeige-Umschalter nominal/beide/real steht neu auch in der Werkzeugleiste (`ValueModeSwitch`, eine Implementierung für beide Orte). Neue Komponente `PhaseCards`; `ElementDetail` bekommt eine `compact`-Variante, `Timeline` die Navigator-Rolle, `Tile` eine rahmenlose Variante. Kein Eingriff in den Rechenkern; 337 Tests unverändert grün. | | 0.41 | 2026-08-17 | Claude (Opus 5) | **Memo 2026081701** – zehn Punkte aus einer Testrunde, mehrheitlich Feinschliff, zwei davon inhaltlich. (1) **Die Kapitalverwendung heisst neu Schuldentilgung · Investition · Cash, und die Tilgung hat ein wählbares Ziel** (`capitalUseDebtTargetElementId`): eine Immobilie **oder** eine sonstige Schuld. Bisher floss die Quote stur in die erstbeste Immobilie – wer neben einer Hypothek zu 1,5 % einen Konsumkredit zu 6 % trägt, konnte den Kredit gar nicht zuerst tilgen. Ohne Wahl bleibt es beim alten Verhalten, damit bestehende Pläne unverändert rechnen. **Cash ist neu eine gleichrangige Zeile** statt einer blassen Fussnote: «ich lasse es liegen» ist ein legitimer Entscheid. Rechnerisch bleibt es der Rest – alle drei frei eintippbar zu machen hiesse, dass beim Tippen still eine andere Zahl wandert. (2) **Die Maske gibt es neu auch bei der Säule 3a.** Dort stellt sich die Frage sogar zwingender als bei der PK: Ein 3a-Konto wird IMMER vollständig bezogen, es fliesst also in jedem Fall ein grosser Betrag – der bis 0.40 stumm auf dem Cash landete. (3) **Je Person entsteht beim Abschluss der Bestandsaufnahme automatisch ein AHV-Element**, idempotent (wer die Bestandsaufnahme später korrigiert, bekommt kein zweites). Die AHV ist die einzige Kategorie ohne Bestand – ein Plus-Knopf mit leerer Karte wäre sinnlos, das Element wegzulassen aber auch: Ohne es rechnet der Plan ab der Pensionierung mit einer AHV-Rente von 0. Die Karte in der Bestandsaufnahme sagt neu, warum dort nichts zu erfassen ist. (4) **Farben folgen der Bedeutung, nicht dem Zustand.** Ein einmaliger Cash-Zufluss am Übergang trug die Akzentfarbe – im warmen Schema ein Orangerot, das eine Erbschaft wie einen Fehler aussehen liess; neu ist er grün, ein Abfluss neutral. Die Knöpfe «Kapital verteilen» und «Sparquote verteilen» sind **grün mit Haken, sobald erledigt** – vorher sah man einer vollständig verteilten Phase nicht an, dass sie fertig war. (5) **Sechs Stellen mit unlesbarer Schrift behoben:** `text-attention-fg` ist **weiss** und für vollflächigen Attention-Grund gedacht; auf hellem oder 10-%-Grund war der Text im warmen und im hellen Schema praktisch unsichtbar (PK-Einkaufswarnung, 3a-Kollision, fehlendes Anlageziel, zwei Stellen in der Kachel «Offene Punkte»). (6) **Der Kapitalanteil-Regler hatte keine sichtbare Schiene**: `appearance-none` schaltet die native Darstellung ab, womit auch `accent-color` nicht mehr greift – übrig blieb der blosse Knopf. (7) **Der Umwandlungssatz verschwindet bei 100 % Kapital**, spiegelbildlich zur Kapitalbezugssteuer bei 0 %. Bei vollem Kapitalbezug wird nichts verrentet. (8) **Der Dialog «Neue Lebensphase» erklärt die Dauer-Kappung** – welches Ereignis sie setzt und warum (die Rechnung leitet Erwerbsstatus und Bezüge am Phasen**beginn** ab). Vorher stand dort nur «max. N». (9) **Zustand statt Regel nach der Pensionierung:** Die Zellen von PK und 3a beschrieben in der Pensionsphase die Regel im Futur («wird berechnet», «wird bezogen»), obwohl der Bezug längst geschehen war. Neu steht dort der gerechnete Stand – bei der PK die Rente in Franken oder «Vollständig bezogen», bei der 3a «Vollständig bezogen». (10) **Eine Vokabel für einen Gedanken:** Die Bestätigung der Pensionsentscheide heisst neu «angeschaut» wie die der Phasenzellen, und der Erklärtext sagt ausdrücklich, dass der Haken **keine Zahl ändert**. Zwei Wörter für dieselbe Sache hatten mehr verwirrt als geholfen. Die Fixpunkt-Kappung um AHV-, PK- und 3a-Bezugsalter zu erweitern wurde **bewusst zurückgestellt** – im Basisszenario ist das Pensionsalter fest 65, das Thema gehört zur Szenario-Art «Frühpensionierung» (Roadmap Nr. 48). 3 Tests ergänzt (334 → 337). | | 0.40.1 | 2026-08-16 | Claude (Opus 5) | **Zwei Fehler aus der Testrunde.** (1) **«Ein Wert, ein Ort» gilt jetzt im Rechenkern, nicht nur in der Oberfläche.** Eine Immobilie zeigte in der ersten Lebensphase Startwert 0 statt Kaufpreis minus Hypothek. Ursache: Der Rechenkern legte die Phasenwerte über die Stammdaten (`{...baseData, ...phaseValues}`) – eine in 0.39 versehentlich gespeicherte 0 gewann damit gegen die Bestandsaufnahme. Seit 0.40 ist das Feld read-only, wodurch dieser Altwert **unerreichbar** war und den Kaufpreis dauerhaft verdeckt hätte. Neu gewinnen bei den fünf Bestandsfeldern (`amount`, `currentValue`, `startValue`, `purchasePrice`, `mortgage`) **immer die Stammdaten**, wo sie einen Wert tragen (`firstPhaseValues`); der Rückfall auf den Phasenwert bleibt nur, solange die Stammdaten leer sind – sonst fielen Pläne aus der Zeit vor 0.36 schlagartig auf 0. Die Annahmen (Rendite, Zins, Wertsteigerung, Teuerung) bleiben in Phase 1 überschreibbar: Sie gelten für einen Zeitraum, nicht für den Anfangsbestand. Damit heilen bestehende Daten von selbst. (2) **«Alle bestätigen» war zu grob.** Der Knopf in der Annahmen-Maske schrieb `confirmed: true` auf jede Zeile – man konnte ihn drücken, ohne je gescrollt zu haben, also genau das Durchwinken, das der Mechanismus verhindern soll. Neu trägt **jede Zeile ein Häkchen «Angeschaut»**, anfangs leer; wer ein Feld ändert, hakt es automatisch an (bearbeiten IST anschauen); oben markiert ein Klick alle auf einmal, aber als sichtbarer Akt. Gespeichert werden die Werte aller Zeilen, bestätigt nur die angehakten – der Knopf nennt die Zahl. 4 Tests ergänzt (330 → 334). | @@ -2558,9 +2559,19 @@ sich eine Grösse über alle Phasen entwickelt. Der Umschalter steht oben rechts; die Wahl gehört zum **Benutzer** und überlebt die Sitzung (`localStorage`) – sie ist eine Arbeitsgewohnheit, keine Eigenschaft des Plans. -> **Ein Diff oder eine Ist-Ansicht erzwingt die Matrix.** Dort ist sie die richtige -> Darstellung; stillschweigend die falsche zu zeigen wäre schlechter als der Wechsel. Ohne -> Lebensphasen gilt dasselbe – eine Karte ohne Phase gibt es nicht. +**Was die Matrix erzwingt und was nicht** (korrigiert in 0.42.1): + +| Fall | Verhalten | Warum | +|---|---|---| +| **Ist-Ansicht** aktiv | Matrix erzwungen | Plan gegen Ist ist eine Frage über alle Phasen – und es ist ein Modus, in den man bewusst wechselt | +| **Keine Lebensphase** | Matrix erzwungen | Eine Karte ohne Phase gibt es nicht | +| **Abweichungen zur Vorlage** | Karten bleiben, mit Hinweis | Die Diff-Markierungen sind eine Verzierung der Matrix, kein Modus | + +> **Der erste Anlauf sperrte die Karten vollständig.** Die Bedingung lautete `!!diff`, und +> `computeScenarioDiff` liefert immer ein Objekt – bei Gleichheit ein leeres. Der Umschalter +> war damit dauerhaft ausgegraut. Aber auch `diff.total > 0` wäre falsch gewesen: Ein +> Kind-Szenario weicht nach der ersten Änderung **immer** ab, und Karten wären dort für immer +> gesperrt. Eine Verzierung darf keinen Modus erzwingen. ### 3.15.5 Drei Spalten @@ -2569,15 +2580,35 @@ Der Umschalter steht oben rechts; die Wahl gehört zum **Benutzer** und überleb ^ Pin haelt sie breit ^ einklappbar ``` -**Links** die Menüleiste, eingeklappt eine Icon-Schiene mit Haus, Plan-Wähler und den vier -Bereichen des aktiven Plans. Beim Überfahren fährt sie als **Overlay** aus – sie kostet dann -keinen Platz –, ein **Pin** hält sie breit und schiebt den Inhalt. Der Szenario-Baum steht -bewusst nicht in der Schiene: Er braucht Text, und Text braucht Breite. +**Links** die Menüleiste: Haus, Plan-Wähler und die vier Bereiche des aktiven Plans, darunter +das Wissen. Beim Überfahren fährt sie als **Overlay** aus – sie kostet dann keinen Platz –, ein +**Pin** hält sie breit und schiebt den Inhalt. + +> **Eine Komponente in zwei Breiten, nicht zwei Darstellungen** (korrigiert in 0.42.1). Die +> Symbole sitzen in einer **festen 3.5-rem-Spalte** und bleiben beim Ausfahren exakt an ihrem +> Platz; rechts daneben kommt nur die Beschriftung dazu. Der erste Anlauf zeigte eingeklappt +> Icons und ausgefahren einen Plan-Baum – das war kein Übergang, sondern ein Sprung, und man +> verlor jedes Mal die Orientierung. + +Damit entfällt der **Baum aller Pläne**. Er wird nicht vermisst: Der Plan wird über den Wähler +gewechselt, und die vier Bereiche beziehen sich immer auf den **aktiven** Plan. Statt einer +Liste von Listen bleiben vier Knöpfe. Was daran hing, hat einen neuen Ort: + +| Vorher im Baum | Neu | +|---|---| +| Plan wechseln | Plan-Wähler (Klapp-Menü) | +| Plan anlegen, Plan löschen | ebenfalls im Plan-Wähler | +| Szenario-Baum mit Kopieren/Löschen | aufklappbar unter «Szenarien» (nur ausgefahren – er braucht Text) | Der Plan-Wähler zeigt eingeklappt die **Initialen** des aktiven Plans. Ohne diesen Bezug wüsste -man nach dem Umschalten nicht mehr, worin man ist – und die vier Icons darunter wären +man nach dem Umschalten nicht mehr, worin man ist – und die vier Knöpfe darunter wären gegenstandslos. +**Die Matrix ist ein eigener Stapel-Kontext** (`relative z-0`). Ihre sticky Köpfe tragen `z-40` +und konkurrierten sonst global mit der ausgefahrenen Leiste – die Ecke oben links schob sich +vor das Menü. Innerhalb der Matrix stapeln sich die Köpfe unverändert; nach aussen tritt sie +als eine Ebene auf. + **Rechts** die vier bisherigen Kacheln als aufklappbare Abschnitte. Das ist der entscheidende Tausch: Oben kosteten sie **vertikalen** Platz, rechts kosten sie horizontalen – und auf einem Breitbildschirm ist Höhe die knappe Grösse. Eingeklappt bleibt die **Zahl der offenen Punkte** @@ -2597,6 +2628,11 @@ Aus dem Umbau sind drei Regeln geworden, die für neue Oberfläche gelten: Bildschirm, in jeder Sitzung, für jeden Benutzer. 3. **Nichts legt sich über das, was man bearbeitet.** Overlays sind für Dinge, die den Arbeitsfluss unterbrechen sollen – nicht für die Arbeit selbst. +4. **Ein Übergang verschiebt, er ersetzt nicht.** Wenn ein Element beim Aus- oder Aufklappen + seinen Platz behält und nur Beiwerk dazukommt, folgt das Auge mit. Wechselt stattdessen die + ganze Darstellung, ist es ein Sprung – und man sucht sich neu zurecht. +5. **Wer `z-index` vergibt, baut einen Stapel-Kontext.** Sonst konkurrieren Zahlen aus + verschiedenen Bauteilen miteinander, und wer gewinnt, ist Zufall der Reihenfolge im DOM. Referenz: `src/components/PhaseCards.tsx`, `src/components/Timeline.tsx`, `IconRail` in `src/components/AppShell.tsx`, `RailSection` und `ValueModeSwitch` in diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 967635a..a5941e9 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -104,8 +104,6 @@ function AppShellInner({ username }: { username: string }) { 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); - // Welche Szenario-Bäume in der Seitenleiste aufgeklappt sind. Standard: eingeklappt. - const [expandedTrees, setExpandedTrees] = useState>({}); const [savedAnalysisId, setSavedAnalysisId] = useState(null); const [showSystemParams, setShowSystemParams] = useState(false); const [showPlanTraces, setShowPlanTraces] = useState(false); @@ -342,184 +340,6 @@ function AppShellInner({ username }: { username: string }) { return base; }, [detail, selectedScenarioId]); - const sidebar = ( -
-
-
- -
- FPT - -
- - -
- ); - const diff = detail ? computeScenarioDiff(detail.plan, detail.base) : null; return ( @@ -528,76 +348,65 @@ function AppShellInner({ username }: { username: string }) { 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 && ( @@ -612,7 +421,60 @@ function AppShellInner({ username }: { username: string }) { > - {sidebar} + setSidebarOpen(false)} + plans={plans} + activeScenarioId={selectedScenarioId} + planNav={planNav} + atHome={selectedScenarioId === null && !planNav && !showSpec && !showSystemParams} + showSpec={showSpec} + showSystemParams={showSystemParams} + planMenuOpen={planMenuOpen} + onTogglePlanMenu={() => setPlanMenuOpen((o) => !o)} + onNewPlan={() => { + setPlanMenuOpen(false); + setShowEmptyPlanDialog(true); + }} + onDeletePlan={handleDeletePlan} + onHome={() => { + setSelectedScenarioId(null); + setPlanNav(null); + setShowSpec(false); + setShowSystemParams(false); + setSidebarOpen(false); + }} + onOpenTab={(planId, tab) => { + setPlanMenuOpen(false); + setSidebarOpen(false); + if (tab === "actuals") void openActualsTab(planId); + else openPlanTab(planId, tab); + }} + onOpenScenario={(id) => { + openScenario(id); + setSidebarOpen(false); + }} + onCopyScenario={setCopyFrom} + onDeleteScenario={handleDeleteScenario} + onSpec={() => { + setShowSpec(true); + setSpecAnchor(null); + setShowSystemParams(false); + setSelectedScenarioId(null); + setPlanNav(null); + setSidebarOpen(false); + }} + onParams={() => { + setShowSystemParams(true); + setShowSpec(false); + setSelectedScenarioId(null); + setPlanNav(null); + setSidebarOpen(false); + }} + onSearch={() => setShowPalette(true)} + /> )} @@ -1197,12 +1059,21 @@ function CopyScenarioDialog({ ); } -// --- Die eingeklappte Icon-Schiene ----------------------------------------------------------- +// --- Die linke Leiste ------------------------------------------------------------------------ // -// Sie zeigt genau das, was man im Alltag braucht: nach Hause, den aktiven Plan, seine vier -// Bereiche. Der Szenario-BAUM steht bewusst nicht hier -- er braucht Text, und Text braucht -// Breite. Wer ihn will, faehrt die Leiste aus oder pinnt sie an. -function IconRail({ +// 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, @@ -1211,13 +1082,21 @@ function IconRail({ showSystemParams, planMenuOpen, onTogglePlanMenu, + onNewPlan, + onDeletePlan, onHome, onOpenTab, + onOpenScenario, + onCopyScenario, + onDeleteScenario, onSpec, onParams, onSearch, - onPin, }: { + wide: boolean; + pinned: boolean; + mobile?: boolean; + onTogglePin: () => void; plans: PlanListItem[]; activeScenarioId: string | null; planNav: { planId: string; tab: string } | null; @@ -1226,107 +1105,259 @@ function IconRail({ 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; - onPin: () => void; }) { - // Welcher Plan ist gerade im Zugriff? Erst der offene, sonst der zuletzt gewaehlte Bereich, - // sonst der einzige. Ohne diesen Bezug waeren die vier Icons darunter gegenstandslos. + // 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() + ? active.name.split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase() : "–"; - const item = ( - label: string, - Icon: typeof LayoutDashboard, - on: boolean, - onClick: () => void, - disabled?: boolean - ) => ( - - ); - 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 ( + ); } diff --git a/src/components/PlanView.tsx b/src/components/PlanView.tsx index 800138a..1cc0c9d 100644 --- a/src/components/PlanView.tsx +++ b/src/components/PlanView.tsx @@ -9,6 +9,7 @@ import { ChevronDown, ChevronRight, CreditCard, + GitCompare, Home, Landmark, Pencil, @@ -166,6 +167,7 @@ export function PlanView({ // dreier: Plan UND Ist als Rohwerte nebeneinander wären mit nominal/real acht Zahlen je // Zelle (siehe SPEZIFIKATION 9.29). const [dataSource, setDataSource] = useState("PLAN"); + const [valueMode, setValueMode] = useState("nominal"); const hasActuals = actualComputed !== null; const computed = dataSource === "ACTUAL" && actualComputed ? actualComputed : planComputed; @@ -202,7 +204,6 @@ export function PlanView({ // 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("nominal"); const [showInventory, setShowInventory] = useState(false); // Karten oder Matrix. Die Wahl gilt fuer den Benutzer, nicht fuer das Szenario -- sie ist // eine Arbeitsgewohnheit, keine Eigenschaft des Plans. @@ -442,10 +443,17 @@ export function PlanView({ } const hasPhases = computed.phases.length > 0; - // Ein Vergleich (Szenario-Diff oder Plan/Ist) ist eine Frage ueber ALLE Phasen -- das kann - // eine einzelne Karte nicht zeigen. In diesen Faellen erzwingt FPT die Matrix, statt still - // die falsche Darstellung anzubieten. - const forceMatrix = !!diff || !!actualComputed; + // Die IST-Ansicht erzwingt die Matrix: Plan gegen Ist ist eine Frage ueber alle Phasen, das + // kann eine einzelne Karte nicht zeigen. + // + // Der Szenario-Diff erzwingt sie NICHT -- und das war der Fehler in 0.42. Die Bedingung + // lautete `!!diff`, aber `computeScenarioDiff` liefert immer ein Objekt (bei Gleichheit ein + // leeres). Damit war `forceMatrix` dauerhaft wahr und die Kartenansicht schlicht nicht + // erreichbar. Auch `diff.total > 0` waere zu scharf: Ein Kind-Szenario weicht nach der + // ersten Aenderung IMMER von seiner Vorlage ab -- Karten waeren dort fuer immer gesperrt. + // Die Diff-Markierungen sind eine Verzierung der Matrix, kein Modus. In der Kartenansicht + // steht stattdessen ein Hinweis. + const forceMatrix = showDeviation; const effectiveView: "cards" | "matrix" = forceMatrix || !hasPhases ? "matrix" : view; const currentPhaseId = computed.phases.find((p) => p.id === activePhaseId)?.id ?? computed.phases[0]?.id ?? null; @@ -568,6 +576,23 @@ export function PlanView({ Sie erscheint, sobald es ELEMENTE gibt -- Lebensphasen sind dafür nicht nötig. Bis 0.36 hing sie an den Phasen, wodurch die Bestandsaufnahme aus Schritt 1 im Nichts verschwand: erfasst, gespeichert, unsichtbar. */} + {/* Abweichungen zur Vorlage sind eine Frage über die ganze Fläche -- die Karte kann sie + nicht zeigen. Sie zu verschweigen wäre aber schlechter, als den Weg dorthin zu + nennen. */} + {effectiveView === "cards" && (diff?.total ?? 0) > 0 && ( + + )} + {effectiveView === "cards" && hasPhases && ( 0) && ( -
+ // `relative z-0` macht die Matrix zu einem eigenen Stapel-Kontext: Ihre sticky Koepfe + // (z-40) stapeln sich nur noch UNTEREINANDER. Vorher konkurrierten sie global und die + // Ecke oben links schob sich vor die ausgefahrene Menueleiste. +
{/* Feste Spaltenbreiten: Alle Phasenspalten sind gleich breit -- bei einer einzigen Phase bleibt die Tabelle dadurch schmal, bei vielen wird horizontal gescrollt. Ein w-full haette die Spalten je nach Anzahl unterschiedlich gedehnt. */} diff --git a/src/components/ReviewTile.tsx b/src/components/ReviewTile.tsx index f027599..fe4d4a8 100644 --- a/src/components/ReviewTile.tsx +++ b/src/components/ReviewTile.tsx @@ -30,7 +30,10 @@ export function ReviewTile({ return (