Files
lotteryAdmin/src/modules/risk/risk-index-console.tsx
kang a4454a54a4 refactor(risk, navigation): update risk management redirects and enhance loading states
Changed default redirects in risk management pages to point to the new risk pools section. Removed unused risk lock log components and streamlined the admin reports page with a loading state for better user experience. Added a new DocFigure component for improved documentation visuals and updated localization files to include new figure descriptions.
2026-06-16 13:50:58 +08:00

240 lines
9.2 KiB
TypeScript

"use client";
import { Shield } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { useExportLabels } from "@/hooks/use-export-labels";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminDraws } from "@/api/admin-draws";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-draws";
const DRAW_STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: "pending", label: "statusOptions.pending" },
{ value: "open", label: "statusOptions.open" },
{ value: "closing", label: "statusOptions.closing" },
{ value: "closed", label: "statusOptions.closed" },
{ value: "drawing", label: "statusOptions.drawing" },
{ value: "review", label: "statusOptions.review" },
{ value: "cooldown", label: "statusOptions.cooldown" },
{ value: "settling", label: "statusOptions.settling" },
{ value: "settled", label: "statusOptions.settled" },
{ value: "cancelled", label: "statusOptions.cancelled" },
];
export function RiskIndexConsole() {
const { t } = useTranslation(["risk", "common"]);
const tRef = useTranslationRef(["risk", "common"]);
const exportLabels = useExportLabels("riskIndex");
const formatDt = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminDrawListData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [drawNoInput, setDrawNoInput] = useState("");
const [drawNoQuery, setDrawNoQuery] = useState("");
const [statusFilter, setStatusFilter] = useState("");
const riskStatusTriggerLabel = useMemo(() => {
if (statusFilter === "") {
return t("all");
}
const key = DRAW_STATUS_OPTIONS.find((o) => o.value === statusFilter)?.label;
return key ? t(key) : statusFilter;
}, [statusFilter, t]);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const d = await getAdminDraws({
page,
per_page: perPage,
...(drawNoQuery.trim() !== "" ? { draw_no: drawNoQuery.trim() } : {}),
...(statusFilter !== "" ? { status: statusFilter } : {}),
});
setData(d);
} catch (e) {
const msg =
e instanceof LotteryApiBizError ? e.message : tRef.current("loadDrawListFailed");
setError(msg);
setData(null);
} finally {
setLoading(false);
}
}, [page, perPage, drawNoQuery, statusFilter]);
useAsyncEffect(() => {
void load();
}, [page, perPage, drawNoQuery, statusFilter]);
function applySearch(): void {
setDrawNoQuery(drawNoInput.trim());
setPage(1);
}
const total = data?.meta.total ?? 0;
const lastPage = Math.max(1, data?.meta.last_page ?? 1);
return (
<Card className="admin-list-card">
<CardHeader className="admin-list-header flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<CardTitle className="admin-list-title">{t("center")}</CardTitle>
<div className="admin-list-toolbar lg:w-auto">
<div className="admin-list-field lg:min-w-0">
<Label
htmlFor="risk-index-draw-no"
className="text-xs text-muted-foreground sm:w-10 sm:shrink-0"
>
{t("drawNo")}
</Label>
<Input
id="risk-index-draw-no"
placeholder={t("fuzzyDrawNo")}
className="w-full sm:w-[18rem] lg:w-[24rem]"
value={drawNoInput}
onChange={(e) => setDrawNoInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
applySearch();
}
}}
/>
</div>
<div className="admin-list-field">
<Label
htmlFor="risk-index-status"
className="text-xs text-muted-foreground sm:w-10 sm:shrink-0"
>
{t("status")}
</Label>
<Select
modal={false}
value={statusFilter === "" ? "all" : statusFilter}
onValueChange={(v) => {
const next = v == null || v === "all" ? "" : v;
setStatusFilter(next);
setPage(1);
}}
>
<SelectTrigger id="risk-index-status" size="sm" className="w-full sm:w-40">
<SelectValue>{riskStatusTriggerLabel}</SelectValue>
</SelectTrigger>
<SelectContent align="start">
<SelectItem value="all">{t("all")}</SelectItem>
{DRAW_STATUS_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{t(o.label)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="admin-list-actions">
<AdminTableExportButton
tableId="risk-index-table"
filename={exportLabels.filename}
sheetName={exportLabels.sheetName}
/>
<Button type="button" size="sm" onClick={() => applySearch()}>
{t("search")}
</Button>
<Button type="button" size="sm" variant="secondary" onClick={() => void load()}>
{t("refresh")}
</Button>
</div>
</div>
</CardHeader>
<CardContent className="admin-list-content">
{error ? <p className="text-sm text-destructive">{error}</p> : null}
<div className="admin-table-shell">
<Table id="risk-index-table">
<TableHeader>
<TableRow>
<TableHead>{t("drawNo")}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead>{t("closeTime")}</TableHead>
<TableHead className="sticky right-0 z-20 bg-muted w-14 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("table.actions", { ns: "common" })}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading && (data?.items.length ?? 0) === 0 ? (
<AdminTableLoadingRow colSpan={4} />
) : (data?.items ?? []).length === 0 ? (
<AdminTableNoResourceRow colSpan={4} className="text-muted-foreground" />
) : (
(data?.items ?? []).map((row: AdminDrawListItem) => (
<TableRow key={row.id}>
<TableCell className="font-mono font-medium">{row.draw_no}</TableCell>
<TableCell>
<DrawStatusBadge status={row.status} />
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{row.close_time ? formatDt(row.close_time) : "—"}
</TableCell>
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
<AdminRowActionsMenu
actions={[
{
key: "risk",
label: t("enterRisk"),
icon: Shield,
href: `/admin/draws/${row.id}/risk/pools`,
},
]}
/>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId="risk-index-draws-per-page"
total={total}
page={page}
lastPage={lastPage}
perPage={perPage}
loading={loading}
onPerPageChange={(n) => {
setPerPage(n);
setPage(1);
}}
onPageChange={setPage}
/>
</CardContent>
</Card>
);
}