Files
lotteryAdmin/src/modules/settlement/settlement-periods-table.tsx
kang 0d984b00f0
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
feat(admin): 仪表盘重构、代理线路修复与后台体验优化
- 各角色仪表盘:KPI 可点击跳转、快捷入口、去重冗余区块,代理/站点降级 analytics
- 代理线路:创建玩家权限、侧栏搜索、深链 URL、档案保存与删除预检等修复
- 统一密码最短 6 位;代理模块表格/侧栏背景色一致(admin-table-inset)
- 报表、钱包、对账、开奖、结算、配置等模块结构与 i18n 同步更新
2026-06-26 14:39:22 +08:00

176 lines
7.2 KiB
TypeScript

"use client";
import { Eye, Lock } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { SettlementPeriodRow } from "@/api/admin-agent-settlement";
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
import { cn } from "@/lib/utils";
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import { signedSettlementMoneyClass } from "@/modules/settlement/settlement-signed-money";
import { settlementPeriodStatusLabel } from "@/modules/settlement/settlement-status-label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const COL_SPAN = 9;
type SettlementPeriodsTableProps = {
periods: SettlementPeriodRow[];
loading?: boolean;
canManage: boolean;
busy?: boolean;
currencyCode?: string;
emptyMessage?: string;
onViewDetail: (periodId: number) => void;
onRequestClose: (row: SettlementPeriodRow) => void;
};
export function SettlementPeriodsTable({
periods,
loading = false,
canManage,
busy = false,
currencyCode = "NPR",
emptyMessage,
onViewDetail,
onRequestClose,
}: SettlementPeriodsTableProps): React.ReactElement {
const { t } = useTranslation(["settlementCenter", "agents", "common"]);
const billCount = (row: SettlementPeriodRow): number =>
(row.summary?.player_bills ?? 0)
+ (row.summary?.agent_bills ?? 0)
+ (row.summary?.adjustment_bills ?? 0);
const winLossScope = periods.find((row) => row.pipeline?.win_loss_scope)?.pipeline?.win_loss_scope
?? "platform";
const winLossLabel =
winLossScope === "agent"
? t("periodTable.agentWinLoss", { defaultValue: "代理输赢" })
: t("periodTable.platformWinLoss", { defaultValue: "平台输赢" });
return (
<div className="admin-table-shell overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("periodTable.range", { defaultValue: "账期" })}</TableHead>
<TableHead>{t("columns.status", { defaultValue: "状态" })}</TableHead>
<TableHead className="text-right">
{t("periodTable.shareLedger", { defaultValue: "占成流水" })}
</TableHead>
<TableHead className="text-right">{winLossLabel}</TableHead>
<TableHead className="text-right">
{t("agents:settlementReports.summary.billCount", { defaultValue: "账单数" })}
</TableHead>
<TableHead className="text-right">
{t("periodTable.pending", { defaultValue: "待确认" })}
</TableHead>
<TableHead className="text-right">
{t("periodTable.awaiting", { defaultValue: "待收付" })}
</TableHead>
<TableHead className="text-right">
{t("agents:settlementReports.summary.totalUnpaid", { defaultValue: "未结合计" })}
</TableHead>
<TableHead className="sticky right-0 z-10 w-36 bg-muted text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
{t("common:table.actions", { defaultValue: "操作" })}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<AdminTableLoadingRow colSpan={COL_SPAN} />
) : periods.length === 0 ? (
<AdminTableNoResourceRow colSpan={COL_SPAN} message={emptyMessage} />
) : (
periods.map((row) => {
const canCloseRow = canManage && row.status === "open";
const todoCount =
(row.summary?.pending_confirm ?? 0) + (row.summary?.awaiting_payment ?? 0);
const detailLabel =
todoCount > 0
? t("periodTable.processBills", { defaultValue: "处理账单" })
: t("periodTable.viewDetail", { defaultValue: "查看详情" });
return (
<TableRow key={row.id}>
<TableCell className="font-medium whitespace-nowrap">
{formatSettlementPeriodSpan(row.period_start, row.period_end)}
</TableCell>
<TableCell>
<AdminStatusBadge status={row.status}>
{settlementPeriodStatusLabel(row.status, t)}
</AdminStatusBadge>
</TableCell>
<TableCell className="text-right tabular-nums">
{row.pipeline?.share_ledger_count ?? "—"}
</TableCell>
<TableCell
className={cn(
"text-right tabular-nums whitespace-nowrap",
row.pipeline?.game_win_loss_total != null
? signedSettlementMoneyClass(row.pipeline.game_win_loss_total, true)
: undefined,
)}
>
{row.pipeline?.game_win_loss_total != null
? formatDashboardMoneyMinor(row.pipeline.game_win_loss_total, currencyCode)
: "—"}
</TableCell>
<TableCell className="text-right tabular-nums">
{billCount(row) > 0 ? billCount(row) : "—"}
</TableCell>
<TableCell className="text-right tabular-nums">
{row.summary?.pending_confirm ?? "—"}
</TableCell>
<TableCell className="text-right tabular-nums">
{row.summary?.awaiting_payment ?? "—"}
</TableCell>
<TableCell className="text-right tabular-nums whitespace-nowrap">
{(row.summary?.total_unpaid ?? 0) > 0
? formatDashboardMoneyMinor(row.summary?.total_unpaid ?? 0, currencyCode)
: "—"}
</TableCell>
<TableCell
className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]"
onClick={(e) => e.stopPropagation()}
>
<AdminRowActionsMenu
busy={busy}
actions={[
{
key: "detail",
label: detailLabel,
icon: Eye,
onClick: () => onViewDetail(row.id),
},
{
key: "close",
label: t("periodTable.close", { defaultValue: "关账" }),
icon: Lock,
hidden: !canCloseRow,
onClick: () => onRequestClose(row),
},
]}
/>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
);
}