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.
240 lines
9.2 KiB
TypeScript
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>
|
|
);
|
|
}
|