Format all monetary amounts as 1'000-separated integers and round amount inputs to nearest 1'000
Deploy App / deploy (push) Successful in 43s
Deploy App / deploy (push) Successful in 43s
This commit is contained in:
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
|||||||
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||||
import { WealthChart, type TimelineSeries } from "@/components/WealthChart";
|
import { WealthChart, type TimelineSeries } from "@/components/WealthChart";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
|
import { formatChf } from "@/lib/format";
|
||||||
import type { PlanComputed } from "@/lib/calculations";
|
import type { PlanComputed } from "@/lib/calculations";
|
||||||
import type { PlanInput } from "@/lib/types";
|
import type { PlanInput } from "@/lib/types";
|
||||||
|
|
||||||
@@ -120,11 +121,7 @@ export function Dashboard({
|
|||||||
tick={{ fontSize: 11 }}
|
tick={{ fontSize: 11 }}
|
||||||
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
|
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip formatter={(v) => (typeof v === "number" ? formatChf(v) : v)} />
|
||||||
formatter={(v) =>
|
|
||||||
typeof v === "number" ? v.toLocaleString("de-CH", { maximumFractionDigits: 0 }) : v
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||||
{barKeys.map((key, i) => (
|
{barKeys.map((key, i) => (
|
||||||
<Bar key={key} dataKey={key} stackId="a" fill={PALETTE[i % PALETTE.length]} />
|
<Bar key={key} dataKey={key} stackId="a" fill={PALETTE[i % PALETTE.length]} />
|
||||||
@@ -144,7 +141,7 @@ function StatCard({ label, value, help }: { label: string; value: number; help?:
|
|||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xl font-semibold text-zinc-900 dark:text-zinc-50">
|
<div className="mt-1 text-xl font-semibold text-zinc-900 dark:text-zinc-50">
|
||||||
{value.toLocaleString("de-CH", { maximumFractionDigits: 0 })} CHF
|
{formatChf(value)} CHF
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { InfoBubble } from "@/components/InfoBubble";
|
import { InfoBubble } from "@/components/InfoBubble";
|
||||||
|
import { formatChf, parseChfInput, roundToThousand } from "@/lib/format";
|
||||||
|
|
||||||
const baseInputClass =
|
const baseInputClass =
|
||||||
"w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm text-zinc-900 focus:border-zinc-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100";
|
"w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm text-zinc-900 focus:border-zinc-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100";
|
||||||
@@ -47,6 +49,59 @@ export function NumberField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Roher Betrags-Input ohne Label (fuer kompakte Tabellenzellen o.ae.). Zeigt den Wert
|
||||||
|
// formatiert mit 1'000er-Trennzeichen an, solange das Feld nicht fokussiert ist, und
|
||||||
|
// rundet beim Verlassen des Feldes auf ein Vielfaches von 1'000 (siehe lib/format.ts).
|
||||||
|
export function MoneyInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
const [text, setText] = useState(() => String(Math.round(value || 0)));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
className={className ?? baseInputClass}
|
||||||
|
value={focused ? text : formatChf(value)}
|
||||||
|
onFocus={() => {
|
||||||
|
setFocused(true);
|
||||||
|
setText(String(Math.round(value || 0)));
|
||||||
|
}}
|
||||||
|
onChange={(e) => setText(e.target.value.replace(/[^0-9-]/g, ""))}
|
||||||
|
onBlur={() => {
|
||||||
|
setFocused(false);
|
||||||
|
onChange(roundToThousand(parseChfInput(text)));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MoneyField({
|
||||||
|
label,
|
||||||
|
help,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
help?: string;
|
||||||
|
value: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FieldLabel label={label} help={help} />
|
||||||
|
<MoneyInput value={value} onChange={onChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function TextField({
|
export function TextField({
|
||||||
label,
|
label,
|
||||||
help,
|
help,
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ import { useState } from "react";
|
|||||||
import { LineChart, Line, ResponsiveContainer } from "recharts";
|
import { LineChart, Line, ResponsiveContainer } from "recharts";
|
||||||
import { PhaseForm } from "@/components/PhaseForm";
|
import { PhaseForm } from "@/components/PhaseForm";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
|
import { formatChf } from "@/lib/format";
|
||||||
import type { HouseholdInput, PhaseInput } from "@/lib/types";
|
import type { HouseholdInput, PhaseInput } from "@/lib/types";
|
||||||
import type { PhaseComputed } from "@/lib/calculations";
|
import type { PhaseComputed } from "@/lib/calculations";
|
||||||
|
|
||||||
function formatChf(value: number) {
|
|
||||||
return value.toLocaleString("de-CH", { maximumFractionDigits: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PhaseCard({
|
export function PhaseCard({
|
||||||
household,
|
household,
|
||||||
phase,
|
phase,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { NumberField, SelectField, TextField } from "@/components/FormField";
|
import { MoneyField, NumberField, SelectField, TextField } from "@/components/FormField";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
|
import { formatChf } from "@/lib/format";
|
||||||
import type {
|
import type {
|
||||||
ExpenseEntryInput,
|
ExpenseEntryInput,
|
||||||
HouseholdInput,
|
HouseholdInput,
|
||||||
@@ -159,7 +160,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Geschaetztes Jahreseinkommen (CHF)"
|
label="Geschaetztes Jahreseinkommen (CHF)"
|
||||||
help="Ihr erwartetes Bruttoeinkommen pro Jahr waehrend dieser Lebensphase."
|
help="Ihr erwartetes Bruttoeinkommen pro Jahr waehrend dieser Lebensphase."
|
||||||
value={entry.amount}
|
value={entry.amount}
|
||||||
@@ -185,7 +186,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
<Section title="Ausgaben">
|
<Section title="Ausgaben">
|
||||||
{expenseEntries.map((entry, i) => (
|
{expenseEntries.map((entry, i) => (
|
||||||
<div key={entry.id} className="grid grid-cols-[1fr_auto] items-end gap-2">
|
<div key={entry.id} className="grid grid-cols-[1fr_auto] items-end gap-2">
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Geschaetzte Gesamtausgaben (CHF/Jahr)"
|
label="Geschaetzte Gesamtausgaben (CHF/Jahr)"
|
||||||
help="Saemtliche laufenden Kosten des Haushalts pro Jahr - inkl. Lebenshaltung, Hypothekarzinsen, laufende Einkommens- und Vermoegenssteuern. Keine separate Kategorisierung noetig."
|
help="Saemtliche laufenden Kosten des Haushalts pro Jahr - inkl. Lebenshaltung, Hypothekarzinsen, laufende Einkommens- und Vermoegenssteuern. Keine separate Kategorisierung noetig."
|
||||||
value={entry.amount}
|
value={entry.amount}
|
||||||
@@ -201,7 +202,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
onClick={() => setExpenseEntries((prev) => [...prev, { id: tempId(), label: null, amount: 0 }])}
|
onClick={() => setExpenseEntries((prev) => [...prev, { id: tempId(), label: null, amount: 0 }])}
|
||||||
/>
|
/>
|
||||||
<div className="rounded-md bg-zinc-50 px-3 py-2 text-sm 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>
|
Verfuegbare Sparquote (CHF/Jahr): <strong>{formatChf(savingsQuota)}</strong>
|
||||||
{" "}(Details und Verteilung siehe Wertschriften weiter unten)
|
{" "}(Details und Verteilung siehe Wertschriften weiter unten)
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -216,7 +217,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
value={s.name}
|
value={s.name}
|
||||||
onChange={(v) => setSecurities((prev) => prev.map((x, idx) => (idx === i ? { ...x, name: v } : x)))}
|
onChange={(v) => setSecurities((prev) => prev.map((x, idx) => (idx === i ? { ...x, name: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Startwert (CHF)"
|
label="Startwert (CHF)"
|
||||||
help="Wert dieser Position zu Beginn der Phase."
|
help="Wert dieser Position zu Beginn der Phase."
|
||||||
value={s.startValue}
|
value={s.startValue}
|
||||||
@@ -229,7 +230,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
value={s.expectedReturn}
|
value={s.expectedReturn}
|
||||||
onChange={(v) => setSecurities((prev) => prev.map((x, idx) => (idx === i ? { ...x, expectedReturn: v } : x)))}
|
onChange={(v) => setSecurities((prev) => prev.map((x, idx) => (idx === i ? { ...x, expectedReturn: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Jaehrlicher Sparbeitrag (CHF)"
|
label="Jaehrlicher Sparbeitrag (CHF)"
|
||||||
help="Der Betrag aus Ihrer verfuegbaren Sparquote, den Sie jaehrlich in diese Position investieren moechten."
|
help="Der Betrag aus Ihrer verfuegbaren Sparquote, den Sie jaehrlich in diese Position investieren moechten."
|
||||||
value={s.annualContribution}
|
value={s.annualContribution}
|
||||||
@@ -274,13 +275,13 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
<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 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">
|
<div className="flex items-center gap-2">
|
||||||
<StatusDot ok={!startCapitalRemaining} />
|
<StatusDot ok={!startCapitalRemaining} />
|
||||||
Verfuegbares Startkapital (aus Verkaeufen der Vorphase): <strong>{phase.incomingCapital.toLocaleString("de-CH")}</strong> CHF
|
Verfuegbares Startkapital (aus Verkaeufen der Vorphase): <strong>{formatChf(phase.incomingCapital)}</strong> CHF
|
||||||
{" "}— zugewiesen: {allocatedStartCapital.toLocaleString("de-CH")}
|
{" "}— zugewiesen: {formatChf(allocatedStartCapital)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StatusDot ok={!savingsRemaining} />
|
<StatusDot ok={!savingsRemaining} />
|
||||||
Verfuegbare Sparquote (CHF/Jahr): <strong>{savingsQuota.toLocaleString("de-CH")}</strong>
|
Verfuegbare Sparquote (CHF/Jahr): <strong>{formatChf(savingsQuota)}</strong>
|
||||||
{" "}— zugewiesen: {allocated.toLocaleString("de-CH")}
|
{" "}— zugewiesen: {formatChf(allocated)}
|
||||||
</div>
|
</div>
|
||||||
{overAllocated && (
|
{overAllocated && (
|
||||||
<div className="text-amber-700 dark:text-amber-400">
|
<div className="text-amber-700 dark:text-amber-400">
|
||||||
@@ -300,13 +301,13 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
value={re.name}
|
value={re.name}
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, name: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, name: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Aktueller Marktwert (CHF)"
|
label="Aktueller Marktwert (CHF)"
|
||||||
help="Geschaetzter heutiger Verkehrswert der Liegenschaft."
|
help="Geschaetzter heutiger Verkehrswert der Liegenschaft."
|
||||||
value={re.marketValue}
|
value={re.marketValue}
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, marketValue: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, marketValue: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Aktuelle Hypothek (CHF)"
|
label="Aktuelle Hypothek (CHF)"
|
||||||
help="Ausstehender Hypothekarbetrag."
|
help="Ausstehender Hypothekarbetrag."
|
||||||
value={re.mortgage}
|
value={re.mortgage}
|
||||||
@@ -319,13 +320,13 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
value={re.valueGrowth}
|
value={re.valueGrowth}
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, valueGrowth: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, valueGrowth: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Jaehrliche Amortisation (CHF)"
|
label="Jaehrliche Amortisation (CHF)"
|
||||||
help="Betrag, um den die Hypothek pro Jahr reduziert wird."
|
help="Betrag, um den die Hypothek pro Jahr reduziert wird."
|
||||||
value={re.amortization}
|
value={re.amortization}
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, amortization: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, amortization: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Geschaetzter Verkaufspreis (CHF)"
|
label="Geschaetzter Verkaufspreis (CHF)"
|
||||||
help="Nur bei geplantem Verkauf am Ende der Phase auszufuellen."
|
help="Nur bei geplantem Verkauf am Ende der Phase auszufuellen."
|
||||||
value={re.salePrice ?? 0}
|
value={re.salePrice ?? 0}
|
||||||
@@ -367,7 +368,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
{ value: "EXPENSE", label: "Ausgabe" },
|
{ value: "EXPENSE", label: "Ausgabe" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Betrag (CHF)"
|
label="Betrag (CHF)"
|
||||||
value={ev.amount}
|
value={ev.amount}
|
||||||
onChange={(v) => setOneTimeEvents((prev) => prev.map((x, idx) => (idx === i ? { ...x, amount: v } : x)))}
|
onChange={(v) => setOneTimeEvents((prev) => prev.map((x, idx) => (idx === i ? { ...x, amount: v } : x)))}
|
||||||
@@ -396,19 +397,19 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
onChange={(v) => setRetirementInfos((prev) => prev.map((x, idx) => (idx === i ? { ...x, personId: v } : x)))}
|
onChange={(v) => setRetirementInfos((prev) => prev.map((x, idx) => (idx === i ? { ...x, personId: v } : x)))}
|
||||||
options={household.persons.map((p) => ({ value: p.id, label: personLabel(household, p.id) }))}
|
options={household.persons.map((p) => ({ value: p.id, label: personLabel(household, p.id) }))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="AHV-Rente (CHF/Jahr)"
|
label="AHV-Rente (CHF/Jahr)"
|
||||||
help="Zusammengesetzt mit der PK-Rente zur 'Erwarteten Rente'. Bei Ehepaaren max. 1.5x AHV-Maximalrente gemeinsam."
|
help="Zusammengesetzt mit der PK-Rente zur 'Erwarteten Rente'. Bei Ehepaaren max. 1.5x AHV-Maximalrente gemeinsam."
|
||||||
value={r.ahvAmount}
|
value={r.ahvAmount}
|
||||||
onChange={(v) => setRetirementInfos((prev) => prev.map((x, idx) => (idx === i ? { ...x, ahvAmount: v } : x)))}
|
onChange={(v) => setRetirementInfos((prev) => prev.map((x, idx) => (idx === i ? { ...x, ahvAmount: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="PK-Rente (CHF/Jahr)"
|
label="PK-Rente (CHF/Jahr)"
|
||||||
help="Pensionskassenrente (2. Saeule)."
|
help="Pensionskassenrente (2. Saeule)."
|
||||||
value={r.pkPensionAmount}
|
value={r.pkPensionAmount}
|
||||||
onChange={(v) => setRetirementInfos((prev) => prev.map((x, idx) => (idx === i ? { ...x, pkPensionAmount: v } : x)))}
|
onChange={(v) => setRetirementInfos((prev) => prev.map((x, idx) => (idx === i ? { ...x, pkPensionAmount: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<MoneyField
|
||||||
label="Kapitalbezug brutto (CHF)"
|
label="Kapitalbezug brutto (CHF)"
|
||||||
help="Zusammengesetzt aus Saeule 3a und/oder Kapitalbezug aus der Pensionskasse."
|
help="Zusammengesetzt aus Saeule 3a und/oder Kapitalbezug aus der Pensionskasse."
|
||||||
value={r.lumpSumAmount}
|
value={r.lumpSumAmount}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { MoneyInput } from "@/components/FormField";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
|
import { formatChf } from "@/lib/format";
|
||||||
import type { PhaseInput, TransitionDecision } from "@/lib/types";
|
import type { PhaseInput, TransitionDecision } from "@/lib/types";
|
||||||
import type { PhaseComputed } from "@/lib/calculations";
|
import type { PhaseComputed } from "@/lib/calculations";
|
||||||
|
|
||||||
@@ -178,20 +180,15 @@ export function TransitionPanel({
|
|||||||
</td>
|
</td>
|
||||||
<td className="py-2">
|
<td className="py-2">
|
||||||
{it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? (
|
{it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? (
|
||||||
<input
|
<MoneyInput
|
||||||
type="number"
|
|
||||||
className="w-32 rounded-md border border-zinc-300 bg-white px-2 py-1 text-xs dark:border-zinc-600 dark:bg-zinc-900"
|
className="w-32 rounded-md border border-zinc-300 bg-white px-2 py-1 text-xs dark:border-zinc-600 dark:bg-zinc-900"
|
||||||
value={it.salePrice ?? 0}
|
value={it.salePrice ?? 0}
|
||||||
onChange={(e) =>
|
onChange={(v) =>
|
||||||
setItems((prev) =>
|
setItems((prev) => prev.map((x, idx) => (idx === i ? { ...x, salePrice: v } : x)))
|
||||||
prev.map((x, idx) => (idx === i ? { ...x, salePrice: e.target.valueAsNumber || 0 } : x))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-zinc-500">
|
<span className="text-xs text-zinc-500">{formatChf(it.carryOverValue)} CHF</span>
|
||||||
{it.decision === "CARRY_OVER" ? it.carryOverValue.toLocaleString("de-CH") : it.carryOverValue.toLocaleString("de-CH")} CHF
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -199,7 +196,7 @@ export function TransitionPanel({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="rounded-md bg-white px-3 py-2 text-sm dark:bg-zinc-900">
|
<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>
|
Verfuegbares Startkapital fuer neue Phase (aus Verkaeufen): <strong>{formatChf(totalAvailableCapital)} CHF</strong>
|
||||||
<p className="mt-1 text-xs text-zinc-500">
|
<p className="mt-1 text-xs text-zinc-500">
|
||||||
Wird beim Speichern automatisch in "{nextPhaseName}" als verfuegbares Startkapital hinterlegt.
|
Wird beim Speichern automatisch in "{nextPhaseName}" als verfuegbares Startkapital hinterlegt.
|
||||||
Uebernommene Wertschriften erscheinen dort automatisch mit ihrem Endwert als neuer Startwert.
|
Uebernommene Wertschriften erscheinen dort automatisch mit ihrem Endwert als neuer Startwert.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
import { formatChf } from "@/lib/format";
|
||||||
import type { PlanComputed } from "@/lib/calculations";
|
import type { PlanComputed } from "@/lib/calculations";
|
||||||
|
|
||||||
export interface TimelineSeries {
|
export interface TimelineSeries {
|
||||||
@@ -67,11 +68,7 @@ export function WealthChart({ series }: { series: TimelineSeries[] }) {
|
|||||||
tick={{ fontSize: 11 }}
|
tick={{ fontSize: 11 }}
|
||||||
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
|
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip formatter={(v) => (typeof v === "number" ? formatChf(v) : v)} />
|
||||||
formatter={(v) =>
|
|
||||||
typeof v === "number" ? v.toLocaleString("de-CH", { maximumFractionDigits: 0 }) : v
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||||
{primary.boundaries.slice(1).map((b) => (
|
{primary.boundaries.slice(1).map((b) => (
|
||||||
<ReferenceLine key={b.year} x={b.year} stroke="#a1a1aa" strokeDasharray="2 2" />
|
<ReferenceLine key={b.year} x={b.year} stroke="#a1a1aa" strokeDasharray="2 2" />
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Einheitliches Format fuer Geldbetraege im ganzen Tool: Tausendertrennzeichen mit
|
||||||
|
// geradem Apostroph, keine Nachkommastellen (z. B. 1'780'000). Bewusst nicht ueber
|
||||||
|
// Intl/toLocaleString("de-CH"), da dessen Gruppentrennzeichen (’ U+2019) nicht dem
|
||||||
|
// geraden Apostroph entspricht.
|
||||||
|
export function formatChf(value: number): string {
|
||||||
|
const rounded = Math.round(value || 0);
|
||||||
|
const sign = rounded < 0 ? "-" : "";
|
||||||
|
const digits = Math.abs(rounded).toString();
|
||||||
|
const withSeparators = digits.replace(/\B(?=(\d{3})+(?!\d))/g, "'");
|
||||||
|
return sign + withSeparators;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rundet auf ein Vielfaches von 1'000 -- Betraege unter 1'000 CHF sind fuer dieses
|
||||||
|
// Planungstool nicht relevant.
|
||||||
|
export function roundToThousand(value: number): number {
|
||||||
|
return Math.round((value || 0) / 1000) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseChfInput(text: string): number {
|
||||||
|
const cleaned = text.replace(/[^0-9-]/g, "");
|
||||||
|
const parsed = parseInt(cleaned, 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user