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.
This commit is contained in:
@@ -7,7 +7,6 @@ import type {
|
|||||||
AdminReportPlayDimensionRow,
|
AdminReportPlayDimensionRow,
|
||||||
AdminReportPlayerWinLossRow,
|
AdminReportPlayerWinLossRow,
|
||||||
AdminReportQueryParams,
|
AdminReportQueryParams,
|
||||||
AdminReportRebateCommissionRow,
|
|
||||||
} from "@/types/api/admin-reports";
|
} from "@/types/api/admin-reports";
|
||||||
|
|
||||||
const A = `/admin`;
|
const A = `/admin`;
|
||||||
@@ -35,11 +34,3 @@ export async function getAdminReportPlayDimension(
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminReportRebateCommission(
|
|
||||||
params: AdminReportQueryParams,
|
|
||||||
): Promise<AdminReportListData<AdminReportRebateCommissionRow>> {
|
|
||||||
return adminRequest.get<AdminReportListData<AdminReportRebateCommissionRow>>(`${A}/reports/rebate-commission`, {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export {
|
|||||||
getAdminReportDailyProfit,
|
getAdminReportDailyProfit,
|
||||||
getAdminReportPlayDimension,
|
getAdminReportPlayDimension,
|
||||||
getAdminReportPlayerWinLoss,
|
getAdminReportPlayerWinLoss,
|
||||||
getAdminReportRebateCommission,
|
|
||||||
} from "@/api/admin-reports";
|
} from "@/api/admin-reports";
|
||||||
export {
|
export {
|
||||||
downloadAdminReportJob,
|
downloadAdminReportJob,
|
||||||
|
|||||||
@@ -190,6 +190,15 @@
|
|||||||
@apply text-sm text-muted-foreground;
|
@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-head"],
|
||||||
[data-slot="table-cell"] {
|
[data-slot="table-cell"] {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
57
src/components/admin/admin-money-display.tsx
Normal file
57
src/components/admin/admin-money-display.tsx
Normal file
@@ -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 (
|
||||||
|
<Component
|
||||||
|
title={resolvedTitle}
|
||||||
|
className={cn(
|
||||||
|
resolvedValue
|
||||||
|
? adminMoneyDisplayClass(resolvedValue, { size, emphasize, className })
|
||||||
|
: cn(
|
||||||
|
"min-w-0 whitespace-normal break-words [overflow-wrap:anywhere] tabular-nums leading-tight tracking-tight",
|
||||||
|
emphasize ? "font-semibold" : "font-medium",
|
||||||
|
className,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/components/admin/admin-table-money.tsx
Normal file
46
src/components/admin/admin-table-money.tsx
Normal file
@@ -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 (
|
||||||
|
<AdminMoneyDisplay
|
||||||
|
as="span"
|
||||||
|
value={text}
|
||||||
|
size={size}
|
||||||
|
emphasize={emphasize}
|
||||||
|
className={cn("inline-block max-w-full", className)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</AdminMoneyDisplay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cn("admin-money-value inline-block max-w-full", className)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TableCell 金额列常用 className */
|
||||||
|
export function adminMoneyCellClassName(className?: string): string {
|
||||||
|
return cn("admin-money-cell min-w-0 whitespace-normal align-top leading-snug", className);
|
||||||
|
}
|
||||||
44
src/hooks/use-admin-permission.ts
Normal file
44
src/hooks/use-admin-permission.ts
Normal file
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -190,6 +190,49 @@
|
|||||||
"bills": "Settlement"
|
"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": {
|
"agent": {
|
||||||
"title": "Operations overview",
|
"title": "Operations overview",
|
||||||
"subtitle": "{{name}} · your line",
|
"subtitle": "{{name}} · your line",
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
"hot_number_risk_report": "Hot number risk",
|
"hot_number_risk_report": "Hot number risk",
|
||||||
"play_dimension_report": "Play dimension",
|
"play_dimension_report": "Play dimension",
|
||||||
"sold_out_number_report": "Sold-out numbers",
|
"sold_out_number_report": "Sold-out numbers",
|
||||||
"rebate_commission_report": "Rebate / commission",
|
|
||||||
"audit_operation_report": "Admin audit"
|
"audit_operation_report": "Admin audit"
|
||||||
},
|
},
|
||||||
"empty": "No matching reports",
|
"empty": "No matching reports",
|
||||||
@@ -173,16 +172,6 @@
|
|||||||
"extra": "Usage",
|
"extra": "Usage",
|
||||||
"time": "Version"
|
"time": "Version"
|
||||||
},
|
},
|
||||||
"rebateCommission": {
|
|
||||||
"primary": "Play",
|
|
||||||
"secondary": "Orders",
|
|
||||||
"metricA": "Rebate",
|
|
||||||
"metricB": "Ticket items",
|
|
||||||
"metricC": "Commission",
|
|
||||||
"status": "Rule hit",
|
|
||||||
"extra": "Note",
|
|
||||||
"time": "Time"
|
|
||||||
},
|
|
||||||
"adminAudit": {
|
"adminAudit": {
|
||||||
"primary": "Log ID",
|
"primary": "Log ID",
|
||||||
"secondary": "Operator type",
|
"secondary": "Operator type",
|
||||||
@@ -303,10 +292,6 @@
|
|||||||
"title": "Sold-out number report",
|
"title": "Sold-out number report",
|
||||||
"summary": "Review sold-out numbers, sold-out time, and risk lock state by draw."
|
"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": {
|
"admin_audit": {
|
||||||
"title": "Admin operation audit report",
|
"title": "Admin operation audit report",
|
||||||
"summary": "Admin actions by operator and record time; defaults to the last 30 days."
|
"summary": "Admin actions by operator and record time; defaults to the last 30 days."
|
||||||
|
|||||||
@@ -187,6 +187,49 @@
|
|||||||
"bills": "सेटलमेन्ट"
|
"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": {
|
"agent": {
|
||||||
"title": "सञ्चालन सारांश",
|
"title": "सञ्चालन सारांश",
|
||||||
"subtitle": "{{name}} · यो लाइन",
|
"subtitle": "{{name}} · यो लाइन",
|
||||||
|
|||||||
@@ -61,7 +61,6 @@
|
|||||||
"hot_number_risk_report": "लोकप्रिय नम्बर जोखिम",
|
"hot_number_risk_report": "लोकप्रिय नम्बर जोखिम",
|
||||||
"play_dimension_report": "प्ले आयाम",
|
"play_dimension_report": "प्ले आयाम",
|
||||||
"sold_out_number_report": "बिक्री समाप्त नम्बर",
|
"sold_out_number_report": "बिक्री समाप्त नम्बर",
|
||||||
"rebate_commission_report": "रिबेट / कमिसन",
|
|
||||||
"audit_operation_report": "प्रशासक अडिट"
|
"audit_operation_report": "प्रशासक अडिट"
|
||||||
},
|
},
|
||||||
"empty": "मिल्ने रिपोर्ट छैन",
|
"empty": "मिल्ने रिपोर्ट छैन",
|
||||||
@@ -172,16 +171,6 @@
|
|||||||
"extra": "प्रयोग",
|
"extra": "प्रयोग",
|
||||||
"time": "संस्करण"
|
"time": "संस्करण"
|
||||||
},
|
},
|
||||||
"rebateCommission": {
|
|
||||||
"primary": "खेल",
|
|
||||||
"secondary": "अर्डर",
|
|
||||||
"metricA": "रिबेट",
|
|
||||||
"metricB": "टिकट आइटम",
|
|
||||||
"metricC": "कमिसन",
|
|
||||||
"status": "नियम मिलान",
|
|
||||||
"extra": "टिप्पणी",
|
|
||||||
"time": "समय"
|
|
||||||
},
|
|
||||||
"adminAudit": {
|
"adminAudit": {
|
||||||
"primary": "लग ID",
|
"primary": "लग ID",
|
||||||
"secondary": "अपरेटर प्रकार",
|
"secondary": "अपरेटर प्रकार",
|
||||||
@@ -300,10 +289,6 @@
|
|||||||
"title": "सोल्ड-आउट नम्बर रिपोर्ट",
|
"title": "सोल्ड-आउट नम्बर रिपोर्ट",
|
||||||
"summary": "ड्र अनुसार सोल्ड-आउट नम्बर, समय र जोखिम लक अवस्था हेर्नुहोस्।"
|
"summary": "ड्र अनुसार सोल्ड-आउट नम्बर, समय र जोखिम लक अवस्था हेर्नुहोस्।"
|
||||||
},
|
},
|
||||||
"rebate_commission": {
|
|
||||||
"title": "कमिसन / रिबेट रिपोर्ट",
|
|
||||||
"summary": "वालेट-मोड तत्काल रिबेट, व्यावसायिक मितिअनुसार; मिति नचयेमा पछिल्लो ३० दिन।"
|
|
||||||
},
|
|
||||||
"admin_audit": {
|
"admin_audit": {
|
||||||
"title": "एडमिन अपरेशन अडिट रिपोर्ट",
|
"title": "एडमिन अपरेशन अडिट रिपोर्ट",
|
||||||
"summary": "अपरेटर र रेकर्ड समय अनुसार; मिति नचयेमा पछिल्लो ३० दिन।"
|
"summary": "अपरेटर र रेकर्ड समय अनुसार; मिति नचयेमा पछिल्लो ३० दिन।"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"deleteSuccess": "已删除 {{name}}",
|
"deleteSuccess": "已删除 {{name}}",
|
||||||
"deleteFailed": "删除失败",
|
"deleteFailed": "删除失败",
|
||||||
"roleListTitle": "平台角色管理",
|
"roleListTitle": "平台角色管理",
|
||||||
"roleListHint": "可新增自定义角色并配置权限;内置角色(超级管理员、站点管理员、代理)不可删除。",
|
"roleListHint": "可新增自定义角色并配置权限;内置角色(超级管理员、站点管理员、站点财务、站点客服、代理)不可删除。",
|
||||||
"createRole": "新增平台角色",
|
"createRole": "新增平台角色",
|
||||||
"roleCreateSuccess": "已创建角色 {{name}}",
|
"roleCreateSuccess": "已创建角色 {{name}}",
|
||||||
"roleUpdateSuccess": "已更新角色 {{name}}",
|
"roleUpdateSuccess": "已更新角色 {{name}}",
|
||||||
|
|||||||
@@ -190,6 +190,49 @@
|
|||||||
"bills": "结算中心"
|
"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": {
|
"agent": {
|
||||||
"title": "经营概览",
|
"title": "经营概览",
|
||||||
"subtitle": "{{name}} · 本线路",
|
"subtitle": "{{name}} · 本线路",
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
"hot_number_risk_report": "热门号码风险",
|
"hot_number_risk_report": "热门号码风险",
|
||||||
"play_dimension_report": "玩法维度",
|
"play_dimension_report": "玩法维度",
|
||||||
"sold_out_number_report": "售罄号码",
|
"sold_out_number_report": "售罄号码",
|
||||||
"rebate_commission_report": "佣金/回水",
|
|
||||||
"audit_operation_report": "后台操作审计"
|
"audit_operation_report": "后台操作审计"
|
||||||
},
|
},
|
||||||
"empty": "没有匹配的报表",
|
"empty": "没有匹配的报表",
|
||||||
@@ -173,16 +172,6 @@
|
|||||||
"extra": "使用率",
|
"extra": "使用率",
|
||||||
"time": "版本"
|
"time": "版本"
|
||||||
},
|
},
|
||||||
"rebateCommission": {
|
|
||||||
"primary": "玩法",
|
|
||||||
"secondary": "订单数",
|
|
||||||
"metricA": "回水",
|
|
||||||
"metricB": "注单数",
|
|
||||||
"metricC": "佣金",
|
|
||||||
"status": "配置命中",
|
|
||||||
"extra": "备注",
|
|
||||||
"time": "时间"
|
|
||||||
},
|
|
||||||
"adminAudit": {
|
"adminAudit": {
|
||||||
"primary": "日志 ID",
|
"primary": "日志 ID",
|
||||||
"secondary": "操作者类型",
|
"secondary": "操作者类型",
|
||||||
@@ -303,10 +292,6 @@
|
|||||||
"title": "售罄号码报表",
|
"title": "售罄号码报表",
|
||||||
"summary": "查看单期已售罄号码、售罄时间和风险封锁情况。"
|
"summary": "查看单期已售罄号码、售罄时间和风险封锁情况。"
|
||||||
},
|
},
|
||||||
"rebate_commission": {
|
|
||||||
"title": "佣金/回水报表",
|
|
||||||
"summary": "钱包盘下注立减回水,按业务日汇总;未选日期默认近 30 天。非信用占成账期。"
|
|
||||||
},
|
|
||||||
"admin_audit": {
|
"admin_audit": {
|
||||||
"title": "后台操作审计报表",
|
"title": "后台操作审计报表",
|
||||||
"summary": "按操作人与记录创建时间筛选;未选日期默认近 30 天。"
|
"summary": "按操作人与记录创建时间筛选;未选日期默认近 30 天。"
|
||||||
|
|||||||
65
src/lib/admin-money-display.ts
Normal file
65
src/lib/admin-money-display.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type AdminMoneyDisplaySize = "sm" | "md" | "lg" | "xl";
|
||||||
|
|
||||||
|
const SIZE_TIERS: Record<AdminMoneyDisplaySize, readonly string[]> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
19
src/lib/admin-permission-codes.ts
Normal file
19
src/lib/admin-permission-codes.ts
Normal file
@@ -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;
|
||||||
@@ -9,3 +9,25 @@ export function adminHasAnyPermission(
|
|||||||
}
|
}
|
||||||
return required.some((slug) => set.includes(slug));
|
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 ?? [];
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,29 @@ export function isAgentOperator(profile: AdminProfile | null | undefined): boole
|
|||||||
return profile?.agent != null && profile.is_super_admin !== true;
|
return profile?.agent != null && profile.is_super_admin !== true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 平台站点管理员(绑定 site_admin 角色、无代理节点)。 */
|
/** 任意接入站点平台账号(site_admin / site_finance / site_cs)。 */
|
||||||
export function isSiteAdminOperator(profile: AdminProfile | null | undefined): boolean {
|
export function isSiteOperator(profile: AdminProfile | null | undefined): boolean {
|
||||||
return profile?.site != null && profile.is_super_admin !== true;
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
/** 盈亏 / 输赢:负红、正绿、零灰 */
|
/** 盈亏 / 输赢:负红、正绿、零灰 */
|
||||||
@@ -49,8 +50,25 @@ export function SignedMoney({
|
|||||||
emphasize?: boolean;
|
emphasize?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
|
const colorClass = signedMoneyClass(amount, emphasize);
|
||||||
|
|
||||||
|
if (typeof children === "string" || typeof children === "number") {
|
||||||
|
const text = String(children);
|
||||||
|
return (
|
||||||
|
<AdminMoneyDisplay
|
||||||
|
as="span"
|
||||||
|
value={text}
|
||||||
|
size="sm"
|
||||||
|
emphasize={emphasize}
|
||||||
|
className={cn(colorClass, className)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</AdminMoneyDisplay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn(signedMoneyClass(amount, emphasize), "tabular-nums", className)}>
|
<span className={cn(colorClass, "tabular-nums", className)}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import type { AdminRoleRow } from "@/types/api/index";
|
|||||||
|
|
||||||
export const PLATFORM_SUPER_ADMIN_SLUG = "super_admin";
|
export const PLATFORM_SUPER_ADMIN_SLUG = "super_admin";
|
||||||
export const PLATFORM_SITE_ADMIN_SLUG = "site_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 const PLATFORM_AGENT_SLUG = "agent";
|
||||||
|
|
||||||
export function isPlatformFixedRole(role: Pick<AdminRoleRow, "slug">): boolean {
|
export function isPlatformFixedRole(role: Pick<AdminRoleRow, "slug">): boolean {
|
||||||
return (
|
return (
|
||||||
role.slug === PLATFORM_SUPER_ADMIN_SLUG
|
role.slug === PLATFORM_SUPER_ADMIN_SLUG
|
||||||
|| role.slug === PLATFORM_SITE_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
|
|| role.slug === PLATFORM_AGENT_SLUG
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export type ReportUiKey =
|
|||||||
| "hot_number_risk"
|
| "hot_number_risk"
|
||||||
| "play_dimension"
|
| "play_dimension"
|
||||||
| "sold_out_number"
|
| "sold_out_number"
|
||||||
| "rebate_commission"
|
|
||||||
| "admin_audit";
|
| "admin_audit";
|
||||||
|
|
||||||
/** Maps UI keys to POST /admin/report-jobs `report_type` */
|
/** Maps UI keys to POST /admin/report-jobs `report_type` */
|
||||||
@@ -30,7 +29,6 @@ export const REPORT_UI_TO_JOB_TYPE: Record<ReportUiKey, string> = {
|
|||||||
hot_number_risk: "hot_number_risk_report",
|
hot_number_risk: "hot_number_risk_report",
|
||||||
play_dimension: "play_dimension_report",
|
play_dimension: "play_dimension_report",
|
||||||
sold_out_number: "sold_out_number_report",
|
sold_out_number: "sold_out_number_report",
|
||||||
rebate_commission: "rebate_commission_report",
|
|
||||||
admin_audit: "audit_operation_report",
|
admin_audit: "audit_operation_report",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { percentValueToUi } from "@/lib/admin-rate-percent";
|
import { percentValueToUi } from "@/lib/admin-rate-percent";
|
||||||
import { isLineRootAgentNode } from "@/lib/agent-profile-caps";
|
import { isLineRootAgentNode } from "@/lib/agent-profile-caps";
|
||||||
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
|
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 { cn } from "@/lib/utils";
|
||||||
import type { AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent";
|
import type { AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent";
|
||||||
|
|
||||||
@@ -369,10 +371,11 @@ function OverviewTab({
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
<div className="grid min-w-0 grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={t("profile.totalShareRate", { defaultValue: "占成比例" })}
|
label={t("profile.totalShareRate", { defaultValue: "占成比例" })}
|
||||||
value={profileLoading ? "…" : `${profile?.total_share_rate ?? 0}%`}
|
value={profileLoading ? "…" : `${profile?.total_share_rate ?? 0}%`}
|
||||||
|
money={false}
|
||||||
subtitle={
|
subtitle={
|
||||||
parentRelativeShare
|
parentRelativeShare
|
||||||
? t("profile.relativeShareRateValue", {
|
? t("profile.relativeShareRateValue", {
|
||||||
@@ -398,16 +401,18 @@ function OverviewTab({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
<div className="grid min-w-0 grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
|
label={t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
|
||||||
value={profileLoading ? "…" : `${rebateCap ?? "0"}%`}
|
value={profileLoading ? "…" : `${rebateCap ?? "0"}%`}
|
||||||
|
money={false}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
|
label={t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
|
||||||
value={
|
value={
|
||||||
profileLoading ? "…" : `${percentValueToUi(profile?.default_player_rebate ?? 0)}%`
|
profileLoading ? "…" : `${percentValueToUi(profile?.default_player_rebate ?? 0)}%`
|
||||||
}
|
}
|
||||||
|
money={false}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={t("profile.riskTags", { defaultValue: "风控标签" })}
|
label={t("profile.riskTags", { defaultValue: "风控标签" })}
|
||||||
@@ -418,6 +423,7 @@ function OverviewTab({
|
|||||||
? profile!.risk_tags!.join(", ")
|
? profile!.risk_tags!.join(", ")
|
||||||
: t("common:states.none", { defaultValue: "无" })
|
: t("common:states.none", { defaultValue: "无" })
|
||||||
}
|
}
|
||||||
|
money={false}
|
||||||
/>
|
/>
|
||||||
<CapabilityMetric
|
<CapabilityMetric
|
||||||
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
|
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
|
||||||
@@ -567,11 +573,11 @@ function DownlineTable({
|
|||||||
</div>
|
</div>
|
||||||
) : "—"}
|
) : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right tabular-nums text-xs">
|
<TableCell className={adminMoneyCellClassName("text-right text-xs")}>
|
||||||
{summary ? formatCredit(summary.credit_limit) : "—"}
|
{summary ? <AdminTableMoney>{formatCredit(summary.credit_limit)}</AdminTableMoney> : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right tabular-nums text-xs">
|
<TableCell className={adminMoneyCellClassName("text-right text-xs")}>
|
||||||
{summary ? formatCredit(summary.allocated_credit) : "—"}
|
{summary ? <AdminTableMoney>{formatCredit(summary.allocated_credit)}</AdminTableMoney> : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center tabular-nums text-xs">
|
<TableCell className="text-center tabular-nums text-xs">
|
||||||
{childCountById.get(child.id) ?? 0}
|
{childCountById.get(child.id) ?? 0}
|
||||||
@@ -625,31 +631,45 @@ function MetricCard({
|
|||||||
subtitle,
|
subtitle,
|
||||||
accent = false,
|
accent = false,
|
||||||
highlight = false,
|
highlight = false,
|
||||||
|
money = true,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
accent?: boolean;
|
accent?: boolean;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
|
/** 金额类指标:自适应字号 + 换行 */
|
||||||
|
money?: boolean;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl border bg-card px-4 py-4 shadow-sm transition-colors",
|
"min-w-0 overflow-visible rounded-xl border bg-card px-4 py-4 shadow-sm transition-colors",
|
||||||
highlight && "border-primary/25 bg-primary/[0.04]",
|
highlight && "border-primary/25 bg-primary/[0.04]",
|
||||||
accent && !highlight && "border-border/70",
|
accent && !highlight && "border-border/70",
|
||||||
!accent && !highlight && "border-border/70",
|
!accent && !highlight && "border-border/70",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||||
<p
|
{money ? (
|
||||||
className={cn(
|
<AdminMoneyDisplay
|
||||||
"mt-1.5 text-2xl font-semibold tabular-nums tracking-tight",
|
as="p"
|
||||||
highlight ? "text-primary" : "text-foreground",
|
value={value}
|
||||||
)}
|
size="lg"
|
||||||
>
|
className={cn("mt-1.5", highlight ? "text-primary" : "text-foreground")}
|
||||||
{value}
|
>
|
||||||
</p>
|
{value}
|
||||||
|
</AdminMoneyDisplay>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"mt-1.5 text-2xl font-semibold tabular-nums tracking-tight",
|
||||||
|
highlight ? "text-primary" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{subtitle ? <p className="mt-1 text-xs text-muted-foreground">{subtitle}</p> : null}
|
{subtitle ? <p className="mt-1 text-xs text-muted-foreground">{subtitle}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type { AgentParentCaps } from "@/types/api/admin-agent";
|
|||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
import { AdminNumericStepper } from "@/components/admin/admin-numeric-stepper";
|
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 { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||||
import {
|
import {
|
||||||
AGENT_PERCENT_HARD_MAX,
|
AGENT_PERCENT_HARD_MAX,
|
||||||
@@ -411,14 +412,14 @@ function ReadOnlyScalar({
|
|||||||
<div
|
<div
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 items-center justify-center rounded-md border border-border/80 bg-muted/35 px-3 text-sm font-semibold tabular-nums text-foreground shadow-xs",
|
"flex min-h-10 min-w-0 items-center justify-center rounded-md border border-border/80 bg-muted/35 px-3 py-2 text-center shadow-xs",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>
|
<AdminMoneyDisplay as="span" value={value} size="sm" emphasize className="text-foreground">
|
||||||
{value}
|
{value}
|
||||||
{suffix ? <span className="ml-0.5 font-medium text-foreground/80">{suffix}</span> : null}
|
{suffix ? <span className="ml-0.5 font-medium text-foreground/80">{suffix}</span> : null}
|
||||||
</span>
|
</AdminMoneyDisplay>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { adminWeekdayKeyForDate, formatAdminBusinessDateIso, formatAdminCalendar
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
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 { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -207,9 +208,14 @@ export function AgentDashboardConsole(): ReactElement {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="break-all text-2xl font-semibold tabular-nums leading-tight">
|
<AdminMoneyDisplay
|
||||||
|
as="p"
|
||||||
|
value={formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
||||||
|
size="xl"
|
||||||
|
className="text-foreground"
|
||||||
|
>
|
||||||
{formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
{formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
||||||
</p>
|
</AdminMoneyDisplay>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{t("agent.creditAvailable", {
|
{t("agent.creditAvailable", {
|
||||||
amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency),
|
amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency),
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
import type { ReactElement } from "react";
|
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 { AgentDashboardConsole } from "@/modules/dashboard/agent-dashboard-console";
|
||||||
import { DashboardConsole } from "@/modules/dashboard/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 { SiteDashboardConsole } from "@/modules/dashboard/site-dashboard-console";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
|
||||||
/** 超管/平台账号走全站仪表盘;站点管理员走站点仪表盘;代理经营账号走代理仪表盘。 */
|
/** 超管/平台账号走全站仪表盘;站点运营账号走站点仪表盘;代理经营账号走代理仪表盘。 */
|
||||||
export function DashboardPageClient(): ReactElement {
|
export function DashboardPageClient(): ReactElement {
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
|
|
||||||
@@ -16,7 +18,15 @@ export function DashboardPageClient(): ReactElement {
|
|||||||
return <AgentDashboardConsole />;
|
return <AgentDashboardConsole />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSiteAdminOperator(profile)) {
|
if (isSiteFinanceOperator(profile)) {
|
||||||
|
return <SiteFinanceDashboardConsole />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSiteCsOperator(profile)) {
|
||||||
|
return <SiteCsDashboardConsole />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSiteOperator(profile)) {
|
||||||
return <SiteDashboardConsole />;
|
return <SiteDashboardConsole />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
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 { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
} from "@/lib/money";
|
} from "@/lib/money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SignedMoney, signedMoneyClass } from "@/lib/admin-signed-money";
|
import { SignedMoney, signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
|
import { adminMoneyDisplayClass } from "@/lib/admin-money-display";
|
||||||
import {
|
import {
|
||||||
buildBatchProgressConfig,
|
buildBatchProgressConfig,
|
||||||
buildFinanceStructureConfig,
|
buildFinanceStructureConfig,
|
||||||
@@ -68,7 +70,7 @@ type DashboardFinanceMetricCell = {
|
|||||||
emphasize: boolean;
|
emphasize: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** KPI 卡片底部三列:仅数字(币种见卡片主值),过长时省略号 + hover 看全称 */
|
/** KPI 卡片底部三列:仅数字(币种见卡片主值),过长时缩小字号 + 换行 */
|
||||||
function formatDashboardMetricAmount(
|
function formatDashboardMetricAmount(
|
||||||
minor: number,
|
minor: number,
|
||||||
currencyCode: string | null,
|
currencyCode: string | null,
|
||||||
@@ -115,7 +117,8 @@ function DashboardFinanceMetricCells({
|
|||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-1 truncate text-center text-[10px] font-bold tabular-nums leading-tight",
|
"mt-1 text-center font-bold tabular-nums leading-tight break-all",
|
||||||
|
adminMoneyDisplayClass(display, { size: "sm", emphasize: true }),
|
||||||
cell.emphasize ? "text-foreground" : "text-muted-foreground",
|
cell.emphasize ? "text-foreground" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
title={title}
|
title={title}
|
||||||
@@ -241,9 +244,14 @@ export function DashboardScopeMetric({
|
|||||||
return (
|
return (
|
||||||
<div className="rounded-lg border bg-muted/30 px-3 py-2.5">
|
<div className="rounded-lg border bg-muted/30 px-3 py-2.5">
|
||||||
<p className="text-xs text-muted-foreground">{label}</p>
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
<p className="mt-1 break-all text-base font-semibold tabular-nums leading-tight text-foreground">
|
<AdminMoneyDisplay
|
||||||
|
as="p"
|
||||||
|
value={value}
|
||||||
|
size="md"
|
||||||
|
className="mt-1 text-foreground"
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</AdminMoneyDisplay>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -303,8 +311,11 @@ export function DashboardKpiCard({
|
|||||||
<p
|
<p
|
||||||
title={valueTitle}
|
title={valueTitle}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 break-words text-base font-bold tabular-nums leading-tight tracking-tight sm:text-lg",
|
"mt-2 text-foreground",
|
||||||
resolvedValueClassName ?? "text-foreground",
|
resolvedValueClassName,
|
||||||
|
typeof resolvedValue === "string"
|
||||||
|
? adminMoneyDisplayClass(resolvedValue, { size: "lg", emphasize: true })
|
||||||
|
: "min-w-0 break-all text-base font-bold tabular-nums leading-tight tracking-tight sm:text-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{resolvedValue}
|
{resolvedValue}
|
||||||
@@ -418,9 +429,21 @@ export function StatCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
<p className="text-sm font-medium text-muted-foreground">{label}</p>
|
<p className="text-sm font-medium text-muted-foreground">{label}</p>
|
||||||
<p className="mt-1 text-2xl font-bold tabular-nums tracking-tight text-foreground">
|
{typeof value === "string" || typeof value === "number" ? (
|
||||||
{value}
|
<AdminMoneyDisplay
|
||||||
</p>
|
as="p"
|
||||||
|
value={String(value)}
|
||||||
|
size="xl"
|
||||||
|
emphasize
|
||||||
|
className="mt-1 text-foreground"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</AdminMoneyDisplay>
|
||||||
|
) : (
|
||||||
|
<p className="mt-1 text-2xl font-bold tabular-nums tracking-tight text-foreground">
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{deltaLabel ? (
|
{deltaLabel ? (
|
||||||
<p className="mt-1 text-xs font-medium tabular-nums">{deltaLabel}</p>
|
<p className="mt-1 text-xs font-medium tabular-nums">{deltaLabel}</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -557,6 +580,16 @@ export function DashboardPanelCard({
|
|||||||
<p className="text-xs font-medium text-muted-foreground">{title}</p>
|
<p className="text-xs font-medium text-muted-foreground">{title}</p>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton className="mt-2 h-8 w-24 rounded-md" />
|
<Skeleton className="mt-2 h-8 w-24 rounded-md" />
|
||||||
|
) : typeof value === "string" || typeof value === "number" ? (
|
||||||
|
<AdminMoneyDisplay
|
||||||
|
as="p"
|
||||||
|
value={String(value)}
|
||||||
|
size="xl"
|
||||||
|
emphasize
|
||||||
|
className="mt-1 text-foreground"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</AdminMoneyDisplay>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-1 text-2xl font-bold tabular-nums leading-none tracking-tight text-foreground">
|
<p className="mt-1 text-2xl font-bold tabular-nums leading-none tracking-tight text-foreground">
|
||||||
{value}
|
{value}
|
||||||
|
|||||||
196
src/modules/dashboard/site-cs-dashboard-console.tsx
Normal file
196
src/modules/dashboard/site-cs-dashboard-console.tsx
Normal file
@@ -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<string | null>(null);
|
||||||
|
const [apiWarnings, setApiWarnings] = useState<AdminDashboardWarning[]>([]);
|
||||||
|
const [overview, setOverview] = useState<AdminDashboardSiteCsOverview | null>(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 (
|
||||||
|
<div className="flex min-w-0 w-full max-w-none flex-col gap-5">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="admin-list-title">{t("cs.title")}</h1>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
{site
|
||||||
|
? t("cs.subtitle", { name: site.name || site.code })
|
||||||
|
: t("cs.subtitleFallback")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8"
|
||||||
|
disabled={loading || refreshing}
|
||||||
|
onClick={() => void load(true)}
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn("size-3.5", refreshing && "animate-spin")} />
|
||||||
|
{t("actions.refresh", { ns: "common" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||||
|
<AlertTitle>{t("notice")}</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!loading && apiWarnings.length > 0 ? (
|
||||||
|
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||||
|
<AlertTitle>{t("notice")}</AlertTitle>
|
||||||
|
<AlertDescription>{apiWarnings.map((w) => w.message).join(" ")}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-24 rounded-xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : overview ? (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("cs.playerCount")}
|
||||||
|
value={overview.player_count}
|
||||||
|
icon={<Users className="size-4" />}
|
||||||
|
hint={t("cs.playerCountHint")}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("cs.ticketsToday")}
|
||||||
|
value={overview.ticket_order_count_today}
|
||||||
|
icon={<ClipboardList className="size-4" />}
|
||||||
|
hint={activityHint}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("cs.activePlayersToday")}
|
||||||
|
value={overview.active_player_count_today}
|
||||||
|
icon={<Search className="size-4" />}
|
||||||
|
hint={t("cs.activePlayersHint")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("cs.workspaceTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-3 sm:grid-cols-3">
|
||||||
|
{quickLinks.map((link) => {
|
||||||
|
const Icon = link.icon;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="flex flex-col gap-2 rounded-xl border bg-muted/20 px-4 py-4 transition-colors hover:bg-muted/40"
|
||||||
|
>
|
||||||
|
<Icon className="size-5 text-primary" aria-hidden />
|
||||||
|
<span className="text-sm font-medium">{link.label}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{t("cs.openModule")}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("cs.scopeTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<DashboardScopeMetric label={t("cs.playerCount")} value={String(overview.player_count)} />
|
||||||
|
<DashboardScopeMetric
|
||||||
|
label={t("cs.ticketsToday")}
|
||||||
|
value={String(overview.ticket_order_count_today)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<AdminNoResourceState className="py-12 text-sm text-muted-foreground">
|
||||||
|
{t("cs.overviewEmpty")}
|
||||||
|
</AdminNoResourceState>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
244
src/modules/dashboard/site-finance-dashboard-console.tsx
Normal file
244
src/modules/dashboard/site-finance-dashboard-console.tsx
Normal file
@@ -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<string | null>(null);
|
||||||
|
const [apiWarnings, setApiWarnings] = useState<AdminDashboardWarning[]>([]);
|
||||||
|
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
|
||||||
|
const [drawId, setDrawId] = useState<number | null>(null);
|
||||||
|
const [overview, setOverview] = useState<AdminDashboardSiteFinanceOverview | null>(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 (
|
||||||
|
<div className="flex min-w-0 w-full max-w-none flex-col gap-5">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="admin-list-title">{t("finance.title")}</h1>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
{site
|
||||||
|
? t("finance.subtitle", { name: site.name || site.code })
|
||||||
|
: t("finance.subtitleFallback")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8"
|
||||||
|
disabled={loading || refreshing}
|
||||||
|
onClick={() => void load(true)}
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn("size-3.5", refreshing && "animate-spin")} />
|
||||||
|
{t("actions.refresh", { ns: "common" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||||
|
<AlertTitle>{t("notice")}</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!loading && apiWarnings.length > 0 ? (
|
||||||
|
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||||
|
<AlertTitle>{t("notice")}</AlertTitle>
|
||||||
|
<AlertDescription>{apiWarnings.map((w) => w.message).join(" ")}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-24 rounded-xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : overview ? (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("finance.abnormalTransfers")}
|
||||||
|
value={abnormalCount ?? "—"}
|
||||||
|
icon={<AlertTriangle className="size-4" />}
|
||||||
|
hint={t("abnormalTransferScope")}
|
||||||
|
accent={(abnormalCount ?? 0) > 0 ? "destructive" : "muted"}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("finance.pendingConfirmBills")}
|
||||||
|
value={overview.pending_confirm_bill_count}
|
||||||
|
icon={<ClipboardList className="size-4" />}
|
||||||
|
hint={t("finance.pendingConfirmHint")}
|
||||||
|
accent={overview.pending_confirm_bill_count > 0 ? "primary" : "muted"}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("finance.payableBills")}
|
||||||
|
value={overview.payable_bill_count}
|
||||||
|
icon={<Scale className="size-4" />}
|
||||||
|
hint={t("finance.payableUnpaid", {
|
||||||
|
amount: formatDashboardMoneyMinor(overview.payable_unpaid_minor, displayCurrency),
|
||||||
|
})}
|
||||||
|
accent={overview.payable_bill_count > 0 ? "destructive" : "muted"}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("finance.walletPlayers")}
|
||||||
|
value={overview.wallet_player_count}
|
||||||
|
icon={<Users className="size-4" />}
|
||||||
|
hint={t("finance.creditPlayersHint", { count: overview.credit_player_count })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("finance.settlementTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<DashboardStatRow
|
||||||
|
label={t("finance.pendingConfirmBills")}
|
||||||
|
value={String(overview.pending_confirm_bill_count)}
|
||||||
|
/>
|
||||||
|
<DashboardStatRow
|
||||||
|
label={t("finance.payableBills")}
|
||||||
|
value={String(overview.payable_bill_count)}
|
||||||
|
/>
|
||||||
|
<DashboardStatRow
|
||||||
|
label={t("finance.payableUnpaidLabel")}
|
||||||
|
value={formatDashboardMoneyMinor(overview.payable_unpaid_minor, displayCurrency)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("finance.reconcileTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<AbnormalTransferPanelFooter
|
||||||
|
total={abnormalCount}
|
||||||
|
walletPermission={walletPermission}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
href="/admin/wallet/transfer-orders?abnormal=1"
|
||||||
|
className={buttonVariants({ variant: "outline", size: "sm", className: "w-full" })}
|
||||||
|
>
|
||||||
|
<Wallet className="size-3.5" />
|
||||||
|
{t("viewTransferOrders")}
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("quickLinksTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-wrap gap-2">
|
||||||
|
{quickLinks.map((link) => (
|
||||||
|
<Link
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className={buttonVariants({ variant: "outline", size: "sm" })}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<AdminNoResourceState className="py-12 text-sm text-muted-foreground">
|
||||||
|
{t("finance.overviewEmpty")}
|
||||||
|
</AdminNoResourceState>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DashboardCurrentDrawCard
|
||||||
|
key={`${hall?.draw_no ?? "empty"}:${loading ? "loading" : "ready"}`}
|
||||||
|
hall={hall}
|
||||||
|
drawId={drawId}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
canDeleteDrawRow,
|
canDeleteDrawRow,
|
||||||
canEditDrawRow,
|
canEditDrawRow,
|
||||||
} from "./draw-list-actions";
|
} from "./draw-list-actions";
|
||||||
|
import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||||
@@ -429,25 +430,30 @@ export function DrawsIndexConsole() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
{canViewFinance ? (
|
{canViewFinance ? (
|
||||||
<>
|
<>
|
||||||
<TableCell className="text-center text-xs tabular-nums">
|
<TableCell className={adminMoneyCellClassName("text-center text-xs")}>
|
||||||
{row.total_bet_minor != null
|
{row.total_bet_minor != null ? (
|
||||||
? formatAdminMinorUnits(row.total_bet_minor, defaultCurrency)
|
<AdminTableMoney>
|
||||||
: "—"}
|
{formatAdminMinorUnits(row.total_bet_minor, defaultCurrency)}
|
||||||
|
</AdminTableMoney>
|
||||||
|
) : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-xs tabular-nums">
|
<TableCell className={adminMoneyCellClassName("text-center text-xs")}>
|
||||||
{row.total_payout_minor != null
|
{row.total_payout_minor != null ? (
|
||||||
? formatAdminMinorUnits(row.total_payout_minor, defaultCurrency)
|
<AdminTableMoney>
|
||||||
: "—"}
|
{formatAdminMinorUnits(row.total_payout_minor, defaultCurrency)}
|
||||||
|
</AdminTableMoney>
|
||||||
|
) : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className={cn(
|
className={adminMoneyCellClassName(
|
||||||
"text-center text-xs tabular-nums",
|
cn("text-center text-xs", signedMoneyClass(row.profit_loss_minor ?? 0, true)),
|
||||||
signedMoneyClass(row.profit_loss_minor ?? 0, true),
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{row.profit_loss_minor != null
|
{row.profit_loss_minor != null ? (
|
||||||
? formatAdminMinorUnits(row.profit_loss_minor, defaultCurrency)
|
<AdminTableMoney>
|
||||||
: "—"}
|
{formatAdminMinorUnits(row.profit_loss_minor, defaultCurrency)}
|
||||||
|
</AdminTableMoney>
|
||||||
|
) : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -343,17 +343,21 @@ export function ReconcileConsole(): React.ReactElement {
|
|||||||
<CardTitle className="admin-list-title">{t("createTitle")}</CardTitle>
|
<CardTitle className="admin-list-title">{t("createTitle")}</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground">{t("createHint")}</p>
|
<p className="text-sm text-muted-foreground">{t("createHint")}</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="admin-list-content pt-4">
|
<CardContent className="admin-list-content">
|
||||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
<div className="admin-list-toolbar">
|
||||||
<div className="grid gap-4">
|
<div className="admin-list-field">
|
||||||
<div className="grid gap-1.5">
|
<span className="text-sm font-medium leading-none sm:shrink-0">{t("reconcileType")}</span>
|
||||||
<Label htmlFor="rc-type">{t("reconcileType")}</Label>
|
<span className="inline-flex h-8 min-h-8 min-w-0 items-center rounded-md border border-border/60 bg-muted/30 px-2.5 text-sm text-foreground">
|
||||||
<Input id="rc-type" value={t("reconcileTypeFixed")} readOnly className="bg-muted/30" />
|
{t("reconcileTypeFixed")}
|
||||||
</div>
|
</span>
|
||||||
<div className="grid gap-1.5">
|
</div>
|
||||||
|
<div className="admin-list-field">
|
||||||
|
<Label htmlFor="rc-date-range" className="sm:shrink-0">
|
||||||
|
{t("dateRange")}
|
||||||
|
</Label>
|
||||||
|
<div className="min-w-0 w-full sm:w-60">
|
||||||
<AdminDateRangeField
|
<AdminDateRangeField
|
||||||
id="rc-date-range"
|
id="rc-date-range"
|
||||||
label={t("dateRange")}
|
|
||||||
from={dateFrom}
|
from={dateFrom}
|
||||||
to={dateTo}
|
to={dateTo}
|
||||||
onRangeChange={({ from, to }) => {
|
onRangeChange={({ from, to }) => {
|
||||||
@@ -363,102 +367,101 @@ export function ReconcileConsole(): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-list-field min-w-0 flex-1">
|
||||||
<div className="grid gap-4">
|
<Label htmlFor="rc-player-search" className="sm:shrink-0">
|
||||||
<div className="grid gap-1.5">
|
{t("playerSearch")}
|
||||||
<Label htmlFor="rc-player-search">{t("playerSearch")}</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="rc-player-search"
|
id="rc-player-search"
|
||||||
value={playerSearch}
|
className="w-full sm:w-52"
|
||||||
onChange={(e) => setPlayerSearch(e.target.value)}
|
value={playerSearch}
|
||||||
placeholder={t("playerSearchPlaceholder")}
|
onChange={(e) => setPlayerSearch(e.target.value)}
|
||||||
/>
|
placeholder={t("playerSearchPlaceholder")}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
{selectedPlayer ? (
|
<div className="admin-list-actions">
|
||||||
<div className="flex items-center justify-between gap-3 rounded-lg border bg-muted/20 px-3 py-2 text-sm">
|
<Button
|
||||||
<div className="min-w-0 truncate font-medium text-foreground">
|
type="button"
|
||||||
{selectedPlayer.site_player_id}
|
className="w-full sm:w-auto"
|
||||||
{selectedPlayer.nickname ? ` · ${selectedPlayer.nickname}` : ""}
|
disabled={submitting}
|
||||||
{selectedPlayer.username ? ` · ${selectedPlayer.username}` : ""}
|
onClick={() =>
|
||||||
{` · ${selectedPlayer.site_code}`}
|
requestConfirm({
|
||||||
</div>
|
title: t("confirmCreateTitle"),
|
||||||
<Button
|
description: t("confirmCreateDescription", {
|
||||||
type="button"
|
playerHint: selectedPlayer
|
||||||
size="sm"
|
? t("confirmCreatePlayer")
|
||||||
variant="outline"
|
: t("confirmCreateAllPlayers"),
|
||||||
onClick={() => {
|
}),
|
||||||
setSelectedPlayer(null);
|
onConfirm: () => onCreate(),
|
||||||
setPlayerSearch("");
|
})
|
||||||
setPlayerResults([]);
|
}
|
||||||
}}
|
>
|
||||||
>
|
{submitting ? t("submitting") : t("createTask")}
|
||||||
{t("playerClear")}
|
</Button>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{playerSearch.trim() !== "" || playerResults.length > 0 || playerLoading ? (
|
|
||||||
<div className="rounded-lg border bg-background">
|
|
||||||
<div className="max-h-56 overflow-y-auto">
|
|
||||||
{playerLoading ? (
|
|
||||||
<AdminLoadingInline className="py-2" label={t("loadingPlayers")} />
|
|
||||||
) : playerResults.length === 0 ? (
|
|
||||||
<AdminNoResourceState compact className="px-3 py-4" />
|
|
||||||
) : (
|
|
||||||
<div className="divide-y">
|
|
||||||
{playerResults.map((player) => {
|
|
||||||
const active = selectedPlayer?.id === player.id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={player.id}
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"flex w-full px-3 py-2.5 text-left text-sm transition-colors hover:bg-muted/25",
|
|
||||||
active && "bg-muted/30 font-medium",
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedPlayer(player);
|
|
||||||
setPlayerSearch(player.site_player_id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="min-w-0 truncate">
|
|
||||||
{player.site_player_id}
|
|
||||||
{player.nickname ? ` · ${player.nickname}` : ""}
|
|
||||||
{player.username ? ` · ${player.username}` : ""}
|
|
||||||
{` · ${player.site_code}`}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex justify-end">
|
{selectedPlayer ? (
|
||||||
<Button
|
<div className="flex items-center justify-between gap-3 rounded-lg border bg-muted/20 px-3 py-2 text-sm">
|
||||||
type="button"
|
<div className="min-w-0 truncate font-medium text-foreground">
|
||||||
className="w-full sm:w-auto"
|
{selectedPlayer.site_player_id}
|
||||||
disabled={submitting}
|
{selectedPlayer.nickname ? ` · ${selectedPlayer.nickname}` : ""}
|
||||||
onClick={() =>
|
{selectedPlayer.username ? ` · ${selectedPlayer.username}` : ""}
|
||||||
requestConfirm({
|
{` · ${selectedPlayer.site_code}`}
|
||||||
title: t("confirmCreateTitle"),
|
</div>
|
||||||
description: t("confirmCreateDescription", {
|
<Button
|
||||||
playerHint: selectedPlayer
|
type="button"
|
||||||
? t("confirmCreatePlayer")
|
size="sm"
|
||||||
: t("confirmCreateAllPlayers"),
|
variant="outline"
|
||||||
}),
|
onClick={() => {
|
||||||
onConfirm: () => onCreate(),
|
setSelectedPlayer(null);
|
||||||
})
|
setPlayerSearch("");
|
||||||
}
|
setPlayerResults([]);
|
||||||
>
|
}}
|
||||||
{submitting ? t("submitting") : t("createTask")}
|
>
|
||||||
</Button>
|
{t("playerClear")}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{playerSearch.trim() !== "" || playerResults.length > 0 || playerLoading ? (
|
||||||
|
<div className="rounded-lg border bg-background">
|
||||||
|
<div className="max-h-56 overflow-y-auto">
|
||||||
|
{playerLoading ? (
|
||||||
|
<AdminLoadingInline className="py-2" label={t("loadingPlayers")} />
|
||||||
|
) : playerResults.length === 0 ? (
|
||||||
|
<AdminNoResourceState compact className="px-3 py-4" />
|
||||||
|
) : (
|
||||||
|
<div className="divide-y">
|
||||||
|
{playerResults.map((player) => {
|
||||||
|
const active = selectedPlayer?.id === player.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={player.id}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"flex w-full px-3 py-2.5 text-left text-sm transition-colors hover:bg-muted/25",
|
||||||
|
active && "bg-muted/30 font-medium",
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPlayer(player);
|
||||||
|
setPlayerSearch(player.site_player_id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
{player.site_player_id}
|
||||||
|
{player.nickname ? ` · ${player.nickname}` : ""}
|
||||||
|
{player.username ? ` · ${player.username}` : ""}
|
||||||
|
{` · ${player.site_code}`}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
CircleDollarSign,
|
|
||||||
Database,
|
Database,
|
||||||
FileDown,
|
FileDown,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
@@ -31,7 +30,6 @@ import {
|
|||||||
getAdminReportDailyProfit,
|
getAdminReportDailyProfit,
|
||||||
getAdminReportPlayDimension,
|
getAdminReportPlayDimension,
|
||||||
getAdminReportPlayerWinLoss,
|
getAdminReportPlayerWinLoss,
|
||||||
getAdminReportRebateCommission,
|
|
||||||
} from "@/api/admin-reports";
|
} from "@/api/admin-reports";
|
||||||
import {
|
import {
|
||||||
buildReportJobParameters,
|
buildReportJobParameters,
|
||||||
@@ -42,7 +40,13 @@ import { getAdminRiskPoolDetail, getAdminRiskPools } from "@/api/admin-risk";
|
|||||||
import { getAdminUsers } from "@/api/admin-users";
|
import { getAdminUsers } from "@/api/admin-users";
|
||||||
import { getAdminTransferOrders } from "@/api/admin-wallet";
|
import { getAdminTransferOrders } from "@/api/admin-wallet";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
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 { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { adminAgentDisplayLabel } from "@/components/admin/admin-agent-columns";
|
import { adminAgentDisplayLabel } from "@/components/admin/admin-agent-columns";
|
||||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
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 { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { formatAdminInstant } from "@/lib/admin-datetime";
|
import { formatAdminInstant } from "@/lib/admin-datetime";
|
||||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||||
|
import { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
|
||||||
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
@@ -88,7 +93,6 @@ import type {
|
|||||||
AdminReportDailyProfitRow,
|
AdminReportDailyProfitRow,
|
||||||
AdminReportPlayDimensionRow,
|
AdminReportPlayDimensionRow,
|
||||||
AdminReportPlayerWinLossRow,
|
AdminReportPlayerWinLossRow,
|
||||||
AdminReportRebateCommissionRow,
|
|
||||||
} from "@/types/api/admin-reports";
|
} from "@/types/api/admin-reports";
|
||||||
|
|
||||||
export type ReportCategory = "profit" | "wallet" | "risk" | "audit";
|
export type ReportCategory = "profit" | "wallet" | "risk" | "audit";
|
||||||
@@ -107,7 +111,6 @@ type ReportKey =
|
|||||||
| "hot_number_risk"
|
| "hot_number_risk"
|
||||||
| "play_dimension"
|
| "play_dimension"
|
||||||
| "sold_out_number"
|
| "sold_out_number"
|
||||||
| "rebate_commission"
|
|
||||||
| "admin_audit";
|
| "admin_audit";
|
||||||
|
|
||||||
type ReportDefinition = {
|
type ReportDefinition = {
|
||||||
@@ -118,8 +121,22 @@ type ReportDefinition = {
|
|||||||
scope: string;
|
scope: string;
|
||||||
fields: FieldKey[];
|
fields: FieldKey[];
|
||||||
connected: boolean;
|
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 = {
|
type PreviewColumns = {
|
||||||
primary: string;
|
primary: string;
|
||||||
secondary: string;
|
secondary: string;
|
||||||
@@ -159,7 +176,6 @@ type ReportResult =
|
|||||||
| { key: "hot_number_risk"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta | null; raw: AdminRiskPoolShowData }
|
| { 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: "play_dimension"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportPlayDimensionRow[] }
|
||||||
| { key: "sold_out_number"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminRiskPoolRow[] }
|
| { 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[] };
|
| { key: "admin_audit"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminAuditLogRow[] };
|
||||||
|
|
||||||
type StatCard = {
|
type StatCard = {
|
||||||
@@ -182,18 +198,6 @@ type PlayOption = {
|
|||||||
label: string;
|
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 = {
|
const emptyFilters: ReportFilters = {
|
||||||
drawNo: "",
|
drawNo: "",
|
||||||
drawId: null,
|
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<Extract<ReportResult, { key: "rebate_commission" }>, "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 {
|
function metaFromList(meta: { current_page: number; per_page: number; total: number; last_page: number }): ReportMeta {
|
||||||
return {
|
return {
|
||||||
total: meta.total,
|
total: meta.total,
|
||||||
@@ -612,13 +584,6 @@ function defaultSummaryCards(
|
|||||||
{ label: t("preview.stats.currency"), value: t("preview.stats.notQueried") },
|
{ label: t("preview.stats.currency"), value: t("preview.stats.notQueried") },
|
||||||
{ label: t("preview.stats.usage"), 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":
|
case "admin_audit":
|
||||||
return [
|
return [
|
||||||
{ label: t("preview.stats.records"), value: t("preview.stats.notQueried") },
|
{ label: t("preview.stats.records"), value: t("preview.stats.notQueried") },
|
||||||
@@ -641,14 +606,15 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const canViewReports = adminHasAnyPermission(profile?.permissions, [PRD_REPORT_VIEW]);
|
const canViewReports = adminHasAnyPermission(profile?.permissions, [PRD_REPORT_VIEW]);
|
||||||
const canExportReports = adminHasAnyPermission(profile?.permissions, [PRD_REPORT_EXPORT]);
|
const canExportReports = adminHasAnyPermission(profile?.permissions, [PRD_REPORT_EXPORT]);
|
||||||
|
const permissionSlugs = useMemo(() => profile?.permissions ?? [], [profile?.permissions]);
|
||||||
useAdminCurrencyCatalog();
|
useAdminCurrencyCatalog();
|
||||||
useAdminPlayTypeCatalog();
|
useAdminPlayTypeCatalog();
|
||||||
const playCodeLabel = useAdminPlayCodeLabel();
|
const playCodeLabel = useAdminPlayCodeLabel();
|
||||||
const formatTs = useAdminDateTimeFormatter();
|
const formatTs = useAdminDateTimeFormatter();
|
||||||
const filteredReports = useMemo(
|
const filteredReports = useMemo(() => {
|
||||||
() => (initialCategory ? REPORTS.filter((report) => report.category === initialCategory) : REPORTS),
|
const visible = REPORTS.filter((report) => adminHasAnyPermission(permissionSlugs, report.requiredAny));
|
||||||
[initialCategory],
|
return initialCategory ? visible.filter((report) => report.category === initialCategory) : visible;
|
||||||
);
|
}, [initialCategory, permissionSlugs]);
|
||||||
const [selectedKey, setSelectedKey] = useState<ReportKey>(
|
const [selectedKey, setSelectedKey] = useState<ReportKey>(
|
||||||
filteredReports[0]?.key ?? REPORTS[0].key,
|
filteredReports[0]?.key ?? REPORTS[0].key,
|
||||||
);
|
);
|
||||||
@@ -758,17 +724,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
extra: t("preview.columns.soldOut.extra"),
|
extra: t("preview.columns.soldOut.extra"),
|
||||||
time: t("preview.columns.soldOut.time"),
|
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":
|
case "admin_audit":
|
||||||
return {
|
return {
|
||||||
primary: t("preview.columns.adminAudit.primary"),
|
primary: t("preview.columns.adminAudit.primary"),
|
||||||
@@ -1057,22 +1012,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
});
|
});
|
||||||
break;
|
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": {
|
case "admin_audit": {
|
||||||
const operatorId = filters.operatorId ?? parsePositiveInteger(filters.operator);
|
const operatorId = filters.operatorId ?? parsePositiveInteger(filters.operator);
|
||||||
const payload = await getAdminAuditLogs({
|
const payload = await getAdminAuditLogs({
|
||||||
@@ -1134,10 +1073,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
...prev,
|
...prev,
|
||||||
drawNo: drawNoFromUrl || prev.drawNo,
|
drawNo: drawNoFromUrl || prev.drawNo,
|
||||||
}));
|
}));
|
||||||
if (drawNoFromUrl) {
|
if (drawNoFromUrl && filteredReports.some((report) => report.key === "draw_profit")) {
|
||||||
setSelectedKey("draw_profit");
|
setSelectedKey("draw_profit");
|
||||||
}
|
}
|
||||||
}, [drawNoFromUrl]);
|
}, [drawNoFromUrl, filteredReports]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -1563,21 +1502,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.key === "rebate_commission") {
|
|
||||||
return result.raw.map((item) => (
|
|
||||||
<TableRow key={item.play_code}>
|
|
||||||
<TableCell className="font-medium">{playCodeLabel(item.play_code)}</TableCell>
|
|
||||||
<TableCell>{item.order_count}</TableCell>
|
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.total_rebate_minor, displayCurrency)}</TableCell>
|
|
||||||
<TableCell className="text-center">{item.ticket_item_count}</TableCell>
|
|
||||||
<TableCell>-</TableCell>
|
|
||||||
<TableCell>-</TableCell>
|
|
||||||
<TableCell>-</TableCell>
|
|
||||||
<TableCell>-</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.key === "admin_audit") {
|
if (result.key === "admin_audit") {
|
||||||
return result.raw.map((item) => (
|
return result.raw.map((item) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
@@ -1598,6 +1522,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
|
||||||
|
{filteredReports.length === 0 ? (
|
||||||
|
<AdminNoResourceState message={t("empty")} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Card className="admin-list-card">
|
<Card className="admin-list-card">
|
||||||
<CardHeader className="admin-list-header pb-3">
|
<CardHeader className="admin-list-header pb-3">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -1652,11 +1580,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid gap-2 md:grid-cols-4">
|
<div className="grid min-w-0 gap-2 md:grid-cols-4">
|
||||||
{(result?.summary ?? defaultSummaryCards(selectedReport.key, filters, t)).map((item) => (
|
{(result?.summary ?? defaultSummaryCards(selectedReport.key, filters, t)).map((item) => (
|
||||||
<div key={item.label} className={cn("rounded-md border px-3 py-2.5", statTone(item.tone))}>
|
<div key={item.label} className={cn("min-w-0 rounded-md border px-3 py-2.5", statTone(item.tone))}>
|
||||||
<div className="text-xs text-muted-foreground">{item.label}</div>
|
<div className="text-xs text-muted-foreground">{item.label}</div>
|
||||||
<div className="mt-0.5 truncate text-base font-semibold tabular-nums">{item.value}</div>
|
<AdminMoneyDisplay as="div" value={item.value} size="md" className="mt-0.5">
|
||||||
|
{item.value}
|
||||||
|
</AdminMoneyDisplay>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1722,6 +1652,8 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
) : null}
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ArrowRight } from "lucide-react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { SettlementBillRow } from "@/api/admin-agent-settlement";
|
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 { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||||
@@ -64,9 +65,15 @@ export function SettlementBillSummaryHeader({
|
|||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("settlementCenter:billDisplay.settlementAmount", { defaultValue: "结算金额" })}
|
{t("settlementCenter:billDisplay.settlementAmount", { defaultValue: "结算金额" })}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-0.5 text-2xl font-bold tabular-nums tracking-tight text-foreground">
|
<AdminMoneyDisplay
|
||||||
|
as="p"
|
||||||
|
value={formatDashboardMoneyMinor(direction.amount, currencyCode)}
|
||||||
|
size="xl"
|
||||||
|
emphasize
|
||||||
|
className="mt-0.5 text-foreground"
|
||||||
|
>
|
||||||
{formatDashboardMoneyMinor(direction.amount, currencyCode)}
|
{formatDashboardMoneyMinor(direction.amount, currencyCode)}
|
||||||
</p>
|
</AdminMoneyDisplay>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,9 +82,15 @@ export function SettlementBillSummaryHeader({
|
|||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("settlementCenter:columns.paid", { defaultValue: "已收付" })}
|
{t("settlementCenter:columns.paid", { defaultValue: "已收付" })}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-0.5 font-medium tabular-nums">
|
<AdminMoneyDisplay
|
||||||
|
as="p"
|
||||||
|
value={formatDashboardMoneyMinor(bill.paid_amount ?? 0, currencyCode)}
|
||||||
|
size="md"
|
||||||
|
emphasize={false}
|
||||||
|
className="mt-0.5"
|
||||||
|
>
|
||||||
{formatDashboardMoneyMinor(bill.paid_amount ?? 0, currencyCode)}
|
{formatDashboardMoneyMinor(bill.paid_amount ?? 0, currencyCode)}
|
||||||
</p>
|
</AdminMoneyDisplay>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -90,14 +103,14 @@ export function SettlementBillSummaryHeader({
|
|||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("settlementCenter:columns.unpaid", { defaultValue: "未结" })}
|
{t("settlementCenter:columns.unpaid", { defaultValue: "未结" })}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<AdminMoneyDisplay
|
||||||
className={cn(
|
as="p"
|
||||||
"mt-0.5 font-semibold tabular-nums",
|
value={formatDashboardMoneyMinor(bill.unpaid_amount, currencyCode)}
|
||||||
unpaid && "text-amber-900 dark:text-amber-200",
|
size="md"
|
||||||
)}
|
className={cn("mt-0.5", unpaid && "text-amber-900 dark:text-amber-200")}
|
||||||
>
|
>
|
||||||
{formatDashboardMoneyMinor(bill.unpaid_amount, currencyCode)}
|
{formatDashboardMoneyMinor(bill.unpaid_amount, currencyCode)}
|
||||||
</p>
|
</AdminMoneyDisplay>
|
||||||
{unpaid ? (
|
{unpaid ? (
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{bill.status === "pending_confirm"
|
{bill.status === "pending_confirm"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state
|
|||||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
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 { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
|
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
|
||||||
@@ -243,16 +244,20 @@ export function SettlementBillsTable({
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
) : null}
|
) : null}
|
||||||
<TableCell className="text-right tabular-nums">
|
<TableCell className={adminMoneyCellClassName(cn("text-right", signedMoneyClass(row.net_amount, true)))}>
|
||||||
<div className={cn("font-semibold", signedMoneyClass(row.net_amount, true))}>
|
<AdminTableMoney>
|
||||||
{formatDashboardMoneyMinor(direction.amount, currencyCode)}
|
{formatDashboardMoneyMinor(direction.amount, currencyCode)}
|
||||||
</div>
|
</AdminTableMoney>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={cn("text-right tabular-nums", paidMoneyClass(row))}>
|
<TableCell className={adminMoneyCellClassName(cn("text-right", paidMoneyClass(row)))}>
|
||||||
{formatDashboardMoneyMinor(row.paid_amount ?? 0, currencyCode)}
|
<AdminTableMoney>
|
||||||
|
{formatDashboardMoneyMinor(row.paid_amount ?? 0, currencyCode)}
|
||||||
|
</AdminTableMoney>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={cn("text-right tabular-nums", unpaidMoneyClass(row))}>
|
<TableCell className={adminMoneyCellClassName(cn("text-right", unpaidMoneyClass(row)))}>
|
||||||
{formatDashboardMoneyMinor(row.unpaid_amount, currencyCode)}
|
<AdminTableMoney>
|
||||||
|
{formatDashboardMoneyMinor(row.unpaid_amount, currencyCode)}
|
||||||
|
</AdminTableMoney>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={row.status}>
|
<AdminStatusBadge status={row.status}>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
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 { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import { settlementAdjustmentTypeLabel } from "@/modules/settlement/settlement-status-label";
|
import { settlementAdjustmentTypeLabel } from "@/modules/settlement/settlement-status-label";
|
||||||
@@ -368,8 +369,10 @@ export function SettlementOperationsPanel({
|
|||||||
<TableCell className="tabular-nums">
|
<TableCell className="tabular-nums">
|
||||||
{row.billId > 0 ? `#${row.billId}` : "—"}
|
{row.billId > 0 ? `#${row.billId}` : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right tabular-nums font-medium">
|
<TableCell className={adminMoneyCellClassName("text-right font-medium")}>
|
||||||
{formatDashboardMoneyMinor(row.amount, currencyCode)}
|
<AdminTableMoney>
|
||||||
|
{formatDashboardMoneyMinor(row.amount, currencyCode)}
|
||||||
|
</AdminTableMoney>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[160px] truncate text-sm">{row.summary}</TableCell>
|
<TableCell className="max-w-[160px] truncate text-sm">{row.summary}</TableCell>
|
||||||
<TableCell className="max-w-[240px] truncate text-sm text-muted-foreground">
|
<TableCell className="max-w-[240px] truncate text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"
|
|||||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||||
import { PlayerLedgerSourceBadge } from "@/components/admin/player-funding-badges";
|
import { PlayerLedgerSourceBadge } from "@/components/admin/player-funding-badges";
|
||||||
|
import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
import { creditLedgerReasonLabel } from "@/modules/settlement/settlement-status-label";
|
import { creditLedgerReasonLabel } from "@/modules/settlement/settlement-status-label";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -578,8 +579,10 @@ export function TransferOrdersPanel(): React.ReactElement {
|
|||||||
<AdminAgentIdentityCells row={row} />
|
<AdminAgentIdentityCells row={row} />
|
||||||
<AdminPlayerIdentityCells row={row} />
|
<AdminPlayerIdentityCells row={row} />
|
||||||
<TableCell>{row.direction}</TableCell>
|
<TableCell>{row.direction}</TableCell>
|
||||||
<TableCell className="tabular-nums">
|
<TableCell className={adminMoneyCellClassName("text-right")}>
|
||||||
{formatAdminMinorUnits(row.amount, row.currency_code)}
|
<AdminTableMoney>
|
||||||
|
{formatAdminMinorUnits(row.amount, row.currency_code)}
|
||||||
|
</AdminTableMoney>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={row.status}>{statusLabelT(row.status, t)}</AdminStatusBadge>
|
<AdminStatusBadge status={row.status}>{statusLabelT(row.status, t)}</AdminStatusBadge>
|
||||||
@@ -907,8 +910,10 @@ export function WalletTxnsPanel(): React.ReactElement {
|
|||||||
{walletTxnBizTypeLabel(row.biz_type, row.ledger_source, t, tSettlement)}
|
{walletTxnBizTypeLabel(row.biz_type, row.ledger_source, t, tSettlement)}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="min-w-[6.5rem] align-top whitespace-nowrap tabular-nums text-xs">
|
<TableCell className={adminMoneyCellClassName("min-w-[6.5rem] text-right text-xs")}>
|
||||||
{row.amount_formatted ?? formatAdminMinorUnits(row.amount)}
|
<AdminTableMoney>
|
||||||
|
{row.amount_formatted ?? formatAdminMinorUnits(row.amount)}
|
||||||
|
</AdminTableMoney>
|
||||||
<span className="ml-1 text-muted-foreground">
|
<span className="ml-1 text-muted-foreground">
|
||||||
({row.direction === 1 ? t("in") : t("out")})
|
({row.direction === 1 ? t("in") : t("out")})
|
||||||
</span>
|
</span>
|
||||||
@@ -1031,9 +1036,13 @@ export function PlayerWalletPanel(): React.ReactElement {
|
|||||||
<TableRow key={w.id}>
|
<TableRow key={w.id}>
|
||||||
<TableCell>{w.wallet_type}</TableCell>
|
<TableCell>{w.wallet_type}</TableCell>
|
||||||
<TableCell>{w.currency_code}</TableCell>
|
<TableCell>{w.currency_code}</TableCell>
|
||||||
<TableCell className="font-mono tabular-nums">{w.balance}</TableCell>
|
<TableCell className={adminMoneyCellClassName("font-mono text-right")}>
|
||||||
<TableCell className="tabular-nums">
|
<AdminTableMoney>{w.balance}</AdminTableMoney>
|
||||||
{formatAdminMinorUnits(w.available_balance, w.currency_code)}
|
</TableCell>
|
||||||
|
<TableCell className={adminMoneyCellClassName("text-right")}>
|
||||||
|
<AdminTableMoney>
|
||||||
|
{formatAdminMinorUnits(w.available_balance, w.currency_code)}
|
||||||
|
</AdminTableMoney>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export function readProfile(): AdminProfile | null {
|
|||||||
const permissions = Array.isArray(v.permissions)
|
const permissions = Array.isArray(v.permissions)
|
||||||
? v.permissions.filter((s): s is string => typeof s === "string")
|
? 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)
|
const navigation = Array.isArray(v.navigation)
|
||||||
? v.navigation.filter((item): item is AdminNavItem => {
|
? v.navigation.filter((item): item is AdminNavItem => {
|
||||||
return (
|
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 {
|
return {
|
||||||
id: v.id,
|
id: v.id,
|
||||||
username: v.username,
|
username: v.username,
|
||||||
nickname: v.nickname,
|
nickname: v.nickname,
|
||||||
email: typeof v.email === "string" || v.email === null ? v.email : null,
|
email: typeof v.email === "string" || v.email === null ? v.email : null,
|
||||||
permissions,
|
permissions,
|
||||||
|
operational_permissions: operationalPermissions,
|
||||||
navigation,
|
navigation,
|
||||||
|
is_super_admin: v.is_super_admin === true ? true : undefined,
|
||||||
|
account_kind: accountKind,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -16,20 +16,31 @@ export type AdminAuthLoginRequest = {
|
|||||||
|
|
||||||
import type { AdminAgentContext } from "@/types/api/admin-agent";
|
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)的管理员摘要 */
|
/** 登录成功后缓存于会话(localStorage)的管理员摘要 */
|
||||||
export type AdminProfile = {
|
export type AdminProfile = {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
/** 与 Laravel `admin_permissions.slug` 一致(如 `prd.*`);超管为全量列表 */
|
/** Legacy 产品权限 slug(如 `prd.*`);侧栏与旧页面门控 */
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
/** 当前管理员可见的后台菜单,由 Laravel 注册表统一下发。 */
|
/** 当前管理员可见的后台菜单,由 Laravel 注册表统一下发。 */
|
||||||
navigation?: AdminNavItem[];
|
navigation?: AdminNavItem[];
|
||||||
/** 代理账号绑定节点;超管为 null */
|
/** 代理账号绑定节点;超管为 null */
|
||||||
agent?: AdminAgentContext | null;
|
agent?: AdminAgentContext | null;
|
||||||
is_super_admin?: boolean;
|
is_super_admin?: boolean;
|
||||||
/** 与 permissions 同值,语义上强调“可操作权限” */
|
/** 账号形态:超管 / 站点运营 / 代理经营 / 其他平台账号 */
|
||||||
|
account_kind?: AdminAccountKind;
|
||||||
|
/** API 鉴权 action code(如 `service.report.view`);与后端 `effectiveMenuActionPermissionCodes` 一致 */
|
||||||
operational_permissions?: string[];
|
operational_permissions?: string[];
|
||||||
/** 当前代理可下放给下级的 prd.* 上限(未配置 grants 时与操作权限一致) */
|
/** 当前代理可下放给下级的 prd.* 上限(未配置 grants 时与操作权限一致) */
|
||||||
delegation_ceiling?: string[];
|
delegation_ceiling?: string[];
|
||||||
|
|||||||
@@ -49,6 +49,31 @@ export type AdminDashboardCapabilities = {
|
|||||||
wallet_transfer_view: boolean;
|
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`) */
|
/** 站点管理员首页摘要(`GET /api/v1/admin/dashboard` → `site_overview`) */
|
||||||
export type AdminDashboardSiteOverview = {
|
export type AdminDashboardSiteOverview = {
|
||||||
admin_site_id: number;
|
admin_site_id: number;
|
||||||
@@ -178,4 +203,6 @@ export type AdminDashboardData = {
|
|||||||
capabilities: AdminDashboardCapabilities;
|
capabilities: AdminDashboardCapabilities;
|
||||||
agent_overview: AdminDashboardAgentOverview | null;
|
agent_overview: AdminDashboardAgentOverview | null;
|
||||||
site_overview: AdminDashboardSiteOverview | null;
|
site_overview: AdminDashboardSiteOverview | null;
|
||||||
|
site_finance_overview: AdminDashboardSiteFinanceOverview | null;
|
||||||
|
site_cs_overview: AdminDashboardSiteCsOverview | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,13 +24,6 @@ export type AdminReportPlayDimensionRow = {
|
|||||||
approx_house_gross_minor: number;
|
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<T> = {
|
export type AdminReportListData<T> = {
|
||||||
items: T[];
|
items: T[];
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
Reference in New Issue
Block a user