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
Deploy App / deploy (push) Successful in 1m27s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -88,6 +88,7 @@ export function PhaseCard({
|
||||
</button>
|
||||
{expanded && (
|
||||
<PhaseForm
|
||||
key={`${phase.id}:${phase.securities.length}:${phase.incomingCapital}`}
|
||||
household={household}
|
||||
phase={phase}
|
||||
onSaved={() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "{nextPhaseName}" manuell anpassen).
|
||||
Wird beim Speichern automatisch in "{nextPhaseName}" 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>}
|
||||
|
||||
Reference in New Issue
Block a user