From a020e34a7d22855784e75d1b7798b7c955159308 Mon Sep 17 00:00:00 2001 From: kang Date: Tue, 16 Jun 2026 16:04:03 +0800 Subject: [PATCH] refactor(admin-reports, i18n): remove rebate commission report and enhance localization Removed the `getAdminReportRebateCommission` function and its references from the admin reports API and localization files. Updated CSS for improved money display handling in admin components. Enhanced localization support by adding new finance and support workspace entries in English, Nepali, and Chinese, improving user experience across the application. --- src/api/admin-reports.ts | 9 - src/api/index.ts | 1 - src/app/globals.css | 9 + src/components/admin/admin-money-display.tsx | 57 ++++ src/components/admin/admin-table-money.tsx | 46 ++++ src/hooks/use-admin-permission.ts | 44 ++++ src/i18n/locales/en/dashboard.json | 43 +++ src/i18n/locales/en/reports.json | 15 -- src/i18n/locales/ne/dashboard.json | 43 +++ src/i18n/locales/ne/reports.json | 15 -- src/i18n/locales/zh/adminUsers.json | 2 +- src/i18n/locales/zh/dashboard.json | 43 +++ src/i18n/locales/zh/reports.json | 15 -- src/lib/admin-money-display.ts | 65 +++++ src/lib/admin-permission-codes.ts | 19 ++ src/lib/admin-permissions.ts | 22 ++ src/lib/admin-session-variants.ts | 28 +- src/lib/admin-signed-money.tsx | 20 +- src/lib/platform-system-roles.ts | 4 + src/lib/report-export-map.ts | 2 - .../agents/agent-line-detail-panel.tsx | 50 ++-- src/modules/agents/agent-profile-fields.tsx | 7 +- .../dashboard/agent-dashboard-console.tsx | 10 +- .../dashboard/dashboard-page-client.tsx | 16 +- src/modules/dashboard/dashboard-visuals.tsx | 51 +++- .../dashboard/site-cs-dashboard-console.tsx | 196 ++++++++++++++ .../site-finance-dashboard-console.tsx | 244 ++++++++++++++++++ src/modules/draws/draws-index-console.tsx | 34 ++- src/modules/reconcile/reconcile-console.tsx | 207 +++++++-------- src/modules/reports/reports-console.tsx | 148 +++-------- .../settlement/settlement-bill-breakdown.tsx | 33 ++- .../settlement/settlement-bills-table.tsx | 19 +- .../settlement-operations-panel.tsx | 7 +- src/modules/wallet/wallet-console.tsx | 23 +- src/stores/admin-profile.ts | 16 ++ src/types/api/admin-auth.ts | 15 +- src/types/api/admin-dashboard.ts | 27 ++ src/types/api/admin-reports.ts | 7 - 38 files changed, 1259 insertions(+), 353 deletions(-) create mode 100644 src/components/admin/admin-money-display.tsx create mode 100644 src/components/admin/admin-table-money.tsx create mode 100644 src/hooks/use-admin-permission.ts create mode 100644 src/lib/admin-money-display.ts create mode 100644 src/lib/admin-permission-codes.ts create mode 100644 src/modules/dashboard/site-cs-dashboard-console.tsx create mode 100644 src/modules/dashboard/site-finance-dashboard-console.tsx diff --git a/src/api/admin-reports.ts b/src/api/admin-reports.ts index ce83a69..1593370 100644 --- a/src/api/admin-reports.ts +++ b/src/api/admin-reports.ts @@ -7,7 +7,6 @@ import type { AdminReportPlayDimensionRow, AdminReportPlayerWinLossRow, AdminReportQueryParams, - AdminReportRebateCommissionRow, } from "@/types/api/admin-reports"; const A = `/admin`; @@ -35,11 +34,3 @@ export async function getAdminReportPlayDimension( params, }); } - -export async function getAdminReportRebateCommission( - params: AdminReportQueryParams, -): Promise> { - return adminRequest.get>(`${A}/reports/rebate-commission`, { - params, - }); -} diff --git a/src/api/index.ts b/src/api/index.ts index 5768684..fcde8c3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -21,7 +21,6 @@ export { getAdminReportDailyProfit, getAdminReportPlayDimension, getAdminReportPlayerWinLoss, - getAdminReportRebateCommission, } from "@/api/admin-reports"; export { downloadAdminReportJob, diff --git a/src/app/globals.css b/src/app/globals.css index a8a8232..ac86ec3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -190,6 +190,15 @@ @apply text-sm text-muted-foreground; } + /* 金额列:允许换行/缩小,避免大额 truncate 或溢出裁切 */ + .admin-money-value { + @apply min-w-0 whitespace-normal break-words [overflow-wrap:anywhere] tabular-nums leading-tight tracking-tight; + } + + .admin-table-shell [data-slot="table-cell"].admin-money-cell { + @apply min-w-[4.5rem] max-w-[11rem] whitespace-normal align-top leading-snug; + } + [data-slot="table-head"], [data-slot="table-cell"] { text-align: center; diff --git a/src/components/admin/admin-money-display.tsx b/src/components/admin/admin-money-display.tsx new file mode 100644 index 0000000..0fbb9a3 --- /dev/null +++ b/src/components/admin/admin-money-display.tsx @@ -0,0 +1,57 @@ +"use client"; + +import type { ElementType, ReactElement, ReactNode } from "react"; + +import { + adminMoneyDisplayClass, + adminMoneyDisplayTitle, + type AdminMoneyDisplaySize, +} from "@/lib/admin-money-display"; +import { cn } from "@/lib/utils"; + +export type AdminMoneyDisplayProps = { + children: ReactNode; + /** 用于自适应字号与 hover 完整值;缺省时从 string children 推断 */ + value?: string | number | null; + size?: AdminMoneyDisplaySize; + emphasize?: boolean; + className?: string; + title?: string; + as?: ElementType; +}; + +/** 卡片/摘要区金额:自适应字号 + 换行,禁止 truncate 裁切 */ +export function AdminMoneyDisplay({ + children, + value, + size = "lg", + emphasize = true, + className, + title, + as: Component = "span", +}: AdminMoneyDisplayProps): ReactElement { + const resolvedValue = + value != null + ? String(value) + : typeof children === "string" || typeof children === "number" + ? String(children) + : ""; + const resolvedTitle = title ?? adminMoneyDisplayTitle(resolvedValue); + + return ( + + {children} + + ); +} diff --git a/src/components/admin/admin-table-money.tsx b/src/components/admin/admin-table-money.tsx new file mode 100644 index 0000000..dbd4c8d --- /dev/null +++ b/src/components/admin/admin-table-money.tsx @@ -0,0 +1,46 @@ +"use client"; + +import type { ReactElement, ReactNode } from "react"; + +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; +import type { AdminMoneyDisplaySize } from "@/lib/admin-money-display"; +import { cn } from "@/lib/utils"; + +/** 表格/列表内金额:自适应字号 + 换行,配合 TableCell 的 admin-money-cell */ +export function AdminTableMoney({ + children, + className, + size = "sm", + emphasize = true, +}: { + children: ReactNode; + className?: string; + size?: AdminMoneyDisplaySize; + emphasize?: boolean; +}): ReactElement { + if (typeof children === "string" || typeof children === "number") { + const text = String(children); + return ( + + {text} + + ); + } + + return ( + + {children} + + ); +} + +/** TableCell 金额列常用 className */ +export function adminMoneyCellClassName(className?: string): string { + return cn("admin-money-cell min-w-0 whitespace-normal align-top leading-snug", className); +} diff --git a/src/hooks/use-admin-permission.ts b/src/hooks/use-admin-permission.ts new file mode 100644 index 0000000..27ccca0 --- /dev/null +++ b/src/hooks/use-admin-permission.ts @@ -0,0 +1,44 @@ +import { + adminHasAnyPermission, + adminHasAnyPermissionCode, + adminOperationalPermissionCodes, +} from "@/lib/admin-permissions"; +import { useAdminProfile } from "@/stores/admin-session"; +import type { AdminAccountKind, AdminProfile } from "@/types/api/admin-auth"; + +export function resolveAdminAccountKind(profile: AdminProfile | null | undefined): AdminAccountKind | null { + if (!profile) { + return null; + } + if (profile.account_kind) { + return profile.account_kind === "site_operator" ? "site_admin" : profile.account_kind; + } + if (profile.is_super_admin) { + return "super_admin"; + } + if (profile.agent != null) { + return "agent_operator"; + } + if (profile.site != null) { + return "site_admin"; + } + return "platform_account"; +} + +/** 统一权限与会话身份读取:legacy `prd.*` 与 action code 并存,新代码优先用 `hasAnyCode`。 */ +export function useAdminPermission() { + const profile = useAdminProfile(); + const legacyPermissions = profile?.permissions ?? []; + const operationalPermissions = adminOperationalPermissionCodes(profile); + + return { + profile, + legacyPermissions, + operationalPermissions, + accountKind: resolveAdminAccountKind(profile), + isSuperAdmin: profile?.is_super_admin === true, + /** @deprecated 逐步改用 {@link hasAnyCode} */ + hasAnyLegacy: (required: readonly string[]) => adminHasAnyPermission(legacyPermissions, required), + hasAnyCode: (required: readonly string[]) => adminHasAnyPermissionCode(operationalPermissions, required), + }; +} diff --git a/src/i18n/locales/en/dashboard.json b/src/i18n/locales/en/dashboard.json index dff0128..662160d 100644 --- a/src/i18n/locales/en/dashboard.json +++ b/src/i18n/locales/en/dashboard.json @@ -190,6 +190,49 @@ "bills": "Settlement" } }, + "finance": { + "title": "Finance workspace", + "subtitle": "{{name}} · reconcile & settlement", + "subtitleFallback": "Site finance · reconcile & settlement", + "abnormalTransfers": "Abnormal transfers", + "pendingConfirmBills": "Bills pending confirm", + "pendingConfirmHint": "Awaiting finance confirmation after period close", + "payableBills": "Bills pending payout", + "payableUnpaid": "Unpaid {{amount}}", + "payableUnpaidLabel": "Total unpaid", + "walletPlayers": "Wallet players", + "creditPlayersHint": "{{count}} credit players", + "settlementTitle": "Credit settlement", + "reconcileTitle": "Wallet reconcile", + "overviewEmpty": "No finance summary. Confirm the integration site is bound.", + "quickLinks": { + "reconcile": "Reconcile", + "transfers": "Transfer orders", + "bills": "Settlement center", + "reports": "Reports" + } + }, + "cs": { + "title": "Support workspace", + "subtitle": "{{name}} · player & ticket lookup", + "subtitleFallback": "Site support · player & ticket lookup", + "playerCount": "Site players", + "playerCountHint": "Registered players on this site", + "ticketsToday": "Tickets today", + "activePlayersToday": "Active players today", + "activePlayersHint": "Players with bets today", + "latestTicketAt": "Latest ticket {{time}}", + "noTicketToday": "No tickets yet today", + "workspaceTitle": "Quick access", + "scopeTitle": "Today at a glance", + "openModule": "Open module", + "overviewEmpty": "No support summary. Confirm the integration site is bound.", + "quickLinks": { + "players": "Players", + "tickets": "Tickets", + "wallet": "Wallet ledger" + } + }, "agent": { "title": "Operations overview", "subtitle": "{{name}} · your line", diff --git a/src/i18n/locales/en/reports.json b/src/i18n/locales/en/reports.json index a262e3b..6e27372 100644 --- a/src/i18n/locales/en/reports.json +++ b/src/i18n/locales/en/reports.json @@ -62,7 +62,6 @@ "hot_number_risk_report": "Hot number risk", "play_dimension_report": "Play dimension", "sold_out_number_report": "Sold-out numbers", - "rebate_commission_report": "Rebate / commission", "audit_operation_report": "Admin audit" }, "empty": "No matching reports", @@ -173,16 +172,6 @@ "extra": "Usage", "time": "Version" }, - "rebateCommission": { - "primary": "Play", - "secondary": "Orders", - "metricA": "Rebate", - "metricB": "Ticket items", - "metricC": "Commission", - "status": "Rule hit", - "extra": "Note", - "time": "Time" - }, "adminAudit": { "primary": "Log ID", "secondary": "Operator type", @@ -303,10 +292,6 @@ "title": "Sold-out number report", "summary": "Review sold-out numbers, sold-out time, and risk lock state by draw." }, - "rebate_commission": { - "title": "Commission / rebate report", - "summary": "Wallet-mode instant rebate by business date; defaults to the last 30 days. Not credit-line period settlement." - }, "admin_audit": { "title": "Admin operation audit report", "summary": "Admin actions by operator and record time; defaults to the last 30 days." diff --git a/src/i18n/locales/ne/dashboard.json b/src/i18n/locales/ne/dashboard.json index 6cd5a7a..36bcd5a 100644 --- a/src/i18n/locales/ne/dashboard.json +++ b/src/i18n/locales/ne/dashboard.json @@ -187,6 +187,49 @@ "bills": "सेटलमेन्ट" } }, + "finance": { + "title": "वित्त कार्यस्थल", + "subtitle": "{{name}} · मिलान र सेटलमेन्ट", + "subtitleFallback": "साइट वित्त · मिलान र सेटलमेन्ट", + "abnormalTransfers": "असामान्य स्थानान्तरण", + "pendingConfirmBills": "पुष्टि बाँकी बिल", + "pendingConfirmHint": "अवधि बन्द पछि वित्त पुष्टि बाँकी", + "payableBills": "भुक्तानी बाँकी बिल", + "payableUnpaid": "नतिरेको {{amount}}", + "payableUnpaidLabel": "कुल बाँकी", + "walletPlayers": "वालेट खेलाडी", + "creditPlayersHint": "क्रेडिट {{count}} जना", + "settlementTitle": "क्रेडिट सेटलमेन्ट", + "reconcileTitle": "वालेट मिलान", + "overviewEmpty": "वित्त सारांश छैन। साइट बाइन्डिङ जाँच गर्नुहोस्।", + "quickLinks": { + "reconcile": "मिलान केन्द्र", + "transfers": "स्थानान्तरण", + "bills": "सेटलमेन्ट केन्द्र", + "reports": "रिपोर्ट" + } + }, + "cs": { + "title": "सपोर्ट कार्यस्थल", + "subtitle": "{{name}} · खेलाडी र टिकट", + "subtitleFallback": "साइट सपोर्ट · खेलाडी र टिकट", + "playerCount": "साइट खेलाडी", + "playerCountHint": "यो साइटका दर्ता खेलाडी", + "ticketsToday": "आजका टिकट", + "activePlayersToday": "आज सक्रिय खेलाडी", + "activePlayersHint": "आज बाजी गर्ने खेलाडी", + "latestTicketAt": "पछिल्लो टिकट {{time}}", + "noTicketToday": "आज टिकट छैन", + "workspaceTitle": "छिटो पहुँच", + "scopeTitle": "आजको झलक", + "openModule": "मोड्युल खोल्नुहोस्", + "overviewEmpty": "सपोर्ट सारांश छैन। साइट बाइन्डिङ जाँच गर्नुहोस्।", + "quickLinks": { + "players": "खेलाडी", + "tickets": "टिकट", + "wallet": "वालेट लेजर" + } + }, "agent": { "title": "सञ्चालन सारांश", "subtitle": "{{name}} · यो लाइन", diff --git a/src/i18n/locales/ne/reports.json b/src/i18n/locales/ne/reports.json index a7164e4..3270596 100644 --- a/src/i18n/locales/ne/reports.json +++ b/src/i18n/locales/ne/reports.json @@ -61,7 +61,6 @@ "hot_number_risk_report": "लोकप्रिय नम्बर जोखिम", "play_dimension_report": "प्ले आयाम", "sold_out_number_report": "बिक्री समाप्त नम्बर", - "rebate_commission_report": "रिबेट / कमिसन", "audit_operation_report": "प्रशासक अडिट" }, "empty": "मिल्ने रिपोर्ट छैन", @@ -172,16 +171,6 @@ "extra": "प्रयोग", "time": "संस्करण" }, - "rebateCommission": { - "primary": "खेल", - "secondary": "अर्डर", - "metricA": "रिबेट", - "metricB": "टिकट आइटम", - "metricC": "कमिसन", - "status": "नियम मिलान", - "extra": "टिप्पणी", - "time": "समय" - }, "adminAudit": { "primary": "लग ID", "secondary": "अपरेटर प्रकार", @@ -300,10 +289,6 @@ "title": "सोल्ड-आउट नम्बर रिपोर्ट", "summary": "ड्र अनुसार सोल्ड-आउट नम्बर, समय र जोखिम लक अवस्था हेर्नुहोस्।" }, - "rebate_commission": { - "title": "कमिसन / रिबेट रिपोर्ट", - "summary": "वालेट-मोड तत्काल रिबेट, व्यावसायिक मितिअनुसार; मिति नचयेमा पछिल्लो ३० दिन।" - }, "admin_audit": { "title": "एडमिन अपरेशन अडिट रिपोर्ट", "summary": "अपरेटर र रेकर्ड समय अनुसार; मिति नचयेमा पछिल्लो ३० दिन।" diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json index 329068d..1323317 100644 --- a/src/i18n/locales/zh/adminUsers.json +++ b/src/i18n/locales/zh/adminUsers.json @@ -16,7 +16,7 @@ "deleteSuccess": "已删除 {{name}}", "deleteFailed": "删除失败", "roleListTitle": "平台角色管理", - "roleListHint": "可新增自定义角色并配置权限;内置角色(超级管理员、站点管理员、代理)不可删除。", + "roleListHint": "可新增自定义角色并配置权限;内置角色(超级管理员、站点管理员、站点财务、站点客服、代理)不可删除。", "createRole": "新增平台角色", "roleCreateSuccess": "已创建角色 {{name}}", "roleUpdateSuccess": "已更新角色 {{name}}", diff --git a/src/i18n/locales/zh/dashboard.json b/src/i18n/locales/zh/dashboard.json index adf1a12..3541350 100644 --- a/src/i18n/locales/zh/dashboard.json +++ b/src/i18n/locales/zh/dashboard.json @@ -190,6 +190,49 @@ "bills": "结算中心" } }, + "finance": { + "title": "财务工作台", + "subtitle": "{{name}} · 对账与结算", + "subtitleFallback": "站点财务 · 对账与结算", + "abnormalTransfers": "异常转账单", + "pendingConfirmBills": "待确认账单", + "pendingConfirmHint": "账期关账后待财务确认", + "payableBills": "待收付账单", + "payableUnpaid": "未收付 {{amount}}", + "payableUnpaidLabel": "待收付合计", + "walletPlayers": "钱包盘玩家", + "creditPlayersHint": "信用盘 {{count}} 人", + "settlementTitle": "信用结算", + "reconcileTitle": "钱包对账", + "overviewEmpty": "暂无财务摘要,请确认已绑定接入站点。", + "quickLinks": { + "reconcile": "对账中心", + "transfers": "转账单", + "bills": "结算中心", + "reports": "报表中心" + } + }, + "cs": { + "title": "客服工作台", + "subtitle": "{{name}} · 玩家与注单查询", + "subtitleFallback": "站点客服 · 玩家与注单查询", + "playerCount": "站点玩家", + "playerCountHint": "本站点注册玩家总数", + "ticketsToday": "今日注单", + "activePlayersToday": "今日活跃玩家", + "activePlayersHint": "今日有下注的玩家数", + "latestTicketAt": "最近注单 {{time}}", + "noTicketToday": "今日暂无注单", + "workspaceTitle": "常用入口", + "scopeTitle": "今日概况", + "openModule": "进入模块", + "overviewEmpty": "暂无客服摘要,请确认已绑定接入站点。", + "quickLinks": { + "players": "玩家查询", + "tickets": "注单查询", + "wallet": "钱包流水" + } + }, "agent": { "title": "经营概览", "subtitle": "{{name}} · 本线路", diff --git a/src/i18n/locales/zh/reports.json b/src/i18n/locales/zh/reports.json index fae9a92..e1a9f2c 100644 --- a/src/i18n/locales/zh/reports.json +++ b/src/i18n/locales/zh/reports.json @@ -62,7 +62,6 @@ "hot_number_risk_report": "热门号码风险", "play_dimension_report": "玩法维度", "sold_out_number_report": "售罄号码", - "rebate_commission_report": "佣金/回水", "audit_operation_report": "后台操作审计" }, "empty": "没有匹配的报表", @@ -173,16 +172,6 @@ "extra": "使用率", "time": "版本" }, - "rebateCommission": { - "primary": "玩法", - "secondary": "订单数", - "metricA": "回水", - "metricB": "注单数", - "metricC": "佣金", - "status": "配置命中", - "extra": "备注", - "time": "时间" - }, "adminAudit": { "primary": "日志 ID", "secondary": "操作者类型", @@ -303,10 +292,6 @@ "title": "售罄号码报表", "summary": "查看单期已售罄号码、售罄时间和风险封锁情况。" }, - "rebate_commission": { - "title": "佣金/回水报表", - "summary": "钱包盘下注立减回水,按业务日汇总;未选日期默认近 30 天。非信用占成账期。" - }, "admin_audit": { "title": "后台操作审计报表", "summary": "按操作人与记录创建时间筛选;未选日期默认近 30 天。" diff --git a/src/lib/admin-money-display.ts b/src/lib/admin-money-display.ts new file mode 100644 index 0000000..2f2e558 --- /dev/null +++ b/src/lib/admin-money-display.ts @@ -0,0 +1,65 @@ +import { cn } from "@/lib/utils"; + +export type AdminMoneyDisplaySize = "sm" | "md" | "lg" | "xl"; + +const SIZE_TIERS: Record = { + sm: ["text-sm", "text-xs", "text-[11px]", "text-[10px]", "text-[9px]"], + md: ["text-base", "text-sm", "text-xs", "text-[11px]", "text-[10px]"], + lg: ["text-lg sm:text-xl", "text-base sm:text-lg", "text-sm sm:text-base", "text-xs sm:text-sm", "text-[11px] sm:text-xs"], + xl: ["text-2xl", "text-xl", "text-lg", "text-base sm:text-lg", "text-sm sm:text-base"], +}; + +function tierForLength(len: number): number { + if (len > 20) { + return 4; + } + if (len > 16) { + return 3; + } + if (len > 12) { + return 2; + } + if (len > 8) { + return 1; + } + + return 0; +} + +/** 按字符长度分档缩小字号,避免卡片/栅格/表格内大额被裁切 */ +export function adminMoneyDisplaySizeClass( + value: string, + size: AdminMoneyDisplaySize = "lg", +): string { + const len = value.replace(/\s/g, "").length; + const tier = tierForLength(len); + return SIZE_TIERS[size][tier] ?? SIZE_TIERS[size][0]; +} + +export function adminMoneyDisplayClass( + value: string, + { + size = "lg", + emphasize = true, + className, + }: { + size?: AdminMoneyDisplaySize; + emphasize?: boolean; + className?: string; + } = {}, +): string { + return cn( + "min-w-0 whitespace-normal break-words [overflow-wrap:anywhere] tabular-nums leading-tight tracking-tight", + emphasize ? "font-semibold" : "font-medium", + adminMoneyDisplaySizeClass(value, size), + className, + ); +} + +export function adminMoneyDisplayTitle(value: string | number | null | undefined): string | undefined { + if (value == null) { + return undefined; + } + const text = String(value).trim(); + return text === "" || text === "…" || text === "—" ? undefined : text; +} diff --git a/src/lib/admin-permission-codes.ts b/src/lib/admin-permission-codes.ts new file mode 100644 index 0000000..a5170d7 --- /dev/null +++ b/src/lib/admin-permission-codes.ts @@ -0,0 +1,19 @@ +/** 与 Laravel `admin_menu_actions.permission_code` / API 鉴权对齐 */ + +export const PERM_DASHBOARD_VIEW = "dashboard.view" as const; +export const PERM_SERVICE_REPORT_VIEW = "service.report.view" as const; +export const PERM_SERVICE_REPORT_EXPORT = "service.report.export" as const; +export const PERM_SERVICE_PLAYERS_VIEW = "service.players.view" as const; +export const PERM_SERVICE_PLAYERS_MANAGE = "service.players.manage" as const; +export const PERM_SERVICE_PLAYERS_FREEZE = "service.players.freeze" as const; +export const PERM_SERVICE_TICKETS_VIEW = "service.tickets.view" as const; +export const PERM_SERVICE_WALLET_VIEW = "service.wallet.view" as const; +export const PERM_SERVICE_WALLET_MANAGE = "service.wallet.manage" as const; +export const PERM_SERVICE_WALLET_ADJUST = "service.wallet.adjust" as const; +export const PERM_SERVICE_RECONCILE_VIEW = "service.reconcile.view" as const; +export const PERM_SERVICE_RECONCILE_MANAGE = "service.reconcile.manage" as const; +export const PERM_SERVICE_AUDIT_VIEW = "service.audit.view" as const; +export const PERM_AGENT_NODE_VIEW = "agent.node.view" as const; +export const PERM_AGENT_NODE_MANAGE = "agent.node.manage" as const; +export const PERM_SETTLEMENT_AGENT_VIEW = "settlement.agent.view" as const; +export const PERM_SETTLEMENT_AGENT_MANAGE = "settlement.agent.manage" as const; diff --git a/src/lib/admin-permissions.ts b/src/lib/admin-permissions.ts index 7e85f8c..e3f2be1 100644 --- a/src/lib/admin-permissions.ts +++ b/src/lib/admin-permissions.ts @@ -9,3 +9,25 @@ export function adminHasAnyPermission( } return required.some((slug) => set.includes(slug)); } + +/** 当前登录管理员是否拥有 `required` 中任一 action code(与 API 鉴权 / `operational_permissions` 对齐)。 */ +export function adminHasAnyPermissionCode( + permissionCodes: readonly string[] | null | undefined, + required: readonly string[], +): boolean { + const set = permissionCodes ?? []; + if (set.length === 0 || required.length === 0) { + return false; + } + return required.some((code) => set.includes(code)); +} + +/** 读取会话中的 operational_permissions;旧缓存无该字段时回退到 permissions(兼容发版前 localStorage)。 */ +export function adminOperationalPermissionCodes( + profile: { operational_permissions?: string[]; permissions?: string[] } | null | undefined, +): readonly string[] { + if (Array.isArray(profile?.operational_permissions) && profile.operational_permissions.length > 0) { + return profile.operational_permissions; + } + return profile?.permissions ?? []; +} diff --git a/src/lib/admin-session-variants.ts b/src/lib/admin-session-variants.ts index 93bf31d..115bf61 100644 --- a/src/lib/admin-session-variants.ts +++ b/src/lib/admin-session-variants.ts @@ -5,7 +5,29 @@ export function isAgentOperator(profile: AdminProfile | null | undefined): boole return profile?.agent != null && profile.is_super_admin !== true; } -/** 平台站点管理员(绑定 site_admin 角色、无代理节点)。 */ -export function isSiteAdminOperator(profile: AdminProfile | null | undefined): boolean { - return profile?.site != null && profile.is_super_admin !== true; +/** 任意接入站点平台账号(site_admin / site_finance / site_cs)。 */ +export function isSiteOperator(profile: AdminProfile | null | undefined): boolean { + if (profile?.is_super_admin === true || profile?.agent != null) { + return false; + } + const kind = profile?.account_kind; + return kind === "site_admin" || kind === "site_finance" || kind === "site_cs" || profile?.site != null; +} + +/** 站点主运营(满配 site_admin);代理页站点方门控仅对此角色放宽。 */ +export function isSiteAdminOperator(profile: AdminProfile | null | undefined): boolean { + return profile?.account_kind === "site_admin" || ( + profile?.site != null + && profile?.account_kind == null + && profile?.is_super_admin !== true + && profile?.agent == null + ); +} + +export function isSiteFinanceOperator(profile: AdminProfile | null | undefined): boolean { + return profile?.account_kind === "site_finance"; +} + +export function isSiteCsOperator(profile: AdminProfile | null | undefined): boolean { + return profile?.account_kind === "site_cs"; } diff --git a/src/lib/admin-signed-money.tsx b/src/lib/admin-signed-money.tsx index 1498b2f..d9cd6ef 100644 --- a/src/lib/admin-signed-money.tsx +++ b/src/lib/admin-signed-money.tsx @@ -2,6 +2,7 @@ import type { ReactElement, ReactNode } from "react"; +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; import { cn } from "@/lib/utils"; /** 盈亏 / 输赢:负红、正绿、零灰 */ @@ -49,8 +50,25 @@ export function SignedMoney({ emphasize?: boolean; className?: string; }): ReactElement { + const colorClass = signedMoneyClass(amount, emphasize); + + if (typeof children === "string" || typeof children === "number") { + const text = String(children); + return ( + + {text} + + ); + } + return ( - + {children} ); diff --git a/src/lib/platform-system-roles.ts b/src/lib/platform-system-roles.ts index 87171e9..b280de7 100644 --- a/src/lib/platform-system-roles.ts +++ b/src/lib/platform-system-roles.ts @@ -2,12 +2,16 @@ import type { AdminRoleRow } from "@/types/api/index"; export const PLATFORM_SUPER_ADMIN_SLUG = "super_admin"; export const PLATFORM_SITE_ADMIN_SLUG = "site_admin"; +export const PLATFORM_SITE_FINANCE_SLUG = "site_finance"; +export const PLATFORM_SITE_CS_SLUG = "site_cs"; export const PLATFORM_AGENT_SLUG = "agent"; export function isPlatformFixedRole(role: Pick): boolean { return ( role.slug === PLATFORM_SUPER_ADMIN_SLUG || role.slug === PLATFORM_SITE_ADMIN_SLUG + || role.slug === PLATFORM_SITE_FINANCE_SLUG + || role.slug === PLATFORM_SITE_CS_SLUG || role.slug === PLATFORM_AGENT_SLUG ); } diff --git a/src/lib/report-export-map.ts b/src/lib/report-export-map.ts index cdf3ecd..57e25ae 100644 --- a/src/lib/report-export-map.ts +++ b/src/lib/report-export-map.ts @@ -18,7 +18,6 @@ export type ReportUiKey = | "hot_number_risk" | "play_dimension" | "sold_out_number" - | "rebate_commission" | "admin_audit"; /** Maps UI keys to POST /admin/report-jobs `report_type` */ @@ -30,7 +29,6 @@ export const REPORT_UI_TO_JOB_TYPE: Record = { hot_number_risk: "hot_number_risk_report", play_dimension: "play_dimension_report", sold_out_number: "sold_out_number_report", - rebate_commission: "rebate_commission_report", admin_audit: "audit_operation_report", }; diff --git a/src/modules/agents/agent-line-detail-panel.tsx b/src/modules/agents/agent-line-detail-panel.tsx index e0a82c8..2b80851 100644 --- a/src/modules/agents/agent-line-detail-panel.tsx +++ b/src/modules/agents/agent-line-detail-panel.tsx @@ -24,6 +24,8 @@ import { Button } from "@/components/ui/button"; import { percentValueToUi } from "@/lib/admin-rate-percent"; import { isLineRootAgentNode } from "@/lib/agent-profile-caps"; import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; +import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money"; import { cn } from "@/lib/utils"; import type { AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent"; @@ -369,10 +371,11 @@ function OverviewTab({

) : null} -
+
-
+
) : "—"} - - {summary ? formatCredit(summary.credit_limit) : "—"} + + {summary ? {formatCredit(summary.credit_limit)} : "—"} - - {summary ? formatCredit(summary.allocated_credit) : "—"} + + {summary ? {formatCredit(summary.allocated_credit)} : "—"} {childCountById.get(child.id) ?? 0} @@ -625,31 +631,45 @@ function MetricCard({ subtitle, accent = false, highlight = false, + money = true, }: { label: string; value: string; subtitle?: string; accent?: boolean; highlight?: boolean; + /** 金额类指标:自适应字号 + 换行 */ + money?: boolean; }): React.ReactElement { return (

{label}

-

- {value} -

+ {money ? ( + + {value} + + ) : ( +

+ {value} +

+ )} {subtitle ?

{subtitle}

: null}
); diff --git a/src/modules/agents/agent-profile-fields.tsx b/src/modules/agents/agent-profile-fields.tsx index ea82284..cae9e2b 100644 --- a/src/modules/agents/agent-profile-fields.tsx +++ b/src/modules/agents/agent-profile-fields.tsx @@ -19,6 +19,7 @@ import type { AgentParentCaps } from "@/types/api/admin-agent"; import { Info } from "lucide-react"; import { AdminNumericStepper } from "@/components/admin/admin-numeric-stepper"; +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { AGENT_PERCENT_HARD_MAX, @@ -411,14 +412,14 @@ function ReadOnlyScalar({
- + {value} {suffix ? {suffix} : null} - +
); } diff --git a/src/modules/dashboard/agent-dashboard-console.tsx b/src/modules/dashboard/agent-dashboard-console.tsx index cdac8d2..2595a6e 100644 --- a/src/modules/dashboard/agent-dashboard-console.tsx +++ b/src/modules/dashboard/agent-dashboard-console.tsx @@ -17,6 +17,7 @@ import { adminWeekdayKeyForDate, formatAdminBusinessDateIso, formatAdminCalendar import { cn } from "@/lib/utils"; import { useAdminProfile } from "@/stores/admin-session"; import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -207,9 +208,14 @@ export function AgentDashboardConsole(): ReactElement {
-

+ {formatDashboardCreditMajor(overview.credit_limit, displayCurrency)} -

+

{t("agent.creditAvailable", { amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency), diff --git a/src/modules/dashboard/dashboard-page-client.tsx b/src/modules/dashboard/dashboard-page-client.tsx index 1c17384..7b164cb 100644 --- a/src/modules/dashboard/dashboard-page-client.tsx +++ b/src/modules/dashboard/dashboard-page-client.tsx @@ -2,13 +2,15 @@ import type { ReactElement } from "react"; -import { isAgentOperator, isSiteAdminOperator } from "@/lib/admin-session-variants"; +import { isAgentOperator, isSiteFinanceOperator, isSiteCsOperator, isSiteOperator } from "@/lib/admin-session-variants"; import { AgentDashboardConsole } from "@/modules/dashboard/agent-dashboard-console"; import { DashboardConsole } from "@/modules/dashboard/dashboard-console"; +import { SiteCsDashboardConsole } from "@/modules/dashboard/site-cs-dashboard-console"; +import { SiteFinanceDashboardConsole } from "@/modules/dashboard/site-finance-dashboard-console"; import { SiteDashboardConsole } from "@/modules/dashboard/site-dashboard-console"; import { useAdminProfile } from "@/stores/admin-session"; -/** 超管/平台账号走全站仪表盘;站点管理员走站点仪表盘;代理经营账号走代理仪表盘。 */ +/** 超管/平台账号走全站仪表盘;站点运营账号走站点仪表盘;代理经营账号走代理仪表盘。 */ export function DashboardPageClient(): ReactElement { const profile = useAdminProfile(); @@ -16,7 +18,15 @@ export function DashboardPageClient(): ReactElement { return ; } - if (isSiteAdminOperator(profile)) { + if (isSiteFinanceOperator(profile)) { + return ; + } + + if (isSiteCsOperator(profile)) { + return ; + } + + if (isSiteOperator(profile)) { return ; } diff --git a/src/modules/dashboard/dashboard-visuals.tsx b/src/modules/dashboard/dashboard-visuals.tsx index 41ce997..14f435a 100644 --- a/src/modules/dashboard/dashboard-visuals.tsx +++ b/src/modules/dashboard/dashboard-visuals.tsx @@ -21,6 +21,7 @@ import { import { Card, CardContent } from "@/components/ui/card"; import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; import { Skeleton } from "@/components/ui/skeleton"; import { ChartContainer, @@ -37,6 +38,7 @@ import { } from "@/lib/money"; import { cn } from "@/lib/utils"; import { SignedMoney, signedMoneyClass } from "@/lib/admin-signed-money"; +import { adminMoneyDisplayClass } from "@/lib/admin-money-display"; import { buildBatchProgressConfig, buildFinanceStructureConfig, @@ -68,7 +70,7 @@ type DashboardFinanceMetricCell = { emphasize: boolean; }; -/** KPI 卡片底部三列:仅数字(币种见卡片主值),过长时省略号 + hover 看全称 */ +/** KPI 卡片底部三列:仅数字(币种见卡片主值),过长时缩小字号 + 换行 */ function formatDashboardMetricAmount( minor: number, currencyCode: string | null, @@ -115,7 +117,8 @@ function DashboardFinanceMetricCells({

{label}

-

+ {value} -

+
); } @@ -303,8 +311,11 @@ export function DashboardKpiCard({

{resolvedValue} @@ -418,9 +429,21 @@ export function StatCard({

{label}

-

- {value} -

+ {typeof value === "string" || typeof value === "number" ? ( + + {value} + + ) : ( +

+ {value} +

+ )} {deltaLabel ? (

{deltaLabel}

) : null} @@ -557,6 +580,16 @@ export function DashboardPanelCard({

{title}

{loading ? ( + ) : typeof value === "string" || typeof value === "number" ? ( + + {value} + ) : (

{value} diff --git a/src/modules/dashboard/site-cs-dashboard-console.tsx b/src/modules/dashboard/site-cs-dashboard-console.tsx new file mode 100644 index 0000000..ece4aad --- /dev/null +++ b/src/modules/dashboard/site-cs-dashboard-console.tsx @@ -0,0 +1,196 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useMemo, useState, type ReactElement } from "react"; +import { useTranslation } from "react-i18next"; +import { ClipboardList, RefreshCw, Search, Users } from "lucide-react"; + +import { getAdminDashboard } from "@/api/admin-dashboard"; +import { useAsyncEffect } from "@/hooks/use-async-effect"; +import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; +import { useTranslationRef } from "@/hooks/use-translation-ref"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; +import { + DashboardKpiCard, + DashboardScopeMetric, +} from "@/modules/dashboard/dashboard-visuals"; +import { cn } from "@/lib/utils"; +import { useAdminProfile } from "@/stores/admin-session"; +import type { + AdminDashboardSiteCsOverview, + AdminDashboardWarning, +} from "@/types/api/admin-dashboard"; +import { LotteryApiBizError } from "@/types/api/errors"; + +export function SiteCsDashboardConsole(): ReactElement { + const { t } = useTranslation(["dashboard", "common"]); + const tRef = useTranslationRef(["dashboard", "common"]); + const formatDt = useAdminDateTimeFormatter(); + const profile = useAdminProfile(); + const site = profile?.site ?? null; + + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [apiWarnings, setApiWarnings] = useState([]); + const [overview, setOverview] = useState(null); + + const load = useCallback(async (isRefresh = false) => { + if (isRefresh) { + setRefreshing(true); + } else { + setLoading(true); + } + setError(null); + + try { + const d = await getAdminDashboard(); + setOverview(d.site_cs_overview); + setApiWarnings(d.warnings ?? []); + } catch (e) { + const msg = + e instanceof LotteryApiBizError ? e.message : tRef.current("warnings.loadFailed"); + setError(msg); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [tRef]); + + useAsyncEffect(() => { + void load(false); + }, []); + + const activityHint = useMemo(() => { + if (!overview) { + return ""; + } + if (overview.latest_ticket_at) { + return t("cs.latestTicketAt", { time: formatDt(overview.latest_ticket_at) }); + } + return t("cs.noTicketToday"); + }, [formatDt, overview, t]); + + const quickLinks = useMemo( + () => [ + { href: "/admin/players", label: t("cs.quickLinks.players"), icon: Users }, + { href: "/admin/tickets", label: t("cs.quickLinks.tickets"), icon: ClipboardList }, + { href: "/admin/wallet/transactions", label: t("cs.quickLinks.wallet"), icon: Search }, + ], + [t], + ); + + return ( +

+
+
+

{t("cs.title")}

+

+ {site + ? t("cs.subtitle", { name: site.name || site.code }) + : t("cs.subtitleFallback")} +

+
+ +
+ + {error ? ( + + {t("notice")} + {error} + + ) : null} + + {!loading && apiWarnings.length > 0 ? ( + + {t("notice")} + {apiWarnings.map((w) => w.message).join(" ")} + + ) : null} + + {loading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : overview ? ( +
+
+ } + hint={t("cs.playerCountHint")} + /> + } + hint={activityHint} + /> + } + hint={t("cs.activePlayersHint")} + /> +
+ + + + {t("cs.workspaceTitle")} + + + {quickLinks.map((link) => { + const Icon = link.icon; + return ( + + + {link.label} + {t("cs.openModule")} + + ); + })} + + + + + + {t("cs.scopeTitle")} + + + + + + +
+ ) : ( + + {t("cs.overviewEmpty")} + + )} +
+ ); +} diff --git a/src/modules/dashboard/site-finance-dashboard-console.tsx b/src/modules/dashboard/site-finance-dashboard-console.tsx new file mode 100644 index 0000000..455630c --- /dev/null +++ b/src/modules/dashboard/site-finance-dashboard-console.tsx @@ -0,0 +1,244 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useMemo, useState, type ReactElement } from "react"; +import { useTranslation } from "react-i18next"; +import { AlertTriangle, ClipboardList, RefreshCw, Scale, Users, Wallet } from "lucide-react"; + +import { getAdminDashboard } from "@/api/admin-dashboard"; +import { useAsyncEffect } from "@/hooks/use-async-effect"; +import { useTranslationRef } from "@/hooks/use-translation-ref"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; +import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card"; +import { + AbnormalTransferPanelFooter, + DashboardKpiCard, + DashboardScopeMetric, + DashboardStatRow, +} from "@/modules/dashboard/dashboard-visuals"; +import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics"; +import { cn } from "@/lib/utils"; +import { useAdminProfile } from "@/stores/admin-session"; +import type { + AdminDashboardSiteFinanceOverview, + AdminDashboardWarning, +} from "@/types/api/admin-dashboard"; +import type { DrawCurrentSnapshot } from "@/types/api/public-draw"; +import { LotteryApiBizError } from "@/types/api/errors"; + +export function SiteFinanceDashboardConsole(): ReactElement { + const { t } = useTranslation(["dashboard", "common"]); + const tRef = useTranslationRef(["dashboard", "common"]); + const profile = useAdminProfile(); + const site = profile?.site ?? null; + + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [apiWarnings, setApiWarnings] = useState([]); + const [hall, setHall] = useState(null); + const [drawId, setDrawId] = useState(null); + const [overview, setOverview] = useState(null); + const [walletPermission, setWalletPermission] = useState(false); + + const load = useCallback(async (isRefresh = false) => { + if (isRefresh) { + setRefreshing(true); + } else { + setLoading(true); + } + setError(null); + + try { + const d = await getAdminDashboard(); + setHall(d.hall); + setOverview(d.site_finance_overview); + setApiWarnings(d.warnings ?? []); + setWalletPermission(d.capabilities?.wallet_transfer_view ?? false); + if (d.resolved_draw != null) { + setDrawId(d.resolved_draw.id); + } else { + setDrawId(null); + } + } catch (e) { + const msg = + e instanceof LotteryApiBizError ? e.message : tRef.current("warnings.loadFailed"); + setError(msg); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [tRef]); + + useAsyncEffect(() => { + void load(false); + }, []); + + const displayCurrency = overview?.currency_code ?? "NPR"; + const abnormalCount = overview?.abnormal_transfer_count ?? null; + + const quickLinks = useMemo( + () => [ + { href: "/admin/reconcile", label: t("finance.quickLinks.reconcile") }, + { href: "/admin/wallet/transfer-orders", label: t("finance.quickLinks.transfers") }, + { href: "/admin/settlement-center", label: t("finance.quickLinks.bills") }, + { href: "/admin/reports", label: t("finance.quickLinks.reports") }, + ], + [t], + ); + + return ( +
+
+
+

{t("finance.title")}

+

+ {site + ? t("finance.subtitle", { name: site.name || site.code }) + : t("finance.subtitleFallback")} +

+
+ +
+ + {error ? ( + + {t("notice")} + {error} + + ) : null} + + {!loading && apiWarnings.length > 0 ? ( + + {t("notice")} + {apiWarnings.map((w) => w.message).join(" ")} + + ) : null} + + {loading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ ) : overview ? ( +
+
+ } + hint={t("abnormalTransferScope")} + accent={(abnormalCount ?? 0) > 0 ? "destructive" : "muted"} + /> + } + hint={t("finance.pendingConfirmHint")} + accent={overview.pending_confirm_bill_count > 0 ? "primary" : "muted"} + /> + } + hint={t("finance.payableUnpaid", { + amount: formatDashboardMoneyMinor(overview.payable_unpaid_minor, displayCurrency), + })} + accent={overview.payable_bill_count > 0 ? "destructive" : "muted"} + /> + } + hint={t("finance.creditPlayersHint", { count: overview.credit_player_count })} + /> +
+ +
+ + + {t("finance.settlementTitle")} + + + + + + + + + + + {t("finance.reconcileTitle")} + + + + + + {t("viewTransferOrders")} + + + +
+ + + + {t("quickLinksTitle")} + + + {quickLinks.map((link) => ( + + {link.label} + + ))} + + +
+ ) : ( + + {t("finance.overviewEmpty")} + + )} + + +
+ ); +} diff --git a/src/modules/draws/draws-index-console.tsx b/src/modules/draws/draws-index-console.tsx index afdb107..de50cd9 100644 --- a/src/modules/draws/draws-index-console.tsx +++ b/src/modules/draws/draws-index-console.tsx @@ -49,6 +49,7 @@ import { canDeleteDrawRow, canEditDrawRow, } from "./draw-list-actions"; +import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money"; import { formatAdminMinorUnits } from "@/lib/money"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useExportLabels } from "@/hooks/use-export-labels"; @@ -429,25 +430,30 @@ export function DrawsIndexConsole() { {canViewFinance ? ( <> - - {row.total_bet_minor != null - ? formatAdminMinorUnits(row.total_bet_minor, defaultCurrency) - : "—"} + + {row.total_bet_minor != null ? ( + + {formatAdminMinorUnits(row.total_bet_minor, defaultCurrency)} + + ) : "—"} - - {row.total_payout_minor != null - ? formatAdminMinorUnits(row.total_payout_minor, defaultCurrency) - : "—"} + + {row.total_payout_minor != null ? ( + + {formatAdminMinorUnits(row.total_payout_minor, defaultCurrency)} + + ) : "—"} - {row.profit_loss_minor != null - ? formatAdminMinorUnits(row.profit_loss_minor, defaultCurrency) - : "—"} + {row.profit_loss_minor != null ? ( + + {formatAdminMinorUnits(row.profit_loss_minor, defaultCurrency)} + + ) : "—"} ) : null} diff --git a/src/modules/reconcile/reconcile-console.tsx b/src/modules/reconcile/reconcile-console.tsx index fa50e61..c9b35fd 100644 --- a/src/modules/reconcile/reconcile-console.tsx +++ b/src/modules/reconcile/reconcile-console.tsx @@ -343,17 +343,21 @@ export function ReconcileConsole(): React.ReactElement { {t("createTitle")}

{t("createHint")}

- -
-
-
- - -
-
+ +
+
+ {t("reconcileType")} + + {t("reconcileTypeFixed")} + +
+
+ +
{ @@ -363,102 +367,101 @@ export function ReconcileConsole(): React.ReactElement { />
- -
-
- - setPlayerSearch(e.target.value)} - placeholder={t("playerSearchPlaceholder")} - /> -
- - {selectedPlayer ? ( -
-
- {selectedPlayer.site_player_id} - {selectedPlayer.nickname ? ` · ${selectedPlayer.nickname}` : ""} - {selectedPlayer.username ? ` · ${selectedPlayer.username}` : ""} - {` · ${selectedPlayer.site_code}`} -
- -
- ) : null} - - {playerSearch.trim() !== "" || playerResults.length > 0 || playerLoading ? ( -
-
- {playerLoading ? ( - - ) : playerResults.length === 0 ? ( - - ) : ( -
- {playerResults.map((player) => { - const active = selectedPlayer?.id === player.id; - return ( - - ); - })} -
- )} -
-
- ) : null} +
+ + setPlayerSearch(e.target.value)} + placeholder={t("playerSearchPlaceholder")} + /> +
+
+
-
- -
+ {selectedPlayer ? ( +
+
+ {selectedPlayer.site_player_id} + {selectedPlayer.nickname ? ` · ${selectedPlayer.nickname}` : ""} + {selectedPlayer.username ? ` · ${selectedPlayer.username}` : ""} + {` · ${selectedPlayer.site_code}`} +
+ +
+ ) : null} + + {playerSearch.trim() !== "" || playerResults.length > 0 || playerLoading ? ( +
+
+ {playerLoading ? ( + + ) : playerResults.length === 0 ? ( + + ) : ( +
+ {playerResults.map((player) => { + const active = selectedPlayer?.id === player.id; + return ( + + ); + })} +
+ )} +
+
+ ) : null} ) : ( diff --git a/src/modules/reports/reports-console.tsx b/src/modules/reports/reports-console.tsx index a4c2831..a626dc9 100644 --- a/src/modules/reports/reports-console.tsx +++ b/src/modules/reports/reports-console.tsx @@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { CalendarDays, - CircleDollarSign, Database, FileDown, FileSpreadsheet, @@ -31,7 +30,6 @@ import { getAdminReportDailyProfit, getAdminReportPlayDimension, getAdminReportPlayerWinLoss, - getAdminReportRebateCommission, } from "@/api/admin-reports"; import { buildReportJobParameters, @@ -42,7 +40,13 @@ import { getAdminRiskPoolDetail, getAdminRiskPools } from "@/api/admin-risk"; import { getAdminUsers } from "@/api/admin-users"; import { getAdminTransferOrders } from "@/api/admin-wallet"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; -import { PRD_REPORT_EXPORT, PRD_REPORT_VIEW } from "@/lib/admin-prd"; +import { + PRD_AUDIT_VIEW, + PRD_REPORT_EXPORT, + PRD_REPORT_VIEW, + PRD_RISK_ACCESS_ANY, + PRD_WALLET_TRANSFER_ACCESS_ANY, +} from "@/lib/admin-prd"; import { useAdminProfile } from "@/stores/admin-session"; import { adminAgentDisplayLabel } from "@/components/admin/admin-agent-columns"; import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; @@ -73,6 +77,7 @@ import { useAdminCurrencyCatalog, getCachedAdminCurrencies } from "@/hooks/use-a import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { formatAdminInstant } from "@/lib/admin-datetime"; import { getAdminRequestLocale } from "@/lib/admin-locale"; +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; import { signedMoneyClass } from "@/lib/admin-signed-money"; import { cn } from "@/lib/utils"; import { formatAdminMinorUnits } from "@/lib/money"; @@ -88,7 +93,6 @@ import type { AdminReportDailyProfitRow, AdminReportPlayDimensionRow, AdminReportPlayerWinLossRow, - AdminReportRebateCommissionRow, } from "@/types/api/admin-reports"; export type ReportCategory = "profit" | "wallet" | "risk" | "audit"; @@ -107,7 +111,6 @@ type ReportKey = | "hot_number_risk" | "play_dimension" | "sold_out_number" - | "rebate_commission" | "admin_audit"; type ReportDefinition = { @@ -118,8 +121,22 @@ type ReportDefinition = { scope: string; fields: FieldKey[]; connected: boolean; + requiredAny: readonly string[]; }; +const PRD_REPORTS_VIEW_ACCESS_ANY = [PRD_REPORT_VIEW] as const; + +const REPORTS: ReportDefinition[] = [ + { key: "draw_profit", category: "profit", icon: Ticket, filterKind: "draw", scope: "drawNo", fields: ["drawNo"], connected: true, requiredAny: PRD_REPORTS_VIEW_ACCESS_ANY }, + { key: "daily_profit", category: "profit", icon: CalendarDays, filterKind: "date", scope: "date", fields: ["period"], connected: true, requiredAny: PRD_REPORTS_VIEW_ACCESS_ANY }, + { key: "player_win_loss", category: "profit", icon: Users, filterKind: "player_period", scope: "playerPeriod", fields: ["player", "period"], connected: true, requiredAny: PRD_REPORTS_VIEW_ACCESS_ANY }, + { key: "player_transfer", category: "wallet", icon: WalletCards, filterKind: "player_period", scope: "playerPeriod", fields: ["player", "period"], connected: true, requiredAny: PRD_WALLET_TRANSFER_ACCESS_ANY }, + { key: "hot_number_risk", category: "risk", icon: ShieldAlert, filterKind: "draw_number", scope: "drawNumber", fields: ["drawNo", "number"], connected: true, requiredAny: PRD_RISK_ACCESS_ANY }, + { key: "play_dimension", category: "profit", icon: ListFilter, filterKind: "play_period", scope: "playPeriod", fields: ["play", "period"], connected: true, requiredAny: PRD_REPORTS_VIEW_ACCESS_ANY }, + { key: "sold_out_number", category: "risk", icon: ShieldCheck, filterKind: "draw", scope: "drawNo", fields: ["drawNo"], connected: true, requiredAny: PRD_RISK_ACCESS_ANY }, + { key: "admin_audit", category: "audit", icon: FileSpreadsheet, filterKind: "operator_period", scope: "operatorPeriod", fields: ["operator", "period"], connected: true, requiredAny: [PRD_AUDIT_VIEW] }, +]; + type PreviewColumns = { primary: string; secondary: string; @@ -159,7 +176,6 @@ type ReportResult = | { key: "hot_number_risk"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta | null; raw: AdminRiskPoolShowData } | { key: "play_dimension"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportPlayDimensionRow[] } | { key: "sold_out_number"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminRiskPoolRow[] } - | { key: "rebate_commission"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportRebateCommissionRow[] } | { key: "admin_audit"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminAuditLogRow[] }; type StatCard = { @@ -182,18 +198,6 @@ type PlayOption = { label: string; }; -const REPORTS: ReportDefinition[] = [ - { key: "draw_profit", category: "profit", icon: Ticket, filterKind: "draw", scope: "drawNo", fields: ["drawNo"], connected: true }, - { key: "daily_profit", category: "profit", icon: CalendarDays, filterKind: "date", scope: "date", fields: ["period"], connected: true }, - { key: "player_win_loss", category: "profit", icon: Users, filterKind: "player_period", scope: "playerPeriod", fields: ["player", "period"], connected: true }, - { key: "player_transfer", category: "wallet", icon: WalletCards, filterKind: "player_period", scope: "playerPeriod", fields: ["player", "period"], connected: true }, - { key: "hot_number_risk", category: "risk", icon: ShieldAlert, filterKind: "draw_number", scope: "drawNumber", fields: ["drawNo", "number"], connected: true }, - { key: "play_dimension", category: "profit", icon: ListFilter, filterKind: "play_period", scope: "playPeriod", fields: ["play", "period"], connected: true }, - { key: "sold_out_number", category: "risk", icon: ShieldCheck, filterKind: "draw", scope: "drawNo", fields: ["drawNo"], connected: true }, - { key: "rebate_commission", category: "profit", icon: CircleDollarSign, filterKind: "play_period", scope: "playPeriod", fields: ["play", "period"], connected: true }, - { key: "admin_audit", category: "audit", icon: FileSpreadsheet, filterKind: "operator_period", scope: "operatorPeriod", fields: ["operator", "period"], connected: true }, -]; - const emptyFilters: ReportFilters = { drawNo: "", drawId: null, @@ -414,38 +418,6 @@ function buildPlayDimensionRowsAndSummary( }; } -function buildRebateCommissionRowsAndSummary( - items: AdminReportRebateCommissionRow[], - total: number, - t: (key: string) => string, - pageScopedLabel: (statKey: string) => string, - currencyCode: string, -): Pick, "rows" | "summary"> { - let totalRebate = 0; - let totalOrders = 0; - - const rows = items.map((item) => { - totalRebate += item.total_rebate_minor; - totalOrders += item.order_count; - return { - play_code: item.play_code, - total_rebate_minor: item.total_rebate_minor, - order_count: item.order_count, - ticket_item_count: item.ticket_item_count, - }; - }); - - return { - rows, - summary: [ - { label: t("preview.stats.records"), value: String(total) }, - { label: t("preview.stats.currentPage"), value: String(items.length) }, - { label: pageScopedLabel("rebate"), value: formatPlainMoney(totalRebate, currencyCode) }, - { label: pageScopedLabel("orders"), value: String(totalOrders) }, - ], - }; -} - function metaFromList(meta: { current_page: number; per_page: number; total: number; last_page: number }): ReportMeta { return { total: meta.total, @@ -612,13 +584,6 @@ function defaultSummaryCards( { label: t("preview.stats.currency"), value: t("preview.stats.notQueried") }, { label: t("preview.stats.usage"), value: t("preview.stats.notQueried") }, ]; - case "rebate_commission": - return [ - { label: t("preview.stats.records"), value: t("preview.stats.notQueried") }, - { label: t("fields.play"), value: filters.play || t("filterAll") }, - { label: t("preview.stats.rebate"), value: t("preview.stats.notQueried") }, - { label: t("preview.stats.orders"), value: t("preview.stats.notQueried") }, - ]; case "admin_audit": return [ { label: t("preview.stats.records"), value: t("preview.stats.notQueried") }, @@ -641,14 +606,15 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa const profile = useAdminProfile(); const canViewReports = adminHasAnyPermission(profile?.permissions, [PRD_REPORT_VIEW]); const canExportReports = adminHasAnyPermission(profile?.permissions, [PRD_REPORT_EXPORT]); + const permissionSlugs = useMemo(() => profile?.permissions ?? [], [profile?.permissions]); useAdminCurrencyCatalog(); useAdminPlayTypeCatalog(); const playCodeLabel = useAdminPlayCodeLabel(); const formatTs = useAdminDateTimeFormatter(); - const filteredReports = useMemo( - () => (initialCategory ? REPORTS.filter((report) => report.category === initialCategory) : REPORTS), - [initialCategory], - ); + const filteredReports = useMemo(() => { + const visible = REPORTS.filter((report) => adminHasAnyPermission(permissionSlugs, report.requiredAny)); + return initialCategory ? visible.filter((report) => report.category === initialCategory) : visible; + }, [initialCategory, permissionSlugs]); const [selectedKey, setSelectedKey] = useState( filteredReports[0]?.key ?? REPORTS[0].key, ); @@ -758,17 +724,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa extra: t("preview.columns.soldOut.extra"), time: t("preview.columns.soldOut.time"), }; - case "rebate_commission": - return { - primary: t("preview.columns.rebateCommission.primary"), - secondary: t("preview.columns.rebateCommission.secondary"), - metricA: t("preview.columns.rebateCommission.metricA"), - metricB: t("preview.columns.rebateCommission.metricB"), - metricC: t("preview.columns.rebateCommission.metricC"), - status: t("preview.columns.rebateCommission.status"), - extra: t("preview.columns.rebateCommission.extra"), - time: t("preview.columns.rebateCommission.time"), - }; case "admin_audit": return { primary: t("preview.columns.adminAudit.primary"), @@ -1057,22 +1012,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa }); break; } - case "rebate_commission": { - const payload = await getAdminReportRebateCommission( - reportListParams(filters, page, perPage), - ); - const currencyCode = resolveDisplayCurrency(payload.currency_code); - setDisplayCurrency(currencyCode); - const next = buildRebateCommissionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel, currencyCode); - setResult({ - key: "rebate_commission", - raw: payload.items, - rows: next.rows, - meta: metaFromList(payload.meta), - summary: next.summary, - }); - break; - } case "admin_audit": { const operatorId = filters.operatorId ?? parsePositiveInteger(filters.operator); const payload = await getAdminAuditLogs({ @@ -1134,10 +1073,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa ...prev, drawNo: drawNoFromUrl || prev.drawNo, })); - if (drawNoFromUrl) { + if (drawNoFromUrl && filteredReports.some((report) => report.key === "draw_profit")) { setSelectedKey("draw_profit"); } - }, [drawNoFromUrl]); + }, [drawNoFromUrl, filteredReports]); useEffect(() => { queueMicrotask(() => { @@ -1563,21 +1502,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa )); } - if (result.key === "rebate_commission") { - return result.raw.map((item) => ( - - {playCodeLabel(item.play_code)} - {item.order_count} - {formatPlainMoney(item.total_rebate_minor, displayCurrency)} - {item.ticket_item_count} - - - - - - - - - - )); - } - if (result.key === "admin_audit") { return result.raw.map((item) => ( @@ -1598,6 +1522,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa return (
+ {filteredReports.length === 0 ? ( + + ) : ( + <>
@@ -1652,11 +1580,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa -
+
{(result?.summary ?? defaultSummaryCards(selectedReport.key, filters, t)).map((item) => ( -
+
{item.label}
-
{item.value}
+ + {item.value} +
))}
@@ -1722,6 +1652,8 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa ) : null} + + )}
); } diff --git a/src/modules/settlement/settlement-bill-breakdown.tsx b/src/modules/settlement/settlement-bill-breakdown.tsx index d4764f0..25ff949 100644 --- a/src/modules/settlement/settlement-bill-breakdown.tsx +++ b/src/modules/settlement/settlement-bill-breakdown.tsx @@ -4,6 +4,7 @@ import { ArrowRight } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { SettlementBillRow } from "@/api/admin-agent-settlement"; +import { AdminMoneyDisplay } from "@/components/admin/admin-money-display"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { cn } from "@/lib/utils"; import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics"; @@ -64,9 +65,15 @@ export function SettlementBillSummaryHeader({

{t("settlementCenter:billDisplay.settlementAmount", { defaultValue: "结算金额" })}

-

+ {formatDashboardMoneyMinor(direction.amount, currencyCode)} -

+
@@ -75,9 +82,15 @@ export function SettlementBillSummaryHeader({

{t("settlementCenter:columns.paid", { defaultValue: "已收付" })}

-

+ {formatDashboardMoneyMinor(bill.paid_amount ?? 0, currencyCode)} -

+
{t("settlementCenter:columns.unpaid", { defaultValue: "未结" })}

-

{formatDashboardMoneyMinor(bill.unpaid_amount, currencyCode)} -

+ {unpaid ? (

{bill.status === "pending_confirm" diff --git a/src/modules/settlement/settlement-bills-table.tsx b/src/modules/settlement/settlement-bills-table.tsx index 1a48403..526861e 100644 --- a/src/modules/settlement/settlement-bills-table.tsx +++ b/src/modules/settlement/settlement-bills-table.tsx @@ -8,6 +8,7 @@ import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; +import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money"; import { signedMoneyClass } from "@/lib/admin-signed-money"; import { cn } from "@/lib/utils"; import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range"; @@ -243,16 +244,20 @@ export function SettlementBillsTable({ )} ) : null} - -

+ + {formatDashboardMoneyMinor(direction.amount, currencyCode)} -
+ - - {formatDashboardMoneyMinor(row.paid_amount ?? 0, currencyCode)} + + + {formatDashboardMoneyMinor(row.paid_amount ?? 0, currencyCode)} + - - {formatDashboardMoneyMinor(row.unpaid_amount, currencyCode)} + + + {formatDashboardMoneyMinor(row.unpaid_amount, currencyCode)} + diff --git a/src/modules/settlement/settlement-operations-panel.tsx b/src/modules/settlement/settlement-operations-panel.tsx index caf06d2..a2b09bc 100644 --- a/src/modules/settlement/settlement-operations-panel.tsx +++ b/src/modules/settlement/settlement-operations-panel.tsx @@ -34,6 +34,7 @@ import { TableRow, } from "@/components/ui/table"; import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; +import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money"; import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics"; import { LotteryApiBizError } from "@/types/api/errors"; import { settlementAdjustmentTypeLabel } from "@/modules/settlement/settlement-status-label"; @@ -368,8 +369,10 @@ export function SettlementOperationsPanel({ {row.billId > 0 ? `#${row.billId}` : "—"} - - {formatDashboardMoneyMinor(row.amount, currencyCode)} + + + {formatDashboardMoneyMinor(row.amount, currencyCode)} + {row.summary} diff --git a/src/modules/wallet/wallet-console.tsx b/src/modules/wallet/wallet-console.tsx index 9939cb3..e49b112 100644 --- a/src/modules/wallet/wallet-console.tsx +++ b/src/modules/wallet/wallet-console.tsx @@ -59,6 +59,7 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter" import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useExportLabels } from "@/hooks/use-export-labels"; import { PlayerLedgerSourceBadge } from "@/components/admin/player-funding-badges"; +import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money"; import { formatAdminMinorUnits } from "@/lib/money"; import { creditLedgerReasonLabel } from "@/modules/settlement/settlement-status-label"; import { LotteryApiBizError } from "@/types/api/errors"; @@ -578,8 +579,10 @@ export function TransferOrdersPanel(): React.ReactElement { {row.direction} - - {formatAdminMinorUnits(row.amount, row.currency_code)} + + + {formatAdminMinorUnits(row.amount, row.currency_code)} + {statusLabelT(row.status, t)} @@ -907,8 +910,10 @@ export function WalletTxnsPanel(): React.ReactElement { {walletTxnBizTypeLabel(row.biz_type, row.ledger_source, t, tSettlement)} - - {row.amount_formatted ?? formatAdminMinorUnits(row.amount)} + + + {row.amount_formatted ?? formatAdminMinorUnits(row.amount)} + ({row.direction === 1 ? t("in") : t("out")}) @@ -1031,9 +1036,13 @@ export function PlayerWalletPanel(): React.ReactElement { {w.wallet_type} {w.currency_code} - {w.balance} - - {formatAdminMinorUnits(w.available_balance, w.currency_code)} + + {w.balance} + + + + {formatAdminMinorUnits(w.available_balance, w.currency_code)} + )) diff --git a/src/stores/admin-profile.ts b/src/stores/admin-profile.ts index 1a70759..f07d781 100644 --- a/src/stores/admin-profile.ts +++ b/src/stores/admin-profile.ts @@ -21,6 +21,9 @@ export function readProfile(): AdminProfile | null { const permissions = Array.isArray(v.permissions) ? v.permissions.filter((s): s is string => typeof s === "string") : []; + const operationalPermissions = Array.isArray(v.operational_permissions) + ? v.operational_permissions.filter((s): s is string => typeof s === "string") + : []; const navigation = Array.isArray(v.navigation) ? v.navigation.filter((item): item is AdminNavItem => { return ( @@ -32,13 +35,26 @@ export function readProfile(): AdminProfile | null { ); }) : []; + const accountKind = + v.account_kind === "super_admin" + || v.account_kind === "site_admin" + || v.account_kind === "site_finance" + || v.account_kind === "site_cs" + || v.account_kind === "site_operator" + || v.account_kind === "agent_operator" + || v.account_kind === "platform_account" + ? (v.account_kind === "site_operator" ? "site_admin" : v.account_kind) + : undefined; return { id: v.id, username: v.username, nickname: v.nickname, email: typeof v.email === "string" || v.email === null ? v.email : null, permissions, + operational_permissions: operationalPermissions, navigation, + is_super_admin: v.is_super_admin === true ? true : undefined, + account_kind: accountKind, }; } } catch { diff --git a/src/types/api/admin-auth.ts b/src/types/api/admin-auth.ts index b52727a..e634603 100644 --- a/src/types/api/admin-auth.ts +++ b/src/types/api/admin-auth.ts @@ -16,20 +16,31 @@ export type AdminAuthLoginRequest = { import type { AdminAgentContext } from "@/types/api/admin-agent"; +/** 登录态账号形态(与 Laravel `auth/me.account_kind` 对齐) */ +export type AdminAccountKind = + | "super_admin" + | "site_admin" + | "site_finance" + | "site_cs" + | "agent_operator" + | "platform_account"; + /** 登录成功后缓存于会话(localStorage)的管理员摘要 */ export type AdminProfile = { id: number; username: string; nickname: string; email: string | null; - /** 与 Laravel `admin_permissions.slug` 一致(如 `prd.*`);超管为全量列表 */ + /** Legacy 产品权限 slug(如 `prd.*`);侧栏与旧页面门控 */ permissions?: string[]; /** 当前管理员可见的后台菜单,由 Laravel 注册表统一下发。 */ navigation?: AdminNavItem[]; /** 代理账号绑定节点;超管为 null */ agent?: AdminAgentContext | null; is_super_admin?: boolean; - /** 与 permissions 同值,语义上强调“可操作权限” */ + /** 账号形态:超管 / 站点运营 / 代理经营 / 其他平台账号 */ + account_kind?: AdminAccountKind; + /** API 鉴权 action code(如 `service.report.view`);与后端 `effectiveMenuActionPermissionCodes` 一致 */ operational_permissions?: string[]; /** 当前代理可下放给下级的 prd.* 上限(未配置 grants 时与操作权限一致) */ delegation_ceiling?: string[]; diff --git a/src/types/api/admin-dashboard.ts b/src/types/api/admin-dashboard.ts index 438a5e4..742816e 100644 --- a/src/types/api/admin-dashboard.ts +++ b/src/types/api/admin-dashboard.ts @@ -49,6 +49,31 @@ export type AdminDashboardCapabilities = { wallet_transfer_view: boolean; }; +export type AdminDashboardSiteFinanceOverview = { + admin_site_id: number; + site_code: string; + site_name: string; + wallet_player_count: number; + credit_player_count: number; + pending_confirm_bill_count: number; + payable_bill_count: number; + payable_unpaid_minor: number; + pending_bill_count: number; + pending_unpaid_minor: number; + abnormal_transfer_count: number; + currency_code: string | null; +}; + +export type AdminDashboardSiteCsOverview = { + admin_site_id: number; + site_code: string; + site_name: string; + player_count: number; + ticket_order_count_today: number; + active_player_count_today: number; + latest_ticket_at: string | null; +}; + /** 站点管理员首页摘要(`GET /api/v1/admin/dashboard` → `site_overview`) */ export type AdminDashboardSiteOverview = { admin_site_id: number; @@ -178,4 +203,6 @@ export type AdminDashboardData = { capabilities: AdminDashboardCapabilities; agent_overview: AdminDashboardAgentOverview | null; site_overview: AdminDashboardSiteOverview | null; + site_finance_overview: AdminDashboardSiteFinanceOverview | null; + site_cs_overview: AdminDashboardSiteCsOverview | null; }; diff --git a/src/types/api/admin-reports.ts b/src/types/api/admin-reports.ts index ba81c2f..1789d11 100644 --- a/src/types/api/admin-reports.ts +++ b/src/types/api/admin-reports.ts @@ -24,13 +24,6 @@ export type AdminReportPlayDimensionRow = { approx_house_gross_minor: number; }; -export type AdminReportRebateCommissionRow = { - play_code: string; - total_rebate_minor: number; - order_count: number; - ticket_item_count: number; -}; - export type AdminReportListData = { items: T[]; meta: {