diff --git a/src/api/admin-risk.ts b/src/api/admin-risk.ts new file mode 100644 index 0000000..7659c80 --- /dev/null +++ b/src/api/admin-risk.ts @@ -0,0 +1,73 @@ +import { adminRequest } from "@/lib/admin-http"; + +import { API_V1_PREFIX } from "./paths"; + +import type { + AdminRiskLockLogListData, + AdminRiskPoolListData, + AdminRiskPoolShowData, +} from "@/types/api/admin-risk"; + +const A = `${API_V1_PREFIX}/admin`; + +export type AdminRiskPoolListQuery = { + page?: number; + per_page?: number; + sold_out_only?: boolean; + sort?: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc"; +}; + +export async function getAdminRiskPools( + drawId: number, + q: AdminRiskPoolListQuery = {}, +): Promise { + return adminRequest.get(`${A}/draws/${drawId}/risk-pools`, { + params: { + page: q.page, + per_page: q.per_page, + sold_out_only: q.sold_out_only === true ? 1 : undefined, + sort: q.sort, + }, + }); +} + +export type AdminRiskLockLogQuery = { + page?: number; + per_page?: number; + action_type?: "lock" | "release"; + normalized_number?: string; +}; + +export async function getAdminRiskPoolLockLogs( + drawId: number, + q: AdminRiskLockLogQuery = {}, +): Promise { + return adminRequest.get( + `${A}/draws/${drawId}/risk-pool-lock-logs`, + { + params: { + page: q.page, + per_page: q.per_page, + action_type: q.action_type, + normalized_number: q.normalized_number, + }, + }, + ); +} + +export type AdminRiskPoolShowQuery = { + page?: number; + per_page?: number; +}; + +export async function getAdminRiskPoolDetail( + drawId: number, + number4d: string, + q: AdminRiskPoolShowQuery = {}, +): Promise { + const encoded = encodeURIComponent(number4d); + return adminRequest.get( + `${A}/draws/${drawId}/risk-pools/${encoded}`, + { params: { page: q.page, per_page: q.per_page } }, + ); +} diff --git a/src/app/admin/(shell)/risk/draws/[drawId]/hot/page.tsx b/src/app/admin/(shell)/risk/draws/[drawId]/hot/page.tsx new file mode 100644 index 0000000..7cd08ab --- /dev/null +++ b/src/app/admin/(shell)/risk/draws/[drawId]/hot/page.tsx @@ -0,0 +1,18 @@ +import { RiskPoolsConsole } from "@/modules/risk/risk-pools-console"; + +export default async function AdminRiskHotPage(props: { + params: Promise<{ drawId: string }>; +}) { + const { drawId } = await props.params; + const id = Number(drawId); + + return ( + + ); +} diff --git a/src/app/admin/(shell)/risk/draws/[drawId]/layout.tsx b/src/app/admin/(shell)/risk/draws/[drawId]/layout.tsx new file mode 100644 index 0000000..17048c7 --- /dev/null +++ b/src/app/admin/(shell)/risk/draws/[drawId]/layout.tsx @@ -0,0 +1,20 @@ +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { RiskDrawHeader } from "@/modules/risk/risk-draw-header"; +import { RiskSubnav } from "@/modules/risk/risk-subnav"; + +export default async function AdminRiskDrawLayout(props: { + children: React.ReactNode; + params: Promise<{ drawId: string }>; +}) { + const { drawId } = await props.params; + const id = Number(drawId); + const safeId = Number.isFinite(id) ? id : 0; + + return ( + + + + {props.children} + + ); +} diff --git a/src/app/admin/(shell)/risk/draws/[drawId]/occupancy/page.tsx b/src/app/admin/(shell)/risk/draws/[drawId]/occupancy/page.tsx new file mode 100644 index 0000000..9cbe9d9 --- /dev/null +++ b/src/app/admin/(shell)/risk/draws/[drawId]/occupancy/page.tsx @@ -0,0 +1,10 @@ +import { RiskLockLogsConsole } from "@/modules/risk/risk-lock-logs-console"; + +export default async function AdminRiskOccupancyPage(props: { + params: Promise<{ drawId: string }>; +}) { + const { drawId } = await props.params; + const id = Number(drawId); + + return ; +} diff --git a/src/app/admin/(shell)/risk/draws/[drawId]/pools/[number]/page.tsx b/src/app/admin/(shell)/risk/draws/[drawId]/pools/[number]/page.tsx new file mode 100644 index 0000000..b6e08b0 --- /dev/null +++ b/src/app/admin/(shell)/risk/draws/[drawId]/pools/[number]/page.tsx @@ -0,0 +1,10 @@ +import { RiskPoolDetailConsole } from "@/modules/risk/risk-pool-detail-console"; + +export default async function AdminRiskPoolDetailPage(props: { + params: Promise<{ drawId: string; number: string }>; +}) { + const { drawId, number } = await props.params; + const id = Number(drawId); + + return ; +} diff --git a/src/app/admin/(shell)/risk/draws/[drawId]/pools/page.tsx b/src/app/admin/(shell)/risk/draws/[drawId]/pools/page.tsx new file mode 100644 index 0000000..1264c60 --- /dev/null +++ b/src/app/admin/(shell)/risk/draws/[drawId]/pools/page.tsx @@ -0,0 +1,19 @@ +import { RiskPoolsConsole } from "@/modules/risk/risk-pools-console"; + +export default async function AdminRiskPoolsPage(props: { + params: Promise<{ drawId: string }>; +}) { + const { drawId } = await props.params; + const id = Number(drawId); + + return ( + + ); +} diff --git a/src/app/admin/(shell)/risk/draws/[drawId]/sold-out/page.tsx b/src/app/admin/(shell)/risk/draws/[drawId]/sold-out/page.tsx new file mode 100644 index 0000000..d90a731 --- /dev/null +++ b/src/app/admin/(shell)/risk/draws/[drawId]/sold-out/page.tsx @@ -0,0 +1,18 @@ +import { RiskPoolsConsole } from "@/modules/risk/risk-pools-console"; + +export default async function AdminRiskSoldOutPage(props: { + params: Promise<{ drawId: string }>; +}) { + const { drawId } = await props.params; + const id = Number(drawId); + + return ( + + ); +} diff --git a/src/app/admin/(shell)/risk/page.tsx b/src/app/admin/(shell)/risk/page.tsx index b205da6..846fddc 100644 --- a/src/app/admin/(shell)/risk/page.tsx +++ b/src/app/admin/(shell)/risk/page.tsx @@ -1,21 +1,10 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { riskModuleMeta } from "@/modules/risk/meta"; -import type { Metadata } from "next"; +import { RiskIndexConsole } from "@/modules/risk/risk-index-console"; -export const metadata: Metadata = { - title: riskModuleMeta.title, -}; - -export default function AdminRiskPage() { +export default function AdminRiskIndexPage() { return ( - -

- 业务组件请放在{" "} - - src/modules/risk - {" "} - 下。 -

+ + ); } diff --git a/src/lib/money.ts b/src/lib/money.ts new file mode 100644 index 0000000..241dae5 --- /dev/null +++ b/src/lib/money.ts @@ -0,0 +1,8 @@ +/** 后台列表统一:最小货币单位 → 主货币展示(默认 2 位小数,与钱包一致) */ +export function formatAdminMinorUnits(minor: number, currencyCode = "NPR"): string { + const major = minor / 100; + return `${currencyCode} ${major.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; +} diff --git a/src/modules/risk/risk-draw-header.tsx b/src/modules/risk/risk-draw-header.tsx new file mode 100644 index 0000000..eb53647 --- /dev/null +++ b/src/modules/risk/risk-draw-header.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { getAdminDraw } from "@/api/admin-draws"; +import { DrawStatusBadge } from "@/modules/draws/draw-status-badge"; +import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminDrawShowData } from "@/types/api/admin-draws"; + +export function RiskDrawHeader({ drawId }: { drawId: number }) { + const [draw, setDraw] = useState(null); + const [error, setError] = useState(null); + + const load = useCallback(async () => { + setError(null); + try { + const d = await getAdminDraw(drawId); + setDraw(d); + } catch (e) { + const msg = + e instanceof LotteryApiBizError ? e.message : "无法加载期号信息"; + setError(msg); + setDraw(null); + } + }, [drawId]); + + useEffect(() => { + queueMicrotask(() => { + void load(); + }); + }, [load]); + + if (error) { + return

{error}

; + } + + if (!draw) { + return

加载期号…

; + } + + return ( +
+

+ 风控 · 第 {draw.draw_no} 期 +

+

+ 数据库状态 + + (大厅展示态:{draw.hall_preview_status}) +

+

+ 产品文档:监控热门号码、售罄与风险占用;数据来自真实下注写入的 `risk_pools` / + `risk_pool_lock_logs`。 +

+
+ ); +} diff --git a/src/modules/risk/risk-index-console.tsx b/src/modules/risk/risk-index-console.tsx new file mode 100644 index 0000000..ff748c2 --- /dev/null +++ b/src/modules/risk/risk-index-console.tsx @@ -0,0 +1,101 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; + +import { getAdminDraws } from "@/api/admin-draws"; +import { 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 { DrawStatusBadge } from "@/modules/draws/draw-status-badge"; +import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; +import { cn } from "@/lib/utils"; +import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-draws"; + +export function RiskIndexConsole() { + const formatDt = useAdminDateTimeFormatter(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const d = await getAdminDraws({ page: 1, per_page: 50 }); + setData(d); + } catch (e) { + const msg = + e instanceof LotteryApiBizError ? e.message : "加载期号列表失败"; + setError(msg); + setData(null); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + queueMicrotask(() => { + void load(); + }); + }, [load]); + + return ( + + + 风控中心 + + 选择期号进入:风险占用流水、热门号码(按占用比)、售罄列表、单号码风险池详情(实施计划 §13.4)。 + + + + {error ?

{error}

: null} + {loading ? ( +

加载中…

+ ) : ( +
+ + + + 期号 + 状态 + 封盘时间 + 操作 + + + + {(data?.items ?? []).map((row: AdminDrawListItem) => ( + + {row.draw_no} + + + + + {row.close_time ? formatDt(row.close_time) : "—"} + + + + 进入风控 + + + + ))} + +
+
+ )} +
+
+ ); +} diff --git a/src/modules/risk/risk-lock-logs-console.tsx b/src/modules/risk/risk-lock-logs-console.tsx new file mode 100644 index 0000000..de932e7 --- /dev/null +++ b/src/modules/risk/risk-lock-logs-console.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { getAdminRiskPoolLockLogs } from "@/api/admin-risk"; +import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +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 { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminRiskLockLogListData, AdminRiskLockLogRow } from "@/types/api/admin-risk"; + +const ACTION_ALL = "__all__"; + +export function RiskLockLogsConsole({ drawId }: { drawId: number }) { + const formatDt = useAdminDateTimeFormatter(); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(25); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [draftNumber, setDraftNumber] = useState(""); + const [appliedNumber, setAppliedNumber] = useState(""); + const [draftAction, setDraftAction] = useState(ACTION_ALL); + const [appliedAction, setAppliedAction] = useState(ACTION_ALL); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const d = await getAdminRiskPoolLockLogs(drawId, { + page, + per_page: perPage, + normalized_number: appliedNumber.trim() === "" ? undefined : appliedNumber.trim(), + action_type: + appliedAction === ACTION_ALL + ? undefined + : (appliedAction as "lock" | "release"), + }); + setData(d); + } catch (e) { + const msg = + e instanceof LotteryApiBizError ? e.message : "加载占用流水失败"; + setError(msg); + setData(null); + } finally { + setLoading(false); + } + }, [drawId, page, perPage, appliedAction, appliedNumber]); + + useEffect(() => { + queueMicrotask(() => { + void load(); + }); + }, [load]); + + return ( + + + 风险占用流水 + + 每次下注锁定 / 回滚释放写入 `risk_pool_lock_logs`;可按号码与动作筛选(产品文档:后台监控风险占用)。 + + + +
+
+ + setDraftNumber(e.target.value.replace(/\D/g, "").slice(0, 4))} + placeholder="可选" + /> +
+
+ + +
+
+ +
+
+ + {error ?

{error}

: null} + + {loading && !data ? ( +

加载中…

+ ) : ( + <> +
+ + + + 时间 + 号码 + 动作 + 金额 + 来源 + 注单号 + 玩法 + + + + {(data?.items ?? []).map((row: AdminRiskLockLogRow) => ( + + + {row.created_at ? formatDt(row.created_at) : "—"} + + {row.normalized_number} + {row.action_type} + + {formatAdminMinorUnits(row.amount)} + + + {row.source_reason ?? "—"} + + {row.ticket_no ?? "—"} + {row.play_code ?? "—"} + + ))} + +
+
+ + {data ? ( + { + setPerPage(n); + setPage(1); + }} + onPageChange={setPage} + /> + ) : null} + + )} +
+
+ ); +} diff --git a/src/modules/risk/risk-pool-detail-console.tsx b/src/modules/risk/risk-pool-detail-console.tsx new file mode 100644 index 0000000..d5376f7 --- /dev/null +++ b/src/modules/risk/risk-pool-detail-console.tsx @@ -0,0 +1,195 @@ +"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, CardDescription, 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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( + + + 风险池详情 + {error} + + + + 返回列表 + + + + ); + } + + if (loading && !data) { + return

加载中…

; + } + + if (!data) { + return null; + } + + const { pool, logs } = data; + + return ( +
+
+ + ← 返回全部风险池 + +
+ + + + + 号码 {pool.normalized_number} + + + 期号 {data.draw_no} · 封顶与占用来自 `risk_pools`;售罄表示剩余额度为 0(产品文档 §6.4)。 + + + +
+

封顶额

+

+ {formatAdminMinorUnits(pool.total_cap_amount)} +

+
+
+

已占用(最坏赔付预留)

+

+ {formatAdminMinorUnits(pool.locked_amount)} +

+
+
+

剩余可售

+

+ {formatAdminMinorUnits(pool.remaining_amount)} +

+
+
+

售罄

+

{pool.is_sold_out ? "是" : "否"}

+

+ 占用比{" "} + {pool.usage_ratio != null ? `${(pool.usage_ratio * 100).toFixed(2)}%` : "—"} +

+
+
+
+ + + + 本号码占用 / 释放流水 + 与「风险占用」页同一数据源,此处限定单号码。 + + +
+ + + + 时间 + 动作 + 金额 + 来源 + 注单号 + 玩法 + + + + {logs.items.map((row: AdminRiskPoolDetailLogRow) => ( + + + {row.created_at ? formatDt(row.created_at) : "—"} + + {row.action_type} + + {formatAdminMinorUnits(row.amount)} + + + {row.source_reason ?? "—"} + + {row.ticket_no ?? "—"} + {row.play_code ?? "—"} + + ))} + +
+
+ + { + setPerPage(n); + setPage(1); + }} + onPageChange={setPage} + /> +
+
+
+ ); +} diff --git a/src/modules/risk/risk-pools-console.tsx b/src/modules/risk/risk-pools-console.tsx new file mode 100644 index 0000000..b21ebb8 --- /dev/null +++ b/src/modules/risk/risk-pools-console.tsx @@ -0,0 +1,196 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; + +import { getAdminRiskPools } from "@/api/admin-risk"; +import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; +import { buttonVariants } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { formatAdminMinorUnits } from "@/lib/money"; +import { cn } from "@/lib/utils"; +import { LotteryApiBizError } from "@/types/api/errors"; +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: "占用比 ↓(热门)" }, + { value: "locked_desc", label: "已占用额 ↓" }, + { value: "remaining_asc", label: "剩余额 ↑(紧俏)" }, + { value: "number_asc", label: "号码 ↑" }, + ]; + +type RiskPoolsConsoleProps = { + drawId: number; + title: string; + description: string; + soldOutOnly: boolean; + defaultSort: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc"; + allowSortChange?: boolean; +}; + +export function RiskPoolsConsole({ + drawId, + title, + description, + soldOutOnly, + defaultSort, + allowSortChange = false, +}: RiskPoolsConsoleProps) { + const [sort, setSort] = useState(defaultSort); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(25); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + 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: soldOutOnly, + sort, + }); + setData(d); + } catch (e) { + const msg = + e instanceof LotteryApiBizError ? e.message : "加载风险池失败"; + setError(msg); + setData(null); + } finally { + setLoading(false); + } + }, [drawId, page, perPage, soldOutOnly, sort]); + + useEffect(() => { + queueMicrotask(() => { + void load(); + }); + }, [load]); + + return ( + + + {title} + {description} + {allowSortChange ? ( +
+
+ + +
+
+ ) : null} +
+ + {error ?

{error}

: null} + {loading && !data ? ( +

加载中…

+ ) : ( + <> +
+ + + + 号码 + 封顶 + 已占用 + 剩余 + 占用比 + 售罄 + 详情 + + + + {(data?.items ?? []).map((row: AdminRiskPoolRow) => ( + + {row.normalized_number} + + {formatAdminMinorUnits(row.total_cap_amount)} + + + {formatAdminMinorUnits(row.locked_amount)} + + + {formatAdminMinorUnits(row.remaining_amount)} + + + {row.usage_ratio != null ? `${(row.usage_ratio * 100).toFixed(2)}%` : "—"} + + {row.is_sold_out ? "是" : "否"} + + + 查看 + + + + ))} + +
+
+ + {data ? ( + { + setPerPage(n); + setPage(1); + }} + onPageChange={setPage} + /> + ) : null} + + )} +
+
+ ); +} diff --git a/src/modules/risk/risk-subnav.tsx b/src/modules/risk/risk-subnav.tsx new file mode 100644 index 0000000..b5afc30 --- /dev/null +++ b/src/modules/risk/risk-subnav.tsx @@ -0,0 +1,46 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +const segments = [ + { suffix: "/occupancy", key: "occupancy", label: "风险占用" }, + { suffix: "/hot", key: "hot", label: "热门号码" }, + { suffix: "/sold-out", key: "sold-out", label: "售罄列表" }, + { suffix: "/pools", key: "pools", label: "全部风险池" }, +] as const; + +export function RiskSubnav({ drawId }: { drawId: string }) { + const pathname = usePathname(); + const base = `/admin/risk/draws/${drawId}`; + + return ( + + ); +} diff --git a/src/types/api/admin-risk.ts b/src/types/api/admin-risk.ts new file mode 100644 index 0000000..ee715c3 --- /dev/null +++ b/src/types/api/admin-risk.ts @@ -0,0 +1,66 @@ +export type AdminRiskPoolListMeta = { + current_page: number; + per_page: number; + total: number; + last_page: number; +}; + +export type AdminRiskPoolRow = { + normalized_number: string; + total_cap_amount: number; + locked_amount: number; + remaining_amount: number; + sold_out_status: number; + is_sold_out: boolean; + usage_ratio: number | null; + version: number; +}; + +export type AdminRiskPoolListData = { + draw_id: number; + draw_no: string; + items: AdminRiskPoolRow[]; + meta: AdminRiskPoolListMeta; +}; + +export type AdminRiskLockLogRow = { + id: number; + normalized_number: string; + action_type: string; + amount: number; + source_reason: string | null; + ticket_item_id: number | null; + ticket_no: string | null; + play_code: string | null; + player_id: number | null; + created_at: string | null; +}; + +export type AdminRiskLockLogListData = { + draw_id: number; + draw_no: string; + items: AdminRiskLockLogRow[]; + meta: AdminRiskPoolListMeta; +}; + +export type AdminRiskPoolDetailLogRow = { + id: number; + action_type: string; + amount: number; + source_reason: string | null; + ticket_item_id: number | null; + ticket_no: string | null; + play_code: string | null; + player_id: number | null; + created_at: string | null; +}; + +export type AdminRiskPoolShowData = { + draw_id: number; + draw_no: string; + pool: AdminRiskPoolRow; + logs: { + items: AdminRiskPoolDetailLogRow[]; + meta: AdminRiskPoolListMeta; + }; +};