Files
fluxent-web/app/accounts/account-dialog.tsx
T

317 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import * as React from "react"
import { CircleAlertIcon, Loader2Icon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
createAccountAction,
updateAccountAction,
type AccountInput,
type AccountWithChannels,
} from "@/lib/actions/account"
type AccountType = "BANK" | "E_WALLET" | "CASH"
const accountLabels = {
BANK: "银行账户",
E_WALLET: "电子钱包",
CASH: "现金",
} as const
const balanceLabels = {
ASSET: "资产类",
LIABILITY: "负债类",
EQUITY: "权益类",
} as const
export function AccountDialog({
open,
onOpenChange,
account,
onSuccess,
}: {
open: boolean
onOpenChange: (open: boolean) => void
account?: AccountWithChannels | null
onSuccess: () => void
}) {
const editing = Boolean(account)
const [accountType, setAccountType] = React.useState<AccountType>(
account?.accountType || "BANK"
)
const [balanceType, setBalanceType] = React.useState<
"ASSET" | "LIABILITY" | "EQUITY"
>(account?.balanceType || "ASSET")
const [name, setName] = React.useState(account?.name || "")
const [primaryCurrency, setPrimaryCurrency] = React.useState(
account?.primaryCurrency || ""
)
const [supportedCurrencies, setSupportedCurrencies] = React.useState(
account?.supportedCurrencies?.join(", ") || ""
)
const [remark, setRemark] = React.useState(account?.remark || "")
const [issuerName, setIssuerName] = React.useState(account?.issuerName || "")
const [accountNumber, setAccountNumber] = React.useState(
account?.accountNumber || ""
)
const [platform, setPlatform] = React.useState(account?.platform || "")
const [accountId, setAccountId] = React.useState(account?.accountId || "")
const [location, setLocation] = React.useState(account?.location || "")
const [isPending, setIsPending] = React.useState(false)
const [errorMessage, setErrorMessage] = React.useState<string | null>(null)
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setErrorMessage(null)
if (!name.trim()) {
setErrorMessage("请输入账户名称")
return
}
const currencies = supportedCurrencies
.split(/[,\s]+/)
.map((item) => item.trim().toUpperCase())
.filter(Boolean)
const payload: AccountInput = {
name: name.trim(),
accountType,
balanceType,
primaryCurrency: primaryCurrency.trim().toUpperCase() || null,
supportedCurrencies: currencies.length ? currencies : null,
remark: remark.trim() || null,
issuerName: accountType === "BANK" ? issuerName.trim() || null : null,
accountNumber:
accountType === "BANK" ? accountNumber.trim() || null : null,
platform: accountType === "E_WALLET" ? platform.trim() || null : null,
accountId: accountType === "E_WALLET" ? accountId.trim() || null : null,
location: accountType === "CASH" ? location.trim() || null : null,
}
setIsPending(true)
try {
const result =
editing && account
? await updateAccountAction(account.id, payload)
: await createAccountAction(payload)
if (!result.success) {
setErrorMessage(result.error || "保存失败,请检查输入")
return
}
onOpenChange(false)
onSuccess()
} catch {
setErrorMessage("网络异常,提交失败")
} finally {
setIsPending(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{editing ? "编辑资金账户" : "新建资金账户"}</DialogTitle>
<DialogDescription>
填写账户主体、分类及其识别信息。
</DialogDescription>
</DialogHeader>
<form id="account-form" onSubmit={submit}>
<FieldGroup>
{errorMessage && (
<Alert variant="destructive">
<CircleAlertIcon />
<AlertTitle>无法保存账户</AlertTitle>
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
<FieldGroup className="grid gap-3 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="accountType">账户类型</FieldLabel>
<Select
value={accountType}
onValueChange={(value) =>
value && setAccountType(value as AccountType)
}
>
<SelectTrigger id="accountType">
<SelectValue>{accountLabels[accountType]}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="BANK">银行账户</SelectItem>
<SelectItem value="E_WALLET">电子钱包</SelectItem>
<SelectItem value="CASH">现金</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="balanceType">余额分类</FieldLabel>
<Select
value={balanceType}
onValueChange={(value) =>
value && setBalanceType(value as typeof balanceType)
}
>
<SelectTrigger id="balanceType">
<SelectValue>{balanceLabels[balanceType]}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="ASSET">资产类</SelectItem>
<SelectItem value="LIABILITY">负债类</SelectItem>
<SelectItem value="EQUITY">权益类</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<FieldGroup>
<Field>
<FieldLabel htmlFor="accountName">账户名称</FieldLabel>
<Input
id="accountName"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="例如:招商银行个人消费卡账户"
disabled={isPending}
required
/>
</Field>
<FieldGroup className="grid gap-3 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="primaryCurrency">主币种</FieldLabel>
<Input
id="primaryCurrency"
value={primaryCurrency}
onChange={(event) => setPrimaryCurrency(event.target.value)}
placeholder="例如:HKD"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="supportedCurrencies">
支持币种
</FieldLabel>
<Input
id="supportedCurrencies"
value={supportedCurrencies}
onChange={(event) =>
setSupportedCurrencies(event.target.value)
}
placeholder="用逗号分隔"
disabled={isPending}
/>
</Field>
</FieldGroup>
</FieldGroup>
<FieldGroup className="grid gap-3 sm:grid-cols-2">
{accountType === "BANK" && (
<>
<Field>
<FieldLabel htmlFor="issuerName">银行机构</FieldLabel>
<Input
id="issuerName"
value={issuerName}
onChange={(event) => setIssuerName(event.target.value)}
placeholder="例如:汇丰银行"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="accountNumber">
账户号码或尾号
</FieldLabel>
<Input
id="accountNumber"
value={accountNumber}
onChange={(event) => setAccountNumber(event.target.value)}
placeholder="可填写完整账号或尾号"
disabled={isPending}
/>
</Field>
</>
)}
{accountType === "E_WALLET" && (
<>
<Field>
<FieldLabel htmlFor="platform">平台名称</FieldLabel>
<Input
id="platform"
value={platform}
onChange={(event) => setPlatform(event.target.value)}
placeholder="例如:支付宝"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="accountId">平台账户标识</FieldLabel>
<Input
id="accountId"
value={accountId}
onChange={(event) => setAccountId(event.target.value)}
placeholder="手机号、邮箱或会员号"
disabled={isPending}
/>
</Field>
</>
)}
{accountType === "CASH" && (
<Field>
<FieldLabel htmlFor="location">存放位置</FieldLabel>
<Input
id="location"
value={location}
onChange={(event) => setLocation(event.target.value)}
placeholder="例如:随身钱包"
disabled={isPending}
/>
</Field>
)}
</FieldGroup>
<Field>
<FieldLabel htmlFor="remark">备注</FieldLabel>
<Input
id="remark"
value={remark}
onChange={(event) => setRemark(event.target.value)}
placeholder="可选"
disabled={isPending}
/>
</Field>
</FieldGroup>
</form>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
取消
</Button>
<Button form="account-form" type="submit" disabled={isPending}>
{isPending && (
<Loader2Icon data-icon="inline-start" className="animate-spin" />
)}
{editing ? "保存修改" : "创建账户"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}