Visual redesign: indigo accent palette, lucide icons per section, lighter/softer cards and dark mode
Deploy App / deploy (push) Successful in 2m59s
Deploy App / deploy (push) Successful in 2m59s
This commit is contained in:
Generated
+10
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 "{nextPhaseName}" als verfuegbares Startkapital hinterlegt.
|
Wird beim Speichern automatisch in "{nextPhaseName}" 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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user