diff --git a/src/api/admin-jackpot.ts b/src/api/admin-jackpot.ts new file mode 100644 index 0000000..98e7d8d --- /dev/null +++ b/src/api/admin-jackpot.ts @@ -0,0 +1,51 @@ +import { adminRequest } from "@/lib/admin-http"; + +import { API_V1_PREFIX } from "./paths"; + +import type { + AdminJackpotContributionsData, + AdminJackpotPoolsData, + AdminJackpotPayoutLogsData, + AdminJackpotPoolRow, +} from "@/types/api/admin-jackpot"; + +const A = `${API_V1_PREFIX}/admin`; + +export async function getAdminJackpotPools(): Promise { + return adminRequest.get(`${A}/jackpot/pools`); +} + +export type AdminJackpotPoolUpdateBody = Partial<{ + current_amount: number; + contribution_rate: number; + trigger_threshold: number; + payout_rate: number; + force_trigger_draw_gap: number; + min_bet_amount: number; + status: number; +}>; + +export async function putAdminJackpotPool( + poolId: number, + body: AdminJackpotPoolUpdateBody, +): Promise { + return adminRequest.put(`${A}/jackpot/pools/${poolId}`, body); +} + +export type AdminJackpotLogsQuery = { + page?: number; + per_page?: number; + draw_no?: string; +}; + +export async function getAdminJackpotPayoutLogs( + q: AdminJackpotLogsQuery = {}, +): Promise { + return adminRequest.get(`${A}/jackpot/payout-logs`, { params: q }); +} + +export async function getAdminJackpotContributions( + q: AdminJackpotLogsQuery = {}, +): Promise { + return adminRequest.get(`${A}/jackpot/contributions`, { params: q }); +} diff --git a/src/api/admin-settlement.ts b/src/api/admin-settlement.ts new file mode 100644 index 0000000..af9652d --- /dev/null +++ b/src/api/admin-settlement.ts @@ -0,0 +1,43 @@ +import { adminRequest } from "@/lib/admin-http"; + +import { API_V1_PREFIX } from "./paths"; + +import type { + AdminSettlementBatchDetailsData, + AdminSettlementBatchListData, + AdminSettlementBatchShowData, +} from "@/types/api/admin-settlement"; + +const A = `${API_V1_PREFIX}/admin`; + +export type AdminSettlementBatchListQuery = { + page?: number; + per_page?: number; + draw_no?: string; + status?: string; +}; + +export async function getAdminSettlementBatches( + q: AdminSettlementBatchListQuery = {}, +): Promise { + return adminRequest.get(`${A}/settlement-batches`, { params: q }); +} + +export async function getAdminSettlementBatch(batchId: number): Promise { + return adminRequest.get(`${A}/settlement-batches/${batchId}`); +} + +export type AdminSettlementBatchDetailsQuery = { + page?: number; + per_page?: number; +}; + +export async function getAdminSettlementBatchDetails( + batchId: number, + q: AdminSettlementBatchDetailsQuery = {}, +): Promise { + return adminRequest.get( + `${A}/settlement-batches/${batchId}/details`, + { params: q }, + ); +} diff --git a/src/app/admin/(shell)/jackpot/layout.tsx b/src/app/admin/(shell)/jackpot/layout.tsx new file mode 100644 index 0000000..86c86d7 --- /dev/null +++ b/src/app/admin/(shell)/jackpot/layout.tsx @@ -0,0 +1,10 @@ +import { JackpotSubNav } from "@/modules/jackpot/jackpot-subnav"; + +export default function AdminJackpotLayout({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/app/admin/(shell)/jackpot/pools/page.tsx b/src/app/admin/(shell)/jackpot/pools/page.tsx new file mode 100644 index 0000000..739c9a5 --- /dev/null +++ b/src/app/admin/(shell)/jackpot/pools/page.tsx @@ -0,0 +1,19 @@ +import { JackpotPoolsConsole } from "@/modules/jackpot/jackpot-pools-console"; +import { jackpotModuleMeta } from "@/modules/jackpot/meta"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: `奖池配置 · ${jackpotModuleMeta.title}`, +}; + +export default function AdminJackpotPoolsPage() { + return ( + <> +
+

{jackpotModuleMeta.title}

+

{jackpotModuleMeta.description}

+
+ + + ); +} diff --git a/src/app/admin/(shell)/jackpot/records/page.tsx b/src/app/admin/(shell)/jackpot/records/page.tsx new file mode 100644 index 0000000..3967290 --- /dev/null +++ b/src/app/admin/(shell)/jackpot/records/page.tsx @@ -0,0 +1,19 @@ +import { JackpotRecordsConsole } from "@/modules/jackpot/jackpot-records-console"; +import { jackpotModuleMeta } from "@/modules/jackpot/meta"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: `Jackpot 记录 · ${jackpotModuleMeta.title}`, +}; + +export default function AdminJackpotRecordsPage() { + return ( + <> +
+

Jackpot 记录

+

派彩与蓄水流水

+
+ + + ); +} diff --git a/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx b/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx new file mode 100644 index 0000000..1f6ebf5 --- /dev/null +++ b/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx @@ -0,0 +1,19 @@ +import { SettlementBatchDetailsConsole } from "@/modules/settlement/settlement-batch-details-console"; +import { settlementModuleMeta } from "@/modules/settlement/meta"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: `结算明细 · ${settlementModuleMeta.title}`, +}; + +export default async function AdminSettlementBatchDetailsPage(props: { + params: Promise<{ batchId: string }>; +}) { + const { batchId } = await props.params; + const id = Number.parseInt(batchId, 10); + if (!Number.isFinite(id) || id < 1) { + return

无效的批次 ID

; + } + + return ; +} diff --git a/src/app/admin/(shell)/settlement-batches/page.tsx b/src/app/admin/(shell)/settlement-batches/page.tsx new file mode 100644 index 0000000..21770f3 --- /dev/null +++ b/src/app/admin/(shell)/settlement-batches/page.tsx @@ -0,0 +1,11 @@ +import { SettlementBatchesConsole } from "@/modules/settlement/settlement-batches-console"; +import { settlementModuleMeta } from "@/modules/settlement/meta"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: settlementModuleMeta.title, +}; + +export default function AdminSettlementBatchesPage() { + return ; +} diff --git a/src/components/admin/admin-sidebar.tsx b/src/components/admin/admin-sidebar.tsx index b5d1059..a86ec92 100644 --- a/src/components/admin/admin-sidebar.tsx +++ b/src/components/admin/admin-sidebar.tsx @@ -24,11 +24,13 @@ import { } from "@/modules/_config/admin-nav-icons"; import { adminShellNavItems, ADMIN_BASE } from "@/modules/_config/admin-nav"; -function isActive(pathname: string, href: string): boolean { - if (href === ADMIN_BASE || href === `${ADMIN_BASE}/`) { +function isActive(pathname: string, item: { href: string; activeMatchPrefix?: string }): boolean { + const { href, activeMatchPrefix } = item; + const prefix = activeMatchPrefix ?? href; + if (prefix === ADMIN_BASE || prefix === `${ADMIN_BASE}/`) { return pathname === ADMIN_BASE || pathname === `${ADMIN_BASE}/`; } - return pathname === href || pathname.startsWith(`${href}/`); + return pathname === prefix || pathname.startsWith(`${prefix}/`); } export function AdminAppSidebar() { @@ -67,7 +69,7 @@ export function AdminAppSidebar() { } > diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index 6f7bddc..8ce0578 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -1,6 +1,8 @@ import type { LucideIcon } from "lucide-react"; import { CalendarClock, + CircleDollarSign, + Landmark, LayoutDashboard, LogIn, Settings, @@ -23,6 +25,8 @@ export const adminNavIconBySegment: Record tickets: Ticket, wallet: Wallet, risk: ShieldAlert, + settlement: Landmark, + jackpot: CircleDollarSign, settings: Settings, }; diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index 886324b..287ffb6 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -16,7 +16,11 @@ export type AdminNavItem = { | "tickets" | "wallet" | "risk" - | "settings"; + | "settings" + | "settlement" + | "jackpot"; + /** 高亮匹配:默认用 `href`;Jackpot 多子页时传公共前缀如 `/admin/jackpot` */ + activeMatchPrefix?: string; }; export const adminShellNavItems: AdminNavItem[] = [ @@ -27,5 +31,12 @@ export const adminShellNavItems: AdminNavItem[] = [ { segment: "tickets", label: "注单 / 票务", href: "/admin/tickets" }, { segment: "wallet", label: "钱包", href: "/admin/wallet" }, { segment: "risk", label: "风控", href: "/admin/risk" }, + { segment: "settlement", label: "结算", href: "/admin/settlement-batches" }, + { + segment: "jackpot", + label: "Jackpot", + href: "/admin/jackpot/pools", + activeMatchPrefix: "/admin/jackpot", + }, { segment: "settings", label: "系统设置", href: "/admin/settings" }, ]; diff --git a/src/modules/jackpot/jackpot-pools-console.tsx b/src/modules/jackpot/jackpot-pools-console.tsx new file mode 100644 index 0000000..70bec60 --- /dev/null +++ b/src/modules/jackpot/jackpot-pools-console.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { getAdminJackpotPools, putAdminJackpotPool } from "@/api/admin-jackpot"; +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +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 { formatAdminMinorUnits } from "@/lib/money"; +import { toast } from "sonner"; +import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminJackpotPoolRow } from "@/types/api/admin-jackpot"; + +type Draft = { + current_amount: string; + contribution_rate: string; + trigger_threshold: string; + payout_rate: string; + force_trigger_draw_gap: string; + min_bet_amount: string; + status: string; +}; + +function toDraft(p: AdminJackpotPoolRow): Draft { + return { + current_amount: String(p.current_amount), + contribution_rate: String(p.contribution_rate), + trigger_threshold: String(p.trigger_threshold), + payout_rate: String(p.payout_rate), + force_trigger_draw_gap: String(p.force_trigger_draw_gap), + min_bet_amount: String(p.min_bet_amount), + status: String(p.status), + }; +} + +export function JackpotPoolsConsole() { + const [items, setItems] = useState([]); + const [drafts, setDrafts] = useState>({}); + const [loading, setLoading] = useState(true); + const [savingId, setSavingId] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + try { + const res = await getAdminJackpotPools(); + setItems(res.items); + const d: Record = {}; + for (const p of res.items) { + d[p.id] = toDraft(p); + } + setDrafts(d); + } catch (e) { + toast.error(e instanceof LotteryApiBizError ? e.message : "加载失败"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + queueMicrotask(() => { + void load(); + }); + }, [load]); + + const updateDraft = (id: number, patch: Partial) => { + setDrafts((prev) => ({ + ...prev, + [id]: { ...prev[id], ...patch }, + })); + }; + + const save = async (p: AdminJackpotPoolRow) => { + const d = drafts[p.id]; + if (!d) return; + setSavingId(p.id); + try { + await putAdminJackpotPool(p.id, { + current_amount: Number.parseInt(d.current_amount, 10), + contribution_rate: Number(d.contribution_rate), + trigger_threshold: Number.parseInt(d.trigger_threshold, 10), + payout_rate: Number(d.payout_rate), + force_trigger_draw_gap: Number.parseInt(d.force_trigger_draw_gap, 10), + min_bet_amount: Number.parseInt(d.min_bet_amount, 10), + status: Number.parseInt(d.status, 10), + }); + toast.success("已保存"); + await load(); + } catch (e) { + toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败"); + } finally { + setSavingId(null); + } + }; + + return ( + + + + Jackpot 奖池配置 + 蓄水比例、爆池阈值、派彩比例等;修改后保存生效 + + + {loading ?

加载中…

: null} + {!loading && items.length === 0 ? ( +

暂无奖池数据

+ ) : null} + {items.map((p) => { + const d = drafts[p.id] ?? toDraft(p); + return ( +
+
+

{p.currency_code}

+ + 展示余额 {formatAdminMinorUnits(p.current_amount, p.currency_code)} + +
+
+
+ + updateDraft(p.id, { current_amount: e.target.value })} + /> +
+
+ + updateDraft(p.id, { contribution_rate: e.target.value })} + /> +
+
+ + updateDraft(p.id, { trigger_threshold: e.target.value })} + /> +
+
+ + updateDraft(p.id, { payout_rate: e.target.value })} + /> +
+
+ + updateDraft(p.id, { force_trigger_draw_gap: e.target.value })} + /> +
+
+ + updateDraft(p.id, { min_bet_amount: e.target.value })} + /> +
+
+ + +
+
+
+ +
+
+ ); + })} +
+
+
+ ); +} diff --git a/src/modules/jackpot/jackpot-records-console.tsx b/src/modules/jackpot/jackpot-records-console.tsx new file mode 100644 index 0000000..161dc53 --- /dev/null +++ b/src/modules/jackpot/jackpot-records-console.tsx @@ -0,0 +1,236 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { getAdminJackpotContributions, getAdminJackpotPayoutLogs } from "@/api/admin-jackpot"; +import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +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 { + 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 { + AdminJackpotContributionsData, + AdminJackpotPayoutLogsData, +} from "@/types/api/admin-jackpot"; + +export function JackpotRecordsConsole() { + const formatDt = useAdminDateTimeFormatter(); + const [drawNo, setDrawNo] = useState(""); + const [appliedDrawNo, setAppliedDrawNo] = useState(""); + + const [payouts, setPayouts] = useState(null); + const [pPage, setPPage] = useState(1); + const [pPer, setPPer] = useState(15); + + const [contribs, setContribs] = useState(null); + const [cPage, setCPage] = useState(1); + const [cPer, setCPer] = useState(15); + + const [loadingP, setLoadingP] = useState(true); + const [loadingC, setLoadingC] = useState(true); + const [err, setErr] = useState(null); + + const loadPayouts = useCallback(async () => { + setLoadingP(true); + try { + const d = await getAdminJackpotPayoutLogs({ + page: pPage, + per_page: pPer, + draw_no: appliedDrawNo.trim() || undefined, + }); + setPayouts(d); + } catch (e) { + setErr(e instanceof LotteryApiBizError ? e.message : "派彩记录加载失败"); + } finally { + setLoadingP(false); + } + }, [pPage, pPer, appliedDrawNo]); + + const loadContribs = useCallback(async () => { + setLoadingC(true); + try { + const d = await getAdminJackpotContributions({ + page: cPage, + per_page: cPer, + draw_no: appliedDrawNo.trim() || undefined, + }); + setContribs(d); + } catch (e) { + setErr(e instanceof LotteryApiBizError ? e.message : "蓄水记录加载失败"); + } finally { + setLoadingC(false); + } + }, [cPage, cPer, appliedDrawNo]); + + useEffect(() => { + queueMicrotask(() => { + void loadPayouts(); + }); + }, [loadPayouts]); + + useEffect(() => { + queueMicrotask(() => { + void loadContribs(); + }); + }, [loadContribs]); + + const applyDraw = () => { + setAppliedDrawNo(drawNo); + setPPage(1); + setCPage(1); + }; + + return ( + + + + 筛选 + 按期号模糊过滤两类记录 + + +
+ + setDrawNo(e.target.value)} + placeholder="可选" + /> +
+ +
+
+ + {err ?

{err}

: null} + + + + Jackpot 派彩记录 + 爆池触发与划出总额 + + + {loadingP && !payouts ? ( +

加载中…

+ ) : ( + + + + ID + 期号 + 触发 + 派彩额 + 中奖人数 + 时间 + + + + {(payouts?.items ?? []).map((r) => ( + + {r.id} + {r.draw_no ?? "—"} + {r.trigger_type} + + {formatAdminMinorUnits(r.total_payout_amount, r.currency_code ?? "NPR")} + + {r.winner_count} + + {formatDt(r.created_at)} + + + ))} + +
+ )} + {payouts ? ( + { + setPPer(n); + setPPage(1); + }} + onPageChange={setPPage} + /> + ) : null} +
+
+ + + + Jackpot 蓄水记录 + 每笔注单蓄水入账流水 + + + {loadingC && !contribs ? ( +

加载中…

+ ) : ( + + + + ID + 期号 + 注单 + 玩家 + 蓄水额 + 时间 + + + + {(contribs?.items ?? []).map((r) => ( + + {r.id} + {r.draw_no ?? "—"} + {r.ticket_no ?? "—"} + + {r.player_username ?? "—"} + + + {formatAdminMinorUnits(r.contribution_amount, r.currency_code ?? "NPR")} + + + {formatDt(r.created_at)} + + + ))} + +
+ )} + {contribs ? ( + { + setCPer(n); + setCPage(1); + }} + onPageChange={setCPage} + /> + ) : null} +
+
+
+ ); +} diff --git a/src/modules/jackpot/jackpot-subnav.tsx b/src/modules/jackpot/jackpot-subnav.tsx new file mode 100644 index 0000000..3a41267 --- /dev/null +++ b/src/modules/jackpot/jackpot-subnav.tsx @@ -0,0 +1,37 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { cn } from "@/lib/utils"; + +const LINKS: { href: string; label: string }[] = [ + { href: "/admin/jackpot/pools", label: "奖池配置" }, + { href: "/admin/jackpot/records", label: "记录" }, +]; + +export function JackpotSubNav() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/src/modules/jackpot/meta.ts b/src/modules/jackpot/meta.ts new file mode 100644 index 0000000..c69398e --- /dev/null +++ b/src/modules/jackpot/meta.ts @@ -0,0 +1,4 @@ +export const jackpotModuleMeta = { + title: "Jackpot", + description: "奖池配置与蓄水 / 派彩记录", +} as const; diff --git a/src/modules/settlement/meta.ts b/src/modules/settlement/meta.ts new file mode 100644 index 0000000..ae9f633 --- /dev/null +++ b/src/modules/settlement/meta.ts @@ -0,0 +1,4 @@ +export const settlementModuleMeta = { + title: "结算批次", + description: "按期查看结算批次与注单结算明细", +} as const; diff --git a/src/modules/settlement/settlement-batch-details-console.tsx b/src/modules/settlement/settlement-batch-details-console.tsx new file mode 100644 index 0000000..945d674 --- /dev/null +++ b/src/modules/settlement/settlement-batch-details-console.tsx @@ -0,0 +1,190 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; + +import { getAdminSettlementBatch, getAdminSettlementBatchDetails } from "@/api/admin-settlement"; +import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { Button, 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 { + AdminSettlementBatchDetailsData, + AdminSettlementBatchShowData, +} from "@/types/api/admin-settlement"; + +type Props = { + batchId: number; +}; + +export function SettlementBatchDetailsConsole({ batchId }: Props) { + const formatDt = useAdminDateTimeFormatter(); + const [summary, setSummary] = useState(null); + const [details, setDetails] = useState(null); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(null); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(25); + + const load = useCallback(async () => { + setLoading(true); + setErr(null); + try { + const [s, d] = await Promise.all([ + getAdminSettlementBatch(batchId), + getAdminSettlementBatchDetails(batchId, { page, per_page: perPage }), + ]); + setSummary(s); + setDetails(d); + } catch (e) { + setErr(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setSummary(null); + setDetails(null); + } finally { + setLoading(false); + } + }, [batchId, page, perPage]); + + useEffect(() => { + const t = window.setTimeout(() => void load(), 0); + return () => window.clearTimeout(t); + }, [load]); + + return ( + +
+ + ← 返回批次列表 + +
+ + {err ? ( + + + 错误 + {err} + + + + + + ) : null} + + {summary ? ( + + + 批次 #{summary.id} + + 期号 {summary.draw_no ?? "—"} · 期状态 {summary.draw_status ?? "—"} · 结果批次 v + {summary.result_batch_version ?? "—"} + + + +

+ 结算状态{" "} + {summary.status} +

+

+ 注单数{" "} + {summary.total_ticket_count} +

+

+ 中奖笔数{" "} + {summary.total_win_count} +

+

+ 派彩合计{" "} + {formatAdminMinorUnits(summary.total_payout_amount)} +

+

+ Jackpot 划出{" "} + + {formatAdminMinorUnits(summary.total_jackpot_payout_amount)} + +

+

+ 开始 {formatDt(summary.started_at)} +

+

+ 结束 {formatDt(summary.finished_at)} +

+
+
+ ) : loading ? ( +

加载摘要…

+ ) : null} + + + + 注单结算明细 + 该批次内每条注项的匹配档与派彩拆分 + + + {details ? ( + <> + + + + 注单号 + 玩法 + 玩家 + 匹配档 + 常规派彩 + Jackpot + + + + {details.items.map((r) => ( + + {r.ticket_no ?? "—"} + {r.play_code ?? "—"} + + {r.player_username ?? r.site_player_id ?? r.player_id ?? "—"} + + {r.matched_prize_tier ?? "—"} + + {formatAdminMinorUnits(r.win_amount)} + + + {formatAdminMinorUnits(r.jackpot_allocation_amount)} + + + ))} + +
+ { + setPerPage(n); + setPage(1); + }} + onPageChange={setPage} + /> + + ) : ( +

{loading ? "加载明细…" : "无数据"}

+ )} +
+
+
+ ); +} diff --git a/src/modules/settlement/settlement-batches-console.tsx b/src/modules/settlement/settlement-batches-console.tsx new file mode 100644 index 0000000..0cb607e --- /dev/null +++ b/src/modules/settlement/settlement-batches-console.tsx @@ -0,0 +1,218 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; + +import { getAdminSettlementBatches } from "@/api/admin-settlement"; +import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { Button, buttonVariants } 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 { cn } from "@/lib/utils"; +import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminSettlementBatchListData, AdminSettlementBatchRow } from "@/types/api/admin-settlement"; + +import { settlementModuleMeta } from "@/modules/settlement/meta"; + +const STATUS_ALL = "__all__"; +const STATUS_OPTIONS: { value: string; label: string }[] = [ + { value: STATUS_ALL, label: "不限" }, + { value: "running", label: "进行中" }, + { value: "completed", label: "已完成" }, + { value: "failed", label: "失败" }, +]; + +export function SettlementBatchesConsole() { + const formatDt = useAdminDateTimeFormatter(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [draftDrawNo, setDraftDrawNo] = useState(""); + const [appliedDrawNo, setAppliedDrawNo] = useState(""); + const [draftStatus, setDraftStatus] = useState(STATUS_ALL); + const [appliedStatus, setAppliedStatus] = useState(STATUS_ALL); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(20); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const d = await getAdminSettlementBatches({ + page, + per_page: perPage, + draw_no: appliedDrawNo.trim() || undefined, + status: + appliedStatus === STATUS_ALL || appliedStatus.trim() === "" + ? undefined + : appliedStatus.trim(), + }); + setData(d); + } catch (e) { + setError(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setData(null); + } finally { + setLoading(false); + } + }, [page, perPage, appliedDrawNo, appliedStatus]); + + useEffect(() => { + const t = window.setTimeout(() => void load(), 0); + return () => window.clearTimeout(t); + }, [load]); + + const applyFilters = () => { + setAppliedDrawNo(draftDrawNo); + setAppliedStatus(draftStatus); + setPage(1); + }; + + return ( + +
+

{settlementModuleMeta.title}

+

{settlementModuleMeta.description}

+
+ + + 筛选 + 按业务期号、批次状态过滤 + + +
+ + setDraftDrawNo(e.target.value)} + placeholder="如 20260511-001" + className="font-mono" + /> +
+
+ + +
+ +
+
+ + + + 结算批次 + 每期与采纳开奖版本对应的一次结算运行 + + + {error ?

{error}

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

加载中…

+ ) : ( + + + + ID + 期号 + 版本 + 状态 + 注单数 + 中奖笔数 + 派彩合计 + Jackpot + 完成时间 + + + + + {(data?.items ?? []).map((row: AdminSettlementBatchRow) => ( + + {row.id} + {row.draw_no ?? "—"} + v{row.settle_version} + + + {row.status} + + + {row.total_ticket_count} + {row.total_win_count} + + {formatAdminMinorUnits(row.total_payout_amount)} + + + {formatAdminMinorUnits(row.total_jackpot_payout_amount)} + + + {formatDt(row.finished_at ?? row.started_at)} + + + + 明细 + + + + ))} + +
+ )} + {data ? ( + { + setPerPage(n); + setPage(1); + }} + onPageChange={setPage} + /> + ) : null} +
+
+
+ ); +} diff --git a/src/types/api/admin-jackpot.ts b/src/types/api/admin-jackpot.ts new file mode 100644 index 0000000..a4941c0 --- /dev/null +++ b/src/types/api/admin-jackpot.ts @@ -0,0 +1,64 @@ +export type AdminJackpotPoolRow = { + id: number; + currency_code: string; + current_amount: number; + contribution_rate: string; + trigger_threshold: number; + payout_rate: string; + force_trigger_draw_gap: number; + min_bet_amount: number; + status: number; + last_trigger_draw_id: number | null; + updated_at: string | null; +}; + +export type AdminJackpotPoolsData = { + items: AdminJackpotPoolRow[]; +}; + +export type AdminJackpotPayoutLogRow = { + id: number; + draw_id: number; + draw_no: string | null; + jackpot_pool_id: number; + currency_code: string | null; + trigger_type: string; + total_payout_amount: number; + winner_count: number; + trigger_snapshot_json: unknown; + created_at: string | null; +}; + +export type AdminJackpotPayoutLogsData = { + items: AdminJackpotPayoutLogRow[]; + meta: { + current_page: number; + per_page: number; + total: number; + last_page: number; + }; +}; + +export type AdminJackpotContributionRow = { + id: number; + draw_id: number; + draw_no: string | null; + jackpot_pool_id: number; + currency_code: string | null; + player_id: number; + player_username: string | null; + ticket_item_id: number | null; + ticket_no: string | null; + contribution_amount: number; + created_at: string | null; +}; + +export type AdminJackpotContributionsData = { + items: AdminJackpotContributionRow[]; + meta: { + current_page: number; + per_page: number; + total: number; + last_page: number; + }; +}; diff --git a/src/types/api/admin-settlement.ts b/src/types/api/admin-settlement.ts new file mode 100644 index 0000000..2062ded --- /dev/null +++ b/src/types/api/admin-settlement.ts @@ -0,0 +1,70 @@ +export type AdminSettlementBatchRow = { + id: number; + draw_id: number; + draw_no: string | null; + result_batch_id: number; + settle_version: number; + status: string; + total_ticket_count: number; + total_win_count: number; + total_payout_amount: number; + total_jackpot_payout_amount: number; + started_at: string | null; + finished_at: string | null; + created_at: string | null; +}; + +export type AdminSettlementBatchListData = { + items: AdminSettlementBatchRow[]; + meta: { + current_page: number; + per_page: number; + total: number; + last_page: number; + }; +}; + +export type AdminSettlementBatchShowData = { + id: number; + draw_id: number; + draw_no: string | null; + draw_status: string | null; + result_batch_id: number; + result_batch_version: number | null; + result_batch_status: string | null; + settle_version: number; + status: string; + total_ticket_count: number; + total_win_count: number; + total_payout_amount: number; + total_jackpot_payout_amount: number; + started_at: string | null; + finished_at: string | null; + created_at: string | null; +}; + +export type AdminSettlementDetailRow = { + id: number; + ticket_item_id: number; + ticket_no: string | null; + play_code: string | null; + player_id: number | null; + player_username: string | null; + site_player_id: string | null; + matched_prize_tier: string | null; + win_amount: number; + jackpot_allocation_amount: number; + match_detail_json: unknown; + created_at: string | null; +}; + +export type AdminSettlementBatchDetailsData = { + batch_id: number; + items: AdminSettlementDetailRow[]; + meta: { + current_page: number; + per_page: number; + total: number; + last_page: number; + }; +};