Visual redesign: indigo accent palette, lucide icons per section, lighter/softer cards and dark mode
Deploy App / deploy (push) Successful in 2m59s

This commit is contained in:
2026-07-10 22:19:10 +02:00
parent e9dacc2026
commit 2e65bb3a2d
13 changed files with 197 additions and 114 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--background: #f8fafc;
--foreground: #1e293b;
}
@theme inline {
@@ -14,8 +14,8 @@
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--background: #10131c;
--foreground: #e2e8f0;
}
}
+24 -16
View File
@@ -2,6 +2,7 @@
import { Suspense, useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Lock, PiggyBank } from "lucide-react";
function LoginForm() {
const router = useRouter();
@@ -59,9 +60,10 @@ function LoginForm() {
<div className="flex flex-1 items-center justify-center px-4">
<form
onSubmit={handleSubmit}
className="flex w-full max-w-sm flex-col gap-4 rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900"
className="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-zinc-200/70 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900"
>
<h1 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
<h1 className="flex items-center gap-2 text-lg font-semibold text-zinc-900 dark:text-zinc-50">
<PiggyBank className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
{passwordSet ? "FPT — Anmelden" : "FPT — Passwort festlegen"}
</h1>
{!passwordSet && (
@@ -70,28 +72,34 @@ function LoginForm() {
um den Zugriff auf Ihre Finanzplanung zu schuetzen.
</p>
)}
<input
type="password"
autoFocus
placeholder="Passwort"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 focus:border-zinc-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
/>
{!passwordSet && (
<div className="relative">
<Lock className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
<input
type="password"
placeholder="Passwort bestaetigen"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
className="w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 focus:border-zinc-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
autoFocus
placeholder="Passwort"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border border-zinc-300 bg-white py-2 pl-9 pr-3 text-sm text-zinc-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
/>
</div>
{!passwordSet && (
<div className="relative">
<Lock className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
<input
type="password"
placeholder="Passwort bestaetigen"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
className="w-full rounded-lg border border-zinc-300 bg-white py-2 pl-9 pr-3 text-sm text-zinc-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
/>
</div>
)}
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
<button
type="submit"
disabled={loading}
className="rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{loading ? "..." : passwordSet ? "Anmelden" : "Passwort festlegen"}
</button>
+30 -23
View File
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { LogOut, PiggyBank, Plus, Settings, X } from "lucide-react";
import { PhaseCard } from "@/components/PhaseCard";
import { TransitionPanel } from "@/components/TransitionPanel";
import { Dashboard } from "@/components/Dashboard";
@@ -91,15 +92,17 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
return (
<div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-8">
<header className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-zinc-900 dark:text-zinc-50">
<h1 className="flex items-center gap-2 text-xl font-semibold text-zinc-900 dark:text-zinc-50">
<PiggyBank className="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
Financial Planning Tool
</h1>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setShowSettings((v) => !v)}
className="rounded-md border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
className="flex items-center gap-1.5 rounded-lg border border-zinc-300 bg-white px-3 py-1.5 text-xs font-medium text-zinc-700 shadow-sm hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
>
<Settings className="h-3.5 w-3.5" />
Grundprofil
</button>
<button
@@ -108,8 +111,9 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
await api.post("/api/auth/logout");
window.location.href = "/login";
}}
className="rounded-md border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
className="flex items-center gap-1.5 rounded-lg border border-zinc-300 bg-white px-3 py-1.5 text-xs font-medium text-zinc-700 shadow-sm hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
>
<LogOut className="h-3.5 w-3.5" />
Abmelden
</button>
</div>
@@ -124,15 +128,15 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
)}
{/* Tab-Leiste */}
<div className="flex flex-wrap items-center gap-2 border-b border-zinc-200 pb-2 dark:border-zinc-700">
<div className="flex flex-wrap items-center gap-2 border-b border-zinc-200 pb-2 dark:border-zinc-800">
{plans.map((p) => (
<div key={p.id} className="flex items-center">
<button
type="button"
onClick={() => setSelectedPlanId(p.id)}
className={`rounded-t-md px-3 py-1.5 text-sm font-medium ${
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
selectedPlanId === p.id
? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
? "bg-indigo-600 text-white shadow-sm dark:bg-indigo-500"
: "text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
}`}
>
@@ -142,10 +146,10 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
<button
type="button"
onClick={() => handleDeletePlan(p.id)}
className="ml-1 text-xs text-zinc-400 hover:text-red-600"
className="ml-1 text-zinc-400 hover:text-red-600"
aria-label="Plan loeschen"
>
<X className="h-3.5 w-3.5" />
</button>
)}
</div>
@@ -154,9 +158,10 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
<button
type="button"
onClick={() => setShowNewPlan((v) => !v)}
className="rounded-md px-2 py-1.5 text-sm text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"
className="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm text-indigo-600 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-500/10"
>
+ Plan
<Plus className="h-3.5 w-3.5" />
Plan
</button>
{showNewPlan && (
<NewPlanPopover
@@ -174,9 +179,10 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
<button
type="button"
onClick={() => setShowScenario((v) => !v)}
className="rounded-md px-2 py-1.5 text-sm text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"
className="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm text-indigo-600 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-500/10"
>
+ Szenario
<Plus className="h-3.5 w-3.5" />
Szenario
</button>
{showScenario && (
<ScenarioPopover
@@ -235,9 +241,10 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
<button
type="button"
onClick={handleAddPhase}
className="self-start rounded-md border border-zinc-300 px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
className="flex items-center gap-1.5 self-start rounded-lg border border-dashed border-indigo-300 bg-indigo-50/50 px-3 py-2 text-sm font-medium text-indigo-700 hover:bg-indigo-50 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300 dark:hover:bg-indigo-500/20"
>
+ Phase hinzufuegen
<Plus className="h-4 w-4" />
Phase hinzufuegen
</button>
{detail.plan.phases.length > 0 && (
@@ -252,9 +259,9 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
function NewPlanPopover({ onCreate, onClose }: { onCreate: (name: string) => void; onClose: () => void }) {
const [name, setName] = useState("Basisplan");
return (
<div className="absolute left-0 top-10 z-10 w-64 rounded-md border border-zinc-200 bg-white p-3 shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
<div className="absolute left-0 top-10 z-10 w-64 rounded-lg border border-zinc-200 bg-white p-3 shadow-lg dark:border-zinc-700 dark:bg-zinc-800">
<input
className="mb-2 w-full rounded-md border border-zinc-300 px-2 py-1 text-sm dark:border-zinc-600 dark:bg-zinc-800"
className="mb-2 w-full rounded-lg border border-zinc-300 px-2 py-1 text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-600 dark:bg-zinc-900"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name des Plans"
@@ -263,11 +270,11 @@ function NewPlanPopover({ onCreate, onClose }: { onCreate: (name: string) => voi
<button
type="button"
onClick={() => onCreate(name)}
className="rounded-md bg-zinc-900 px-2 py-1 text-xs text-white dark:bg-zinc-100 dark:text-zinc-900"
className="rounded-lg bg-indigo-600 px-2 py-1 text-xs font-medium text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Erstellen
</button>
<button type="button" onClick={onClose} className="text-xs text-zinc-500">
<button type="button" onClick={onClose} className="text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300">
Abbrechen
</button>
</div>
@@ -287,16 +294,16 @@ function ScenarioPopover({
const [name, setName] = useState("Neues Szenario");
const [branchFromPhaseId, setBranchFromPhaseId] = useState(phases[phases.length - 1]?.id ?? "");
return (
<div className="absolute left-0 top-10 z-10 w-72 rounded-md border border-zinc-200 bg-white p-3 shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
<div className="absolute left-0 top-10 z-10 w-72 rounded-lg border border-zinc-200 bg-white p-3 shadow-lg dark:border-zinc-700 dark:bg-zinc-800">
<input
className="mb-2 w-full rounded-md border border-zinc-300 px-2 py-1 text-sm dark:border-zinc-600 dark:bg-zinc-800"
className="mb-2 w-full rounded-lg border border-zinc-300 px-2 py-1 text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-600 dark:bg-zinc-900"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name des Szenarios"
/>
<label className="mb-1 block text-xs text-zinc-500">Verzweigen ab Phase</label>
<select
className="mb-2 w-full rounded-md border border-zinc-300 px-2 py-1 text-sm dark:border-zinc-600 dark:bg-zinc-800"
className="mb-2 w-full rounded-lg border border-zinc-300 px-2 py-1 text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-600 dark:bg-zinc-900"
value={branchFromPhaseId}
onChange={(e) => setBranchFromPhaseId(e.target.value)}
>
@@ -310,11 +317,11 @@ function ScenarioPopover({
<button
type="button"
onClick={() => onCreate(name, branchFromPhaseId)}
className="rounded-md bg-zinc-900 px-2 py-1 text-xs text-white dark:bg-zinc-100 dark:text-zinc-900"
className="rounded-lg bg-indigo-600 px-2 py-1 text-xs font-medium text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Erstellen
</button>
<button type="button" onClick={onClose} className="text-xs text-zinc-500">
<button type="button" onClick={onClose} className="text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300">
Abbrechen
</button>
</div>
+14 -8
View File
@@ -2,13 +2,14 @@
import { useMemo, useState } from "react";
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { BarChart3, Download, LineChart as LineChartIcon } from "lucide-react";
import { WealthChart, type TimelineSeries } from "@/components/WealthChart";
import { api } from "@/lib/api-client";
import { formatChf } from "@/lib/format";
import type { PlanComputed } from "@/lib/calculations";
import type { PlanInput } from "@/lib/types";
const PALETTE = ["#3f3f46", "#2563eb", "#16a34a", "#d97706", "#dc2626", "#7c3aed"];
const PALETTE = ["#4f46e5", "#0ea5e9", "#16a34a", "#d97706", "#dc2626", "#7c3aed"];
interface PlanListItem {
id: string;
@@ -80,13 +81,17 @@ export function Dashboard({
<StatCard label="Geschaetzter Nachlass" value={computed.nachlass} help="Endvermoegen der letzten Phase - potenziell vererbbar." />
</div>
<section className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<section className="rounded-xl border border-zinc-200/70 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-zinc-800 dark:text-zinc-100">Vermoegensverlauf</h3>
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
<LineChartIcon className="h-4 w-4 text-indigo-500 dark:text-indigo-400" />
Vermoegensverlauf
</h3>
<a
href={`/api/plans/${plan.id}/export`}
className="rounded-md border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
className="flex items-center gap-1.5 rounded-lg border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
<Download className="h-3.5 w-3.5" />
CSV-Export
</a>
</div>
@@ -108,8 +113,9 @@ export function Dashboard({
<WealthChart series={series} />
</section>
<section className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<h3 className="mb-3 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
<section className="rounded-xl border border-zinc-200/70 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<h3 className="mb-3 flex items-center gap-1.5 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
<BarChart3 className="h-4 w-4 text-indigo-500 dark:text-indigo-400" />
Vermoegensaufteilung pro Phase (Endvermoegen)
</h3>
<div className="h-72 w-full">
@@ -136,11 +142,11 @@ export function Dashboard({
function StatCard({ label, value, help }: { label: string; value: number; help?: string }) {
return (
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<div className="rounded-xl border border-zinc-200/70 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="text-xs text-zinc-500 dark:text-zinc-400" title={help}>
{label}
</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-indigo-600 dark:text-indigo-400">
{formatChf(value)} CHF
</div>
</div>
+7 -6
View File
@@ -1,11 +1,12 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { InfoBubble } from "@/components/InfoBubble";
import { floorToThousand, formatChf, parseChfInput } from "@/lib/format";
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-lg border border-zinc-300 bg-white px-2.5 py-1.5 text-sm text-zinc-900 shadow-sm transition-colors focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100";
export function FieldLabel({ label, help }: { label: string; help?: string }) {
return (
@@ -114,7 +115,7 @@ export function MoneyInput({
onChange(floorToThousand(parseChfInput(text)));
}}
/>
<div className="absolute inset-y-0 right-0 flex w-5 flex-col overflow-hidden rounded-r-md border-l border-zinc-300 dark:border-zinc-600">
<div className="absolute inset-y-0 right-0 flex w-6 flex-col overflow-hidden rounded-r-lg border-l border-zinc-300 dark:border-zinc-700">
<button
type="button"
tabIndex={-1}
@@ -130,9 +131,9 @@ export function MoneyInput({
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-zinc-500 hover:bg-indigo-50 hover:text-indigo-600 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-indigo-400"
>
<ChevronUp className="h-3 w-3" />
</button>
<button
type="button"
@@ -149,9 +150,9 @@ export function MoneyInput({
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-zinc-500 hover:bg-indigo-50 hover:text-indigo-600 dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-indigo-400"
>
<ChevronDown className="h-3 w-3" />
</button>
</div>
</div>
+3 -3
View File
@@ -50,7 +50,7 @@ export function HouseholdSettings({
}
return (
<div className="flex flex-col gap-4 rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<div className="flex flex-col gap-4 rounded-xl border border-zinc-200/70 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<SelectField
label="Haushaltsform"
value={householdType}
@@ -88,14 +88,14 @@ export function HouseholdSettings({
type="button"
disabled={saving}
onClick={handleSubmit}
className="rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900"
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{saving ? "Speichern..." : "Speichern"}
</button>
<button
type="button"
onClick={onClose}
className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:text-zinc-200"
className="rounded-lg border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
Abbrechen
</button>
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { Info } from "lucide-react";
export function InfoBubble({ text }: { text: string }) {
const [open, setOpen] = useState(false);
@@ -13,12 +14,12 @@ export function InfoBubble({ text }: { text: string }) {
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onClick={() => setOpen((o) => !o)}
className="flex h-4 w-4 items-center justify-center rounded-full bg-zinc-200 text-[10px] font-semibold text-zinc-600 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-600"
className="flex h-4 w-4 items-center justify-center rounded-full bg-indigo-100 text-indigo-500 hover:bg-indigo-200 dark:bg-indigo-500/20 dark:text-indigo-300 dark:hover:bg-indigo-500/30"
>
i
<Info className="h-2.5 w-2.5" strokeWidth={2.5} />
</button>
{open && (
<span className="absolute left-1/2 top-6 z-20 w-64 -translate-x-1/2 rounded-md border border-zinc-200 bg-white p-2 text-xs leading-snug text-zinc-700 shadow-lg dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">
<span className="absolute left-1/2 top-6 z-20 w-64 -translate-x-1/2 rounded-lg border border-zinc-200 bg-white p-2.5 text-xs leading-snug text-zinc-700 shadow-lg dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">
{text}
</span>
)}
+10 -6
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { PiggyBank } from "lucide-react";
import { api } from "@/lib/api-client";
import { NumberField, SelectField } from "@/components/FormField";
import type { HouseholdInput, HouseholdType, PersonRole } from "@/lib/types";
@@ -52,14 +53,17 @@ export function Onboarding({ onDone }: { onDone: (household: HouseholdInput) =>
return (
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col justify-center px-6 py-16">
<h1 className="mb-2 text-2xl font-semibold text-zinc-900 dark:text-zinc-50">
Willkommen beim Financial Planning Tool
</h1>
<div className="mb-2 flex items-center gap-2">
<PiggyBank className="h-7 w-7 text-indigo-600 dark:text-indigo-400" />
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-50">
Willkommen beim Financial Planning Tool
</h1>
</div>
<p className="mb-8 text-sm text-zinc-600 dark:text-zinc-400">
Bevor es losgeht, brauchen wir ein paar Eckdaten zu Ihrem Haushalt.
</p>
<div className="flex flex-col gap-5 rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900">
<div className="flex flex-col gap-5 rounded-xl border border-zinc-200/70 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<SelectField
label="Haushaltsform"
help="Waehlen Sie, ob Sie alleine oder gemeinsam mit einer Partnerin / einem Partner planen."
@@ -72,7 +76,7 @@ export function Onboarding({ onDone }: { onDone: (household: HouseholdInput) =>
/>
{persons.map((person, index) => (
<div key={person.role} className="grid grid-cols-2 gap-3 rounded-md bg-zinc-50 p-3 dark:bg-zinc-800/50">
<div key={person.role} className="grid grid-cols-2 gap-3 rounded-xl border border-zinc-100 bg-zinc-50/60 p-3 dark:border-zinc-800 dark:bg-zinc-800/30">
<div className="col-span-2 text-xs font-semibold uppercase tracking-wide text-zinc-500">
{householdType === "COUPLE" ? (person.role === "PERSON_A" ? "Person A" : "Person B") : "Ihre Angaben"}
</div>
@@ -105,7 +109,7 @@ export function Onboarding({ onDone }: { onDone: (household: HouseholdInput) =>
type="button"
disabled={saving}
onClick={handleSubmit}
className="mt-2 rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
className="mt-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{saving ? "Speichern..." : "Weiter"}
</button>
+11 -5
View File
@@ -2,6 +2,7 @@
import { useState } from "react";
import { LineChart, Line, ResponsiveContainer } from "recharts";
import { AlertTriangle, ChevronDown, ChevronRight, Trash2 } from "lucide-react";
import { PhaseForm } from "@/components/PhaseForm";
import { api } from "@/lib/api-client";
import { formatChf } from "@/lib/format";
@@ -43,19 +44,23 @@ export function PhaseCard({
}
return (
<div className="overflow-hidden rounded-lg border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
<div className="overflow-hidden rounded-xl border border-zinc-200/70 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="flex w-full items-center gap-4 px-4 py-3 text-left hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
>
<span className="text-zinc-400">{expanded ? "▾" : "▸"}</span>
<span className="text-indigo-500 dark:text-indigo-400">
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</span>
<div className="flex-1">
<div className="flex items-baseline gap-2">
<span className="font-medium text-zinc-900 dark:text-zinc-100">{phase.name}</span>
<span className="text-xs text-zinc-500">{phase.durationYears} Jahre</span>
{computed.savingsWarning && (
<span className="text-xs text-amber-600 dark:text-amber-400"> Sparquote ueberschritten</span>
<span className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
<AlertTriangle className="h-3 w-3" /> Sparquote ueberschritten
</span>
)}
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400">
@@ -65,7 +70,7 @@ export function PhaseCard({
<div className="h-8 w-24">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={sparklineData}>
<Line type="monotone" dataKey="value" stroke="#3f3f46" strokeWidth={1.5} dot={false} />
<Line type="monotone" dataKey="value" stroke="#4f46e5" strokeWidth={1.5} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
@@ -77,8 +82,9 @@ export function PhaseCard({
e.stopPropagation();
handleDelete();
}}
className="rounded-md border border-zinc-300 px-2 py-1 text-xs text-zinc-500 hover:bg-red-50 hover:text-red-600 dark:border-zinc-600 dark:hover:bg-red-950"
className="flex items-center gap-1 rounded-lg border border-zinc-300 px-2 py-1 text-xs text-zinc-500 hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:border-zinc-700 dark:hover:border-red-500/30 dark:hover:bg-red-950"
>
<Trash2 className="h-3.5 w-3.5" />
{deleting ? "…" : "Loeschen"}
</span>
)}
+64 -31
View File
@@ -1,6 +1,19 @@
"use client";
import { useState } from "react";
import {
AlertTriangle,
CheckCircle2,
Gift,
Home,
Plus,
PiggyBank,
ShoppingCart,
TrendingUp,
Wallet,
X,
XCircle,
} from "lucide-react";
import { MoneyField, NumberField, SelectField, TextField } from "@/components/FormField";
import { api } from "@/lib/api-client";
import { formatChf } from "@/lib/format";
@@ -147,7 +160,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</section>
{/* Einkommen */}
<Section title="Einkommen">
<Section title="Einkommen" icon={<Wallet className="h-4 w-4" />}>
{incomeEntries.map((entry, i) => (
<div key={entry.id} className="grid grid-cols-[1fr_1fr_auto] items-end gap-2">
{incomeMode === "PER_PERSON" ? (
@@ -183,7 +196,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div>
))}
<AddButton
label="+ Einkommensposten"
label="Einkommensposten"
onClick={() =>
setIncomeEntries((prev) => [
...prev,
@@ -194,7 +207,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</Section>
{/* Ausgaben */}
<Section title="Ausgaben">
<Section title="Ausgaben" icon={<ShoppingCart className="h-4 w-4" />}>
{expenseEntries.map((entry, i) => (
<div key={entry.id} className="grid grid-cols-[1fr_auto] items-end gap-2">
<MoneyField
@@ -209,19 +222,19 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div>
))}
<AddButton
label="+ Ausgabenposten"
label="Ausgabenposten"
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-xl bg-indigo-50/60 px-3 py-2 text-sm text-zinc-600 dark:bg-indigo-500/10 dark:text-zinc-300">
Verfuegbare Sparquote (CHF/Jahr): <strong>{formatChf(savingsQuota)}</strong>
{" "}(Details und Verteilung siehe Wertschriften weiter unten)
</div>
</Section>
{/* Wertschriften */}
<Section title="Wertschriften">
<Section title="Wertschriften" icon={<TrendingUp className="h-4 w-4" />}>
{securities.map((s, i) => (
<div key={s.id} className="grid grid-cols-2 gap-2 rounded-md bg-zinc-50 p-3 sm:grid-cols-5 dark:bg-zinc-800/50">
<div key={s.id} className="grid grid-cols-2 gap-2 rounded-xl border border-zinc-100 bg-zinc-50/60 p-3 sm:grid-cols-5 dark:border-zinc-800 dark:bg-zinc-800/30">
<TextField
label="Name"
help="Frei waehlbare Bezeichnung, z. B. 'Globaler ETF' oder 'Schweizer Aktien'."
@@ -266,7 +279,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div>
))}
<AddButton
label="+ Wertschrift"
label="Wertschrift"
onClick={() =>
setSecurities((prev) => [
...prev,
@@ -283,7 +296,7 @@ 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-xl bg-indigo-50/60 px-3 py-2 text-sm dark:bg-indigo-500/10">
<div className="flex items-center gap-2">
<StatusDot ok={!startCapitalRemaining} />
Verfuegbares Startkapital (aus Verkaeufen der Vorphase): <strong>{formatChf(phase.incomingCapital)}</strong> CHF
@@ -295,17 +308,18 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
{" "} zugewiesen: {formatChf(allocated)}
</div>
{overAllocated && (
<div className="text-amber-700 dark:text-amber-400">
Die zugewiesenen Sparbeitraege uebersteigen die verfuegbare Sparquote.
<div className="flex items-center gap-1.5 text-amber-700 dark:text-amber-400">
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
Die zugewiesenen Sparbeitraege uebersteigen die verfuegbare Sparquote.
</div>
)}
</div>
</Section>
{/* Immobilien */}
<Section title="Immobilien">
<Section title="Immobilien" icon={<Home className="h-4 w-4" />}>
{realEstates.map((re, i) => (
<div key={re.id} className="grid grid-cols-2 gap-2 rounded-md bg-zinc-50 p-3 sm:grid-cols-4 dark:bg-zinc-800/50">
<div key={re.id} className="grid grid-cols-2 gap-2 rounded-xl border border-zinc-100 bg-zinc-50/60 p-3 sm:grid-cols-4 dark:border-zinc-800 dark:bg-zinc-800/30">
<TextField
label="Bezeichnung"
help="Z. B. 'Eigenheim' oder 'Ferienwohnung'."
@@ -336,7 +350,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div>
))}
<AddButton
label="+ Immobilie"
label="Immobilie"
onClick={() =>
setRealEstates((prev) => [
...prev,
@@ -347,7 +361,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</Section>
{/* Sondereinnahmen/-ausgaben */}
<Section title="Sondereinnahmen / -ausgaben">
<Section title="Sondereinnahmen / -ausgaben" icon={<Gift className="h-4 w-4" />}>
{oneTimeEvents.map((ev, i) => (
<div key={ev.id} className="grid grid-cols-[auto_1fr_2fr_auto] items-end gap-2">
<SelectField
@@ -374,15 +388,15 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div>
))}
<AddButton
label="+ Sondereinnahme/-ausgabe"
label="Sondereinnahme/-ausgabe"
onClick={() => setOneTimeEvents((prev) => [...prev, { id: tempId(), type: "EXPENSE", amount: 0, description: null }])}
/>
</Section>
{/* Pensionierung */}
<Section title="Pensionierung (optional)">
<Section title="Pensionierung (optional)" icon={<PiggyBank className="h-4 w-4" />}>
{retirementInfos.map((r, i) => (
<div key={r.id} className="grid grid-cols-2 gap-2 rounded-md bg-zinc-50 p-3 sm:grid-cols-4 dark:bg-zinc-800/50">
<div key={r.id} className="grid grid-cols-2 gap-2 rounded-xl border border-zinc-100 bg-zinc-50/60 p-3 sm:grid-cols-4 dark:border-zinc-800 dark:bg-zinc-800/30">
<SelectField
label="Person"
value={r.personId}
@@ -419,7 +433,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div>
))}
<AddButton
label="+ Pensionierungsangaben"
label="Pensionierungsangaben"
onClick={() =>
setRetirementInfos((prev) => [
...prev,
@@ -436,21 +450,26 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
/>
</Section>
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
{error && (
<p className="flex items-center gap-1.5 text-sm text-red-600 dark:text-red-400">
<AlertTriangle className="h-4 w-4 shrink-0" />
{error}
</p>
)}
<div className="flex gap-2">
<button
type="button"
disabled={saving}
onClick={handleSave}
className="rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{saving ? "Speichern..." : "Speichern"}
</button>
<button
type="button"
onClick={onCancel}
className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
className="rounded-lg border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
Abbrechen
</button>
@@ -459,10 +478,21 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
function Section({
title,
icon,
children,
}: {
title: string;
icon: React.ReactNode;
children: React.ReactNode;
}) {
return (
<section className="flex flex-col gap-2">
<h4 className="text-sm font-semibold text-zinc-800 dark:text-zinc-100">{title}</h4>
<h4 className="flex items-center gap-1.5 text-sm font-semibold text-zinc-800 dark:text-zinc-100">
<span className="text-indigo-500 dark:text-indigo-400">{icon}</span>
{title}
</h4>
{children}
</section>
);
@@ -473,19 +503,22 @@ function AddButton({ label, onClick }: { label: string; onClick: () => void }) {
<button
type="button"
onClick={onClick}
className="self-start text-xs font-medium text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
className="flex items-center gap-1 self-start text-xs font-medium text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
>
<Plus className="h-3.5 w-3.5" />
{label}
</button>
);
}
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"}`}
return ok ? (
<CheckCircle2
className="h-4 w-4 shrink-0 text-emerald-500"
aria-label="Vollstaendig verteilt"
/>
) : (
<XCircle className="h-4 w-4 shrink-0 text-red-500" aria-label="Noch nicht vollstaendig verteilt" />
);
}
@@ -495,9 +528,9 @@ function RemoveButton({ onClick }: { onClick: () => void }) {
type="button"
onClick={onClick}
aria-label="Entfernen"
className="rounded-md border border-zinc-300 px-2 py-1.5 text-xs text-zinc-500 hover:bg-red-50 hover:text-red-600 dark:border-zinc-600 dark:hover:bg-red-950"
className="rounded-lg border border-zinc-300 px-2 py-1.5 text-zinc-500 hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:border-zinc-700 dark:hover:border-red-500/30 dark:hover:bg-red-950"
>
<X className="h-3.5 w-3.5" />
</button>
);
}
+15 -9
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { ArrowDown, CheckCircle2 } from "lucide-react";
import { MoneyInput } from "@/components/FormField";
import { api } from "@/lib/api-client";
import { floorToThousand, formatChf } from "@/lib/format";
@@ -116,7 +117,7 @@ export function TransitionPanel({
if (!loaded) {
return (
<div className="mx-2 rounded-md bg-zinc-100 px-4 py-3 text-xs text-zinc-500 dark:bg-zinc-800">
<div className="mx-2 rounded-xl bg-zinc-100 px-4 py-3 text-xs text-zinc-500 dark:bg-zinc-800">
Uebergang wird geladen
</div>
);
@@ -166,8 +167,9 @@ export function TransitionPanel({
}
return (
<div className="mx-2 flex flex-col gap-3 rounded-md border border-dashed border-zinc-300 bg-zinc-50 p-4 dark:border-zinc-600 dark:bg-zinc-800/40">
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
<div className="mx-2 flex flex-col gap-3 rounded-xl border border-dashed border-indigo-200 bg-indigo-50/40 p-4 dark:border-indigo-500/30 dark:bg-indigo-500/5">
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-indigo-600 dark:text-indigo-400">
<ArrowDown className="h-3.5 w-3.5" />
Uebergang {nextPhaseName}
</div>
<table className="w-full text-sm">
@@ -181,11 +183,11 @@ export function TransitionPanel({
</thead>
<tbody>
{items.map((it, i) => (
<tr key={`${it.positionType}-${it.id}`} className="border-t border-zinc-200 dark:border-zinc-700">
<tr key={`${it.positionType}-${it.id}`} className="border-t border-zinc-200 dark:border-zinc-800">
<td className="py-2 pr-2">{it.name}</td>
<td className="py-2 pr-2">
<select
className="rounded-md border border-zinc-300 bg-white px-2 py-1 text-xs dark:border-zinc-600 dark:bg-zinc-900"
className="rounded-lg border border-zinc-300 bg-white px-2 py-1 text-xs focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-700 dark:bg-zinc-900"
value={it.decision}
onChange={(e) =>
setItems((prev) =>
@@ -218,7 +220,7 @@ export function TransitionPanel({
{it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? (
<input
type="number"
className="w-20 rounded-md border border-zinc-300 bg-white px-2 py-1 text-xs dark:border-zinc-600 dark:bg-zinc-900"
className="w-20 rounded-lg border border-zinc-300 bg-white px-2 py-1 text-xs focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:border-zinc-700 dark:bg-zinc-900"
value={it.saleTaxRate}
onChange={(e) =>
setItems((prev) =>
@@ -234,7 +236,7 @@ export function TransitionPanel({
))}
</tbody>
</table>
<div className="rounded-md bg-white px-3 py-2 text-sm dark:bg-zinc-900">
<div className="rounded-xl bg-white px-3 py-2 text-sm shadow-sm dark:bg-zinc-900">
Verfuegbares Startkapital fuer neue Phase (aus Verkaeufen): <strong>{formatChf(totalAvailableCapital)} CHF</strong>
<p className="mt-1 text-xs text-zinc-500">
Wird beim Speichern automatisch in &quot;{nextPhaseName}&quot; als verfuegbares Startkapital hinterlegt.
@@ -248,11 +250,15 @@ export function TransitionPanel({
type="button"
disabled={saving}
onClick={handleSave}
className="self-start rounded-md bg-zinc-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-zinc-700 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900"
className="self-start rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{saving ? "Speichern..." : "Uebergang speichern"}
</button>
{saved && <span className="text-xs text-emerald-600 dark:text-emerald-400">Gespeichert.</span>}
{saved && (
<span className="flex items-center gap-1 text-xs text-emerald-600 dark:text-emerald-400">
<CheckCircle2 className="h-3.5 w-3.5" /> Gespeichert.
</span>
)}
</div>
</div>
);