From ce27a3ec8a6068ecd7c535e1d12314923b0943d3 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 3 Jun 2026 10:07:51 +0800 Subject: [PATCH] feat(admin, i18n): enhance admin dashboard and user management with new features and translations Added the ability to filter admin dashboard data by site code and agent node ID, improving data retrieval capabilities. Introduced new functions for fetching dashboard data based on these parameters. Updated the admin users and roles management components to reflect these changes. Enhanced multi-language support by adding new translations for agent management and permission levels in English, Nepali, and Chinese, ensuring a consistent user experience across the admin interface. --- eslint.config.mjs | 19 + src/api/admin-dashboard.ts | 23 + src/api/admin-users.ts | 11 - .../admin/(shell)/reports/[category]/page.tsx | 26 + src/app/admin/(shell)/reports/layout.tsx | 14 + src/app/admin/(shell)/reports/page.tsx | 17 +- src/app/globals.css | 14 + src/components/admin/admin-agent-filter.tsx | 11 +- .../admin-permission-package-selector.tsx | 222 +++++++++ .../admin/admin-permission-selector.tsx | 200 ++++++++ src/components/admin/login-form.tsx | 1 - src/components/admin/toolbar.tsx | 1 - src/components/ui/table.tsx | 4 +- src/hooks/use-async-effect.ts | 1 - src/i18n/locales/en/adminUsers.json | 38 +- src/i18n/locales/en/agents.json | 1 + src/i18n/locales/en/audit.json | 7 +- src/i18n/locales/en/common.json | 3 +- src/i18n/locales/en/settlement.json | 1 + src/i18n/locales/en/tickets.json | 1 + src/i18n/locales/en/wallet.json | 11 +- src/i18n/locales/ne/adminUsers.json | 38 +- src/i18n/locales/ne/agents.json | 1 + src/i18n/locales/ne/audit.json | 7 +- src/i18n/locales/ne/common.json | 3 +- src/i18n/locales/ne/settlement.json | 1 + src/i18n/locales/ne/tickets.json | 1 + src/i18n/locales/ne/wallet.json | 11 +- src/i18n/locales/zh/adminUsers.json | 38 +- src/i18n/locales/zh/agents.json | 9 +- src/i18n/locales/zh/audit.json | 7 +- src/i18n/locales/zh/common.json | 3 +- src/i18n/locales/zh/settlement.json | 1 + src/i18n/locales/zh/tickets.json | 1 + src/i18n/locales/zh/wallet.json | 11 +- src/lib/admin-permission-packages.ts | 111 +++++ .../admin-roles/admin-roles-console.tsx | 165 +------ .../admin-users/admin-users-console.tsx | 10 +- src/modules/agents/agents-console.tsx | 455 ++++++++---------- src/modules/audit/audit-logs-console.tsx | 14 +- .../config/doc/risk-cap-doc-screen.tsx | 4 +- .../dashboard/dashboard-analytics-panel.tsx | 4 +- src/modules/dashboard/dashboard-console.tsx | 10 +- src/modules/dashboard/dashboard-visuals.tsx | 2 +- .../dashboard/use-dashboard-analytics.ts | 8 +- src/modules/draws/draw-review-console.tsx | 4 +- src/modules/draws/draws-index-console.tsx | 27 +- .../integration/integration-sites-console.tsx | 4 +- src/modules/players/players-console.tsx | 98 ++-- src/modules/reconcile/reconcile-console.tsx | 6 +- src/modules/reports/report-jobs-panel.tsx | 4 +- src/modules/reports/reports-console.tsx | 77 ++- src/modules/reports/reports-subnav.tsx | 40 ++ src/modules/risk/risk-index-console.tsx | 4 +- src/modules/risk/risk-pools-console.tsx | 4 +- .../settings/currency-settings-panel.tsx | 4 +- .../panels/settlement-settings-panel.tsx | 8 + .../settlement-batch-details-console.tsx | 22 +- .../settlement/settlement-batches-console.tsx | 24 +- .../tickets/player-tickets-console.tsx | 87 +--- src/modules/update_sticky_actions.js | 67 +++ src/modules/wallet/wallet-console.tsx | 49 +- src/modules/wallet/wallet-subnav.tsx | 2 + src/types/api/admin-audit.ts | 5 +- src/types/api/admin-auth.ts | 2 + src/types/api/admin-dashboard-analytics.ts | 2 + 66 files changed, 1361 insertions(+), 720 deletions(-) create mode 100644 src/app/admin/(shell)/reports/[category]/page.tsx create mode 100644 src/app/admin/(shell)/reports/layout.tsx create mode 100644 src/components/admin/admin-permission-package-selector.tsx create mode 100644 src/components/admin/admin-permission-selector.tsx create mode 100644 src/lib/admin-permission-packages.ts create mode 100644 src/modules/reports/reports-subnav.tsx create mode 100644 src/modules/update_sticky_actions.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..4b338a2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,10 +1,29 @@ import { defineConfig, globalIgnores } from "eslint/config"; import nextVitals from "eslint-config-next/core-web-vitals"; import nextTs from "eslint-config-next/typescript"; +import reactHooks from "eslint-plugin-react-hooks"; const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, + { + plugins: { + "react-hooks": reactHooks, + }, + rules: { + // Keep strict linting back on; only compiler-adjacent legacy rules stay relaxed for now. + "react-hooks/set-state-in-effect": "off", + "react-hooks/static-components": "off", + "react-hooks/refs": "off", + "react-hooks/use-memo": "off", + }, + }, + { + files: ["**/*.cjs"], + rules: { + "@typescript-eslint/no-require-imports": "off", + }, + }, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: diff --git a/src/api/admin-dashboard.ts b/src/api/admin-dashboard.ts index a932269..830e4c6 100644 --- a/src/api/admin-dashboard.ts +++ b/src/api/admin-dashboard.ts @@ -8,12 +8,29 @@ import type { } from "@/types/api/admin-dashboard-analytics"; const A = `/admin`; +const DASHBOARD_SCOPE_SITE_PARAM = "site_code"; +const DASHBOARD_SCOPE_AGENT_PARAM = "agent_node_id"; /** 首页仪表盘聚合(大厅 + 当期财务/风控/异常转账等,按账号权限填充各块) */ export async function getAdminDashboard(): Promise { return adminRequest.get(`${A}/dashboard`); } +export async function getAdminDashboardByScope(scope: { + site_code?: string; + agent_node_id?: number; +}): Promise { + const params = new URLSearchParams(); + if (scope.site_code && scope.site_code.trim() !== "") { + params.set(DASHBOARD_SCOPE_SITE_PARAM, scope.site_code.trim()); + } + if (scope.agent_node_id && Number.isInteger(scope.agent_node_id) && scope.agent_node_id > 0) { + params.set(DASHBOARD_SCOPE_AGENT_PARAM, String(scope.agent_node_id)); + } + const qs = params.toString(); + return adminRequest.get(`${A}/dashboard${qs ? `?${qs}` : ""}`); +} + /** 仪表盘可筛选分析(区间汇总、日趋势、玩法拆解) */ export async function getAdminDashboardAnalytics( query: AdminDashboardAnalyticsQuery = {}, @@ -34,6 +51,12 @@ export async function getAdminDashboardAnalytics( if (query.play_code) { params.set("play_code", query.play_code); } + if (query.site_code && query.site_code.trim() !== "") { + params.set(DASHBOARD_SCOPE_SITE_PARAM, query.site_code.trim()); + } + if (query.agent_node_id && Number.isInteger(query.agent_node_id) && query.agent_node_id > 0) { + params.set(DASHBOARD_SCOPE_AGENT_PARAM, String(query.agent_node_id)); + } const qs = params.toString(); return adminRequest.get( diff --git a/src/api/admin-users.ts b/src/api/admin-users.ts index 6a60c42..cb04191 100644 --- a/src/api/admin-users.ts +++ b/src/api/admin-users.ts @@ -12,7 +12,6 @@ import type { AdminUserDeleteResult, AdminUserPermissionListData, AdminUserPermissionRow, - AdminUserPermissionSyncData, AdminUserRoleSyncData, AdminUserUpdatePayload, } from "@/types/api/admin-user"; @@ -80,16 +79,6 @@ export async function putAdminRolePermissions( }); } -export async function putAdminUserPermissions( - adminUserId: number, - permissionSlugs: string[], -): Promise { - return adminRequest.put( - `${A}/admin-users/${adminUserId}/permissions`, - { permission_slugs: permissionSlugs }, - ); -} - export async function putAdminUserRoles( adminUserId: number, roleSlugs: string[], diff --git a/src/app/admin/(shell)/reports/[category]/page.tsx b/src/app/admin/(shell)/reports/[category]/page.tsx new file mode 100644 index 0000000..051f8dc --- /dev/null +++ b/src/app/admin/(shell)/reports/[category]/page.tsx @@ -0,0 +1,26 @@ +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"; + +type Category = "profit" | "wallet" | "risk" | "audit"; + +export const metadata: Metadata = buildPageMetadata("reports", "title"); + +export default async function AdminReportsCategoryPage({ + params, +}: { + params: Promise<{ category: string }>; +}) { + const { category } = await params; + if (!["profit", "wallet", "risk", "audit"].includes(category)) { + notFound(); + } + return ( + + + + ); +} diff --git a/src/app/admin/(shell)/reports/layout.tsx b/src/app/admin/(shell)/reports/layout.tsx new file mode 100644 index 0000000..ecd3a14 --- /dev/null +++ b/src/app/admin/(shell)/reports/layout.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from "react"; +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { ReportsSubnav } from "@/modules/reports/reports-subnav"; + +export default function AdminReportsLayout({ children }: { children: ReactNode }) { + return ( + +
+ +
+ {children} +
+ ); +} diff --git a/src/app/admin/(shell)/reports/page.tsx b/src/app/admin/(shell)/reports/page.tsx index d501b59..ac34cb8 100644 --- a/src/app/admin/(shell)/reports/page.tsx +++ b/src/app/admin/(shell)/reports/page.tsx @@ -1,18 +1,5 @@ -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { ModuleScaffold } from "@/components/admin/module-scaffold"; -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", "title"); +import { redirect } from "next/navigation"; export default function AdminReportsPage() { - return ( - - - - - - ); + redirect("/admin/reports/profit"); } diff --git a/src/app/globals.css b/src/app/globals.css index 29d4258..15937a2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -168,6 +168,20 @@ @apply overflow-x-auto rounded-2xl border border-border/80 bg-card shadow-sm; } + /* Sticky columns need an opaque background so scrolled cells/headers do not show through */ + [data-slot="table-head"][class*="sticky"] { + @apply bg-muted; + } + + /* Match table body (white card), not page background (#f7fbff) or header muted tint */ + [data-slot="table-cell"][class*="sticky"] { + background-color: var(--card); + } + + [data-slot="table-row"]:hover > [data-slot="table-cell"][class*="sticky"] { + @apply bg-muted/35; + } + .admin-table-toolbar { @apply flex items-center justify-end border-b border-border/70 bg-muted/20 px-4 py-2.5; } diff --git a/src/components/admin/admin-agent-filter.tsx b/src/components/admin/admin-agent-filter.tsx index 59d0fca..6e1399f 100644 --- a/src/components/admin/admin-agent-filter.tsx +++ b/src/components/admin/admin-agent-filter.tsx @@ -55,6 +55,7 @@ export function AdminAgentFilter({ id = "admin-agent-filter", value, onChange, c }, [profile?.agent?.admin_site_id]); const selectValue = value ? String(value) : ALL; + const selectedLabel = options.find((opt) => String(opt.id) === selectValue)?.label; return (
@@ -67,7 +68,15 @@ export function AdminAgentFilter({ id = "admin-agent-filter", value, onChange, c disabled={loading || options.length === 0} > - + + {(raw) => { + const current = String(raw ?? ALL); + if (current === ALL) { + return t("agentColumns.filterAll"); + } + return selectedLabel ?? t("agentColumns.filterAll"); + }} + {t("agentColumns.filterAll")} diff --git a/src/components/admin/admin-permission-package-selector.tsx b/src/components/admin/admin-permission-package-selector.tsx new file mode 100644 index 0000000..ccbc86f --- /dev/null +++ b/src/components/admin/admin-permission-package-selector.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { useMemo } from "react"; + +import { Checkbox } from "@/components/ui/checkbox"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/utils"; +import { ADMIN_PERMISSION_PACKAGES } from "@/lib/admin-permission-packages"; +import type { AdminPermissionCatalogData } from "@/types/api/admin-user"; + +type PackageSelectorProps = { + catalog: AdminPermissionCatalogData | null; + selectedSlugs: string[]; + onChange: (next: string[]) => void; + resolveGroupLabel: (key: string, fallback: string) => string; + resolvePackageLabel: (key: string, fallback: string) => string; + selectableSlugs?: string[] | null; + helperText?: string; + summaryText?: string; + emptyText: string; + heightClassName?: string; +}; + +type RenderGroup = { + key: string; + label: string; + packages: Array<{ key: string; label: string; slugs: string[] }>; +}; + +const PACKAGE_LEVEL_ORDER: Record = { + view: 10, + review: 20, + export: 20, + manage: 30, + config: 30, + control: 30, + reopen: 30, + special: 40, +}; + +export function AdminPermissionPackageSelector({ + catalog, + selectedSlugs, + onChange, + resolveGroupLabel, + resolvePackageLabel, + selectableSlugs = null, + helperText, + summaryText, + emptyText, + heightClassName = "h-[52vh]", +}: PackageSelectorProps): React.ReactElement { + const selectedSet = useMemo(() => new Set(selectedSlugs), [selectedSlugs]); + const catalogSlugSet = useMemo( + () => new Set((catalog?.permissions ?? []).map((permission) => permission.slug)), + [catalog], + ); + const allowedSet = useMemo( + () => (selectableSlugs ? new Set(selectableSlugs) : null), + [selectableSlugs], + ); + + const groups = useMemo(() => { + const defs = catalog?.permission_menu_groups ?? []; + const out: RenderGroup[] = []; + for (const group of defs) { + const bundles = ADMIN_PERMISSION_PACKAGES[group.key] ?? []; + if (bundles.length === 0) { + continue; + } + const renderedPackages = bundles + .map((bundle) => { + const slugs = bundle.slugs.filter((slug) => { + if (!catalogSlugSet.has(slug)) { + return false; + } + if (allowedSet && !allowedSet.has(slug)) { + return false; + } + return true; + }); + return { + key: bundle.key, + label: resolvePackageLabel(bundle.key, bundle.label), + slugs, + }; + }) + .filter((bundle) => bundle.slugs.length > 0); + if (renderedPackages.length === 0) { + continue; + } + out.push({ + key: group.key, + label: resolveGroupLabel(group.key, group.label), + packages: renderedPackages, + }); + } + return out; + }, [allowedSet, catalog, catalogSlugSet, resolveGroupLabel, resolvePackageLabel]); + + const bundleCount = useMemo( + () => groups.reduce((sum, group) => sum + group.packages.length, 0), + [groups], + ); + + if (groups.length === 0 || bundleCount === 0) { + return ( +
+ {emptyText} +
+ ); + } + + 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); + } + 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); + } + onChange(Array.from(next).sort()); + }; + + return ( +
+ {helperText || summaryText ? ( +
+ {helperText} + {summaryText} +
+ ) : null} +
+ + + + + + + + + {groups.map((group) => { + const groupAllSlugs = group.packages.flatMap((p) => p.slugs); + const groupSelectedCount = groupAllSlugs.filter((slug) => selectedSet.has(slug)).length; + const groupChecked = groupSelectedCount === groupAllSlugs.length && groupAllSlugs.length > 0; + + return ( + + + + + ); + })} + +
模块具体权限
+ + +
+ {group.packages.map((bundle) => { + const checked = bundle.slugs.every((slug) => selectedSet.has(slug)); + return ( + + ); + })} +
+
+
+
+ ); +} diff --git a/src/components/admin/admin-permission-selector.tsx b/src/components/admin/admin-permission-selector.tsx new file mode 100644 index 0000000..46b13fc --- /dev/null +++ b/src/components/admin/admin-permission-selector.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { ChevronDown } from "lucide-react"; +import { useMemo, useState } from "react"; + +import { Checkbox } from "@/components/ui/checkbox"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import type { AdminPermissionCatalogData } from "@/types/api/admin-user"; + +type PermissionSelectorProps = { + catalog: AdminPermissionCatalogData | null; + selectedSlugs: string[]; + onChange: (next: string[]) => void; + resolveGroupLabel: (key: string, fallback: string) => string; + resolvePermissionLabel: (slug: string, fallback: string) => string; + selectableSlugs?: string[] | null; + helperText?: string; + summaryText?: string; + emptyText: string; + heightClassName?: string; +}; + +export function AdminPermissionSelector({ + catalog, + selectedSlugs, + onChange, + resolveGroupLabel, + resolvePermissionLabel, + selectableSlugs = null, + helperText, + summaryText, + emptyText, + heightClassName = "h-[52vh]", +}: PermissionSelectorProps): React.ReactElement { + const [expandedGroups, setExpandedGroups] = useState>({}); + const selectedSet = useMemo(() => new Set(selectedSlugs), [selectedSlugs]); + const allowedSet = useMemo( + () => (selectableSlugs ? new Set(selectableSlugs) : null), + [selectableSlugs], + ); + + const groups = useMemo(() => { + const rawGroups = catalog?.permission_menu_groups ?? []; + const mapped = rawGroups + .map((group) => ({ + key: group.key, + label: resolveGroupLabel(group.key, group.label), + permissions: group.permissions.filter((permission) => + allowedSet ? allowedSet.has(permission.slug) : true, + ), + })) + .filter((group) => group.permissions.length > 0); + + if (mapped.length > 0) { + return mapped; + } + + const fallbackPermissions = (catalog?.permissions ?? []).filter((permission) => + allowedSet ? allowedSet.has(permission.slug) : true, + ); + + if (fallbackPermissions.length === 0) { + return []; + } + + return [ + { + key: "all", + label: resolveGroupLabel("all", "全部权限"), + permissions: fallbackPermissions, + }, + ]; + }, [allowedSet, catalog, resolveGroupLabel]); + + const totalCount = useMemo( + () => groups.reduce((sum, group) => sum + group.permissions.length, 0), + [groups], + ); + + const toggleOne = (slug: string, checked: boolean) => { + const next = new Set(selectedSet); + if (checked) { + next.add(slug); + } else { + next.delete(slug); + } + onChange(Array.from(next).sort()); + }; + + const toggleGroup = (slugs: string[], checked: boolean) => { + const next = new Set(selectedSet); + for (const slug of slugs) { + if (checked) { + next.add(slug); + } else { + next.delete(slug); + } + } + onChange(Array.from(next).sort()); + }; + + const toggleExpanded = (key: string) => { + setExpandedGroups((prev) => ({ ...prev, [key]: !(prev[key] ?? true) })); + }; + + const isExpanded = (key: string): boolean => expandedGroups[key] ?? true; + + if (groups.length === 0 || totalCount === 0) { + return ( +
+ {emptyText} +
+ ); + } + + return ( +
+ {helperText || summaryText ? ( +
+ {helperText} + {summaryText} +
+ ) : null} + + +
+ {groups.map((group) => { + const expanded = isExpanded(group.key); + const groupSlugs = group.permissions.map((permission) => permission.slug); + const selectedCount = groupSlugs.filter((slug) => selectedSet.has(slug)).length; + const checkedState = + selectedCount === 0 ? false : selectedCount === groupSlugs.length ? true : "indeterminate"; + + return ( +
+
+ + toggleGroup(groupSlugs, value === true)} + /> + + + {selectedCount}/{group.permissions.length} + +
+ {expanded ? ( +
+ {group.permissions.map((permission, index) => ( + + ))} +
+ ) : null} +
+ ); + })} +
+
+
+ ); +} diff --git a/src/components/admin/login-form.tsx b/src/components/admin/login-form.tsx index 44a9dd5..4aed275 100644 --- a/src/components/admin/login-form.tsx +++ b/src/components/admin/login-form.tsx @@ -253,7 +253,6 @@ export function LoginForm() { aria-label={loadingCaptcha ? t("captchaLoading") : t("captchaRefresh")} > {captchaSrc ? ( - // eslint-disable-next-line @next/next/no-img-element -- data URL from API ) { return (
) { return ( ) diff --git a/src/hooks/use-async-effect.ts b/src/hooks/use-async-effect.ts index 97012f2..e808f40 100644 --- a/src/hooks/use-async-effect.ts +++ b/src/hooks/use-async-effect.ts @@ -16,6 +16,5 @@ export function useAsyncEffect( queueMicrotask(() => { void factoryRef.current(); }); - // eslint-disable-next-line react-hooks/exhaustive-deps -- factory 经 ref 同步,deps 仅含真实查询参数 }, deps); } diff --git a/src/i18n/locales/en/adminUsers.json b/src/i18n/locales/en/adminUsers.json index 5df98ce..b0e732e 100644 --- a/src/i18n/locales/en/adminUsers.json +++ b/src/i18n/locales/en/adminUsers.json @@ -30,6 +30,7 @@ "saveRoleFailed": "Failed to save roles", "savePermissionSuccess": "Updated permissions for {{name}}", "savePermissionFailed": "Failed to save permissions", + "modelGuide": "Accounts bind roles only. Maintain functional permissions in Role Management.", "saving": "Saving…", "deleting": "Deleting…", "common": { @@ -70,6 +71,16 @@ "roleActions": { "permissions": "Permissions" }, + "permissionLevels": { + "view": "View", + "manage": "Manage", + "review": "Review", + "export": "Export", + "control": "Control", + "config": "Configure", + "reopen": "Reopen", + "special": "Privileged" + }, "permissionDialog": { "title": "Assign roles", "rolesTitle": "Roles", @@ -106,7 +117,7 @@ "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 roles or grant direct permissions in Permissions.", + "rolesDescription": "After creation, you can continue adjusting role bindings in Assign Roles.", "noRoles": "No roles available yet. Wait for the list to finish loading and try again." }, "delete": { @@ -124,21 +135,37 @@ "dashboard": "Dashboard", "admin_users": "Admin Users", "admin_roles": "Role Management", + "agents": "Agent Management", "players": "Players", + "currencies": "Currencies", "wallet": "Wallet", "draws": "Draws", "config": "Configuration", + "rules_plays": "Play Rules", + "rules_odds": "Odds & Rebate", + "risk_cap": "Risk Cap Rules", "risk": "Risk", "settlement": "Settlement", "jackpot": "Jackpot", "reconcile": "Reconcile", + "reports": "Reports", "tickets": "Tickets", "audit": "Audit Logs", - "settings": "Settings" + "settings": "Settings", + "integration": "Integration Sites" }, "permissionNames": { + "prd.dashboard.view": "Dashboard · View", + "prd.agent.view": "Agent Management · View", + "prd.agent.manage": "Agent Management · Manage", + "prd.agent.role.view": "Agent Roles · View", + "prd.agent.role.manage": "Agent Roles · Manage", + "prd.agent.user.view": "Agent Accounts · View", + "prd.agent.user.manage": "Agent Accounts · Manage", "prd.admin_user.manage": "Admin Users · Manage", "prd.admin_role.manage": "Role Management · Manage", + "prd.integration.view": "Integration Sites · View", + "prd.integration.manage": "Integration Sites · Manage", "prd.users.manage": "Players · Manage", "prd.currency.manage": "Currency Management · Manage", "prd.users.view_finance": "Players · View Finance", @@ -153,6 +180,7 @@ "prd.draw_reopen.manage": "Draw Reopen · Manage", "prd.play_switch.manage": "Play Switches · Manage", "prd.odds.manage": "Odds Configuration · Manage", + "prd.odds.view": "Odds Configuration · View", "prd.risk_cap.manage": "Risk Caps · Manage", "prd.risk_cap.view": "Risk Caps · View", "prd.rebate.manage": "Commission/Rebate · Manage", @@ -163,7 +191,11 @@ "prd.payout.manage": "Payout Confirmation · Manage", "prd.payout.review": "Payout Confirmation · Review", "prd.payout.view": "Payout Confirmation · View", + "prd.tickets.view": "Player Tickets · View", "prd.audit.view": "Audit Logs · View", - "prd.report.view": "Reports · View" + "prd.report.view": "Reports · View", + "prd.report.export": "Reports · Export", + "prd.risk.view": "Risk Center · View", + "prd.risk.manage": "Risk Center · Manage" } } diff --git a/src/i18n/locales/en/agents.json b/src/i18n/locales/en/agents.json index 18b37f3..dfb5258 100644 --- a/src/i18n/locales/en/agents.json +++ b/src/i18n/locales/en/agents.json @@ -20,6 +20,7 @@ "deleteSuccess": "Deleted agent {{name}}", "saveFailed": "Save failed", "codeRequired": "Code and name are required", + "modelGuide": "Agent layer controls data scope and delegation ceiling. Account permissions are assigned through roles.", "tabs": { "overview": "Overview", "roles": "Roles", diff --git a/src/i18n/locales/en/audit.json b/src/i18n/locales/en/audit.json index 105f166..f80f7ef 100644 --- a/src/i18n/locales/en/audit.json +++ b/src/i18n/locales/en/audit.json @@ -11,5 +11,10 @@ "action": "Action", "target": "Target", "time": "Time", - "empty": "No data" + "empty": "No data", + "operatorTypes": { + "admin": "Admin", + "player": "Player", + "system": "System" + } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 701f812..2f34d49 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -118,7 +118,8 @@ "deniedDescription": "Your account does not have permission to open this page. Ask an administrator to assign the required role permissions." }, "table": { - "id": "ID" + "id": "ID", + "actions": "Actions" }, "playerColumns": { "site": "Site", diff --git a/src/i18n/locales/en/settlement.json b/src/i18n/locales/en/settlement.json index 4e7510b..dea58d5 100644 --- a/src/i18n/locales/en/settlement.json +++ b/src/i18n/locales/en/settlement.json @@ -3,6 +3,7 @@ "filter": "Filter", "drawNo": "Draw no.", "status": "Status", + "actions": "Actions", "apply": "Apply", "batchList": "Settlement batches", "allStatuses": "All", diff --git a/src/i18n/locales/en/tickets.json b/src/i18n/locales/en/tickets.json index 5bce758..48a8f6a 100644 --- a/src/i18n/locales/en/tickets.json +++ b/src/i18n/locales/en/tickets.json @@ -24,6 +24,7 @@ "betAmount": "Bet amount", "actualDeduct": "Actual deduct", "status": "Status", + "actions": "Actions", "failReason": "Fail reason", "winAmount": "Win amount", "placedAt": "Placed at", diff --git a/src/i18n/locales/en/wallet.json b/src/i18n/locales/en/wallet.json index db1e642..0120e71 100644 --- a/src/i18n/locales/en/wallet.json +++ b/src/i18n/locales/en/wallet.json @@ -3,6 +3,7 @@ "subnavLabel": "Wallet sub pages", "subnavTransactions": "Wallet transactions", "subnavTransferOrders": "Transfer orders", + "subnavPlayerWallet": "Player wallet", "noPermission": "Current account has no access to this page", "copySuccess": "{{label}} copied to clipboard", "copyFailed": "Copy failed. Check browser permissions or copy manually.", @@ -11,7 +12,7 @@ "statusFailed": "Failed", "statusPendingReconcile": "Pending reconcile", "statusReversed": "Reversed", - "statusManuallyProcessed": "Manually processed", + "statusCaseClosed": "Case closed", "statusPosted": "Posted", "filterAll": "All", "transferIn": "Main site transfer in", @@ -44,19 +45,19 @@ "actionsMenuAriaLabel": "Transfer order actions", "reverse": "Reverse", "completeCredit": "Complete credit", - "manualProcess": "Manual process", + "markCaseClosed": "Close case", "processing": "Processing…", "reverseSuccess": "Reversed successfully", "completeCreditSuccess": "Transfer-in credited successfully", - "manualProcessSuccess": "Manually processed successfully", + "markCaseClosedSuccess": "Case marked closed", "actionFailed": "Action failed", "confirm": { "reverseTitle": "Confirm reverse transfer?", "reverseDescription": "Reverse order {{transferNo}}. This may affect player wallet balance.", "completeCreditTitle": "Confirm complete transfer-in credit?", "completeCreditDescription": "When the main site has already debited, credit lottery wallet for order {{transferNo}} and mark it successful.", - "manualProcessTitle": "Confirm manual process?", - "manualProcessDescription": "Mark order {{transferNo}} as manually processed without automatic wallet adjustment." + "markCaseClosedTitle": "Close this case?", + "markCaseClosedDescription": "Only marks order {{transferNo}} as closed. Wallet balances are not adjusted. Confirm it was already handled outside the system." }, "txnNo": "Txn no.", "bizType": "Business type", diff --git a/src/i18n/locales/ne/adminUsers.json b/src/i18n/locales/ne/adminUsers.json index 3f7b245..dcbbf44 100644 --- a/src/i18n/locales/ne/adminUsers.json +++ b/src/i18n/locales/ne/adminUsers.json @@ -30,6 +30,7 @@ "saveRoleFailed": "भूमिका सुरक्षित गर्न असफल", "savePermissionSuccess": "{{name}} को अनुमति अपडेट भयो", "savePermissionFailed": "अनुमति सुरक्षित गर्न असफल", + "modelGuide": "खाता तहमा भूमिका मात्र बाँधिन्छ; कार्य अनुमति भूमिका व्यवस्थापनमा मिलाउनुहोस्।", "saving": "सेभ हुँदैछ…", "deleting": "मेटिँदैछ…", "common": { @@ -70,6 +71,16 @@ "roleActions": { "permissions": "अनुमति" }, + "permissionLevels": { + "view": "हेर्नुहोस्", + "manage": "व्यवस्थापन", + "review": "समीक्षा", + "export": "निर्यात", + "control": "नियन्त्रण", + "config": "कन्फिगर", + "reopen": "पुनःखोल्ने", + "special": "विशेष" + }, "permissionDialog": { "title": "भूमिका तोक्नुहोस्", "rolesTitle": "भूमिका", @@ -106,7 +117,7 @@ "passwordPlaceholderCreate": "कम्तीमा 8 वर्ण", "passwordPlaceholderEdit": "परिवर्तन नगर्न खाली छोड्नुहोस्", "rolesRequired": "भूमिका (पूर्वनिर्धारित साइट, कम्तीमा एक)", - "rolesDescription": "सिर्जना भएपछि अनुमतिमा गएर भूमिका वा प्रत्यक्ष अनुमति थप समायोजन गर्न सकिन्छ।", + "rolesDescription": "सिर्जना भएपछि \"भूमिका तोक्नुहोस्\" मा गएर भूमिका बाइन्डिङ थप समायोजन गर्न सकिन्छ।", "noRoles": "अहिले भूमिका डाटा छैन। सूची लोड भएपछि फेरि प्रयास गर्नुहोस्।" }, "delete": { @@ -124,21 +135,37 @@ "dashboard": "ड्यासबोर्ड", "admin_users": "प्रशासक सूची", "admin_roles": "भूमिका व्यवस्थापन", + "agents": "एजेन्ट व्यवस्थापन", "players": "खेलाडी सूची", + "currencies": "मुद्रा व्यवस्थापन", "wallet": "वालेट", "draws": "ड्रअ सूची", "config": "कन्फिगरेसन", + "rules_plays": "प्ले नियम", + "rules_odds": "ओड्स र रिबेट", + "risk_cap": "जोखिम सीमा नियम", "risk": "जोखिम", "settlement": "सेटलमेन्ट", "jackpot": "ज्याकपोट", "reconcile": "मिलान", + "reports": "रिपोर्टहरू", "tickets": "टिकटहरू", "audit": "अडिट लग", - "settings": "सेटिङ" + "settings": "सेटिङ", + "integration": "इन्टिग्रेशन साइटहरू" }, "permissionNames": { + "prd.dashboard.view": "ड्यासबोर्ड · हेर्नुहोस्", + "prd.agent.view": "एजेन्ट व्यवस्थापन · हेर्नुहोस्", + "prd.agent.manage": "एजेन्ट व्यवस्थापन · व्यवस्थापन", + "prd.agent.role.view": "एजेन्ट भूमिका · हेर्नुहोस्", + "prd.agent.role.manage": "एजेन्ट भूमिका · व्यवस्थापन", + "prd.agent.user.view": "एजेन्ट खाता · हेर्नुहोस्", + "prd.agent.user.manage": "एजेन्ट खाता · व्यवस्थापन", "prd.admin_user.manage": "प्रशासक सूची · व्यवस्थापन", "prd.admin_role.manage": "भूमिका व्यवस्थापन · व्यवस्थापन", + "prd.integration.view": "इन्टिग्रेशन साइट · हेर्नुहोस्", + "prd.integration.manage": "इन्टिग्रेशन साइट · व्यवस्थापन", "prd.users.manage": "खेलाडी व्यवस्थापन · व्यवस्थापन", "prd.currency.manage": "मुद्रा व्यवस्थापन · व्यवस्थापन", "prd.users.view_finance": "खेलाडी व्यवस्थापन · वित्त हेर्नुहोस्", @@ -153,6 +180,7 @@ "prd.draw_reopen.manage": "ड्रअ पुनःखोल्ने · व्यवस्थापन", "prd.play_switch.manage": "प्ले स्विच · व्यवस्थापन", "prd.odds.manage": "ओड्स कन्फिगरेसन · व्यवस्थापन", + "prd.odds.view": "ओड्स कन्फिगरेसन · हेर्नुहोस्", "prd.risk_cap.manage": "जोखिम सीमा · व्यवस्थापन", "prd.risk_cap.view": "जोखिम सीमा · हेर्नुहोस्", "prd.rebate.manage": "कमिसन/रिबेट · व्यवस्थापन", @@ -163,7 +191,11 @@ "prd.payout.manage": "भुक्तानी पुष्टि · व्यवस्थापन", "prd.payout.review": "भुक्तानी पुष्टि · समीक्षा", "prd.payout.view": "भुक्तानी पुष्टि · हेर्नुहोस्", + "prd.tickets.view": "खेलाडी टिकट · हेर्नुहोस्", "prd.audit.view": "अडिट लग · हेर्नुहोस्", - "prd.report.view": "रिपोर्ट · हेर्नुहोस्" + "prd.report.view": "रिपोर्ट · हेर्नुहोस्", + "prd.report.export": "रिपोर्ट · निर्यात", + "prd.risk.view": "जोखिम केन्द्र · हेर्नुहोस्", + "prd.risk.manage": "जोखिम केन्द्र · व्यवस्थापन" } } diff --git a/src/i18n/locales/ne/agents.json b/src/i18n/locales/ne/agents.json index addfe2e..708d6f8 100644 --- a/src/i18n/locales/ne/agents.json +++ b/src/i18n/locales/ne/agents.json @@ -20,6 +20,7 @@ "deleteSuccess": "Deleted agent {{name}}", "saveFailed": "Save failed", "codeRequired": "Code and name are required", + "modelGuide": "एजेन्ट तहले डाटा स्कोप र delegation ceiling नियन्त्रण गर्छ; खाताको अनुमति भूमिका मार्फत बाँडिन्छ।", "tabs": { "overview": "Overview", "roles": "Roles", diff --git a/src/i18n/locales/ne/audit.json b/src/i18n/locales/ne/audit.json index 7c413b5..8f6b09a 100644 --- a/src/i18n/locales/ne/audit.json +++ b/src/i18n/locales/ne/audit.json @@ -11,5 +11,10 @@ "action": "कार्य", "target": "लक्ष्य", "time": "समय", - "empty": "डाटा छैन" + "empty": "डाटा छैन", + "operatorTypes": { + "admin": "प्रशासक", + "player": "खेलाडी", + "system": "प्रणाली" + } } diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index 232782d..3a4c730 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -118,7 +118,8 @@ "deniedDescription": "यो पृष्ठ खोल्ने अनुमति तपाईंको खातामा छैन। भूमिका व्यवस्थापनबाट आवश्यक अनुमति दिन प्रशासकलाई सम्पर्क गर्नुहोस्।" }, "table": { - "id": "ID" + "id": "ID", + "actions": "कार्य" }, "playerColumns": { "site": "साइट", diff --git a/src/i18n/locales/ne/settlement.json b/src/i18n/locales/ne/settlement.json index 01c8237..a32c3b9 100644 --- a/src/i18n/locales/ne/settlement.json +++ b/src/i18n/locales/ne/settlement.json @@ -3,6 +3,7 @@ "filter": "फिल्टर", "drawNo": "ड्रअ नं.", "status": "स्थिति", + "actions": "कार्य", "apply": "लागू गर्नुहोस्", "batchList": "सेटलमेन्ट ब्याच", "allStatuses": "सबै", diff --git a/src/i18n/locales/ne/tickets.json b/src/i18n/locales/ne/tickets.json index c1014c3..2c4d86b 100644 --- a/src/i18n/locales/ne/tickets.json +++ b/src/i18n/locales/ne/tickets.json @@ -22,6 +22,7 @@ "betAmount": "बेट रकम", "actualDeduct": "कटौती", "status": "स्थिति", + "actions": "कार्य", "failReason": "असफल कारण", "winAmount": "जित रकम", "placedAt": "बेट समय", diff --git a/src/i18n/locales/ne/wallet.json b/src/i18n/locales/ne/wallet.json index e20033a..cc349cb 100644 --- a/src/i18n/locales/ne/wallet.json +++ b/src/i18n/locales/ne/wallet.json @@ -3,6 +3,7 @@ "subnavLabel": "वालेट उपपृष्ठहरू", "subnavTransactions": "वालेट कारोबार", "subnavTransferOrders": "ट्रान्सफर अर्डर", + "subnavPlayerWallet": "खेलाडी वालेट", "noPermission": "हालको खातासँग यो पृष्ठमा पहुँच अनुमति छैन", "copySuccess": "{{label}} क्लिपबोर्डमा प्रतिलिपि भयो", "copyFailed": "प्रतिलिपि असफल भयो। ब्राउजर अनुमति जाँच गर्नुहोस् वा म्यानुअल रूपमा कपी गर्नुहोस्।", @@ -11,7 +12,7 @@ "statusFailed": "असफल", "statusPendingReconcile": "मिलान बाँकी", "statusReversed": "रिभर्स भयो", - "statusManuallyProcessed": "म्यानुअल रूपमा प्रक्रिया गरियो", + "statusCaseClosed": "केस बन्द भयो", "statusPosted": "पोस्ट गरियो", "filterAll": "सबै", "transferIn": "मुख्य साइटबाट भित्र", @@ -44,19 +45,19 @@ "actionsMenuAriaLabel": "ट्रान्सफर अर्डर कार्य मेनु", "reverse": "रिभर्स", "completeCredit": "क्रेडिट पूरा गर्नुहोस्", - "manualProcess": "म्यानुअल प्रक्रिया", + "markCaseClosed": "केस बन्द चिन्ह", "processing": "प्रक्रियामा…", "reverseSuccess": "रिभर्स सफल भयो", "completeCreditSuccess": "ट्रान्सफर-इन क्रेडिट सफल भयो", - "manualProcessSuccess": "म्यानुअल प्रक्रिया सफल भयो", + "markCaseClosedSuccess": "केस बन्द चिन्ह लाग्यो", "actionFailed": "कार्य असफल भयो", "confirm": { "reverseTitle": "ट्रान्सफर रिभर्स पुष्टि गर्ने?", "reverseDescription": "अर्डर {{transferNo}} रिभर्स गर्नेछ, खेलाडी वालेट प्रभावित हुन सक्छ।", "completeCreditTitle": "ट्रान्सफर-इन क्रेडिट पूरा गर्ने?", "completeCreditDescription": "मुख्य साइटले पहिले नै कटौती गरेको छ भने, अर्डर {{transferNo}} को लागि लटरी वालेटमा क्रेडिट गरी सफल चिन्ह लगाउँछ।", - "manualProcessTitle": "म्यानुअल प्रक्रिया पुष्टि?", - "manualProcessDescription": "अर्डर {{transferNo}} म्यानुअल प्रक्रिया भएको चिन्ह लगाउँछ, वालेट स्वचालित मिलाउँदैन।" + "markCaseClosedTitle": "केस बन्द चिन्ह पुष्टि?", + "markCaseClosedDescription": "अर्डर {{transferNo}} मात्र बन्द भएको चिन्ह लगाउँछ; वालेट मिलाउँदैन। बाहिरै समाधान भइसकेको पुष्टि गर्नुहोस्।" }, "txnNo": "कारोबार नं.", "bizType": "व्यवसाय प्रकार", diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json index e0be3d6..fd3952b 100644 --- a/src/i18n/locales/zh/adminUsers.json +++ b/src/i18n/locales/zh/adminUsers.json @@ -30,6 +30,7 @@ "saveRoleFailed": "保存角色失败", "savePermissionSuccess": "已更新 {{name}} 的权限", "savePermissionFailed": "保存权限失败", + "modelGuide": "账号层只绑定角色,不直接分配功能权限;具体权限请到「角色管理」维护。", "saving": "保存中…", "deleting": "删除中…", "common": { @@ -70,6 +71,16 @@ "roleActions": { "permissions": "配权限" }, + "permissionLevels": { + "view": "查看", + "manage": "管理", + "review": "审核", + "export": "导出", + "control": "控制", + "config": "配置", + "reopen": "重开", + "special": "特权" + }, "permissionDialog": { "title": "分配角色", "rolesTitle": "角色", @@ -106,7 +117,7 @@ "passwordPlaceholderCreate": "至少 8 位", "passwordPlaceholderEdit": "不修改请留空", "rolesRequired": "角色(默认站点,至少一项)", - "rolesDescription": "创建后即可在「权限」中继续调整角色或直接授权。", + "rolesDescription": "创建后可在「分配角色」中继续调整角色绑定。", "noRoles": "暂无角色数据,请等待列表加载完成后重试。" }, "delete": { @@ -134,21 +145,37 @@ "dashboard": "仪表盘", "admin_users": "管理列表", "admin_roles": "角色管理", + "agents": "代理管理", "players": "玩家列表", + "currencies": "币种管理", "wallet": "钱包流水", "draws": "期号列表", "config": "运营配置", + "rules_plays": "投注规则", + "rules_odds": "赔率与回水", + "risk_cap": "限额版本", "risk": "风控", "settlement": "结算", "jackpot": "奖池", "reconcile": "对账", + "reports": "报表中心", "tickets": "玩家注单", "audit": "审计日志", - "settings": "系统设置" + "settings": "系统设置", + "integration": "接入站点" }, "permissionNames": { + "prd.dashboard.view": "仪表盘·查看", + "prd.agent.view": "代理管理·查看", + "prd.agent.manage": "代理管理·可管理", + "prd.agent.role.view": "代理角色·查看", + "prd.agent.role.manage": "代理角色·可管理", + "prd.agent.user.view": "代理账号·查看", + "prd.agent.user.manage": "代理账号·可管理", "prd.admin_user.manage": "管理员列表·可管理", "prd.admin_role.manage": "角色管理·可管理", + "prd.integration.view": "接入站点·查看", + "prd.integration.manage": "接入站点·可管理", "prd.users.manage": "用户管理·可管理", "prd.currency.manage": "币种管理·可管理", "prd.users.view_finance": "用户管理·财务查看", @@ -163,6 +190,7 @@ "prd.draw_reopen.manage": "开奖结果重开·可管理", "prd.play_switch.manage": "玩法开关·可管理", "prd.odds.manage": "赔率配置·可管理", + "prd.odds.view": "赔率配置·查看", "prd.risk_cap.manage": "封顶配置·可管理", "prd.risk_cap.view": "封顶配置·查看", "prd.rebate.manage": "佣金/回水·可管理", @@ -173,7 +201,11 @@ "prd.payout.manage": "派彩确认·可管理", "prd.payout.review": "派彩确认·可审核", "prd.payout.view": "派彩确认·查看", + "prd.tickets.view": "玩家注单·查看", "prd.audit.view": "审计日志·查看", - "prd.report.view": "报表中心·查看" + "prd.report.view": "报表中心·查看", + "prd.report.export": "报表中心·导出", + "prd.risk.view": "风控中心·查看", + "prd.risk.manage": "风控中心·可管理" } } diff --git a/src/i18n/locales/zh/agents.json b/src/i18n/locales/zh/agents.json index 1956793..7c414a2 100644 --- a/src/i18n/locales/zh/agents.json +++ b/src/i18n/locales/zh/agents.json @@ -20,11 +20,12 @@ "deleteSuccess": "已删除代理 {{name}}", "saveFailed": "保存失败", "codeRequired": "请填写编码与名称", + "modelGuide": "代理层负责数据范围(Scope)与授权上限(Ceiling),账号权限请通过角色分配。", "tabs": { "overview": "概况", "roles": "角色", "users": "账号", - "delegation": "下放权限" + "delegation": "授权上限" }, "delegation": { "title": "下放权限上限", @@ -47,7 +48,11 @@ "deleteSuccess": "已删除角色 {{name}}", "permissionSaveSuccess": "权限已更新", "readOnlyTemplate": "只读模板", - "permissionSubsetHint": "只能分配您当前拥有的权限" + "permissionSubsetHint": "只能分配您当前拥有的权限", + "selectedCount": "已选 {{selected}} / {{total}} 项", + "groupSelectedCount": "已选 {{selected}} / {{total}}", + "selectGroup": "本组全选", + "noAssignablePermissions": "当前没有可分配权限" }, "users": { "title": "代理账号", diff --git a/src/i18n/locales/zh/audit.json b/src/i18n/locales/zh/audit.json index f2f34b8..cc090e4 100644 --- a/src/i18n/locales/zh/audit.json +++ b/src/i18n/locales/zh/audit.json @@ -11,5 +11,10 @@ "action": "动作", "target": "目标", "time": "时间", - "empty": "无数据" + "empty": "无数据", + "operatorTypes": { + "admin": "管理员", + "player": "玩家", + "system": "系统" + } } diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 6fcc900..3a8b9bb 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -118,7 +118,8 @@ "deniedDescription": "当前账号没有访问此页面的权限。如需开通,请联系管理员在角色管理中分配相应功能权限。" }, "table": { - "id": "ID" + "id": "ID", + "actions": "操作" }, "playerColumns": { "site": "主站", diff --git a/src/i18n/locales/zh/settlement.json b/src/i18n/locales/zh/settlement.json index 8d654f4..d5f22b2 100644 --- a/src/i18n/locales/zh/settlement.json +++ b/src/i18n/locales/zh/settlement.json @@ -3,6 +3,7 @@ "filter": "筛选", "drawNo": "期号", "status": "状态", + "actions": "操作", "apply": "应用", "batchList": "结算批次", "allStatuses": "不限", diff --git a/src/i18n/locales/zh/tickets.json b/src/i18n/locales/zh/tickets.json index 0fa7225..5d659e8 100644 --- a/src/i18n/locales/zh/tickets.json +++ b/src/i18n/locales/zh/tickets.json @@ -24,6 +24,7 @@ "betAmount": "下注", "actualDeduct": "实扣", "status": "状态", + "actions": "操作", "failReason": "失败原因", "winAmount": "中奖", "placedAt": "下单时间", diff --git a/src/i18n/locales/zh/wallet.json b/src/i18n/locales/zh/wallet.json index 67f57b2..c685279 100644 --- a/src/i18n/locales/zh/wallet.json +++ b/src/i18n/locales/zh/wallet.json @@ -3,6 +3,7 @@ "subnavLabel": "钱包子页", "subnavTransactions": "钱包流水", "subnavTransferOrders": "转账单", + "subnavPlayerWallet": "玩家钱包", "noPermission": "当前账号无访问该页的权限", "copySuccess": "{{label}}已复制到剪贴板", "copyFailed": "复制失败,请检查浏览器权限或手动选择文本", @@ -11,7 +12,7 @@ "statusFailed": "失败", "statusPendingReconcile": "待对账", "statusReversed": "已冲正", - "statusManuallyProcessed": "已人工处理", + "statusCaseClosed": "已结案", "statusPosted": "已记账", "filterAll": "不限", "transferIn": "主站转入", @@ -44,19 +45,19 @@ "actionsMenuAriaLabel": "转账单操作菜单", "reverse": "冲正", "completeCredit": "补完成入账", - "manualProcess": "人工处理", + "markCaseClosed": "标记结案", "processing": "处理中…", "reverseSuccess": "冲正成功", "completeCreditSuccess": "补入账成功", - "manualProcessSuccess": "人工处理成功", + "markCaseClosedSuccess": "已标记结案", "actionFailed": "操作失败", "confirm": { "reverseTitle": "确认冲正转账单?", "reverseDescription": "将对单号 {{transferNo}} 执行冲正,可能影响玩家钱包余额。", "completeCreditTitle": "确认补完成转入入账?", "completeCreditDescription": "主站已扣款时,将为单号 {{transferNo}} 在彩票钱包补记转入并标记成功。", - "manualProcessTitle": "确认人工处理?", - "manualProcessDescription": "将标记单号 {{transferNo}} 为已人工处理,不会自动调整钱包。" + "markCaseClosedTitle": "确认标记结案?", + "markCaseClosedDescription": "仅将单号 {{transferNo}} 标为已结案,不会调整彩票或主站余额。请确认已在系统外处理完毕。" }, "txnNo": "流水号", "bizType": "类型(业务)", diff --git a/src/lib/admin-permission-packages.ts b/src/lib/admin-permission-packages.ts new file mode 100644 index 0000000..8286051 --- /dev/null +++ b/src/lib/admin-permission-packages.ts @@ -0,0 +1,111 @@ +export type AdminPermissionPackage = { + key: string; + label: string; + slugs: string[]; +}; + +export const ADMIN_PERMISSION_PACKAGES: Record = { + dashboard: [ + { key: "view", label: "查看", slugs: ["prd.dashboard.view"] }, + ], + admin_users: [ + { key: "manage", label: "管理", slugs: ["prd.admin_user.manage"] }, + ], + admin_roles: [ + { key: "manage", label: "管理", slugs: ["prd.admin_role.manage"] }, + ], + agents: [ + { + key: "view", + label: "查看", + slugs: ["prd.agent.view", "prd.agent.role.view", "prd.agent.user.view"], + }, + { + key: "manage", + label: "管理", + slugs: ["prd.agent.manage", "prd.agent.role.manage", "prd.agent.user.manage"], + }, + ], + players: [ + { + key: "view", + label: "查看", + slugs: ["prd.users.view_finance", "prd.users.view_cs"], + }, + { key: "manage", label: "管理", slugs: ["prd.users.manage"] }, + { key: "control", label: "控制", slugs: ["prd.player_freeze.manage"] }, + ], + currencies: [ + { key: "manage", label: "管理", slugs: ["prd.currency.manage"] }, + ], + wallet: [ + { + key: "view", + label: "查看", + slugs: ["prd.wallet_reconcile.view", "prd.wallet_reconcile.view_cs", "prd.users.view_finance"], + }, + { + key: "manage", + label: "管理", + slugs: ["prd.wallet_reconcile.manage", "prd.wallet_adjust.manage"], + }, + ], + draws: [ + { key: "view", label: "查看", slugs: ["prd.draw_result.view"] }, + { key: "manage", label: "管理", slugs: ["prd.draw_result.manage"] }, + { key: "reopen", label: "重开", slugs: ["prd.draw_reopen.manage"] }, + ], + rules_plays: [ + { key: "manage", label: "管理", slugs: ["prd.play_switch.manage"] }, + { key: "view", label: "查看", slugs: ["prd.odds.view"] }, + { key: "config", label: "配置", slugs: ["prd.odds.manage"] }, + ], + rules_odds: [ + { key: "view", label: "查看", slugs: ["prd.rebate.view"] }, + { key: "manage", label: "管理", slugs: ["prd.odds.manage", "prd.rebate.manage"] }, + ], + risk_cap: [ + { key: "view", label: "查看", slugs: ["prd.risk_cap.view"] }, + { key: "manage", label: "管理", slugs: ["prd.risk_cap.manage"] }, + ], + risk: [ + { key: "view", label: "查看", slugs: ["prd.risk.view"] }, + { key: "manage", label: "管理", slugs: ["prd.risk.manage"] }, + ], + settlement: [ + { key: "view", label: "查看", slugs: ["prd.payout.view"] }, + { key: "review", label: "审核", slugs: ["prd.payout.review"] }, + { key: "manage", label: "管理", slugs: ["prd.payout.manage"] }, + ], + reconcile: [ + { + key: "view", + label: "查看", + slugs: ["prd.wallet_reconcile.view", "prd.wallet_reconcile.view_cs"], + }, + { key: "manage", label: "管理", slugs: ["prd.wallet_reconcile.manage"] }, + ], + reports: [ + { key: "view", label: "查看", slugs: ["prd.report.view"] }, + { key: "export", label: "导出", slugs: ["prd.report.export"] }, + ], + tickets: [ + { key: "view", label: "查看", slugs: ["prd.tickets.view"] }, + ], + audit: [ + { key: "view", label: "查看", slugs: ["prd.audit.view"] }, + ], + settings: [ + { key: "manage", label: "管理", slugs: ["prd.wallet_reconcile.manage", "prd.currency.manage"] }, + ], + integration: [ + { key: "view", label: "查看", slugs: ["prd.integration.view"] }, + { key: "manage", label: "管理", slugs: ["prd.integration.manage"] }, + ], + jackpot: [ + { key: "view", label: "查看", slugs: ["prd.jackpot.view"] }, + { key: "manage", label: "管理", slugs: ["prd.jackpot.manage"] }, + { key: "special", label: "特权", slugs: ["prd.jackpot.manual_burst"] }, + ], +}; + diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx index 835bb29..ae51029 100644 --- a/src/modules/admin-roles/admin-roles-console.tsx +++ b/src/modules/admin-roles/admin-roles-console.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useMemo, useState } from "react"; -import { ChevronDown, KeyRound, Pencil, Trash2 } from "lucide-react"; +import { KeyRound, Pencil, Trash2 } from "lucide-react"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useExportLabels } from "@/hooks/use-export-labels"; import { useTranslation } from "react-i18next"; @@ -19,13 +19,13 @@ import { } from "@/api/admin-users"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; +import { AdminPermissionPackageSelector } from "@/components/admin/admin-permission-package-selector"; import { Badge } from "@/components/ui/badge"; import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -55,9 +55,9 @@ function permissionGroupLabel(key: string, fallback: string, t: (key: string) => return translated === `permissionGroups.${key}` ? fallback : translated; } -function permissionLabel(slug: string, fallback: string, t: (key: string) => string): string { - const translated = t(`permissionNames.${slug}`); - return translated === `permissionNames.${slug}` ? fallback : translated; +function permissionPackageLabel(key: string, fallback: string, t: (key: string) => string): string { + const translated = t(`permissionLevels.${key}`); + return translated === `permissionLevels.${key}` ? fallback : translated; } export function AdminRolesConsole(): React.ReactElement { @@ -71,7 +71,6 @@ export function AdminRolesConsole(): React.ReactElement { const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); - const [directMenuExpanded, setDirectMenuExpanded] = useState>({}); const [rolePermissionOpen, setRolePermissionOpen] = useState(false); const [selectedRoleId, setSelectedRoleId] = useState(null); @@ -94,22 +93,6 @@ export function AdminRolesConsole(): React.ReactElement { () => roles.find((role) => role.id === selectedRoleId) ?? null, [roles, selectedRoleId], ); - const selectedPermissionSet = useMemo( - () => new Set(draftRolePermissions), - [draftRolePermissions], - ); - - const directPermissionGroups = useMemo(() => { - const groups = catalog?.permission_menu_groups; - if (groups && groups.length > 0) { - return groups; - } - const flatPermissions = catalog?.permissions ?? []; - if (flatPermissions.length > 0) { - return [{ key: "all", label: t("allPermissions"), permissions: flatPermissions }]; - } - return []; - }, [catalog, t]); const load = useCallback(async () => { setLoading(true); @@ -134,36 +117,6 @@ export function AdminRolesConsole(): React.ReactElement { void load(); }, []); - function isDirectGroupOpen(key: string): boolean { - return directMenuExpanded[key] === true; - } - - function toggleDirectGroup(key: string): void { - setDirectMenuExpanded((prev) => { - const wasOpen = prev[key] === true; - return { ...prev, [key]: !wasOpen }; - }); - } - - function toggleRolePermission(slug: string, checked: boolean): void { - setDraftRolePermissions((prev) => { - if (checked) { - return Array.from(new Set([...prev, slug])).sort(); - } - return prev.filter((value) => value !== slug); - }); - } - - function toggleGroupPermissions(slugs: string[], checked: boolean): void { - setDraftRolePermissions((prev) => { - if (checked) { - return Array.from(new Set([...prev, ...slugs])).sort(); - } - const remove = new Set(slugs); - return prev.filter((value) => !remove.has(value)); - }); - } - function openCreateRole(): void { setRoleMode("create"); setEditingRoleId(null); @@ -187,7 +140,6 @@ export function AdminRolesConsole(): React.ReactElement { function openRolePermissionEditor(role: AdminRoleRow): void { setSelectedRoleId(role.id); setDraftRolePermissions([...role.permission_slugs].sort()); - setDirectMenuExpanded({}); setRolePermissionOpen(true); } @@ -205,20 +157,6 @@ export function AdminRolesConsole(): React.ReactElement { } } - function getGroupSelectionState(slugs: string[]): boolean | "indeterminate" { - if (slugs.length === 0) { - return false; - } - const selectedCount = slugs.filter((slug) => selectedPermissionSet.has(slug)).length; - if (selectedCount === 0) { - return false; - } - if (selectedCount === slugs.length) { - return true; - } - return "indeterminate"; - } - async function saveRolePermissions(): Promise { if (!selectedRole) { return; @@ -342,7 +280,7 @@ export function AdminRolesConsole(): React.ReactElement { {t("roleTable.status")} {t("roleTable.users")} {t("roleTable.permissions")} - {t("roleTable.actions")} + {t("roleTable.actions")} @@ -379,7 +317,7 @@ export function AdminRolesConsole(): React.ReactElement { {role.user_count} {role.permission_slugs.length} - + {canManageRoles ? (
-
- {directPermissionGroups.map((group) => { - const isOpen = isDirectGroupOpen(group.key); - const groupSlugs = group.permissions.map((permission) => permission.slug); - const selectedCount = group.permissions.filter((permission) => - selectedPermissionSet.has(permission.slug), - ).length; - const checkedState = getGroupSelectionState(groupSlugs); - - return ( -
-
- - toggleGroupPermissions(groupSlugs, value === true)} - /> - - - {selectedCount}/{group.permissions.length} - -
- {isOpen ? ( -
- {group.permissions.map((permission, index) => ( - - ))} -
- ) : null} -
- ); - })} -
+ permissionGroupLabel(key, fallback, t)} + resolvePackageLabel={(key, fallback) => permissionPackageLabel(key, fallback, t)} + emptyText={t("states.noData", { ns: "common" })} + heightClassName="h-[min(56vh,520px)]" + />
) : null}
+
+ {t("modelGuide", { + defaultValue: + "账号层只绑定角色,不直接分配功能权限;具体权限请到“角色管理”维护。", + })} +
{row.effective_permissions.length} - + {canManageUsers ? ( ) => string): string { + const translated = t(`adminUsers:permissionGroups.${key}`); + return translated === `adminUsers:permissionGroups.${key}` ? fallback : translated; +} + +function permissionPackageLabel( + key: string, + fallback: string, + t: (key: string, options?: Record) => string, +): string { + const translated = t(`adminUsers:permissionLevels.${key}`); + return translated === `adminUsers:permissionLevels.${key}` ? fallback : translated; +} + function flattenTree(nodes: AgentNodeRow[]): AgentNodeRow[] { const out: AgentNodeRow[] = []; const walk = (list: AgentNodeRow[]) => { @@ -257,6 +269,7 @@ export function AgentsConsole(): React.ReactElement { selected !== null && !selected.is_root && (isSuperAdmin || profile?.agent?.id === selected.parent_id); + const defaultDetailTab = canViewRoles ? "roles" : canViewUsers ? "users" : canManageDelegation ? "delegation" : "roles"; const assignablePermissionSlugs = useMemo(() => { const mine = new Set(profile?.permissions ?? []); @@ -279,6 +292,24 @@ export function AgentsConsole(): React.ReactElement { return slugs; }, [catalog, profile?.permissions]); + const selectedRoleCountText = useMemo( + () => t("roles.selectedCount", { + defaultValue: "已选 {{selected}} / {{total}} 项", + selected: rolePerms.length, + total: assignablePermissionSlugs.length, + }), + [assignablePermissionSlugs.length, rolePerms.length, t], + ); + + const selectedDraftCountText = useMemo( + () => t("roles.selectedCount", { + defaultValue: "已选 {{selected}} / {{total}} 项", + selected: draftPerms.length, + total: assignablePermissionSlugs.length, + }), + [assignablePermissionSlugs.length, draftPerms.length, t], + ); + const loadTree = useCallback(async (siteId?: number | null) => { setLoading(true); setErr(null); @@ -514,7 +545,11 @@ export function AgentsConsole(): React.ReactElement { onValueChange={(v) => setAdminSiteId(Number(v))} > - + + {adminSiteId !== null + ? siteOptions.find((opt) => opt.id === adminSiteId)?.label ?? adminSiteId + : undefined} + {siteOptions.map((opt) => ( @@ -526,6 +561,12 @@ export function AgentsConsole(): React.ReactElement { ) : null}
+
+ {t("modelGuide", { + defaultValue: + "代理层负责数据范围(Scope)与授权上限(Ceiling),账号权限请通过角色分配。", + })} +
{err ?

{err}

: null} @@ -588,7 +629,7 @@ export function AgentsConsole(): React.ReactElement { {!selected ? (

{t("selectNode")}

) : ( - +

{t("status")}

@@ -619,7 +660,6 @@ export function AgentsConsole(): React.ReactElement {
- {t("tabs.overview")} {canViewRoles ? {t("tabs.roles")} : null} {canViewUsers ? {t("tabs.users")} : null} {canManageDelegation ? ( @@ -640,97 +680,6 @@ export function AgentsConsole(): React.ReactElement { ) : null}
- -
-
-

- {t("code")}: {selected.code} -

-

- {t("depth")}: {selected.depth} -

-

- {t("path")}:{" "} - {selected.path} -

-
-
-

{t("quickActions", { defaultValue: "常用操作" })}

- {canManageNode ? ( - - ) : null} - {canManageNode && !selected.is_root ? ( - - ) : null} - {canManageNode && !selected.is_root ? ( - - ) : null} - {canViewRoles ? ( - - ) : null} - {canViewUsers ? ( - - ) : null} -
-
-
- {canViewRoles ? (
@@ -750,68 +699,70 @@ export function AgentsConsole(): React.ReactElement { ) : null}
-
- - - {t("roles.slug")} - {t("name")} - {t("roles.userCount")} - - - - - {roles.map((role) => ( - - {role.slug} - {role.name} - {role.user_count} - - {canManageRoles && !role.is_read_only_template ? ( - { - setPermRoleId(role.id); - setDraftPerms([...role.permission_slugs]); - setPermDialogOpen(true); - }, - }, - { - key: "delete", - label: t("common:actions.delete", { defaultValue: "Delete" }), - icon: Trash2, - destructive: true, - onClick: () => { - requestConfirm({ - title: role.name, - description: t("common:confirm.deleteDescription", { - defaultValue: "This cannot be undone.", - }), - onConfirm: async () => { - await deleteAgentRole(role.id); - toast.success(t("roles.deleteSuccess", { name: role.name })); - if (selectedId !== null) { - await loadDetail(selectedId); - } - }, - }); - }, - }, - ]} - /> - ) : role.is_read_only_template ? ( - - {t("roles.readOnlyTemplate")} - - ) : null} - +
+
+ + + {t("roles.slug")} + {t("name")} + {t("roles.userCount")} + - ))} - -
+ + + {roles.map((role) => ( + + {role.slug} + {role.name} + {role.user_count} + + {canManageRoles && !role.is_read_only_template ? ( + { + setPermRoleId(role.id); + setDraftPerms([...role.permission_slugs]); + setPermDialogOpen(true); + }, + }, + { + key: "delete", + label: t("common:actions.delete", { defaultValue: "Delete" }), + icon: Trash2, + destructive: true, + onClick: () => { + requestConfirm({ + title: role.name, + description: t("common:confirm.deleteDescription", { + defaultValue: "This cannot be undone.", + }), + onConfirm: async () => { + await deleteAgentRole(role.id); + toast.success(t("roles.deleteSuccess", { name: role.name })); + if (selectedId !== null) { + await loadDetail(selectedId); + } + }, + }); + }, + }, + ]} + /> + ) : role.is_read_only_template ? ( + + {t("roles.readOnlyTemplate")} + + ) : null} + + + ))} + + +
) : null} @@ -835,24 +786,26 @@ export function AgentsConsole(): React.ReactElement { ) : null}
- - - - {t("users.username")} - {t("name")} - {t("users.roles")} - - - - {users.map((user) => ( - - {user.username} - {user.nickname} - {user.roles.join(", ") || "—"} +
+
+ + + {t("users.username")} + {t("name")} + {t("users.roles")} - ))} - -
+ + + {users.map((user) => ( + + {user.username} + {user.nickname} + {user.roles.join(", ") || "—"} + + ))} + + + ) : null} @@ -862,40 +815,42 @@ export function AgentsConsole(): React.ReactElement { {delegationGrants.length === 0 ? (

{t("delegation.empty")}

) : ( - - - - {t("delegation.permission")} - {t("delegation.canDelegate")} - - - - {delegationGrants.map((grant) => ( - - -
{grant.name}
-
- {grant.permission_code} -
-
- - { - setDelegationGrants((prev) => - prev.map((row) => - row.menu_action_id === grant.menu_action_id - ? { ...row, can_delegate: checked === true } - : row, - ), - ); - }} - /> - +
+
+ + + {t("delegation.permission")} + {t("delegation.canDelegate")} - ))} - -
+ + + {delegationGrants.map((grant) => ( + + +
{grant.name}
+
+ {grant.permission_code} +
+
+ + { + setDelegationGrants((prev) => + prev.map((row) => + row.menu_action_id === grant.menu_action_id + ? { ...row, can_delegate: checked === true } + : row, + ), + ); + }} + /> + +
+ ))} +
+ + )}
- {canChooseSite ? ( -
- - -
- ) : null} -