Raten ueber Lebensphasen uebernehmen + Rate in der Verlaufsgrafik
Deploy App / deploy (push) Successful in 1m6s
Deploy App / deploy (push) Successful in 1m6s
(1) Beim Aendern eines Ratenfelds fragt das Panel nach der Reichweite: nur diese Phase (Vorgabe), diese + folgende, alle Phasen. Gilt fuer expectedReturn, valueGrowth, interestRate und teuerungsausgleich. Inline statt Modal -- das Zahlenfeld loest je Tastendruck aus. Die Zielphasen behalten ihre uebrigen Werte (der Endpunkt ersetzt den ganzen Satz; ein Kopieren des Entwurfs haette dort Betraege geloescht). (2) ElementYearPoint fuehrt neu `rate` mit -- additiv, nur durchgereicht. Verlaufsgrafik zeigt sie auf zweiter Y-Achse als Stufenlinie. Golden Tests unveraendert. Spezifikation 0.20, 17 Tests (164 -> 181). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,14 @@ import { AlertTriangle } from "lucide-react";
|
||||
import { FieldLabel, MoneyField, NumberField, SelectField, TextField } from "@/components/FormField";
|
||||
import { formatChf } from "@/lib/format";
|
||||
import { api } from "@/lib/api-client";
|
||||
import {
|
||||
buildRateWrites,
|
||||
changedRateFields,
|
||||
scopeQuestion,
|
||||
SCOPE_OPTIONS,
|
||||
type PhaseRef,
|
||||
type RateScope,
|
||||
} from "@/lib/ratefields";
|
||||
import { ahvAnnualPension, ahvMdje, type AhvCareer } from "@/lib/calculations";
|
||||
import { CATEGORY_LABELS, num } from "@/lib/elements";
|
||||
import {
|
||||
@@ -49,6 +57,10 @@ interface Props {
|
||||
context: CellContext;
|
||||
phaseData: PhaseData;
|
||||
transitionData: TransitionData;
|
||||
// Alle Phasen des Szenarios und die dort bereits erfassten Werte -- Grundlage für die
|
||||
// Übernahme einer geänderten Rate auf weitere Phasen (siehe SPEZIFIKATION 3.6.11).
|
||||
allPhases?: PhaseRef[];
|
||||
phaseDataByPhase?: Record<string, PhaseData>;
|
||||
onSaved: () => void;
|
||||
onDeleteElement: () => void;
|
||||
}
|
||||
@@ -886,14 +898,29 @@ export function ElementTransitionFields({
|
||||
}
|
||||
}
|
||||
|
||||
export function ElementDetail({ element, context, phaseData, transitionData, onSaved, onDeleteElement }: Props) {
|
||||
export function ElementDetail({
|
||||
element,
|
||||
context,
|
||||
phaseData,
|
||||
transitionData,
|
||||
allPhases = [],
|
||||
phaseDataByPhase = {},
|
||||
onSaved,
|
||||
onDeleteElement,
|
||||
}: Props) {
|
||||
const [pd, setPd] = useState<PhaseData>({ ...phaseData });
|
||||
const [td, setTd] = useState<TransitionData>({ ...transitionData });
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Reichweite einer geänderten Rate. Vorgabe ist das bisherige Verhalten: nur diese Phase.
|
||||
const [scope, setScope] = useState<RateScope>("THIS");
|
||||
|
||||
const isTransition = context.kind === "transition";
|
||||
|
||||
// Welche Ratenfelder hat der Nutzer verändert? Nur dann wird überhaupt gefragt.
|
||||
const changedRates = isTransition ? [] : changedRateFields(element.category, phaseData, pd);
|
||||
const showScope = changedRates.length > 0 && allPhases.length > 1;
|
||||
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
@@ -914,6 +941,21 @@ export function ElementDetail({ element, context, phaseData, transitionData, onS
|
||||
delete payload.amount;
|
||||
}
|
||||
await api.put(`/api/elements/${element.id}/phase/${context.phaseId}`, payload);
|
||||
|
||||
// Geänderte Raten auf die gewählten weiteren Phasen übertragen. Die bestehenden
|
||||
// Werte der Zielphasen bleiben erhalten -- übernommen wird ausschliesslich die Rate.
|
||||
// Alle Schreibvorgänge fallen in dieselbe Bearbeitungssitzung und ergeben daher
|
||||
// EINE Nebenversion, nicht eine je Phase (siehe 3.8.1).
|
||||
for (const w of buildRateWrites(
|
||||
allPhases,
|
||||
context.phaseId,
|
||||
scope,
|
||||
changedRates,
|
||||
pd,
|
||||
phaseDataByPhase
|
||||
)) {
|
||||
await api.put(`/api/elements/${element.id}/phase/${w.phaseId}`, w.data);
|
||||
}
|
||||
}
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
@@ -959,6 +1001,38 @@ export function ElementDetail({ element, context, phaseData, transitionData, onS
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Rückfrage zur Reichweite. Erscheint erst, wenn eine Rate tatsächlich verändert
|
||||
wurde -- und bewusst hier statt als Modal beim Tippen: Das Zahlenfeld löst bei
|
||||
jedem Tastendruck aus, ein Dialog erschiene bei «5.2» viermal. */}
|
||||
{showScope && (
|
||||
<div className="rounded-xl border border-attention bg-attention-soft p-3">
|
||||
<p className="mb-2 text-xs text-attention-soft-fg">
|
||||
{scopeQuestion(changedRates, allPhases.length)}
|
||||
</p>
|
||||
<div className="inline-flex flex-wrap gap-0.5 rounded-lg border border-border bg-surface p-0.5 text-xs">
|
||||
{SCOPE_OPTIONS.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => setScope(o.value)}
|
||||
className={`rounded-md px-2.5 py-1 font-medium transition-colors ${
|
||||
scope === o.value ? "bg-accent text-accent-fg" : "text-muted hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-1.5 text-[11px] text-attention-soft-fg/80">
|
||||
{scope === "THIS"
|
||||
? "Andere Phasen bleiben unverändert."
|
||||
: `Wird beim Speichern in ${
|
||||
buildRateWrites(allPhases, context.phaseId, scope, changedRates, pd, phaseDataByPhase).length
|
||||
} weitere Phase(n) übertragen. Beträge und Raten dort bleiben erhalten – nur der geänderte Wert wird gesetzt.`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="text-sm text-danger">{error}</p>}
|
||||
<div>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user