Press-and-hold spinner buttons repeat by 1000; floor compound interest growth (securities and real estate) and pension lump sum to nearest 1000 throughout
Deploy App / deploy (push) Successful in 43s
Deploy App / deploy (push) Successful in 43s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { InfoBubble } from "@/components/InfoBubble";
|
import { InfoBubble } from "@/components/InfoBubble";
|
||||||
import { floorToThousand, formatChf, parseChfInput } from "@/lib/format";
|
import { floorToThousand, formatChf, parseChfInput } from "@/lib/format";
|
||||||
|
|
||||||
@@ -64,11 +64,39 @@ export function MoneyInput({
|
|||||||
}) {
|
}) {
|
||||||
const [focused, setFocused] = useState(false);
|
const [focused, setFocused] = useState(false);
|
||||||
const [text, setText] = useState(() => String(Math.floor(value || 0)));
|
const [text, setText] = useState(() => String(Math.floor(value || 0)));
|
||||||
|
const valueRef = useRef(value);
|
||||||
|
useEffect(() => {
|
||||||
|
valueRef.current = value;
|
||||||
|
}, [value]);
|
||||||
|
const holdTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const holdInterval = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
function step(delta: number) {
|
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 (
|
return (
|
||||||
<div className={`relative ${className ?? "w-full"}`}>
|
<div className={`relative ${className ?? "w-full"}`}>
|
||||||
<input
|
<input
|
||||||
@@ -91,8 +119,17 @@ export function MoneyInput({
|
|||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label="Um 1'000 erhoehen"
|
aria-label="Um 1'000 erhoehen"
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => {
|
||||||
onClick={() => step(1000)}
|
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"
|
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"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label="Um 1'000 verringern"
|
aria-label="Um 1'000 verringern"
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => {
|
||||||
onClick={() => step(-1000)}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
▼
|
▼
|
||||||
|
|||||||
+12
-8
@@ -1,4 +1,5 @@
|
|||||||
import { AHV_COUPLE_CAP_FACTOR, AHV_MAX_PENSION_PER_YEAR } from "@/lib/constants";
|
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";
|
import type { HouseholdInput, PhaseInput, PlanInput } from "@/lib/types";
|
||||||
|
|
||||||
export interface SecurityComputed {
|
export interface SecurityComputed {
|
||||||
@@ -70,16 +71,19 @@ export interface PlanComputed {
|
|||||||
totalSavingsWarnings: number;
|
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(
|
export function computeSecurityYearlyValues(
|
||||||
startValue: number,
|
startValue: number,
|
||||||
expectedReturn: number,
|
expectedReturn: number,
|
||||||
annualContribution: number,
|
annualContribution: number,
|
||||||
durationYears: number
|
durationYears: number
|
||||||
): number[] {
|
): number[] {
|
||||||
const values = [startValue];
|
const values = [floorToThousand(startValue)];
|
||||||
for (let year = 1; year <= durationYears; year++) {
|
for (let year = 1; year <= durationYears; year++) {
|
||||||
const previous = values[year - 1];
|
const previous = values[year - 1];
|
||||||
values.push(previous * (1 + expectedReturn / 100) + annualContribution);
|
values.push(floorToThousand(previous * (1 + expectedReturn / 100) + annualContribution));
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
@@ -91,11 +95,11 @@ export function computeRealEstateYearly(
|
|||||||
amortization: number,
|
amortization: number,
|
||||||
durationYears: number
|
durationYears: number
|
||||||
): { marketValues: number[]; mortgages: number[] } {
|
): { marketValues: number[]; mortgages: number[] } {
|
||||||
const marketValues = [marketValue];
|
const marketValues = [floorToThousand(marketValue)];
|
||||||
const mortgages = [mortgage];
|
const mortgages = [floorToThousand(mortgage)];
|
||||||
for (let year = 1; year <= durationYears; year++) {
|
for (let year = 1; year <= durationYears; year++) {
|
||||||
marketValues.push(marketValues[year - 1] * (1 + valueGrowth / 100));
|
marketValues.push(floorToThousand(marketValues[year - 1] * (1 + valueGrowth / 100)));
|
||||||
mortgages.push(Math.max(0, mortgages[year - 1] - amortization));
|
mortgages.push(floorToThousand(Math.max(0, mortgages[year - 1] - amortization)));
|
||||||
}
|
}
|
||||||
return { marketValues, mortgages };
|
return { marketValues, mortgages };
|
||||||
}
|
}
|
||||||
@@ -111,7 +115,7 @@ function computeRetirement(
|
|||||||
ahvAmount: info.ahvAmount,
|
ahvAmount: info.ahvAmount,
|
||||||
pkPensionAmount: info.pkPensionAmount,
|
pkPensionAmount: info.pkPensionAmount,
|
||||||
lumpSumAmount: info.lumpSumAmount,
|
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);
|
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
|
// Vereinfachung gemaess TDD 3.3: Gewinn = Verkaufspreis - urspruenglich erfasster Startwert
|
||||||
const gain = Math.max(0, re.salePrice! - re.marketValue);
|
const gain = Math.max(0, re.salePrice! - re.marketValue);
|
||||||
taxAmount = gain * (re.saleTaxRate / 100);
|
taxAmount = gain * (re.saleTaxRate / 100);
|
||||||
saleNetProceeds = re.salePrice! - taxAmount;
|
saleNetProceeds = floorToThousand(re.salePrice! - taxAmount);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: re.id,
|
id: re.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user