feat: 添加财务摘要接口,更新管理员抽奖模块和导航,优化权限管理逻辑

This commit is contained in:
2026-05-11 16:21:22 +08:00
parent f083b28fc6
commit b539bf0660
57 changed files with 2134 additions and 108 deletions

View File

@@ -0,0 +1,172 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, 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";
/** PRD §15.4:单期投注/派彩与结算批次(`GET …/draws/{id}/finance-summary` */
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 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]);
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>;
}
const cur = data.currency_code ?? "—";
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription>
{cur} = + Jackpot
</CardDescription>
</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>
<Link
href="/admin/settlement-batches"
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
>
</Link>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardDescription> `settlement_batches` </CardDescription>
</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>
);
}