186 lines
6.6 KiB
TypeScript
186 lines
6.6 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useCallback, useEffect, useState } from "react";
|
||
|
||
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
|
||
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
|
||
import { Button, buttonVariants } from "@/components/ui/button";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/components/ui/table";
|
||
import { cn } from "@/lib/utils";
|
||
import { LotteryApiBizError } from "@/types/api/errors";
|
||
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
||
import { toast } from "sonner";
|
||
|
||
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
|
||
const idNum = Number(drawId);
|
||
const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null);
|
||
const [err, setErr] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [settling, setSettling] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
if (!Number.isFinite(idNum) || idNum < 1) {
|
||
setErr("无效的期号 ID");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
setErr(null);
|
||
try {
|
||
setData(await getAdminDrawFinanceSummary(idNum));
|
||
} catch (e) {
|
||
setErr(e instanceof LotteryApiBizError ? e.message : "加载失败");
|
||
setData(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [idNum]);
|
||
|
||
async function runSettlement(): Promise<void> {
|
||
if (!Number.isFinite(idNum) || idNum < 1) return;
|
||
setSettling(true);
|
||
try {
|
||
const res = await postAdminRunDrawSettlement(idNum);
|
||
toast.success(res.ran ? "已触发结算" : "当前状态不可结算或已处理");
|
||
await load();
|
||
} catch (e) {
|
||
toast.error(e instanceof LotteryApiBizError ? e.message : "触发结算失败");
|
||
} finally {
|
||
setSettling(false);
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
queueMicrotask(() => {
|
||
void load();
|
||
});
|
||
}, [load]);
|
||
|
||
if (loading && !data) {
|
||
return <p className="text-muted-foreground text-sm">加载中…</p>;
|
||
}
|
||
|
||
if (err || !data) {
|
||
return <p className="text-destructive text-sm">{err ?? "无数据"}</p>;
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">期号收支概览</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
|
||
<div>
|
||
<span className="text-muted-foreground">期号</span>
|
||
<p className="font-mono font-semibold">{data.draw_no}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">状态</span>
|
||
<p>{data.draw_status}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">订单数 / 注项数</span>
|
||
<p className="tabular-nums">
|
||
{data.order_count} / {data.ticket_item_count}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">当期实扣投注</span>
|
||
<p className="tabular-nums font-medium">{data.total_bet_minor}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">当期派彩合计</span>
|
||
<p className="tabular-nums font-medium">{data.total_payout_minor}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">近似毛损益</span>
|
||
<p
|
||
className={cn(
|
||
"tabular-nums font-semibold",
|
||
data.approx_house_gross_minor >= 0 ? "text-emerald-600" : "text-destructive",
|
||
)}
|
||
>
|
||
{data.approx_house_gross_minor}
|
||
</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
|
||
刷新
|
||
</Button>
|
||
<Button type="button" size="sm" disabled={settling} onClick={() => void runSettlement()}>
|
||
{settling ? "处理中…" : "触发结算"}
|
||
</Button>
|
||
<Link
|
||
href="/admin/settlement-batches"
|
||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||
>
|
||
结算批次列表(按期号筛选)
|
||
</Link>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">本关联期结算批次</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{data.settlement_batches.length === 0 ? (
|
||
<p className="text-muted-foreground text-sm">暂无结算批次记录。</p>
|
||
) : (
|
||
<div className="overflow-x-auto rounded-md border">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-20">ID</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead className="text-right">票数</TableHead>
|
||
<TableHead className="text-right">中奖数</TableHead>
|
||
<TableHead className="text-right">派彩</TableHead>
|
||
<TableHead className="text-right">Jackpot</TableHead>
|
||
<TableHead>完成时间</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{data.settlement_batches.map((b) => (
|
||
<TableRow key={b.id}>
|
||
<TableCell className="font-mono text-xs">{b.id}</TableCell>
|
||
<TableCell className="text-xs">{b.status}</TableCell>
|
||
<TableCell className="text-right tabular-nums text-xs">
|
||
{b.total_ticket_count}
|
||
</TableCell>
|
||
<TableCell className="text-right tabular-nums text-xs">
|
||
{b.total_win_count}
|
||
</TableCell>
|
||
<TableCell className="text-right tabular-nums text-xs">
|
||
{b.total_payout_amount}
|
||
</TableCell>
|
||
<TableCell className="text-right tabular-nums text-xs">
|
||
{b.total_jackpot_payout_amount}
|
||
</TableCell>
|
||
<TableCell className="font-mono text-[11px] text-muted-foreground">
|
||
{b.finished_at ?? "—"}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|