Neuer Unterpunkt "Berichte" je Plan: Liste plus Assistent (Titel, Notiz, nominal ODER real, Plan-/Ist-Daten, bis zu drei Szenarien, gespeicherte Analysen). Layout immer gleich, Auswahl bestimmt nur die Bausteine. Die PDF-Datei wird ALS DATEI abgelegt (BYTEA in Postgres, nicht im Container-Dateisystem): Ein Bericht muss in drei Jahren byte-identisch wieder herunterladbar sein -- eine Neuerzeugung koennte das nach Aenderungen an Plan, Rechenkern oder Layout nicht garantieren. Kennzahlen je Szenario inkl. offener Entscheide. Deren Zaehlung liegt neu als reine Funktion in decisions.ts, die Matrix UND Bericht benutzen -- sonst nennen beide verschiedene Zahlen. Zu jeder Kennzahl ihre Grundlage als Verweis; die vollstaendigen Annahmen einmal je Szenario. Haftungsausschluss ist verpflichtend (per Test). Technik: pdfkit in der Node-Runtime statt Headless-Browser; @react-pdf/renderer bricht mit React 19. Als externes Paket deklariert, weil pdfkit Font-Metriken ueber Dateipfade laedt. Spezifikation 0.25 (3.11 neu), 9 Tests (212 -> 221). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
Copy,
|
||||
Dices,
|
||||
FileText,
|
||||
FileSpreadsheet,
|
||||
FolderKanban,
|
||||
CalendarClock,
|
||||
GitBranch,
|
||||
@@ -33,6 +34,7 @@ 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";
|
||||
@@ -105,7 +107,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
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" } | null>(null);
|
||||
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<Record<string, boolean>>({});
|
||||
const [savedAnalysisId, setSavedAnalysisId] = useState<string | null>(null);
|
||||
@@ -182,7 +184,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
|
||||
// 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") {
|
||||
function openPlanTab(planId: string, tab: "dashboard" | "scenarios" | "actuals" | "analyses" | "reports") {
|
||||
setPlanNav({ planId, tab });
|
||||
setSelectedScenarioId(null);
|
||||
setShowSpec(false);
|
||||
@@ -347,7 +349,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
|
||||
{plans.map((p) => {
|
||||
const navHere = planNav?.planId === p.id;
|
||||
const subItem = (tab: "dashboard" | "scenarios" | "actuals" | "analyses", label: string, Icon: typeof FolderKanban) => (
|
||||
const subItem = (tab: "dashboard" | "scenarios" | "actuals" | "analyses" | "reports", label: string, Icon: typeof FolderKanban) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openPlanTab(p.id, tab)}
|
||||
@@ -427,6 +429,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
Effektive Werte
|
||||
</button>
|
||||
{subItem("analyses", "Analysen", BarChart3)}
|
||||
{subItem("reports", "Berichte", FileSpreadsheet)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -510,7 +513,11 @@ function AppShellInner({ username }: { username: string }) {
|
||||
: showSpec
|
||||
? "So rechnet FPT"
|
||||
: planNav
|
||||
? `${activePlan?.name ?? "Plan"} · ${planNav.tab === "dashboard" ? "Dashboard" : planNav.tab === "scenarios" ? "Szenarien" : "Analysen"}`
|
||||
? `${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"}
|
||||
@@ -567,6 +574,14 @@ function AppShellInner({ username }: { username: string }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showSpec && !showSystemParams && planNav?.tab === "reports" && activePlan && (
|
||||
<ReportsView
|
||||
planId={planNav.planId}
|
||||
scenarios={activePlan.scenarios.map((s) => ({ id: s.id, name: s.name, isBase: s.isBase }))}
|
||||
hasActuals={(activePlan._count?.actuals ?? 0) > 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showSpec && !showSystemParams && !loading && !planNav && selectedScenarioId === null && (
|
||||
<DashboardHome
|
||||
username={username}
|
||||
|
||||
Reference in New Issue
Block a user