feat(ledger): add bookkeeping workbench and transaction views

- Add transactions_dirty table and zero-tolerance cleansing action

- Implement bookkeeping workbench (/bookkeeping) with searchable Combobox channel selector and card suffix / account identifier

- Implement transaction ledger (/transactions) with date groupings, multi-currency metrics, and filters

- Update AGENTS.md guidelines for Base UI Select and Combobox bindings
This commit is contained in:
2026-09-06 23:23:29 +08:00
parent 1ccee1414c
commit c33857ffac
42 changed files with 3329 additions and 598 deletions
+31 -29
View File
@@ -1,9 +1,9 @@
"use client";
"use client"
import * as React from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useActionState } from "react";
import * as React from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useActionState } from "react"
import {
ArrowLeftIcon,
CheckCircle2Icon,
@@ -13,11 +13,11 @@ import {
MailIcon,
SparklesIcon,
UserIcon,
} from "lucide-react";
} from "lucide-react"
import { registerAction, type RegisterState } from "@/lib/actions/auth";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { registerAction, type RegisterState } from "@/lib/actions/auth"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
@@ -25,45 +25,45 @@ import {
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { FluxentLogo } from "@/components/fluxent-logo";
import { ThemeToggle } from "@/components/theme-toggle";
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { FluxentLogo } from "@/components/fluxent-logo"
import { ThemeToggle } from "@/components/theme-toggle"
const initialState: RegisterState = {};
const initialState: RegisterState = {}
export function RegisterForm() {
const router = useRouter();
const router = useRouter()
const [state, formAction, isPending] = useActionState(
registerAction,
initialState
);
)
const [password, setPassword] = React.useState("");
const [confirmPassword, setConfirmPassword] = React.useState("");
const [password, setPassword] = React.useState("")
const [confirmPassword, setConfirmPassword] = React.useState("")
// 当注册成功时,优雅倒计时或自动跳转
React.useEffect(() => {
if (state?.success) {
const timer = setTimeout(() => {
router.push("/login?registered=1");
}, 1500);
return () => clearTimeout(timer);
router.push("/login?registered=1")
}, 1500)
return () => clearTimeout(timer)
}
}, [state?.success, router]);
}, [state?.success, router])
// 密码复杂度提示计算
const hasMinLen = password.length >= 8;
const hasMinLen = password.length >= 8
const passwordsMatch = Boolean(
password && confirmPassword && password === confirmPassword
);
)
return (
<div className="relative flex min-h-svh flex-col justify-between overflow-hidden bg-background">
{/* Decorative ambient gradients */}
<div className="pointer-events-none absolute -top-40 -left-40 size-[32rem] rounded-full bg-primary/5 blur-3xl" />
<div className="pointer-events-none absolute -bottom-40 -right-40 size-[34rem] rounded-full bg-primary/5 blur-3xl" />
<div className="pointer-events-none absolute -right-40 -bottom-40 size-[34rem] rounded-full bg-primary/5 blur-3xl" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.08),rgba(255,255,255,0))]" />
{/* Header */}
@@ -300,7 +300,7 @@ export function RegisterForm() {
>
<ArrowLeftIcon
data-icon="inline-start"
className="inline size-3 mr-0.5"
className="mr-0.5 inline size-3"
/>
返回直接登录
</Link>
@@ -312,8 +312,10 @@ export function RegisterForm() {
{/* Footer */}
<footer className="relative z-10 flex h-12 items-center justify-center border-t border-border/40 px-6 text-center text-[11px] text-muted-foreground">
<span>© {new Date().getFullYear()} Fluxent Financial. 保留所有权利。</span>
<span>
© {new Date().getFullYear()} Fluxent Financial. 保留所有权利。
</span>
</footer>
</div>
);
)
}