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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user