import AppsOutlined from "@mui/icons-material/AppsOutlined";
import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined";
import CasinoOutlined from "@mui/icons-material/CasinoOutlined";
import SavingsOutlined from "@mui/icons-material/SavingsOutlined";
import { useMemo, useState } from "react";
import { AdminFilterSelect, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { KpiCard } from "@/shared/ui/KpiCard.jsx";
import { TimeText } from "@/shared/ui/TimeText.jsx";
import { luckyGiftPoolKey } from "../api.js";
import { formatNumber, formatPercentFromPPM } from "../format.js";
import { LuckyGiftPoolActions } from "./LuckyGiftPoolActions.jsx";
import { OpsStatusBadge } from "./OpsStatusBadge.jsx";
export function OverviewView({ canCredit, canDebit, data, disabled, onCredit, onDebit }) {
const { appSummaries, poolBalances, summary } = data;
const [poolScope, setPoolScope] = useState("current");
const poolBalanceColumns = useMemo(
() => buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDebit }),
[canCredit, canDebit, disabled, onCredit, onDebit],
);
const poolCounts = useMemo(
() => ({
all: poolBalances.length,
current: poolBalances.filter((pool) => !pool.is_historical).length,
history: poolBalances.filter((pool) => pool.is_historical).length,
}),
[poolBalances],
);
const visiblePoolBalances = useMemo(() => {
if (poolScope === "history") {
return poolBalances.filter((pool) => pool.is_historical);
}
if (poolScope === "all") {
return poolBalances;
}
// 当前策略是默认视图;历史账本只从显式筛选进入,避免同名 V2/V3 被误读为重复奖池。
return poolBalances.filter((pool) => !pool.is_historical);
}, [poolBalances, poolScope]);
return (
}
/>
item.app_code}
title="各应用抽奖汇总"
total={appSummaries.length}
/>
);
}
function buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDebit }) {
const columns = [
{ key: "app_code", label: "应用", width: "minmax(96px, 0.8fr)" },
{ key: "pool_id", label: "奖池", width: "minmax(110px, 1fr)" },
{ key: "strategy_version", label: "策略", width: "minmax(112px, 0.9fr)" },
{
key: "materialized",
label: "水位状态",
// materialized=false 是 owner 根据当前策略推导的初始水位;仍允许补水或扣水,首次调整由 owner 原子物化该策略奖池。
render: (item) => {
if (item.is_historical) {
return ;
}
return item.materialized === false ? (
) : (
);
},
width: "minmax(132px, 0.9fr)",
},
{ key: "balance", label: "当前余额", render: (item) => formatNumber(item.balance) },
{ key: "available_balance", label: "可用余额", render: (item) => formatNumber(item.available_balance) },
{
key: "reserve_floor",
label: "保底线",
render: (item) => formatNumber(item.reserve_floor),
width: "minmax(96px, 0.8fr)",
},
{ key: "total_in", label: "业务流入", render: (item) => formatNumber(item.total_in) },
{ key: "total_out", label: "返奖流出", render: (item) => formatNumber(item.total_out) },
{ key: "manual_credit_total", label: "人工添加", render: (item) => formatNumber(item.manual_credit_total) },
{ key: "manual_debit_total", label: "人工扣减", render: (item) => formatNumber(item.manual_debit_total) },
{
key: "updated_at_ms",
label: "更新时间",
render: (item) => (item.materialized === false ? "-" : ),
width: "minmax(150px, 1fr)",
},
];
if (canCredit || canDebit) {
columns.push({
fixed: "right",
key: "actions",
label: "操作",
render: (item) => (
),
width: canCredit && canDebit ? "220px" : "120px",
});
}
return columns;
}
const appSummaryColumns = [
{ key: "app_code", label: "应用", width: "minmax(96px, 0.8fr)" },
{ key: "total_draws", label: "抽奖次数", render: (item) => formatNumber(item.total_draws) },
{ key: "unique_users", label: "参与用户", render: (item) => formatNumber(item.unique_users) },
{ key: "unique_rooms", label: "参与房间", render: (item) => formatNumber(item.unique_rooms) },
{ key: "total_spent_coins", label: "消耗金币", render: (item) => formatNumber(item.total_spent_coins) },
{ key: "total_reward_coins", label: "返还金币", render: (item) => formatNumber(item.total_reward_coins) },
{
key: "actual_rtp_ppm",
label: "实际 RTP",
render: (item) => (item.total_draws > 0 ? formatPercentFromPPM(item.actual_rtp_ppm) : "-"),
},
{
key: "exceptions",
label: "发放异常",
// 待发放和失败合并成一列告警:正常应为 0,非 0 说明发奖链路有积压或失败,需要跟进抽奖记录。
render: (item) => {
const pending = Number(item.pending_draws || 0);
const failed = Number(item.failed_draws || 0);
if (!pending && !failed) {
return ;
}
return (
);
},
width: "minmax(170px, 1.2fr)",
},
];
function countBySource(apps = [], source) {
return apps.filter((item) => (item.source || "registry") === source).length;
}