Files
FPT/src/lib/queries.ts
T
admGitAICDS c2fb82b0de
Deploy App / deploy (push) Successful in 1m1s
UI-Gesamtumbau: Du-Form, Inspector-Panel, Onboarding-Assistent, Tour, Palette
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>
2026-07-19 07:40:42 +02:00

116 lines
4.0 KiB
TypeScript

import { Prisma } from "@/generated/prisma/client";
import { prisma } from "@/lib/db";
import { cashTransitionSchema, phaseDataSchema, transitionDataSchema } from "@/lib/elements";
import type { CashTransitionData, PhaseData, TransitionData } from "@/lib/elements";
import type { PlanInput } from "@/lib/types";
export const planInclude = {
persons: { orderBy: { role: "asc" } },
phases: { orderBy: { sequenceNumber: "asc" } },
elements: {
orderBy: { orderIndex: "asc" },
include: { phaseValues: true, transitionValues: true },
},
} satisfies Prisma.ScenarioInclude;
export type PlanWithRelations = Prisma.ScenarioGetPayload<{ include: typeof planInclude }>;
function parsePhaseData(raw: unknown): PhaseData {
const parsed = phaseDataSchema.safeParse(raw);
return parsed.success ? parsed.data : {};
}
function parseTransitionData(raw: unknown): TransitionData {
const parsed = transitionDataSchema.safeParse(raw);
return parsed.success ? parsed.data : {};
}
function parseCashTransition(raw: unknown): CashTransitionData {
const parsed = cashTransitionSchema.safeParse(raw);
return parsed.success ? parsed.data : {};
}
// Wandelt ein Szenario (DB) in die berechenbare Einheit (PlanInput) um.
export function toPlanInput(plan: PlanWithRelations): PlanInput {
return {
id: plan.id,
name: plan.name,
householdType: plan.householdType,
inflationRateDefault: plan.inflationRateDefault,
initialCash: plan.initialCash,
startYear: plan.startYear,
persons: plan.persons.map((p) => ({
id: p.id,
role: p.role,
name: p.name,
age: p.age,
retirementAge: p.retirementAge,
})),
phases: plan.phases.map((phase) => ({
id: phase.id,
sequenceNumber: phase.sequenceNumber,
name: phase.name,
durationYears: phase.durationYears,
cashTransition: parseCashTransition(phase.cashTransition),
sourcePhaseId: phase.sourcePhaseId,
})),
elements: plan.elements.map((e) => {
const phaseValues: Record<string, PhaseData> = {};
for (const pv of e.phaseValues) phaseValues[pv.phaseId] = parsePhaseData(pv.data);
const transitionValues: Record<string, TransitionData> = {};
for (const tv of e.transitionValues) transitionValues[tv.fromPhaseId] = parseTransitionData(tv.data);
return {
id: e.id,
category: e.category,
name: e.name,
ownerRole: e.ownerRole,
orderIndex: e.orderIndex,
phaseValues,
transitionValues,
sourceElementId: e.sourceElementId,
};
}),
};
}
// --- Ownership-Abfragen. Der Eigentümer hängt neu am PLAN; Szenario/Phase/Element
// erben ihn über die Kette Scenario -> Plan -> User. ---
// Lädt ein Szenario inkl. Profil + Phasen + Elemente, nur wenn es dem Benutzer gehört.
export async function getOwnedScenario(scenarioId: string, userId: string) {
return prisma.scenario.findFirst({
where: { id: scenarioId, plan: { userId } },
include: planInclude,
});
}
// Wie oben, zusätzlich mit den Kopfdaten (Plan, Basis-Flag, Elternteil).
export async function getOwnedScenarioWithMeta(scenarioId: string, userId: string) {
return prisma.scenario.findFirst({
where: { id: scenarioId, plan: { userId } },
include: { ...planInclude, plan: true },
});
}
// Lädt einen Plan (Behälter) inkl. Szenario-Kopfdaten.
export async function getOwnedPlan(planId: string, userId: string) {
return prisma.plan.findFirst({
where: { id: planId, userId },
include: { scenarios: { orderBy: { createdAt: "asc" } } },
});
}
// Lädt eine Phase (Basisdaten), aber nur wenn sie dem Benutzer gehört.
export async function getOwnedPhase(phaseId: string, userId: string) {
return prisma.phase.findFirst({
where: { id: phaseId, scenario: { plan: { userId } } },
});
}
// Lädt ein Element (Basisdaten), aber nur wenn es dem Benutzer gehört.
export async function getOwnedElement(elementId: string, userId: string) {
return prisma.financialElement.findFirst({
where: { id: elementId, scenario: { plan: { userId } } },
});
}