feat(agents, config, dashboard, i18n): add agent line provision wizard, site deletion, and site dashboard with multi-language support

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
This commit is contained in:
2026-06-12 20:47:53 +08:00
parent 24fd7c10bd
commit 6ea0a6feec
48 changed files with 1573 additions and 629 deletions

View File

@@ -3,7 +3,9 @@
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,
@@ -165,15 +167,19 @@ export function AgentSettlementReportView({
</p>
);
}
const stats = [
{ label: t("settlementReports.platformPnl.billNet", { defaultValue: "平台账单净额" }), value: money(root.platform_bill_net, currencyCode) },
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: "尾差调整" }),
value: money(root.platform_rounding_adjustment, currencyCode),
amount: Number(root.platform_rounding_adjustment ?? 0),
},
{
label: t("settlementReports.platformPnl.shareProfit", { defaultValue: "占成利润(元数据)" }),
value: money(root.share_profit_meta, currencyCode),
amount: Number(root.share_profit_meta ?? 0),
signed: true,
},
];
return (
@@ -181,7 +187,15 @@ export function AgentSettlementReportView({
{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 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>
@@ -190,16 +204,16 @@ export function AgentSettlementReportView({
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 }[]> = {
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 },
{ 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 },
{ 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: "笔数" }) },
],
@@ -216,7 +230,7 @@ export function AgentSettlementReportView({
],
draw_period: [
{ key: "draw_no", header: t("settlementReports.columns.drawNo", { defaultValue: "期号" }) },
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true },
{ 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: "笔数" }) },
],
@@ -238,7 +252,7 @@ function ReportTable({
currencyCode,
}: {
rows: Record<string, unknown>[];
columns: { key: string; header: string; money?: boolean; creditMajor?: boolean }[];
columns: { key: string; header: string; money?: boolean; signed?: boolean; creditMajor?: boolean }[];
currencyCode: string;
}): React.ReactElement {
const { t } = useTranslation("common");
@@ -272,9 +286,15 @@ function ReportTable({
>
{col.creditMajor
? creditMoney(row[col.key], currencyCode)
: col.money
? money(row[col.key], currencyCode)
: String(row[col.key] ?? "—")}
: 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>

View File

@@ -47,6 +47,7 @@ import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { formatAdminMinorUnits } from "@/lib/money";
import { signedMoneyClass } from "@/lib/admin-signed-money";
import { cn } from "@/lib/utils";
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
@@ -273,12 +274,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
</p>
<p>
<span className="text-muted-foreground">{t("platformProfit")}</span>{" "}
<span
className={cn(
"font-mono tabular-nums",
summary.platform_profit < 0 ? "text-destructive" : "text-emerald-700",
)}
>
<span className={cn("font-mono tabular-nums", signedMoneyClass(summary.platform_profit, true))}>
{formatAdminMinorUnits(summary.platform_profit, summary.currency_code ?? "NPR")}
</span>
</p>

View File

@@ -51,6 +51,7 @@ import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/c
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { formatAdminMinorUnits } from "@/lib/money";
import { signedMoneyClass } from "@/lib/admin-signed-money";
import { cn } from "@/lib/utils";
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
@@ -270,7 +271,7 @@ export function SettlementBatchesConsole() {
<TableCell
className={cn(
"text-center font-mono text-xs tabular-nums",
row.platform_profit < 0 ? "text-destructive" : "text-emerald-700",
signedMoneyClass(row.platform_profit, true),
)}
>
{formatAdminMinorUnits(row.platform_profit, row.currency_code ?? "NPR")}

View File

@@ -8,9 +8,12 @@ import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { signedMoneyClass } from "@/lib/admin-signed-money";
import { cn } from "@/lib/utils";
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import {
formatSignedSettlementMoney,
} from "@/modules/settlement/settlement-signed-money";
import {
describeBillPaymentDirection,
} from "@/modules/settlement/settlement-bill-display";
@@ -76,26 +79,6 @@ function billTypeTone(row: SettlementBillRow): string {
return "border-border/70 bg-muted/25 text-muted-foreground";
}
function signedMoneyClass(amount: number, emphasize = false): string {
if (amount < 0) {
return cn("text-destructive", emphasize && "font-medium");
}
if (amount > 0) {
return cn("text-emerald-700", emphasize && "font-medium");
}
return "text-muted-foreground";
}
function formatSignedMoney(amount: number, currencyCode: string): string {
if (amount === 0) {
return formatDashboardMoneyMinor(0, currencyCode);
}
const prefix = amount < 0 ? "" : "+";
return `${prefix}${formatDashboardMoneyMinor(Math.abs(amount), currencyCode)}`;
}
function unpaidMoneyClass(row: SettlementBillRow): string {
if (row.unpaid_amount <= 0) {
return "text-muted-foreground";
@@ -253,7 +236,7 @@ export function SettlementBillsTable({
)}
>
{row.gross_win_loss != null ? (
<div>{formatSignedMoney(row.gross_win_loss, currencyCode)}</div>
<div>{formatSignedSettlementMoney(row.gross_win_loss, currencyCode)}</div>
) : (
"—"
)}

View File

@@ -10,6 +10,7 @@ import { toast } from "sonner";
import { getSettlementPeriods, type SettlementPeriodRow } from "@/api/admin-agent-settlement";
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { AdminNoIntegrationSiteState } from "@/components/admin/admin-no-integration-site-state";
import { AgentBillDetail } from "@/modules/settlement/agent-bill-detail";
import { SettlementCenterPeriodDetail } from "@/modules/settlement/settlement-center-period-detail";
import {
@@ -284,7 +285,9 @@ export function SettlementCenterShell(): React.ReactElement {
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
{siteId === null ? (
{siteId === null && siteOptions.length === 0 && boundAgent === null ? (
<AdminNoIntegrationSiteState canCreate={profile?.is_super_admin === true} />
) : siteId === null ? (
<p className="text-sm text-muted-foreground">{t("empty.noSite", { defaultValue: "请选择站点。" })}</p>
) : !periodsReady ? (
<AdminLoadingState />

View File

@@ -1,17 +1,6 @@
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import { cn } from "@/lib/utils";
/** 结算金额正负着色:负红、正绿、零灰 */
export function signedSettlementMoneyClass(amount: number, emphasize = false): string {
if (amount < 0) {
return cn("text-destructive", emphasize && "font-medium");
}
if (amount > 0) {
return cn("text-emerald-700 dark:text-emerald-400", emphasize && "font-medium");
}
return "text-muted-foreground";
}
export { signedMoneyClass as signedSettlementMoneyClass } from "@/lib/admin-signed-money";
export function formatSignedSettlementMoney(amount: number, currencyCode: string): string {
if (amount === 0) {