256 lines
8.0 KiB
TypeScript
256 lines
8.0 KiB
TypeScript
"use client"
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
import { format } from "date-fns"
|
|
import Link from "next/link"
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { Suspense, useCallback } from "react"
|
|
import { Controller, useForm } from "react-hook-form"
|
|
import z from "zod"
|
|
import { getPageBalance } from "@/actions/balance"
|
|
import { DataTable, useDataTable } from "@/components/data-table"
|
|
import { Page } from "@/components/page"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
|
|
import { Input } from "@/components/ui/input"
|
|
import type { Balance } from "@/models/balance"
|
|
|
|
type FilterValues = {
|
|
user_phone?: string
|
|
bill_no?: string
|
|
admin_id?: string
|
|
created_at_start?: Date
|
|
created_at_end?: Date
|
|
}
|
|
|
|
const filterSchema = z
|
|
.object({
|
|
phone: z.string().optional(),
|
|
bill_no: z.string().optional(),
|
|
admin_id: z.string().optional(),
|
|
created_at_start: z.string().optional(),
|
|
created_at_end: z.string().optional(),
|
|
})
|
|
.superRefine((data, ctx) => {
|
|
if (data.created_at_start && data.created_at_end) {
|
|
const start = new Date(data.created_at_start)
|
|
const end = new Date(data.created_at_end)
|
|
|
|
if (end < start) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "结束时间不能早于开始时间",
|
|
path: ["created_at_end"],
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
type FormValues = z.infer<typeof filterSchema>
|
|
|
|
export default function BalancePage() {
|
|
const searchParams = useSearchParams()
|
|
const billNo = searchParams.get("bill_no")
|
|
const router = useRouter()
|
|
const { control, handleSubmit, reset, getValues } = useForm<FormValues>({
|
|
resolver: zodResolver(filterSchema),
|
|
defaultValues: {
|
|
phone: "",
|
|
bill_no: billNo || "",
|
|
admin_id: "",
|
|
created_at_start: "",
|
|
created_at_end: "",
|
|
},
|
|
})
|
|
|
|
const fetchUsers = useCallback(
|
|
(page: number, size: number) => {
|
|
const result: FilterValues = {}
|
|
const filters = getValues()
|
|
if (filters.phone?.trim()) result.user_phone = filters.phone.trim()
|
|
if (filters.bill_no?.trim()) result.bill_no = filters.bill_no.trim()
|
|
if (filters.created_at_start)
|
|
result.created_at_start = new Date(filters.created_at_start)
|
|
if (filters.created_at_end)
|
|
result.created_at_end = new Date(filters.created_at_end)
|
|
return getPageBalance({ page, size, ...result })
|
|
},
|
|
[getValues],
|
|
)
|
|
|
|
const table = useDataTable<Balance>(fetchUsers)
|
|
|
|
const onFilter = handleSubmit(() => {
|
|
table.pagination.onPageChange(1)
|
|
})
|
|
|
|
return (
|
|
<Page>
|
|
<form onSubmit={onFilter} className="bg-card p-4 rounded-lg">
|
|
<div className="flex items-end gap-4">
|
|
<Controller
|
|
name="phone"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>会员号</FieldLabel>
|
|
<Input {...field} placeholder="请输入会员号" clearable />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="bill_no"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>账单号</FieldLabel>
|
|
<Input {...field} placeholder="请输入账单号" clearable />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="created_at_start"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>开始时间</FieldLabel>
|
|
<Input type="date" {...field} />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="created_at_end"
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
data-invalid={fieldState.invalid}
|
|
className="w-40 flex-none"
|
|
>
|
|
<FieldLabel>结束时间</FieldLabel>
|
|
<Input type="date" {...field} />
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
</Field>
|
|
)}
|
|
/>
|
|
|
|
<Button type="submit">搜索</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => {
|
|
reset({
|
|
phone: "",
|
|
bill_no: "",
|
|
admin_id: "",
|
|
created_at_start: "",
|
|
created_at_end: "",
|
|
})
|
|
router.replace("./balance")
|
|
table.pagination.onPageChange(1)
|
|
}}
|
|
>
|
|
重置
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
|
|
<Suspense>
|
|
<DataTable<Balance>
|
|
{...table}
|
|
columns={[
|
|
{
|
|
header: "会员号",
|
|
accessorFn: row => row.user?.phone || "",
|
|
},
|
|
{
|
|
header: "账单编号",
|
|
accessorKey: "bill?.bill_no",
|
|
cell: ({ row }) => {
|
|
const bill_no = row.original.bill?.bill_no
|
|
return (
|
|
<Link
|
|
href={`/billing?bill_no=${bill_no}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600"
|
|
>
|
|
{bill_no}
|
|
</Link>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
header: "管理员",
|
|
accessorKey: "admin_id",
|
|
accessorFn: row => row.admin?.name || "",
|
|
},
|
|
{
|
|
header: "变动金额",
|
|
accessorKey: "amount",
|
|
cell: ({ row }) => {
|
|
const amount = row.original.amount
|
|
const isPositive = Number(amount) > 0
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
<span
|
|
className={`font-semibold ${
|
|
isPositive ? "text-red-600" : "text-green-600"
|
|
}`}
|
|
>
|
|
{isPositive ? "+" : ""}
|
|
{Number(amount).toFixed(2)}
|
|
</span>
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
header: "余额变化",
|
|
accessorKey: "balance_prev",
|
|
cell: ({ row }) => (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-muted-foreground text-sm">
|
|
¥{Number(row.original.balance_prev).toFixed(2)}
|
|
</span>
|
|
<span className="text-muted-foreground">→</span>
|
|
<span className="font-medium text-foreground">
|
|
¥{Number(row.original.balance_curr).toFixed(2)}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
header: "备注",
|
|
accessorKey: "remark",
|
|
},
|
|
{
|
|
header: "创建时间",
|
|
accessorKey: "created_at",
|
|
cell: ({ row }) => {
|
|
const createdAt = row.original.created_at
|
|
if (!createdAt) return <span>-</span>
|
|
|
|
const date = new Date(createdAt)
|
|
if (isNaN(date.getTime())) return <span>-</span>
|
|
|
|
return format(date, "yyyy-MM-dd HH:mm:ss")
|
|
},
|
|
},
|
|
]}
|
|
/>
|
|
</Suspense>
|
|
</Page>
|
|
)
|
|
}
|