Added agent line provision wizard page with permission gating, replacing redirect placeholder. Introduced site deletion API and UI with confirmation dialog in integration sites management. Added new site-scoped dashboard panel showing bet metrics, P/L trends, active players, and quick links. Enhanced chart tooltip to support custom formatters and fix indicator color
307 lines
13 KiB
TypeScript
307 lines
13 KiB
TypeScript
"use client";
|
||
|
||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import { SignedMoney } from "@/lib/admin-signed-money";
|
||
import { formatDashboardCreditMajor, formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||
import { formatSignedSettlementMoney } from "@/modules/settlement/settlement-signed-money";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/components/ui/table";
|
||
import type { AgentSettlementReportType } from "@/api/admin-agent-settlement";
|
||
|
||
type AgentSettlementReportViewProps = {
|
||
reportType: AgentSettlementReportType;
|
||
data: unknown;
|
||
currencyCode: string;
|
||
};
|
||
|
||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: null;
|
||
}
|
||
|
||
function asRows(value: unknown): Record<string, unknown>[] {
|
||
if (!Array.isArray(value)) {
|
||
return [];
|
||
}
|
||
return value.filter((row): row is Record<string, unknown> => row !== null && typeof row === "object");
|
||
}
|
||
|
||
function money(
|
||
value: unknown,
|
||
currencyCode: string,
|
||
): string {
|
||
return formatDashboardMoneyMinor(Number(value ?? 0), currencyCode);
|
||
}
|
||
|
||
function creditMoney(value: unknown, currencyCode: string): string {
|
||
return formatDashboardCreditMajor(Number(value ?? 0), currencyCode);
|
||
}
|
||
|
||
export function AgentSettlementReportView({
|
||
reportType,
|
||
data,
|
||
currencyCode,
|
||
}: AgentSettlementReportViewProps): React.ReactElement {
|
||
const { t } = useTranslation(["agents", "common"]);
|
||
const root = asRecord(data);
|
||
|
||
if (reportType === "summary" && root) {
|
||
const stats = [
|
||
{ label: t("settlementReports.summary.billCount", { defaultValue: "账单数" }), value: String(root.bill_count ?? 0) },
|
||
{ label: t("settlementReports.summary.totalNet", { defaultValue: "净额合计" }), value: money(root.total_net, currencyCode) },
|
||
{ label: t("settlementReports.summary.totalUnpaid", { defaultValue: "未结合计" }), value: money(root.total_unpaid, currencyCode) },
|
||
{ label: t("settlementReports.summary.overdueCount", { defaultValue: "逾期账单" }), value: String(root.overdue_count ?? 0) },
|
||
{
|
||
label: t("settlementReports.summary.platformRounding", { defaultValue: "平台尾差合计" }),
|
||
value: money(root.platform_rounding_total, currencyCode),
|
||
},
|
||
];
|
||
return (
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{stats.map((item) => (
|
||
<div key={item.label} className="rounded-md border border-border/60 px-3 py-2">
|
||
<div className="text-xs text-muted-foreground">{item.label}</div>
|
||
<div className="mt-1 text-sm font-semibold tabular-nums">{item.value}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (reportType === "rebate" && root) {
|
||
const byType = asRows(root.by_type);
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||
{[
|
||
["accrued_total", t("settlementReports.rebate.accrued", { defaultValue: "应计" })],
|
||
["in_bill_total", t("settlementReports.rebate.inBill", { defaultValue: "已入账单" })],
|
||
["settled_total", t("settlementReports.rebate.settled", { defaultValue: "已结算" })],
|
||
["allocated_total", t("settlementReports.rebate.allocated", { defaultValue: "已分摊" })],
|
||
].map(([key, label]) => (
|
||
<div key={key} className="rounded-md border border-border/60 px-3 py-2">
|
||
<div className="text-xs text-muted-foreground">{label}</div>
|
||
<div className="mt-1 text-sm font-semibold tabular-nums">{money(root[key], currencyCode)}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{byType.length > 0 ? (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>{t("settlementReports.columns.rebateType", { defaultValue: "类型" })}</TableHead>
|
||
<TableHead>{t("settlementReports.columns.status", { defaultValue: "状态" })}</TableHead>
|
||
<TableHead className="text-right">{t("settlementReports.columns.amount", { defaultValue: "金额" })}</TableHead>
|
||
<TableHead className="text-right">{t("settlementReports.columns.count", { defaultValue: "笔数" })}</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{byType.map((row, idx) => (
|
||
<TableRow key={`${row.rebate_type}-${row.status}-${idx}`}>
|
||
<TableCell>{String(row.rebate_type ?? "")}</TableCell>
|
||
<TableCell>{String(row.status ?? "")}</TableCell>
|
||
<TableCell className="text-right tabular-nums">{money(row.total, currencyCode)}</TableCell>
|
||
<TableCell className="text-right tabular-nums">{String(row.count ?? 0)}</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (reportType === "credit" && root) {
|
||
const agents = asRows(root.agents);
|
||
const players = asRows(root.players);
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<p className="mb-2 text-sm font-medium">{t("settlementReports.credit.agents", { defaultValue: "代理授信" })}</p>
|
||
<ReportTable
|
||
rows={agents}
|
||
columns={[
|
||
{ key: "code", header: t("settlementReports.columns.code", { defaultValue: "编码" }) },
|
||
{ key: "name", header: t("settlementReports.columns.name", { defaultValue: "名称" }) },
|
||
{ key: "credit_limit", header: t("settlementReports.columns.creditLimit", { defaultValue: "授信" }), creditMajor: true },
|
||
{ key: "allocated_credit", header: t("settlementReports.columns.allocated", { defaultValue: "已下发" }), creditMajor: true },
|
||
{ key: "available_credit", header: t("settlementReports.columns.available", { defaultValue: "可用" }), creditMajor: true },
|
||
]}
|
||
currencyCode={currencyCode}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<p className="mb-2 text-sm font-medium">{t("settlementReports.credit.players", { defaultValue: "玩家授信" })}</p>
|
||
<ReportTable
|
||
rows={players}
|
||
columns={[
|
||
{ key: "username", header: t("settlementReports.columns.player", { defaultValue: "玩家" }) },
|
||
{ key: "credit_limit", header: t("settlementReports.columns.creditLimit", { defaultValue: "授信" }), creditMajor: true },
|
||
{ key: "used_credit", header: t("settlementReports.columns.used", { defaultValue: "已用" }), creditMajor: true },
|
||
{ key: "frozen_credit", header: t("settlementReports.columns.frozen", { defaultValue: "冻结" }), creditMajor: true },
|
||
{ key: "available_credit", header: t("settlementReports.columns.available", { defaultValue: "可用" }), creditMajor: true },
|
||
]}
|
||
currencyCode={currencyCode}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (reportType === "platform_pnl" && root) {
|
||
if (root.error) {
|
||
return (
|
||
<p className="text-sm text-amber-800">
|
||
{t("settlementReports.platformPnl.periodRequired", {
|
||
defaultValue: "请选择具体账期后查看平台盈亏(需 settlement_period_id)。",
|
||
})}
|
||
</p>
|
||
);
|
||
}
|
||
const stats: { label: string; amount: number; signed?: boolean }[] = [
|
||
{
|
||
label: t("settlementReports.platformPnl.billNet", { defaultValue: "平台账单净额" }),
|
||
amount: Number(root.platform_bill_net ?? 0),
|
||
},
|
||
{
|
||
label: t("settlementReports.platformPnl.rounding", { defaultValue: "尾差调整" }),
|
||
amount: Number(root.platform_rounding_adjustment ?? 0),
|
||
},
|
||
{
|
||
label: t("settlementReports.platformPnl.shareProfit", { defaultValue: "占成利润(元数据)" }),
|
||
amount: Number(root.share_profit_meta ?? 0),
|
||
signed: true,
|
||
},
|
||
];
|
||
return (
|
||
<div className="grid gap-3 sm:grid-cols-3">
|
||
{stats.map((item) => (
|
||
<div key={item.label} className="rounded-md border border-border/60 px-3 py-2">
|
||
<div className="text-xs text-muted-foreground">{item.label}</div>
|
||
<div className="mt-1 text-sm font-semibold tabular-nums">
|
||
{item.signed ? (
|
||
<SignedMoney amount={item.amount} emphasize>
|
||
{formatSignedSettlementMoney(item.amount, currencyCode)}
|
||
</SignedMoney>
|
||
) : (
|
||
money(item.amount, currencyCode)
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const items = asRows(root?.items ?? (reportType === "player_win_loss" || reportType === "agent_share" || reportType === "unpaid_bills" || reportType === "overdue" || reportType === "draw_period" ? data : null));
|
||
|
||
const columnSets: Record<string, { key: string; header: string; money?: boolean; signed?: boolean }[]> = {
|
||
player_win_loss: [
|
||
{ key: "username", header: t("settlementReports.columns.player", { defaultValue: "玩家" }) },
|
||
{ key: "game_type", header: t("settlementReports.columns.gameType", { defaultValue: "玩法" }) },
|
||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true, signed: true },
|
||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||
],
|
||
agent_share: [
|
||
{ key: "agent_node_id", header: t("settlementReports.columns.agentId", { defaultValue: "代理 ID" }) },
|
||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true, signed: true },
|
||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||
{ key: "entry_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
||
],
|
||
unpaid_bills: [
|
||
{ key: "bill_id", header: t("settlementReports.columns.billId", { defaultValue: "账单" }) },
|
||
{ key: "bill_type", header: t("settlementReports.columns.billType", { defaultValue: "类型" }) },
|
||
{ key: "unpaid_amount", header: t("settlementReports.columns.unpaid", { defaultValue: "未结" }), money: true },
|
||
{ key: "status", header: t("settlementReports.columns.status", { defaultValue: "状态" }) },
|
||
],
|
||
overdue: [
|
||
{ key: "bill_id", header: t("settlementReports.columns.billId", { defaultValue: "账单" }) },
|
||
{ key: "overdue_days", header: t("settlementReports.columns.overdueDays", { defaultValue: "逾期天数" }) },
|
||
{ key: "unpaid_amount", header: t("settlementReports.columns.unpaid", { defaultValue: "未结" }), money: true },
|
||
],
|
||
draw_period: [
|
||
{ key: "draw_no", header: t("settlementReports.columns.drawNo", { defaultValue: "期号" }) },
|
||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true, signed: true },
|
||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||
{ key: "ticket_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
||
],
|
||
};
|
||
|
||
const columns = columnSets[reportType];
|
||
if (!columns) {
|
||
return (
|
||
<AdminNoResourceState className="text-sm text-muted-foreground" />
|
||
);
|
||
}
|
||
|
||
return <ReportTable rows={items} columns={columns} currencyCode={currencyCode} />;
|
||
}
|
||
|
||
function ReportTable({
|
||
rows,
|
||
columns,
|
||
currencyCode,
|
||
}: {
|
||
rows: Record<string, unknown>[];
|
||
columns: { key: string; header: string; money?: boolean; signed?: boolean; creditMajor?: boolean }[];
|
||
currencyCode: string;
|
||
}): React.ReactElement {
|
||
const { t } = useTranslation("common");
|
||
|
||
if (rows.length === 0) {
|
||
return <AdminNoResourceState className="text-sm text-muted-foreground" />;
|
||
}
|
||
|
||
return (
|
||
<div className="admin-table-shell max-h-96 overflow-auto">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
{columns.map((col) => (
|
||
<TableHead
|
||
key={col.key}
|
||
className={col.money || col.creditMajor ? "text-right" : undefined}
|
||
>
|
||
{col.header}
|
||
</TableHead>
|
||
))}
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{rows.map((row, idx) => (
|
||
<TableRow key={idx}>
|
||
{columns.map((col) => (
|
||
<TableCell
|
||
key={col.key}
|
||
className={col.money || col.creditMajor ? "text-right tabular-nums" : undefined}
|
||
>
|
||
{col.creditMajor
|
||
? creditMoney(row[col.key], currencyCode)
|
||
: col.money && col.signed
|
||
? (
|
||
<SignedMoney amount={Number(row[col.key] ?? 0)} emphasize>
|
||
{formatSignedSettlementMoney(Number(row[col.key] ?? 0), currencyCode)}
|
||
</SignedMoney>
|
||
)
|
||
: col.money
|
||
? money(row[col.key], currencyCode)
|
||
: String(row[col.key] ?? "—")}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
);
|
||
}
|