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
+10
View File
@@ -13,6 +13,7 @@
"@prisma/client": "^7.8.0", "@prisma/client": "^7.8.0",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"jose": "^6.2.3", "jose": "^6.2.3",
"lucide-react": "^1.24.0",
"next": "16.2.10", "next": "16.2.10",
"pg": "^8.22.0", "pg": "^8.22.0",
"prisma": "^7.8.0", "prisma": "^7.8.0",
@@ -6101,6 +6102,15 @@
"url": "https://github.com/sponsors/wellwelwel" "url": "https://github.com/sponsors/wellwelwel"
} }
}, },
"node_modules/lucide-react": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
"integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+1
View File
@@ -14,6 +14,7 @@
"@prisma/client": "^7.8.0", "@prisma/client": "^7.8.0",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"jose": "^6.2.3", "jose": "^6.2.3",
"lucide-react": "^1.24.0",
"next": "16.2.10", "next": "16.2.10",
"pg": "^8.22.0", "pg": "^8.22.0",
"prisma": "^7.8.0", "prisma": "^7.8.0",
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: #ffffff; --background: #f8fafc;
--foreground: #171717; --foreground: #1e293b;
} }
@theme inline { @theme inline {
@@ -14,8 +14,8 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background: #0a0a0a; --background: #10131c;
--foreground: #ededed; --foreground: #e2e8f0;
} }
} }
+13 -5
View File
@@ -2,6 +2,7 @@
import { Suspense, useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { Lock, PiggyBank } from "lucide-react";
function LoginForm() { function LoginForm() {
const router = useRouter(); const router = useRouter();
@@ -59,9 +60,10 @@ function LoginForm() {
<div className="flex flex-1 items-center justify-center px-4"> <div className="flex flex-1 items-center justify-center px-4">
<form <form
onSubmit={handleSubmit} 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"} {passwordSet ? "FPT — Anmelden" : "FPT — Passwort festlegen"}
</h1> </h1>
{!passwordSet && ( {!passwordSet && (
@@ -70,28 +72,34 @@ function LoginForm() {
um den Zugriff auf Ihre Finanzplanung zu schuetzen. um den Zugriff auf Ihre Finanzplanung zu schuetzen.
</p> </p>
)} )}
<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 <input
type="password" type="password"
autoFocus autoFocus
placeholder="Passwort" placeholder="Passwort"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} 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" 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 && ( {!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 <input
type="password" type="password"
placeholder="Passwort bestaetigen" placeholder="Passwort bestaetigen"
value={passwordConfirm} value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)} 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" 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>} {error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
<button <button
type="submit" type="submit"
disabled={loading} 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"} {loading ? "..." : passwordSet ? "Anmelden" : "Passwort festlegen"}
</button> </button>
+30 -23
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { LogOut, PiggyBank, Plus, Settings, X } from "lucide-react";
import { PhaseCard } from "@/components/PhaseCard"; import { PhaseCard } from "@/components/PhaseCard";
import { TransitionPanel } from "@/components/TransitionPanel"; import { TransitionPanel } from "@/components/TransitionPanel";
import { Dashboard } from "@/components/Dashboard"; import { Dashboard } from "@/components/Dashboard";
@@ -91,15 +92,17 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
return ( return (
<div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-8"> <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"> <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 Financial Planning Tool
</h1> </h1>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => setShowSettings((v) => !v)} 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 Grundprofil
</button> </button>
<button <button
@@ -108,8 +111,9 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
await api.post("/api/auth/logout"); await api.post("/api/auth/logout");
window.location.href = "/login"; 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 Abmelden
</button> </button>
</div> </div>
@@ -124,15 +128,15 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
)} )}
{/* Tab-Leiste */} {/* 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) => ( {plans.map((p) => (
<div key={p.id} className="flex items-center"> <div key={p.id} className="flex items-center">
<button <button
type="button" type="button"
onClick={() => setSelectedPlanId(p.id)} 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 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" : "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 <button
type="button" type="button"
onClick={() => handleDeletePlan(p.id)} 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" aria-label="Plan loeschen"
> >
<X className="h-3.5 w-3.5" />
</button> </button>
)} )}
</div> </div>
@@ -154,9 +158,10 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
<button <button
type="button" type="button"
onClick={() => setShowNewPlan((v) => !v)} 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> </button>
{showNewPlan && ( {showNewPlan && (
<NewPlanPopover <NewPlanPopover
@@ -174,9 +179,10 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
<button <button
type="button" type="button"
onClick={() => setShowScenario((v) => !v)} 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> </button>
{showScenario && ( {showScenario && (
<ScenarioPopover <ScenarioPopover
@@ -235,9 +241,10 @@ export function AppShell({ initialHousehold }: { initialHousehold: HouseholdInpu
<button <button
type="button" type="button"
onClick={handleAddPhase} 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> </button>
{detail.plan.phases.length > 0 && ( {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 }) { function NewPlanPopover({ onCreate, onClose }: { onCreate: (name: string) => void; onClose: () => void }) {
const [name, setName] = useState("Basisplan"); const [name, setName] = useState("Basisplan");
return ( 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 <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} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="Name des Plans" placeholder="Name des Plans"
@@ -263,11 +270,11 @@ function NewPlanPopover({ onCreate, onClose }: { onCreate: (name: string) => voi
<button <button
type="button" type="button"
onClick={() => onCreate(name)} 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 Erstellen
</button> </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 Abbrechen
</button> </button>
</div> </div>
@@ -287,16 +294,16 @@ function ScenarioPopover({
const [name, setName] = useState("Neues Szenario"); const [name, setName] = useState("Neues Szenario");
const [branchFromPhaseId, setBranchFromPhaseId] = useState(phases[phases.length - 1]?.id ?? ""); const [branchFromPhaseId, setBranchFromPhaseId] = useState(phases[phases.length - 1]?.id ?? "");
return ( 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 <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} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="Name des Szenarios" placeholder="Name des Szenarios"
/> />
<label className="mb-1 block text-xs text-zinc-500">Verzweigen ab Phase</label> <label className="mb-1 block text-xs text-zinc-500">Verzweigen ab Phase</label>
<select <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} value={branchFromPhaseId}
onChange={(e) => setBranchFromPhaseId(e.target.value)} onChange={(e) => setBranchFromPhaseId(e.target.value)}
> >
@@ -310,11 +317,11 @@ function ScenarioPopover({
<button <button
type="button" type="button"
onClick={() => onCreate(name, branchFromPhaseId)} 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 Erstellen
</button> </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 Abbrechen
</button> </button>
</div> </div>
+14 -8
View File
@@ -2,13 +2,14 @@
import { useMemo, useState } from "react"; 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 { BarChart3, Download, LineChart as LineChartIcon } from "lucide-react";
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 { 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";
const PALETTE = ["#3f3f46", "#2563eb", "#16a34a", "#d97706", "#dc2626", "#7c3aed"]; const PALETTE = ["#4f46e5", "#0ea5e9", "#16a34a", "#d97706", "#dc2626", "#7c3aed"];
interface PlanListItem { interface PlanListItem {
id: string; id: string;
@@ -80,13 +81,17 @@ export function Dashboard({
<StatCard label="Geschaetzter Nachlass" value={computed.nachlass} help="Endvermoegen der letzten Phase - potenziell vererbbar." /> <StatCard label="Geschaetzter Nachlass" value={computed.nachlass} help="Endvermoegen der letzten Phase - potenziell vererbbar." />
</div> </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"> <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 <a
href={`/api/plans/${plan.id}/export`} 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 CSV-Export
</a> </a>
</div> </div>
@@ -108,8 +113,9 @@ export function Dashboard({
<WealthChart series={series} /> <WealthChart series={series} />
</section> </section>
<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">
<h3 className="mb-3 text-sm font-semibold text-zinc-800 dark:text-zinc-100"> <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) Vermoegensaufteilung pro Phase (Endvermoegen)
</h3> </h3>
<div className="h-72 w-full"> <div className="h-72 w-full">
@@ -136,11 +142,11 @@ export function Dashboard({
function StatCard({ label, value, help }: { label: string; value: number; help?: string }) { function StatCard({ label, value, help }: { label: string; value: number; help?: string }) {
return ( 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}> <div className="text-xs text-zinc-500 dark:text-zinc-400" title={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-indigo-600 dark:text-indigo-400">
{formatChf(value)} CHF {formatChf(value)} CHF
</div> </div>
</div> </div>
+7 -6
View File
@@ -1,11 +1,12 @@
"use client"; "use client";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-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";
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-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 }) { export function FieldLabel({ label, help }: { label: string; help?: string }) {
return ( return (
@@ -114,7 +115,7 @@ export function MoneyInput({
onChange(floorToThousand(parseChfInput(text))); 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 <button
type="button" type="button"
tabIndex={-1} tabIndex={-1}
@@ -130,9 +131,9 @@ export function MoneyInput({
startHold(1000); startHold(1000);
}} }}
onTouchEnd={stopHold} 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>
<button <button
type="button" type="button"
@@ -149,9 +150,9 @@ export function MoneyInput({
startHold(-1000); startHold(-1000);
}} }}
onTouchEnd={stopHold} 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> </button>
</div> </div>
</div> </div>
+3 -3
View File
@@ -50,7 +50,7 @@ export function HouseholdSettings({
} }
return ( 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 <SelectField
label="Haushaltsform" label="Haushaltsform"
value={householdType} value={householdType}
@@ -88,14 +88,14 @@ export function HouseholdSettings({
type="button" type="button"
disabled={saving} disabled={saving}
onClick={handleSubmit} 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"} {saving ? "Speichern..." : "Speichern"}
</button> </button>
<button <button
type="button" type="button"
onClick={onClose} 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 Abbrechen
</button> </button>
+4 -3
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Info } from "lucide-react";
export function InfoBubble({ text }: { text: string }) { export function InfoBubble({ text }: { text: string }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -13,12 +14,12 @@ export function InfoBubble({ text }: { text: string }) {
onMouseEnter={() => setOpen(true)} onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)} onMouseLeave={() => setOpen(false)}
onClick={() => setOpen((o) => !o)} 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> </button>
{open && ( {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} {text}
</span> </span>
)} )}
+8 -4
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { PiggyBank } from "lucide-react";
import { api } from "@/lib/api-client"; import { api } from "@/lib/api-client";
import { NumberField, SelectField } from "@/components/FormField"; import { NumberField, SelectField } from "@/components/FormField";
import type { HouseholdInput, HouseholdType, PersonRole } from "@/lib/types"; import type { HouseholdInput, HouseholdType, PersonRole } from "@/lib/types";
@@ -52,14 +53,17 @@ export function Onboarding({ onDone }: { onDone: (household: HouseholdInput) =>
return ( return (
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col justify-center px-6 py-16"> <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"> <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 Willkommen beim Financial Planning Tool
</h1> </h1>
</div>
<p className="mb-8 text-sm text-zinc-600 dark:text-zinc-400"> <p className="mb-8 text-sm text-zinc-600 dark:text-zinc-400">
Bevor es losgeht, brauchen wir ein paar Eckdaten zu Ihrem Haushalt. Bevor es losgeht, brauchen wir ein paar Eckdaten zu Ihrem Haushalt.
</p> </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 <SelectField
label="Haushaltsform" label="Haushaltsform"
help="Waehlen Sie, ob Sie alleine oder gemeinsam mit einer Partnerin / einem Partner planen." 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) => ( {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"> <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"} {householdType === "COUPLE" ? (person.role === "PERSON_A" ? "Person A" : "Person B") : "Ihre Angaben"}
</div> </div>
@@ -105,7 +109,7 @@ export function Onboarding({ onDone }: { onDone: (household: HouseholdInput) =>
type="button" type="button"
disabled={saving} disabled={saving}
onClick={handleSubmit} 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"} {saving ? "Speichern..." : "Weiter"}
</button> </button>
+11 -5
View File
@@ -2,6 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { LineChart, Line, ResponsiveContainer } from "recharts"; import { LineChart, Line, ResponsiveContainer } from "recharts";
import { AlertTriangle, ChevronDown, ChevronRight, Trash2 } from "lucide-react";
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 { formatChf } from "@/lib/format";
@@ -43,19 +44,23 @@ export function PhaseCard({
} }
return ( 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 <button
type="button" type="button"
onClick={() => setExpanded((v) => !v)} 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" 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-1">
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<span className="font-medium text-zinc-900 dark:text-zinc-100">{phase.name}</span> <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> <span className="text-xs text-zinc-500">{phase.durationYears} Jahre</span>
{computed.savingsWarning && ( {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>
<div className="text-xs text-zinc-500 dark:text-zinc-400"> <div className="text-xs text-zinc-500 dark:text-zinc-400">
@@ -65,7 +70,7 @@ export function PhaseCard({
<div className="h-8 w-24"> <div className="h-8 w-24">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={sparklineData}> <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> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@@ -77,8 +82,9 @@ export function PhaseCard({
e.stopPropagation(); e.stopPropagation();
handleDelete(); 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"} {deleting ? "…" : "Loeschen"}
</span> </span>
)} )}
+64 -31
View File
@@ -1,6 +1,19 @@
"use client"; "use client";
import { useState } from "react"; 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 { 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 { formatChf } from "@/lib/format";
@@ -147,7 +160,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</section> </section>
{/* Einkommen */} {/* Einkommen */}
<Section title="Einkommen"> <Section title="Einkommen" icon={<Wallet className="h-4 w-4" />}>
{incomeEntries.map((entry, i) => ( {incomeEntries.map((entry, i) => (
<div key={entry.id} className="grid grid-cols-[1fr_1fr_auto] items-end gap-2"> <div key={entry.id} className="grid grid-cols-[1fr_1fr_auto] items-end gap-2">
{incomeMode === "PER_PERSON" ? ( {incomeMode === "PER_PERSON" ? (
@@ -183,7 +196,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div> </div>
))} ))}
<AddButton <AddButton
label="+ Einkommensposten" label="Einkommensposten"
onClick={() => onClick={() =>
setIncomeEntries((prev) => [ setIncomeEntries((prev) => [
...prev, ...prev,
@@ -194,7 +207,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</Section> </Section>
{/* Ausgaben */} {/* Ausgaben */}
<Section title="Ausgaben"> <Section title="Ausgaben" icon={<ShoppingCart className="h-4 w-4" />}>
{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">
<MoneyField <MoneyField
@@ -209,19 +222,19 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div> </div>
))} ))}
<AddButton <AddButton
label="+ Ausgabenposten" label="Ausgabenposten"
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-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> 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>
{/* Wertschriften */} {/* Wertschriften */}
<Section title="Wertschriften"> <Section title="Wertschriften" icon={<TrendingUp className="h-4 w-4" />}>
{securities.map((s, i) => ( {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 <TextField
label="Name" label="Name"
help="Frei waehlbare Bezeichnung, z. B. 'Globaler ETF' oder 'Schweizer Aktien'." help="Frei waehlbare Bezeichnung, z. B. 'Globaler ETF' oder 'Schweizer Aktien'."
@@ -266,7 +279,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div> </div>
))} ))}
<AddButton <AddButton
label="+ Wertschrift" label="Wertschrift"
onClick={() => onClick={() =>
setSecurities((prev) => [ setSecurities((prev) => [
...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"> <div className="flex items-center gap-2">
<StatusDot ok={!startCapitalRemaining} /> <StatusDot ok={!startCapitalRemaining} />
Verfuegbares Startkapital (aus Verkaeufen der Vorphase): <strong>{formatChf(phase.incomingCapital)}</strong> CHF 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)} {" "} zugewiesen: {formatChf(allocated)}
</div> </div>
{overAllocated && ( {overAllocated && (
<div className="text-amber-700 dark:text-amber-400"> <div className="flex items-center gap-1.5 text-amber-700 dark:text-amber-400">
Die zugewiesenen Sparbeitraege uebersteigen die verfuegbare Sparquote. <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
Die zugewiesenen Sparbeitraege uebersteigen die verfuegbare Sparquote.
</div> </div>
)} )}
</div> </div>
</Section> </Section>
{/* Immobilien */} {/* Immobilien */}
<Section title="Immobilien"> <Section title="Immobilien" icon={<Home className="h-4 w-4" />}>
{realEstates.map((re, i) => ( {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 <TextField
label="Bezeichnung" label="Bezeichnung"
help="Z. B. 'Eigenheim' oder 'Ferienwohnung'." help="Z. B. 'Eigenheim' oder 'Ferienwohnung'."
@@ -336,7 +350,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div> </div>
))} ))}
<AddButton <AddButton
label="+ Immobilie" label="Immobilie"
onClick={() => onClick={() =>
setRealEstates((prev) => [ setRealEstates((prev) => [
...prev, ...prev,
@@ -347,7 +361,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</Section> </Section>
{/* Sondereinnahmen/-ausgaben */} {/* Sondereinnahmen/-ausgaben */}
<Section title="Sondereinnahmen / -ausgaben"> <Section title="Sondereinnahmen / -ausgaben" icon={<Gift className="h-4 w-4" />}>
{oneTimeEvents.map((ev, i) => ( {oneTimeEvents.map((ev, i) => (
<div key={ev.id} className="grid grid-cols-[auto_1fr_2fr_auto] items-end gap-2"> <div key={ev.id} className="grid grid-cols-[auto_1fr_2fr_auto] items-end gap-2">
<SelectField <SelectField
@@ -374,15 +388,15 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div> </div>
))} ))}
<AddButton <AddButton
label="+ Sondereinnahme/-ausgabe" label="Sondereinnahme/-ausgabe"
onClick={() => setOneTimeEvents((prev) => [...prev, { id: tempId(), type: "EXPENSE", amount: 0, description: null }])} onClick={() => setOneTimeEvents((prev) => [...prev, { id: tempId(), type: "EXPENSE", amount: 0, description: null }])}
/> />
</Section> </Section>
{/* Pensionierung */} {/* Pensionierung */}
<Section title="Pensionierung (optional)"> <Section title="Pensionierung (optional)" icon={<PiggyBank className="h-4 w-4" />}>
{retirementInfos.map((r, i) => ( {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 <SelectField
label="Person" label="Person"
value={r.personId} value={r.personId}
@@ -419,7 +433,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
</div> </div>
))} ))}
<AddButton <AddButton
label="+ Pensionierungsangaben" label="Pensionierungsangaben"
onClick={() => onClick={() =>
setRetirementInfos((prev) => [ setRetirementInfos((prev) => [
...prev, ...prev,
@@ -436,21 +450,26 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
/> />
</Section> </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"> <div className="flex gap-2">
<button <button
type="button" type="button"
disabled={saving} disabled={saving}
onClick={handleSave} 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"} {saving ? "Speichern..." : "Speichern"}
</button> </button>
<button <button
type="button" type="button"
onClick={onCancel} 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 Abbrechen
</button> </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 ( return (
<section className="flex flex-col gap-2"> <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} {children}
</section> </section>
); );
@@ -473,19 +503,22 @@ function AddButton({ label, onClick }: { label: string; onClick: () => void }) {
<button <button
type="button" type="button"
onClick={onClick} 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} {label}
</button> </button>
); );
} }
function StatusDot({ ok }: { ok: boolean }) { function StatusDot({ ok }: { ok: boolean }) {
return ( return ok ? (
<span <CheckCircle2
title={ok ? "Vollstaendig verteilt" : "Noch nicht vollstaendig verteilt"} className="h-4 w-4 shrink-0 text-emerald-500"
className={`inline-block h-2.5 w-2.5 shrink-0 rounded-full ${ok ? "bg-emerald-500" : "bg-red-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" type="button"
onClick={onClick} onClick={onClick}
aria-label="Entfernen" 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> </button>
); );
} }
+15 -9
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ArrowDown, CheckCircle2 } from "lucide-react";
import { MoneyInput } from "@/components/FormField"; import { MoneyInput } from "@/components/FormField";
import { api } from "@/lib/api-client"; import { api } from "@/lib/api-client";
import { floorToThousand, formatChf } from "@/lib/format"; import { floorToThousand, formatChf } from "@/lib/format";
@@ -116,7 +117,7 @@ export function TransitionPanel({
if (!loaded) { if (!loaded) {
return ( 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 Uebergang wird geladen
</div> </div>
); );
@@ -166,8 +167,9 @@ export function TransitionPanel({
} }
return ( 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="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="text-xs font-semibold uppercase tracking-wide text-zinc-500"> <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} Uebergang {nextPhaseName}
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
@@ -181,11 +183,11 @@ export function TransitionPanel({
</thead> </thead>
<tbody> <tbody>
{items.map((it, i) => ( {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">{it.name}</td>
<td className="py-2 pr-2"> <td className="py-2 pr-2">
<select <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} value={it.decision}
onChange={(e) => onChange={(e) =>
setItems((prev) => setItems((prev) =>
@@ -218,7 +220,7 @@ export function TransitionPanel({
{it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? ( {it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? (
<input <input
type="number" 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} value={it.saleTaxRate}
onChange={(e) => onChange={(e) =>
setItems((prev) => setItems((prev) =>
@@ -234,7 +236,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-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> 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 &quot;{nextPhaseName}&quot; als verfuegbares Startkapital hinterlegt. Wird beim Speichern automatisch in &quot;{nextPhaseName}&quot; als verfuegbares Startkapital hinterlegt.
@@ -248,11 +250,15 @@ export function TransitionPanel({
type="button" type="button"
disabled={saving} disabled={saving}
onClick={handleSave} 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"} {saving ? "Speichern..." : "Uebergang speichern"}
</button> </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>
</div> </div>
); );