Navigation auf Plan-Ebene und gespeicherte Analysen (Roadmap-Redesign)
Deploy App / deploy (push) Successful in 1m45s
Deploy App / deploy (push) Successful in 1m45s
Sidebar zweistufig: pro Plan die Unterpunkte Szenarien / Effektive Werte / Analysen; Klick auf Plan-Name oeffnet ein Plan-Dashboard. Plan-Dashboard: Kennzahlen, gerechnete Werte ausdruecklich "laut Basisszenario", Ist-Abweichung falls erfasst. Szenario-Liste: Version, Elementzahl, Endvermoegen, Ruinalter + Aktionen Historie und Matrix. Baum in der Sidebar bleibt. Analysen: vier umklappende Kacheln (auch per Antippen). Grafiken oeffnen neu mit Auswahl EINER Grafik. Szenario-Vergleich zu den Grafiken, CSV-Export auf die Matrix. Gespeicherte Analysen: Grafik/MC/Einflussfaktoren als ZAHLEN einfrieren (read-only, nichts wird neu gerechnet) -- druckfaehig fuer den spaeteren PDF-Bericht, ohne finalWealthSorted. Einheitliche generische Ergebnisform. Neue Tabelle SavedAnalysis, Endpunkte /analyses und /dashboard, Module analyses.ts, Komponenten PlanViews/SavedAnalysisView/SaveAnalysisButton. Kein Eingriff in den Rechenkern. Spezifikation 0.24 (3.10 und 9.30 neu). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+171
-39
@@ -11,6 +11,7 @@ import {
|
||||
CalendarClock,
|
||||
GitBranch,
|
||||
History,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
Menu,
|
||||
PiggyBank,
|
||||
@@ -28,6 +29,8 @@ 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 { buildViews } from "@/lib/dataview";
|
||||
import type { ActualsSetInput, ElementOrigin } from "@/lib/actuals";
|
||||
import { VersionHistoryDialog } from "@/components/VersionHistoryDialog";
|
||||
@@ -99,6 +102,9 @@ function AppShellInner({ username }: { username: string }) {
|
||||
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" | "analyses" } | null>(null);
|
||||
const [savedAnalysisId, setSavedAnalysisId] = useState<string | null>(null);
|
||||
const [showSystemParams, setShowSystemParams] = useState(false);
|
||||
const [showPlanTraces, setShowPlanTraces] = useState(false);
|
||||
const [showPalette, setShowPalette] = useState(false);
|
||||
@@ -110,6 +116,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
setShowSpec(true);
|
||||
setShowSystemParams(false);
|
||||
setSelectedScenarioId(null);
|
||||
setPlanNav(null);
|
||||
}
|
||||
|
||||
const loadPlans = useCallback(async () => {
|
||||
@@ -123,7 +130,9 @@ function AppShellInner({ username }: { username: string }) {
|
||||
const loadDetail = useCallback(async (scenarioId: string, silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
setDetail(await api.get<ScenarioDetail>(`/api/scenarios/${scenarioId}`));
|
||||
const d = await api.get<ScenarioDetail>(`/api/scenarios/${scenarioId}`);
|
||||
setDetail(d);
|
||||
return d;
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
@@ -138,9 +147,9 @@ function AppShellInner({ username }: { username: string }) {
|
||||
if (selectedScenarioId) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- asynchroner Datenabruf
|
||||
loadDetail(selectedScenarioId);
|
||||
} else {
|
||||
setDetail(null);
|
||||
}
|
||||
// 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.
|
||||
@@ -161,11 +170,45 @@ function AppShellInner({ username }: { username: string }) {
|
||||
|
||||
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" | "analyses") {
|
||||
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 von der Plan-Ebene aus öffnen (braucht das Basisszenario geladen).
|
||||
async function openActualsForPlan(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) return;
|
||||
if (detail?.meta.id !== baseId) await loadDetail(baseId, true);
|
||||
setShowActuals(true);
|
||||
}
|
||||
|
||||
async function handleDeletePlan(id: string) {
|
||||
const plan = plans.find((p) => p.id === id);
|
||||
const ok = await confirm({
|
||||
@@ -217,7 +260,9 @@ function AppShellInner({ username }: { username: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const activePlan = plans.find((p) => p.scenarios.some((s) => s.id === selectedScenarioId)) ?? 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.
|
||||
@@ -231,9 +276,9 @@ function AppShellInner({ username }: { username: string }) {
|
||||
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); } },
|
||||
{ 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(
|
||||
@@ -270,12 +315,13 @@ function AppShellInner({ username }: { username: string }) {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedScenarioId(null);
|
||||
setPlanNav(null);
|
||||
setShowSpec(false);
|
||||
setShowSystemParams(false);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
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"
|
||||
selectedScenarioId === null && !planNav && !showSpec && !showSystemParams ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
@@ -295,31 +341,67 @@ function AppShellInner({ username }: { username: string }) {
|
||||
</div>
|
||||
{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">
|
||||
<div className="group flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold text-fg">
|
||||
<FolderKanban className="h-3.5 w-3.5 shrink-0 text-faint" />
|
||||
<span className="min-w-0 flex-1 truncate" title={p.name}>{p.name}</span>
|
||||
{plans.map((p) => {
|
||||
const navHere = planNav?.planId === p.id;
|
||||
const subItem = (tab: "dashboard" | "scenarios" | "analyses", label: string, Icon: typeof FolderKanban) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openPlanTab(p.id, tab)}
|
||||
className={`flex w-full items-center gap-2 rounded-lg py-1.5 pl-8 pr-3 text-left text-xs font-medium transition-colors ${
|
||||
navHere && planNav?.tab === tab ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 shrink-0" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<div key={p.id} className="mb-1">
|
||||
{/* Plan-Name: Klick öffnet das Plan-Dashboard. */}
|
||||
<div className="group flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold text-fg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openPlanTab(p.id, "dashboard")}
|
||||
className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
|
||||
title={p.name}
|
||||
>
|
||||
<FolderKanban className={`h-3.5 w-3.5 shrink-0 ${navHere && planNav?.tab === "dashboard" ? "text-accent" : "text-faint"}`} />
|
||||
<span className="min-w-0 flex-1 truncate">{p.name}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Plan löschen"
|
||||
onClick={() => handleDeletePlan(p.id)}
|
||||
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>
|
||||
|
||||
{/* Unter-Menüpunkte, leicht eingezogen. */}
|
||||
{subItem("scenarios", "Szenarien", Layers)}
|
||||
{/* Der Szenario-Baum bleibt darunter -- ein Klick führt direkt in die Matrix. */}
|
||||
<ScenarioTree
|
||||
scenarios={p.scenarios}
|
||||
parentId={null}
|
||||
depth={1}
|
||||
selectedId={selectedScenarioId}
|
||||
onSelect={openScenario}
|
||||
onCopy={setCopyFrom}
|
||||
onDelete={handleDeleteScenario}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Plan löschen"
|
||||
onClick={() => handleDeletePlan(p.id)}
|
||||
className="rounded p-0.5 text-faint opacity-60 transition-opacity hover:bg-danger-soft hover:text-danger group-hover:opacity-100"
|
||||
onClick={() => openActualsForPlan(p.id)}
|
||||
className="flex w-full items-center gap-2 rounded-lg py-1.5 pl-8 pr-3 text-left text-xs font-medium text-muted transition-colors hover:bg-surface-2"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<CalendarClock className="h-3.5 w-3.5 shrink-0" />
|
||||
Effektive Werte
|
||||
</button>
|
||||
{subItem("analyses", "Analysen", BarChart3)}
|
||||
</div>
|
||||
<ScenarioTree
|
||||
scenarios={p.scenarios}
|
||||
parentId={null}
|
||||
depth={0}
|
||||
selectedId={selectedScenarioId}
|
||||
onSelect={openScenario}
|
||||
onCopy={setCopyFrom}
|
||||
onDelete={handleDeleteScenario}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="mt-4 flex flex-col gap-1 border-t border-border pt-3">
|
||||
<span className="px-3 pb-1 text-[11px] font-semibold uppercase tracking-wide text-faint">Wissen</span>
|
||||
@@ -330,6 +412,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
setSpecAnchor(null);
|
||||
setShowSystemParams(false);
|
||||
setSelectedScenarioId(null);
|
||||
setPlanNav(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 ${
|
||||
@@ -345,6 +428,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
setShowSystemParams(true);
|
||||
setShowSpec(false);
|
||||
setSelectedScenarioId(null);
|
||||
setPlanNav(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 ${
|
||||
@@ -397,9 +481,11 @@ function AppShellInner({ username }: { username: string }) {
|
||||
? "Systemparameter"
|
||||
: showSpec
|
||||
? "So rechnet FPT"
|
||||
: detail
|
||||
? `${detail.meta.planName} · ${detail.meta.name}`
|
||||
: "Übersicht"}
|
||||
: planNav
|
||||
? `${activePlan?.name ?? "Plan"} · ${planNav.tab === "dashboard" ? "Dashboard" : planNav.tab === "scenarios" ? "Szenarien" : "Analysen"}`
|
||||
: selectedScenarioId && detail
|
||||
? `${detail.meta.planName} · ${detail.meta.name}`
|
||||
: "Übersicht"}
|
||||
</h1>
|
||||
<ProfileMenu username={username} />
|
||||
</header>
|
||||
@@ -409,14 +495,39 @@ function AppShellInner({ username }: { username: string }) {
|
||||
|
||||
{showSpec && <SpecView anchor={specAnchor} />}
|
||||
|
||||
{!showSpec && !showSystemParams && loading && <PlanSkeleton />}
|
||||
{!showSpec && !showSystemParams && loading && !planNav && <PlanSkeleton />}
|
||||
|
||||
{!showSpec && !showSystemParams && !loading && selectedScenarioId === null && (
|
||||
{/* Plan-Ebene: Dashboard / Szenarien / Analysen */}
|
||||
{!showSpec && !showSystemParams && planNav?.tab === "dashboard" && (
|
||||
<PlanDashboardView planId={planNav.planId} />
|
||||
)}
|
||||
{!showSpec && !showSystemParams && planNav?.tab === "scenarios" && (
|
||||
<ScenarioListView
|
||||
planId={planNav.planId}
|
||||
onOpenMatrix={openScenario}
|
||||
onOpenHistory={(sid) => {
|
||||
void loadDetail(sid, true).then(() => setShowHistory(true));
|
||||
}}
|
||||
onNew={() => {
|
||||
const base = activePlan?.scenarios.find((s) => s.isBase) ?? activePlan?.scenarios[0];
|
||||
if (base) setCopyFrom({ id: base.id, planId: base.planId, planName: activePlan?.name ?? "", name: base.name, isBase: base.isBase, parentScenarioId: base.parentScenarioId } as ScenarioMeta);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!showSpec && !showSystemParams && planNav?.tab === "analyses" && (
|
||||
<AnalysesView
|
||||
planId={planNav.planId}
|
||||
onLaunch={(kind) => void launchTool(planNav.planId, kind)}
|
||||
onOpenSaved={(id) => setSavedAnalysisId(id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showSpec && !showSystemParams && !loading && !planNav && selectedScenarioId === null && (
|
||||
<DashboardHome
|
||||
username={username}
|
||||
plans={plans}
|
||||
creatingDemo={creatingDemo}
|
||||
onSelect={openScenario}
|
||||
onOpenPlan={(id) => openPlanTab(id, "dashboard")}
|
||||
onCreateGuided={() => setShowWizard(true)}
|
||||
onCreateChooser={() => setShowNewPlanChooser(true)}
|
||||
onCreateDemo={handleCreateDemo}
|
||||
@@ -546,6 +657,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
<ChartsDialog onClose={() => setShowCharts(false)} title={`Grafiken · ${detail.meta.name}`}>
|
||||
<Dashboard
|
||||
plan={detail.plan}
|
||||
planId={detail.meta.planId}
|
||||
computed={detail.computed}
|
||||
siblings={(activePlan?.scenarios ?? []).filter((s) => s.id !== detail.meta.id)}
|
||||
actuals={detail.actuals ?? []}
|
||||
@@ -569,6 +681,7 @@ function AppShellInner({ username }: { username: string }) {
|
||||
{showSensitivity && detail && (
|
||||
<SensitivityDialog
|
||||
plan={detail.plan}
|
||||
planId={detail.meta.planId}
|
||||
actuals={detail.actuals ?? []}
|
||||
origins={detail.elementOrigins ?? []}
|
||||
onClose={() => setShowSensitivity(false)}
|
||||
@@ -584,6 +697,14 @@ function AppShellInner({ username }: { username: string }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{savedAnalysisId && planNav && (
|
||||
<SavedAnalysisView
|
||||
planId={planNav.planId}
|
||||
analysisId={savedAnalysisId}
|
||||
onClose={() => setSavedAnalysisId(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showActuals && detail && activePlan && (
|
||||
<ActualsDialog
|
||||
planId={detail.meta.planId}
|
||||
@@ -817,7 +938,7 @@ function DashboardHome({
|
||||
username,
|
||||
plans,
|
||||
creatingDemo,
|
||||
onSelect,
|
||||
onOpenPlan,
|
||||
onCreateGuided,
|
||||
onCreateChooser,
|
||||
onCreateDemo,
|
||||
@@ -826,7 +947,7 @@ function DashboardHome({
|
||||
username: string;
|
||||
plans: PlanListItem[];
|
||||
creatingDemo: boolean;
|
||||
onSelect: (scenarioId: string) => void;
|
||||
onOpenPlan: (planId: string) => void;
|
||||
onCreateGuided: () => void;
|
||||
onCreateChooser: () => void;
|
||||
onCreateDemo: () => void;
|
||||
@@ -858,13 +979,15 @@ function DashboardHome({
|
||||
) : (
|
||||
<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;
|
||||
const persons = (p.persons ?? [])
|
||||
.map((pe) => pe.name || (pe.role === "PERSON_A" ? "Person A" : "Person B"))
|
||||
.join(" · ");
|
||||
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)}
|
||||
onClick={() => onOpenPlan(p.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-soft">
|
||||
@@ -873,7 +996,8 @@ function DashboardHome({
|
||||
<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"}` : ""}
|
||||
{persons || "Basisszenario"}
|
||||
{others > 0 ? ` · +${others} Variante${others === 1 ? "" : "n"}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -888,6 +1012,14 @@ function DashboardHome({
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Ein paar Kennzahlen pro Plan direkt in der Übersicht. */}
|
||||
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted">
|
||||
<span>{p.scenarios.length} Szenario{p.scenarios.length === 1 ? "" : "s"}</span>
|
||||
{p.startYear && <span>ab {p.startYear}</span>}
|
||||
{p._count && p._count.actuals > 0 && <span>{p._count.actuals} Ist-Datensatz{p._count.actuals === 1 ? "" : "e"}</span>}
|
||||
{p._count && p._count.analyses > 0 && <span>{p._count.analyses} Analyse{p._count.analyses === 1 ? "" : "n"}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user