Files
lotteryAdmin/src/modules/risk/risk-pool-detail-console.tsx

193 lines
6.7 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 Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { getAdminRiskPoolDetail } from "@/api/admin-risk";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { 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 { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { formatAdminMinorUnits } from "@/lib/money";
import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminRiskPoolDetailLogRow, AdminRiskPoolShowData } from "@/types/api/admin-risk";
export function RiskPoolDetailConsole({
drawId,
number4d,
}: {
drawId: number;
number4d: string;
}) {
const formatDt = useAdminDateTimeFormatter();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [data, setData] = useState<AdminRiskPoolShowData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const d = await getAdminRiskPoolDetail(drawId, number4d, { page, per_page: perPage });
setData(d);
} catch (e) {
const msg =
e instanceof LotteryApiBizError ? e.message : "加载风险池详情失败";
setError(msg);
setData(null);
} finally {
setLoading(false);
}
}, [drawId, number4d, page, perPage]);
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
if (error && !data) {
return (
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<p className="text-sm text-destructive">{error}</p>
<Link
href={`/admin/risk/draws/${drawId}/pools`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
>
</Link>
</CardContent>
</Card>
);
}
if (loading && !data) {
return <p className="text-sm text-muted-foreground"></p>;
}
if (!data) {
return null;
}
const { pool, logs } = data;
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-2">
<Link
href={`/admin/risk/draws/${drawId}/pools`}
className={cn(buttonVariants({ variant: "ghost", size: "sm" }))}
>
</Link>
</div>
<Card>
<CardHeader>
<CardTitle className="text-lg">
<span className="font-mono">{pool.normalized_number}</span>
</CardTitle>
<p className="text-sm text-muted-foreground"> {data.draw_no}</p>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 font-mono text-sm font-medium tabular-nums">
{formatAdminMinorUnits(pool.total_cap_amount)}
</p>
</div>
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 font-mono text-sm font-medium tabular-nums">
{formatAdminMinorUnits(pool.locked_amount)}
</p>
</div>
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 font-mono text-sm font-medium tabular-nums">
{formatAdminMinorUnits(pool.remaining_amount)}
</p>
</div>
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 text-sm font-medium">{pool.is_sold_out ? "是" : "否"}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{" "}
{pool.usage_ratio != null ? `${(pool.usage_ratio * 100).toFixed(2)}%` : "—"}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> / </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.items.map((row: AdminRiskPoolDetailLogRow) => (
<TableRow key={row.id}>
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
{row.created_at ? formatDt(row.created_at) : "—"}
</TableCell>
<TableCell className="text-sm">{row.action_type}</TableCell>
<TableCell className="text-right text-sm tabular-nums">
{formatAdminMinorUnits(row.amount)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.source_reason ?? "—"}
</TableCell>
<TableCell className="font-mono text-xs">{row.ticket_no ?? "—"}</TableCell>
<TableCell className="text-xs">{row.play_code ?? "—"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId={`risk-pool-detail-${drawId}-${number4d}`}
total={logs.meta.total}
page={logs.meta.current_page}
lastPage={logs.meta.last_page}
perPage={logs.meta.per_page}
loading={loading}
onPerPageChange={(n) => {
setPerPage(n);
setPage(1);
}}
onPageChange={setPage}
/>
</CardContent>
</Card>
</div>
);
}