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
+65 -63
View File
@@ -1,9 +1,9 @@
"use client";
"use client"
import * as React from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import * as React from "react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { signIn } from "next-auth/react"
import {
ArrowRightIcon,
CheckCircle2Icon,
@@ -16,10 +16,10 @@ import {
PieChartIcon,
TrendingUpIcon,
WalletIcon,
} from "lucide-react";
} from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
@@ -27,90 +27,90 @@ import {
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { FluxentLogo } from "@/components/fluxent-logo";
import { ThemeToggle } from "@/components/theme-toggle";
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { FluxentLogo } from "@/components/fluxent-logo"
import { ThemeToggle } from "@/components/theme-toggle"
interface LoginFormProps {
oidcEnabled: boolean;
oidcName: string;
oidcEnabled: boolean
oidcName: string
}
export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/";
const authError = searchParams.get("error");
const registered = searchParams.get("registered");
const router = useRouter()
const searchParams = useSearchParams()
const callbackUrl = searchParams.get("callbackUrl") || "/"
const authError = searchParams.get("error")
const registered = searchParams.get("registered")
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [rememberMe, setRememberMe] = React.useState(false);
const [isPending, setIsPending] = React.useState(false);
const [isOidcPending, setIsOidcPending] = React.useState(false);
const [email, setEmail] = React.useState("")
const [password, setPassword] = React.useState("")
const [rememberMe, setRememberMe] = React.useState(false)
const [isPending, setIsPending] = React.useState(false)
const [isOidcPending, setIsOidcPending] = React.useState(false)
const [errorMessage, setErrorMessage] = React.useState<string | null>(() => {
if (authError === "CredentialsSignin") {
return "邮箱或密码错误,请核对后重试";
return "邮箱或密码错误,请核对后重试"
}
if (authError) {
return "认证过程中遇到问题,请重新登录";
return "认证过程中遇到问题,请重新登录"
}
return null;
});
return null
})
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrorMessage(null);
e.preventDefault()
setErrorMessage(null)
if (!email.trim()) {
setErrorMessage("请输入登录邮箱");
return;
setErrorMessage("请输入登录邮箱")
return
}
if (!password) {
setErrorMessage("请输入登录密码");
return;
setErrorMessage("请输入登录密码")
return
}
try {
setIsPending(true);
setIsPending(true)
const res = await signIn("credentials", {
email: email.trim(),
password,
redirect: false,
callbackUrl,
});
})
if (res?.error) {
if (res.error === "CredentialsSignin" || res.code === "credentials") {
setErrorMessage("账号或密码不正确,请重新检查");
setErrorMessage("账号或密码不正确,请重新检查")
} else {
setErrorMessage("登录失败,请稍后重试");
setErrorMessage("登录失败,请稍后重试")
}
return;
return
}
router.push(callbackUrl);
router.refresh();
router.push(callbackUrl)
router.refresh()
} catch {
setErrorMessage("网络异常,无法连接到认证服务器");
setErrorMessage("网络异常,无法连接到认证服务器")
} finally {
setIsPending(false);
setIsPending(false)
}
};
}
const handleOidcLogin = async () => {
try {
setIsOidcPending(true);
await signIn("oidc", { callbackUrl });
setIsOidcPending(true)
await signIn("oidc", { callbackUrl })
} catch {
setIsOidcPending(false);
setErrorMessage("SSO 单点登录发起失败,请稍后重试");
setIsOidcPending(false)
setErrorMessage("SSO 单点登录发起失败,请稍后重试")
}
};
}
return (
<div className="relative flex min-h-svh flex-col justify-between overflow-hidden bg-background">
@@ -147,7 +147,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
<div className="hidden flex-col gap-8 pr-4 lg:flex">
<div className="flex flex-col gap-3">
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-border/80 bg-muted/60 px-3 py-1 text-xs text-muted-foreground backdrop-blur-xs">
<span className="inline-block size-1.5 rounded-full bg-primary animate-pulse" />
<span className="inline-block size-1.5 animate-pulse rounded-full bg-primary" />
全球多币种 • 全场景记账 • 实时汇率
</div>
<h1 className="font-heading text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
@@ -155,7 +155,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
<br />
让财务管理行云流水。
</h1>
<p className="text-muted-foreground text-sm leading-relaxed">
<p className="text-sm leading-relaxed text-muted-foreground">
无论是跨国多币种账户、信用卡记账,还是银行与电子钱包资金调度,Fluxent
提供银行级的精确记录与现代化的优雅交互体验。
</p>
@@ -170,7 +170,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
<h3 className="text-xs font-semibold text-foreground">
全币种与汇率追踪
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
<p className="text-[11px] leading-normal text-muted-foreground">
多币种入账与清算汇率对账,资产估值一目了然
</p>
</div>
@@ -182,7 +182,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
<h3 className="text-xs font-semibold text-foreground">
全渠道多账户管理
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
<p className="text-[11px] leading-normal text-muted-foreground">
银行账户、电子钱包与支付卡渠道统一调度
</p>
</div>
@@ -194,7 +194,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
<h3 className="text-xs font-semibold text-foreground">
复式记账与场景分类
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
<p className="text-[11px] leading-normal text-muted-foreground">
标准借贷分录与消费场景画像,财务合规严谨
</p>
</div>
@@ -206,7 +206,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
<h3 className="text-xs font-semibold text-foreground">
资产汇总与统计
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
<p className="text-[11px] leading-normal text-muted-foreground">
跨账户资金结构分布分析,收支报表一览无余
</p>
</div>
@@ -273,7 +273,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
<div className="relative flex items-center justify-center">
<Separator className="w-full" />
<span className="absolute bg-card px-2 text-[11px] uppercase tracking-wider text-muted-foreground">
<span className="absolute bg-card px-2 text-[11px] tracking-wider text-muted-foreground uppercase">
或者使用邮箱密码
</span>
</div>
@@ -337,7 +337,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
/>
<label
htmlFor="rememberMe"
className="text-xs text-muted-foreground cursor-pointer select-none"
className="cursor-pointer text-xs text-muted-foreground select-none"
>
保持此设备的登录状态
</label>
@@ -373,7 +373,7 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
立即注册新账号
<ArrowRightIcon
data-icon="inline-end"
className="inline size-3 ml-0.5"
className="ml-0.5 inline size-3"
/>
</Link>
</p>
@@ -389,8 +389,10 @@ export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
{/* 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>
);
)
}