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} -