Automate transition: carried-over securities auto-appear in next phase, sale proceeds tracked as incoming capital with allocation status indicators
Deploy App / deploy (push) Successful in 1m27s

This commit is contained in:
2026-07-08 20:20:20 +02:00
parent 837da980f2
commit a1e689167c
11 changed files with 172 additions and 31 deletions
+6 -1
View File
@@ -220,7 +220,12 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
onChanged={refreshCurrent}
/>
{nextPhase && (
<TransitionPanel phase={phase} computed={computedPhase} nextPhaseName={nextPhase.name} />
<TransitionPanel
phase={phase}
computed={computedPhase}
nextPhaseName={nextPhase.name}
onChanged={refreshCurrent}
/>
)}
</div>
);
+1
View File
@@ -88,6 +88,7 @@ export function PhaseCard({
</button>
{expanded && (
<PhaseForm
key={`${phase.id}:${phase.securities.length}:${phase.incomingCapital}`}
household={household}
phase={phase}
onSaved={() => {
+45 -10
View File
@@ -58,6 +58,13 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
const savingsQuota = totalIncome - totalExpense;
const allocated = securities.reduce((s, sec) => s + sec.annualContribution, 0);
const overAllocated = allocated > savingsQuota;
const savingsRemaining = savingsQuota - allocated > 0.5;
const allocatedStartCapital = securities.reduce(
(s, sec) => s + Math.max(0, sec.startValue - sec.carriedBaseValue),
0
);
const startCapitalRemaining = phase.incomingCapital - allocatedStartCapital > 0.5;
async function handleSave() {
setSaving(true);
@@ -193,16 +200,9 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
label="+ Ausgabenposten"
onClick={() => setExpenseEntries((prev) => [...prev, { id: tempId(), label: null, amount: 0 }])}
/>
<div
className={`rounded-md px-3 py-2 text-sm ${
overAllocated
? "bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-300"
: "bg-zinc-50 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300"
}`}
>
<div className="rounded-md bg-zinc-50 px-3 py-2 text-sm text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
Verfuegbare Sparquote (CHF/Jahr): <strong>{savingsQuota.toLocaleString("de-CH")}</strong>
{" "} zugewiesen an Wertschriften: {allocated.toLocaleString("de-CH")}
{overAllocated && " ⚠ Die zugewiesenen Sparbeitraege uebersteigen die verfuegbare Sparquote."}
{" "}(Details und Verteilung siehe Wertschriften weiter unten)
</div>
</Section>
@@ -258,10 +258,36 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
onClick={() =>
setSecurities((prev) => [
...prev,
{ id: tempId(), name: "", startValue: 0, expectedReturn: 0, annualContribution: 0, ownerTag: "HOUSEHOLD", saleTaxRate: 0 },
{
id: tempId(),
name: "",
startValue: 0,
expectedReturn: 0,
annualContribution: 0,
ownerTag: "HOUSEHOLD",
saleTaxRate: 0,
carriedBaseValue: 0,
},
])
}
/>
<div className="flex flex-col gap-1.5 rounded-md bg-zinc-50 px-3 py-2 text-sm dark:bg-zinc-800">
<div className="flex items-center gap-2">
<StatusDot ok={!startCapitalRemaining} />
Verfuegbares Startkapital (aus Verkaeufen der Vorphase): <strong>{phase.incomingCapital.toLocaleString("de-CH")}</strong> CHF
{" "} zugewiesen: {allocatedStartCapital.toLocaleString("de-CH")}
</div>
<div className="flex items-center gap-2">
<StatusDot ok={!savingsRemaining} />
Verfuegbare Sparquote (CHF/Jahr): <strong>{savingsQuota.toLocaleString("de-CH")}</strong>
{" "} zugewiesen: {allocated.toLocaleString("de-CH")}
</div>
{overAllocated && (
<div className="text-amber-700 dark:text-amber-400">
Die zugewiesenen Sparbeitraege uebersteigen die verfuegbare Sparquote.
</div>
)}
</div>
</Section>
{/* Immobilien */}
@@ -461,6 +487,15 @@ function AddButton({ label, onClick }: { label: string; onClick: () => void }) {
);
}
function StatusDot({ ok }: { ok: boolean }) {
return (
<span
title={ok ? "Vollstaendig verteilt" : "Noch nicht vollstaendig verteilt"}
className={`inline-block h-2.5 w-2.5 shrink-0 rounded-full ${ok ? "bg-emerald-500" : "bg-red-500"}`}
/>
);
}
function RemoveButton({ onClick }: { onClick: () => void }) {
return (
<button
+5 -2
View File
@@ -21,10 +21,12 @@ export function TransitionPanel({
phase,
computed,
nextPhaseName,
onChanged,
}: {
phase: PhaseInput;
computed: PhaseComputed;
nextPhaseName: string;
onChanged: () => void;
}) {
const [items, setItems] = useState<ItemDraft[]>([]);
const [loaded, setLoaded] = useState(false);
@@ -131,6 +133,7 @@ export function TransitionPanel({
})),
});
setSaved(true);
onChanged();
} catch (e) {
setError(e instanceof Error ? e.message : "Speichern fehlgeschlagen.");
} finally {
@@ -198,8 +201,8 @@ export function TransitionPanel({
<div className="rounded-md bg-white px-3 py-2 text-sm dark:bg-zinc-900">
Verfuegbares Startkapital fuer neue Phase (aus Verkaeufen): <strong>{totalAvailableCapital.toLocaleString("de-CH")} CHF</strong>
<p className="mt-1 text-xs text-zinc-500">
Dieser Betrag kann anschliessend frei auf neue oder bestehende Wertschriften der Folgephase verteilt werden
(Startwert der jeweiligen Wertschrift in &quot;{nextPhaseName}&quot; manuell anpassen).
Wird beim Speichern automatisch in &quot;{nextPhaseName}&quot; als verfuegbares Startkapital hinterlegt.
Uebernommene Wertschriften erscheinen dort automatisch mit ihrem Endwert als neuer Startwert.
</p>
</div>
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}