"use client"; import { useEffect, useRef, useState } from "react"; import { ChevronDown, ChevronUp } from "lucide-react"; import { InfoBubble } from "@/components/InfoBubble"; import { formatChf, parseChfInput } from "@/lib/format"; const baseInputClass = "w-full rounded-lg border border-border bg-input px-2.5 py-1.5 text-sm text-fg shadow-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/25"; export function FieldLabel({ label, help }: { label: string; help?: string }) { return ( ); } export function NumberField({ label, help, value, onChange, step, min, max, }: { label: string; help?: string; value: number; onChange: (value: number) => void; step?: number; min?: number; max?: number; }) { return (
e.currentTarget.select()} onChange={(e) => onChange(e.target.valueAsNumber || 0)} />
); } // Pflicht-Zahlenfeld, das wirklich LEER sein kann (NumberField erzwingt eine Zahl und hätte // damit immer einen Default -- genau das soll bei Annahmen vermieden werden, siehe // SPEZIFIKATION 9.15/9.18: ein stiller Default wird nicht hinterfragt). Der Wert wird als // String geführt; die Validierung liegt beim Aufrufer. export function RequiredNumberField({ label, help, value, onChange, step = 0.1, suffix, placeholder = "Pflicht", }: { label: string; help?: string; value: string; onChange: (value: string) => void; step?: number; suffix?: string; placeholder?: string; }) { const empty = value.trim() === ""; return (
onChange(e.target.value)} className={`${baseInputClass} ${suffix ? "pr-8" : ""} ${ empty ? "border-danger focus:border-danger" : "" }`} /> {suffix && ( {suffix} )}
{empty &&

Pflichtfeld – bitte ausfüllen.

}
); } // Ganzzahliges Betragsfeld. Zeigt den Wert unfokussiert mit 1'000er-Trennzeichen an, // akzeptiert fokussiert beliebige ganze Zahlen (keine Nachkommastellen) und bietet // Pfeil-Buttons mit Klick-und-Halten-BESCHLEUNIGUNG (1 -> 10 -> 100 -> 1'000 -> ...). // Optionales `max` (und `min`) klammern die Eingabe hart (Live-Cap). export function MoneyInput({ value, onChange, className, min = 0, max, }: { value: number; onChange: (value: number) => void; className?: string; min?: number; max?: number; }) { const [focused, setFocused] = useState(false); const [text, setText] = useState(() => String(Math.round(value || 0))); const valueRef = useRef(value); const inputRef = useRef(null); useEffect(() => { valueRef.current = value; }, [value]); const holdTimeout = useRef | null>(null); const holdInterval = useRef | null>(null); function clamp(v: number): number { let result = Math.round(v); if (min != null) result = Math.max(min, result); if (max != null) result = Math.min(result, Math.round(max)); return result; } function stepBy(delta: number) { onChange(clamp(Math.round(valueRef.current) + delta)); } function stopHold() { if (holdTimeout.current) { clearTimeout(holdTimeout.current); holdTimeout.current = null; } if (holdInterval.current) { clearInterval(holdInterval.current); holdInterval.current = null; } } // Klick = 1 Schritt. Halten: nach 400 ms Wiederholung im 70-ms-Takt, wobei die // Schrittweite mit der Haltedauer wächst (immer schneller). function startHold(sign: number) { stepBy(sign); const start = Date.now(); holdTimeout.current = setTimeout(() => { holdInterval.current = setInterval(() => { const s = (Date.now() - start) / 1000; const mag = s < 1.5 ? 1 : s < 3 ? 10 : s < 4.5 ? 100 : s < 6 ? 1000 : 10000; stepBy(sign * mag); }, 70); }, 400); } useEffect(() => stopHold, []); const arrowBtn = "flex flex-1 items-center justify-center text-faint hover:bg-accent-soft hover:text-accent-soft-fg"; return (
{ setFocused(true); const current = Math.round(value || 0); // Default-0 sofort leeren; sonst Wert markieren, damit man ihn überschreiben kann. setText(current === 0 ? "" : String(current)); requestAnimationFrame(() => inputRef.current?.select()); }} onChange={(e) => setText(e.target.value.replace(min < 0 ? /[^0-9-]/g : /[^0-9]/g, ""))} onKeyDown={(e) => { if (e.key === "ArrowUp") { e.preventDefault(); stepBy(e.shiftKey ? 100 : 1); } else if (e.key === "ArrowDown") { e.preventDefault(); stepBy(e.shiftKey ? -100 : -1); } }} onBlur={() => { setFocused(false); onChange(clamp(parseChfInput(text))); }} />
); } export function MoneyField({ label, help, value, onChange, min, max, }: { label: string; help?: string; value: number; onChange: (value: number) => void; min?: number; max?: number; }) { return (
); } export function TextField({ label, help, value, onChange, placeholder, }: { label: string; help?: string; value: string; onChange: (value: string) => void; placeholder?: string; }) { return (
onChange(e.target.value)} />
); } export function SelectField({ label, help, value, onChange, options, }: { label: string; help?: string; value: T; onChange: (value: T) => void; options: { value: T; label: string }[]; }) { return (
); } // --- "Aus Vorphase uebernehmen" (Roadmap Nr. 44, Punkt A) -------------------------------- // // Wiederkehr-Parameter (Raten, Beitraege, Amortisation) gelten weiter, bis man sie bewusst // aendert. Bisher war das unsichtbar: Beim Anlegen einer Phase wurde der Wert KOPIERT, und // spaetere Aenderungen an der Vorphase kamen deshalb nie an. Jetzt entscheidet ein sichtbares // Haekchen: angehakt = Feld leer lassen und live erben; abgehakt = eigener Wert fuer diese // Phase. In der ersten Phase gibt es nichts zu erben -- dort erscheint das Haekchen nicht. export function InheritableField({ label, help, canInherit, isInheriting, inheritedDisplay, onToggle, children, }: { label: string; help?: string; canInherit: boolean; isInheriting: boolean; inheritedDisplay: string; onToggle: (inherit: boolean) => void; children: React.ReactNode; }) { return (
{isInheriting ? ( <>
{inheritedDisplay} (aus der Vorphase)
) : ( children )} {canInherit && ( )}
); }