diff --git a/AGENTS.md b/AGENTS.md index 8bd0e39..0af1776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,3 +3,28 @@ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + +## 玩家双模式(钱包 / 信用) + +后端字段(`players` 表): + +| 字段 | 值 | 含义 | +|------|-----|------| +| `funding_mode` | `wallet` / `credit` | 主站钱包 vs 信用盘 | +| `auth_source` | `main_site_sso` / `lottery_native` | 主站 SSO vs 彩票端账号 | + +**后台统一用法(不要各页手写 `funding_mode === "credit"`):** + +- `isCreditFundingPlayer(row)` / `playerUsesCredit(row)` — 是否信用盘 +- `playerShowsTransferOrders(row)` — 是否展示主站转账单 +- `playerFundingModeLabel` / `playerAuthSourceLabel` — 文案 +- `playerBalanceCells` — 列表余额列(信用用 major 额度,钱包用 minor) +- `` / `` — 表格徽章 + +**模块边界:** + +- `/admin/wallet/*`:仅**主站钱包**流水与转账单 +- `/admin/settlement-center` → **账务流水**单页(筛选 + 分类 + 表格 + ⋯ 操作),合并信用/收付/调账/坏账;**账单**单页用分类筛选待确认/待收付等 +- 玩家详情:按 `funding_mode` 切换 Tab(信用流水 / 钱包流水;信用盘隐藏转账单) + +新增涉及玩家资金的页面时,先读 `src/lib/admin-player-display.ts`。 diff --git a/public/notdata.png b/public/notdata.png new file mode 100644 index 0000000..bebd7ac Binary files /dev/null and b/public/notdata.png differ diff --git a/src/api/admin-agent-settlement.ts b/src/api/admin-agent-settlement.ts new file mode 100644 index 0000000..150c30a --- /dev/null +++ b/src/api/admin-agent-settlement.ts @@ -0,0 +1,287 @@ +import { adminRequest } from "@/lib/admin-http"; + +const A = `/admin`; + +export type SettlementPeriodSummary = { + player_bills: number; + agent_bills: number; + adjustment_bills: number; + pending_confirm: number; + awaiting_payment: number; + settled: number; + total_unpaid: number; +}; + +export type SettlementPeriodPipeline = { + credit_ledger_count: number; + share_ledger_count: number; +}; + +export type SettlementPeriodRow = { + id: number; + admin_site_id: number; + period_start: string; + period_end: string; + status: string; + summary?: SettlementPeriodSummary; + pipeline?: SettlementPeriodPipeline; +}; + +export type AgentSettlementReportType = + | "summary" + | "player_win_loss" + | "agent_share" + | "rebate" + | "credit" + | "unpaid_bills" + | "overdue" + | "platform_pnl" + | "draw_period"; + +export type SettlementBillRow = { + id: number; + settlement_period_id: number; + bill_type: string; + owner_type: string; + owner_id: number; + counterparty_type: string; + counterparty_id: number; + gross_win_loss?: number; + rebate_amount?: number; + platform_rounding_adjustment?: number; + net_amount: number; + unpaid_amount: number; + paid_amount: number; + status: string; + owner_label?: string; + counterparty_label?: string; + owner_funding_mode?: string | null; + owner_auth_source?: string | null; + period_start?: string; + period_end?: string; + admin_site_id?: number; + meta_json?: string | Record | null; +}; + +export async function getSettlementPeriods(params?: { + admin_site_id?: number; +}): Promise<{ items: SettlementPeriodRow[] }> { + return adminRequest.get(`${A}/settlement-periods`, { params }); +} + +export async function postSettlementPeriod(body: { + admin_site_id: number; + period_start: string; + period_end: string; +}): Promise { + return adminRequest.post(`${A}/settlement-periods`, body); +} + +export type SettlementPeriodCloseResult = { + period_id: number; + unsettled_ticket_count?: number; + player_count?: number; +}; + +export async function postSettlementPeriodClose( + periodId: number, +): Promise { + return adminRequest.post(`${A}/settlement-periods/${periodId}/close`); +} + +export async function postSettlementBillBadDebtWriteOff( + billId: number, + body?: { reason?: string }, +): Promise<{ original_bill_id: number; bad_debt_bill_id: number; bill: SettlementBillRow }> { + return adminRequest.post(`${A}/settlement-bills/${billId}/bad-debt-write-off`, body ?? {}); +} + +export type SettlementBillListScope = + | "pending_confirm" + | "awaiting_payment" + | "settled" + | "adjustment"; + +export async function getSettlementBills(params?: { + settlement_period_id?: number; + admin_site_id?: number; + bill_type?: string; + scope?: SettlementBillListScope; +}): Promise<{ items: SettlementBillRow[] }> { + return adminRequest.get(`${A}/settlement-bills`, { params }); +} + +export type SettlementPaymentRow = { + id: number; + settlement_bill_id: number; + payer_type: string; + payer_id: number; + payee_type: string; + payee_id: number; + amount: number; + method: string | null; + proof?: string | null; + remark?: string | null; + status: string; + bill_type: string; + owner_type: string; + owner_id: number; + period_start?: string; + period_end?: string; + confirmed_at?: string | null; + created_at?: string; +}; + +export async function getSettlementPayments(params?: { + settlement_period_id?: number; + admin_site_id?: number; +}): Promise<{ items: SettlementPaymentRow[] }> { + return adminRequest.get(`${A}/settlement-payments`, { params }); +} + +export type SettlementAdjustmentRow = { + id: number; + settlement_period_id: number | null; + original_bill_id: number | null; + adjustment_type: string; + amount: number; + reason: string | null; + created_by: number | null; + period_start?: string; + period_end?: string; + original_bill_type?: string | null; + original_owner_type?: string | null; + original_owner_id?: number | null; + created_at?: string; +}; + +export async function getSettlementAdjustments(params?: { + settlement_period_id?: number; + admin_site_id?: number; + adjustment_type?: string; +}): Promise<{ items: SettlementAdjustmentRow[] }> { + return adminRequest.get(`${A}/settlement-adjustments`, { params }); +} + +export type SettlementLedgerRow = { + entry_kind: "credit" | "payment" | "adjustment"; + id: number; + row_key?: string; + txn_no: string; + player_id: number; + site_code?: string; + site_player_id?: string | null; + username?: string | null; + nickname?: string | null; + biz_type: string; + type?: string; + biz_no?: string | null; + direction: number; + amount: number; + amount_formatted?: string; + signed_amount?: number; + currency_code?: string; + status: string; + created_at?: string | null; + ledger_source: string; + funding_mode?: string; + auth_source?: string | null; + settlement_bill_id?: number | null; + bill_status?: string | null; + bill_type?: string | null; + bill_unpaid_amount?: number | null; + available_actions?: string[]; +}; + +/** @deprecated Use {@link SettlementLedgerRow} */ +export type CreditLedgerRow = SettlementLedgerRow; + +export async function getCreditLedger(params?: { + admin_site_id?: number; + settlement_period_id?: number; + player_id?: number; + player_account?: string; + txn_no?: string; + reason?: string; + biz_type?: string; + entry_kind?: string; + bill_status?: string; + actionable_only?: boolean; + bad_debt_only?: boolean; + created_from?: string; + created_to?: string; + page?: number; + per_page?: number; +}): Promise<{ + items: SettlementLedgerRow[]; + total: number; + page: number; + per_page: number; + ledger_source: string; +}> { + return adminRequest.get(`${A}/credit-ledger`, { params }); +} + +export type RebateAllocationRow = { + id: number; + rebate_record_id: number; + participant_type: string; + participant_id: number; + actual_share_rate: number; + allocated_amount: number; + allocation_rule: string; +}; + +export type SettlementPaymentRow = { + id: number; + amount: number; + status: string; + method?: string | null; + proof?: string | null; + remark?: string | null; +}; + +export async function getSettlementBill(billId: number): Promise<{ + bill: SettlementBillRow; + payments: SettlementPaymentRow[]; + rebate_allocations: RebateAllocationRow[]; + adjustments: Array<{ id: number; amount: number; adjustment_type: string; reason: string | null }>; + tier_edge?: string | null; +}> { + return adminRequest.get(`${A}/settlement-bills/${billId}`); +} + +export async function postSettlementBillConfirm(billId: number): Promise<{ bill_id: number; status: string }> { + return adminRequest.post(`${A}/settlement-bills/${billId}/confirm`); +} + +export async function postSettlementBillPayment( + billId: number, + body: { amount: number; method?: string; proof?: string; remark?: string }, +): Promise<{ bill: SettlementBillRow }> { + return adminRequest.post(`${A}/settlement-bills/${billId}/payments`, body); +} + +export type AgentSettlementReportResponse = { + type: string; + settlement_period_id: number | null; + period_start: string; + period_end: string; + data: unknown; + footnote: string | null; +}; + +export async function getAgentSettlementReport(params: { + type: AgentSettlementReportType; + settlement_period_id?: number; + admin_site_id?: number; +}): Promise { + return adminRequest.get(`${A}/settlement-reports`, { params }); +} + +export async function postSettlementBillAdjustment( + billId: number, + body: { amount: number; adjustment_type?: "adjustment" | "reversal"; reason?: string }, +): Promise<{ adjustment_bill_id: number; bill: SettlementBillRow }> { + return adminRequest.post(`${A}/settlement-bills/${billId}/adjustments`, body); +} diff --git a/src/api/admin-integration-sites.ts b/src/api/admin-integration-sites.ts index b533767..59239da 100644 --- a/src/api/admin-integration-sites.ts +++ b/src/api/admin-integration-sites.ts @@ -8,6 +8,7 @@ import type { AdminIntegrationSiteListData, AdminIntegrationSiteParameterSheet, AdminIntegrationSiteUpdatePayload, + AdminIntegrationSiteSecrets, AdminIntegrationSiteWithSecrets, } from "@/types/api/admin-integration-site"; @@ -63,3 +64,9 @@ export async function getAdminIntegrationSiteExport( ); } +export async function getAdminIntegrationSiteSecrets( + id: number, +): Promise { + return adminRequest.get(`${A}/integration-sites/${id}/secrets`); +} + diff --git a/src/api/admin-users.ts b/src/api/admin-users.ts index cb04191..072a9d6 100644 --- a/src/api/admin-users.ts +++ b/src/api/admin-users.ts @@ -13,6 +13,7 @@ import type { AdminUserPermissionListData, AdminUserPermissionRow, AdminUserRoleSyncData, + AdminUserRoleSyncPayload, AdminUserUpdatePayload, } from "@/types/api/admin-user"; @@ -81,9 +82,7 @@ export async function putAdminRolePermissions( export async function putAdminUserRoles( adminUserId: number, - roleSlugs: string[], + body: AdminUserRoleSyncPayload, ): Promise { - return adminRequest.put(`${A}/admin-users/${adminUserId}/roles`, { - role_slugs: roleSlugs, - }); + return adminRequest.put(`${A}/admin-users/${adminUserId}/roles`, body); } diff --git a/src/app/admin/(shell)/agents/layout.tsx b/src/app/admin/(shell)/agents/layout.tsx index 69b3b5c..a8f835e 100644 --- a/src/app/admin/(shell)/agents/layout.tsx +++ b/src/app/admin/(shell)/agents/layout.tsx @@ -4,10 +4,8 @@ import { AgentsSubnav } from "@/modules/agents/agents-subnav"; export default function AdminAgentsLayout({ children }: { children: ReactNode }) { return ( -
-
- -
+
+ {children}
); diff --git a/src/app/admin/(shell)/agents/provision/page.tsx b/src/app/admin/(shell)/agents/provision/page.tsx index 228211d..e9087f8 100644 --- a/src/app/admin/(shell)/agents/provision/page.tsx +++ b/src/app/admin/(shell)/agents/provision/page.tsx @@ -10,7 +10,10 @@ export const metadata: Metadata = buildPageMetadata("agents", "lineProvision.tit export default function AgentLineProvisionPage(): React.ReactElement { return ( - + diff --git a/src/app/admin/(shell)/agents/settlement-bills/page.tsx b/src/app/admin/(shell)/agents/settlement-bills/page.tsx index c6a95b2..667da63 100644 --- a/src/app/admin/(shell)/agents/settlement-bills/page.tsx +++ b/src/app/admin/(shell)/agents/settlement-bills/page.tsx @@ -1,18 +1,5 @@ -import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { AgentBillsConsole } from "@/modules/settlement/agent-bills-console"; -import { PRD_SETTLEMENT_AGENT_ACCESS_ANY } from "@/lib/admin-prd"; -import { buildPageMetadata } from "@/lib/page-metadata"; -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -export const metadata: Metadata = buildPageMetadata("agents", "subnav.settlementBills"); - -export default function AgentSettlementBillsPage(): React.ReactElement { - return ( - - - - - - ); +export default function AgentSettlementBillsPage(): never { + redirect("/admin/settlement-center"); } diff --git a/src/app/admin/(shell)/agents/sites/page.tsx b/src/app/admin/(shell)/agents/sites/page.tsx index 741a001..f33a190 100644 --- a/src/app/admin/(shell)/agents/sites/page.tsx +++ b/src/app/admin/(shell)/agents/sites/page.tsx @@ -1,15 +1,6 @@ -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { IntegrationSitesConsole } from "@/modules/integration/integration-sites-console"; -import { PRD_AGENT_SITES_ACCESS_ANY } from "@/lib/admin-prd"; -import { buildPageMetadata } from "@/lib/page-metadata"; -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -export const metadata: Metadata = buildPageMetadata("agents", "sitesTitle"); - -export default function AgentLineSitesPage() { - return ( - - - - ); +/** @deprecated 接入站点配置已移至「运营配置」 */ +export default function LegacyAgentLineSitesPage() { + redirect("/admin/config/integration-sites"); } diff --git a/src/app/admin/(shell)/config/integration-sites/page.tsx b/src/app/admin/(shell)/config/integration-sites/page.tsx index 897b1c8..9dce126 100644 --- a/src/app/admin/(shell)/config/integration-sites/page.tsx +++ b/src/app/admin/(shell)/config/integration-sites/page.tsx @@ -1,6 +1,18 @@ -import { redirect } from "next/navigation"; +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { IntegrationSitesConsole } from "@/modules/integration/integration-sites-console"; +import { PRD_INTEGRATION_ACCESS_ANY } from "@/lib/admin-prd"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; -/** @deprecated 接入配置已并入「代理线路」目录 */ -export default function LegacyIntegrationSitesPage() { - redirect("/admin/agents/sites"); +export const metadata: Metadata = buildPageMetadata("config", "integrationSites.title"); + +export default function ConfigIntegrationSitesPage() { + return ( + + + + + + ); } diff --git a/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx b/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx index 1fdf3e6..dd4fe62 100644 --- a/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx @@ -1,6 +1,6 @@ import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { DrawFinanceConsole } from "@/modules/draws/draw-finance-console"; -import { PRD_DRAW_ACCESS_ANY } from "@/lib/admin-prd"; +import { PRD_DRAW_FINANCE_ACCESS_ANY } from "@/lib/admin-prd"; import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; @@ -11,7 +11,7 @@ export default async function AdminDrawFinancePage(props: { }) { const { drawId } = await props.params; return ( - + ); diff --git a/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx b/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx index bdd5cd3..fb99837 100644 --- a/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx @@ -1,4 +1,6 @@ +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { DrawPublishConsole } from "@/modules/draws/draw-publish-console"; +import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd"; import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; @@ -8,5 +10,9 @@ export default async function AdminDrawPublishBatchPage(props: { params: Promise<{ drawId: string; batchId: string }>; }) { const { drawId, batchId } = await props.params; - return ; + return ( + + + + ); } diff --git a/src/app/admin/(shell)/draws/[drawId]/review/page.tsx b/src/app/admin/(shell)/draws/[drawId]/review/page.tsx index dd46b43..f15f648 100644 --- a/src/app/admin/(shell)/draws/[drawId]/review/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/review/page.tsx @@ -1,13 +1,13 @@ import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { DrawReviewConsole } from "@/modules/draws/draw-review-console"; -import { PRD_DRAW_ACCESS_ANY } from "@/lib/admin-prd"; +import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd"; export default async function AdminDrawReviewPage(props: { params: Promise<{ drawId: string }>; }) { const { drawId } = await props.params; return ( - + ); diff --git a/src/app/admin/(shell)/page.tsx b/src/app/admin/(shell)/page.tsx index 8ba82bc..bae3805 100644 --- a/src/app/admin/(shell)/page.tsx +++ b/src/app/admin/(shell)/page.tsx @@ -1,7 +1,7 @@ import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { PRD_DASHBOARD_ACCESS_ANY } from "@/lib/admin-prd"; -import { DashboardConsole } from "@/modules/dashboard/dashboard-console"; +import { DashboardPageClient } from "@/modules/dashboard/dashboard-page-client"; import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; @@ -11,7 +11,7 @@ export default function AdminDashboardPage() { return ( - + ); diff --git a/src/app/admin/(shell)/reports/legacy/page.tsx b/src/app/admin/(shell)/reports/legacy/page.tsx new file mode 100644 index 0000000..3853888 --- /dev/null +++ b/src/app/admin/(shell)/reports/legacy/page.tsx @@ -0,0 +1,16 @@ +import { notFound } from "next/navigation"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { PRD_REPORTS_VIEW_ACCESS_ANY } from "@/lib/admin-prd"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import { ReportsConsole } from "@/modules/reports/reports-console"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("reports", "legacyTitle"); + +export default function AdminReportsLegacyPage(): React.ReactElement { + return ( + + + + ); +} diff --git a/src/app/admin/(shell)/settlement-center/page.tsx b/src/app/admin/(shell)/settlement-center/page.tsx new file mode 100644 index 0000000..ab81a9a --- /dev/null +++ b/src/app/admin/(shell)/settlement-center/page.tsx @@ -0,0 +1,21 @@ +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { SettlementCenterConsole } from "@/modules/settlement/settlement-center-console"; +import { PRD_SETTLEMENT_AGENT_ACCESS_ANY } from "@/lib/admin-prd"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("settlementCenter", "title"); + +export default function SettlementCenterPage(): React.ReactElement { + return ( + + + + + + ); +} diff --git a/src/app/admin/(shell)/settlement/agent-bills/page.tsx b/src/app/admin/(shell)/settlement/agent-bills/page.tsx index 0cf2463..6d8a317 100644 --- a/src/app/admin/(shell)/settlement/agent-bills/page.tsx +++ b/src/app/admin/(shell)/settlement/agent-bills/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function LegacyAgentBillsPage() { - redirect("/admin/agents/settlement-bills"); + redirect("/admin/settlement-center"); } diff --git a/src/app/admin/(shell)/wallet/layout.tsx b/src/app/admin/(shell)/wallet/layout.tsx index 94e0f19..37ae7cd 100644 --- a/src/app/admin/(shell)/wallet/layout.tsx +++ b/src/app/admin/(shell)/wallet/layout.tsx @@ -1,12 +1,14 @@ import type { ReactNode } from "react"; +import { WalletScopeHint } from "@/modules/wallet/wallet-scope-hint"; import { WalletSubnav } from "@/modules/wallet/wallet-subnav"; export default function AdminWalletLayout({ children }: { children: ReactNode }) { return (
-
+
+
{children}
diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index bdac577..f56889e 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -43,8 +43,7 @@ const TOP_ROUTE_LABELS: Record = { const AGENT_ROUTE_LABELS: Record = { list: "agents.directoryTitle", provision: "agents.subnav.provision", - sites: "agents.sitesTitle", - "settlement-bills": "agents.subnav.settlementBills", + "settlement-bills": "settlementCenter.title", }; const CONFIG_ROUTE_LABELS: Record = { diff --git a/src/components/admin/admin-no-resource-state.tsx b/src/components/admin/admin-no-resource-state.tsx new file mode 100644 index 0000000..38fdf23 --- /dev/null +++ b/src/components/admin/admin-no-resource-state.tsx @@ -0,0 +1,83 @@ +"use client"; + +import Image from "next/image"; +import type { ReactElement, ReactNode } from "react"; +import { useTranslation } from "react-i18next"; + +import { TableCell, TableRow } from "@/components/ui/table"; +import { cn } from "@/lib/utils"; + +const NOTDATA_IMAGE = "/notdata.png"; + +export function AdminNoResourceState({ + className, + message, + compact = false, + imageClassName, + children, +}: { + className?: string; + /** 默认「暂无资源」 */ + message?: string; + compact?: boolean; + imageClassName?: string; + children?: ReactNode; +}): ReactElement { + const { t } = useTranslation("common"); + const label = message ?? t("states.noResource", { defaultValue: "暂无资源" }); + + return ( +
+ +

+ {label} +

+ {children} +
+ ); +} + +/** 表格无数据行(图片 + 暂无资源,竖排居中) */ +export function AdminTableNoResourceRow({ + colSpan, + className, + cellClassName, + message, + compact, +}: { + colSpan: number; + className?: string; + cellClassName?: string; + message?: string; + compact?: boolean; +}): ReactElement { + return ( + + + + + + ); +} diff --git a/src/components/admin/admin-permission-gate.tsx b/src/components/admin/admin-permission-gate.tsx index 801d8f9..3d85101 100644 --- a/src/components/admin/admin-permission-gate.tsx +++ b/src/components/admin/admin-permission-gate.tsx @@ -11,6 +11,10 @@ type AdminPermissionGateProps = { requiredAny: readonly string[]; children: ReactNode; className?: string; + /** 与 `isAgentLineSubnavTabVisible` 一致:绑定线路代理时额外放行 */ + allowWhenBoundLineAgent?: boolean; + /** 绑定线路代理时一律拒绝(如开通线路) */ + denyWhenBoundLineAgent?: boolean; }; /** 深链进入无权限页面时展示拒绝说明,避免空白或反复 403。 */ @@ -18,10 +22,29 @@ export function AdminPermissionGate({ requiredAny, children, className, + allowWhenBoundLineAgent = false, + denyWhenBoundLineAgent = false, }: AdminPermissionGateProps): React.ReactElement { const { t } = useTranslation("common"); const profile = useAdminProfile(); - const allowed = adminHasAnyPermission(profile?.permissions, [...requiredAny]); + const boundAgent = profile?.agent ?? null; + + if (denyWhenBoundLineAgent && boundAgent !== null) { + return ( + + + {t("permission.deniedTitle")} + + +

{t("permission.deniedDescription")}

+
+
+ ); + } + + const allowed = + (allowWhenBoundLineAgent && boundAgent !== null) || + adminHasAnyPermission(profile?.permissions, [...requiredAny]); if (allowed) { return <>{children}; diff --git a/src/components/admin/admin-permission-package-selector.tsx b/src/components/admin/admin-permission-package-selector.tsx index 2874854..ad449a9 100644 --- a/src/components/admin/admin-permission-package-selector.tsx +++ b/src/components/admin/admin-permission-package-selector.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { Checkbox } from "@/components/ui/checkbox"; +import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, @@ -119,8 +120,8 @@ export function AdminPermissionPackageSelector({ if (groups.length === 0 || bundleCount === 0) { return ( -
- {emptyText} +
+
); } @@ -128,26 +129,51 @@ export function AdminPermissionPackageSelector({ const toggleBundle = (group: RenderGroup, bundleKey: string, slugs: string[], checked: boolean) => { const next = new Set(selectedSet); const currentLevel = PACKAGE_LEVEL_ORDER[bundleKey] ?? 10; - const relatedSlugs = group.packages - .filter((item) => { - const level = PACKAGE_LEVEL_ORDER[item.key] ?? 10; - return checked ? level <= currentLevel : level >= currentLevel; - }) - .flatMap((item) => item.slugs); - for (const slug of relatedSlugs.length > 0 ? relatedSlugs : slugs) { - if (checked) next.add(slug); - else next.delete(slug); + if (checked) { + const implied = group.packages + .filter((item) => (PACKAGE_LEVEL_ORDER[item.key] ?? 10) <= currentLevel) + .flatMap((item) => item.slugs); + for (const slug of implied.length > 0 ? implied : slugs) { + next.add(slug); + } + for (const item of group.packages) { + if ((PACKAGE_LEVEL_ORDER[item.key] ?? 10) > currentLevel) { + for (const slug of item.slugs) { + next.delete(slug); + } + } + } + } else { + for (const slug of slugs) { + next.delete(slug); + } + if (bundleKey === "manage" || bundleKey === "reopen") { + for (const item of group.packages) { + if ((PACKAGE_LEVEL_ORDER[item.key] ?? 10) > currentLevel) { + for (const slug of item.slugs) { + next.delete(slug); + } + } + } + } } + onChange(Array.from(next).sort()); }; const toggleGroup = (group: RenderGroup, checked: boolean) => { const next = new Set(selectedSet); - const relatedSlugs = group.packages.flatMap((item) => item.slugs); - for (const slug of relatedSlugs) { - if (checked) next.add(slug); - else next.delete(slug); + if (checked) { + const viewBundle = + group.packages.find((item) => item.key === "view") ?? group.packages[0]; + for (const slug of viewBundle?.slugs ?? []) { + next.add(slug); + } + } else { + for (const slug of group.packages.flatMap((item) => item.slugs)) { + next.delete(slug); + } } onChange(Array.from(next).sort()); }; diff --git a/src/components/admin/admin-permission-selector.tsx b/src/components/admin/admin-permission-selector.tsx index 46b13fc..fecfca7 100644 --- a/src/components/admin/admin-permission-selector.tsx +++ b/src/components/admin/admin-permission-selector.tsx @@ -4,6 +4,7 @@ import { ChevronDown } from "lucide-react"; import { useMemo, useState } from "react"; import { Checkbox } from "@/components/ui/checkbox"; +import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import type { AdminPermissionCatalogData } from "@/types/api/admin-user"; @@ -108,8 +109,8 @@ export function AdminPermissionSelector({ if (groups.length === 0 || totalCount === 0) { return ( -
- {emptyText} +
+
); } diff --git a/src/components/admin/player-funding-badges.tsx b/src/components/admin/player-funding-badges.tsx new file mode 100644 index 0000000..e1e804e --- /dev/null +++ b/src/components/admin/player-funding-badges.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useTranslation } from "react-i18next"; + +import { playerFundingModeLabel } from "@/lib/admin-player-display"; +import { cn } from "@/lib/utils"; +import type { AdminPlayerRow } from "@/types/api/admin-player"; + +type FundingRow = Pick; + +export function PlayerFundingModeBadge({ + row, + className, +}: { + row: FundingRow; + className?: string; +}): React.ReactElement { + const { t } = useTranslation("players"); + const isCredit = row.funding_mode === "credit" || row.uses_credit === true; + + return ( + + {playerFundingModeLabel(row, t)} + + ); +} + +export function PlayerLedgerSourceBadge({ + ledgerSource, + className, +}: { + ledgerSource?: string | null; + className?: string; +}): React.ReactElement | null { + const { t } = useTranslation("wallet"); + if (ledgerSource !== "credit_ledger" && ledgerSource !== "wallet_txn") { + return null; + } + + const isCredit = ledgerSource === "credit_ledger"; + + return ( + + {isCredit + ? t("ledgerCredit", { defaultValue: "信用流水" }) + : t("ledgerWallet", { defaultValue: "钱包流水" })} + + ); +} diff --git a/src/hooks/use-admin-site-code-options.ts b/src/hooks/use-admin-site-code-options.ts index d74d8a4..2b37655 100644 --- a/src/hooks/use-admin-site-code-options.ts +++ b/src/hooks/use-admin-site-code-options.ts @@ -32,11 +32,13 @@ async function fetchSiteCodeOptions(): Promise { inflightSites = getAdminIntegrationSites() .then((data) => { - cachedSites = data.items.map((row) => ({ - id: row.id, - code: row.code, - name: row.name, - })); + const byId = new Map(); + for (const row of data.items) { + if (!byId.has(row.id)) { + byId.set(row.id, { id: row.id, code: row.code, name: row.name }); + } + } + cachedSites = [...byId.values()]; return cachedSites; }) .catch(() => { diff --git a/src/i18n/index.ts b/src/i18n/index.ts index c70d2e7..db3bde8 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -25,6 +25,7 @@ import enReconcile from "@/i18n/locales/en/reconcile.json"; import enReports from "@/i18n/locales/en/reports.json"; import enWallet from "@/i18n/locales/en/wallet.json"; import enAgents from "@/i18n/locales/en/agents.json"; +import enSettlementCenter from "@/i18n/locales/en/settlementCenter.json"; import neAudit from "@/i18n/locales/ne/audit.json"; import neAdminUsers from "@/i18n/locales/ne/adminUsers.json"; import neAuth from "@/i18n/locales/ne/auth.json"; @@ -57,12 +58,13 @@ import zhReconcile from "@/i18n/locales/zh/reconcile.json"; import zhReports from "@/i18n/locales/zh/reports.json"; import zhWallet from "@/i18n/locales/zh/wallet.json"; import zhAgents from "@/i18n/locales/zh/agents.json"; +import zhSettlementCenter from "@/i18n/locales/zh/settlementCenter.json"; export const ADMIN_SUPPORTED_LANGUAGES = ["en", "ne", "zh"] as const; export type AdminLanguage = (typeof ADMIN_SUPPORTED_LANGUAGES)[number]; export const ADMIN_DEFAULT_LANGUAGE: AdminLanguage = "zh"; -const namespaces = ["common", "auth", "dashboard", "audit", "draws", "settlement", "risk", "jackpot", "players", "tickets", "reconcile", "reports", "wallet", "adminUsers", "agents", "config"] as const; +const namespaces = ["common", "auth", "dashboard", "audit", "draws", "settlement", "settlementCenter", "risk", "jackpot", "players", "tickets", "reconcile", "reports", "wallet", "adminUsers", "agents", "config"] as const; const resources = { en: { @@ -82,6 +84,7 @@ const resources = { settlement: enSettlement, wallet: enWallet, agents: enAgents, + settlementCenter: enSettlementCenter, }, ne: { common: neCommon, @@ -100,6 +103,7 @@ const resources = { settlement: neSettlement, wallet: neWallet, agents: neAgents, + settlementCenter: enSettlementCenter, }, zh: { common: zhCommon, @@ -118,6 +122,7 @@ const resources = { settlement: zhSettlement, wallet: zhWallet, agents: zhAgents, + settlementCenter: zhSettlementCenter, }, } satisfies Record>>; diff --git a/src/i18n/locales/en/adminUsers.json b/src/i18n/locales/en/adminUsers.json index 63647ab..1e0726d 100644 --- a/src/i18n/locales/en/adminUsers.json +++ b/src/i18n/locales/en/adminUsers.json @@ -6,6 +6,7 @@ "loadFailed": "Failed to load admin list", "nicknameRequired": "Enter a nickname", "newPasswordMin": "New password must be at least 8 characters", + "siteRequired": "Select a site", "roleRequired": "Select at least one role", "usernameRequired": "Enter a login username", "passwordMin": "Password must be at least 8 characters", @@ -40,6 +41,7 @@ "account": "Account", "nickname": "Nickname", "status": "Status", + "sites": "Bound sites", "roles": "Roles", "effective": "Effective", "actions": "Actions" @@ -90,7 +92,8 @@ "permissionDialog": { "title": "Assign roles", "rolesTitle": "Roles", - "rolesDescription": "Admins only bind roles here. Maintain detailed permissions in Role Management.", + "site": "Site", + "rolesDescription": "Admins only bind roles here. Maintain detailed permissions in Role Management. Save roles per site.", "rolePermissionCount": "Contains {{count}} functional permissions", "selectedRoles": "Selected roles:", "saveRoles": "Save roles" @@ -125,8 +128,10 @@ "passwordOptional": "Password (optional)", "passwordPlaceholderCreate": "At least 8 characters", "passwordPlaceholderEdit": "Leave empty to keep unchanged", - "rolesRequired": "Roles (default site, at least one)", - "rolesDescription": "After creation, you can continue adjusting role bindings in Assign Roles.", + "site": "Bound site", + "sitePlaceholder": "Select which site this account can access", + "rolesRequired": "Roles (at least one)", + "rolesDescription": "After creation, adjust per-site role bindings in Assign Roles.", "noRoles": "No roles available yet. Wait for the list to finish loading and try again." }, "delete": { diff --git a/src/i18n/locales/en/agents.json b/src/i18n/locales/en/agents.json index 857b860..fdb94dd 100644 --- a/src/i18n/locales/en/agents.json +++ b/src/i18n/locales/en/agents.json @@ -1,15 +1,42 @@ { - "title": "Agent lines", - "sitesTitle": "Sites", - "sitesListHint": "For the full site table (keys, callbacks, etc.), go to", - "sitesListLink": "Sites", + "title": "Agent management", + "sitesListHint": "For integration keys and callbacks, go to", + "sitesListLink": "Config · Connected sites", + "lineUi": { + "kicker": "Credit share · Agent tree", + "agentCount": "{{count}} agents in this group", + "searchPlaceholder": "Search name or login", + "directChildren": "{{count}} direct downline", + "selectAgent": "Select an agent to view share & credit", + "selectAgentHint": "Settlement boundaries follow the agent tree; share, credit and rebate are configured per node.", + "allocatedCredit": "Allocated", + "availableCredit": "Available", + "profileFootnote": "Rebate cap {{rebate}}% · Default {{defaultRebate}}% · {{cycle}}", + "tabOverview": "Overview", + "tabProfile": "Share & credit", + "tabProfileReadOnly": "Share & credit (read-only)", + "currentSite": "Site", + "viewAll": "View all", + "shareRebateCap": "Rebate cap {{rate}}%", + "overviewDownlineCard": "{{count}} direct downline — manage in the Downline tab.", + "downlineEmptyTitle": "No direct downline yet", + "editAccount": "Account & status", + "saveProfile": "Save share & credit", + "tabDownline": "Downline", + "tabPlayers": "Players", + "noDelegatedTabs": "This agent cannot create downline agents or players; only share and credit settings apply." + }, + "listTitle": "Agents", + "listSearch": "Search name / code / login", + "parentAgent": "Parent", + "childrenCount": "Direct downline", "subnav": { - "label": "Agent line navigation", + "label": "Agent management navigation", "noPermission": "No permission", - "operations": "Operations", - "provision": "Provision line", - "sites": "Sites", - "settlementBills": "Agent bills" + "operations": "Line & agent tree", + "provision": "Provision level-1 agent", + "settlementBills": "Periods & bills", + "provisionHint": "One-time onboarding; use Line & agent tree for daily work" }, "includeRoots": "Include root nodes", "includeRootsHint": "Root nodes represent site boundaries and are excluded from operating agent counts by default.", @@ -35,7 +62,7 @@ "detailTitle": "Node details", "selectNode": "Select an agent node from the tree", "loadFailed": "Failed to load agent tree", - "siteLabel": "Site", + "lineFilter": "Level-1 agent", "createChild": "Add child agent", "viewDownline": "View sub-agents and players", "downlineDialogTitle": "{{name}} — sub-agents and players", @@ -86,8 +113,8 @@ "section": "Share & credit", "totalShareRate": "Share rate (%)", "creditLimit": "Credit limit", - "rebateLimit": "Rebate ceiling", - "defaultPlayerRebate": "Default player rebate", + "rebateLimit": "Rebate ceiling (%)", + "defaultPlayerRebate": "Default player rebate (%)", "settlementCycle": "Settlement cycle", "canGrantExtraRebate": "Allow extra rebate", "canCreatePlayer": "Allow creating players", @@ -101,41 +128,72 @@ "validation": { "shareRange": "Share rate must be between 0 and 100", "creditInvalid": "Credit limit cannot be negative", - "rebateLimitRange": "Rebate ceiling must be between 0 and 1 (e.g. 0.005 = 0.5%)", - "defaultRebateRange": "Default player rebate must be between 0 and 1", + "rebateLimitRange": "Rebate ceiling must be between 0 and 100%", + "defaultRebateRange": "Default player rebate must be between 0 and 100%", "defaultExceedsLimit": "Default player rebate cannot exceed the rebate ceiling" } }, "settlementBills": { "title": "Agent bills", - "description": "Player/agent bills generated after a period is closed", + "description": "Bills appear after a period is closed; latest period is selected by default.", + "periodLabel": "Period", + "periodPlaceholder": "Select period", + "allPeriods": "All periods", + "filteredByPeriodRange": "Bills for {{range}}", + "emptyNoPeriodsManage": "No periods or bills yet. Use quick presets under Period management, then close the period.", + "emptyNoPeriodsAgent": "No bills yet. Your upline or platform will close periods; you do not need to enter dates.", + "emptyNoClosed": "No closed period yet. Bills are generated after close.", + "typePlayer": "Player bill", + "typeAgent": "Agent bill", "columns": { "id": "ID", + "period": "Period", "type": "Type", "net": "Net", "unpaid": "Unpaid", "status": "Status" } }, + "settlementPeriods": { + "manageTitle": "Period management", + "manageHint": "Open and close periods here; bills above update automatically. Quick presets are usually enough.", + "presetThisWeek": "This week", + "presetLastWeek": "Last week", + "presetThisMonth": "This month", + "statusOpen": "Open", + "statusClosed": "Closed" + }, "lineProvision": { - "title": "Provision agent line", - "description": "Creates site, root agent, and admin account in one step (site_code matches agent code).", - "code": "Site code", - "name": "Line name", - "username": "Agent login", + "title": "Create level-1 agent", + "description": "Creates the level-1 agent, admin login, and line settings (share, credit, rebate, settlement cycle) in one step. Code cannot be changed later.", + "code": "Agent code", + "name": "Level-1 agent name", + "username": "Admin login", "password": "Initial password", - "walletUrl": "Wallet API URL", - "submit": "Provision", - "success": "Line provisioned", - "secretsOnce": "Secrets are shown once — save them now", - "link": "Provision line" + "walletUrl": "Wallet API URL (optional technical field)", + "submit": "Create level-1 agent", + "success": "Level-1 agent created", + "secretsOnce": "Integration secrets are shown once — save them now", + "link": "Create level-1 agent" }, "noAccess": "You do not have permission to manage agents. Contact an administrator.", "playersPanel": { "create": "Create player", "scopedTo": "Direct players: {{agent}}", "allUnderSite": "Players visible on this site", - "filterHint": "Filter direct players by parent agent." + "filterHint": "Filter direct players by parent agent.", + "loginRequired": "Enter login username and initial password", + "loginUsername": "Login username", + "initialPassword": "Initial password", + "externalIdOptional": "External ID (optional)", + "externalIdHint": "Leave blank to auto-generate", + "creditLimit": "Credit limit", + "rebateRate": "Rebate rate (%)", + "rebateRateHint": "Enter percent, e.g. 0.5 = 0.5%", + "availableToGrant": "Agent available to grant: {{amount}}", + "riskTags": "Risk tags", + "riskTagsPlaceholder": "Comma-separated", + "createSuccessNative": "Player {{name}} created — use lottery /login" }, "delegation": { "title": "Delegation ceiling", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 636f84d..0f91f9b 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -106,7 +106,8 @@ "next": "Next" }, "states": { - "noData": "No data", + "noResource": "No resources", + "noData": "No resources", "loading": "Loading…", "comingSoon": "Feature under development" }, @@ -162,6 +163,7 @@ "account": "Account settings", "integration": "Integration", "agents": "Agent lines", + "settlement_center": "Settlement center", "config": "Operations config" }, "sidebar": { diff --git a/src/i18n/locales/en/dashboard.json b/src/i18n/locales/en/dashboard.json index 9b4d91e..2f14844 100644 --- a/src/i18n/locales/en/dashboard.json +++ b/src/i18n/locales/en/dashboard.json @@ -154,6 +154,31 @@ "riskMonitor": "Risk monitor", "systemSettings": "System settings" }, + "agent": { + "title": "Operations overview", + "subtitle": "Your line scope · {{name}}", + "creditTitle": "Credit limit", + "creditAvailable": "Available {{amount}}", + "creditAllocated": "Allocated {{amount}}", + "creditUsed": "Used {{amount}}", + "shareRate": "Total share {{rate}}%", + "settlementCycle": "Cycle {{cycle}}", + "teamTitle": "Team size", + "directChildren": "Direct child agents", + "directPlayers": "Direct players", + "subtreeAgents": "Agents in line", + "pendingBills": "Open agent bills", + "pendingUnpaid": "Unpaid total {{amount}}", + "viewBills": "View bills", + "viewLine": "Agent line", + "quickLinks": { + "tickets": "Tickets", + "players": "Players", + "reports": "Reports", + "agents": "Downline agents", + "bills": "Agent bills" + } + }, "warnings": { "drawPermission": "This account has no draw/dashboard view permission. Finance and risk data were not returned.", "walletPermission": "This account has no wallet reconciliation permission. Abnormal transfer count was not returned.", diff --git a/src/i18n/locales/en/jackpot.json b/src/i18n/locales/en/jackpot.json index ccbb967..d8741fe 100644 --- a/src/i18n/locales/en/jackpot.json +++ b/src/i18n/locales/en/jackpot.json @@ -31,12 +31,12 @@ "confirmAdjustmentTitle": "Confirm pool balance adjustment?", "confirmAdjustmentDescription": "This writes a ledger entry and updates the pool balance. Verify amount and reason.", "recentAdjustments": "Recent adjustments", - "contributionRate": "Contribution rate 0-1", - "contributionRatePlaceholder": "Enter contribution rate, for example 0.02", + "contributionRate": "Contribution rate (%)", + "contributionRatePlaceholder": "e.g. 2 = 2%", "triggerThreshold": "Burst threshold (minor unit)", "triggerThresholdPlaceholder": "Enter burst threshold", - "payoutRate": "Burst payout rate 0-1", - "payoutRatePlaceholder": "Enter payout rate, for example 0.05", + "payoutRate": "Burst payout rate (%)", + "payoutRatePlaceholder": "e.g. 5 = 5%", "forceTriggerGap": "Force burst gap (settled draws)", "forceTriggerGapPlaceholder": "Enter forced burst gap in draws", "minBetAmount": "Minimum bet amount (minor unit)", diff --git a/src/i18n/locales/en/players.json b/src/i18n/locales/en/players.json index dbef32d..d455654 100644 --- a/src/i18n/locales/en/players.json +++ b/src/i18n/locales/en/players.json @@ -8,6 +8,7 @@ "tabOverview": "Overview", "tabTickets": "Tickets", "tabWalletTxns": "Wallet transactions", + "tabCreditLedger": "Credit ledger", "tabTransferOrders": "Transfer orders", "profileSection": "Profile", "walletsSection": "Wallets", @@ -21,6 +22,11 @@ "searchPlaceholder": "Search by player ID / username / nickname", "filterSite": "Site", "filterAllSites": "All sites", + "scopeAllSites": "Scope: all players on all sites (super admin)", + "scopeFilteredSite": "Scope: site {{site}}", + "scopeAgentLine": "Scope: {{site}} · agent line “{{name}}” and downstream players", + "scopeSingleSite": "Scope: site {{site}}", + "scopeMultiSite": "Scope: {{count}} bound site(s); use filter to narrow", "search": "Search", "refresh": "Refresh", "loadFailed": "Failed to load player list", @@ -49,6 +55,16 @@ "available": "Available", "status": "Status", "lastLogin": "Last login", + "fundingMode": "Funding mode", + "authSource": "Auth source", + "creditSection": "Credit line", + "usedCredit": "Used credit", + "fundingCredit": "Credit line", + "fundingWallet": "Main-site wallet", + "authMainSite": "Main-site SSO", + "authNative": "Lottery native", + "creditLimit": "Credit limit", + "availableCredit": "Available credit", "actions": "Actions", "edit": "Edit", "freeze": "Freeze", diff --git a/src/i18n/locales/en/reports.json b/src/i18n/locales/en/reports.json index ad3cefa..2bf8f0c 100644 --- a/src/i18n/locales/en/reports.json +++ b/src/i18n/locales/en/reports.json @@ -225,10 +225,12 @@ "status": "Status", "createdAt": "Created at" }, + "legacyTitle": "Legacy wallet reports", "categories": { "all": "All", "profit": "Profit", "wallet": "Funds", + "legacy": "Legacy", "risk": "Risk", "audit": "Audit" }, @@ -297,7 +299,8 @@ }, "rebate_commission": { "title": "Commission / rebate report", - "summary": "Summarize commission, rebate, and matched rules by play and period." + "summary": "Summarize commission, rebate, and matched rules by play and period.", + "disclaimer": "Wallet-mode instant rebate/commission — not agent credit-line period settlement. Use Agent → Settlement bills for credit-line reports." }, "admin_audit": { "title": "Admin operation audit report", diff --git a/src/i18n/locales/en/settlementCenter.json b/src/i18n/locales/en/settlementCenter.json new file mode 100644 index 0000000..5270cb0 --- /dev/null +++ b/src/i18n/locales/en/settlementCenter.json @@ -0,0 +1,203 @@ +{ + "title": "Settlement center", + "header": { + "subtitle": "Credit-line settlement", + "statusRunning": "Period open", + "statusIdle": "No open period", + "statusCompleted": "Period completed" + }, + "subnav": { + "label": "Settlement center navigation" + }, + "nav": { + "aria": "Settlement center navigation", + "group": { + "hub": "Workbench", + "finance": "Finance", + "ledger": "Ledger", + "bills": "Bills" + }, + "overview": "Overview", + "periods": "Periods", + "ledger": "Account ledger", + "bills": "Bills", + "creditLedger": "Credit ledger", + "playerBills": "Player bills", + "agentBills": "Agent bills", + "pendingConfirm": "Pending confirm", + "awaitingPayment": "Awaiting payment", + "payments": "Payment log", + "adjustments": "Adjust / reverse", + "badDebt": "Bad debt", + "reports": "Period reports" + }, + "filters": { + "period": "Period", + "allPeriods": "All periods", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusCompleted": "Completed" + }, + "overview": { + "pendingConfirm": "Pending confirm", + "awaitingPayment": "Awaiting payment", + "totalUnpaid": "Total unpaid", + "openPeriod": "Open period", + "creditLedger": "Credit ledger (in period)", + "shareLedger": "Share ledger (in period)", + "pipelineHint": "Bills are created after period close; counts below are in-period activity." + }, + "ledger": { + "groupIntro": "In-period money movements: credit holds, bill payments, adjustments, and bad debt. Share bills after close are under Bills.", + "paymentsIntro": "Confirmed bill payments (payment_records). Register from bill detail; this page is the period-wide log.", + "adjustmentsIntro": "Bill adjustments and reversals (settlement_adjustments).", + "badDebtIntro": "Bad debt write-off entries linked to original bills." + }, + "creditLedger": { + "intro": "In-period credit holds, bill payments, adjustments, and bad debt. Use ⋯ on a row to confirm, record payment, adjust, reverse, or write off (when a bill is linked).", + "columns": { + "txn": "Txn ID", + "player": "Player", + "reason": "Type", + "ref": "Reference", + "amount": "Amount", + "channel": "Channel", + "status": "Status", + "time": "Time" + }, + "channelCredit": "Credit line", + "viewPlayer": "Player detail", + "entryKind": { + "adjustment": "Adjustment" + }, + "actions": { + "viewPlayer": "Player detail", + "viewBill": "Bill detail", + "confirm": "Confirm bill", + "confirmDesc": "After confirm, the bill moves to awaiting payment.", + "payment": "Record payment", + "adjustment": "Adjust", + "reversal": "Reverse", + "badDebt": "Bad debt" + }, + "reason": { + "payment_record": "Bill payment", + "bet_hold": "Bet hold", + "bet_hold_release": "Hold release", + "game_settlement_loss": "Draw settlement", + "settlement_confirm": "Period confirm" + } + }, + "columns": { + "period": "Period", + "type": "Type", + "owner": "Owner", + "counterparty": "Counterparty", + "gross": "Win/loss", + "net": "Net", + "paid": "Paid", + "unpaid": "Unpaid", + "status": "Status", + "billId": "Bill ID", + "payer": "Payer", + "payee": "Payee", + "amount": "Amount", + "method": "Method", + "time": "Time", + "adjustmentType": "Type", + "originalBill": "Original bill", + "reason": "Reason", + "badDebtAmount": "Write-off" + }, + "billStatus": { + "pending_confirm": "Pending confirm", + "confirmed": "Confirmed", + "partial_paid": "Partial paid", + "settled": "Settled", + "overdue": "Overdue", + "reversed": "Reversed" + }, + "billType": { + "adjustment": "Adjustment", + "reversal": "Reversal", + "badDebt": "Bad debt write-off" + }, + "adjustmentType": { + "adjustment": "Adjustment", + "reversal": "Reversal", + "bad_debt": "Bad debt" + }, + "actions": { + "detail": "Detail", + "viewBill": "View bill", + "billDetail": "Bill detail" + }, + "ledgerPanel": { + "search": "Search", + "searchBtn": "Search", + "reset": "Reset filters", + "refresh": "Refresh page", + "filterAll": "Any", + "playerAccount": "Player account", + "playerAccountPh": "Username or site player ID", + "playerId": "Player ID", + "optional": "Optional", + "billStatus": "Bill status", + "dateRange": "Date range", + "rowPosted": "Posted", + "category": { + "all": "All", + "credit": "Credit holds", + "payment": "Payments", + "adjustment": "Adjust / reverse", + "badDebt": "Bad debt", + "actionable": "Needs action" + } + }, + "billsPanel": { + "intro": "Share bills after period close. Filter by type or status; open detail to confirm or record payment.", + "category": { + "all": "All", + "player": "Player bills", + "agent": "Agent bills", + "pendingConfirm": "Pending confirm", + "awaitingPayment": "Awaiting payment" + } + }, + "panels": { + "overview": { "title": "Overview" }, + "ledger": { "title": "Account ledger" }, + "bills": { "title": "Bills" }, + "creditLedger": { "title": "Credit ledger" }, + "playerBills": { "title": "Player bills" }, + "agentBills": { "title": "Agent bills" }, + "pendingConfirm": { "title": "Pending confirm" }, + "awaiting": { "title": "Awaiting payment" }, + "payments": { "title": "Payment log" }, + "adjustments": { "title": "Adjust / reverse" }, + "reports": { "title": "Period reports" }, + "badDebt": { "title": "Bad debt" } + }, + "empty": { + "noSite": "Select an integration site.", + "noPeriods": "Open and close a period under Periods first.", + "noClosed": "Close a period to generate bills.", + "noBadDebt": "No bad debt write-offs yet.", + "noCreditLedger": "No credit ledger rows in this period. Check credit players placed bets and the period date range.", + "billsNeedClose": "Share bills appear after period close. If credit ledger has rows but bills are empty, settle draws then close the period." + }, + "periods": { + "loadFailed": "Failed to load periods" + }, + "toast": { + "periodClosed": "Period closed", + "periodClosedUnsettled": "Period closed; {{count}} ticket(s) still unsettled." + }, + "errors": { + "loadBills": "Failed to load bills", + "loadPayments": "Failed to load payments", + "loadAdjustments": "Failed to load adjustments", + "loadBadDebt": "Failed to load bad debt records", + "loadCreditLedger": "Failed to load credit ledger" + } +} diff --git a/src/i18n/locales/en/wallet.json b/src/i18n/locales/en/wallet.json index 0120e71..732442f 100644 --- a/src/i18n/locales/en/wallet.json +++ b/src/i18n/locales/en/wallet.json @@ -1,8 +1,14 @@ { "title": "Wallet", "subnavLabel": "Wallet sub pages", - "subnavTransactions": "Wallet transactions", - "subnavTransferOrders": "Transfer orders", + "subnavTransactions": "Main-site wallet txns", + "subnavTransferOrders": "Main-site transfers", + "scopeHint": "This area is for main-site wallet mode (wallet txns and transfers). For credit-line bet holds and settlement entries, see", + "scopeHintSettlementLink": "Settlement center → Credit ledger", + "scopeHintSettlement": "Settlement center → Credit ledger", + "ledgerChannel": "Ledger", + "ledgerCredit": "Credit ledger", + "ledgerWallet": "Wallet txn", "subnavPlayerWallet": "Player wallet", "noPermission": "Current account has no access to this page", "copySuccess": "{{label}} copied to clipboard", diff --git a/src/i18n/locales/ne/agents.json b/src/i18n/locales/ne/agents.json index 96c0ed3..dc5268f 100644 --- a/src/i18n/locales/ne/agents.json +++ b/src/i18n/locales/ne/agents.json @@ -1,14 +1,12 @@ { "title": "एजेन्ट लाइन", - "sitesTitle": "साइट सूची", - "sitesListHint": "पूर्ण साइट तालिका (कुञ्जी, कलब्याक) को लागि", - "sitesListLink": "साइट सूची", + "sitesListHint": "कुञ्जी र कलब्याकको लागि", + "sitesListLink": "कन्फिग · साइट", "subnav": { "label": "एजेन्ट लाइन नेभ", "noPermission": "अनुमति छैन", "operations": "सञ्चालन", - "provision": "लाइन खोल्नुहोस्", - "sites": "साइट सूची", + "provision": "प्रथम-स्तर एजेन्ट सिर्जना", "settlementBills": "एजेन्ट बिल" }, "tabs": { @@ -35,7 +33,7 @@ "detailTitle": "Node details", "selectNode": "Select an agent node from the tree", "loadFailed": "Failed to load agent tree", - "siteLabel": "Site", + "lineFilter": "प्रथम-स्तर एजेन्ट", "createChild": "Add child agent", "viewDownline": "चाइल्ड एजेन्ट र खेलाडी हेर्नुहोस्", "downlineDialogTitle": "{{name}} — चाइल्ड एजेन्ट र खेलाडी", @@ -118,17 +116,17 @@ } }, "lineProvision": { - "title": "एजेन्ट लाइन खोल्नुहोस्", - "description": "एकै चरणमा साइट, रुट एजेन्ट र खाता सिर्जना (site_code = agent code)।", - "code": "साइट code", - "name": "लाइन नाम", - "username": "एजेन्ट लगइन", + "title": "प्रथम-स्तर एजेन्ट सिर्जना", + "description": "एकै चरणमा प्रथम-स्तर एजेन्ट, खाता र लाइन सेटिङ। कोड पछि बदल्न मिल्दैन।", + "code": "एजेन्ट कोड", + "name": "एजेन्ट नाम", + "username": "एडमिन लगइन", "password": "प्रारम्भिक पासवर्ड", - "walletUrl": "वालेट API URL", - "submit": "खोल्नुहोस्", - "success": "लाइन खोलियो", + "walletUrl": "वालेट API URL (वैकल्पिक)", + "submit": "सिर्जना गर्नुहोस्", + "success": "प्रथम-स्तर एजेन्ट सिर्जना भयो", "secretsOnce": "कुञ्जी एक पटक मात्र देखाइन्छ", - "link": "लाइन खोल्नुहोस्" + "link": "प्रथम-स्तर एजेन्ट सिर्जना" }, "noAccess": "एजेन्ट सञ्चालन अनुमति छैन। प्रशासकलाई सम्पर्क गर्नुहोस्।", "playersPanel": { diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index e2f80ed..87faad9 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -106,7 +106,8 @@ "next": "अर्को" }, "states": { - "noData": "डाटा छैन", + "noResource": "स्रोत छैन", + "noData": "स्रोत छैन", "loading": "लोड हुँदैछ…", "comingSoon": "सुविधा विकासमा छ" }, diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json index 3df81bc..d479a93 100644 --- a/src/i18n/locales/zh/adminUsers.json +++ b/src/i18n/locales/zh/adminUsers.json @@ -16,6 +16,7 @@ "deleteSuccess": "已删除 {{name}}", "deleteFailed": "删除失败", "roleListTitle": "平台角色管理", + "roleListHint": "平台仅保留「超级管理员」与「代理」两个内置角色;超级管理员自动拥有全部权限。", "createRole": "新增平台角色", "roleCreateSuccess": "已创建角色 {{name}}", "roleUpdateSuccess": "已更新角色 {{name}}", @@ -40,10 +41,12 @@ "account": "账号", "nickname": "昵称", "status": "状态", + "sites": "绑定站点", "roles": "角色", "effective": "生效权限", "actions": "操作" }, + "siteRequired": "请选择绑定站点", "status": { "enabled": "启用", "disabled": "禁用" @@ -90,13 +93,15 @@ "permissionDialog": { "title": "分配角色", "rolesTitle": "角色", - "rolesDescription": "管理员只绑定角色;具体权限请到「角色管理」里维护。", + "site": "站点", + "rolesDescription": "管理员只绑定角色;具体权限请到「角色管理」里维护。按站点分别保存角色。", "rolePermissionCount": "含 {{count}} 项功能权限", "selectedRoles": "当前勾选的角色:", "saveRoles": "保存角色" }, "rolePermissionDialog": { - "title": "角色权限" + "title": "角色权限", + "packageHint": "勾选左侧模块行仅授予「查看」;录入、封盘、开奖等管理操作请单独勾选「管理」。" }, "roleDialog": { "createTitle": "新增角色", @@ -125,8 +130,10 @@ "passwordOptional": "密码(可选)", "passwordPlaceholderCreate": "至少 8 位", "passwordPlaceholderEdit": "不修改请留空", - "rolesRequired": "角色(默认站点,至少一项)", - "rolesDescription": "创建后可在「分配角色」中继续调整角色绑定。", + "site": "绑定站点", + "sitePlaceholder": "选择该账号可访问的数据站点", + "rolesRequired": "角色(至少一项)", + "rolesDescription": "创建后可在「分配角色」中按站点继续调整角色绑定。", "noRoles": "暂无角色数据,请等待列表加载完成后重试。" }, "delete": { diff --git a/src/i18n/locales/zh/agents.json b/src/i18n/locales/zh/agents.json index 9d5af25..43b0d27 100644 --- a/src/i18n/locales/zh/agents.json +++ b/src/i18n/locales/zh/agents.json @@ -1,18 +1,63 @@ { - "title": "代理线路", - "sitesTitle": "站点列表", - "sitesListHint": "完整站点表格(密钥、回调等)请前往", - "sitesListLink": "站点列表", + "title": "代理管理", + "sitesListHint": "线路对接参数(密钥、回调等)请前往", + "sitesListLink": "运营配置 · 接入站点", + "lineUi": { + "kicker": "信用占成 · 代理树", + "agentCount": "本组 {{count}} 个代理", + "searchPlaceholder": "搜索名称或登录名", + "directChildren": "直属下级 {{count}}", + "selectAgent": "选择左侧代理查看占成与授信", + "selectAgentHint": "信用占成盘以代理树为结算边界,占成、授信与回水均在代理节点配置。", + "allocatedCredit": "已下发", + "availableCredit": "可下发", + "profileFootnote": "回水上限 {{rebate}}% · 默认回水 {{defaultRebate}}% · {{cycle}}", + "tabOverview": "概览", + "currentSite": "当前站点", + "viewAll": "查看全部", + "shareRebateCap": "回水上限 {{rate}}%", + "overviewDownlineCard": "{{count}} 个,可在对应 Tab 管理下级代理。", + "downlineEmptyTitle": "暂无直属下级", + "tabProfile": "占成与授信", + "tabProfileReadOnly": "占成与授信(只读)", + "profileReadOnlyHint": "占成、授信与回水由上级配置,如需调整请联系上级代理或平台。", + "selfAgentOverviewHint": "以下为上级为您分配的授信额度,占成与回水由上级在后台维护,本账号不可查看或修改。", + "overviewDownlineHint": "直属下级 {{count}} 个,可在「直属下级」Tab 管理。", + "overviewPlayersHint": "直属玩家请在「直属玩家」Tab 维护。", + "tabDownline": "直属下级", + "tabPlayers": "直属玩家", + "downlineColumns": { + "email": "邮箱", + "downlineCount": "下级数" + }, + "editAccount": "账号与状态", + "editCurrent": "编辑本代理", + "saveProfile": "保存占成与授信", + "profileTabHint": "占成、授信、回水与风控标签在此维护;登录名与密码请用「账号与状态」。", + "nextSteps": "建议操作", + "stepProfile": "配置占成、授信与回水", + "stepDownline": "管理直属下级(当前 {{count}} 个)", + "stepPlayers": "创建或维护直属玩家", + "downlineEmpty": "暂无直属下级。创建下级代理后将在此展示。", + "downlineEmptyShort": "暂无直属下级。", + "noDelegatedTabs": "该代理未开放创建下级或玩家。请使用「编辑本代理」维护占成、授信与风控标签。", + "expand": "展开", + "collapse": "收起" + }, + "listTitle": "代理列表", + "listSearch": "搜索代理名称 / 编码 / 登录名", + "parentAgent": "上级代理", + "childrenCount": "直属下级", "subnav": { - "label": "代理线路导航", + "label": "代理管理导航", "noPermission": "无权限", - "operations": "代理经营", - "provision": "开通线路", - "sites": "站点列表", - "settlementBills": "代理账单" + "operations": "线路与代理树", + "provision": "开通一级代理", + "settlementBills": "账期与账单", + "provisionHint": "请先在「平台管理 → 接入配置」创建接入站点;对接密钥在站点创建时一次性展示。" }, "includeRoots": "包含根节点", - "includeRootsHint": "根节点用于表示站点边界,默认不计入经营代理列表。", + "includeRootsHint": "根节点表示一级代理线路边界,默认不计入经营代理列表。", "directoryStatus": { "all": "全部状态", "enabled": "仅启用", @@ -35,7 +80,7 @@ "detailTitle": "代理详情", "selectNode": "请选择代理", "loadFailed": "加载代理列表失败", - "siteLabel": "站点", + "lineFilter": "一级代理", "createChild": "添加下级代理", "viewDownline": "查看下级代理和玩家", "downlineDialogTitle": "{{name}} — 下级代理与玩家", @@ -73,12 +118,12 @@ "modelGuide": "代理层负责数据范围(Scope)与授权上限(Ceiling),账号权限请通过角色分配。", "pageGuide": "这里统一管理代理树、代理角色、代理账号与下放上限。平台账号和平台角色请到各自的平台治理页面维护。", "summary": { - "currentSiteNodes": "当前站点节点总数", - "currentSiteAgents": "当前站点经营代理数", + "currentSiteNodes": "当前线路节点总数", + "currentSiteAgents": "当前线路经营代理数", "visibleList": "当前平铺列表条数", "visibleAgents": "当前可见经营代理数", - "globalNodes": "全部站点节点总数", - "globalAgents": "全部站点经营代理数", + "globalNodes": "全部线路节点总数", + "globalAgents": "全部线路经营代理数", "enabledAgents": "启用中的经营代理数", "rootNodes": "根节点数量" }, @@ -86,12 +131,15 @@ "section": "占成与授信", "totalShareRate": "占成比例 (%)", "creditLimit": "授信额度", - "rebateLimit": "回水上限", - "defaultPlayerRebate": "默认玩家回水", + "rebateLimit": "回水上限 (%)", + "defaultPlayerRebate": "默认玩家回水 (%)", "settlementCycle": "结算周期", "canGrantExtraRebate": "允许额外回水", "canCreatePlayer": "允许创建玩家", "canCreateChildAgent": "允许创建下级代理", + "capabilityHint": "保存后对该代理主账号生效;若平台「代理」角色含玩家/节点管理权限,此前仍可能可操作——现已按本开关自动收紧登录权限。", + "parentCaps": "上级占成 {{share}}%,可下发 {{credit}}", + "availableCredit": "可下发额度 {{amount}}", "cycleDaily": "日结", "cycleWeekly": "周结", "cycleMonthly": "月结", @@ -101,41 +149,184 @@ "validation": { "shareRange": "占成比例须在 0–100 之间", "creditInvalid": "授信额度不能为负数", - "rebateLimitRange": "回水上限须在 0–1 之间(如 0.005 表示 0.5%)", - "defaultRebateRange": "默认玩家回水须在 0–1 之间", + "rebateLimitRange": "回水上限须在 0–100% 之间", + "defaultRebateRange": "默认玩家回水须在 0–100% 之间", "defaultExceedsLimit": "默认玩家回水不能超过回水上限" - } + }, + "riskTags": "风控标签", + "riskTagsPlaceholder": "逗号分隔,如 overdue, high_turnover", + "saveSuccess": "占成与授信已保存" }, "settlementBills": { "title": "代理账单", - "description": "账期关闭后生成的玩家/代理账单", + "description": "账期关闭后生成的玩家/代理账单;默认展示最近一期。", + "periodLabel": "查看账期", + "periodPlaceholder": "选择账期", + "allPeriods": "全部账期", + "filteredByPeriodRange": "账期 {{range}} 的账单", + "emptyNoPeriodsManage": "尚无账期与账单。请在下方「账期管理」点「本周」开期,到期后关账,账单会自动出现在这里。", + "emptyNoPeriodsAgent": "尚无账单。账期由上级或平台关账后自动生成,无需您手动筛选时间。", + "emptyNoClosed": "当前没有已关账的账期,账单尚未生成。请等待负责人关账后再查看。", + "typePlayer": "玩家账单", + "typeAgent": "代理层级账单", + "platform": "平台", + "filteredByPeriod": "当前仅显示账期 #{{id}} 的账单", + "clearPeriodFilter": "显示全部账期", "columns": { "id": "ID", + "period": "账期", "type": "类型", + "party": "本方", + "counterparty": "对方", "net": "净额", "unpaid": "未结", - "status": "状态" + "status": "状态", + "grossWinLoss": "输赢" + }, + "detail": "详情", + "confirm": "确认账单", + "confirmed": "已确认", + "paymentAmount": "收付金额", + "recordPayment": "登记收付", + "paid": "已登记收付", + "subtreeSummary": "子树汇总", + "grossWinLoss": "输赢 (gross_win_loss)", + "rebateAmount": "回水", + "shareProfit": "占成利润", + "platformRounding": "平台尾差" + }, + "settlementReports": { + "title": "账期报表(信用占成盘)", + "description": "§21.12 报表集:玩家输赢、代理占成、回水、授信、未结/逾期与平台盈亏。", + "type": "报表类型", + "footnote": "本组报表为信用占成盘账期口径,与「佣金/回水」旧钱包报表不同。", + "noPeriodHint": "未选具体账期时使用近 7 日区间;平台盈亏需选择账期。", + "types": { + "summary": "摘要", + "player_win_loss": "玩家输赢", + "agent_share": "代理占成", + "rebate": "回水", + "credit": "授信", + "unpaid_bills": "未结账单", + "overdue": "逾期", + "platform_pnl": "平台盈亏", + "draw_period": "期号维度" + }, + "summary": { + "billCount": "账单数", + "totalNet": "净额合计", + "totalUnpaid": "未结合计", + "overdueCount": "逾期账单", + "platformRounding": "平台尾差合计" + }, + "rebate": { + "accrued": "应计", + "inBill": "已入账单", + "settled": "已结算", + "allocated": "已分摊" + }, + "credit": { + "agents": "代理授信", + "players": "玩家授信" + }, + "platformPnl": { + "periodRequired": "请选择具体账期后查看平台盈亏。", + "billNet": "平台账单净额", + "rounding": "尾差调整", + "shareProfit": "占成利润(元数据)" + }, + "columns": { + "player": "玩家", + "gameType": "玩法", + "grossWinLoss": "输赢", + "rebate": "回水", + "agentId": "代理 ID", + "count": "笔数", + "billId": "账单", + "billType": "类型", + "unpaid": "未结", + "status": "状态", + "overdueDays": "逾期天数", + "drawNo": "期号", + "code": "编码", + "name": "名称", + "creditLimit": "授信", + "allocated": "已下发", + "available": "可用", + "used": "已用", + "frozen": "冻结", + "rebateType": "回水类型", + "amount": "金额" } }, + "settlementPeriods": { + "title": "代理账期", + "manageTitle": "账期管理", + "manageHint": "平台或负责人开期并关账后,上方账单会自动生成。一般用快捷账期即可,无需手填时间。", + "presetHint": "快捷账期(推荐)", + "presetThisWeek": "本周", + "presetLastWeek": "上周", + "presetThisMonth": "本月", + "openWithPreset": "按上方时间开期", + "showAdvanced": "自定义起止时间", + "hideAdvanced": "收起自定义时间", + "range": "账期", + "statusOpen": "进行中", + "statusClosed": "已关账", + "empty": "尚无账期。点「本周」等快捷账期后开期,到期后在此关账。", + "start": "开始", + "end": "结束", + "status": "状态", + "open": "开期", + "close": "关账并生成账单", + "viewBills": "账单", + "opened": "账期已开启", + "closed": "账期已关账,账单已生成", + "openFailed": "开期失败", + "closeFailed": "关账失败", + "datesRequired": "请填写账期起止" + }, "lineProvision": { - "title": "开通代理线路", - "description": "一次创建站点、根代理与后台账号(site_code 与代理 code 一致)。", - "code": "站点 code", - "name": "线路名称", - "username": "代理账号", + "title": "创建一级代理", + "description": "将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水、结算周期。代理编码创建后不可修改。", + "siteCode": "接入站点", + "siteCodePlaceholder": "选择站点", + "siteRequired": "请选择接入站点", + "noUnboundSite": "暂无未绑定一级代理的站点", + "openIntegrationSites": "前往接入站点", + "code": "代理编码", + "name": "一级代理名称", + "username": "后台登录账号", "password": "初始密码", - "walletUrl": "钱包 API URL", - "submit": "开通线路", - "success": "线路已开通", - "secretsOnce": "密钥仅显示一次,请妥善保存", - "link": "开通线路" + "submit": "创建一级代理", + "success": "一级代理已创建", + "link": "创建一级代理" }, "noAccess": "您没有代理经营相关权限,请联系管理员开通。", "playersPanel": { "create": "创建玩家", "scopedTo": "直属玩家:{{agent}}", - "allUnderSite": "当前站点下可见玩家", - "filterHint": "可按上级代理查看其直属玩家。" + "allUnderSite": "当前一级代理线路下可见玩家", + "filterHint": "可按上级代理查看其直属玩家。", + "loginRequired": "请填写登录账号与初始密码", + "loginUsername": "登录账号", + "initialPassword": "初始密码", + "externalIdOptional": "外部 ID(可选)", + "externalIdHint": "留空则系统自动生成", + "creditLimit": "授信额度", + "rebateRate": "回水比例 (%)", + "rebateRateHint": "填写百分比,如 0.5 表示 0.5%", + "availableToGrant": "代理剩余可下发:{{amount}}", + "riskTags": "风控标签", + "riskTagsPlaceholder": "逗号分隔", + "fundingMode": "资金模式", + "authSource": "登录来源", + "rebateInherited": "继承代理默认回水", + "creditListHint": "信用占成盘:下列为玩家授信额度与可用信用,非主站钱包余额。", + "playerRef": "玩家标识", + "usernameNickname": "用户名 / 昵称", + "creditLimitAvailable": "授信 / 可用", + "createSuccessNative": "玩家 {{name}} 已创建,请使用彩票端 /login 登录" }, "delegation": { "title": "下放权限上限", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index be6713d..dd1a406 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -106,7 +106,8 @@ "next": "下一页" }, "states": { - "noData": "暂无数据", + "noResource": "暂无资源", + "noData": "暂无资源", "loading": "加载中…", "comingSoon": "功能开发中" }, @@ -162,6 +163,7 @@ "account": "账号设置", "integration": "接入配置", "agents": "代理线路", + "settlement_center": "结算中心", "config": "运营配置" }, "sidebar": { diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index 0e05a91..b57ee44 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -21,7 +21,7 @@ }, "hub": { "title": "运营配置总览", - "description": "按业务域进入玩法、赔率回水、奖池与限额配置。侧栏已提供直达入口,本页为汇总导航。", + "description": "按业务域进入玩法、赔率回水、奖池与限额配置;接入站点在侧栏「平台管理 → 接入配置」。", "playsTitle": "投注规则", "playsDesc": "玩法开关、限额与规则说明", "oddsTitle": "赔率与回水", @@ -84,10 +84,19 @@ "columns": { "code": "site_code", "name": "名称", + "currency": "币种", "status": "状态", + "lineRoot": "一级代理", "walletUrl": "钱包 API", + "h5Url": "彩票 H5", + "ssoSecret": "SSO 密钥", + "walletApiKey": "钱包密钥", "actions": "操作" }, + "lineRootBound": "已绑定", + "lineRootUnbound": "未绑定", + "secretNotConfigured": "尚未配置密钥", + "secretCopyRequiresManage": "需要接入站点管理权限才能复制密钥", "fields": { "code": "site_code", "name": "站点名称", diff --git a/src/i18n/locales/zh/dashboard.json b/src/i18n/locales/zh/dashboard.json index f328e5d..f0d7369 100644 --- a/src/i18n/locales/zh/dashboard.json +++ b/src/i18n/locales/zh/dashboard.json @@ -154,6 +154,31 @@ "riskMonitor": "风控监控", "systemSettings": "系统设置" }, + "agent": { + "title": "经营概览", + "subtitle": "本线路数据范围 · {{name}}", + "creditTitle": "授信额度", + "creditAvailable": "可下发 {{amount}}", + "creditAllocated": "已下发 {{amount}}", + "creditUsed": "已占用 {{amount}}", + "shareRate": "总占成 {{rate}}%", + "settlementCycle": "账期 {{cycle}}", + "teamTitle": "团队规模", + "directChildren": "直属下级代理", + "directPlayers": "直属玩家", + "subtreeAgents": "线路代理数", + "pendingBills": "待结代理账单", + "pendingUnpaid": "未结合计 {{amount}}", + "viewBills": "查看账单", + "viewLine": "代理线路", + "quickLinks": { + "tickets": "注单查询", + "players": "玩家管理", + "reports": "报表统计", + "agents": "下级代理", + "bills": "代理账单" + } + }, "warnings": { "drawPermission": "当前账号无开奖/仪表盘查看权限,财务与风控数据未返回。", "walletPermission": "当前账号无钱包对账查看权限,异常转账计数未返回。", diff --git a/src/i18n/locales/zh/jackpot.json b/src/i18n/locales/zh/jackpot.json index 4b69d64..d7268f8 100644 --- a/src/i18n/locales/zh/jackpot.json +++ b/src/i18n/locales/zh/jackpot.json @@ -31,12 +31,12 @@ "confirmAdjustmentTitle": "确认提交奖池余额调整?", "confirmAdjustmentDescription": "将写入调整流水并更新当前池余额,请确认金额与原因无误。", "recentAdjustments": "最近调整记录", - "contributionRate": "蓄水比例 0–1", - "contributionRatePlaceholder": "请输入贡献比例,如 0.02", + "contributionRate": "蓄水比例 (%)", + "contributionRatePlaceholder": "如 2 表示 2%", "triggerThreshold": "爆池阈值(最小单位)", "triggerThresholdPlaceholder": "请输入触发阈值", - "payoutRate": "爆池派彩比例 0–1", - "payoutRatePlaceholder": "请输入派彩比例,如 0.05", + "payoutRate": "爆池派彩比例 (%)", + "payoutRatePlaceholder": "如 5 表示 5%", "forceTriggerGap": "强制爆池间隔(已结算期数)", "forceTriggerGapPlaceholder": "请输入强制触发间隔期数", "minBetAmount": "最低下注额(最小单位)", diff --git a/src/i18n/locales/zh/players.json b/src/i18n/locales/zh/players.json index c4e4a72..2067d3c 100644 --- a/src/i18n/locales/zh/players.json +++ b/src/i18n/locales/zh/players.json @@ -8,6 +8,7 @@ "tabOverview": "概览", "tabTickets": "注单", "tabWalletTxns": "钱包流水", + "tabCreditLedger": "信用流水", "tabTransferOrders": "转账单", "profileSection": "基本资料", "walletsSection": "钱包余额", @@ -21,6 +22,11 @@ "searchPlaceholder": "按玩家 ID / 用户名 / 昵称搜索", "filterSite": "主站站点", "filterAllSites": "全部站点", + "scopeAllSites": "数据范围:全部站点玩家(超管)", + "scopeFilteredSite": "数据范围:主站 {{site}}", + "scopeAgentLine": "数据范围:{{site}} · 代理线「{{name}}」及下级玩家", + "scopeSingleSite": "数据范围:主站 {{site}}", + "scopeMultiSite": "数据范围:已绑定 {{count}} 个主站(可用筛选收窄)", "search": "搜索", "refresh": "刷新", "loadFailed": "加载玩家列表失败", @@ -49,6 +55,18 @@ "available": "可用", "status": "状态", "lastLogin": "最后登录", + "fundingMode": "资金模式", + "authSource": "登录来源", + "creditSection": "信用额度", + "usedCredit": "已用信用", + "fundingCredit": "信用盘", + "fundingWallet": "主站钱包", + "authMainSite": "主站 SSO", + "authNative": "彩票端", + "creditLimit": "授信额度", + "availableCredit": "可用信用", + "rebateRate": "回水", + "riskTags": "风控标签", "actions": "操作", "edit": "编辑", "freeze": "冻结", diff --git a/src/i18n/locales/zh/reports.json b/src/i18n/locales/zh/reports.json index d721c6d..4986972 100644 --- a/src/i18n/locales/zh/reports.json +++ b/src/i18n/locales/zh/reports.json @@ -225,10 +225,12 @@ "status": "状态", "createdAt": "创建时间" }, + "legacyTitle": "旧版钱包报表", "categories": { "all": "全部", "profit": "盈亏", "wallet": "资金", + "legacy": "旧版口径", "risk": "风控", "audit": "审计" }, @@ -297,7 +299,8 @@ }, "rebate_commission": { "title": "佣金/回水报表", - "summary": "按玩法与时间段汇总佣金、回水与配置命中情况。" + "summary": "按玩法与时间段汇总佣金、回水与配置命中情况。", + "disclaimer": "本报表为钱包盘「下注立减回水/佣金」口径,不属于信用占成盘账期结算。占成盘请使用「代理 → 代理账单」中的账期报表。" }, "admin_audit": { "title": "后台操作审计报表", diff --git a/src/i18n/locales/zh/settlementCenter.json b/src/i18n/locales/zh/settlementCenter.json new file mode 100644 index 0000000..35badb6 --- /dev/null +++ b/src/i18n/locales/zh/settlementCenter.json @@ -0,0 +1,203 @@ +{ + "title": "结算中心", + "header": { + "subtitle": "信用占成账务", + "statusRunning": "账期进行中", + "statusIdle": "等待开期", + "statusCompleted": "账期已结清" + }, + "subnav": { + "label": "结算中心导航" + }, + "nav": { + "aria": "结算中心导航", + "group": { + "hub": "工作台", + "finance": "账务", + "ledger": "账务流水", + "bills": "账单管理" + }, + "overview": "概览", + "periods": "账期管理", + "ledger": "账务流水", + "bills": "账单", + "creditLedger": "信用流水", + "playerBills": "玩家账单", + "agentBills": "代理账单", + "pendingConfirm": "待确认", + "awaitingPayment": "待收付", + "payments": "收付记录", + "adjustments": "调账 / 冲正", + "badDebt": "坏账核销", + "reports": "账期报表" + }, + "filters": { + "period": "账期范围", + "allPeriods": "全部账期", + "statusOpen": "进行中", + "statusClosed": "已关账", + "statusCompleted": "已结清" + }, + "overview": { + "pendingConfirm": "待确认", + "awaitingPayment": "待收付", + "totalUnpaid": "未结合计", + "openPeriod": "进行中账期", + "creditLedger": "信用流水(账期内)", + "shareLedger": "占成流水(账期内)", + "pipelineHint": "账单须关账后生成;下方为账期内实时流水笔数。" + }, + "ledger": { + "groupIntro": "账期内资金变动明细:信用占用、账单收付、调账与坏账。关账后生成的占成账单在「账单管理」。", + "paymentsIntro": "针对已确认账单的实收实付登记(payment_records),可在账单详情中操作,此处为账期汇总查询。", + "adjustmentsIntro": "账单补差、冲正等调账流水(settlement_adjustments)。", + "badDebtIntro": "坏账核销产生的调账流水,关联原账单。" + }, + "creditLedger": { + "intro": "账期内信用占用、账单收付、调账与坏账等流水;行内「⋯」可确认账单、登记收付、调账、冲正或坏账(需关联账单)。", + "columns": { + "txn": "流水号", + "player": "玩家", + "reason": "业务类型", + "ref": "关联", + "amount": "金额", + "channel": "渠道", + "status": "状态", + "time": "时间" + }, + "channelCredit": "信用盘", + "viewPlayer": "玩家详情", + "entryKind": { + "adjustment": "调账流水" + }, + "actions": { + "viewPlayer": "玩家详情", + "viewBill": "账单详情", + "confirm": "确认账单", + "confirmDesc": "确认后账单进入待收付状态。", + "payment": "登记收付", + "adjustment": "调账", + "reversal": "冲正", + "badDebt": "坏账核销" + }, + "reason": { + "payment_record": "账单收付", + "bet_hold": "下注占用", + "bet_hold_release": "占用释放", + "game_settlement_loss": "开奖结算扣款", + "settlement_confirm": "账期结算确认" + } + }, + "columns": { + "period": "账期", + "type": "类型", + "owner": "本方", + "counterparty": "对方", + "gross": "输赢", + "net": "净额", + "paid": "已收付", + "unpaid": "未结", + "status": "状态", + "billId": "账单 ID", + "payer": "付款方", + "payee": "收款方", + "amount": "金额", + "method": "方式", + "time": "时间", + "adjustmentType": "调账类型", + "originalBill": "原账单", + "reason": "原因", + "badDebtAmount": "核销金额" + }, + "billStatus": { + "pending_confirm": "待确认", + "confirmed": "已确认", + "partial_paid": "部分结清", + "settled": "已结清", + "overdue": "逾期", + "reversed": "已冲正" + }, + "billType": { + "adjustment": "补差单", + "reversal": "冲正单", + "badDebt": "坏账核销" + }, + "adjustmentType": { + "adjustment": "补差", + "reversal": "冲正", + "bad_debt": "坏账核销" + }, + "actions": { + "detail": "详情", + "viewBill": "查看账单", + "billDetail": "账单详情" + }, + "ledgerPanel": { + "search": "搜索", + "searchBtn": "搜索", + "reset": "重置筛选", + "refresh": "刷新当前页", + "filterAll": "不限", + "playerAccount": "玩家账号", + "playerAccountPh": "用户名 / 站点玩家 ID", + "playerId": "玩家 ID", + "optional": "可选", + "billStatus": "账单状态", + "dateRange": "时间范围", + "rowPosted": "已记账", + "category": { + "all": "全部", + "credit": "信用占用", + "payment": "收付", + "adjustment": "调账 / 冲正", + "badDebt": "坏账", + "actionable": "待操作" + } + }, + "billsPanel": { + "intro": "关账后生成的占成账单;可按类型与状态筛选,打开详情进行确认与收付。", + "category": { + "all": "全部", + "player": "玩家账单", + "agent": "代理账单", + "pendingConfirm": "待确认", + "awaitingPayment": "待收付" + } + }, + "panels": { + "overview": { "title": "结算概览" }, + "ledger": { "title": "账务流水" }, + "bills": { "title": "账单" }, + "creditLedger": { "title": "信用流水" }, + "playerBills": { "title": "玩家账单" }, + "agentBills": { "title": "代理账单" }, + "pendingConfirm": { "title": "待确认账单" }, + "awaiting": { "title": "待收付账单" }, + "payments": { "title": "收付记录" }, + "adjustments": { "title": "调账 / 冲正" }, + "reports": { "title": "账期报表" }, + "badDebt": { "title": "坏账核销" } + }, + "empty": { + "noSite": "请选择接入站点。", + "noPeriods": "请先在「账期管理」开期并关账。", + "noClosed": "请先关账生成账单。", + "noBadDebt": "暂无坏账核销记录。", + "noCreditLedger": "所选账期内暂无信用流水。请确认信用盘玩家已下注且账期时间范围正确。", + "billsNeedClose": "占成账单须先关账才会出现;若上方「信用流水」有数据而账单为空,请完成开奖结算后执行关账。" + }, + "periods": { + "loadFailed": "账期列表加载失败" + }, + "toast": { + "periodClosed": "账期已关账", + "periodClosedUnsettled": "账期已关账;仍有 {{count}} 笔注单未结算,请尽快处理。" + }, + "errors": { + "loadBills": "账单加载失败", + "loadPayments": "收付记录加载失败", + "loadAdjustments": "调账记录加载失败", + "loadBadDebt": "坏账记录加载失败", + "loadCreditLedger": "信用流水加载失败" + } +} diff --git a/src/i18n/locales/zh/wallet.json b/src/i18n/locales/zh/wallet.json index c685279..bd73df0 100644 --- a/src/i18n/locales/zh/wallet.json +++ b/src/i18n/locales/zh/wallet.json @@ -1,8 +1,14 @@ { "title": "钱包", "subnavLabel": "钱包子页", - "subnavTransactions": "钱包流水", - "subnavTransferOrders": "转账单", + "subnavTransactions": "主站钱包流水", + "subnavTransferOrders": "主站转账单", + "scopeHint": "本模块为主站钱包模式:钱包流水与主站转账单。信用盘玩家的下注占用、结算记账请查看", + "scopeHintSettlementLink": "结算中心 → 信用流水", + "scopeHintSettlement": "结算中心 → 信用流水", + "ledgerChannel": "账本", + "ledgerCredit": "信用流水", + "ledgerWallet": "钱包流水", "subnavPlayerWallet": "玩家钱包", "noPermission": "当前账号无访问该页的权限", "copySuccess": "{{label}}已复制到剪贴板", diff --git a/src/lib/admin-locale.ts b/src/lib/admin-locale.ts index 6417f6d..e1a3d8d 100644 --- a/src/lib/admin-locale.ts +++ b/src/lib/admin-locale.ts @@ -40,7 +40,8 @@ function requestLocale(): AdminApiLocale { return primary; } } - return "en"; + // 与 i18n `ADMIN_DEFAULT_LANGUAGE`(zh)一致,避免界面中文而校验仍为英文 + return "zh"; } /** 当前生效的语言(与即将发出的 API 头一致) */ @@ -112,6 +113,8 @@ export function hydrateAdminUiLocale(): AdminApiLocale | null { return stored; } + setAdminRequestLocale("zh"); + return null; } diff --git a/src/lib/admin-nav-label.ts b/src/lib/admin-nav-label.ts index 7558640..c21ca11 100644 --- a/src/lib/admin-nav-label.ts +++ b/src/lib/admin-nav-label.ts @@ -15,6 +15,7 @@ const NAV_SEGMENT_I18N_KEYS: Record = { risk_cap: "risk_cap", risk: "risk", settlement: "settlement", + settlement_center: "settlement_center", reconcile: "reconcile", reports: "reports", tickets: "tickets", diff --git a/src/lib/admin-page-title.ts b/src/lib/admin-page-title.ts index f3de508..181b440 100644 --- a/src/lib/admin-page-title.ts +++ b/src/lib/admin-page-title.ts @@ -18,9 +18,10 @@ const EXACT_ROUTES: Record = { "/admin/agents": { ns: "agents", key: "title" }, "/admin/agents/list": { ns: "agents", key: "directoryTitle" }, "/admin/agents/provision": { ns: "agents", key: "subnav.provision" }, - "/admin/agents/sites": { ns: "agents", key: "sitesTitle" }, - "/admin/agents/settlement-bills": { ns: "agents", key: "subnav.settlementBills" }, - "/admin/config/integration-sites": { ns: "agents", key: "sitesTitle" }, + "/admin/agents/sites": { ns: "config", key: "integrationSites.title" }, + "/admin/settlement-center": { ns: "settlementCenter", key: "title" }, + "/admin/agents/settlement-bills": { ns: "settlementCenter", key: "title" }, + "/admin/config/integration-sites": { ns: "config", key: "integrationSites.title" }, "/admin/wallet": { ns: "wallet", key: "title" }, "/admin/wallet/transactions": { ns: "wallet", key: "walletTransactions" }, "/admin/wallet/transfer-orders": { ns: "wallet", key: "transferOrders" }, diff --git a/src/lib/admin-permission-packages.ts b/src/lib/admin-permission-packages.ts index a4e4651..382f501 100644 --- a/src/lib/admin-permission-packages.ts +++ b/src/lib/admin-permission-packages.ts @@ -98,6 +98,10 @@ export const ADMIN_PERMISSION_PACKAGES: Record { key: "view", label: "查看", slugs: ["prd.integration.view"] }, { key: "manage", label: "管理", slugs: ["prd.integration.manage"] }, ], + settlement_agent: [ + { key: "view", label: "查看", slugs: ["prd.settlement.agent.view"] }, + { key: "manage", label: "管理", slugs: ["prd.settlement.agent.manage"] }, + ], jackpot: [ { key: "view", label: "查看", slugs: ["prd.jackpot.view"] }, { key: "manage", label: "管理", slugs: ["prd.jackpot.manage"] }, diff --git a/src/lib/admin-player-display.ts b/src/lib/admin-player-display.ts new file mode 100644 index 0000000..5a28e6c --- /dev/null +++ b/src/lib/admin-player-display.ts @@ -0,0 +1,90 @@ +import { formatAdminCreditMajor } from "@/lib/money"; +import type { AdminPlayerRow, AdminPlayerWalletRow } from "@/types/api/admin-player"; + +/** 信用额度与代理 profile 一致,按主货币整数展示(非 wallet minor)。 */ +export function formatPlayerCreditAmount(amount: number, currencyCode: string): string { + return formatAdminCreditMajor(amount, currencyCode.trim() || "NPR"); +} + +export function preferredDisplayWallet(row: AdminPlayerRow): AdminPlayerWalletRow | null { + const { wallets, default_currency } = row; + if (wallets.length === 0) { + return null; + } + const code = default_currency.trim().toUpperCase(); + return wallets.find((w) => w.currency_code.toUpperCase() === code) ?? wallets[0]; +} + +/** 与后端 {@see PlayerFundingMode::usesCredit} 一致。 */ +export function isCreditFundingPlayer( + row: Pick, +): boolean { + if (row.uses_credit === true) { + return true; + } + if (row.funding_mode === "credit") { + return true; + } + return row.funding_mode !== "wallet" && (row.available_credit != null || row.credit_limit != null); +} + +/** 主站 ↔ 彩票钱包划转单,仅钱包模式玩家适用。 */ +export function playerShowsTransferOrders( + row: Pick, +): boolean { + return !isCreditFundingPlayer(row); +} + +export function playerBalanceCells( + row: AdminPlayerRow, + formatWalletMinor: (minor: number, currency: string) => string, +): { balance: string; available: string; balanceLabel: "credit" | "wallet" } { + if (isCreditFundingPlayer(row)) { + const ccy = row.default_currency; + return { + balance: formatPlayerCreditAmount(row.credit_limit ?? 0, ccy), + available: formatPlayerCreditAmount(row.available_credit ?? 0, ccy), + balanceLabel: "credit", + }; + } + const displayWallet = preferredDisplayWallet(row); + if (!displayWallet) { + const ccy = row.default_currency; + return { + balance: formatWalletMinor(0, ccy), + available: formatWalletMinor(0, ccy), + balanceLabel: "wallet", + }; + } + return { + balance: formatWalletMinor(displayWallet.balance, displayWallet.currency_code), + available: formatWalletMinor(displayWallet.available_balance, displayWallet.currency_code), + balanceLabel: "wallet", + }; +} + +export function playerFundingModeLabel( + row: Pick, + t: (key: string, opts?: { defaultValue?: string }) => string, +): string { + if (row.funding_mode === "credit") { + return t("players:fundingCredit", { defaultValue: "信用" }); + } + if (row.funding_mode === "wallet") { + return t("players:fundingWallet", { defaultValue: "钱包" }); + } + return row.funding_mode ?? "—"; +} + +export function playerAuthSourceLabel( + row: Pick, + t: (key: string, opts?: { defaultValue?: string }) => string, +): string { + if (row.auth_source === "main_site_sso") { + return t("players:authMainSite", { defaultValue: "主站 SSO" }); + } + if (row.auth_source === "lottery_native") { + return t("players:authNative", { defaultValue: "彩票端" }); + } + return row.auth_source ?? "—"; +} diff --git a/src/lib/admin-prd.ts b/src/lib/admin-prd.ts index 684bdd7..d6e36d2 100644 --- a/src/lib/admin-prd.ts +++ b/src/lib/admin-prd.ts @@ -129,6 +129,16 @@ export const PRD_PAYOUT_ACCESS_ANY = [ PRD_PAYOUT_MANAGE, ] as const; +/** 期号内「资金」Tab(开奖管理或财务/报表视角) */ +export const PRD_DRAW_FINANCE_ACCESS_ANY = [ + PRD_DRAW_RESULT_MANAGE, + PRD_PAYOUT_VIEW, + PRD_PAYOUT_MANAGE, + PRD_PAYOUT_REVIEW, + PRD_REPORT_VIEW, + PRD_USERS_VIEW_FINANCE, +] as const; + /** 接入站点配置页 */ export const PRD_INTEGRATION_ACCESS_ANY = [PRD_INTEGRATION_VIEW, PRD_INTEGRATION_MANAGE] as const; @@ -143,13 +153,11 @@ export const PRD_AGENT_USER_MANAGE = "prd.agent.user.manage" as const; export const PRD_AGENT_LINE_PROVISION = "prd.agent-line.provision" as const; export const PRD_AGENT_PROFILE_MANAGE = "prd.agent.profile.manage" as const; -/** 代理线路内「站点列表」入口(接入权限或线路经营权限) */ -export const PRD_AGENT_SITES_ACCESS_ANY = [ - ...PRD_INTEGRATION_ACCESS_ANY, - PRD_AGENT_LINE_PROVISION, - PRD_AGENT_MANAGE, - PRD_AGENT_VIEW, -] as const; +/** + * 运营配置「接入站点」入口:仅平台侧技术配置。 + * 不含 prd.agent.view|manage,避免经营代理看到接入密钥。 + */ +export const PRD_AGENT_SITES_ACCESS_ANY = [...PRD_INTEGRATION_ACCESS_ANY] as const; export const PRD_AGENTS_ACCESS_ANY = [ PRD_AGENT_VIEW, @@ -161,14 +169,15 @@ export const PRD_AGENTS_ACCESS_ANY = [ PRD_AGENT_PROFILE_MANAGE, ] as const; -export const PRD_AGENT_LINE_PROVISION_ACCESS_ANY = [ - PRD_AGENT_LINE_PROVISION, - PRD_AGENT_MANAGE, -] as const; +/** 仅平台开通新线路;经营代理的 prd.agent.manage 不含开通线路页。 */ +export const PRD_AGENT_LINE_PROVISION_ACCESS_ANY = [PRD_AGENT_LINE_PROVISION] as const; + +export const PRD_SETTLEMENT_AGENT_VIEW = "prd.settlement.agent.view" as const; +export const PRD_SETTLEMENT_AGENT_MANAGE = "prd.settlement.agent.manage" as const; export const PRD_SETTLEMENT_AGENT_ACCESS_ANY = [ - "prd.settlement.agent.view", - "prd.settlement.agent.manage", + PRD_SETTLEMENT_AGENT_VIEW, + PRD_SETTLEMENT_AGENT_MANAGE, ] as const; /** 侧栏「代理线路」分组:含经营、开通、接入配置、代理账单任一权限即可见入口 */ @@ -176,6 +185,6 @@ export const PRD_AGENT_HUB_ACCESS_ANY = [ ...PRD_AGENTS_ACCESS_ANY, PRD_AGENT_LINE_PROVISION, ...PRD_INTEGRATION_ACCESS_ANY, - "prd.settlement.agent.view", - "prd.settlement.agent.manage", + PRD_SETTLEMENT_AGENT_VIEW, + PRD_SETTLEMENT_AGENT_MANAGE, ] as const; diff --git a/src/lib/admin-rate-percent.ts b/src/lib/admin-rate-percent.ts new file mode 100644 index 0000000..bb0bd80 --- /dev/null +++ b/src/lib/admin-rate-percent.ts @@ -0,0 +1,59 @@ +/** API / 存储用小数比例(0–1);后台表单统一用百分比(0–100)展示与录入。 */ + +const RATIO_TO_PERCENT = 100; + +/** 表单展示:去掉无意义的尾随 0(20.00 → "20",0.50 → "0.5") */ +function formatPercentNumber(value: number, decimals = 2): string { + const scaled = Math.round(value * 10 ** decimals) / 10 ** decimals; + return String(scaled); +} + +/** API 已是 0–100 的占成等,直接格式化 */ +export function percentValueToUi( + percent: number | string | null | undefined, + decimals = 2, +): string { + const n = typeof percent === "string" ? Number.parseFloat(percent) : percent; + if (n == null || !Number.isFinite(n)) { + return "0"; + } + return formatPercentNumber(n, decimals); +} + +/** 存库小数 0–1 → 表单百分比,如 0.2 → "20",0.005 → "0.5" */ +export function ratioToPercentUi( + ratio: number | string | null | undefined, + decimals = 2, +): string { + const n = typeof ratio === "string" ? Number.parseFloat(ratio) : ratio; + if (n == null || !Number.isFinite(n)) { + return "0"; + } + return formatPercentNumber(n * RATIO_TO_PERCENT, decimals); +} + +/** "0.5" → 0.005 */ +export function percentUiToRatio(percent: number | string | null | undefined): number { + const n = typeof percent === "string" ? Number.parseFloat(percent.trim()) : percent; + if (n == null || !Number.isFinite(n)) { + return 0; + } + return n / RATIO_TO_PERCENT; +} + +export function parsePercentUi(value: string): number | null { + const trimmed = value.trim(); + if (trimmed === "") { + return null; + } + const n = Number.parseFloat(trimmed); + return Number.isFinite(n) ? n : null; +} + +/** 只读展示:0.005 → "0.50%" */ +export function formatRatioAsPercent( + ratio: number | null | undefined, + decimals = 2, +): string { + return `${ratioToPercentUi(ratio, decimals)}%`; +} diff --git a/src/lib/agent-default-role-permissions.ts b/src/lib/agent-default-role-permissions.ts new file mode 100644 index 0000000..a6752cd --- /dev/null +++ b/src/lib/agent-default-role-permissions.ts @@ -0,0 +1,54 @@ +/** + * 与 Laravel 平台角色模板对齐;经营主账号只用「平台角色管理 → 代理」(slug=agent)。 + * 线路内「角色」页用于子账号自定义角色,不再使用 agent_owner_* 默认包。 + */ +export const AGENT_OWNER_BASE_SLUGS = [ + "prd.dashboard.view", + "prd.agent.view", + "prd.agent.role.view", + "prd.agent.user.view", + "prd.tickets.view", + "prd.report.view", + "prd.settlement.agent.view", +] as const; + +export const AGENT_OWNER_LINE_ROOT_EXTRA_SLUGS = [ + "prd.agent.manage", + "prd.agent.profile.manage", + "prd.agent.role.manage", + "prd.agent.user.manage", + "prd.users.manage", + "prd.users.view_finance", + "prd.users.view_cs", + "prd.settlement.agent.manage", +] as const; + +/** 代理模块内可下放/勾选的产品权限分组(不含平台专属:对账、赔率、接入站点等)。 */ +export const AGENT_PERMISSION_PACKAGES: Record< + string, + { key: string; label: string; slugs: string[] }[] +> = { + dashboard: [{ key: "view", label: "查看", slugs: ["prd.dashboard.view"] }], + agents: [ + { key: "node_view", label: "线路·查看", slugs: ["prd.agent.view"] }, + { key: "node_manage", label: "线路·管理", slugs: ["prd.agent.manage", "prd.agent.profile.manage"] }, + { key: "role_view", label: "角色·查看", slugs: ["prd.agent.role.view"] }, + { key: "role_manage", label: "角色·管理", slugs: ["prd.agent.role.manage"] }, + { key: "user_view", label: "账号·查看", slugs: ["prd.agent.user.view"] }, + { key: "user_manage", label: "账号·管理", slugs: ["prd.agent.user.manage"] }, + ], + players: [ + { + key: "view", + label: "查看", + slugs: ["prd.users.view_finance", "prd.users.view_cs"], + }, + { key: "manage", label: "管理", slugs: ["prd.users.manage"] }, + ], + tickets: [{ key: "view", label: "查看", slugs: ["prd.tickets.view"] }], + reports: [{ key: "view", label: "查看", slugs: ["prd.report.view"] }], + settlement_agent: [ + { key: "view", label: "账单·查看", slugs: ["prd.settlement.agent.view"] }, + { key: "manage", label: "账单·管理", slugs: ["prd.settlement.agent.manage"] }, + ], +}; diff --git a/src/lib/agent-settlement-period-range.ts b/src/lib/agent-settlement-period-range.ts new file mode 100644 index 0000000..3d4b0cc --- /dev/null +++ b/src/lib/agent-settlement-period-range.ts @@ -0,0 +1,113 @@ +import type { AgentSettlementCycle } from "@/lib/agent-settlement-cycle"; + +export type SettlementPeriodPresetKey = "this_week" | "last_week" | "this_month"; + +/** `datetime-local` 控件取值格式 */ +export function toDateTimeLocalValue(date: Date): string { + const pad = (n: number) => String(n).padStart(2, "0"); + + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; +} + +function startOfDay(date: Date): Date { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + + return d; +} + +function endOfDay(date: Date): Date { + const d = new Date(date); + d.setHours(23, 59, 0, 0); + + return d; +} + +/** 周一为一周起始(与产品文档「周结」一致) */ +function startOfWeekMonday(date: Date): Date { + const d = startOfDay(date); + const day = d.getDay(); + const diff = day === 0 ? -6 : 1 - day; + d.setDate(d.getDate() + diff); + + return d; +} + +function addDays(date: Date, days: number): Date { + const d = new Date(date); + d.setDate(d.getDate() + days); + + return d; +} + +function startOfMonth(date: Date): Date { + const d = startOfDay(date); + d.setDate(1); + + return d; +} + +function endOfMonth(date: Date): Date { + const d = startOfDay(date); + d.setMonth(d.getMonth() + 1); + d.setDate(0); + + return endOfDay(d); +} + +export function settlementPeriodPresetRange( + key: SettlementPeriodPresetKey, + now: Date = new Date(), +): { period_start: string; period_end: string } { + switch (key) { + case "this_week": { + const start = startOfWeekMonday(now); + const end = endOfDay(addDays(start, 6)); + + return { + period_start: toDateTimeLocalValue(start), + period_end: toDateTimeLocalValue(end), + }; + } + case "last_week": { + const thisStart = startOfWeekMonday(now); + const start = addDays(thisStart, -7); + const end = endOfDay(addDays(start, 6)); + + return { + period_start: toDateTimeLocalValue(start), + period_end: toDateTimeLocalValue(end), + }; + } + case "this_month": { + const start = startOfMonth(now); + const end = endOfMonth(now); + + return { + period_start: toDateTimeLocalValue(start), + period_end: toDateTimeLocalValue(end), + }; + } + } +} + +/** 按代理结算周期推荐默认快捷开期(周结优先) */ +export function defaultSettlementPeriodPreset( + cycle: AgentSettlementCycle, +): SettlementPeriodPresetKey { + if (cycle === "monthly") { + return "this_month"; + } + + return "this_week"; +} + +export function formatSettlementPeriodSpan( + periodStart: string | undefined, + periodEnd: string | undefined, +): string { + const start = periodStart?.slice(0, 10) ?? "—"; + const end = periodEnd?.slice(0, 10) ?? "—"; + + return `${start} ~ ${end}`; +} diff --git a/src/lib/draw-access.ts b/src/lib/draw-access.ts new file mode 100644 index 0000000..8fa11ac --- /dev/null +++ b/src/lib/draw-access.ts @@ -0,0 +1,31 @@ +import { + PRD_DRAW_RESULT_MANAGE, + PRD_DRAW_RESULT_VIEW, + PRD_PAYOUT_MANAGE, + PRD_PAYOUT_REVIEW, + PRD_PAYOUT_VIEW, + PRD_REPORT_VIEW, + PRD_USERS_VIEW_FINANCE, +} from "@/lib/admin-prd"; +import { adminHasAnyPermission } from "@/lib/admin-permissions"; + +export function canManageDrawResults(permissions: readonly string[] | undefined): boolean { + return adminHasAnyPermission(permissions, [PRD_DRAW_RESULT_MANAGE]); +} + +export function canViewDrawFinance(permissions: readonly string[] | undefined): boolean { + return ( + canManageDrawResults(permissions) || + adminHasAnyPermission(permissions, [ + PRD_PAYOUT_VIEW, + PRD_PAYOUT_MANAGE, + PRD_PAYOUT_REVIEW, + PRD_REPORT_VIEW, + PRD_USERS_VIEW_FINANCE, + ]) + ); +} + +export function canViewDrawResults(permissions: readonly string[] | undefined): boolean { + return adminHasAnyPermission(permissions, [PRD_DRAW_RESULT_VIEW, PRD_DRAW_RESULT_MANAGE]); +} diff --git a/src/lib/money.ts b/src/lib/money.ts index c5ae68a..da4c4bf 100644 --- a/src/lib/money.ts +++ b/src/lib/money.ts @@ -44,6 +44,44 @@ export function formatAdminMinorUnits( })}`; } +/** + * 信用占成盘授信/已下发/可用额度:库内为主货币整数(与后台录入一致),展示带小数位。 + */ +export function formatAdminCreditMajor( + major: number, + currencyCode = "NPR", + decimalPlaces?: number, +): string { + const safeMajor = Number.isFinite(major) ? major : 0; + const resolvedDecimalPlaces = + typeof decimalPlaces === "number" && Number.isFinite(decimalPlaces) && decimalPlaces >= 0 + ? decimalPlaces + : getAdminCurrencyDecimalPlaces(currencyCode); + + return `${currencyCode} ${safeMajor.toLocaleString(undefined, { + minimumFractionDigits: resolvedDecimalPlaces, + maximumFractionDigits: resolvedDecimalPlaces, + })}`; +} + +/** 授信额度展示(无币种前缀,用于代理概要卡片等)。 */ +export function formatAdminCreditMajorDecimal( + major: number, + currencyCode = "NPR", + decimalPlaces?: number, +): string { + const safeMajor = Number.isFinite(major) ? major : 0; + const resolvedDecimalPlaces = + typeof decimalPlaces === "number" && Number.isFinite(decimalPlaces) && decimalPlaces >= 0 + ? decimalPlaces + : getAdminCurrencyDecimalPlaces(currencyCode); + + return safeMajor.toLocaleString(undefined, { + minimumFractionDigits: resolvedDecimalPlaces, + maximumFractionDigits: resolvedDecimalPlaces, + }); +} + export function formatAdminMinorDecimal( minor: number, currencyCode = "NPR", diff --git a/src/lib/page-metadata.ts b/src/lib/page-metadata.ts index ba69020..caffebc 100644 --- a/src/lib/page-metadata.ts +++ b/src/lib/page-metadata.ts @@ -16,6 +16,7 @@ import enCommon from "@/i18n/locales/en/common.json"; import enTickets from "@/i18n/locales/en/tickets.json"; import enWallet from "@/i18n/locales/en/wallet.json"; import enAgents from "@/i18n/locales/en/agents.json"; +import enSettlementCenter from "@/i18n/locales/en/settlementCenter.json"; const EN_FLAT: Record> = { dashboard: enDashboard, @@ -35,6 +36,7 @@ const EN_FLAT: Record> = { common: enCommon, auth: enAuth, agents: enAgents, + settlementCenter: enSettlementCenter, }; function getByPath(obj: Record, path: string): string | undefined { diff --git a/src/lib/platform-system-roles.ts b/src/lib/platform-system-roles.ts new file mode 100644 index 0000000..f1b847c --- /dev/null +++ b/src/lib/platform-system-roles.ts @@ -0,0 +1,12 @@ +import type { AdminRoleRow } from "@/types/api/index"; + +export const PLATFORM_SUPER_ADMIN_SLUG = "super_admin"; +export const PLATFORM_AGENT_SLUG = "agent"; + +export function isPlatformFixedRole(role: Pick): boolean { + return role.slug === PLATFORM_SUPER_ADMIN_SLUG || role.slug === PLATFORM_AGENT_SLUG; +} + +export function isPlatformSuperAdminRole(role: Pick): boolean { + return role.slug === PLATFORM_SUPER_ADMIN_SLUG; +} diff --git a/src/lib/player-funding.ts b/src/lib/player-funding.ts new file mode 100644 index 0000000..2ab5e2d --- /dev/null +++ b/src/lib/player-funding.ts @@ -0,0 +1,13 @@ +/** + * 玩家资金模式:统一从 {@see admin-player-display} 导出,避免各页重复判断。 + */ +export { + isCreditFundingPlayer, + isCreditFundingPlayer as playerUsesCredit, + playerAuthSourceLabel, + playerBalanceCells, + playerFundingModeLabel, + playerShowsTransferOrders, + formatPlayerCreditAmount, + preferredDisplayWallet, +} from "@/lib/admin-player-display"; diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index 7db1ae9..b4fd4d9 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -10,6 +10,7 @@ import { Network, Scale, ScrollText, + Receipt, Settings, ShieldAlert, ShieldCheck, @@ -37,6 +38,7 @@ export const adminNavIconBySegment: Record reports: FileSpreadsheet, risk: ShieldAlert, settlement: Landmark, + settlement_center: Receipt, reconcile: Scale, audit: ScrollText, admin_users: ShieldCheck, diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index 8187394..63e52d3 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -23,6 +23,7 @@ export type AdminNavSegment = | "risk" | "settings" | "settlement" + | "settlement_center" | "reconcile" | "audit" | "admin_users" diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx index e6e8ceb..61954ef 100644 --- a/src/modules/admin-roles/admin-roles-console.tsx +++ b/src/modules/admin-roles/admin-roles-console.tsx @@ -13,11 +13,12 @@ import { deleteAdminRole, getAdminRoles, getAdminUserPermissionCatalog, - postAdminRole, putAdminRole, putAdminRolePermissions, } from "@/api/admin-users"; +import { isPlatformFixedRole, isPlatformSuperAdminRole } from "@/lib/platform-system-roles"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; +import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { AdminPermissionPackageSelector } from "@/components/admin/admin-permission-package-selector"; import { Badge } from "@/components/ui/badge"; @@ -77,7 +78,6 @@ export function AdminRolesConsole(): React.ReactElement { const [roleSaving, setRoleSaving] = useState(false); const [roleDialogOpen, setRoleDialogOpen] = useState(false); - const [roleMode, setRoleMode] = useState<"create" | "edit">("create"); const [editingRoleId, setEditingRoleId] = useState(null); const [roleSlug, setRoleSlug] = useState(""); const [roleName, setRoleName] = useState(""); @@ -116,18 +116,10 @@ export function AdminRolesConsole(): React.ReactElement { void load(); }, []); - function openCreateRole(): void { - setRoleMode("create"); - setEditingRoleId(null); - setRoleSlug(""); - setRoleName(""); - setRoleDescription(""); - setRoleStatus(1); - setRoleDialogOpen(true); - } - function openEditRole(role: AdminRoleRow): void { - setRoleMode("edit"); + if (isPlatformSuperAdminRole(role)) { + return; + } setEditingRoleId(role.id); setRoleSlug(role.slug); setRoleName(role.name); @@ -180,39 +172,24 @@ export function AdminRolesConsole(): React.ReactElement { async function submitRole(): Promise { const name = roleName.trim(); const slug = roleSlug.trim().toLowerCase(); - if (name === "" || slug === "") { + if (name === "" || slug === "" || editingRoleId === null) { toast.error(t("roleFormRequired")); return; } setRoleFormSaving(true); try { - if (roleMode === "create") { - const created = await postAdminRole({ - slug, - name, - description: roleDescription.trim() === "" ? null : roleDescription.trim(), - status: roleStatus, - }); - setRoles((prev) => [...prev, created]); - setCatalog((prev) => (prev ? { ...prev, roles: [...prev.roles, created] } : prev)); - toast.success(t("roleCreateSuccess", { name: created.name })); - } else { - if (editingRoleId === null) { - return; - } - const updated = await putAdminRole(editingRoleId, { - slug, - name, - description: roleDescription.trim() === "" ? null : roleDescription.trim(), - status: roleStatus, - }); - setRoles((prev) => prev.map((role) => (role.id === updated.id ? updated : role))); - setCatalog((prev) => - prev ? { ...prev, roles: prev.roles.map((role) => (role.id === updated.id ? updated : role)) } : prev, - ); - toast.success(t("roleUpdateSuccess", { name: updated.name })); - } + const updated = await putAdminRole(editingRoleId, { + slug, + name, + description: roleDescription.trim() === "" ? null : roleDescription.trim(), + status: roleStatus, + }); + setRoles((prev) => prev.map((role) => (role.id === updated.id ? updated : role))); + setCatalog((prev) => + prev ? { ...prev, roles: prev.roles.map((role) => (role.id === updated.id ? updated : role)) } : prev, + ); + toast.success(t("roleUpdateSuccess", { name: updated.name })); handleRoleDialogOpenChange(false); } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : t("roleSaveFailed"); @@ -249,11 +226,6 @@ export function AdminRolesConsole(): React.ReactElement {
{t("roleListTitle", { defaultValue: "平台角色管理" })} - {canManageRoles ? ( - - ) : null}
+

+ {t("roleListHint", { + defaultValue: "平台仅保留「超级管理员」与「代理」两个内置角色;超级管理员自动拥有全部权限。", + })} +

{err ?

{err}

: null}
@@ -286,13 +263,13 @@ export function AdminRolesConsole(): React.ReactElement { {loading && roles.length === 0 ? ( ) : roles.length === 0 ? ( - - - {t("states.noData", { ns: "common" })} - - + ) : ( - roles.map((role) => ( + roles.map((role) => { + const fixedRole = isPlatformFixedRole(role); + const superAdminRole = isPlatformSuperAdminRole(role); + + return ( {role.id} @@ -324,12 +301,14 @@ export function AdminRolesConsole(): React.ReactElement { key: "permissions", label: t("roleActions.permissions"), icon: KeyRound, + disabled: superAdminRole, onClick: () => openRolePermissionEditor(role), }, { key: "edit", label: t("actions.edit"), icon: Pencil, + disabled: superAdminRole, onClick: () => openEditRole(role), }, { @@ -337,7 +316,7 @@ export function AdminRolesConsole(): React.ReactElement { label: t("actions.delete"), icon: Trash2, destructive: true, - disabled: role.is_system || role.user_count > 0, + disabled: fixedRole || role.user_count > 0, onClick: () => setRoleDeleteTarget(role), }, ]} @@ -347,7 +326,8 @@ export function AdminRolesConsole(): React.ReactElement { )} - )) + ); + }) )}
@@ -375,6 +355,10 @@ export function AdminRolesConsole(): React.ReactElement { onChange={setDraftRolePermissions} resolveGroupLabel={(key, fallback) => permissionGroupLabel(key, fallback, t)} resolvePackageLabel={(key, fallback) => permissionPackageLabel(key, fallback, t)} + helperText={t("rolePermissionDialog.packageHint", { + defaultValue: + "勾选左侧模块行仅授予「查看」;录入、封盘、开奖等管理操作请单独勾选「管理」。", + })} emptyText={t("states.noData", { ns: "common" })} heightClassName="h-[min(56vh,520px)]" /> @@ -406,9 +390,7 @@ export function AdminRolesConsole(): React.ReactElement { - - {roleMode === "create" ? t("roleDialog.createTitle") : t("roleDialog.editTitle")} - + {t("roleDialog.editTitle")} {t("roleDialog.description")}
@@ -418,7 +400,7 @@ export function AdminRolesConsole(): React.ReactElement { value={roleSlug} placeholder={t("roleDialog.slugPlaceholder")} onChange={(e) => setRoleSlug(e.target.value)} - disabled={roleMode === "edit"} + disabled />
@@ -462,10 +444,7 @@ export function AdminRolesConsole(): React.ReactElement { onClick={() => requestConfirm({ title: t("confirmSaveRoleTitle"), - description: - roleMode === "create" - ? t("confirmSaveRoleCreateDescription", { name: roleName || roleSlug || "—" }) - : t("confirmSaveRoleEditDescription", { name: roleName || "—" }), + description: t("confirmSaveRoleEditDescription", { name: roleName || "—" }), confirmLabel: t("confirm.confirmSave", { ns: "common" }), onConfirm: () => submitRole(), }) diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx index 56faf29..b108630 100644 --- a/src/modules/admin-users/admin-users-console.tsx +++ b/src/modules/admin-users/admin-users-console.tsx @@ -3,6 +3,7 @@ import { KeyRound, Pencil, Trash2 } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { useConfirmAction } from "@/hooks/use-confirm-action"; +import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options"; import { useExportLabels } from "@/hooks/use-export-labels"; import { useTranslation } from "react-i18next"; import { useAsyncEffect } from "@/hooks/use-async-effect"; @@ -18,6 +19,7 @@ import { putAdminUserRoles, } from "@/api/admin-users"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; +import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; @@ -36,6 +38,13 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; import { Table, @@ -49,7 +58,11 @@ import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { PRD_ADMIN_USER_MANAGE } from "@/lib/admin-prd"; import { cn } from "@/lib/utils"; import { useAdminProfile } from "@/stores/admin-session"; -import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index"; +import type { + AdminPermissionCatalogData, + AdminUserPermissionRow, + AdminUserSiteBinding, +} from "@/types/api/index"; import { LotteryApiBizError } from "@/types/api/errors"; export function AdminUsersConsole(): React.ReactElement { @@ -58,6 +71,17 @@ export function AdminUsersConsole(): React.ReactElement { const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const exportLabels = useExportLabels("adminUsers"); const profile = useAdminProfile(); + const { sites: hookSiteOptions } = useAdminSiteCodeOptions(); + const siteOptions = useMemo(() => { + if (hookSiteOptions.length > 0) { + return hookSiteOptions; + } + return (profile?.accessible_sites ?? []).map((site) => ({ + id: site.id, + code: site.code, + name: site.name, + })); + }, [hookSiteOptions, profile?.accessible_sites]); const canManageUsers = adminHasAnyPermission(profile?.permissions, [PRD_ADMIN_USER_MANAGE]); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(10); @@ -86,6 +110,8 @@ export function AdminUsersConsole(): React.ReactElement { const [formPassword, setFormPassword] = useState(""); const [formStatus, setFormStatus] = useState(0); const [formCreateRoles, setFormCreateRoles] = useState([]); + const [formAdminSiteId, setFormAdminSiteId] = useState(null); + const [roleEditSiteId, setRoleEditSiteId] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteBusy, setDeleteBusy] = useState(false); @@ -99,6 +125,39 @@ export function AdminUsersConsole(): React.ReactElement { [catalog], ); + const defaultSiteId = useMemo(() => siteOptions[0]?.id ?? null, [siteOptions]); + const roleEditSiteLabel = useMemo(() => { + const site = siteOptions.find((item) => item.id === roleEditSiteId); + return site ? `${site.name} (${site.code})` : null; + }, [roleEditSiteId, siteOptions]); + const formAdminSiteLabel = useMemo(() => { + const site = siteOptions.find((item) => item.id === formAdminSiteId); + return site ? `${site.name} (${site.code})` : null; + }, [formAdminSiteId, siteOptions]); + + useAsyncEffect(() => { + if (formAdminSiteId === null && defaultSiteId !== null && accountOpen && accountMode === "create") { + setFormAdminSiteId(defaultSiteId); + } + }, [accountOpen, accountMode, defaultSiteId, formAdminSiteId]); + + function formatSiteBindings(bindings: AdminUserSiteBinding[] | undefined): string { + if (!bindings || bindings.length === 0) { + return ""; + } + return bindings + .map((b) => `${b.site_code}${b.role_slugs.length > 0 ? ` (${b.role_slugs.length})` : ""}`) + .join(", "); + } + + function rolesForSite(bindings: AdminUserSiteBinding[] | undefined, siteId: number | null): string[] { + if (siteId === null) { + return []; + } + const match = bindings?.find((b) => b.site_id === siteId); + return match ? [...match.role_slugs].sort() : []; + } + const load = useCallback(async () => { setLoading(true); setErr(null); @@ -149,8 +208,16 @@ export function AdminUsersConsole(): React.ReactElement { } function openPermissionEditor(row: AdminUserPermissionRow): void { + const bindings = row.site_bindings ?? []; + const initialSiteId = + bindings[0]?.site_id ?? defaultSiteId ?? siteOptions[0]?.id ?? null; setSelectedId(row.id); - setDraftRoles([...row.roles].sort()); + setRoleEditSiteId(initialSiteId); + setDraftRoles( + rolesForSite(bindings, initialSiteId).length > 0 + ? rolesForSite(bindings, initialSiteId) + : [...row.roles].sort(), + ); setPermissionOpen(true); } @@ -158,6 +225,7 @@ export function AdminUsersConsole(): React.ReactElement { setPermissionOpen(open); if (!open) { setSelectedId(null); + setRoleEditSiteId(null); } } @@ -170,6 +238,7 @@ export function AdminUsersConsole(): React.ReactElement { setFormPassword(""); setFormStatus(0); setFormCreateRoles([]); + setFormAdminSiteId(defaultSiteId); setAccountOpen(true); } @@ -205,6 +274,10 @@ export function AdminUsersConsole(): React.ReactElement { toast.error(t("roleRequired")); return; } + if (accountMode === "create" && (formAdminSiteId === null || formAdminSiteId <= 0)) { + toast.error(t("siteRequired")); + return; + } setAccountSaving(true); try { @@ -224,6 +297,7 @@ export function AdminUsersConsole(): React.ReactElement { email: formEmail.trim() === "" ? null : formEmail.trim(), password: formPassword, status: formStatus, + admin_site_id: formAdminSiteId as number, role_slugs: formCreateRoles, }); setItems((prev) => [created, ...prev]); @@ -265,9 +339,16 @@ export function AdminUsersConsole(): React.ReactElement { if (!selectedUser) { return; } + if (roleEditSiteId === null || roleEditSiteId <= 0) { + toast.error(t("siteRequired")); + return; + } setSavingRoles(true); try { - const result = await putAdminUserRoles(selectedUser.id, draftRoles); + const result = await putAdminUserRoles(selectedUser.id, { + admin_site_id: roleEditSiteId, + role_slugs: draftRoles, + }); setDraftRoles([...result.roles].sort()); setItems((prev) => prev.map((row) => @@ -275,6 +356,7 @@ export function AdminUsersConsole(): React.ReactElement { ? { ...row, roles: result.roles, + site_bindings: result.site_bindings ?? row.site_bindings, effective_permissions: result.effective_permissions, } : row, @@ -378,6 +460,7 @@ export function AdminUsersConsole(): React.ReactElement { {t("table.account")} {t("table.nickname")} {t("table.status")} + {t("table.sites")} {t("table.roles")} {t("table.effective")} {t("table.actions")} @@ -385,13 +468,9 @@ export function AdminUsersConsole(): React.ReactElement { {loading && items.length === 0 ? ( - + ) : items.length === 0 ? ( - - - {t("states.noData", { ns: "common" })} - - + ) : ( items.map((row) => ( @@ -408,6 +487,9 @@ export function AdminUsersConsole(): React.ReactElement { {row.status === 0 ? t("status.enabled") : t("status.disabled")} + + {formatSiteBindings(row.site_bindings) || "—"} +
{row.roles.length === 0 ? ( @@ -494,6 +576,33 @@ export function AdminUsersConsole(): React.ReactElement {

{t("permissionDialog.rolesDescription")}

+ {siteOptions.length > 0 ? ( +
+ + +
+ ) : null}
{(catalog?.roles ?? []).map((role) => { const checked = draftRoles.includes(role.slug); @@ -606,6 +715,29 @@ export function AdminUsersConsole(): React.ReactElement { onChange={(e) => setFormPassword(e.target.value)} />
+ {accountMode === "create" && siteOptions.length > 0 ? ( +
+
{t("accountDialog.site")}
+ +
+ ) : null} {accountMode === "create" ? (
{t("accountDialog.rolesRequired")}
diff --git a/src/modules/agents/agent-line-detail-panel.tsx b/src/modules/agents/agent-line-detail-panel.tsx new file mode 100644 index 0000000..6694d16 --- /dev/null +++ b/src/modules/agents/agent-line-detail-panel.tsx @@ -0,0 +1,699 @@ +"use client"; + +import type { ComponentType } from "react"; +import { ChevronRight, Network, Pencil, Plus, Trash2, Users } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; +import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; +import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { AgentsPlayersPanel } from "@/modules/agents/agents-players-panel"; +import { AgentProfileFields, type AgentProfileFieldsProps } from "@/modules/agents/agent-profile-fields"; +import { formatCredit } from "@/modules/agents/agent-line-sidebar"; +import { Button } from "@/components/ui/button"; +import { ratioToPercentUi } from "@/lib/admin-rate-percent"; +import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; +import { cn } from "@/lib/utils"; +import type { AgentNodeProfileSummary, AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent"; + +function settlementCycleLabel( + cycle: AgentNodeProfileSummary["settlement_cycle"] | undefined, + t: (key: string, opts?: { defaultValue?: string }) => string, +): string { + if (cycle === "daily") { + return t("profile.cycleDaily", { defaultValue: "日结" }); + } + if (cycle === "monthly") { + return t("profile.cycleMonthly", { defaultValue: "月结" }); + } + return t("profile.cycleWeekly", { defaultValue: "周结" }); +} + +export type AgentDetailTab = "overview" | "profile" | "downline" | "players"; + +export type AgentLineDetailPanelProps = { + node: AgentNodeRow | null; + profile: AgentProfileRow | null; + profileLoading: boolean; + childAgents: AgentNodeRow[]; + childCountById: Map; + siteCode: string; + siteLabel: string | null; + parentName: string | null; + detailTab: AgentDetailTab; + onDetailTabChange: (tab: AgentDetailTab) => void; + canViewProfileTab: boolean; + canEditProfileTab: boolean; + profileReadOnly: boolean; + canViewDownlineTab: boolean; + canViewPlayersTab: boolean; + canManageNode: boolean; + canCreateChild: boolean; + canDeleteChild: (node: AgentNodeRow) => boolean; + onEditChild: (node: AgentNodeRow) => void; + onDeleteChild: (node: AgentNodeRow) => void; + onAddChild: () => void; + onEditCurrent: () => void; + onSelectChild: (node: AgentNodeRow) => void; + profileFields: AgentProfileFieldsProps | null; + profileSaving: boolean; + onSaveProfile: () => void; +}; + +export function AgentLineDetailPanel({ + node, + profile, + profileLoading, + childAgents, + childCountById, + siteCode, + siteLabel, + parentName, + detailTab, + onDetailTabChange, + canViewProfileTab, + canEditProfileTab, + profileReadOnly, + canViewDownlineTab, + canViewPlayersTab, + canManageNode, + canCreateChild, + canDeleteChild, + onEditChild, + onDeleteChild, + onAddChild, + onEditCurrent, + onSelectChild, + profileFields, + profileSaving, + onSaveProfile, +}: AgentLineDetailPanelProps): React.ReactElement { + const { t } = useTranslation(["agents", "common"]); + + if (node === null) { + return ( +
+
+ +
+

+ {t("lineUi.selectAgent", { defaultValue: "选择左侧代理查看占成与授信" })} +

+

+ {t("lineUi.selectAgentHint", { + defaultValue: "信用占成盘以代理树为结算边界,占成、授信与回水均在代理节点配置。", + })} +

+
+ ); + } + + const cycleLabel = + profile?.settlement_cycle === "daily" + ? t("profile.cycleDaily", { defaultValue: "日结" }) + : profile?.settlement_cycle === "monthly" + ? t("profile.cycleMonthly", { defaultValue: "月结" }) + : t("profile.cycleWeekly", { defaultValue: "周结" }); + + const tabs: { key: AgentDetailTab; label: string; count?: number; visible: boolean }[] = [ + { + key: "overview", + label: t("lineUi.tabOverview", { defaultValue: "概览" }), + visible: true, + }, + { + key: "profile", + label: profileReadOnly + ? t("lineUi.tabProfileReadOnly", { defaultValue: "占成与授信(只读)" }) + : t("lineUi.tabProfile", { defaultValue: "占成与授信" }), + visible: canViewProfileTab, + }, + { + key: "downline", + label: t("lineUi.tabDownline", { defaultValue: "直属下级" }), + count: childAgents.length, + visible: canViewDownlineTab, + }, + { + key: "players", + label: t("lineUi.tabPlayers", { defaultValue: "直属玩家" }), + visible: canViewPlayersTab, + }, + ]; + + const siteDisplay = + siteLabel && siteCode.trim() !== "" + ? `${siteLabel} (${siteCode})` + : siteLabel ?? siteCode; + + return ( +
+
+
+
+
+

+ {node.name} +

+ + {node.status === 1 + ? t("common:status.enabled", { defaultValue: "启用" }) + : t("common:status.disabled", { defaultValue: "停用" })} + +
+

+ {node.code} + {node.username ? ( + <> + · + {node.username} + + ) : null} + {parentName ? ( + <> + · + {t("parentAgent", { defaultValue: "上级代理" })} {parentName} + + ) : null} +

+
+ +
+ {siteDisplay ? ( +
+ + {t("lineUi.currentSite", { defaultValue: "当前站点" })} + + | + {siteDisplay} +
+ ) : null} + {canManageNode ? ( +
+ + {canCreateChild ? ( + + ) : null} +
+ ) : null} +
+
+
+ +
+ {tabs + .filter((tab) => tab.visible) + .map((tab) => ( + onDetailTabChange(tab.key)} + label={tab.label} + count={tab.count} + /> + ))} +
+ +
+ {detailTab === "overview" ? ( + onDetailTabChange("downline")} + onGoToPlayers={() => onDetailTabChange("players")} + /> + ) : null} + + {detailTab === "profile" && canViewProfileTab && profileFields ? ( + + + + {profileReadOnly + ? t("lineUi.tabProfileReadOnly", { defaultValue: "占成与授信(只读)" }) + : t("lineUi.tabProfile", { defaultValue: "占成与授信" })} + +

+ {profileReadOnly + ? t("lineUi.profileReadOnlyHint", { + defaultValue: "占成、授信与回水由上级配置,如需调整请联系上级代理或平台。", + }) + : t("lineUi.profileTabHint", { + defaultValue: + "占成、授信、回水与风控标签在此维护;登录名与密码请用「账号与状态」。", + })} +

+
+ + + {canManageNode && canEditProfileTab ? ( +
+ +
+ ) : null} +
+
+ ) : null} + + {detailTab === "downline" && canViewDownlineTab ? ( + + ) : null} + + {detailTab === "players" && canViewPlayersTab ? ( + + ) : null} +
+
+ ); +} + +function OverviewTab({ + profile, + profileLoading, + cycleLabel, + profileReadOnly, + canViewDownlineTab, + canViewPlayersTab, + childCount, + onGoToDownline, + onGoToPlayers, +}: { + profile: AgentProfileRow | null; + profileLoading: boolean; + cycleLabel: string; + profileReadOnly: boolean; + canViewDownlineTab: boolean; + canViewPlayersTab: boolean; + childCount: number; + onGoToDownline: () => void; + onGoToPlayers: () => void; +}): React.ReactElement { + const { t } = useTranslation(["agents", "common"]); + + const rebateCap = + profile && !profileLoading ? ratioToPercentUi(profile.rebate_limit ?? 0) : null; + + return ( +
+ {profileReadOnly ? ( +
+ + + +
+ ) : ( +
+ + + + +
+ )} + + {!profileReadOnly && !profileLoading && profile ? ( +

+ {t("lineUi.profileFootnote", { + defaultValue: "回水上限 {{rebate}}% · 默认回水 {{defaultRebate}}% · {{cycle}}", + rebate: ratioToPercentUi(profile.rebate_limit ?? 0), + defaultRebate: ratioToPercentUi(profile.default_player_rebate ?? 0), + cycle: cycleLabel, + })} + {(profile.risk_tags?.length ?? 0) > 0 + ? ` · ${t("profile.riskTags", { defaultValue: "风控" })}: ${profile.risk_tags?.join(", ")}` + : ""} +

+ ) : null} + + {profileReadOnly ? ( +

+ {t("lineUi.selfAgentOverviewHint", { + defaultValue: + "以下为上级为您分配的授信额度,占成与回水由上级在后台维护,本账号不可查看或修改。", + })} +

+ ) : null} + + {canViewDownlineTab || canViewPlayersTab ? ( +
+ {canViewDownlineTab ? ( + + ) : null} + {canViewPlayersTab ? ( + + ) : null} +
+ ) : null} +
+ ); +} + +function OverviewLinkCard({ + icon: Icon, + title, + description, + actionLabel, + onAction, +}: { + icon: ComponentType<{ className?: string }>; + title: string; + description: string; + actionLabel: string; + onAction: () => void; +}): React.ReactElement { + return ( + + +
+
+ +
+
+

{title}

+

{description}

+
+
+ +
+
+ ); +} + +function DownlineTable({ + childAgents, + childCountById, + canManageNode, + canCreateChild, + canDeleteChild, + onEditChild, + onDeleteChild, + onSelectChild, + onAddChild, +}: { + childAgents: AgentNodeRow[]; + childCountById: Map; + canManageNode: boolean; + canCreateChild: boolean; + canDeleteChild: (node: AgentNodeRow) => boolean; + onEditChild: (node: AgentNodeRow) => void; + onDeleteChild: (node: AgentNodeRow) => void; + onSelectChild: (node: AgentNodeRow) => void; + onAddChild: () => void; +}): React.ReactElement { + const { t } = useTranslation(["agents", "common"]); + + if (childAgents.length === 0) { + return ( +
+ + {canManageNode && canCreateChild ? ( + + ) : null} + +
+ ); + } + + return ( +
+
+ + + + {t("agentCode", { defaultValue: "代理编码" })} + {t("agentName", { defaultValue: "代理名称" })} + {t("loginUsername", { defaultValue: "登录名" })} + {t("lineUi.downlineColumns.email", { defaultValue: "邮箱" })} + + {t("profile.totalShareRate", { defaultValue: "占成 (%)" })} + + + {t("profile.creditLimit", { defaultValue: "授信额度" })} + + + {t("lineUi.allocatedCredit", { defaultValue: "已下发" })} + + + {t("profile.settlementCycle", { defaultValue: "结算周期" })} + + + {t("lineUi.downlineColumns.downlineCount", { defaultValue: "下级数" })} + + {t("common:status.label", { defaultValue: "状态" })} + {canManageNode ? ( + + {t("common:table.actions", { defaultValue: "操作" })} + + ) : null} + + + + {childAgents.map((child) => { + const summary = child.profile_summary; + return ( + onSelectChild(child)} + > + {child.code} + {child.name} + {child.username ?? "—"} + + {child.email ?? "—"} + + + {summary ? `${ratioToPercentUi(summary.total_share_rate)}%` : "—"} + + + {summary ? formatCredit(summary.credit_limit) : "—"} + + + {summary ? formatCredit(summary.allocated_credit) : "—"} + + + {summary ? settlementCycleLabel(summary.settlement_cycle, t) : "—"} + + + {childCountById.get(child.id) ?? 0} + + + + {child.status === 1 + ? t("common:status.enabled", { defaultValue: "启用" }) + : t("common:status.disabled", { defaultValue: "停用" })} + + + {canManageNode ? ( + e.stopPropagation()} + > + onEditChild(child), + }, + { + key: "delete", + label: t("deleteNode", { defaultValue: "删除代理" }), + icon: Trash2, + destructive: true, + disabled: !canDeleteChild(child), + onClick: () => onDeleteChild(child), + }, + ]} + /> + + ) : null} + + ); + })} + +
+
+
+ ); +} + +function MetricCard({ + label, + value, + subtitle, + accent = false, + highlight = false, +}: { + label: string; + value: string; + subtitle?: string; + accent?: boolean; + highlight?: boolean; +}): React.ReactElement { + return ( +
+

{label}

+

+ {value} +

+ {subtitle ?

{subtitle}

: null} +
+ ); +} + +function TabButton({ + active, + onClick, + label, + count, +}: { + active: boolean; + onClick: () => void; + label: string; + count?: number; +}): React.ReactElement { + return ( + + ); +} diff --git a/src/modules/agents/agent-line-provision-wizard.tsx b/src/modules/agents/agent-line-provision-wizard.tsx index 0170f62..abfbff2 100644 --- a/src/modules/agents/agent-line-provision-wizard.tsx +++ b/src/modules/agents/agent-line-provision-wizard.tsx @@ -1,10 +1,12 @@ "use client"; -import { useState } from "react"; +import Link from "next/link"; +import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { postAdminAgentLine } from "@/api/admin-agent-lines"; +import { getAdminIntegrationSites } from "@/api/admin-integration-sites"; import { AdminPageCard } from "@/components/admin/admin-page-card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -17,21 +19,22 @@ import { SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; -import { Textarea } from "@/components/ui/textarea"; +import { useAsyncEffect } from "@/hooks/use-async-effect"; +import { percentUiToRatio } from "@/lib/admin-rate-percent"; import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminIntegrationSiteRow } from "@/types/api/admin-integration-site"; export function AgentLineProvisionWizard(): React.ReactElement { const { t } = useTranslation(["agents", "common"]); const [submitting, setSubmitting] = useState(false); - const [secrets, setSecrets] = useState<{ sso: string; wallet: string } | null>(null); + const [sitesLoading, setSitesLoading] = useState(true); + const [sites, setSites] = useState([]); const [form, setForm] = useState({ + site_code: "", code: "", name: "", username: "", password: "", - currency_code: "NPR", - wallet_api_url: "", - notes: "", total_share_rate: "0", credit_limit: "0", rebate_limit: "0", @@ -40,33 +43,51 @@ export function AgentLineProvisionWizard(): React.ReactElement { can_grant_extra_rebate: false, }); + useAsyncEffect(() => { + setSitesLoading(true); + void getAdminIntegrationSites() + .then((data) => setSites(data.items)) + .catch(() => setSites([])) + .finally(() => setSitesLoading(false)); + }, []); + + const unboundSites = useMemo( + () => sites.filter((row) => !row.has_line_root), + [sites], + ); + async function onSubmit(e: React.FormEvent): Promise { e.preventDefault(); + if (!form.site_code.trim()) { + toast.error(t("agents:lineProvision.siteRequired", { defaultValue: "请选择接入站点" })); + return; + } setSubmitting(true); - setSecrets(null); try { - const result = await postAdminAgentLine({ + await postAdminAgentLine({ + site_code: form.site_code.trim().toLowerCase(), code: form.code.trim().toLowerCase(), name: form.name.trim(), username: form.username.trim(), password: form.password, - currency_code: form.currency_code, - wallet_api_url: form.wallet_api_url.trim() || null, - notes: form.notes.trim() || null, total_share_rate: Number.parseFloat(form.total_share_rate) || 0, credit_limit: Number.parseInt(form.credit_limit, 10) || 0, - rebate_limit: Number.parseFloat(form.rebate_limit) || 0, - default_player_rebate: Number.parseFloat(form.default_player_rebate) || 0, + rebate_limit: percentUiToRatio(form.rebate_limit), + default_player_rebate: percentUiToRatio(form.default_player_rebate), settlement_cycle: form.settlement_cycle, can_grant_extra_rebate: form.can_grant_extra_rebate, }); - if (result.secrets) { - setSecrets({ - sso: result.secrets.sso_jwt_secret, - wallet: result.secrets.wallet_api_key, - }); - } - toast.success(t("agents:lineProvision.success", { defaultValue: "线路已开通" })); + toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" })); + setForm((f) => ({ + ...f, + site_code: "", + code: "", + name: "", + username: "", + password: "", + })); + const data = await getAdminIntegrationSites(); + setSites(data.items); } catch (err) { const msg = err instanceof LotteryApiBizError ? err.message : t("common:error.generic"); @@ -77,10 +98,61 @@ export function AgentLineProvisionWizard(): React.ReactElement { } return ( - + +

+ {t("agents:subnav.provisionHint", { + defaultValue: + "请先在「平台管理 → 接入配置」创建接入站点;对接密钥在站点创建时一次性展示。", + })} +

+

+ {t("agents:lineProvision.description", { + defaultValue: + "将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水、结算周期。代理编码创建后不可修改。", + })}{" "} + + {t("agents:lineProvision.openIntegrationSites", { + defaultValue: "前往接入站点", + })} + +

- + + +
+
+ setForm((f) => ({ ...f, code: e.target.value }))} @@ -89,7 +161,7 @@ export function AgentLineProvisionWizard(): React.ReactElement { />
- + setForm((f) => ({ ...f, name: e.target.value }))} @@ -97,7 +169,7 @@ export function AgentLineProvisionWizard(): React.ReactElement { />
- + setForm((f) => ({ ...f, username: e.target.value }))} @@ -114,13 +186,6 @@ export function AgentLineProvisionWizard(): React.ReactElement { minLength={8} />
-
- - setForm((f) => ({ ...f, wallet_api_url: e.target.value }))} - /> -

{t("agents:profile.section", { defaultValue: "占成与授信" })} @@ -147,24 +212,26 @@ export function AgentLineProvisionWizard(): React.ReactElement { />

- + setForm((f) => ({ ...f, rebate_limit: e.target.value }))} />
- + setForm((f) => ({ ...f, default_player_rebate: e.target.value }))} />
@@ -208,32 +275,12 @@ export function AgentLineProvisionWizard(): React.ReactElement {
-
- -