"use client"; import { Eye, Lock, Unlock } from "lucide-react"; import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useTranslationRef } from "@/hooks/use-translation-ref"; import { toast } from "sonner"; import { getAdminRiskPools, postAdminRiskPoolManualClose, postAdminRiskPoolRecover, } from "@/api/admin-risk"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; 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 { adminHasAnyPermission } from "@/lib/admin-permissions"; import { PRD_DRAW_RESULT_MANAGE, PRD_RISK_MANAGE } from "@/lib/admin-prd"; import { useAdminProfile } from "@/stores/admin-session"; import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useExportLabels } from "@/hooks/use-export-labels"; import { formatAdminMinorUnits } from "@/lib/money"; import { cn } from "@/lib/utils"; import { LotteryApiBizError } from "@/types/api/errors"; import type { RiskPoolsPageTitleKey } from "@/modules/risk/risk-display"; import type { AdminRiskPoolListData, AdminRiskPoolRow } from "@/types/api/admin-risk"; const SORT_OPTIONS: { value: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc"; label: string }[] = [ { value: "usage_desc", label: "sortUsageDesc" }, { value: "locked_desc", label: "sortLockedDesc" }, { value: "remaining_asc", label: "sortRemainingAsc" }, { value: "number_asc", label: "sortNumberAsc" }, ]; function riskSortLabel( value: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc", t: (key: string) => string, ): string { const option = SORT_OPTIONS.find((item) => item.value === value); return option ? t(option.label) : value; } export type RiskPoolListFilter = "all" | "active" | "sold_out" | "high_risk"; type RiskPoolsConsoleProps = { drawId: number; /** @deprecated 优先使用 titleKey */ title?: string; titleKey?: RiskPoolsPageTitleKey; /** @deprecated 使用 initialFilter */ soldOutOnly?: boolean; initialFilter?: RiskPoolListFilter; defaultSort?: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc"; allowSortChange?: boolean; }; function resolveInitialFilter( initialFilter: RiskPoolListFilter | undefined, soldOutOnly: boolean | undefined, ): RiskPoolListFilter { if (initialFilter) { return initialFilter; } if (soldOutOnly) { return "sold_out"; } return "active"; } export function RiskPoolsConsole({ drawId, title, titleKey, soldOutOnly, initialFilter: initialFilterProp, defaultSort = "usage_desc", allowSortChange = true, }: RiskPoolsConsoleProps) { const { t } = useTranslation(["risk", "common"]); const tRef = useTranslationRef(["risk", "common"]); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const profile = useAdminProfile(); const canManageRiskPools = adminHasAnyPermission(profile?.permissions, [ PRD_RISK_MANAGE, PRD_DRAW_RESULT_MANAGE, ]); const initialFilter = resolveInitialFilter(initialFilterProp, soldOutOnly); const pageTitle = titleKey ? t(titleKey) : (title ?? t("poolsTitle")); const exportLabels = useExportLabels("riskPools"); useAdminCurrencyCatalog(); const [sort, setSort] = useState(defaultSort); const [filter, setFilter] = useState(initialFilter); const [number, setNumber] = useState(""); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(10); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [actingNumber, setActingNumber] = useState(null); const [error, setError] = useState(null); const load = useCallback(async () => { setLoading(true); setError(null); try { const d = await getAdminRiskPools(drawId, { page, per_page: perPage, sold_out_only: filter === "sold_out", high_risk_only: filter === "high_risk", active_only: filter === "active" && number.trim() === "", normalized_number: number.trim(), sort, }); setData(d); } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : tRef.current("loadPoolsFailed"); setError(msg); setData(null); } finally { setLoading(false); } }, [drawId, filter, number, page, perPage, sort]); useAsyncEffect(() => { void load(); }, [drawId, filter, number, page, perPage, sort]); const handleManualStatus = useCallback( async (row: AdminRiskPoolRow) => { setActingNumber(row.normalized_number); try { const updated = row.is_sold_out ? await postAdminRiskPoolRecover(drawId, row.normalized_number) : await postAdminRiskPoolManualClose(drawId, row.normalized_number); setData((current) => { if (!current) return current; return { ...current, items: current.items.map((item) => item.normalized_number === updated.normalized_number ? updated : item, ), }; }); toast.success(row.is_sold_out ? t("recoverSuccess") : t("manualCloseSuccess")); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed")); } finally { setActingNumber(null); } }, [drawId, t], ); return ( <> {pageTitle}
{ setNumber(event.target.value.replace(/\D/g, "").slice(0, 4)); setPage(1); }} />
{allowSortChange ? (
) : null}
{error ?

{error}

: null} <>
{t("searchNumber")} {t("capAmount")} {t("lockedAmount")} {t("remainingAmount")} {t("usageRatio")} {t("poolStatus")} {t("table.actions", { ns: "common" })} {loading && !data ? : null} {(data?.items ?? []).map((row: AdminRiskPoolRow) => { const highRisk = (row.usage_ratio ?? 0) >= 0.8; const acting = actingNumber === row.normalized_number; const currencyCode = data?.currency_code ?? "NPR"; return ( {row.normalized_number} {formatAdminMinorUnits(row.total_cap_amount, currencyCode)} {formatAdminMinorUnits(row.locked_amount, currencyCode)} {formatAdminMinorUnits(row.remaining_amount, currencyCode)} {row.usage_ratio != null ? `${(row.usage_ratio * 100).toFixed(2)}%` : "—"} {row.is_sold_out ? t("soldOut") : highRisk ? t("warning") : t("normal")} requestConfirm({ title: row.is_sold_out ? t("confirm.recoverTitle") : t("confirm.closeTitle"), description: row.is_sold_out ? t("confirm.recoverDescription", { number: row.normalized_number, }) : t("confirm.closeDescription", { number: row.normalized_number, }), confirmVariant: row.is_sold_out ? "default" : "destructive", onConfirm: () => handleManualStatus(row), }), }, ]} /> ); })}
{data ? ( { setPerPage(n); setPage(1); }} onPageChange={setPage} /> ) : null}
); }