diff --git a/src/components/FormField.tsx b/src/components/FormField.tsx index 1e8527d..c0de79e 100644 --- a/src/components/FormField.tsx +++ b/src/components/FormField.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { InfoBubble } from "@/components/InfoBubble"; import { floorToThousand, formatChf, parseChfInput } from "@/lib/format"; @@ -64,11 +64,39 @@ export function MoneyInput({ }) { const [focused, setFocused] = useState(false); const [text, setText] = useState(() => String(Math.floor(value || 0))); + const valueRef = useRef(value); + useEffect(() => { + valueRef.current = value; + }, [value]); + const holdTimeout = useRef | null>(null); + const holdInterval = useRef | null>(null); function step(delta: number) { - onChange(floorToThousand(value) + delta); + onChange(floorToThousand(valueRef.current) + delta); } + function stopHold() { + if (holdTimeout.current) { + clearTimeout(holdTimeout.current); + holdTimeout.current = null; + } + if (holdInterval.current) { + clearInterval(holdInterval.current); + holdInterval.current = null; + } + } + + // Klick-und-Halten: sofortiger erster Schritt, nach kurzer Verzoegerung + // fortlaufende Wiederholung, bis losgelassen wird. + function startHold(delta: number) { + step(delta); + holdTimeout.current = setTimeout(() => { + holdInterval.current = setInterval(() => step(delta), 100); + }, 400); + } + + useEffect(() => stopHold, []); + return (
e.preventDefault()} - onClick={() => step(1000)} + onMouseDown={(e) => { + e.preventDefault(); + startHold(1000); + }} + onMouseUp={stopHold} + onMouseLeave={stopHold} + onTouchStart={(e) => { + e.preventDefault(); + startHold(1000); + }} + onTouchEnd={stopHold} className="flex flex-1 items-center justify-center text-[8px] leading-none text-zinc-500 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800" > ▲ @@ -101,8 +138,17 @@ export function MoneyInput({ type="button" tabIndex={-1} aria-label="Um 1'000 verringern" - onMouseDown={(e) => e.preventDefault()} - onClick={() => step(-1000)} + onMouseDown={(e) => { + e.preventDefault(); + startHold(-1000); + }} + onMouseUp={stopHold} + onMouseLeave={stopHold} + onTouchStart={(e) => { + e.preventDefault(); + startHold(-1000); + }} + onTouchEnd={stopHold} className="flex flex-1 items-center justify-center border-t border-zinc-300 text-[8px] leading-none text-zinc-500 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800" > ▼ diff --git a/src/lib/calculations.ts b/src/lib/calculations.ts index b034374..4d7969f 100644 --- a/src/lib/calculations.ts +++ b/src/lib/calculations.ts @@ -1,4 +1,5 @@ import { AHV_COUPLE_CAP_FACTOR, AHV_MAX_PENSION_PER_YEAR } from "@/lib/constants"; +import { floorToThousand } from "@/lib/format"; import type { HouseholdInput, PhaseInput, PlanInput } from "@/lib/types"; export interface SecurityComputed { @@ -70,16 +71,19 @@ export interface PlanComputed { totalSavingsWarnings: number; } +// Alle Zwischen- und Endwerte werden auf ein Vielfaches von 1'000 abgerundet (siehe +// lib/format.ts): nur so bleiben Betraege, die spaeter bei einem Verkauf oder Uebergang +// auf Wertschriften verteilt werden muessen, ueberhaupt vollstaendig verteilbar. export function computeSecurityYearlyValues( startValue: number, expectedReturn: number, annualContribution: number, durationYears: number ): number[] { - const values = [startValue]; + const values = [floorToThousand(startValue)]; for (let year = 1; year <= durationYears; year++) { const previous = values[year - 1]; - values.push(previous * (1 + expectedReturn / 100) + annualContribution); + values.push(floorToThousand(previous * (1 + expectedReturn / 100) + annualContribution)); } return values; } @@ -91,11 +95,11 @@ export function computeRealEstateYearly( amortization: number, durationYears: number ): { marketValues: number[]; mortgages: number[] } { - const marketValues = [marketValue]; - const mortgages = [mortgage]; + const marketValues = [floorToThousand(marketValue)]; + const mortgages = [floorToThousand(mortgage)]; for (let year = 1; year <= durationYears; year++) { - marketValues.push(marketValues[year - 1] * (1 + valueGrowth / 100)); - mortgages.push(Math.max(0, mortgages[year - 1] - amortization)); + marketValues.push(floorToThousand(marketValues[year - 1] * (1 + valueGrowth / 100))); + mortgages.push(floorToThousand(Math.max(0, mortgages[year - 1] - amortization))); } return { marketValues, mortgages }; } @@ -111,7 +115,7 @@ function computeRetirement( ahvAmount: info.ahvAmount, pkPensionAmount: info.pkPensionAmount, lumpSumAmount: info.lumpSumAmount, - lumpSumNet: info.lumpSumAmount * (1 - info.lumpSumTaxRate / 100), + lumpSumNet: floorToThousand(info.lumpSumAmount * (1 - info.lumpSumTaxRate / 100)), })); const ahvSum = perPerson.reduce((sum, p) => sum + p.ahvAmount, 0); @@ -182,7 +186,7 @@ function computePhase( // Vereinfachung gemaess TDD 3.3: Gewinn = Verkaufspreis - urspruenglich erfasster Startwert const gain = Math.max(0, re.salePrice! - re.marketValue); taxAmount = gain * (re.saleTaxRate / 100); - saleNetProceeds = re.salePrice! - taxAmount; + saleNetProceeds = floorToThousand(re.salePrice! - taxAmount); } return { id: re.id,