From af982bb9f77e753292219350b0bfc45178e6208d Mon Sep 17 00:00:00 2001 From: kang Date: Fri, 5 Jun 2026 18:00:59 +0800 Subject: [PATCH] feat(api, agents, i18n): enhance settlement features and multi-language support Added new types and API functions for settlement period summaries and credit ledgers, improving the management of agent settlements. Updated the admin console to reflect these changes, enhancing user experience with better navigation and data presentation. Additionally, expanded multi-language support by incorporating new translations in English, Nepali, and Chinese for settlement-related terms, ensuring consistency across the platform. --- src/api/admin-agent-settlement.ts | 133 +++-- src/app/admin/(shell)/config/rebate/page.tsx | 2 +- .../admin/admin-list-pagination-footer.tsx | 4 +- .../admin/admin-no-resource-state.tsx | 6 +- src/components/admin/admin-sidebar-nav.tsx | 17 +- src/components/admin/admin-subnav.tsx | 128 +++++ src/components/ui/tabs.tsx | 5 +- src/i18n/locales/en/config.json | 16 +- src/i18n/locales/en/reconcile.json | 1 + src/i18n/locales/en/settlementCenter.json | 166 +++++- src/i18n/locales/en/wallet.json | 6 +- src/i18n/locales/ne/config.json | 16 +- src/i18n/locales/ne/reconcile.json | 1 + src/i18n/locales/zh/agents.json | 4 +- src/i18n/locales/zh/config.json | 16 +- src/i18n/locales/zh/reconcile.json | 1 + src/i18n/locales/zh/settlementCenter.json | 209 +++++-- src/i18n/locales/zh/wallet.json | 6 +- src/lib/admin-select-display.ts | 51 ++ src/lib/admin-site-display.ts | 13 + src/lib/admin-status-tone.ts | 5 + src/modules/_config/admin-nav-icons.tsx | 19 +- src/modules/_config/admin-nav.ts | 1 + .../agents/agent-line-detail-panel.tsx | 54 +- .../agents/agent-line-provision-wizard.tsx | 39 +- src/modules/agents/agent-profile-fields.tsx | 20 +- src/modules/agents/agents-console.tsx | 57 +- src/modules/agents/agents-subnav.tsx | 108 ++-- src/modules/config/config-nav-model.ts | 8 - src/modules/config/config-subnav.tsx | 23 +- src/modules/config/config-version-actions.tsx | 5 +- src/modules/config/doc/odds-config-dirty.ts | 34 ++ .../config/doc/odds-config-doc-screen.tsx | 316 +++++++---- .../config/doc/odds-config-draft-bar.tsx | 51 ++ .../config/doc/odds-config-play-nav.tsx | 158 ++++++ .../config/doc/odds-config-summary-panel.tsx | 145 +++-- src/modules/config/risk-cap-runtime-panel.tsx | 10 +- src/modules/draws/draw-detail-console.tsx | 1 + src/modules/draws/draw-subnav.tsx | 20 +- .../jackpot/jackpot-records-console.tsx | 21 +- src/modules/players/player-detail-console.tsx | 19 +- src/modules/players/players-console.tsx | 19 +- src/modules/reconcile/reconcile-console.tsx | 25 +- src/modules/reports/reports-subnav.tsx | 21 +- src/modules/risk/risk-subnav.tsx | 39 +- .../rules/rules-odds-config-screen.tsx | 32 +- src/modules/settlement/agent-bill-detail.tsx | 522 ++++++++++-------- .../settlement/agent-periods-console.tsx | 307 ---------- .../agent-settlement-period-select.tsx | 33 +- .../agent-settlement-reports-panel.tsx | 8 +- .../settlement/settlement-bill-breakdown.tsx | 196 +++++++ .../settlement/settlement-bill-display.ts | 320 +++++++++++ .../settlement/settlement-bills-panel.tsx | 140 ----- .../settlement/settlement-bills-table.tsx | 304 ++++++++-- .../settlement/settlement-center-nav.ts | 35 ++ .../settlement/settlement-center-nav.tsx | 101 ---- .../settlement-center-period-detail.tsx | 111 ++++ .../settlement/settlement-center-shell.tsx | 481 +++++----------- .../settlement-credit-ledger-panel.tsx | 361 ++++++++++++ .../settlement-credit-ledger-table.tsx | 172 ------ .../settlement/settlement-ledger-panel.tsx | 378 ------------- .../settlement-ledger-row-actions.tsx | 127 ----- .../settlement/settlement-main-panel.tsx | 342 ++++++++++++ .../settlement/settlement-party-cells.tsx | 35 ++ .../settlement-period-workbench.tsx | 508 +++++++++++++++++ .../settlement/settlement-periods-table.tsx | 169 ++++++ .../settlement/settlement-signed-money.ts | 13 + .../settlement/settlement-status-label.ts | 7 + src/modules/wallet/wallet-scope-hint.tsx | 6 +- src/modules/wallet/wallet-subnav.tsx | 38 +- src/types/api/admin-player-tickets.ts | 8 + src/types/api/admin-settlement.ts | 20 + src/types/api/admin-tickets.ts | 8 + 73 files changed, 4307 insertions(+), 2494 deletions(-) create mode 100644 src/components/admin/admin-subnav.tsx create mode 100644 src/lib/admin-select-display.ts create mode 100644 src/lib/admin-site-display.ts create mode 100644 src/modules/config/doc/odds-config-dirty.ts create mode 100644 src/modules/config/doc/odds-config-draft-bar.tsx create mode 100644 src/modules/config/doc/odds-config-play-nav.tsx delete mode 100644 src/modules/settlement/agent-periods-console.tsx create mode 100644 src/modules/settlement/settlement-bill-breakdown.tsx create mode 100644 src/modules/settlement/settlement-bill-display.ts delete mode 100644 src/modules/settlement/settlement-bills-panel.tsx create mode 100644 src/modules/settlement/settlement-center-nav.ts delete mode 100644 src/modules/settlement/settlement-center-nav.tsx create mode 100644 src/modules/settlement/settlement-center-period-detail.tsx create mode 100644 src/modules/settlement/settlement-credit-ledger-panel.tsx delete mode 100644 src/modules/settlement/settlement-credit-ledger-table.tsx delete mode 100644 src/modules/settlement/settlement-ledger-panel.tsx delete mode 100644 src/modules/settlement/settlement-ledger-row-actions.tsx create mode 100644 src/modules/settlement/settlement-main-panel.tsx create mode 100644 src/modules/settlement/settlement-party-cells.tsx create mode 100644 src/modules/settlement/settlement-period-workbench.tsx create mode 100644 src/modules/settlement/settlement-periods-table.tsx create mode 100644 src/modules/settlement/settlement-signed-money.ts diff --git a/src/api/admin-agent-settlement.ts b/src/api/admin-agent-settlement.ts index 150c30a..3865d5d 100644 --- a/src/api/admin-agent-settlement.ts +++ b/src/api/admin-agent-settlement.ts @@ -10,11 +10,16 @@ export type SettlementPeriodSummary = { awaiting_payment: number; settled: number; total_unpaid: number; + total_net?: number; }; export type SettlementPeriodPipeline = { credit_ledger_count: number; share_ledger_count: number; + game_win_loss_total?: number; + win_loss_scope?: "platform" | "agent"; + basic_rebate_total?: number; + unsettled_ticket_count?: number; }; export type SettlementPeriodRow = { @@ -55,6 +60,12 @@ export type SettlementBillRow = { status: string; owner_label?: string; counterparty_label?: string; + player_username?: string | null; + player_site_player_id?: string | null; + player_id_display?: number | null; + direct_agent_label?: string | null; + superior_agent_label?: string | null; + owner_party_label?: string | null; owner_funding_mode?: string | null; owner_auth_source?: string | null; period_start?: string; @@ -102,12 +113,71 @@ export type SettlementBillListScope = | "settled" | "adjustment"; +export type SettlementCreditLedgerRow = { + entry_kind: string; + id: number; + row_key: string; + txn_no: string; + player_id: number; + site_code?: string | null; + username?: string | null; + nickname?: string | null; + site_player_id?: string | null; + biz_type: string; + biz_no?: string | null; + ref_type?: string | null; + ref_id?: number | null; + direction: 1 | 2; + amount: number; + amount_formatted?: string; + signed_amount?: number; + currency_code?: string; + status: string; + created_at?: string | null; + ledger_source: string; + funding_mode?: string | null; + direct_agent_label?: string | null; + parent_agent_label?: string | null; + play_code?: string | null; + draw_no?: string | null; + ticket_item_id?: number | null; +}; + +export async function getCreditLedger(params: { + admin_site_id: number; + settlement_period_id: number; + entry_kind?: "credit"; + bet_flow_only?: boolean; + bet_flow_display?: "simple"; + player_account?: string; + reason?: string; + page?: number; + per_page?: number; +}): Promise<{ + items: SettlementCreditLedgerRow[]; + total: number; + page: number; + per_page: number; + ledger_source: string; +}> { + return adminRequest.get(`${A}/credit-ledger`, { params }); +} + export async function getSettlementBills(params?: { settlement_period_id?: number; admin_site_id?: number; bill_type?: string; scope?: SettlementBillListScope; -}): Promise<{ items: SettlementBillRow[] }> { + bill_id?: number; + keyword?: string; + page?: number; + per_page?: number; +}): Promise<{ + items: SettlementBillRow[]; + total?: number; + page?: number; + per_page?: number; +}> { return adminRequest.get(`${A}/settlement-bills`, { params }); } @@ -163,70 +233,13 @@ export async function getSettlementAdjustments(params?: { 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; + settlement_bill_id?: number; participant_type: string; participant_id: number; + participant_label?: string; actual_share_rate: number; allocated_amount: number; allocation_rule: string; diff --git a/src/app/admin/(shell)/config/rebate/page.tsx b/src/app/admin/(shell)/config/rebate/page.tsx index 6df3bca..a4e7c33 100644 --- a/src/app/admin/(shell)/config/rebate/page.tsx +++ b/src/app/admin/(shell)/config/rebate/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function AdminConfigRebateRedirectPage() { - redirect("/admin/rules/odds#rebate"); + redirect("/admin/rules/odds"); } diff --git a/src/components/admin/admin-list-pagination-footer.tsx b/src/components/admin/admin-list-pagination-footer.tsx index 45c6eba..afa54d9 100644 --- a/src/components/admin/admin-list-pagination-footer.tsx +++ b/src/components/admin/admin-list-pagination-footer.tsx @@ -81,7 +81,9 @@ export function AdminPerPagePicker({ }} > - + + {(v) => (v == null || v === "" ? String(perPage) : String(v))} + {ADMIN_LIST_PER_PAGE_OPTIONS.map((n) => ( diff --git a/src/components/admin/admin-no-resource-state.tsx b/src/components/admin/admin-no-resource-state.tsx index 38fdf23..a6fed41 100644 --- a/src/components/admin/admin-no-resource-state.tsx +++ b/src/components/admin/admin-no-resource-state.tsx @@ -30,7 +30,7 @@ export function AdminNoResourceState({

diff --git a/src/components/admin/admin-sidebar-nav.tsx b/src/components/admin/admin-sidebar-nav.tsx index d0bb3b6..9ef0ceb 100644 --- a/src/components/admin/admin-sidebar-nav.tsx +++ b/src/components/admin/admin-sidebar-nav.tsx @@ -14,6 +14,7 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, + useSidebar, } from "@/components/ui/sidebar"; import { ADMIN_NAV_GROUP_ICON, @@ -29,7 +30,7 @@ const NAV_BTN = "h-8 gap-2 px-2.5 py-0 text-[13px] leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground [&_svg]:size-4"; const NAV_ACTIVE = "data-active:bg-red-600 data-active:text-white data-active:font-medium data-active:shadow-sm"; const SUB_NAV = - "h-8 min-h-8 rounded-sm px-2.5 py-0 text-sm leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground data-[size=md]:text-sm data-[size=sm]:text-sm [&>span]:text-sm"; + "h-8 min-h-8 gap-2 rounded-sm px-2.5 py-0 text-sm leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground data-[size=md]:text-sm data-[size=sm]:text-sm [&>span]:text-sm [&_svg]:size-4"; function isActive( pathname: string, @@ -101,6 +102,7 @@ function NavSubLeaf({ pathname: string; t: TFunction; }): ReactElement { + const Icon = resolveAdminNavIcon(item.segment); const active = isActive(pathname, item); const label = adminNavLabel(item.segment, t, item.label); @@ -112,6 +114,7 @@ function NavSubLeaf({ render={} className={cn(SUB_NAV, NAV_ACTIVE)} > + {label} @@ -175,7 +178,9 @@ export function AdminSidebarNav({ }): ReactElement { const { t } = useTranslation("common"); const pathname = usePathname(); + const { state } = useSidebar(); const navGroups = useMemo(() => groupAdminNavItems(items), [items]); + const flatItems = useMemo(() => navGroups.flatMap((g) => g.items), [navGroups]); const [openGroups, setOpenGroups] = useState>(() => defaultOpenGroups(navGroups, pathname), @@ -195,6 +200,16 @@ export function AdminSidebarNav({ }); }, [pathname, navGroups]); + if (state === "collapsed") { + return ( + + {flatItems.map((item) => ( + + ))} + + ); + } + const overview = navGroups.find((g) => g.group === "overview"); const collapsible = navGroups.filter((g) => g.group !== "overview"); diff --git a/src/components/admin/admin-subnav.tsx b/src/components/admin/admin-subnav.tsx new file mode 100644 index 0000000..bdf12ec --- /dev/null +++ b/src/components/admin/admin-subnav.tsx @@ -0,0 +1,128 @@ +"use client"; + +import Link from "next/link"; +import type { ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +export function adminSubnavItemClassName(active: boolean, disabled = false): string { + if (disabled) { + return "border-b-2 border-transparent px-4 py-3 text-sm font-medium text-muted-foreground/45 cursor-not-allowed"; + } + + return cn( + "relative -mb-px shrink-0 border-b-2 px-4 py-3 text-sm font-medium transition-colors", + active + ? "border-primary text-primary" + : "border-transparent text-muted-foreground hover:border-border/80 hover:text-foreground", + ); +} + +type AdminSubnavProps = { + children: ReactNode; + className?: string; + /** Accessible name for the tab list */ + "aria-label": string; +}; + +/** Underline-style horizontal tab bar used across admin page headers. */ +export function AdminSubnav({ children, className, "aria-label": ariaLabel }: AdminSubnavProps) { + return ( +

+ ); +} + +type AdminSubnavLinkProps = { + href: string; + active: boolean; + children: ReactNode; + className?: string; + disabled?: boolean; + disabledTitle?: string; +}; + +export function AdminSubnavLink({ + href, + active, + children, + className, + disabled = false, + disabledTitle, +}: AdminSubnavLinkProps) { + if (disabled) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +type AdminSubnavButtonProps = { + active: boolean; + onClick: () => void; + children: ReactNode; + className?: string; + count?: number; + disabled?: boolean; +}; + +export function AdminSubnavButton({ + active, + onClick, + children, + className, + count, + disabled = false, +}: AdminSubnavButtonProps) { + return ( + + ); +} + +type AdminSubnavBarProps = { + children: ReactNode; + trailing?: ReactNode; + className?: string; +}; + +/** Wrapper for subnav plus optional trailing controls (site selector, back link, etc.). */ +export function AdminSubnavBar({ children, trailing, className }: AdminSubnavBarProps) { + return ( +
+ {children} + {trailing ?
{trailing}
: null} +
+ ); +} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 8ee8054..993cb33 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -24,7 +24,7 @@ function Tabs({ } const tabsListVariants = cva( - "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none", + "group/tabs-list inline-flex w-fit items-end justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none", { variants: { variant: { @@ -59,9 +59,10 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { data-slot="tabs-trigger" className={cn( "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group-data-[variant=line]/tabs-list:-mb-px group-data-[variant=line]/tabs-list:h-auto group-data-[variant=line]/tabs-list:shrink-0 group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:border-b-2 group-data-[variant=line]/tabs-list:border-transparent group-data-[variant=line]/tabs-list:px-4 group-data-[variant=line]/tabs-list:py-3 group-data-[variant=line]/tabs-list:text-muted-foreground group-data-[variant=line]/tabs-list:hover:border-border/80 group-data-[variant=line]/tabs-list:hover:text-foreground group-data-[variant=line]/tabs-list:data-active:border-primary group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:text-primary dark:group-data-[variant=line]/tabs-list:data-active:border-primary dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", - "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", + "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:after:hidden group-data-[variant=line]/tabs-list:data-active:after:opacity-0", className )} {...props} diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index 6331abd..ef1f187 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -17,6 +17,7 @@ "rulesPlaysTitle": "Play rules", "rulesOddsTitle": "Odds & rebate", "rulesOddsDescription": "Odds matrix and rebate rates on one page, sharing the same odds version line.", + "rulesOddsDescriptionShort": "Pick a play on the left, edit odds and rebate on the right, then save and publish.", "riskCapTitle": "Risk cap rules" }, "hub": { @@ -411,6 +412,16 @@ "oddsConfig": "Odds" }, "currentSelection": "Selection: {{category}} / {{play}}", + "playSelectPlaceholder": "Select play type", + "readOnlyBanner": "This version is read-only. Create a draft to edit odds and rebate.", + "table": { + "prizeScope": "Prize scope", + "multiplier": "Odds multiplier" + }, + "draftBar": { + "unsaved": "Unsaved changes", + "saved": "Changes kept in local draft" + }, "playGroups": { "bigSmall": "Big / small", "combo4": "4D position", @@ -420,10 +431,13 @@ }, "summary": { "title": "Summary", - "version": "Version", + "contextTitle": "Version & tips", + "version": "Editing version", + "activeVersion": "Active version", "statusLabel": "Status", "readOnlyTag": "Read-only", "readOnlyHint": "This version is read-only. Create a draft to make changes.", + "draftHint": "Save draft changes before publishing; publish affects new tickets only.", "activeHint": "This version is active; new tickets use these settings." }, "tabs": { diff --git a/src/i18n/locales/en/reconcile.json b/src/i18n/locales/en/reconcile.json index 6987b74..8e2adf9 100644 --- a/src/i18n/locales/en/reconcile.json +++ b/src/i18n/locales/en/reconcile.json @@ -27,6 +27,7 @@ "playerAllPlayersHint": "If no player is selected, the reconcile job will cover all players in the chosen date range.", "createSummaryAll": "A manual reconcile will run for all players from {{from}} to {{to}}.", "createSummaryPlayer": "A manual reconcile will run for player {{player}} from {{from}} to {{to}}.", + "createSummaryPending": "Choose a complete reconcile date range before creating a job.", "jobsTitle": "Reconcile jobs", "jobsDesc": "Use the action on the right to open paginated item details.", "refresh": "Refresh", diff --git a/src/i18n/locales/en/settlementCenter.json b/src/i18n/locales/en/settlementCenter.json index 5270cb0..61fb005 100644 --- a/src/i18n/locales/en/settlementCenter.json +++ b/src/i18n/locales/en/settlementCenter.json @@ -1,5 +1,50 @@ { "title": "Settlement center", + "subtitle": "Period close, bill confirm, and payments", + "subtitleList": "Period list: open/close periods; summary columns included — use row actions for bills and bet ledger.", + "period": { + "title": "Period", + "statusCompleted": "Completed", + "pipelineShare": "{{count}} ledger entries", + "billTodo": "Pending {{p}} · Awaiting {{a}}", + "openTitle": "Open period", + "openBtn": "Open period", + "closeNeedLedger": "No ledger activity yet. Complete draw settlement first.", + "closeDialogTitle": "Close period", + "closeDialogDesc": "Summarize {{range}} and generate bills.", + "closeDialogShare": "{{count}} ledger entries", + "closeDialogUnsettled": "{{count}} tickets still unsettled", + "closeDialogIrreversible": "Cannot undo. Use adjustments or reversals to fix errors.", + "closeDialogConfirm": "Close period" + }, + "periodDetail": { + "back": "Back to periods", + "notFound": "Period not found or site changed. Go back to the list." + }, + "periodTable": { + "title": "Periods", + "statusFilter": "Status", + "range": "Period", + "ledgerCount": "Ledger", + "pending": "Pending", + "awaiting": "Awaiting", + "shareLedger": "Share ledger", + "gameWinLoss": "Win/loss total", + "platformWinLoss": "Platform win/loss", + "agentWinLoss": "Agent win/loss", + "basicRebate": "Rebate total", + "unsettledTickets": "Unsettled tickets", + "openReportHint": "Open period: share/win-loss from in-period ledger; bill count updates after close.", + "viewDetail": "View details", + "close": "Close", + "closeNow": "Close now", + "hasOpen": "Period {{range}} is open. Close it before opening a new one.", + "emptyOpenHint": "No periods yet. Click Open period in the toolbar.", + "emptyReadOnly": "No period records.", + "emptyFiltered": "No rows match the filter. Reset filters.", + "emptyFilteredOpen": "The open period is hidden by the filter. Choose All or Open.", + "readOnlyHint": "Bound agent accounts cannot open or close site periods." + }, "header": { "subtitle": "Credit-line settlement", "statusRunning": "Period open", @@ -9,18 +54,16 @@ "subnav": { "label": "Settlement center navigation" }, + "workbench": { + "viewPeriod": "Period", + "closePreset": "Close · {{label}}", + "closeNoData": "Close failed: no share ledger in period. Run credit game settlement first.", + "openPeriodPipeline": "Open {{range}} · {{share}} share entries" + }, "nav": { - "aria": "Settlement center navigation", - "group": { - "hub": "Workbench", - "finance": "Finance", - "ledger": "Ledger", - "bills": "Bills" - }, - "overview": "Overview", "periods": "Periods", - "ledger": "Account ledger", "bills": "Bills", + "ledger": "Account ledger", "creditLedger": "Credit ledger", "playerBills": "Player bills", "agentBills": "Agent bills", @@ -33,6 +76,7 @@ }, "filters": { "period": "Period", + "statusAll": "All", "allPeriods": "All periods", "statusOpen": "Open", "statusClosed": "Closed", @@ -54,7 +98,9 @@ "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).", + "periodIntro": "Credit-line bets in this period: bet hold and draw settlement (merged per ticket).", + "emptyPeriod": "No bet ledger entries in this period. Ensure credit players placed bets and draws were settled.", + "intro": "Ledger entries for the selected period.", "columns": { "txn": "Txn ID", "player": "Player", @@ -83,9 +129,16 @@ "reason": { "payment_record": "Bill payment", "bet_hold": "Bet hold", + "game_settlement": "Draw settlement", + "game_settlement_win": "Draw settlement credit", "bet_hold_release": "Hold release", - "game_settlement_loss": "Draw settlement", - "settlement_confirm": "Period confirm" + "game_settlement_loss": "Draw settlement debit", + "settlement_payout": "Settlement payout", + "settlement_confirm": "Period confirm", + "adjustment": "Adjustment", + "reversal": "Reversal", + "bad_debt": "Bad debt", + "share_ledger": "Share ledger" } }, "columns": { @@ -132,6 +185,51 @@ "viewBill": "View bill", "billDetail": "Bill detail" }, + "billDisplay": { + "settlementFlow": "Who pays whom", + "settlementAmount": "Settlement amount", + "pays": "Pays", + "paysShort": "Pays", + "howAmountWorks": "How the amount is calculated", + "playerBreakdownIntro": "Players settle only with their direct agent: net = win/loss − rebate.", + "agentBreakdownIntro": "Agents settle only with their upline: net = team net − own share profit.", + "playerGross": "Game win/loss", + "playerLostHint": "Player lost; owes agent", + "playerWonHint": "Player won; agent owes player", + "playerNet": "Player net payable", + "playerNetReceive": "Agent pays player", + "teamGross": "Team game win/loss", + "teamGrossHint": "Includes this agent and all downline players", + "teamGrossShort": "Team", + "playerGrossShort": "Player", + "teamRebate": "Team rebate", + "teamNet": "Team net", + "rebate": "Rebate", + "agentShareKeep": "{{agent}} share kept", + "agentShareKeepHint": "Profit retained at this tier by share ratio", + "agentNet": "{{agent}} pays upline", + "agentNetReceive": "Upline pays {{agent}}", + "billOwner": "Bill owner", + "billCounterparty": "Counterparty", + "unpaidPendingConfirm": "Confirm the bill before recording payment", + "unpaidAwaitingPayment": "Record offline payment", + "fullySettled": "Fully settled this period", + "recordReceiptFrom": "Record receipt ({{payer}} → {{payee}})", + "recordPayoutTo": "Record payout ({{payer}} → {{payee}})", + "rebateAllocationsHint": "How rebate is allocated across agent tiers.", + "payment": "Payment", + "flowHint": { + "playerPayAgent": "Player should settle with the direct agent", + "agentPayPlayer": "Direct agent should settle with the player", + "agentPayUpstream": "This agent should settle with upline / platform", + "upstreamPayAgent": "Upline / platform should settle with this agent", + "adjustment": "Adjustment settles separately and keeps the original bill relation", + "badDebt": "Write off unpaid amount and archive it as bad debt", + "reversal": "Reverse the original bill impact according to reversal rules", + "generic": "Apply payment or adjustment based on this bill relation" + }, + "hierarchyHint": "One period creates multiple bills: players pay their agent first; each agent keeps share profit and remits the rest upline. Gross win/loss may match across rows while settlement amounts step down." + }, "ledgerPanel": { "search": "Search", "searchBtn": "Search", @@ -144,18 +242,34 @@ "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" - } + "rowPosted": "Posted" }, "billsPanel": { "intro": "Share bills after period close. Filter by type or status; open detail to confirm or record payment.", + "hierarchyHint": "One period creates multiple bills: players pay their agent first; each agent keeps share profit and remits the rest upline. Gross win/loss may match across rows while settlement amounts step down.", + "quickFilter": { + "title": "Which settlement layer do you want to review", + "desc": "Choose player or agent settlement first, then narrow by status or bill id.", + "allTitle": "All bills", + "allHint": "View player, agent, adjustment, and bad debt bills together", + "playerTitle": "Player settlement", + "playerHint": "Credit settlement between players and their direct agent", + "agentTitle": "Agent settlement", + "agentHint": "Tier settlement between agents and their upline / platform" + }, + "activeHint": { + "all": "Current focus: all settlement documents in this period", + "player": "Current focus: credit settlement between players and direct agents", + "agent": "Current focus: payments between agents and their upline / platform" + }, + "layer": { + "player": "Player vs direct agent", + "agent": "Agent vs upline / platform", + "adjustment": "Settlement adjustment", + "badDebt": "Bad debt archive", + "reversal": "Historical bill reversal", + "generic": "Settlement supporting document" + }, "category": { "all": "All", "player": "Player bills", @@ -179,12 +293,12 @@ "badDebt": { "title": "Bad debt" } }, "empty": { - "noSite": "Select an integration site.", - "noPeriods": "Open and close a period under Periods first.", + "noSite": "Select a site.", + "noPeriods": "Close the current period 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." + "noBadDebt": "No bad debt records.", + "noCreditLedger": "No ledger entries in this period.", + "billsNeedClose": "Bills are created after period close." }, "periods": { "loadFailed": "Failed to load periods" diff --git a/src/i18n/locales/en/wallet.json b/src/i18n/locales/en/wallet.json index 732442f..9473a48 100644 --- a/src/i18n/locales/en/wallet.json +++ b/src/i18n/locales/en/wallet.json @@ -3,9 +3,9 @@ "subnavLabel": "Wallet sub pages", "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", + "scopeHint": "This area is for main-site wallet mode (wallet txns and transfers). For credit-line period settlement, see", + "scopeHintSettlementLink": "Settlement center", + "scopeHintSettlement": "Settlement center", "ledgerChannel": "Ledger", "ledgerCredit": "Credit ledger", "ledgerWallet": "Wallet txn", diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index 342cd2b..9fe9572 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -17,6 +17,7 @@ "rulesPlaysTitle": "खेल नियम", "rulesOddsTitle": "बाधा र रिबेट", "rulesOddsDescription": "बाधा म्याट्रिक्स र रिबेट दर एउटै पृष्ठमा, एउटै बाधा संस्करण लाइनमा।", + "rulesOddsDescriptionShort": "बायाँबाट खेल छान्नुहोस्, दायाँबाट बाधा र रिबेट सम्पादन गर्नुहोस्, त्यसपछि सेभ र प्रकाशन गर्नुहोस्।", "riskCapTitle": "जोखिम क्याप संस्करण" }, "hub": { @@ -394,6 +395,16 @@ "oddsConfig": "बाधा सेटिङ" }, "currentSelection": "हालको छनोट: {{category}} / {{play}}", + "playSelectPlaceholder": "खेल प्रकार छान्नुहोस्", + "readOnlyBanner": "यो संस्करण पढ्न मात्र हो। बाधा र रिबेट सम्पादन गर्न ड्राफ्ट बनाउनुहोस्।", + "table": { + "prizeScope": "पुरस्कार दायरा", + "multiplier": "बाधा गुणक" + }, + "draftBar": { + "unsaved": "नसेभ परिवर्तनहरू", + "saved": "परिवर्तन स्थानीय ड्राफ्टमा राखिएको छ" + }, "playGroups": { "bigSmall": "ठूलो / सानो", "combo4": "4D स्थिति", @@ -403,10 +414,13 @@ }, "summary": { "title": "सारांश", - "version": "संस्करण", + "contextTitle": "संस्करण र सुझाव", + "version": "सम्पादन संस्करण", + "activeVersion": "सक्रिय संस्करण", "statusLabel": "स्थिति", "readOnlyTag": "पढ्न मात्र", "readOnlyHint": "यो संस्करण पढ्न मात्र हो। परिवर्तन गर्न ड्राफ्ट बनाउनुहोस्।", + "draftHint": "प्रकाशन अघि ड्राफ्ट सेभ गर्नुहोस्; प्रकाशनले नयाँ टिकटमा मात्र असर गर्छ।", "activeHint": "यो संस्करण सक्रिय छ; नयाँ टिकट यही सेटिङ प्रयोग गर्छ।" }, "tabs": { diff --git a/src/i18n/locales/ne/reconcile.json b/src/i18n/locales/ne/reconcile.json index dc28da4..0e0243f 100644 --- a/src/i18n/locales/ne/reconcile.json +++ b/src/i18n/locales/ne/reconcile.json @@ -23,6 +23,7 @@ "playerAllPlayersHint": "खेलाडी नछानेमा, छनोट गरिएको मिति दायराभित्र सबै खेलाडीका लागि मिलान चलाइनेछ।", "createSummaryAll": "{{from}} देखि {{to}} सम्म सबै खेलाडीका लागि म्यानुअल मिलान चलाइनेछ।", "createSummaryPlayer": "खेलाडी {{player}} का लागि {{from}} देखि {{to}} सम्म म्यानुअल मिलान चलाइनेछ।", + "createSummaryPending": "कार्य सिर्जना गर्नु अघि पूरा मिलान मिति दायरा छान्नुहोस्।", "jobsTitle": "मिलान कार्यहरू", "jobsDesc": "दायाँपट्टिको कार्यबाट विवरण खोल्नुहोस्।", "refresh": "रिफ्रेस", diff --git a/src/i18n/locales/zh/agents.json b/src/i18n/locales/zh/agents.json index 43b0d27..6e6411b 100644 --- a/src/i18n/locales/zh/agents.json +++ b/src/i18n/locales/zh/agents.json @@ -164,7 +164,7 @@ "periodPlaceholder": "选择账期", "allPeriods": "全部账期", "filteredByPeriodRange": "账期 {{range}} 的账单", - "emptyNoPeriodsManage": "尚无账期与账单。请在下方「账期管理」点「本周」开期,到期后关账,账单会自动出现在这里。", + "emptyNoPeriodsManage": "尚无账单。请在结算中心完成「关账出账」后,账单会出现在此处。", "emptyNoPeriodsAgent": "尚无账单。账期由上级或平台关账后自动生成,无需您手动筛选时间。", "emptyNoClosed": "当前没有已关账的账期,账单尚未生成。请等待负责人关账后再查看。", "typePlayer": "玩家账单", @@ -273,7 +273,7 @@ "range": "账期", "statusOpen": "进行中", "statusClosed": "已关账", - "empty": "尚无账期。点「本周」等快捷账期后开期,到期后在此关账。", + "empty": "尚无账期,请选择快捷账期开期。", "start": "开始", "end": "结束", "status": "状态", diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index b57ee44..40c2dc6 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -17,6 +17,7 @@ "rulesPlaysTitle": "投注规则", "rulesOddsTitle": "赔率与回水", "rulesOddsDescription": "赔率矩阵与回水比例在同一页维护,共用赔率版本线。", + "rulesOddsDescriptionShort": "左侧选玩法,右侧改赔率与回水;修改后记得保存草稿并发布。", "riskCapTitle": "限额版本" }, "hub": { @@ -420,6 +421,16 @@ "oddsConfig": "赔率配置" }, "currentSelection": "当前选择:{{category}} / {{play}}", + "playSelectPlaceholder": "选择玩法", + "readOnlyBanner": "当前版本只读,需先创建草稿才能修改赔率与回水。", + "table": { + "prizeScope": "奖级范围", + "multiplier": "赔率倍数" + }, + "draftBar": { + "unsaved": "有未保存的修改", + "saved": "修改已同步到本地草稿" + }, "playGroups": { "bigSmall": "大小类", "combo4": "组合类", @@ -429,10 +440,13 @@ }, "summary": { "title": "配置摘要", - "version": "版本", + "contextTitle": "版本与提示", + "version": "当前编辑版本", + "activeVersion": "线上生效版本", "statusLabel": "状态", "readOnlyTag": "只读", "readOnlyHint": "当前为只读版本,如需修改请先创建草稿。", + "draftHint": "草稿中的修改需保存后才会写入版本;发布后对后续新注单生效。", "activeHint": "当前版本已生效,新注单将按此配置计算。" }, "tabs": { diff --git a/src/i18n/locales/zh/reconcile.json b/src/i18n/locales/zh/reconcile.json index 3260f12..673d42d 100644 --- a/src/i18n/locales/zh/reconcile.json +++ b/src/i18n/locales/zh/reconcile.json @@ -27,6 +27,7 @@ "playerAllPlayersHint": "不选择玩家时,会按日期范围对全量玩家做一次人工对账。", "createSummaryAll": "将对 {{from}} 至 {{to}} 的全量玩家发起人工对账。", "createSummaryPlayer": "将对玩家 {{player}} 在 {{from}} 至 {{to}} 的数据发起人工对账。", + "createSummaryPending": "请选择完整的对账日期范围后,再创建任务。", "jobsTitle": "对账任务", "jobsDesc": "在右侧操作中查看差异明细与分页。", "refresh": "刷新", diff --git a/src/i18n/locales/zh/settlementCenter.json b/src/i18n/locales/zh/settlementCenter.json index 35badb6..0452cb3 100644 --- a/src/i18n/locales/zh/settlementCenter.json +++ b/src/i18n/locales/zh/settlementCenter.json @@ -1,26 +1,55 @@ { "title": "结算中心", - "header": { - "subtitle": "信用占成账务", - "statusRunning": "账期进行中", - "statusIdle": "等待开期", - "statusCompleted": "账期已结清" + "subtitle": "账期关账、账单确认与收付登记", + "subtitleList": "账期列表:开账、关账;列表已含账期汇总,行内「查看详情」进入账单与下注流水。", + "period": { + "title": "账期", + "statusCompleted": "已结清", + "pipelineShare": "流水 {{count}} 笔", + "billTodo": "待确认 {{p}} · 待收付 {{a}}", + "openTitle": "开账", + "openBtn": "开账", + "closeNeedLedger": "本期暂无流水,请先完成开奖结算。", + "closeDialogTitle": "确认关账", + "closeDialogDesc": "将汇总 {{range}} 内的流水并生成账单。", + "closeDialogShare": "流水 {{count}} 笔", + "closeDialogEmpty": "本期暂无占成流水,关账后不会生成账单。", + "closeDialogUnsettled": "仍有 {{count}} 笔注单未结算", + "closeDialogIrreversible": "关账后不可撤销,差错请通过调账或冲正处理。", + "closeDialogConfirm": "确认关账" }, - "subnav": { - "label": "结算中心导航" + "periodDetail": { + "back": "返回账期列表", + "notFound": "账期不存在或已切换站点,请返回列表。" + }, + "periodTable": { + "title": "账期管理", + "statusFilter": "状态", + "range": "账期", + "ledgerCount": "流水", + "pending": "待确认", + "awaiting": "待收付", + "shareLedger": "占成流水", + "gameWinLoss": "输赢合计", + "platformWinLoss": "平台输赢", + "agentWinLoss": "代理输赢", + "basicRebate": "退水合计", + "unsettledTickets": "未结算注单", + "openReportHint": "进行中账期:占成/输赢来自账期内流水;账单数在关账后更新。", + "viewDetail": "查看详情", + "close": "关账", + "closeNow": "立即关账", + "hasOpen": "已有进行中账期 {{range}},须先关账才能开新期。", + "emptyOpenHint": "暂无账期,请点击工具栏「开账」创建。", + "emptyReadOnly": "暂无账期记录。", + "emptyFiltered": "筛选结果为空,请重置筛选。", + "emptyFilteredOpen": "当前筛选未包含进行中的账期,请选「全部」或「进行中」。", + "readOnlyHint": "绑定代理账号不可开/关账期,仅可查看与收付。" }, "nav": { - "aria": "结算中心导航", - "group": { - "hub": "工作台", - "finance": "账务", - "ledger": "账务流水", - "bills": "账单管理" - }, - "overview": "概览", - "periods": "账期管理", - "ledger": "账务流水", + "periods": "账期", "bills": "账单", + "ledger": "账务流水", "creditLedger": "信用流水", "playerBills": "玩家账单", "agentBills": "代理账单", @@ -33,6 +62,7 @@ }, "filters": { "period": "账期范围", + "statusAll": "全部", "allPeriods": "全部账期", "statusOpen": "进行中", "statusClosed": "已关账", @@ -54,7 +84,9 @@ "badDebtIntro": "坏账核销产生的调账流水,关联原账单。" }, "creditLedger": { - "intro": "账期内信用占用、账单收付、调账与坏账等流水;行内「⋯」可确认账单、登记收付、调账、冲正或坏账(需关联账单)。", + "periodIntro": "待开奖显示「下注冻结」(非扣款);已开奖每注单仅一条「开奖结算」输赢,不会与冻结重复出现。", + "emptyPeriod": "本账期暂无下注流水。请确认信用盘玩家已在账期内下注并完成开奖结算。", + "intro": "查询账期内的额度变动、收付与调账记录。", "columns": { "txn": "流水号", "player": "玩家", @@ -82,15 +114,30 @@ }, "reason": { "payment_record": "账单收付", - "bet_hold": "下注占用", + "bet_hold": "下注冻结", + "freezeAmount": "冻结 {{amount}}", + "game_settlement": "开奖结算", + "game_settlement_win": "开奖结算入账", "bet_hold_release": "占用释放", "game_settlement_loss": "开奖结算扣款", - "settlement_confirm": "账期结算确认" + "settlement_payout": "结算收付入账", + "settlement_confirm": "账期结算确认", + "adjustment": "补差", + "reversal": "冲正", + "bad_debt": "坏账核销", + "share_ledger": "占成流水" } }, "columns": { + "billId": "账单 ID", "period": "账期", "type": "类型", + "playerAccount": "玩家账号", + "playerId": "玩家 ID", + "directAgent": "直属代理", + "superiorAgent": "上级代理", + "play": "玩法", + "drawNo": "期号", "owner": "本方", "counterparty": "对方", "gross": "输赢", @@ -132,6 +179,51 @@ "viewBill": "查看账单", "billDetail": "账单详情" }, + "billDisplay": { + "settlementFlow": "谁付谁", + "settlementAmount": "结算金额", + "pays": "应付", + "paysShort": "应付", + "howAmountWorks": "金额怎么来的", + "playerBreakdownIntro": "玩家只与直属代理结算,净额 = 输赢 − 回水。", + "agentBreakdownIntro": "代理只与直属上级结算,净额 = 团队净额 − 本级占成。", + "playerGross": "游戏输赢", + "playerLostHint": "玩家输了,应付代理", + "playerWonHint": "玩家赢了,代理应付玩家", + "playerNet": "玩家应付净额", + "playerNetReceive": "代理应付玩家", + "teamGross": "团队游戏输赢", + "teamGrossHint": "含本级及下级玩家的合计", + "teamGrossShort": "团队", + "playerGrossShort": "玩家", + "teamRebate": "团队回水", + "teamNet": "团队净额", + "rebate": "回水", + "agentShareKeep": "{{agent}} 本级占成", + "agentShareKeepHint": "本级按占成比例留下的利润", + "agentNet": "{{agent}} 应付上级", + "agentNetReceive": "上级应付 {{agent}}", + "billOwner": "账单主体", + "billCounterparty": "结算对手", + "unpaidPendingConfirm": "确认账单后可登记收付", + "unpaidAwaitingPayment": "请登记线下收付", + "fullySettled": "本期已结清", + "recordReceiptFrom": "登记收款({{payer}} 付给 {{payee}})", + "recordPayoutTo": "登记付款({{payer}} 付给 {{payee}})", + "rebateAllocationsHint": "各层级代理对回水的承担明细。", + "payment": "收付", + "flowHint": { + "playerPayAgent": "玩家应向直属代理结算", + "agentPayPlayer": "直属代理应向玩家结算", + "agentPayUpstream": "本级代理应向上级 / 平台结算", + "upstreamPayAgent": "上级 / 平台应向本级代理结算", + "adjustment": "补差单独结转,不改变原账单主体关系", + "badDebt": "核销未结金额,并生成坏账归档记录", + "reversal": "冲正原账单影响,按冲正规则回退", + "generic": "按账单结算关系执行收付或调账" + }, + "hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。" + }, "ledgerPanel": { "search": "搜索", "searchBtn": "搜索", @@ -144,30 +236,67 @@ "optional": "可选", "billStatus": "账单状态", "dateRange": "时间范围", - "rowPosted": "已记账", - "category": { - "all": "全部", - "credit": "信用占用", - "payment": "收付", - "adjustment": "调账 / 冲正", - "badDebt": "坏账", - "actionable": "待操作" - } + "rowPosted": "已记账" }, "billsPanel": { - "intro": "关账后生成的占成账单;可按类型与状态筛选,打开详情进行确认与收付。", + "billId": "账单 ID", + "ownerKeyword": "本方 / 对方", + "ownerKeywordPh": "玩家账号、代理名称", + "status": "账单状态", + "billType": "账单类型", + "filterAll": "全部状态", + "filterAllTypes": "全部类型", + "filterAdjustment": "调账 / 冲正", + "optional": "可选", + "searchBtn": "搜索", + "reset": "重置", + "refresh": "刷新", + "clientFilterHint": "本方筛选:显示 {{shown}} / {{total}} 条", "category": { - "all": "全部", "player": "玩家账单", "agent": "代理账单", "pendingConfirm": "待确认", "awaitingPayment": "待收付" - } + }, + "quickFilter": { + "title": "当前想看哪一层结算", + "desc": "先区分结玩家还是结代理,再做状态和账单检索会更直观。", + "allTitle": "全部账单", + "allHint": "同时查看玩家、代理、补差和坏账单据", + "playerTitle": "结玩家", + "playerHint": "玩家与直属代理之间的信用结算", + "agentTitle": "结代理", + "agentHint": "代理与上级 / 平台之间的层级结算" + }, + "activeHint": { + "all": "当前聚焦:账期内全部结算单据", + "player": "当前聚焦:玩家与直属代理的信用结算", + "agent": "当前聚焦:代理向上级 / 平台缴纳或收取结算" + }, + "layer": { + "player": "玩家与直属代理结算", + "agent": "代理与上级 / 平台结算", + "adjustment": "结算差异调账", + "badDebt": "坏账核销归档", + "reversal": "历史账单冲正", + "generic": "结算辅助单据" + }, + "rowHint": { + "playerOwner": "玩家主体", + "agentOwner": "本级代理", + "adjustmentOwner": "调账归属方", + "badDebtOwner": "坏账归属方", + "reversalOwner": "冲正归属方", + "playerUpline": "直属代理的上级", + "agentUpline": "本单结算上级" + }, + "hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。" }, "panels": { + "workbench": { "title": "工作台" }, "overview": { "title": "结算概览" }, "ledger": { "title": "账务流水" }, - "bills": { "title": "账单" }, + "bills": { "title": "全部账单" }, "creditLedger": { "title": "信用流水" }, "playerBills": { "title": "玩家账单" }, "agentBills": { "title": "代理账单" }, @@ -178,13 +307,17 @@ "reports": { "title": "账期报表" }, "badDebt": { "title": "坏账核销" } }, + "billsPanel": { + "emptyFiltered": "当前筛选下暂无账单,请改为「全部状态」或重置筛选。", + "emptyClosed": "本期已关账但暂无账单。常见原因:账期内无信用盘玩家的已结算注单,或占成流水不在本账期时间范围内。" + }, "empty": { - "noSite": "请选择接入站点。", - "noPeriods": "请先在「账期管理」开期并关账。", + "noSite": "请选择站点。", + "noPeriods": "请先关账当前账期。", "noClosed": "请先关账生成账单。", - "noBadDebt": "暂无坏账核销记录。", - "noCreditLedger": "所选账期内暂无信用流水。请确认信用盘玩家已下注且账期时间范围正确。", - "billsNeedClose": "占成账单须先关账才会出现;若上方「信用流水」有数据而账单为空,请完成开奖结算后执行关账。" + "noBadDebt": "暂无坏账记录。", + "noCreditLedger": "该账期内暂无流水。", + "billsNeedClose": "账单在关账后生成。请返回账期列表,对本期执行「关账」后再查看。" }, "periods": { "loadFailed": "账期列表加载失败" diff --git a/src/i18n/locales/zh/wallet.json b/src/i18n/locales/zh/wallet.json index bd73df0..04bcb68 100644 --- a/src/i18n/locales/zh/wallet.json +++ b/src/i18n/locales/zh/wallet.json @@ -3,9 +3,9 @@ "subnavLabel": "钱包子页", "subnavTransactions": "主站钱包流水", "subnavTransferOrders": "主站转账单", - "scopeHint": "本模块为主站钱包模式:钱包流水与主站转账单。信用盘玩家的下注占用、结算记账请查看", - "scopeHintSettlementLink": "结算中心 → 信用流水", - "scopeHintSettlement": "结算中心 → 信用流水", + "scopeHint": "本模块为主站钱包模式:钱包流水与主站转账单。信用盘玩家的账期结账请查看", + "scopeHintSettlementLink": "结算中心", + "scopeHintSettlement": "结算中心", "ledgerChannel": "账本", "ledgerCredit": "信用流水", "ledgerWallet": "钱包流水", diff --git a/src/lib/admin-select-display.ts b/src/lib/admin-select-display.ts new file mode 100644 index 0000000..195ea72 --- /dev/null +++ b/src/lib/admin-select-display.ts @@ -0,0 +1,51 @@ +/** Base UI Select 会直接显示 `value`;筛选用哨兵时需手动映射展示文案。 */ +export const ADMIN_SELECT_FILTER_ALL = "__all__"; + +export function isAdminSelectFilterAll(raw: unknown): boolean { + const v = raw == null ? "" : String(raw); + return v === "" || v === ADMIN_SELECT_FILTER_ALL; +} + +export function adminSelectFilterLabel( + raw: unknown, + allLabel: string, + resolveOptionLabel: (value: string) => string, +): string { + const v = raw == null ? "" : String(raw); + if (isAdminSelectFilterAll(v)) { + return allLabel; + } + return resolveOptionLabel(v); +} + +export type AdminSiteOption = { code: string; name?: string | null }; + +export function adminSiteSelectLabel( + raw: unknown, + sites: readonly AdminSiteOption[], + allLabel: string, +): string { + return adminSelectFilterLabel(raw, allLabel, (code) => { + const site = sites.find((s) => s.code === code); + if (!site) { + return code; + } + return site.name ? `${site.name} (${site.code})` : site.code; + }); +} + +export function adminSiteCodeLabel( + raw: unknown, + sites: readonly AdminSiteOption[], + placeholder: string, +): string { + const v = raw == null ? "" : String(raw); + if (v === "") { + return placeholder; + } + const site = sites.find((s) => s.code === v); + if (!site) { + return v; + } + return site.name ? `${site.name} (${site.code})` : site.code; +} diff --git a/src/lib/admin-site-display.ts b/src/lib/admin-site-display.ts new file mode 100644 index 0000000..062f373 --- /dev/null +++ b/src/lib/admin-site-display.ts @@ -0,0 +1,13 @@ +/** 后台展示用站点名:优先中文名,不暴露内部 code。 */ +export function formatAdminSiteLabel( + name?: string | null, + code?: string | null, +): string { + const trimmedName = name?.trim(); + if (trimmedName) { + return trimmedName; + } + + const trimmedCode = code?.trim(); + return trimmedCode ?? "—"; +} diff --git a/src/lib/admin-status-tone.ts b/src/lib/admin-status-tone.ts index d025f5e..854c11c 100644 --- a/src/lib/admin-status-tone.ts +++ b/src/lib/admin-status-tone.ts @@ -58,6 +58,11 @@ const STATUS_TONE_MAP: Record = { completed: "success", failed: "danger", + // 代理账期账单 + confirmed: "warning", + partial_paid: "warning", + overdue: "danger", + // 注单 pending_confirm: "info", partial_pending_confirm: "warning", diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index b4fd4d9..82b1a98 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -2,20 +2,25 @@ import type { LucideIcon } from "lucide-react"; import { CalendarClock, CircleDollarSign, + ClipboardList, + Coins, FileSpreadsheet, Globe, + KeyRound, Landmark, LayoutDashboard, LogIn, Network, + Percent, Scale, ScrollText, Receipt, Settings, ShieldAlert, - ShieldCheck, SlidersHorizontal, Ticket, + Trophy, + UserCog, Users, Wallet, } from "lucide-react"; @@ -29,9 +34,9 @@ export const adminNavIconBySegment: Record agents: Network, players: Users, draws: CalendarClock, - rules_plays: SlidersHorizontal, - rules_odds: SlidersHorizontal, - jackpot: CircleDollarSign, + rules_plays: ClipboardList, + rules_odds: Percent, + jackpot: Trophy, risk_cap: ShieldAlert, tickets: Ticket, wallet: Wallet, @@ -41,9 +46,9 @@ export const adminNavIconBySegment: Record settlement_center: Receipt, reconcile: Scale, audit: ScrollText, - admin_users: ShieldCheck, - admin_roles: ShieldCheck, - currencies: CircleDollarSign, + admin_users: UserCog, + admin_roles: KeyRound, + currencies: Coins, integration: Globe, settings: Settings, }; diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index 63e52d3..7a866f3 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -37,6 +37,7 @@ export type AdminNavItem = { segment: AdminNavSegment; nav_group?: AdminNavGroup; platform_only?: boolean; + agent_hidden?: boolean; activeMatchPrefix?: string; requiredAny?: readonly string[]; }; diff --git a/src/modules/agents/agent-line-detail-panel.tsx b/src/modules/agents/agent-line-detail-panel.tsx index 6694d16..90e5147 100644 --- a/src/modules/agents/agent-line-detail-panel.tsx +++ b/src/modules/agents/agent-line-detail-panel.tsx @@ -4,6 +4,7 @@ import type { ComponentType } from "react"; import { ChevronRight, Network, Pencil, Plus, Trash2, Users } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { AdminSubnav, AdminSubnavButton } from "@/components/admin/admin-subnav"; 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"; @@ -218,19 +219,23 @@ export function AgentLineDetailPanel({
-
+ {tabs .filter((tab) => tab.visible) .map((tab) => ( - onDetailTabChange(tab.key)} - label={tab.label} count={tab.count} - /> + > + {tab.label} + ))} -
+
{detailTab === "overview" ? ( @@ -569,7 +574,7 @@ function DownlineTable({ {child.email ?? "—"} - {summary ? `${ratioToPercentUi(summary.total_share_rate)}%` : "—"} + {summary ? `${summary.total_share_rate ?? 0}%` : "—"} {summary ? formatCredit(summary.credit_limit) : "—"} @@ -660,40 +665,3 @@ function MetricCard({
); } - -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 abfbff2..3c17243 100644 --- a/src/modules/agents/agent-line-provision-wizard.tsx +++ b/src/modules/agents/agent-line-provision-wizard.tsx @@ -21,6 +21,7 @@ import { import { Switch } from "@/components/ui/switch"; import { useAsyncEffect } from "@/hooks/use-async-effect"; import { percentUiToRatio } from "@/lib/admin-rate-percent"; +import { adminSiteCodeLabel } from "@/lib/admin-select-display"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminIntegrationSiteRow } from "@/types/api/admin-integration-site"; @@ -128,19 +129,23 @@ export function AgentLineProvisionWizard(): React.ReactElement { disabled={sitesLoading || unboundSites.length === 0} > - + {(v) => + adminSiteCodeLabel( + v, + unboundSites, + sitesLoading + ? t("common:loading", { defaultValue: "加载中…" }) + : unboundSites.length === 0 + ? t("agents:lineProvision.noUnboundSite", { + defaultValue: "暂无未绑定一级代理的站点", + }) + : t("agents:lineProvision.siteCodePlaceholder", { + defaultValue: "选择站点", + }), + ) } - /> + {unboundSites.map((site) => ( @@ -248,7 +253,15 @@ export function AgentLineProvisionWizard(): React.ReactElement { } > - + + {(v) => + v === "daily" + ? t("agents:profile.cycleDaily", { defaultValue: "日结" }) + : v === "monthly" + ? t("agents:profile.cycleMonthly", { defaultValue: "月结" }) + : t("agents:profile.cycleWeekly", { defaultValue: "周结" }) + } + diff --git a/src/modules/agents/agent-profile-fields.tsx b/src/modules/agents/agent-profile-fields.tsx index 1df8ac4..c645980 100644 --- a/src/modules/agents/agent-profile-fields.tsx +++ b/src/modules/agents/agent-profile-fields.tsx @@ -122,7 +122,9 @@ export function AgentProfileFields({ >
onShareRateChange(e.target.value)} /> + {parentCaps && shareRate ? ( +

+ {t("profile.actualShareRate", { + defaultValue: "实际占成 {{rate}}%", + rate: Number((Number(parentCaps.total_share_rate) * Number(shareRate) / 100).toFixed(2)), + })} +

+ ) : null}
+ + ); +} diff --git a/src/modules/config/doc/odds-config-play-nav.tsx b/src/modules/config/doc/odds-config-play-nav.tsx new file mode 100644 index 0000000..d8d90bb --- /dev/null +++ b/src/modules/config/doc/odds-config-play-nav.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useTranslation } from "react-i18next"; + +import { ConfigChip, ConfigChipGroup } from "@/modules/config/config-chip-group"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + buildOddsPlayFilterGroups, + filterOddsPlayTypesByCategory, + type OddsCategoryTab, +} from "@/modules/config/doc/odds-play-type-groups"; +import { resolveAdminPlayTypeDisplayName } from "@/lib/admin-play-types"; +import { cn } from "@/lib/utils"; +import type { AdminPlayTypeRow } from "@/types/api/admin-config"; + +const PLAY_SELECT_NONE = "__none__"; + +type OddsConfigPlayNavProps = { + catTab: OddsCategoryTab; + onCatTabChange: (tab: OddsCategoryTab) => void; + onPlayCodeChange: (code: string) => void; + types: AdminPlayTypeRow[]; + resolvedPlayCode: string; + className?: string; +}; + +export function OddsConfigPlayNav({ + catTab, + onCatTabChange, + onPlayCodeChange, + types, + resolvedPlayCode, + className, +}: OddsConfigPlayNavProps) { + const { t, i18n } = useTranslation("config"); + + const catTabs = [ + { id: "all" as const, label: t("odds.tabs.all") }, + { id: "d4" as const, label: "4D" }, + { id: "d3" as const, label: "3D" }, + { id: "d2" as const, label: "2D" }, + ]; + + const filteredTypes = filterOddsPlayTypesByCategory(catTab, types); + const playGroups = buildOddsPlayFilterGroups(catTab, types); + const playSelectValue = resolvedPlayCode || PLAY_SELECT_NONE; + + const playLabel = (code: string): string => { + const row = types.find((item) => item.play_code === code); + return resolveAdminPlayTypeDisplayName(code, i18n.language, row); + }; + + return ( +
+ + {catTabs.map((tab) => ( + onCatTabChange(tab.id)} + > + {tab.label} + + ))} + + + {/* 小屏:下拉快速切换玩法 */} +
+

{t("odds.playType")}

+ +
+ + {/* 大屏:侧栏玩法列表,点选即切换 */} + +
+ ); +} diff --git a/src/modules/config/doc/odds-config-summary-panel.tsx b/src/modules/config/doc/odds-config-summary-panel.tsx index c568956..d143fbd 100644 --- a/src/modules/config/doc/odds-config-summary-panel.tsx +++ b/src/modules/config/doc/odds-config-summary-panel.tsx @@ -5,28 +5,21 @@ import { useTranslation } from "react-i18next"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { ConfigStatusBadge } from "@/modules/config/config-status-badge"; -import { inferRebatePercentFromDimension, rateToPercentUi } from "@/modules/config/doc/odds-rebate-rates"; +import { rateToPercentUi } from "@/modules/config/doc/odds-rebate-rates"; import { prizeScopeLabel, type PrizeScopeCode } from "@/modules/config/doc/prize-scopes"; +import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { cn } from "@/lib/utils"; -import type { AdminPlayTypeRow, OddsItemRow, OddsVersionDetail } from "@/types/api/admin-config"; - -function oddsMultiplierLabel(oddsValue: number): string { - return (oddsValue / 10000).toFixed(4); -} - -type SummaryRow = { - label: string; - value: string; -}; +import type { ConfigVersionSummary, OddsItemRow, OddsVersionDetail } from "@/types/api/admin-config"; type OddsConfigSummaryPanelProps = { - catTabLabel: string; - playLabel: string; + catTabLabel?: string; + playLabel?: string; detail: OddsVersionDetail | null; - draftRows: OddsItemRow[]; - types: AdminPlayTypeRow[]; - scopeRows: Partial>; - playRebatePercent: string; + scopeRows?: Partial>; + playRebatePercent?: string; + activeHead?: ConfigVersionSummary | null; + /** 合并页:仅展示版本与操作提示,不重复主编辑区数值 */ + compact?: boolean; className?: string; }; @@ -34,63 +27,32 @@ export function OddsConfigSummaryPanel({ catTabLabel, playLabel, detail, - draftRows, - types, scopeRows, playRebatePercent, + activeHead = null, + compact = false, className, }: OddsConfigSummaryPanelProps) { const { t } = useTranslation("config"); + const formatDt = useAdminDateTimeFormatter(); const isDraft = detail?.status === "draft"; const isActive = detail?.status === "active"; - - const rows: SummaryRow[] = [ - { label: t("odds.category"), value: catTabLabel }, - { label: t("odds.playType"), value: playLabel || "—" }, - ]; - - for (const scope of ["first", "second", "third", "starter", "consolation"] as PrizeScopeCode[]) { - const row = scopeRows[scope]; - rows.push({ - label: prizeScopeLabel(scope, t), - value: row ? oddsMultiplierLabel(row.odds_value) : "—", - }); - } - - rows.push({ - label: t("odds.rebateRate"), - value: playRebatePercent, - }); - - rows.push( - { - label: t("rebate.fields.d2"), - value: inferRebatePercentFromDimension(2, draftRows, types), - }, - { - label: t("rebate.fields.d3"), - value: inferRebatePercentFromDimension(3, draftRows, types), - }, - { - label: t("rebate.fields.d4"), - value: inferRebatePercentFromDimension(4, draftRows, types), - }, - ); - const versionLabel = detail ? `v${detail.version_no}` : "—"; return (