feat(agents, config, dashboard, i18n): add agent line provision wizard, site deletion, and site dashboard with multi-language support
Added agent line provision wizard page with permission gating, replacing redirect placeholder. Introduced site deletion API and UI with confirmation dialog in integration sites management. Added new site-scoped dashboard panel showing bet metrics, P/L trends, active players, and quick links. Enhanced chart tooltip to support custom formatters and fix indicator color
This commit is contained in:
@@ -28,3 +28,8 @@ This version has breaking changes — APIs, conventions, and file structure may
|
|||||||
- 玩家详情:按 `funding_mode` 切换 Tab(信用流水 / 钱包流水;信用盘隐藏转账单)
|
- 玩家详情:按 `funding_mode` 切换 Tab(信用流水 / 钱包流水;信用盘隐藏转账单)
|
||||||
|
|
||||||
新增涉及玩家资金的页面时,先读 `src/lib/admin-player-display.ts`。
|
新增涉及玩家资金的页面时,先读 `src/lib/admin-player-display.ts`。
|
||||||
|
|
||||||
|
## Learned Workspace Facts
|
||||||
|
|
||||||
|
- 无接入站时依赖站点的页面展示 `<AdminNoIntegrationSiteState />`;仅 `profile.is_super_admin` 显示创建入口。
|
||||||
|
- 超管判定用登录态 `is_super_admin`,勿用站点角色或 `admin_user_site_roles` 绑定推断。
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ export async function putAdminIntegrationSite(
|
|||||||
return adminRequest.put<AdminIntegrationSiteDetail>(`${A}/integration-sites/${id}`, body);
|
return adminRequest.put<AdminIntegrationSiteDetail>(`${A}/integration-sites/${id}`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminIntegrationSite(id: number): Promise<null> {
|
||||||
|
return adminRequest.delete<null>(`${A}/integration-sites/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function postAdminIntegrationSiteRotateSecrets(
|
export async function postAdminIntegrationSiteRotateSecrets(
|
||||||
id: number,
|
id: number,
|
||||||
): Promise<AdminIntegrationSiteWithSecrets> {
|
): Promise<AdminIntegrationSiteWithSecrets> {
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
|
import { AgentLineProvisionWizard } from "@/modules/agents/agent-line-provision-wizard";
|
||||||
|
import { PRD_AGENT_LINE_PROVISION_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export default function AgentProvisionRedirectPage(): never {
|
export const metadata: Metadata = buildPageMetadata("agents", "subnav.provision");
|
||||||
redirect("/admin/agents");
|
|
||||||
|
export default function AgentProvisionPage() {
|
||||||
|
return (
|
||||||
|
<ModuleScaffold embedded>
|
||||||
|
<AdminPermissionGate requiredAny={[...PRD_AGENT_LINE_PROVISION_ACCESS_ANY]}>
|
||||||
|
<AgentLineProvisionWizard />
|
||||||
|
</AdminPermissionGate>
|
||||||
|
</ModuleScaffold>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/components/admin/admin-no-integration-site-state.tsx
Normal file
26
src/components/admin/admin-no-integration-site-state.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export function AdminNoIntegrationSiteState({
|
||||||
|
canCreate = false,
|
||||||
|
}: {
|
||||||
|
canCreate?: boolean;
|
||||||
|
}): ReactElement {
|
||||||
|
const { t } = useTranslation("common");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminNoResourceState message={t("integrationSites.emptyPlatformHint")}>
|
||||||
|
{canCreate ? (
|
||||||
|
<Button nativeButton={false} size="sm" render={<Link href="/admin/config/integration-sites" />}>
|
||||||
|
{t("integrationSites.createSite")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</AdminNoResourceState>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -202,7 +202,15 @@ function ChartTooltipContent({
|
|||||||
.map((item, index) => {
|
.map((item, index) => {
|
||||||
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`
|
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
const indicatorColor = color ?? item.payload?.fill ?? item.color
|
const configColor =
|
||||||
|
itemConfig && "color" in itemConfig ? itemConfig.color : undefined
|
||||||
|
const indicatorColor =
|
||||||
|
color ?? item.color ?? item.payload?.fill ?? configColor
|
||||||
|
|
||||||
|
const formattedValue =
|
||||||
|
formatter && item?.value !== undefined && item.name
|
||||||
|
? formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -212,56 +220,50 @@ function ChartTooltipContent({
|
|||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
{itemConfig?.icon ? (
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
<itemConfig.icon />
|
||||||
) : (
|
) : (
|
||||||
<>
|
!hideIndicator && (
|
||||||
{itemConfig?.icon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
||||||
{
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("shrink-0 rounded-[2px]", {
|
||||||
"flex flex-1 justify-between leading-none",
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
nestLabel ? "items-end" : "items-center"
|
"w-1": indicator === "line",
|
||||||
)}
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
>
|
indicator === "dashed",
|
||||||
<div className="grid gap-1.5">
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
{nestLabel ? tooltipLabel : null}
|
})}
|
||||||
<span className="text-muted-foreground">
|
style={
|
||||||
{itemConfig?.label ?? item.name}
|
indicator === "dashed"
|
||||||
</span>
|
? { borderColor: indicatorColor }
|
||||||
</div>
|
: {
|
||||||
{item.value != null && (
|
backgroundColor: indicatorColor,
|
||||||
<span className="font-mono font-medium text-foreground tabular-nums">
|
borderColor: indicatorColor,
|
||||||
{typeof item.value === "number"
|
}
|
||||||
? item.value.toLocaleString()
|
}
|
||||||
: String(item.value)}
|
/>
|
||||||
</span>
|
)
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{itemConfig?.label ?? item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value != null && (
|
||||||
|
<span className="font-mono font-medium text-foreground tabular-nums">
|
||||||
|
{formattedValue ??
|
||||||
|
(typeof item.value === "number"
|
||||||
|
? item.value.toLocaleString()
|
||||||
|
: String(item.value))}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import neReconcile from "@/i18n/locales/ne/reconcile.json";
|
|||||||
import neReports from "@/i18n/locales/ne/reports.json";
|
import neReports from "@/i18n/locales/ne/reports.json";
|
||||||
import neWallet from "@/i18n/locales/ne/wallet.json";
|
import neWallet from "@/i18n/locales/ne/wallet.json";
|
||||||
import neAgents from "@/i18n/locales/ne/agents.json";
|
import neAgents from "@/i18n/locales/ne/agents.json";
|
||||||
|
import neSettlementCenter from "@/i18n/locales/ne/settlementCenter.json";
|
||||||
import zhAudit from "@/i18n/locales/zh/audit.json";
|
import zhAudit from "@/i18n/locales/zh/audit.json";
|
||||||
import zhAdminUsers from "@/i18n/locales/zh/adminUsers.json";
|
import zhAdminUsers from "@/i18n/locales/zh/adminUsers.json";
|
||||||
import zhAuth from "@/i18n/locales/zh/auth.json";
|
import zhAuth from "@/i18n/locales/zh/auth.json";
|
||||||
@@ -103,7 +104,7 @@ const resources = {
|
|||||||
settlement: neSettlement,
|
settlement: neSettlement,
|
||||||
wallet: neWallet,
|
wallet: neWallet,
|
||||||
agents: neAgents,
|
agents: neAgents,
|
||||||
settlementCenter: enSettlementCenter,
|
settlementCenter: neSettlementCenter,
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
common: zhCommon,
|
common: zhCommon,
|
||||||
|
|||||||
@@ -176,8 +176,7 @@
|
|||||||
},
|
},
|
||||||
"lineProvision": {
|
"lineProvision": {
|
||||||
"title": "Create level-1 agent",
|
"title": "Create level-1 agent",
|
||||||
"description": "Creates the level-1 agent, admin login, and line settings (share, credit, rebate, settlement cycle) in one step. Code cannot be changed later.",
|
"description": "Creates the level-1 agent, admin login, and line settings (share, credit, rebate, settlement cycle) in one step.",
|
||||||
"code": "Agent code",
|
|
||||||
"name": "Level-1 agent name",
|
"name": "Level-1 agent name",
|
||||||
"username": "Admin login",
|
"username": "Admin login",
|
||||||
"password": "Initial password",
|
"password": "Initial password",
|
||||||
|
|||||||
@@ -111,6 +111,10 @@
|
|||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"comingSoon": "Feature under development"
|
"comingSoon": "Feature under development"
|
||||||
},
|
},
|
||||||
|
"integrationSites": {
|
||||||
|
"emptyPlatformHint": "No integration sites yet. Create a site before managing agents, players, or settlement.",
|
||||||
|
"createSite": "Create integration site"
|
||||||
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadFailed": "Failed to load"
|
"loadFailed": "Failed to load"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,6 +72,13 @@
|
|||||||
"rotateConfirmTitle": "Rotate secrets?",
|
"rotateConfirmTitle": "Rotate secrets?",
|
||||||
"rotateConfirmDescription": "New SSO and wallet keys will be generated for {{code}}. Old keys stop working immediately.",
|
"rotateConfirmDescription": "New SSO and wallet keys will be generated for {{code}}. Old keys stop working immediately.",
|
||||||
"rotateConfirm": "Rotate",
|
"rotateConfirm": "Rotate",
|
||||||
|
"delete": "Delete site",
|
||||||
|
"deleteSuccess": "Deleted site {{code}}",
|
||||||
|
"deleteFailed": "Failed to delete site",
|
||||||
|
"deleteConfirmTitle": "Delete this site?",
|
||||||
|
"deleteConfirmDescription": "This permanently removes site {{code}} ({{name}}), its agent line, settlement periods, players, and site admin accounts. This cannot be undone.",
|
||||||
|
"deleteConfirm": "Delete",
|
||||||
|
"deleting": "Deleting…",
|
||||||
"secretsTitle": "Save these secrets now",
|
"secretsTitle": "Save these secrets now",
|
||||||
"secretsDescription": "Secrets for {{code}} are shown only once.",
|
"secretsDescription": "Secrets for {{code}} are shown only once.",
|
||||||
"secretsDismiss": "I have saved them",
|
"secretsDismiss": "I have saved them",
|
||||||
|
|||||||
@@ -156,9 +156,35 @@
|
|||||||
"riskMonitor": "Risk monitor",
|
"riskMonitor": "Risk monitor",
|
||||||
"systemSettings": "System settings"
|
"systemSettings": "System settings"
|
||||||
},
|
},
|
||||||
|
"site": {
|
||||||
|
"title": "Site overview",
|
||||||
|
"subtitle": "{{name}} · this site",
|
||||||
|
"todayBet": "Today's bets",
|
||||||
|
"todayProfit": "Today's P/L",
|
||||||
|
"sevenDayTitle": "Last 7 days",
|
||||||
|
"sevenDayProfit": "7-day P/L",
|
||||||
|
"profitScopeHint": "Site scope: bets minus payouts",
|
||||||
|
"activePlayersToday": "Active players today",
|
||||||
|
"betOrdersTodayHint": "{{count}} orders today",
|
||||||
|
"pendingBills": "Pending bills",
|
||||||
|
"pendingUnpaid": "Unpaid {{amount}}",
|
||||||
|
"latestBetAt": "Latest bet {{time}}",
|
||||||
|
"noBetToday": "No bets yet today",
|
||||||
|
"scaleTitle": "Site scale",
|
||||||
|
"agentCount": "Agent nodes",
|
||||||
|
"playerCount": "Players",
|
||||||
|
"topAgentToday": "Top agent today: {{name}} ({{amount}})",
|
||||||
|
"quickLinks": {
|
||||||
|
"tickets": "Tickets",
|
||||||
|
"players": "Players",
|
||||||
|
"reports": "Reports",
|
||||||
|
"agents": "Agents",
|
||||||
|
"bills": "Settlement"
|
||||||
|
}
|
||||||
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"title": "Operations overview",
|
"title": "Operations overview",
|
||||||
"subtitle": "Your line scope · {{name}}",
|
"subtitle": "{{name}} · your line",
|
||||||
"heroEyebrow": "Today's line cockpit",
|
"heroEyebrow": "Today's line cockpit",
|
||||||
"heroTitle": "{{name}} live operations",
|
"heroTitle": "{{name}} live operations",
|
||||||
"creditTitle": "Credit limit",
|
"creditTitle": "Credit limit",
|
||||||
@@ -176,6 +202,7 @@
|
|||||||
"teamPlayers": "Players in line",
|
"teamPlayers": "Players in line",
|
||||||
"activePlayersToday": "Active players today",
|
"activePlayersToday": "Active players today",
|
||||||
"betOrdersToday": "Bet orders today",
|
"betOrdersToday": "Bet orders today",
|
||||||
|
"betOrdersTodayHint": "{{count}} orders today",
|
||||||
"todayBet": "Today's bet",
|
"todayBet": "Today's bet",
|
||||||
"todayPayout": "Today's payout",
|
"todayPayout": "Today's payout",
|
||||||
"todayProfit": "Today's profit",
|
"todayProfit": "Today's profit",
|
||||||
@@ -202,6 +229,7 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"viewBills": "View bills",
|
"viewBills": "View bills",
|
||||||
|
"lineMeta": "Depth {{depth}} · child agents {{childAgent}} · players {{player}}",
|
||||||
"viewLine": "Agent line",
|
"viewLine": "Agent line",
|
||||||
"quickLinks": {
|
"quickLinks": {
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"closeDialogShare": "{{count}} ledger entries",
|
"closeDialogShare": "{{count}} ledger entries",
|
||||||
"closeDialogUnsettled": "{{count}} tickets still unsettled",
|
"closeDialogUnsettled": "{{count}} tickets still unsettled",
|
||||||
"closeDialogIrreversible": "Cannot undo. Use adjustments or reversals to fix errors.",
|
"closeDialogIrreversible": "Cannot undo. Use adjustments or reversals to fix errors.",
|
||||||
"closeDialogConfirm": "Close period"
|
"closeDialogConfirm": "Close period",
|
||||||
|
"closeDialogEmpty": "No share ledger this period; closing will not generate bills."
|
||||||
},
|
},
|
||||||
"periodDetail": {
|
"periodDetail": {
|
||||||
"back": "Back to periods",
|
"back": "Back to periods",
|
||||||
@@ -152,7 +153,8 @@
|
|||||||
"adjustment": "Adjustment",
|
"adjustment": "Adjustment",
|
||||||
"reversal": "Reversal",
|
"reversal": "Reversal",
|
||||||
"bad_debt": "Bad debt",
|
"bad_debt": "Bad debt",
|
||||||
"share_ledger": "Share ledger"
|
"share_ledger": "Share ledger",
|
||||||
|
"freezeAmount": "Hold {{amount}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"columns": {
|
"columns": {
|
||||||
@@ -176,7 +178,13 @@
|
|||||||
"adjustmentType": "Type",
|
"adjustmentType": "Type",
|
||||||
"originalBill": "Original bill",
|
"originalBill": "Original bill",
|
||||||
"reason": "Reason",
|
"reason": "Reason",
|
||||||
"badDebtAmount": "Write-off"
|
"badDebtAmount": "Write-off",
|
||||||
|
"playerAccount": "Player account",
|
||||||
|
"playerId": "Player ID",
|
||||||
|
"directAgent": "Direct agent",
|
||||||
|
"superiorAgent": "Upline agent",
|
||||||
|
"play": "Play",
|
||||||
|
"drawNo": "Draw no."
|
||||||
},
|
},
|
||||||
"billStatus": {
|
"billStatus": {
|
||||||
"pending_confirm": "Pending confirm",
|
"pending_confirm": "Pending confirm",
|
||||||
@@ -302,21 +310,72 @@
|
|||||||
"agent": "Agent bills",
|
"agent": "Agent bills",
|
||||||
"pendingConfirm": "Pending confirm",
|
"pendingConfirm": "Pending confirm",
|
||||||
"awaitingPayment": "Awaiting payment"
|
"awaitingPayment": "Awaiting payment"
|
||||||
|
},
|
||||||
|
"billId": "Bill ID",
|
||||||
|
"ownerKeyword": "Owner / counterparty",
|
||||||
|
"ownerKeywordPh": "Player account or agent name",
|
||||||
|
"status": "Bill status",
|
||||||
|
"billType": "Bill type",
|
||||||
|
"filterAll": "All statuses",
|
||||||
|
"filterAllTypes": "All types",
|
||||||
|
"filterAdjustment": "Adjust / reverse",
|
||||||
|
"optional": "Optional",
|
||||||
|
"searchBtn": "Search",
|
||||||
|
"reset": "Reset",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"clientFilterHint": "Owner filter: showing {{shown}} / {{total}}",
|
||||||
|
"emptyFiltered": "No bills match the filter. Try All statuses or reset.",
|
||||||
|
"emptyClosed": "Period closed but no bills. Often no settled credit tickets in range.",
|
||||||
|
"rowHint": {
|
||||||
|
"playerOwner": "Player owner",
|
||||||
|
"agentOwner": "Agent owner",
|
||||||
|
"adjustmentOwner": "Adjustment owner",
|
||||||
|
"badDebtOwner": "Bad debt owner",
|
||||||
|
"reversalOwner": "Reversal owner",
|
||||||
|
"playerUpline": "Player's upline agent",
|
||||||
|
"agentUpline": "Settlement upline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"panels": {
|
"panels": {
|
||||||
"overview": { "title": "Overview" },
|
"overview": {
|
||||||
"ledger": { "title": "Account ledger" },
|
"title": "Overview"
|
||||||
"bills": { "title": "Bills" },
|
},
|
||||||
"creditLedger": { "title": "Credit ledger" },
|
"ledger": {
|
||||||
"playerBills": { "title": "Player bills" },
|
"title": "Account ledger"
|
||||||
"agentBills": { "title": "Agent bills" },
|
},
|
||||||
"pendingConfirm": { "title": "Pending confirm" },
|
"bills": {
|
||||||
"awaiting": { "title": "Awaiting payment" },
|
"title": "Bills"
|
||||||
"payments": { "title": "Payment log" },
|
},
|
||||||
"adjustments": { "title": "Adjust / reverse" },
|
"creditLedger": {
|
||||||
"reports": { "title": "Period reports" },
|
"title": "Credit ledger"
|
||||||
"badDebt": { "title": "Bad debt" }
|
},
|
||||||
|
"playerBills": {
|
||||||
|
"title": "Player bills"
|
||||||
|
},
|
||||||
|
"agentBills": {
|
||||||
|
"title": "Agent bills"
|
||||||
|
},
|
||||||
|
"pendingConfirm": {
|
||||||
|
"title": "Pending confirm"
|
||||||
|
},
|
||||||
|
"awaiting": {
|
||||||
|
"title": "Awaiting payment"
|
||||||
|
},
|
||||||
|
"payments": {
|
||||||
|
"title": "Payment log"
|
||||||
|
},
|
||||||
|
"adjustments": {
|
||||||
|
"title": "Adjust / reverse"
|
||||||
|
},
|
||||||
|
"reports": {
|
||||||
|
"title": "Period reports"
|
||||||
|
},
|
||||||
|
"badDebt": {
|
||||||
|
"title": "Bad debt"
|
||||||
|
},
|
||||||
|
"workbench": {
|
||||||
|
"title": "Workbench"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"noSite": "Select a site.",
|
"noSite": "Select a site.",
|
||||||
|
|||||||
@@ -163,6 +163,8 @@
|
|||||||
"account": "खाता सेटिङ",
|
"account": "खाता सेटिङ",
|
||||||
"integration": "मुख्य साइट एकीकरण",
|
"integration": "मुख्य साइट एकीकरण",
|
||||||
"agents": "एजेन्ट लाइन",
|
"agents": "एजेन्ट लाइन",
|
||||||
|
"agent_list": "एजेन्ट सूची",
|
||||||
|
"settlement_center": "सेटलमेन्ट केन्द्र",
|
||||||
"config": "सञ्चालन कन्फिगरेसन"
|
"config": "सञ्चालन कन्फिगरेसन"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -153,6 +153,32 @@
|
|||||||
"riskMonitor": "जोखिम निगरानी",
|
"riskMonitor": "जोखिम निगरानी",
|
||||||
"systemSettings": "प्रणाली सेटिङ"
|
"systemSettings": "प्रणाली सेटिङ"
|
||||||
},
|
},
|
||||||
|
"site": {
|
||||||
|
"title": "साइट सारांश",
|
||||||
|
"subtitle": "{{name}} · यो साइट",
|
||||||
|
"todayBet": "आजको बाजी",
|
||||||
|
"todayProfit": "आजको नाफा/नोक्सान",
|
||||||
|
"sevenDayTitle": "पछिल्लो ७ दिन",
|
||||||
|
"sevenDayProfit": "७-दिने नाफा/नोक्सान",
|
||||||
|
"profitScopeHint": "साइट दायरा: बाजी माइनस भुक्तानी",
|
||||||
|
"activePlayersToday": "आज सक्रिय खेलाडी",
|
||||||
|
"betOrdersTodayHint": "आज {{count}} अर्डर",
|
||||||
|
"pendingBills": "बाँकी बिल",
|
||||||
|
"pendingUnpaid": "नतिरेको {{amount}}",
|
||||||
|
"latestBetAt": "पछिल्लो बाजी {{time}}",
|
||||||
|
"noBetToday": "आज अहिलेसम्म बाजी छैन",
|
||||||
|
"scaleTitle": "साइट परिमाण",
|
||||||
|
"agentCount": "एजेन्ट नोड",
|
||||||
|
"playerCount": "खेलाडी संख्या",
|
||||||
|
"topAgentToday": "आजको शीर्ष एजेन्ट: {{name}} ({{amount}})",
|
||||||
|
"quickLinks": {
|
||||||
|
"tickets": "टिकट",
|
||||||
|
"players": "खेलाडी",
|
||||||
|
"reports": "रिपोर्ट",
|
||||||
|
"agents": "एजेन्ट",
|
||||||
|
"bills": "सेटलमेन्ट"
|
||||||
|
}
|
||||||
|
},
|
||||||
"warnings": {
|
"warnings": {
|
||||||
"drawPermission": "यो खातासँग ड्रअ/ड्यासबोर्ड हेर्ने अनुमति छैन। वित्तीय र जोखिम डाटा फिर्ता आएन।",
|
"drawPermission": "यो खातासँग ड्रअ/ड्यासबोर्ड हेर्ने अनुमति छैन। वित्तीय र जोखिम डाटा फिर्ता आएन।",
|
||||||
"walletPermission": "यो खातासँग वालेट मिलान हेर्ने अनुमति छैन। असामान्य ट्रान्सफर संख्या फिर्ता आएन।",
|
"walletPermission": "यो खातासँग वालेट मिलान हेर्ने अनुमति छैन। असामान्य ट्रान्सफर संख्या फिर्ता आएन।",
|
||||||
|
|||||||
402
src/i18n/locales/ne/settlementCenter.json
Normal file
402
src/i18n/locales/ne/settlementCenter.json
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
{
|
||||||
|
"title": "सेटलमेन्ट केन्द्र",
|
||||||
|
"subtitle": "अवधि बन्द, बिल पुष्टि र भुक्तानी",
|
||||||
|
"subtitleList": "अवधि सूची: खोल्नुहोस्/बन्द गर्नुहोस्; सारांश स्तम्भहरू समावेश — बिल र बेट लेजरका लागि पङ्क्ति कार्य प्रयोग गर्नुहोस्।",
|
||||||
|
"period": {
|
||||||
|
"title": "Period",
|
||||||
|
"statusCompleted": "Completed",
|
||||||
|
"pipelineShare": "{{count}} ledger entries",
|
||||||
|
"billTodo": "Pending {{p}} · Awaiting {{a}}",
|
||||||
|
"openTitle": "Open period",
|
||||||
|
"openBtn": "Open period",
|
||||||
|
"closeNeedLedger": "No ledger activity yet. Complete draw settlement first.",
|
||||||
|
"closeDialogTitle": "Close period",
|
||||||
|
"closeDialogDesc": "Summarize {{range}} and generate bills.",
|
||||||
|
"closeDialogShare": "{{count}} ledger entries",
|
||||||
|
"closeDialogUnsettled": "{{count}} tickets still unsettled",
|
||||||
|
"closeDialogIrreversible": "Cannot undo. Use adjustments or reversals to fix errors.",
|
||||||
|
"closeDialogConfirm": "Close period",
|
||||||
|
"closeDialogEmpty": "No share ledger this period; closing will not generate bills."
|
||||||
|
},
|
||||||
|
"periodDetail": {
|
||||||
|
"back": "Back to periods",
|
||||||
|
"notFound": "Period not found or site changed. Go back to the list."
|
||||||
|
},
|
||||||
|
"periodTable": {
|
||||||
|
"title": "Periods",
|
||||||
|
"statusFilter": "Status",
|
||||||
|
"range": "Period",
|
||||||
|
"ledgerCount": "Ledger",
|
||||||
|
"pending": "Pending",
|
||||||
|
"awaiting": "Awaiting",
|
||||||
|
"shareLedger": "Share ledger",
|
||||||
|
"gameWinLoss": "Win/loss total",
|
||||||
|
"platformWinLoss": "Platform win/loss",
|
||||||
|
"agentWinLoss": "Agent win/loss",
|
||||||
|
"basicRebate": "Rebate total",
|
||||||
|
"unsettledTickets": "Unsettled tickets",
|
||||||
|
"openReportHint": "Open period: share/win-loss from in-period ledger; bill count updates after close.",
|
||||||
|
"viewDetail": "View details",
|
||||||
|
"close": "Close",
|
||||||
|
"closeNow": "Close now",
|
||||||
|
"hasOpen": "Period {{range}} is open. Close it before opening a new one.",
|
||||||
|
"emptyOpenHint": "No periods yet. Click Open period in the toolbar.",
|
||||||
|
"emptyReadOnly": "No period records.",
|
||||||
|
"emptyFiltered": "No rows match the filter. Reset filters.",
|
||||||
|
"emptyFilteredOpen": "The open period is hidden by the filter. Choose All or Open.",
|
||||||
|
"readOnlyHint": "Bound agent accounts cannot open or close site periods."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"subtitle": "क्रेडिट-लाइन सेटलमेन्ट",
|
||||||
|
"statusRunning": "अवधि खुला",
|
||||||
|
"statusIdle": "कुनै खुला अवधि छैन",
|
||||||
|
"statusCompleted": "अवधि पूरा भयो"
|
||||||
|
},
|
||||||
|
"subnav": {
|
||||||
|
"label": "सेटलमेन्ट केन्द्र नेभिगेसन"
|
||||||
|
},
|
||||||
|
"workbench": {
|
||||||
|
"viewPeriod": "Period",
|
||||||
|
"closePreset": "Close · {{label}}",
|
||||||
|
"closeNoData": "Close failed: no share ledger in period. Run credit game settlement first.",
|
||||||
|
"openPeriodPipeline": "Open {{range}} · {{share}} share entries"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"periods": "अवधिहरू",
|
||||||
|
"bills": "बिलहरू",
|
||||||
|
"operations": "भुक्तानी र समायोजन",
|
||||||
|
"ledger": "लेजर",
|
||||||
|
"creditLedger": "क्रेडिट लेजर",
|
||||||
|
"playerBills": "Player bills",
|
||||||
|
"agentBills": "Agent bills",
|
||||||
|
"pendingConfirm": "Pending confirm",
|
||||||
|
"awaitingPayment": "Awaiting payment",
|
||||||
|
"payments": "Payment log",
|
||||||
|
"adjustments": "Adjust / reverse",
|
||||||
|
"badDebt": "Bad debt",
|
||||||
|
"reports": "Period reports"
|
||||||
|
},
|
||||||
|
"operations": {
|
||||||
|
"hint": "Payment registration, bad-debt write-offs, and adjustments. Player credit movements are under Account ledger.",
|
||||||
|
"adjustmentsTitle": "Adjustments / reversals / bad debt",
|
||||||
|
"loadFailed": "Failed to load payment and adjustment records",
|
||||||
|
"operationType": "Operation type",
|
||||||
|
"filterAllTypes": "All types",
|
||||||
|
"typePayment": "Payment",
|
||||||
|
"keyword": "Keyword",
|
||||||
|
"keywordPh": "Method, reason, proof, payer/payee"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"period": "Period",
|
||||||
|
"statusAll": "All",
|
||||||
|
"allPeriods": "All periods",
|
||||||
|
"statusOpen": "Open",
|
||||||
|
"statusClosed": "Closed",
|
||||||
|
"statusCompleted": "Completed"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"pendingConfirm": "Pending confirm",
|
||||||
|
"awaitingPayment": "Awaiting payment",
|
||||||
|
"totalUnpaid": "Total unpaid",
|
||||||
|
"openPeriod": "Open period",
|
||||||
|
"creditLedger": "Credit ledger (in period)",
|
||||||
|
"shareLedger": "Share ledger (in period)",
|
||||||
|
"pipelineHint": "Bills are created after period close; counts below are in-period activity."
|
||||||
|
},
|
||||||
|
"ledger": {
|
||||||
|
"groupIntro": "In-period money movements: credit holds, bill payments, adjustments, and bad debt. Share bills after close are under Bills.",
|
||||||
|
"paymentsIntro": "Confirmed bill payments (payment_records). Register from bill detail; this page is the period-wide log.",
|
||||||
|
"adjustmentsIntro": "Bill adjustments and reversals (settlement_adjustments).",
|
||||||
|
"badDebtIntro": "Bad debt write-off entries linked to original bills."
|
||||||
|
},
|
||||||
|
"creditLedger": {
|
||||||
|
"periodIntro": "Credit-line bets in this period: bet hold and draw settlement (merged per ticket).",
|
||||||
|
"emptyPeriod": "No bet ledger entries in this period. Ensure credit players placed bets and draws were settled.",
|
||||||
|
"intro": "Ledger entries for the selected period.",
|
||||||
|
"columns": {
|
||||||
|
"txn": "Txn ID",
|
||||||
|
"player": "Player",
|
||||||
|
"reason": "Type",
|
||||||
|
"ref": "Reference",
|
||||||
|
"amount": "Amount",
|
||||||
|
"channel": "Channel",
|
||||||
|
"status": "Status",
|
||||||
|
"time": "Time"
|
||||||
|
},
|
||||||
|
"channelCredit": "Credit line",
|
||||||
|
"viewPlayer": "Player detail",
|
||||||
|
"entryKind": {
|
||||||
|
"adjustment": "Adjustment"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"posted": "Posted"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"viewPlayer": "Player detail",
|
||||||
|
"viewBill": "Bill detail",
|
||||||
|
"confirm": "Confirm bill",
|
||||||
|
"confirmDesc": "After confirm, the bill moves to awaiting payment.",
|
||||||
|
"payment": "Record payment",
|
||||||
|
"adjustment": "Adjust",
|
||||||
|
"reversal": "Reverse",
|
||||||
|
"badDebt": "Bad debt"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"payment_record": "Bill payment",
|
||||||
|
"bet_hold": "Bet hold",
|
||||||
|
"game_settlement": "Draw settlement",
|
||||||
|
"game_settlement_win": "Draw settlement credit",
|
||||||
|
"bet_hold_release": "Hold release",
|
||||||
|
"game_settlement_loss": "Draw settlement debit",
|
||||||
|
"settlement_payout": "Settlement payout",
|
||||||
|
"settlement_confirm": "Period confirm",
|
||||||
|
"adjustment": "Adjustment",
|
||||||
|
"reversal": "Reversal",
|
||||||
|
"bad_debt": "Bad debt",
|
||||||
|
"share_ledger": "Share ledger",
|
||||||
|
"freezeAmount": "Hold {{amount}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"period": "Period",
|
||||||
|
"type": "Type",
|
||||||
|
"owner": "Owner",
|
||||||
|
"counterparty": "Counterparty",
|
||||||
|
"gross": "Win/loss",
|
||||||
|
"net": "Net",
|
||||||
|
"paid": "Paid",
|
||||||
|
"unpaid": "Unpaid",
|
||||||
|
"status": "Status",
|
||||||
|
"billId": "Bill ID",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"amount": "Amount",
|
||||||
|
"method": "Method",
|
||||||
|
"time": "Time",
|
||||||
|
"summary": "Summary",
|
||||||
|
"detail": "Detail",
|
||||||
|
"adjustmentType": "Type",
|
||||||
|
"originalBill": "Original bill",
|
||||||
|
"reason": "Reason",
|
||||||
|
"badDebtAmount": "Write-off",
|
||||||
|
"playerAccount": "Player account",
|
||||||
|
"playerId": "Player ID",
|
||||||
|
"directAgent": "Direct agent",
|
||||||
|
"superiorAgent": "Upline agent",
|
||||||
|
"play": "Play",
|
||||||
|
"drawNo": "Draw no."
|
||||||
|
},
|
||||||
|
"billStatus": {
|
||||||
|
"pending_confirm": "Pending confirm",
|
||||||
|
"confirmed": "Confirmed",
|
||||||
|
"partial_paid": "Partial paid",
|
||||||
|
"settled": "Settled",
|
||||||
|
"overdue": "Overdue",
|
||||||
|
"reversed": "Reversed"
|
||||||
|
},
|
||||||
|
"billType": {
|
||||||
|
"adjustment": "Adjustment",
|
||||||
|
"reversal": "Reversal",
|
||||||
|
"badDebt": "Bad debt write-off"
|
||||||
|
},
|
||||||
|
"adjustmentType": {
|
||||||
|
"adjustment": "Adjustment",
|
||||||
|
"reversal": "Reversal",
|
||||||
|
"bad_debt": "Bad debt"
|
||||||
|
},
|
||||||
|
"paymentStatus": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"confirmed": "Confirmed"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"detail": "Detail",
|
||||||
|
"viewBill": "View bill",
|
||||||
|
"billDetail": "Bill detail"
|
||||||
|
},
|
||||||
|
"billDisplay": {
|
||||||
|
"settlementFlow": "Who pays whom",
|
||||||
|
"settlementAmount": "Settlement amount",
|
||||||
|
"pays": "Pays",
|
||||||
|
"paysShort": "Pays",
|
||||||
|
"howAmountWorks": "Breakdown",
|
||||||
|
"payerLabel": "Payer",
|
||||||
|
"payeeLabel": "Payee",
|
||||||
|
"playerBreakdownIntro": "Players settle only with their direct agent: net = win/loss − rebate.",
|
||||||
|
"agentBreakdownIntro": "Agents settle only with their upline: net = team net − downline share − own share.",
|
||||||
|
"playerGross": "Game win/loss",
|
||||||
|
"playerLostHint": "Player lost; owes agent",
|
||||||
|
"playerWonHint": "Player won; agent owes player",
|
||||||
|
"playerNet": "Player net payable",
|
||||||
|
"playerNetReceive": "Agent pays player",
|
||||||
|
"teamGross": "Team game win/loss",
|
||||||
|
"teamGrossHint": "Includes this agent and all downline players",
|
||||||
|
"teamGrossShort": "Team",
|
||||||
|
"playerGrossShort": "Player",
|
||||||
|
"teamRebate": "Team rebate",
|
||||||
|
"teamNet": "Team net",
|
||||||
|
"rebate": "Rebate",
|
||||||
|
"agentShareKeep": "Share kept at this tier",
|
||||||
|
"agentShareKeepHint": "Profit retained at this tier by share ratio",
|
||||||
|
"agentDownlineShare": "Downline share",
|
||||||
|
"agentDownlineShareHint": "Profit retained by downline agents (see breakdown below)",
|
||||||
|
"agentDownlineShareItem": "{{agent}} kept",
|
||||||
|
"agentNet": "Pay {{counterparty}}",
|
||||||
|
"agentNetReceive": "{{counterparty}} pays this tier",
|
||||||
|
"billOwner": "Bill owner",
|
||||||
|
"billCounterparty": "Counterparty",
|
||||||
|
"unpaidPendingConfirm": "Confirm the bill before recording payment",
|
||||||
|
"unpaidAwaitingPayment": "Record offline payment",
|
||||||
|
"fullySettled": "Fully settled this period",
|
||||||
|
"confirmHint": "Confirm the bill before recording payment.",
|
||||||
|
"recordReceiptFrom": "Record receipt ({{payer}} → {{payee}})",
|
||||||
|
"recordPayoutTo": "Record payout ({{payer}} → {{payee}})",
|
||||||
|
"rebateAllocationsHint": "How rebate is allocated across agent tiers.",
|
||||||
|
"payment": "Payment",
|
||||||
|
"flowHint": {
|
||||||
|
"playerPayAgent": "Player should settle with the direct agent",
|
||||||
|
"agentPayPlayer": "Direct agent should settle with the player",
|
||||||
|
"agentPayUpstream": "This agent should settle with upline / platform",
|
||||||
|
"upstreamPayAgent": "Upline / platform should settle with this agent",
|
||||||
|
"adjustment": "Adjustment settles separately and keeps the original bill relation",
|
||||||
|
"badDebt": "Write off unpaid amount and archive it as bad debt",
|
||||||
|
"reversal": "Reverse the original bill impact according to reversal rules",
|
||||||
|
"generic": "Apply payment or adjustment based on this bill relation"
|
||||||
|
},
|
||||||
|
"hierarchyHint": "One period creates multiple bills: players pay their agent first; each agent keeps share profit and remits the rest upline. Gross win/loss may match across rows while settlement amounts step down."
|
||||||
|
},
|
||||||
|
"ledgerPanel": {
|
||||||
|
"search": "Search",
|
||||||
|
"searchBtn": "Search",
|
||||||
|
"reset": "Reset filters",
|
||||||
|
"refresh": "Refresh page",
|
||||||
|
"filterAll": "Any",
|
||||||
|
"playerAccount": "Player account",
|
||||||
|
"playerAccountPh": "Username or site player ID",
|
||||||
|
"playerId": "Player ID",
|
||||||
|
"optional": "Optional",
|
||||||
|
"billStatus": "Bill status",
|
||||||
|
"dateRange": "Date range",
|
||||||
|
"rowPosted": "Posted"
|
||||||
|
},
|
||||||
|
"billsPanel": {
|
||||||
|
"intro": "Share bills after period close. Filter by type or status; open detail to confirm or record payment.",
|
||||||
|
"hierarchyHint": "One period creates multiple bills: players pay their agent first; each agent keeps share profit and remits the rest upline. Gross win/loss may match across rows while settlement amounts step down.",
|
||||||
|
"quickFilter": {
|
||||||
|
"title": "Which settlement layer do you want to review",
|
||||||
|
"desc": "Choose player or agent settlement first, then narrow by status or bill id.",
|
||||||
|
"allTitle": "All bills",
|
||||||
|
"allHint": "View player, agent, adjustment, and bad debt bills together",
|
||||||
|
"playerTitle": "Player settlement",
|
||||||
|
"playerHint": "Credit settlement between players and their direct agent",
|
||||||
|
"agentTitle": "Agent settlement",
|
||||||
|
"agentHint": "Tier settlement between agents and their upline / platform"
|
||||||
|
},
|
||||||
|
"activeHint": {
|
||||||
|
"all": "Current focus: all settlement documents in this period",
|
||||||
|
"player": "Current focus: credit settlement between players and direct agents",
|
||||||
|
"agent": "Current focus: payments between agents and their upline / platform"
|
||||||
|
},
|
||||||
|
"layer": {
|
||||||
|
"player": "Player vs direct agent",
|
||||||
|
"agent": "Agent vs upline / platform",
|
||||||
|
"adjustment": "Settlement adjustment",
|
||||||
|
"badDebt": "Bad debt archive",
|
||||||
|
"reversal": "Historical bill reversal",
|
||||||
|
"generic": "Settlement supporting document"
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"all": "All",
|
||||||
|
"player": "Player bills",
|
||||||
|
"agent": "Agent bills",
|
||||||
|
"pendingConfirm": "Pending confirm",
|
||||||
|
"awaitingPayment": "Awaiting payment"
|
||||||
|
},
|
||||||
|
"billId": "Bill ID",
|
||||||
|
"ownerKeyword": "Owner / counterparty",
|
||||||
|
"ownerKeywordPh": "Player account or agent name",
|
||||||
|
"status": "Bill status",
|
||||||
|
"billType": "Bill type",
|
||||||
|
"filterAll": "All statuses",
|
||||||
|
"filterAllTypes": "All types",
|
||||||
|
"filterAdjustment": "Adjust / reverse",
|
||||||
|
"optional": "Optional",
|
||||||
|
"searchBtn": "Search",
|
||||||
|
"reset": "Reset",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"clientFilterHint": "Owner filter: showing {{shown}} / {{total}}",
|
||||||
|
"emptyFiltered": "No bills match the filter. Try All statuses or reset.",
|
||||||
|
"emptyClosed": "Period closed but no bills. Often no settled credit tickets in range.",
|
||||||
|
"rowHint": {
|
||||||
|
"playerOwner": "Player owner",
|
||||||
|
"agentOwner": "Agent owner",
|
||||||
|
"adjustmentOwner": "Adjustment owner",
|
||||||
|
"badDebtOwner": "Bad debt owner",
|
||||||
|
"reversalOwner": "Reversal owner",
|
||||||
|
"playerUpline": "Player's upline agent",
|
||||||
|
"agentUpline": "Settlement upline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"overview": {
|
||||||
|
"title": "Overview"
|
||||||
|
},
|
||||||
|
"ledger": {
|
||||||
|
"title": "Account ledger"
|
||||||
|
},
|
||||||
|
"bills": {
|
||||||
|
"title": "Bills"
|
||||||
|
},
|
||||||
|
"creditLedger": {
|
||||||
|
"title": "Credit ledger"
|
||||||
|
},
|
||||||
|
"playerBills": {
|
||||||
|
"title": "Player bills"
|
||||||
|
},
|
||||||
|
"agentBills": {
|
||||||
|
"title": "Agent bills"
|
||||||
|
},
|
||||||
|
"pendingConfirm": {
|
||||||
|
"title": "Pending confirm"
|
||||||
|
},
|
||||||
|
"awaiting": {
|
||||||
|
"title": "Awaiting payment"
|
||||||
|
},
|
||||||
|
"payments": {
|
||||||
|
"title": "Payment log"
|
||||||
|
},
|
||||||
|
"adjustments": {
|
||||||
|
"title": "Adjust / reverse"
|
||||||
|
},
|
||||||
|
"reports": {
|
||||||
|
"title": "Period reports"
|
||||||
|
},
|
||||||
|
"badDebt": {
|
||||||
|
"title": "Bad debt"
|
||||||
|
},
|
||||||
|
"workbench": {
|
||||||
|
"title": "Workbench"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"noSite": "Select a site.",
|
||||||
|
"noPeriods": "Close the current period first.",
|
||||||
|
"noClosed": "Close a period to generate bills.",
|
||||||
|
"noBadDebt": "No bad debt records.",
|
||||||
|
"noCreditLedger": "No ledger entries in this period.",
|
||||||
|
"billsNeedClose": "Bills are created after period close."
|
||||||
|
},
|
||||||
|
"periods": {
|
||||||
|
"loadFailed": "Failed to load periods"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"periodClosed": "Period closed",
|
||||||
|
"periodClosedUnsettled": "Period closed; {{count}} ticket(s) still unsettled."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"loadBills": "Failed to load bills",
|
||||||
|
"loadPayments": "Failed to load payments",
|
||||||
|
"loadAdjustments": "Failed to load adjustments",
|
||||||
|
"loadBadDebt": "Failed to load bad debt records",
|
||||||
|
"loadCreditLedger": "Failed to load credit ledger"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -301,15 +301,12 @@
|
|||||||
},
|
},
|
||||||
"lineProvision": {
|
"lineProvision": {
|
||||||
"title": "创建一级代理",
|
"title": "创建一级代理",
|
||||||
"description": "将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水、结算周期。代理编码创建后不可修改。",
|
"description": "将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水、结算周期。",
|
||||||
"siteCode": "接入站点",
|
"siteCode": "接入站点",
|
||||||
"siteCodePlaceholder": "选择站点",
|
"siteCodePlaceholder": "选择站点",
|
||||||
"siteRequired": "请选择接入站点",
|
"siteRequired": "请选择接入站点",
|
||||||
"codeRequired": "请填写代理编码",
|
|
||||||
"codePatternInvalid": "代理编码仅支持字母、数字、下划线和中划线,且需以字母或数字开头",
|
|
||||||
"noUnboundSite": "暂无未绑定一级代理的站点",
|
"noUnboundSite": "暂无未绑定一级代理的站点",
|
||||||
"openIntegrationSites": "前往接入站点",
|
"openIntegrationSites": "前往接入站点",
|
||||||
"code": "代理编码",
|
|
||||||
"name": "一级代理名称",
|
"name": "一级代理名称",
|
||||||
"username": "后台登录账号",
|
"username": "后台登录账号",
|
||||||
"password": "初始密码",
|
"password": "初始密码",
|
||||||
|
|||||||
@@ -111,6 +111,10 @@
|
|||||||
"loading": "加载中…",
|
"loading": "加载中…",
|
||||||
"comingSoon": "功能开发中"
|
"comingSoon": "功能开发中"
|
||||||
},
|
},
|
||||||
|
"integrationSites": {
|
||||||
|
"emptyPlatformHint": "暂无接入站点。请先创建站点,再进行代理、玩家、结算等业务操作。",
|
||||||
|
"createSite": "去创建接入站点"
|
||||||
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadFailed": "加载失败"
|
"loadFailed": "加载失败"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,6 +72,13 @@
|
|||||||
"rotateConfirmTitle": "确认重置密钥?",
|
"rotateConfirmTitle": "确认重置密钥?",
|
||||||
"rotateConfirmDescription": "将重新生成站点 {{code}} 的 SSO 与钱包密钥,旧密钥立即失效。",
|
"rotateConfirmDescription": "将重新生成站点 {{code}} 的 SSO 与钱包密钥,旧密钥立即失效。",
|
||||||
"rotateConfirm": "确认重置",
|
"rotateConfirm": "确认重置",
|
||||||
|
"delete": "删除站点",
|
||||||
|
"deleteSuccess": "已删除站点 {{code}}",
|
||||||
|
"deleteFailed": "删除站点失败",
|
||||||
|
"deleteConfirmTitle": "确认删除站点?",
|
||||||
|
"deleteConfirmDescription": "将永久删除站点 {{code}}({{name}})及其代理链、账期、玩家与站点后台账号,此操作不可恢复。",
|
||||||
|
"deleteConfirm": "确认删除",
|
||||||
|
"deleting": "删除中…",
|
||||||
"secretsTitle": "请妥善保存密钥",
|
"secretsTitle": "请妥善保存密钥",
|
||||||
"secretsDescription": "站点 {{code}} 的密钥仅显示一次,关闭后无法再次查看完整内容。",
|
"secretsDescription": "站点 {{code}} 的密钥仅显示一次,关闭后无法再次查看完整内容。",
|
||||||
"secretsDismiss": "我已保存",
|
"secretsDismiss": "我已保存",
|
||||||
|
|||||||
@@ -156,9 +156,35 @@
|
|||||||
"riskMonitor": "风控监控",
|
"riskMonitor": "风控监控",
|
||||||
"systemSettings": "系统设置"
|
"systemSettings": "系统设置"
|
||||||
},
|
},
|
||||||
|
"site": {
|
||||||
|
"title": "站点概览",
|
||||||
|
"subtitle": "{{name}} · 本站点",
|
||||||
|
"todayBet": "今日下注",
|
||||||
|
"todayProfit": "今日盈亏",
|
||||||
|
"sevenDayTitle": "近 7 天走势",
|
||||||
|
"sevenDayProfit": "近 7 天盈亏",
|
||||||
|
"profitScopeHint": "站点口径:投注减派彩(不含占成拆分)",
|
||||||
|
"activePlayersToday": "今日活跃玩家",
|
||||||
|
"betOrdersTodayHint": "今日 {{count}} 单",
|
||||||
|
"pendingBills": "待结账单",
|
||||||
|
"pendingUnpaid": "未结合计 {{amount}}",
|
||||||
|
"latestBetAt": "最近下注 {{time}}",
|
||||||
|
"noBetToday": "今日暂时没有下注",
|
||||||
|
"scaleTitle": "站点规模",
|
||||||
|
"agentCount": "代理节点",
|
||||||
|
"playerCount": "玩家总数",
|
||||||
|
"topAgentToday": "今日投注最高代理:{{name}}({{amount}})",
|
||||||
|
"quickLinks": {
|
||||||
|
"tickets": "注单查询",
|
||||||
|
"players": "玩家管理",
|
||||||
|
"reports": "报表统计",
|
||||||
|
"agents": "代理组织",
|
||||||
|
"bills": "结算中心"
|
||||||
|
}
|
||||||
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"title": "经营概览",
|
"title": "经营概览",
|
||||||
"subtitle": "本线路数据范围 · {{name}}",
|
"subtitle": "{{name}} · 本线路",
|
||||||
"heroEyebrow": "今日经营驾驶舱",
|
"heroEyebrow": "今日经营驾驶舱",
|
||||||
"heroTitle": "{{name}} 的线路动态",
|
"heroTitle": "{{name}} 的线路动态",
|
||||||
"creditTitle": "授信额度",
|
"creditTitle": "授信额度",
|
||||||
@@ -176,6 +202,7 @@
|
|||||||
"teamPlayers": "线路玩家数",
|
"teamPlayers": "线路玩家数",
|
||||||
"activePlayersToday": "今日活跃玩家",
|
"activePlayersToday": "今日活跃玩家",
|
||||||
"betOrdersToday": "今日下注单数",
|
"betOrdersToday": "今日下注单数",
|
||||||
|
"betOrdersTodayHint": "今日 {{count}} 单",
|
||||||
"todayBet": "今日下注",
|
"todayBet": "今日下注",
|
||||||
"todayPayout": "今日派彩",
|
"todayPayout": "今日派彩",
|
||||||
"todayProfit": "今日盈亏",
|
"todayProfit": "今日盈亏",
|
||||||
@@ -202,6 +229,7 @@
|
|||||||
"yes": "是",
|
"yes": "是",
|
||||||
"no": "否",
|
"no": "否",
|
||||||
"viewBills": "查看账单",
|
"viewBills": "查看账单",
|
||||||
|
"lineMeta": "层级 {{depth}} · 可开下级 {{childAgent}} · 可开玩家 {{player}}",
|
||||||
"viewLine": "代理线路",
|
"viewLine": "代理线路",
|
||||||
"quickLinks": {
|
"quickLinks": {
|
||||||
"tickets": "注单查询",
|
"tickets": "注单查询",
|
||||||
|
|||||||
@@ -159,7 +159,6 @@
|
|||||||
"paid": "已收付",
|
"paid": "已收付",
|
||||||
"unpaid": "未结",
|
"unpaid": "未结",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"billId": "账单 ID",
|
|
||||||
"payer": "付款方",
|
"payer": "付款方",
|
||||||
"payee": "收款方",
|
"payee": "收款方",
|
||||||
"amount": "金额",
|
"amount": "金额",
|
||||||
@@ -282,7 +281,8 @@
|
|||||||
"player": "玩家账单",
|
"player": "玩家账单",
|
||||||
"agent": "代理账单",
|
"agent": "代理账单",
|
||||||
"pendingConfirm": "待确认",
|
"pendingConfirm": "待确认",
|
||||||
"awaitingPayment": "待收付"
|
"awaitingPayment": "待收付",
|
||||||
|
"all": "全部"
|
||||||
},
|
},
|
||||||
"quickFilter": {
|
"quickFilter": {
|
||||||
"title": "当前想看哪一层结算",
|
"title": "当前想看哪一层结算",
|
||||||
@@ -318,22 +318,49 @@
|
|||||||
},
|
},
|
||||||
"hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。",
|
"hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。",
|
||||||
"emptyFiltered": "当前筛选下暂无账单,请改为「全部状态」或重置筛选。",
|
"emptyFiltered": "当前筛选下暂无账单,请改为「全部状态」或重置筛选。",
|
||||||
"emptyClosed": "本期已关账但暂无账单。常见原因:账期内无信用盘玩家的已结算注单,或占成流水不在本账期时间范围内。"
|
"emptyClosed": "本期已关账但暂无账单。常见原因:账期内无信用盘玩家的已结算注单,或占成流水不在本账期时间范围内。",
|
||||||
|
"intro": "关账后生成的占成账单。可按类型或状态筛选,详情内确认或登记收付。"
|
||||||
},
|
},
|
||||||
"panels": {
|
"panels": {
|
||||||
"workbench": { "title": "工作台" },
|
"workbench": {
|
||||||
"overview": { "title": "结算概览" },
|
"title": "工作台"
|
||||||
"ledger": { "title": "账务流水" },
|
},
|
||||||
"bills": { "title": "全部账单" },
|
"overview": {
|
||||||
"creditLedger": { "title": "信用流水" },
|
"title": "结算概览"
|
||||||
"playerBills": { "title": "玩家账单" },
|
},
|
||||||
"agentBills": { "title": "代理账单" },
|
"ledger": {
|
||||||
"pendingConfirm": { "title": "待确认账单" },
|
"title": "账务流水"
|
||||||
"awaiting": { "title": "待收付账单" },
|
},
|
||||||
"payments": { "title": "收付记录" },
|
"bills": {
|
||||||
"adjustments": { "title": "调账 / 冲正" },
|
"title": "全部账单"
|
||||||
"reports": { "title": "账期报表" },
|
},
|
||||||
"badDebt": { "title": "坏账核销" }
|
"creditLedger": {
|
||||||
|
"title": "信用流水"
|
||||||
|
},
|
||||||
|
"playerBills": {
|
||||||
|
"title": "玩家账单"
|
||||||
|
},
|
||||||
|
"agentBills": {
|
||||||
|
"title": "代理账单"
|
||||||
|
},
|
||||||
|
"pendingConfirm": {
|
||||||
|
"title": "待确认账单"
|
||||||
|
},
|
||||||
|
"awaiting": {
|
||||||
|
"title": "待收付账单"
|
||||||
|
},
|
||||||
|
"payments": {
|
||||||
|
"title": "收付记录"
|
||||||
|
},
|
||||||
|
"adjustments": {
|
||||||
|
"title": "调账 / 冲正"
|
||||||
|
},
|
||||||
|
"reports": {
|
||||||
|
"title": "账期报表"
|
||||||
|
},
|
||||||
|
"badDebt": {
|
||||||
|
"title": "坏账核销"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"noSite": "请选择站点。",
|
"noSite": "请选择站点。",
|
||||||
@@ -356,5 +383,20 @@
|
|||||||
"loadAdjustments": "调账记录加载失败",
|
"loadAdjustments": "调账记录加载失败",
|
||||||
"loadBadDebt": "坏账记录加载失败",
|
"loadBadDebt": "坏账记录加载失败",
|
||||||
"loadCreditLedger": "信用流水加载失败"
|
"loadCreditLedger": "信用流水加载失败"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"subtitle": "信用盘结算",
|
||||||
|
"statusRunning": "账期进行中",
|
||||||
|
"statusIdle": "无进行中账期",
|
||||||
|
"statusCompleted": "账期已结清"
|
||||||
|
},
|
||||||
|
"subnav": {
|
||||||
|
"label": "结算中心导航"
|
||||||
|
},
|
||||||
|
"workbench": {
|
||||||
|
"viewPeriod": "账期",
|
||||||
|
"closePreset": "关账 · {{label}}",
|
||||||
|
"closeNoData": "关账失败:账期内无占成流水,请先完成信用盘开奖结算。",
|
||||||
|
"openPeriodPipeline": "开账 {{range}} · 占成流水 {{share}} 笔"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/lib/admin-session-variants.ts
Normal file
11
src/lib/admin-session-variants.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { AdminProfile } from "@/types/api/admin-auth";
|
||||||
|
|
||||||
|
/** 绑定代理节点的经营账号(非超管)。 */
|
||||||
|
export function isAgentOperator(profile: AdminProfile | null | undefined): boolean {
|
||||||
|
return profile?.agent != null && profile.is_super_admin !== true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 平台站点管理员(绑定 site_admin 角色、无代理节点)。 */
|
||||||
|
export function isSiteAdminOperator(profile: AdminProfile | null | undefined): boolean {
|
||||||
|
return profile?.site != null && profile.is_super_admin !== true;
|
||||||
|
}
|
||||||
53
src/lib/admin-signed-money.tsx
Normal file
53
src/lib/admin-signed-money.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/** 盈亏 / 输赢:负红、正绿、零灰 */
|
||||||
|
export function signedMoneyClass(amount: number, emphasize = false): string {
|
||||||
|
if (amount < 0) {
|
||||||
|
return cn("text-destructive", emphasize && "font-semibold");
|
||||||
|
}
|
||||||
|
if (amount > 0) {
|
||||||
|
return cn("text-emerald-600 dark:text-emerald-400", emphasize && "font-semibold");
|
||||||
|
}
|
||||||
|
|
||||||
|
return cn("text-muted-foreground", emphasize && "font-medium");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignedMoney({
|
||||||
|
amount,
|
||||||
|
children,
|
||||||
|
emphasize,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
amount: number;
|
||||||
|
children: ReactNode;
|
||||||
|
emphasize?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<span className={cn(signedMoneyClass(amount, emphasize), "tabular-nums", className)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 报表 / 结算字段是否应按正负着色 */
|
||||||
|
export function isSignedMoneyField(key: string): boolean {
|
||||||
|
return (
|
||||||
|
key === "approx_house_gross_minor" ||
|
||||||
|
key === "net_win_loss_minor" ||
|
||||||
|
key === "gross_win_loss" ||
|
||||||
|
key === "game_win_loss" ||
|
||||||
|
key === "profit_loss_minor" ||
|
||||||
|
key === "today_profit_minor" ||
|
||||||
|
key === "seven_day_profit_minor" ||
|
||||||
|
key === "platform_profit" ||
|
||||||
|
key === "platform_profit_minor" ||
|
||||||
|
key === "share_profit" ||
|
||||||
|
key === "share_profit_meta" ||
|
||||||
|
key.endsWith("_profit_minor")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import type { AdminRoleRow } from "@/types/api/index";
|
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_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 role.slug === PLATFORM_SUPER_ADMIN_SLUG || role.slug === PLATFORM_AGENT_SLUG;
|
return (
|
||||||
|
role.slug === PLATFORM_SUPER_ADMIN_SLUG
|
||||||
|
|| role.slug === PLATFORM_SITE_ADMIN_SLUG
|
||||||
|
|| role.slug === PLATFORM_AGENT_SLUG
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPlatformSuperAdminRole(role: Pick<AdminRoleRow, "slug">): boolean {
|
export function isPlatformSuperAdminRole(role: Pick<AdminRoleRow, "slug">): boolean {
|
||||||
|
|||||||
@@ -27,11 +27,14 @@ import type { AdminAgentLineProvisionResult } from "@/types/api/admin-agent-line
|
|||||||
|
|
||||||
type AgentLineProvisionWizardProps = {
|
type AgentLineProvisionWizardProps = {
|
||||||
embedded?: boolean;
|
embedded?: boolean;
|
||||||
|
/** 预选接入站点(如代理线路页当前选中的站点) */
|
||||||
|
defaultSiteCode?: string;
|
||||||
onSuccess?: (result: AdminAgentLineProvisionResult) => void | Promise<void>;
|
onSuccess?: (result: AdminAgentLineProvisionResult) => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AgentLineProvisionWizard({
|
export function AgentLineProvisionWizard({
|
||||||
embedded = false,
|
embedded = false,
|
||||||
|
defaultSiteCode,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}: AgentLineProvisionWizardProps): React.ReactElement {
|
}: AgentLineProvisionWizardProps): React.ReactElement {
|
||||||
const { t } = useTranslation(["agents", "common"]);
|
const { t } = useTranslation(["agents", "common"]);
|
||||||
@@ -40,7 +43,6 @@ export function AgentLineProvisionWizard({
|
|||||||
const [sites, setSites] = useState<AdminIntegrationSiteRow[]>([]);
|
const [sites, setSites] = useState<AdminIntegrationSiteRow[]>([]);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
site_code: "",
|
site_code: "",
|
||||||
code: "",
|
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -64,24 +66,22 @@ export function AgentLineProvisionWizard({
|
|||||||
[sites],
|
[sites],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useAsyncEffect(() => {
|
||||||
|
if (sitesLoading || form.site_code !== "" || !defaultSiteCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = defaultSiteCode.trim().toLowerCase();
|
||||||
|
if (unboundSites.some((row) => row.code.toLowerCase() === normalized)) {
|
||||||
|
setForm((f) => ({ ...f, site_code: normalized }));
|
||||||
|
}
|
||||||
|
}, [defaultSiteCode, form.site_code, sitesLoading, unboundSites]);
|
||||||
|
|
||||||
async function onSubmit(e: React.FormEvent): Promise<void> {
|
async function onSubmit(e: React.FormEvent): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!form.site_code.trim()) {
|
if (!form.site_code.trim()) {
|
||||||
toast.error(t("agents:lineProvision.siteRequired", { defaultValue: "请选择接入站点" }));
|
toast.error(t("agents:lineProvision.siteRequired", { defaultValue: "请选择接入站点" }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.code.trim()) {
|
|
||||||
toast.error(t("agents:lineProvision.codeRequired", { defaultValue: "请填写代理编码" }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(form.code.trim())) {
|
|
||||||
toast.error(
|
|
||||||
t("agents:lineProvision.codePatternInvalid", {
|
|
||||||
defaultValue: "代理编码仅支持字母、数字、下划线和中划线,且需以字母或数字开头",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!form.name.trim()) {
|
if (!form.name.trim()) {
|
||||||
toast.error(t("agents:nameRequired", { defaultValue: "请填写代理名称" }));
|
toast.error(t("agents:nameRequired", { defaultValue: "请填写代理名称" }));
|
||||||
return;
|
return;
|
||||||
@@ -152,7 +152,6 @@ export function AgentLineProvisionWizard({
|
|||||||
try {
|
try {
|
||||||
const result = await postAdminAgentLine({
|
const result = await postAdminAgentLine({
|
||||||
site_code: form.site_code.trim().toLowerCase(),
|
site_code: form.site_code.trim().toLowerCase(),
|
||||||
code: form.code.trim().toLowerCase(),
|
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
username: form.username.trim(),
|
username: form.username.trim(),
|
||||||
password: form.password,
|
password: form.password,
|
||||||
@@ -165,7 +164,6 @@ export function AgentLineProvisionWizard({
|
|||||||
toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" }));
|
toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" }));
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
site_code: "",
|
site_code: "",
|
||||||
code: "",
|
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -200,7 +198,7 @@ export function AgentLineProvisionWizard({
|
|||||||
<p className="mb-4 max-w-xl text-sm text-muted-foreground">
|
<p className="mb-4 max-w-xl text-sm text-muted-foreground">
|
||||||
{t("agents:lineProvision.description", {
|
{t("agents:lineProvision.description", {
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水。代理编码创建后不可修改。",
|
"将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水。",
|
||||||
})}{" "}
|
})}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/admin/config/integration-sites"
|
href="/admin/config/integration-sites"
|
||||||
@@ -247,15 +245,6 @@ export function AgentLineProvisionWizard({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>{t("agents:lineProvision.code", { defaultValue: "代理编码" })}</Label>
|
|
||||||
<Input
|
|
||||||
value={form.code}
|
|
||||||
onChange={(e) => setForm((f) => ({ ...f, code: e.target.value }))}
|
|
||||||
required
|
|
||||||
pattern="[a-z0-9][a-z0-9_-]*"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>{t("agents:lineProvision.name", { defaultValue: "一级代理名称" })}</Label>
|
<Label>{t("agents:lineProvision.name", { defaultValue: "一级代理名称" })}</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ import {
|
|||||||
AgentLineDetailPanel,
|
AgentLineDetailPanel,
|
||||||
type AgentDetailTab,
|
type AgentDetailTab,
|
||||||
} from "@/modules/agents/agent-line-detail-panel";
|
} from "@/modules/agents/agent-line-detail-panel";
|
||||||
|
import { AgentLineProvisionWizard } from "@/modules/agents/agent-line-provision-wizard";
|
||||||
import { AgentLineSidebar } from "@/modules/agents/agent-line-sidebar";
|
import { AgentLineSidebar } from "@/modules/agents/agent-line-sidebar";
|
||||||
import { AgentProfileFields } from "@/modules/agents/agent-profile-fields";
|
import { AgentProfileFields } from "@/modules/agents/agent-profile-fields";
|
||||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
|
import { AdminNoIntegrationSiteState } from "@/components/admin/admin-no-integration-site-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -47,6 +49,7 @@ import {
|
|||||||
PRD_USERS_MANAGE,
|
PRD_USERS_MANAGE,
|
||||||
} from "@/lib/admin-prd";
|
} from "@/lib/admin-prd";
|
||||||
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
|
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
|
||||||
|
import { isSiteAdminOperator } from "@/lib/admin-session-variants";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { useAgentManagementSiteStore } from "@/stores/agent-management-site";
|
import { useAgentManagementSiteStore } from "@/stores/agent-management-site";
|
||||||
import type { AgentNodeRow, AgentParentCaps, AgentProfileRow } from "@/types/api/admin-agent";
|
import type { AgentNodeRow, AgentParentCaps, AgentProfileRow } from "@/types/api/admin-agent";
|
||||||
@@ -102,7 +105,7 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
boundAgent === null &&
|
boundAgent === null &&
|
||||||
(isSuperAdmin ||
|
(isSuperAdmin ||
|
||||||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_LINE_PROVISION_ACCESS_ANY]));
|
adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_LINE_PROVISION_ACCESS_ANY]));
|
||||||
const { sites: siteOptions } = useAdminSiteCodeOptions();
|
const { sites: siteOptions, loading: sitesLoading } = useAdminSiteCodeOptions();
|
||||||
const adminSiteId = useAgentManagementSiteStore((s) => s.adminSiteId);
|
const adminSiteId = useAgentManagementSiteStore((s) => s.adminSiteId);
|
||||||
const setAdminSiteId = useAgentManagementSiteStore((s) => s.setAdminSiteId);
|
const setAdminSiteId = useAgentManagementSiteStore((s) => s.setAdminSiteId);
|
||||||
const [tree, setTree] = useState<AgentNodeRow[]>([]);
|
const [tree, setTree] = useState<AgentNodeRow[]>([]);
|
||||||
@@ -241,10 +244,17 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
[flatNodes],
|
[flatNodes],
|
||||||
);
|
);
|
||||||
const visibleAgentRows = flatNodes;
|
const visibleAgentRows = flatNodes;
|
||||||
const selectedSiteLabel = useMemo(
|
const boundSite = profile?.site ?? null;
|
||||||
() => siteOptions.find((site) => site.id === adminSiteId)?.name ?? null,
|
const selectedSiteLabel = useMemo(() => {
|
||||||
[adminSiteId, siteOptions],
|
const fromOptions = siteOptions.find((site) => site.id === adminSiteId)?.name;
|
||||||
);
|
if (fromOptions) {
|
||||||
|
return fromOptions;
|
||||||
|
}
|
||||||
|
if (boundSite != null && boundSite.id === adminSiteId) {
|
||||||
|
return boundSite.name;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [adminSiteId, boundSite, siteOptions]);
|
||||||
const activeSiteCode = useMemo(() => {
|
const activeSiteCode = useMemo(() => {
|
||||||
const fromAgent = boundAgent?.site_code?.trim();
|
const fromAgent = boundAgent?.site_code?.trim();
|
||||||
if (fromAgent) {
|
if (fromAgent) {
|
||||||
@@ -254,8 +264,11 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
if (fromSite) {
|
if (fromSite) {
|
||||||
return fromSite;
|
return fromSite;
|
||||||
}
|
}
|
||||||
|
if (boundSite != null && boundSite.id === adminSiteId) {
|
||||||
|
return boundSite.code.trim();
|
||||||
|
}
|
||||||
return flatNodes.find((node) => node.depth === 0)?.code?.trim() ?? "";
|
return flatNodes.find((node) => node.depth === 0)?.code?.trim() ?? "";
|
||||||
}, [adminSiteId, boundAgent?.site_code, flatNodes, siteOptions]);
|
}, [adminSiteId, boundAgent?.site_code, boundSite, flatNodes, siteOptions]);
|
||||||
const rootNode = useMemo(
|
const rootNode = useMemo(
|
||||||
() => flatNodes.find((node) => node.is_root || node.depth === 0) ?? null,
|
() => flatNodes.find((node) => node.is_root || node.depth === 0) ?? null,
|
||||||
[flatNodes],
|
[flatNodes],
|
||||||
@@ -319,11 +332,23 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
setAdminSiteId(profile.agent.admin_site_id);
|
setAdminSiteId(profile.agent.admin_site_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (profile?.site?.id) {
|
||||||
|
setAdminSiteId(profile.site.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (siteOptions.length > 0 && isSuperAdmin) {
|
if (siteOptions.length > 0 && isSuperAdmin) {
|
||||||
setAdminSiteId(siteOptions[0]?.id ?? null);
|
setAdminSiteId(siteOptions[0]?.id ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [adminSiteId, canViewAgents, isSuperAdmin, profile?.agent?.admin_site_id, setAdminSiteId, siteOptions]);
|
}, [
|
||||||
|
adminSiteId,
|
||||||
|
canViewAgents,
|
||||||
|
isSuperAdmin,
|
||||||
|
profile?.agent?.admin_site_id,
|
||||||
|
profile?.site?.id,
|
||||||
|
setAdminSiteId,
|
||||||
|
siteOptions,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Number.isInteger(selectedNodeIdFromUrl) || selectedNodeIdFromUrl <= 0) {
|
if (!Number.isInteger(selectedNodeIdFromUrl) || selectedNodeIdFromUrl <= 0) {
|
||||||
@@ -786,10 +811,60 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canViewAgents && loading && tree.length === 0) {
|
const hasSiteContext =
|
||||||
|
siteOptions.length > 0 ||
|
||||||
|
profile?.site != null ||
|
||||||
|
(profile?.accessible_sites?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
if (canViewAgents && profile?.agent == null && !sitesLoading && !hasSiteContext) {
|
||||||
|
return <AdminNoIntegrationSiteState canCreate={isSuperAdmin} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canViewAgents && loading && tree.length === 0 && adminSiteId !== null) {
|
||||||
return <AdminLoadingState label={t("listTitle", { defaultValue: "代理列表" })} />;
|
return <AdminLoadingState label={t("listTitle", { defaultValue: "代理列表" })} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showSiteAdminAwaitingRoot =
|
||||||
|
!loading &&
|
||||||
|
flatNodes.length === 0 &&
|
||||||
|
!canProvisionLine &&
|
||||||
|
isSiteAdminOperator(profile);
|
||||||
|
|
||||||
|
if (showSiteAdminAwaitingRoot) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-border/70 bg-card px-5 py-10 text-center shadow-sm">
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
{t("lineUi.awaitingRootAgentTitle", {
|
||||||
|
defaultValue: "本站尚未开通一级代理",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p className="mx-auto mt-2 max-w-md text-sm text-muted-foreground">
|
||||||
|
{t("lineUi.awaitingRootAgentHint", {
|
||||||
|
defaultValue:
|
||||||
|
"一级代理需由平台超级管理员在「开通一级代理」中创建。开通后您可在此管理下级代理、占成与授信。",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const showProvisionEmpty =
|
||||||
|
!loading && flatNodes.length === 0 && canProvisionLine;
|
||||||
|
|
||||||
|
if (showProvisionEmpty) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[32rem] flex-col gap-0">
|
||||||
|
<AgentLineProvisionWizard
|
||||||
|
embedded
|
||||||
|
defaultSiteCode={activeSiteCode}
|
||||||
|
onSuccess={async () => {
|
||||||
|
await loadTree(adminSiteId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[32rem] flex-col gap-0">
|
<div className="flex min-h-[32rem] flex-col gap-0">
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
@@ -861,9 +936,13 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-2xl border border-border/70 bg-card px-5 py-8 text-sm text-muted-foreground shadow-sm">
|
<div className="rounded-2xl border border-border/70 bg-card px-5 py-8 text-sm text-muted-foreground shadow-sm">
|
||||||
{t("lineUi.provisionOnlyHint", {
|
{flatNodes.length === 0
|
||||||
defaultValue: "当前账号仅可开通一级代理线路,请前往「开通一级代理」页面创建新线路。",
|
? t("lineUi.noRootAgentHint", {
|
||||||
})}
|
defaultValue: "该站点尚未开通一级代理,请联系平台管理员在「开通一级代理」中创建线路。",
|
||||||
|
})
|
||||||
|
: t("lineUi.provisionOnlyHint", {
|
||||||
|
defaultValue: "当前账号仅可开通一级代理线路,请前往「开通一级代理」页面创建新线路。",
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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 { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { formatAdminCreditMajorDecimal } from "@/lib/money";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -44,7 +45,7 @@ function formatCredit(value: number | null | undefined): string {
|
|||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }).format(value);
|
return formatAdminCreditMajorDecimal(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(status: number, t: (key: string, options?: { defaultValue?: string }) => string): string {
|
function statusLabel(status: number, t: (key: string, options?: { defaultValue?: string }) => string): string {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function AgentsSubnav(): React.ReactElement {
|
|||||||
adminHasAnyPermission(profile?.permissions, [...PRD_INTEGRATION_ACCESS_ANY]);
|
adminHasAnyPermission(profile?.permissions, [...PRD_INTEGRATION_ACCESS_ANY]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (adminSiteId !== null || siteOptions.length === 0) {
|
if (adminSiteId !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const boundSiteId = profile?.agent?.admin_site_id;
|
const boundSiteId = profile?.agent?.admin_site_id;
|
||||||
@@ -43,14 +43,26 @@ export function AgentsSubnav(): React.ReactElement {
|
|||||||
setAdminSiteId(boundSiteId);
|
setAdminSiteId(boundSiteId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAdminSiteId(siteOptions[0]?.id ?? null);
|
if (profile?.site?.id) {
|
||||||
}, [adminSiteId, profile?.agent?.admin_site_id, setAdminSiteId, siteOptions]);
|
setAdminSiteId(profile.site.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (siteOptions.length > 0) {
|
||||||
|
setAdminSiteId(siteOptions[0]?.id ?? null);
|
||||||
|
}
|
||||||
|
}, [adminSiteId, profile?.agent?.admin_site_id, profile?.site?.id, setAdminSiteId, siteOptions]);
|
||||||
|
|
||||||
const selectSiteId = adminSiteId ?? siteOptions[0]?.id ?? null;
|
const selectSiteId = adminSiteId ?? profile?.site?.id ?? siteOptions[0]?.id ?? null;
|
||||||
const selectedSite = useMemo(() => {
|
const selectedSite = useMemo(() => {
|
||||||
const site = siteOptions.find((item) => item.id === selectSiteId);
|
const site = siteOptions.find((item) => item.id === selectSiteId);
|
||||||
return site ?? null;
|
if (site) {
|
||||||
}, [selectSiteId, siteOptions]);
|
return site;
|
||||||
|
}
|
||||||
|
if (profile?.site != null && profile.site.id === selectSiteId) {
|
||||||
|
return profile.site;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [profile?.site, selectSiteId, siteOptions]);
|
||||||
|
|
||||||
const filteredSites = useMemo(() => {
|
const filteredSites = useMemo(() => {
|
||||||
const normalized = deferredKeyword.trim().toLowerCase();
|
const normalized = deferredKeyword.trim().toLowerCase();
|
||||||
@@ -61,6 +73,16 @@ export function AgentsSubnav(): React.ReactElement {
|
|||||||
return siteOptions.filter((site) => site.name.toLowerCase().includes(normalized));
|
return siteOptions.filter((site) => site.name.toLowerCase().includes(normalized));
|
||||||
}, [deferredKeyword, siteOptions]);
|
}, [deferredKeyword, siteOptions]);
|
||||||
|
|
||||||
|
const siteReadOnlyLabel =
|
||||||
|
pathname !== "/admin/agents/list" &&
|
||||||
|
!canSwitchSite &&
|
||||||
|
selectedSite != null ? (
|
||||||
|
<div className="flex h-10 min-w-[200px] items-center justify-end gap-2 rounded-md border border-border/70 bg-background px-3 text-sm">
|
||||||
|
<span className="min-w-0 truncate font-medium text-foreground">{selectedSite.name}</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">{selectedSite.code}</span>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const siteSelector =
|
const siteSelector =
|
||||||
pathname !== "/admin/agents/list" && canSwitchSite && siteOptions.length > 0 && selectSiteId !== null ? (
|
pathname !== "/admin/agents/list" && canSwitchSite && siteOptions.length > 0 && selectSiteId !== null ? (
|
||||||
<Popover open={sitePickerOpen} onOpenChange={setSitePickerOpen}>
|
<Popover open={sitePickerOpen} onOpenChange={setSitePickerOpen}>
|
||||||
@@ -129,7 +151,7 @@ export function AgentsSubnav(): React.ReactElement {
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminSubnavBar trailing={siteSelector}>
|
<AdminSubnavBar trailing={siteSelector ?? siteReadOnlyLabel}>
|
||||||
<div className="pb-1">
|
<div className="pb-1">
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="text-sm font-medium text-foreground">
|
||||||
{t("title", { defaultValue: "代理管理" })}
|
{t("title", { defaultValue: "代理管理" })}
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { BarChart3, RefreshCw, TrendingUp, Users, Wallet } from "lucide-react";
|
||||||
BarChart3,
|
|
||||||
Flame,
|
|
||||||
Landmark,
|
|
||||||
Network,
|
|
||||||
RefreshCw,
|
|
||||||
Sparkles,
|
|
||||||
Ticket,
|
|
||||||
TrendingUp,
|
|
||||||
Users,
|
|
||||||
Wallet,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { getAdminDashboard } from "@/api/admin-dashboard";
|
import { getAdminDashboard } from "@/api/admin-dashboard";
|
||||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||||
@@ -22,24 +10,18 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"
|
|||||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||||
import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
|
import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
|
||||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
|
||||||
import {
|
|
||||||
PRD_AGENT_HUB_ACCESS_ANY,
|
|
||||||
PRD_PLAYERS_ACCESS_ANY,
|
|
||||||
PRD_REPORTS_VIEW_ACCESS_ANY,
|
|
||||||
PRD_SETTLEMENT_AGENT_ACCESS_ANY,
|
|
||||||
PRD_TICKETS_ACCESS_ANY,
|
|
||||||
} from "@/lib/admin-prd";
|
|
||||||
import { normalizeAdminLanguage } from "@/i18n";
|
import { normalizeAdminLanguage } from "@/i18n";
|
||||||
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button, buttonVariants } 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";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
||||||
import { DashboardAnalyticsPanel } from "@/modules/dashboard/dashboard-analytics-panel";
|
import { DashboardAnalyticsPanel } from "@/modules/dashboard/dashboard-analytics-panel";
|
||||||
|
import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals";
|
||||||
import {
|
import {
|
||||||
formatDashboardCreditMajor,
|
formatDashboardCreditMajor,
|
||||||
formatDashboardMoneyMinor,
|
formatDashboardMoneyMinor,
|
||||||
@@ -49,14 +31,27 @@ import type { AdminDashboardAgentOverview } from "@/types/api/admin-dashboard";
|
|||||||
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
|
||||||
|
function AgentMetric({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border bg-muted/30 px-3 py-2.5">
|
||||||
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
|
<p className="mt-1 text-base font-semibold tabular-nums text-foreground">{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AgentDashboardConsole(): ReactElement {
|
export function AgentDashboardConsole(): ReactElement {
|
||||||
const { t, i18n } = useTranslation(["dashboard", "common", "agents"]);
|
const { t, i18n } = useTranslation(["dashboard", "common", "agents"]);
|
||||||
const tRef = useTranslationRef(["dashboard", "common"]);
|
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||||
const formatDt = useAdminDateTimeFormatter();
|
const formatDt = useAdminDateTimeFormatter();
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const agent = profile?.agent ?? null;
|
const agent = profile?.agent ?? null;
|
||||||
const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]);
|
|
||||||
|
|
||||||
const todayLabel = useMemo(() => {
|
const todayLabel = useMemo(() => {
|
||||||
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
||||||
const weekday = t(`date.weekdays.${adminWeekdayKeyForDate()}`, { ns: "common" });
|
const weekday = t(`date.weekdays.${adminWeekdayKeyForDate()}`, { ns: "common" });
|
||||||
@@ -118,47 +113,6 @@ export function AgentDashboardConsole(): ReactElement {
|
|||||||
const currency = "NPR";
|
const currency = "NPR";
|
||||||
const displayCurrency = overview?.currency_code ?? currency;
|
const displayCurrency = overview?.currency_code ?? currency;
|
||||||
|
|
||||||
const quickLinks = useMemo(() => {
|
|
||||||
const links: { href: string; label: string; icon: ReactElement }[] = [];
|
|
||||||
if (adminHasAnyPermission(permissions, [...PRD_TICKETS_ACCESS_ANY])) {
|
|
||||||
links.push({
|
|
||||||
href: "/admin/tickets",
|
|
||||||
label: t("agent.quickLinks.tickets"),
|
|
||||||
icon: <Ticket className="size-4" />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (adminHasAnyPermission(permissions, [...PRD_PLAYERS_ACCESS_ANY])) {
|
|
||||||
links.push({
|
|
||||||
href: "/admin/players",
|
|
||||||
label: t("agent.quickLinks.players"),
|
|
||||||
icon: <Users className="size-4" />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (adminHasAnyPermission(permissions, [...PRD_REPORTS_VIEW_ACCESS_ANY])) {
|
|
||||||
links.push({
|
|
||||||
href: "/admin/reports",
|
|
||||||
label: t("agent.quickLinks.reports"),
|
|
||||||
icon: <BarChart3 className="size-4" />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (adminHasAnyPermission(permissions, [...PRD_AGENT_HUB_ACCESS_ANY])) {
|
|
||||||
links.push({
|
|
||||||
href: "/admin/agents",
|
|
||||||
label: t("agent.quickLinks.agents"),
|
|
||||||
icon: <Network className="size-4" />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY])) {
|
|
||||||
links.push({
|
|
||||||
href: "/admin/settlement-center",
|
|
||||||
label: t("agent.quickLinks.bills"),
|
|
||||||
icon: <Wallet className="size-4" />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return links;
|
|
||||||
}, [permissions, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 w-full max-w-none flex-col gap-5">
|
<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="flex flex-wrap items-center justify-between gap-2">
|
||||||
@@ -191,270 +145,134 @@ export function AgentDashboardConsole(): ReactElement {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
<Skeleton key={i} className="h-28 rounded-xl" />
|
<Skeleton key={i} className="h-24 rounded-xl" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : overview ? (
|
) : overview ? (
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="grid min-w-0 grid-cols-1 gap-4 xl:grid-cols-[1.35fr_0.95fr]">
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
<Card className="overflow-hidden border-slate-200 bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.18),_transparent_42%),linear-gradient(135deg,_#0f172a,_#111827_55%,_#1f2937)] text-white shadow-[0_20px_60px_-25px_rgba(15,23,42,0.65)]">
|
<DashboardKpiCard
|
||||||
<CardHeader className="pb-3">
|
label={t("agent.todayBet")}
|
||||||
<div className="flex items-start justify-between gap-3">
|
value={formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
||||||
<div>
|
icon={<TrendingUp className="size-4" />}
|
||||||
<p className="text-xs uppercase tracking-[0.24em] text-emerald-200/80">
|
hint={
|
||||||
{t("agent.heroEyebrow")}
|
overview.latest_bet_at
|
||||||
</p>
|
? t("agent.latestBetAt", { time: formatDt(overview.latest_bet_at) })
|
||||||
<CardTitle className="mt-2 text-2xl font-semibold">
|
: t("agent.noBetToday")
|
||||||
{t("agent.heroTitle", { name: overview.agent_name || overview.agent_code })}
|
}
|
||||||
</CardTitle>
|
/>
|
||||||
</div>
|
<DashboardKpiCard
|
||||||
<div className="rounded-full border border-white/15 bg-white/10 p-2">
|
label={t("agent.todayShareProfit")}
|
||||||
<Sparkles className="size-4 text-emerald-200" />
|
value={formatDashboardSignedMoneyMinor(overview.today_profit_minor, displayCurrency)}
|
||||||
</div>
|
icon={<BarChart3 className="size-4" />}
|
||||||
</div>
|
hint={t("agent.shareRate", { rate: overview.total_share_rate })}
|
||||||
|
valueClassName={signedMoneyClass(overview.today_profit_minor, true)}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("agent.activePlayersToday")}
|
||||||
|
value={overview.active_player_count_today}
|
||||||
|
icon={<Users className="size-4" />}
|
||||||
|
hint={t("agent.betOrdersTodayHint", { count: overview.bet_order_count_today })}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("agent.pendingBills")}
|
||||||
|
value={overview.pending_bill_count}
|
||||||
|
icon={<Wallet className="size-4" />}
|
||||||
|
hint={t("agent.pendingUnpaid", {
|
||||||
|
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
||||||
|
})}
|
||||||
|
accent={overview.pending_bill_count > 0 ? "destructive" : "muted"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("agent.creditTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-semibold tabular-nums">
|
||||||
|
{formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{t("agent.creditAvailable", {
|
||||||
|
amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<AgentMetric
|
||||||
|
label={t("agent.creditAllocatedLabel")}
|
||||||
|
value={formatDashboardCreditMajor(overview.allocated_credit, displayCurrency)}
|
||||||
|
/>
|
||||||
|
<AgentMetric
|
||||||
|
label={t("agent.creditUsedLabel")}
|
||||||
|
value={formatDashboardCreditMajor(overview.used_credit, displayCurrency)}
|
||||||
|
/>
|
||||||
|
<AgentMetric
|
||||||
|
label={t("agent.pendingBills")}
|
||||||
|
value={String(overview.pending_bill_count)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("agent.lineMeta", {
|
||||||
|
depth: overview.depth,
|
||||||
|
childAgent: overview.can_create_child_agent ? t("agent.yes") : t("agent.no"),
|
||||||
|
player: overview.can_create_player ? t("agent.yes") : t("agent.no"),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("agent.sevenDayTitle")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-2 text-sm">
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
<span className="text-muted-foreground">{t("agent.todayBet")}</span>
|
||||||
<p className="text-xs text-slate-300">{t("agent.todayBet")}</p>
|
<span className="font-semibold tabular-nums">
|
||||||
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
{formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)}
|
||||||
{formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
|
||||||
<p className="text-xs text-slate-300">{t("agent.todayPayout")}</p>
|
|
||||||
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
|
||||||
{formatDashboardMoneyMinor(overview.today_payout_minor, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
|
||||||
<p className="text-xs text-slate-300">{t("agent.todayShareProfit")}</p>
|
|
||||||
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
|
||||||
{formatDashboardSignedMoneyMinor(overview.today_profit_minor, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="rounded-xl border border-white/10 px-4 py-3">
|
<span className="text-muted-foreground">{t("agent.todayShareProfit")}</span>
|
||||||
<p className="text-[11px] text-slate-300">{t("agent.activePlayersToday")}</p>
|
<span
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.active_player_count_today}</p>
|
className={cn(
|
||||||
</div>
|
"font-semibold tabular-nums",
|
||||||
<div className="rounded-xl border border-white/10 px-4 py-3">
|
signedMoneyClass(overview.seven_day_profit_minor, true),
|
||||||
<p className="text-[11px] text-slate-300">{t("agent.betOrdersToday")}</p>
|
)}
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.bet_order_count_today}</p>
|
>
|
||||||
</div>
|
{formatDashboardSignedMoneyMinor(overview.seven_day_profit_minor, displayCurrency)}
|
||||||
<div className="rounded-xl border border-white/10 px-4 py-3">
|
|
||||||
<p className="text-[11px] text-slate-300">{t("agent.pendingBills")}</p>
|
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-slate-300">
|
|
||||||
<span>{t("agent.shareRate", { rate: overview.total_share_rate })}</span>
|
|
||||||
<span>
|
|
||||||
{overview.latest_bet_at
|
|
||||||
? t("agent.latestBetAt", { time: formatDt(overview.latest_bet_at) })
|
|
||||||
: t("agent.noBetToday")}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="border-slate-200 bg-[linear-gradient(180deg,_rgba(248,250,252,0.98),_rgba(241,245,249,0.92))]">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="text-sm font-semibold">{t("agent.creditTitle")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-3xl font-semibold tabular-nums text-slate-900">
|
|
||||||
{formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-xs text-slate-500">
|
|
||||||
{t("agent.creditAvailable", {
|
|
||||||
amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div className="rounded-2xl border bg-white px-4 py-3">
|
|
||||||
<p className="text-xs text-slate-500">{t("agent.creditAllocatedLabel")}</p>
|
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">
|
|
||||||
{formatDashboardCreditMajor(overview.allocated_credit, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl border bg-white px-4 py-3">
|
|
||||||
<p className="text-xs text-slate-500">{t("agent.creditUsedLabel")}</p>
|
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">
|
|
||||||
{formatDashboardCreditMajor(overview.used_credit, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-500">
|
|
||||||
{t("agent.pendingUnpaid", {
|
|
||||||
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
<CardTitle className="text-sm font-semibold">{t("agent.teamTitle")}</CardTitle>
|
||||||
<TrendingUp className="size-4 text-emerald-600" />
|
|
||||||
{t("agent.sevenDayTitle")}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-1 text-sm">
|
|
||||||
<p className="text-xl font-semibold tabular-nums">
|
|
||||||
{formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("agent.sevenDayPayout", {
|
|
||||||
amount: formatDashboardMoneyMinor(overview.seven_day_payout_minor, displayCurrency),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("agent.sevenDayShareProfit", {
|
|
||||||
amount: formatDashboardSignedMoneyMinor(overview.seven_day_profit_minor, displayCurrency),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
|
||||||
<Users className="size-4 text-sky-600" />
|
|
||||||
{t("agent.teamTitle")}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<AgentMetric
|
||||||
<p className="text-xs text-muted-foreground">{t("agent.directChildren")}</p>
|
label={t("agent.directChildren")}
|
||||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_child_count}</p>
|
value={String(overview.direct_child_count)}
|
||||||
</div>
|
/>
|
||||||
<div>
|
<AgentMetric
|
||||||
<p className="text-xs text-muted-foreground">{t("agent.subtreeAgents")}</p>
|
label={t("agent.directPlayers")}
|
||||||
<p className="text-xl font-semibold tabular-nums">{overview.subtree_agent_count}</p>
|
value={String(overview.direct_player_count)}
|
||||||
</div>
|
/>
|
||||||
<div>
|
<AgentMetric
|
||||||
<p className="text-xs text-muted-foreground">{t("agent.directPlayers")}</p>
|
label={t("agent.subtreeAgents")}
|
||||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_player_count}</p>
|
value={String(overview.subtree_agent_count)}
|
||||||
</div>
|
/>
|
||||||
<div>
|
<AgentMetric
|
||||||
<p className="text-xs text-muted-foreground">{t("agent.teamPlayers")}</p>
|
label={t("agent.teamPlayers")}
|
||||||
<p className="text-xl font-semibold tabular-nums">{overview.team_player_count}</p>
|
value={String(overview.team_player_count)}
|
||||||
</div>
|
/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
|
||||||
<Wallet className="size-4 text-amber-600" />
|
|
||||||
{t("agent.pendingBills")}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-1">
|
|
||||||
<p className="text-2xl font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("agent.pendingUnpaid", {
|
|
||||||
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
|
||||||
<Flame className="size-4 text-rose-600" />
|
|
||||||
{t("agent.topMomentum")}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-1">
|
|
||||||
{overview.top_agent_today ? (
|
|
||||||
<>
|
|
||||||
<p className="text-sm font-semibold text-slate-900">
|
|
||||||
{overview.top_agent_today.agent_name || overview.top_agent_today.agent_code}
|
|
||||||
</p>
|
|
||||||
<p className="text-xl font-semibold tabular-nums">
|
|
||||||
{formatDashboardMoneyMinor(overview.top_agent_today.total_bet_minor, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("agent.topMomentumPayout", {
|
|
||||||
amount: formatDashboardMoneyMinor(
|
|
||||||
overview.top_agent_today.total_payout_minor,
|
|
||||||
displayCurrency,
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">{t("agent.noBetToday")}</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
|
||||||
<Landmark className="size-4 text-slate-700" />
|
|
||||||
{t("agent.managementFocus")}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-3 sm:grid-cols-3">
|
|
||||||
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
|
||||||
<p className="text-xs text-slate-500">{t("agent.focusBet")}</p>
|
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">
|
|
||||||
{formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
|
||||||
<p className="text-xs text-slate-500">{t("agent.focusPlayers")}</p>
|
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.active_player_count_today}</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
|
||||||
<p className="text-xs text-slate-500">{t("agent.focusBills")}</p>
|
|
||||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-semibold">{t("agent.quickStatsTitle")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3 text-sm">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground">{t("agent.canCreateChildAgent")}</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{overview.can_create_child_agent ? t("agent.yes") : t("agent.no")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground">{t("agent.canCreatePlayer")}</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{overview.can_create_player ? t("agent.yes") : t("agent.no")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground">{t("agent.lineDepth")}</span>
|
|
||||||
<span className="font-medium tabular-nums">{overview.depth}</span>
|
|
||||||
</div>
|
|
||||||
{adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? (
|
|
||||||
<Link
|
|
||||||
href="/admin/settlement-center"
|
|
||||||
className={cn(buttonVariants({ variant: "link", size: "sm" }), "h-auto px-0")}
|
|
||||||
>
|
|
||||||
{t("agent.viewBills")}
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -480,21 +298,6 @@ export function AgentDashboardConsole(): ReactElement {
|
|||||||
<AlertDescription>{t("warnings.drawPermission")}</AlertDescription>
|
<AlertDescription>{t("warnings.drawPermission")}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{quickLinks.length > 0 ? (
|
|
||||||
<section className="flex flex-wrap gap-2">
|
|
||||||
{quickLinks.map((link) => (
|
|
||||||
<Link
|
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-8 gap-1.5")}
|
|
||||||
>
|
|
||||||
{link.icon}
|
|
||||||
{link.label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals";
|
import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals";
|
||||||
import { DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config";
|
import { DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config";
|
||||||
@@ -241,6 +242,7 @@ export function DashboardAnalyticsMain({ analytics }: { analytics: DashboardAnal
|
|||||||
: t("analytics.summaryProfit")
|
: t("analytics.summaryProfit")
|
||||||
}
|
}
|
||||||
value={formatSignedMoney(summary.approx_house_gross_minor, currency)}
|
value={formatSignedMoney(summary.approx_house_gross_minor, currency)}
|
||||||
|
valueClassName={signedMoneyClass(summary.approx_house_gross_minor, true)}
|
||||||
hint={
|
hint={
|
||||||
profitScope === "share_profit"
|
profitScope === "share_profit"
|
||||||
? t("analytics.shareProfitHint")
|
? t("analytics.shareProfitHint")
|
||||||
@@ -452,7 +454,14 @@ export function DashboardAgentRankingCard({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 text-right text-xs font-semibold tabular-nums">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 text-right text-xs font-semibold tabular-nums",
|
||||||
|
rankingMetric === "profit"
|
||||||
|
? signedMoneyClass(row.approx_house_gross_minor, true)
|
||||||
|
: undefined,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{formatRowValue(row)}
|
{formatRowValue(row)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,22 +2,9 @@
|
|||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { AlertTriangle, ClipboardList, RefreshCw, Shield, Wallet } from "lucide-react";
|
||||||
AlertTriangle,
|
|
||||||
ClipboardList,
|
|
||||||
Diamond,
|
|
||||||
FileSearch,
|
|
||||||
RefreshCw,
|
|
||||||
ScrollText,
|
|
||||||
Settings,
|
|
||||||
Shield,
|
|
||||||
Ticket,
|
|
||||||
Wallet,
|
|
||||||
BarChart3,
|
|
||||||
Scale,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { getAdminDashboardByScope } from "@/api/admin-dashboard";
|
import { getAdminDashboardByScope } from "@/api/admin-dashboard";
|
||||||
import { useAdminPlayTypeCatalog } from "@/hooks/use-admin-play-type-catalog";
|
import { useAdminPlayTypeCatalog } from "@/hooks/use-admin-play-type-catalog";
|
||||||
@@ -275,23 +262,6 @@ export function DashboardConsole(): ReactElement {
|
|||||||
});
|
});
|
||||||
const showAnalytics = canFinance;
|
const showAnalytics = canFinance;
|
||||||
|
|
||||||
const quickLinks: { href: string; label: string; icon: ReactNode }[] = [
|
|
||||||
{ href: "/admin/draws", label: t("quickLinks.createDrawPlan"), icon: <Diamond className="size-4" /> },
|
|
||||||
{ href: "/admin/draws", label: t("quickLinks.drawSchedule"), icon: <Ticket className="size-4" /> },
|
|
||||||
{
|
|
||||||
href: drawId != null ? `/admin/draws/${drawId}/results` : "/admin/draws",
|
|
||||||
label: t("quickLinks.results"),
|
|
||||||
icon: <FileSearch className="size-4" />,
|
|
||||||
},
|
|
||||||
{ href: "/admin/tickets", label: t("quickLinks.tickets"), icon: <Shield className="size-4" /> },
|
|
||||||
{ href: "/admin/wallet/transactions", label: t("quickLinks.walletTransactions"), icon: <Wallet className="size-4" /> },
|
|
||||||
{ href: "/admin/audit-logs", label: t("quickLinks.auditLogs"), icon: <ScrollText className="size-4" /> },
|
|
||||||
{ href: "/admin/reports", label: t("quickLinks.reports"), icon: <BarChart3 className="size-4" /> },
|
|
||||||
{ href: "/admin/rules/odds", label: t("quickLinks.payoutRules"), icon: <Scale className="size-4" /> },
|
|
||||||
{ href: "/admin/risk", label: t("quickLinks.riskMonitor"), icon: <Shield className="size-4" /> },
|
|
||||||
{ href: "/admin/settings", label: t("quickLinks.systemSettings"), icon: <Settings className="size-4" /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 w-full max-w-none flex-col gap-5">
|
<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="flex flex-wrap items-center justify-between gap-2">
|
||||||
@@ -518,42 +488,20 @@ export function DashboardConsole(): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showAnalytics ? (
|
{!showAnalytics ? (
|
||||||
<div className="grid min-w-0 grid-cols-1 gap-4 md:grid-cols-2">
|
<Card className="admin-list-card min-w-0 py-0">
|
||||||
<Card className="admin-list-card min-w-0 py-0">
|
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">
|
||||||
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">
|
<CardTitle className="text-sm font-semibold">{t("financeStructure")}</CardTitle>
|
||||||
<CardTitle className="text-sm font-semibold">{t("financeStructure")}</CardTitle>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent className="px-4 py-4">
|
||||||
<CardContent className="px-4 py-4">
|
{loading ? (
|
||||||
{loading ? (
|
<Skeleton className="h-52 w-full" />
|
||||||
<Skeleton className="h-52 w-full" />
|
) : finance ? (
|
||||||
) : finance ? (
|
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
|
||||||
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
|
) : (
|
||||||
) : (
|
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
||||||
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
)}
|
||||||
)}
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="admin-list-card min-w-0 py-0">
|
|
||||||
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">
|
|
||||||
<CardTitle className="text-sm font-semibold">{t("quickLinksTitle")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid grid-cols-2 gap-2 px-4 py-4 sm:grid-cols-3">
|
|
||||||
{quickLinks.map((q) => (
|
|
||||||
<Link
|
|
||||||
key={q.href + q.label}
|
|
||||||
href={q.href}
|
|
||||||
className="flex min-w-0 flex-col items-center gap-2 rounded-lg border border-border/70 bg-muted/10 px-2 py-3 text-center text-[11px] font-medium leading-snug text-foreground transition-colors hover:border-primary/30 hover:bg-muted/30"
|
|
||||||
>
|
|
||||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-card text-primary shadow-sm">
|
|
||||||
{q.icon}
|
|
||||||
</span>
|
|
||||||
<span className="line-clamp-2">{q.label}</span>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -576,26 +524,6 @@ export function DashboardConsole(): ReactElement {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="admin-list-card py-0">
|
|
||||||
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">
|
|
||||||
<CardTitle className="text-sm font-semibold">{t("quickLinksTitle")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid grid-cols-2 gap-2 px-4 py-4 sm:grid-cols-3">
|
|
||||||
{quickLinks.map((q) => (
|
|
||||||
<Link
|
|
||||||
key={q.href + q.label}
|
|
||||||
href={q.href}
|
|
||||||
className="flex min-w-0 flex-col items-center gap-2 rounded-lg border border-border/70 bg-muted/10 px-2 py-3 text-center text-[11px] font-medium leading-snug text-foreground transition-colors hover:border-primary/30 hover:bg-muted/30"
|
|
||||||
>
|
|
||||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-card text-primary shadow-sm">
|
|
||||||
{q.icon}
|
|
||||||
</span>
|
|
||||||
<span className="line-clamp-2">{q.label}</span>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</aside>
|
</aside>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,19 +2,23 @@
|
|||||||
|
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
|
import { isAgentOperator, isSiteAdminOperator } 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 { 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();
|
||||||
const isAgentOperator =
|
|
||||||
profile?.agent != null && profile.is_super_admin !== true;
|
|
||||||
|
|
||||||
if (isAgentOperator) {
|
if (isAgentOperator(profile)) {
|
||||||
return <AgentDashboardConsole />;
|
return <AgentDashboardConsole />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSiteAdminOperator(profile)) {
|
||||||
|
return <SiteDashboardConsole />;
|
||||||
|
}
|
||||||
|
|
||||||
return <DashboardConsole />;
|
return <DashboardConsole />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { buildTrendChartConfig, DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config";
|
import { buildTrendChartConfig, DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config";
|
||||||
import { DashboardChartEmpty } from "@/modules/dashboard/dashboard-chart-empty";
|
import { DashboardChartEmpty } from "@/modules/dashboard/dashboard-chart-empty";
|
||||||
import type { AdminDashboardAnalyticsPlayRow } from "@/types/api/admin-dashboard-analytics";
|
import type { AdminDashboardAnalyticsPlayRow } from "@/types/api/admin-dashboard-analytics";
|
||||||
@@ -332,7 +334,14 @@ export function PeriodCompareStrip({
|
|||||||
<span className="text-sm font-medium text-foreground">{row.label}</span>
|
<span className="text-sm font-medium text-foreground">{row.label}</span>
|
||||||
<span className="text-xs tabular-nums text-muted-foreground">{row.pctText}</span>
|
<span className="text-xs tabular-nums text-muted-foreground">{row.pctText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-1 text-sm font-semibold tabular-nums">{formatMoney(row.value, currency)}</div>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-1 text-sm font-semibold tabular-nums",
|
||||||
|
row.key === "profit" ? signedMoneyClass(row.value, true) : undefined,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatMoney(row.value, currency)}
|
||||||
|
</div>
|
||||||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-[width] duration-500"
|
className="h-full rounded-full transition-[width] duration-500"
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ export function DashboardKpiCard({
|
|||||||
hint,
|
hint,
|
||||||
icon,
|
icon,
|
||||||
accent = "primary",
|
accent = "primary",
|
||||||
|
valueClassName,
|
||||||
sparklineValues,
|
sparklineValues,
|
||||||
deltaLabel,
|
deltaLabel,
|
||||||
}: {
|
}: {
|
||||||
@@ -194,6 +195,8 @@ export function DashboardKpiCard({
|
|||||||
hint?: ReactNode;
|
hint?: ReactNode;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
accent?: DashboardKpiAccent;
|
accent?: DashboardKpiAccent;
|
||||||
|
/** 覆盖主数值颜色(如盈亏红绿) */
|
||||||
|
valueClassName?: string;
|
||||||
sparklineValues?: number[];
|
sparklineValues?: number[];
|
||||||
deltaLabel?: ReactNode;
|
deltaLabel?: ReactNode;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
@@ -210,7 +213,12 @@ export function DashboardKpiCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||||
<p className="mt-1 truncate text-xl font-bold tabular-nums tracking-tight text-foreground">
|
<p
|
||||||
|
className={cn(
|
||||||
|
"mt-1 truncate text-xl font-bold tabular-nums tracking-tight",
|
||||||
|
valueClassName ?? "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
{deltaLabel ? <div className="mt-1 text-xs font-medium tabular-nums">{deltaLabel}</div> : null}
|
{deltaLabel ? <div className="mt-1 text-xs font-medium tabular-nums">{deltaLabel}</div> : null}
|
||||||
|
|||||||
255
src/modules/dashboard/site-dashboard-console.tsx
Normal file
255
src/modules/dashboard/site-dashboard-console.tsx
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { BarChart3, RefreshCw, TrendingUp, Users, Wallet } 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 { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
|
||||||
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { PRD_REPORTS_VIEW_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
|
import { normalizeAdminLanguage } from "@/i18n";
|
||||||
|
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
||||||
|
import { DashboardAnalyticsPanel } from "@/modules/dashboard/dashboard-analytics-panel";
|
||||||
|
import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals";
|
||||||
|
import {
|
||||||
|
formatDashboardMoneyMinor,
|
||||||
|
formatDashboardSignedMoneyMinor,
|
||||||
|
} from "@/modules/dashboard/use-dashboard-analytics";
|
||||||
|
import type { AdminDashboardSiteOverview } from "@/types/api/admin-dashboard";
|
||||||
|
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
||||||
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
|
||||||
|
function SiteMetric({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border bg-muted/30 px-3 py-2.5">
|
||||||
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
|
<p className="mt-1 text-base font-semibold tabular-nums text-foreground">{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SiteDashboardConsole(): ReactElement {
|
||||||
|
const { t, i18n } = useTranslation(["dashboard", "common"]);
|
||||||
|
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||||
|
const formatDt = useAdminDateTimeFormatter();
|
||||||
|
const profile = useAdminProfile();
|
||||||
|
const site = profile?.site ?? null;
|
||||||
|
const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]);
|
||||||
|
|
||||||
|
const todayLabel = useMemo(() => {
|
||||||
|
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
||||||
|
const weekday = t(`date.weekdays.${adminWeekdayKeyForDate()}`, { ns: "common" });
|
||||||
|
|
||||||
|
return formatAdminCalendarToday(locale, weekday);
|
||||||
|
}, [i18n.language, i18n.resolvedLanguage, t]);
|
||||||
|
|
||||||
|
const playOptions = useCachedPlayTypeOptions();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
|
||||||
|
const [drawId, setDrawId] = useState<number | null>(null);
|
||||||
|
const [overview, setOverview] = useState<AdminDashboardSiteOverview | null>(null);
|
||||||
|
|
||||||
|
const analyticsScope = useMemo(
|
||||||
|
() => ({
|
||||||
|
siteCode: site?.code ?? overview?.site_code ?? "",
|
||||||
|
}),
|
||||||
|
[overview?.site_code, site?.code],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canAnalytics = adminHasAnyPermission(permissions, [...PRD_REPORTS_VIEW_ACCESS_ANY]);
|
||||||
|
|
||||||
|
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_overview);
|
||||||
|
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";
|
||||||
|
|
||||||
|
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("site.title")}</h1>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
{site
|
||||||
|
? t("site.subtitle", { name: site.name || site.code })
|
||||||
|
: todayLabel}
|
||||||
|
</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 ? (
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 xl: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 xl:grid-cols-4">
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("site.todayBet")}
|
||||||
|
value={formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
||||||
|
icon={<TrendingUp className="size-4" />}
|
||||||
|
hint={
|
||||||
|
overview.latest_bet_at
|
||||||
|
? t("site.latestBetAt", { time: formatDt(overview.latest_bet_at) })
|
||||||
|
: t("site.noBetToday")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("site.todayProfit")}
|
||||||
|
value={formatDashboardSignedMoneyMinor(overview.today_profit_minor, displayCurrency)}
|
||||||
|
icon={<BarChart3 className="size-4" />}
|
||||||
|
hint={t("site.profitScopeHint")}
|
||||||
|
valueClassName={signedMoneyClass(overview.today_profit_minor, true)}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("site.activePlayersToday")}
|
||||||
|
value={overview.active_player_count_today}
|
||||||
|
icon={<Users className="size-4" />}
|
||||||
|
hint={t("site.betOrdersTodayHint", { count: overview.bet_order_count_today })}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
label={t("site.pendingBills")}
|
||||||
|
value={overview.pending_bill_count}
|
||||||
|
icon={<Wallet className="size-4" />}
|
||||||
|
hint={t("site.pendingUnpaid", {
|
||||||
|
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
||||||
|
})}
|
||||||
|
accent={overview.pending_bill_count > 0 ? "destructive" : "muted"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("site.sevenDayTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-muted-foreground">{t("site.todayBet")}</span>
|
||||||
|
<span className="font-semibold tabular-nums">
|
||||||
|
{formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-muted-foreground">{t("site.sevenDayProfit")}</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-semibold tabular-nums",
|
||||||
|
signedMoneyClass(overview.seven_day_profit_minor, true),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatDashboardSignedMoneyMinor(overview.seven_day_profit_minor, displayCurrency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("site.scaleTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<SiteMetric label={t("site.agentCount")} value={String(overview.agent_count)} />
|
||||||
|
<SiteMetric label={t("site.playerCount")} value={String(overview.player_count)} />
|
||||||
|
{overview.top_agent_today ? (
|
||||||
|
<div className="col-span-2 rounded-lg border bg-muted/20 px-3 py-2.5 text-xs text-muted-foreground">
|
||||||
|
{t("site.topAgentToday", {
|
||||||
|
name: overview.top_agent_today.agent_name || overview.top_agent_today.agent_code,
|
||||||
|
amount: formatDashboardMoneyMinor(
|
||||||
|
overview.top_agent_today.total_bet_minor,
|
||||||
|
displayCurrency,
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<DashboardCurrentDrawCard
|
||||||
|
key={`${hall?.draw_no ?? "empty"}:${loading ? "loading" : "ready"}`}
|
||||||
|
hall={hall}
|
||||||
|
drawId={drawId}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{canAnalytics ? (
|
||||||
|
<DashboardAnalyticsPanel
|
||||||
|
enabled={canAnalytics}
|
||||||
|
playOptions={playOptions}
|
||||||
|
scope={analyticsScope}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import type { AdminDrawShowData } from "@/types/api/admin-draws";
|
|||||||
import { canManageDrawResults } from "@/lib/draw-access";
|
import { canManageDrawResults } from "@/lib/draw-access";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -309,7 +310,15 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border bg-muted/20 px-3 py-2.5">
|
<div className="rounded-lg border bg-muted/20 px-3 py-2.5">
|
||||||
<p className="text-xs font-medium text-muted-foreground">{t("overviewProfitLoss")}</p>
|
<p className="text-xs font-medium text-muted-foreground">{t("overviewProfitLoss")}</p>
|
||||||
<p className="mt-1 font-mono text-sm tabular-nums">
|
<p
|
||||||
|
className={cn(
|
||||||
|
"mt-1 font-mono text-sm tabular-nums",
|
||||||
|
signedMoneyClass(
|
||||||
|
financeSummary?.approx_house_gross_minor ?? data.profit_loss_minor ?? 0,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
>
|
||||||
{formatAdminMinorUnits(
|
{formatAdminMinorUnits(
|
||||||
financeSummary?.approx_house_gross_minor ?? data.profit_loss_minor ?? 0,
|
financeSummary?.approx_house_gross_minor ?? data.profit_loss_minor ?? 0,
|
||||||
financeCurrency,
|
financeCurrency,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -140,12 +141,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">{t("grossProfit")}</span>
|
<span className="text-muted-foreground">{t("grossProfit")}</span>
|
||||||
<p
|
<p className={cn("tabular-nums font-semibold", signedMoneyClass(data.approx_house_gross_minor, true))}>
|
||||||
className={cn(
|
|
||||||
"tabular-nums font-semibold",
|
|
||||||
data.approx_house_gross_minor >= 0 ? "text-emerald-600" : "text-destructive",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatMoney(data.approx_house_gross_minor)}
|
{formatMoney(data.approx_house_gross_minor)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
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";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -441,7 +442,7 @@ export function DrawsIndexConsole() {
|
|||||||
<TableCell
|
<TableCell
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-center text-xs tabular-nums",
|
"text-center text-xs tabular-nums",
|
||||||
(row.profit_loss_minor ?? 0) < 0 ? "text-destructive" : "text-emerald-600",
|
signedMoneyClass(row.profit_loss_minor ?? 0, true),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{row.profit_loss_minor != null
|
{row.profit_loss_minor != null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Copy, Download, Link2, Pencil, ShieldAlert } from "lucide-react";
|
import { Copy, Download, Link2, Pencil, ShieldAlert, Trash2 } from "lucide-react";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||||
@@ -8,6 +8,7 @@ import { useTranslationRef } from "@/hooks/use-translation-ref";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
deleteAdminIntegrationSite,
|
||||||
getAdminIntegrationSite,
|
getAdminIntegrationSite,
|
||||||
getAdminIntegrationSiteExport,
|
getAdminIntegrationSiteExport,
|
||||||
getAdminIntegrationSites,
|
getAdminIntegrationSites,
|
||||||
@@ -243,6 +244,8 @@ export function IntegrationSitesConsole({
|
|||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
const [form, setForm] = useState<FormState>(EMPTY_FORM);
|
const [form, setForm] = useState<FormState>(EMPTY_FORM);
|
||||||
const [rotateTarget, setRotateTarget] = useState<AdminIntegrationSiteRow | null>(null);
|
const [rotateTarget, setRotateTarget] = useState<AdminIntegrationSiteRow | null>(null);
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<AdminIntegrationSiteRow | null>(null);
|
||||||
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||||
const [rotateBusy, setRotateBusy] = useState(false);
|
const [rotateBusy, setRotateBusy] = useState(false);
|
||||||
const [secretsDialog, setSecretsDialog] = useState<{
|
const [secretsDialog, setSecretsDialog] = useState<{
|
||||||
siteCode: string;
|
siteCode: string;
|
||||||
@@ -388,6 +391,25 @@ export function IntegrationSitesConsole({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmDelete(): Promise<void> {
|
||||||
|
if (!deleteTarget || !canManage) return;
|
||||||
|
|
||||||
|
setDeleteBusy(true);
|
||||||
|
try {
|
||||||
|
await deleteAdminIntegrationSite(deleteTarget.id);
|
||||||
|
toast.success(t("integrationSites.deleteSuccess", { code: deleteTarget.code }));
|
||||||
|
secretsCacheRef.current.delete(deleteTarget.id);
|
||||||
|
setDeleteTarget(null);
|
||||||
|
await load();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof LotteryApiBizError ? error.message : t("integrationSites.deleteFailed"),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setDeleteBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openConnectivity(row: AdminIntegrationSiteRow): void {
|
function openConnectivity(row: AdminIntegrationSiteRow): void {
|
||||||
setConnectivityTarget(row);
|
setConnectivityTarget(row);
|
||||||
setConnectivityPlayerId("10001");
|
setConnectivityPlayerId("10001");
|
||||||
@@ -519,7 +541,13 @@ export function IntegrationSitesConsole({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<AdminLoadingState minHeight="8rem" label={t("integrationSites.loading")} />
|
<AdminLoadingState minHeight="8rem" label={t("integrationSites.loading")} />
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<AdminNoResourceState />
|
<AdminNoResourceState message={t("integrationSites.empty")}>
|
||||||
|
{canCreate ? (
|
||||||
|
<Button type="button" size="sm" onClick={openCreate}>
|
||||||
|
{t("integrationSites.create")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</AdminNoResourceState>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -637,6 +665,14 @@ export function IntegrationSitesConsole({
|
|||||||
hidden: !canManage,
|
hidden: !canManage,
|
||||||
onClick: () => setRotateTarget(row),
|
onClick: () => setRotateTarget(row),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "delete",
|
||||||
|
label: t("integrationSites.delete"),
|
||||||
|
icon: Trash2,
|
||||||
|
destructive: true,
|
||||||
|
hidden: !canManage,
|
||||||
|
onClick: () => setDeleteTarget(row),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -850,6 +886,33 @@ export function IntegrationSitesConsole({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("integrationSites.deleteConfirmTitle")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("integrationSites.deleteConfirmDescription", {
|
||||||
|
code: deleteTarget?.code ?? "",
|
||||||
|
name: deleteTarget?.name ?? "",
|
||||||
|
})}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setDeleteTarget(null)}>
|
||||||
|
{t("integrationSites.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={deleteBusy}
|
||||||
|
onClick={() => void confirmDelete()}
|
||||||
|
>
|
||||||
|
{deleteBusy ? t("integrationSites.deleting") : t("integrationSites.deleteConfirm")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={connectivityTarget !== null}
|
open={connectivityTarget !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
|||||||
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 { 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";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -461,6 +462,10 @@ function formatPlainMoney(value: number, currencyCode: string | null | undefined
|
|||||||
return formatAdminMinorUnits(value, currencyCode || "NPR");
|
return formatAdminMinorUnits(value, currencyCode || "NPR");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signedProfitCell(amount: number, currencyCode: string | null | undefined): string {
|
||||||
|
return cn("text-center tabular-nums", signedMoneyClass(amount, true));
|
||||||
|
}
|
||||||
|
|
||||||
function formatUsagePercent(ratio: number | null | undefined): string {
|
function formatUsagePercent(ratio: number | null | undefined): string {
|
||||||
return ratio == null ? "-" : `${Math.round(ratio * 100)}%`;
|
return ratio == null ? "-" : `${Math.round(ratio * 100)}%`;
|
||||||
}
|
}
|
||||||
@@ -906,6 +911,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
payload.items.reduce((sum, item) => sum - item.net_win_loss_minor, 0),
|
payload.items.reduce((sum, item) => sum - item.net_win_loss_minor, 0),
|
||||||
"NPR",
|
"NPR",
|
||||||
),
|
),
|
||||||
|
tone: (() => {
|
||||||
|
const houseGross = payload.items.reduce((sum, item) => sum - item.net_win_loss_minor, 0);
|
||||||
|
return houseGross >= 0 ? "good" : "bad";
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
{ label: t("preview.stats.players"), value: String(new Set(payload.items.map((item) => item.player_id)).size) },
|
{ label: t("preview.stats.players"), value: String(new Set(payload.items.map((item) => item.player_id)).size) },
|
||||||
],
|
],
|
||||||
@@ -1445,7 +1454,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
<TableCell className="text-center">{summary.ticket_item_count}</TableCell>
|
<TableCell className="text-center">{summary.ticket_item_count}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(summary.total_bet_minor, summary.currency_code)}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(summary.total_bet_minor, summary.currency_code)}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(summary.total_payout_minor, summary.currency_code)}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(summary.total_payout_minor, summary.currency_code)}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(summary.approx_house_gross_minor, summary.currency_code)}</TableCell>
|
<TableCell className={signedProfitCell(summary.approx_house_gross_minor, summary.currency_code)}>
|
||||||
|
{formatPlainMoney(summary.approx_house_gross_minor, summary.currency_code)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>{summary.settlement_batches.length}</TableCell>
|
<TableCell>{summary.settlement_batches.length}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{summary.settlement_batches.map((batch) => (
|
{summary.settlement_batches.map((batch) => (
|
||||||
@@ -1530,7 +1541,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.approx_house_gross_minor, "NPR")}</TableCell>
|
<TableCell className={signedProfitCell(item.approx_house_gross_minor, "NPR")}>
|
||||||
|
{formatPlainMoney(item.approx_house_gross_minor, "NPR")}
|
||||||
|
</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
@@ -1548,7 +1561,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.net_win_loss_minor, "NPR")}</TableCell>
|
<TableCell className={signedProfitCell(item.net_win_loss_minor, "NPR")}>
|
||||||
|
{formatPlainMoney(item.net_win_loss_minor, "NPR")}
|
||||||
|
</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
@@ -1563,7 +1578,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
|||||||
<TableCell>{item.dimension}D</TableCell>
|
<TableCell>{item.dimension}D</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-center">{formatPlainMoney(item.approx_house_gross_minor, "NPR")}</TableCell>
|
<TableCell className={signedProfitCell(item.approx_house_gross_minor, "NPR")}>
|
||||||
|
{formatPlainMoney(item.approx_house_gross_minor, "NPR")}
|
||||||
|
</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { SignedMoney } from "@/lib/admin-signed-money";
|
||||||
import { formatDashboardCreditMajor, formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
import { formatDashboardCreditMajor, formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||||
|
import { formatSignedSettlementMoney } from "@/modules/settlement/settlement-signed-money";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -165,15 +167,19 @@ export function AgentSettlementReportView({
|
|||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const stats = [
|
const stats: { label: string; amount: number; signed?: boolean }[] = [
|
||||||
{ label: t("settlementReports.platformPnl.billNet", { defaultValue: "平台账单净额" }), value: money(root.platform_bill_net, currencyCode) },
|
{
|
||||||
|
label: t("settlementReports.platformPnl.billNet", { defaultValue: "平台账单净额" }),
|
||||||
|
amount: Number(root.platform_bill_net ?? 0),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("settlementReports.platformPnl.rounding", { defaultValue: "尾差调整" }),
|
label: t("settlementReports.platformPnl.rounding", { defaultValue: "尾差调整" }),
|
||||||
value: money(root.platform_rounding_adjustment, currencyCode),
|
amount: Number(root.platform_rounding_adjustment ?? 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("settlementReports.platformPnl.shareProfit", { defaultValue: "占成利润(元数据)" }),
|
label: t("settlementReports.platformPnl.shareProfit", { defaultValue: "占成利润(元数据)" }),
|
||||||
value: money(root.share_profit_meta, currencyCode),
|
amount: Number(root.share_profit_meta ?? 0),
|
||||||
|
signed: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
@@ -181,7 +187,15 @@ export function AgentSettlementReportView({
|
|||||||
{stats.map((item) => (
|
{stats.map((item) => (
|
||||||
<div key={item.label} className="rounded-md border border-border/60 px-3 py-2">
|
<div key={item.label} className="rounded-md border border-border/60 px-3 py-2">
|
||||||
<div className="text-xs text-muted-foreground">{item.label}</div>
|
<div className="text-xs text-muted-foreground">{item.label}</div>
|
||||||
<div className="mt-1 text-sm font-semibold tabular-nums">{item.value}</div>
|
<div className="mt-1 text-sm font-semibold tabular-nums">
|
||||||
|
{item.signed ? (
|
||||||
|
<SignedMoney amount={item.amount} emphasize>
|
||||||
|
{formatSignedSettlementMoney(item.amount, currencyCode)}
|
||||||
|
</SignedMoney>
|
||||||
|
) : (
|
||||||
|
money(item.amount, currencyCode)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -190,16 +204,16 @@ export function AgentSettlementReportView({
|
|||||||
|
|
||||||
const items = asRows(root?.items ?? (reportType === "player_win_loss" || reportType === "agent_share" || reportType === "unpaid_bills" || reportType === "overdue" || reportType === "draw_period" ? data : null));
|
const items = asRows(root?.items ?? (reportType === "player_win_loss" || reportType === "agent_share" || reportType === "unpaid_bills" || reportType === "overdue" || reportType === "draw_period" ? data : null));
|
||||||
|
|
||||||
const columnSets: Record<string, { key: string; header: string; money?: boolean }[]> = {
|
const columnSets: Record<string, { key: string; header: string; money?: boolean; signed?: boolean }[]> = {
|
||||||
player_win_loss: [
|
player_win_loss: [
|
||||||
{ key: "username", header: t("settlementReports.columns.player", { defaultValue: "玩家" }) },
|
{ key: "username", header: t("settlementReports.columns.player", { defaultValue: "玩家" }) },
|
||||||
{ key: "game_type", header: t("settlementReports.columns.gameType", { defaultValue: "玩法" }) },
|
{ key: "game_type", header: t("settlementReports.columns.gameType", { defaultValue: "玩法" }) },
|
||||||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true },
|
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true, signed: true },
|
||||||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||||||
],
|
],
|
||||||
agent_share: [
|
agent_share: [
|
||||||
{ key: "agent_node_id", header: t("settlementReports.columns.agentId", { defaultValue: "代理 ID" }) },
|
{ key: "agent_node_id", header: t("settlementReports.columns.agentId", { defaultValue: "代理 ID" }) },
|
||||||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true },
|
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true, signed: true },
|
||||||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||||||
{ key: "entry_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
{ key: "entry_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
||||||
],
|
],
|
||||||
@@ -216,7 +230,7 @@ export function AgentSettlementReportView({
|
|||||||
],
|
],
|
||||||
draw_period: [
|
draw_period: [
|
||||||
{ key: "draw_no", header: t("settlementReports.columns.drawNo", { defaultValue: "期号" }) },
|
{ key: "draw_no", header: t("settlementReports.columns.drawNo", { defaultValue: "期号" }) },
|
||||||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true },
|
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true, signed: true },
|
||||||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||||||
{ key: "ticket_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
{ key: "ticket_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
||||||
],
|
],
|
||||||
@@ -238,7 +252,7 @@ function ReportTable({
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
}: {
|
}: {
|
||||||
rows: Record<string, unknown>[];
|
rows: Record<string, unknown>[];
|
||||||
columns: { key: string; header: string; money?: boolean; creditMajor?: boolean }[];
|
columns: { key: string; header: string; money?: boolean; signed?: boolean; creditMajor?: boolean }[];
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
@@ -272,9 +286,15 @@ function ReportTable({
|
|||||||
>
|
>
|
||||||
{col.creditMajor
|
{col.creditMajor
|
||||||
? creditMoney(row[col.key], currencyCode)
|
? creditMoney(row[col.key], currencyCode)
|
||||||
: col.money
|
: col.money && col.signed
|
||||||
? money(row[col.key], currencyCode)
|
? (
|
||||||
: String(row[col.key] ?? "—")}
|
<SignedMoney amount={Number(row[col.key] ?? 0)} emphasize>
|
||||||
|
{formatSignedSettlementMoney(Number(row[col.key] ?? 0), currencyCode)}
|
||||||
|
</SignedMoney>
|
||||||
|
)
|
||||||
|
: col.money
|
||||||
|
? money(row[col.key], currencyCode)
|
||||||
|
: String(row[col.key] ?? "—")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
|||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "@/lib/admin-prd";
|
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
@@ -273,12 +274,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">{t("platformProfit")}</span>{" "}
|
<span className="text-muted-foreground">{t("platformProfit")}</span>{" "}
|
||||||
<span
|
<span className={cn("font-mono tabular-nums", signedMoneyClass(summary.platform_profit, true))}>
|
||||||
className={cn(
|
|
||||||
"font-mono tabular-nums",
|
|
||||||
summary.platform_profit < 0 ? "text-destructive" : "text-emerald-700",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatAdminMinorUnits(summary.platform_profit, summary.currency_code ?? "NPR")}
|
{formatAdminMinorUnits(summary.platform_profit, summary.currency_code ?? "NPR")}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/c
|
|||||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
|
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "@/lib/admin-prd";
|
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
@@ -270,7 +271,7 @@ export function SettlementBatchesConsole() {
|
|||||||
<TableCell
|
<TableCell
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-center font-mono text-xs tabular-nums",
|
"text-center font-mono text-xs tabular-nums",
|
||||||
row.platform_profit < 0 ? "text-destructive" : "text-emerald-700",
|
signedMoneyClass(row.platform_profit, true),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatAdminMinorUnits(row.platform_profit, row.currency_code ?? "NPR")}
|
{formatAdminMinorUnits(row.platform_profit, row.currency_code ?? "NPR")}
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ 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 { 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";
|
||||||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
import {
|
||||||
|
formatSignedSettlementMoney,
|
||||||
|
} from "@/modules/settlement/settlement-signed-money";
|
||||||
import {
|
import {
|
||||||
describeBillPaymentDirection,
|
describeBillPaymentDirection,
|
||||||
} from "@/modules/settlement/settlement-bill-display";
|
} from "@/modules/settlement/settlement-bill-display";
|
||||||
@@ -76,26 +79,6 @@ function billTypeTone(row: SettlementBillRow): string {
|
|||||||
return "border-border/70 bg-muted/25 text-muted-foreground";
|
return "border-border/70 bg-muted/25 text-muted-foreground";
|
||||||
}
|
}
|
||||||
|
|
||||||
function signedMoneyClass(amount: number, emphasize = false): string {
|
|
||||||
if (amount < 0) {
|
|
||||||
return cn("text-destructive", emphasize && "font-medium");
|
|
||||||
}
|
|
||||||
if (amount > 0) {
|
|
||||||
return cn("text-emerald-700", emphasize && "font-medium");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "text-muted-foreground";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSignedMoney(amount: number, currencyCode: string): string {
|
|
||||||
if (amount === 0) {
|
|
||||||
return formatDashboardMoneyMinor(0, currencyCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = amount < 0 ? "−" : "+";
|
|
||||||
return `${prefix}${formatDashboardMoneyMinor(Math.abs(amount), currencyCode)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unpaidMoneyClass(row: SettlementBillRow): string {
|
function unpaidMoneyClass(row: SettlementBillRow): string {
|
||||||
if (row.unpaid_amount <= 0) {
|
if (row.unpaid_amount <= 0) {
|
||||||
return "text-muted-foreground";
|
return "text-muted-foreground";
|
||||||
@@ -253,7 +236,7 @@ export function SettlementBillsTable({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{row.gross_win_loss != null ? (
|
{row.gross_win_loss != null ? (
|
||||||
<div>{formatSignedMoney(row.gross_win_loss, currencyCode)}</div>
|
<div>{formatSignedSettlementMoney(row.gross_win_loss, currencyCode)}</div>
|
||||||
) : (
|
) : (
|
||||||
"—"
|
"—"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { toast } from "sonner";
|
|||||||
import { getSettlementPeriods, type SettlementPeriodRow } from "@/api/admin-agent-settlement";
|
import { getSettlementPeriods, type SettlementPeriodRow } from "@/api/admin-agent-settlement";
|
||||||
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
|
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
|
||||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
|
import { AdminNoIntegrationSiteState } from "@/components/admin/admin-no-integration-site-state";
|
||||||
import { AgentBillDetail } from "@/modules/settlement/agent-bill-detail";
|
import { AgentBillDetail } from "@/modules/settlement/agent-bill-detail";
|
||||||
import { SettlementCenterPeriodDetail } from "@/modules/settlement/settlement-center-period-detail";
|
import { SettlementCenterPeriodDetail } from "@/modules/settlement/settlement-center-period-detail";
|
||||||
import {
|
import {
|
||||||
@@ -284,7 +285,9 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
|
|
||||||
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">
|
||||||
{siteId === null ? (
|
{siteId === null && siteOptions.length === 0 && boundAgent === null ? (
|
||||||
|
<AdminNoIntegrationSiteState canCreate={profile?.is_super_admin === true} />
|
||||||
|
) : siteId === null ? (
|
||||||
<p className="text-sm text-muted-foreground">{t("empty.noSite", { defaultValue: "请选择站点。" })}</p>
|
<p className="text-sm text-muted-foreground">{t("empty.noSite", { defaultValue: "请选择站点。" })}</p>
|
||||||
) : !periodsReady ? (
|
) : !periodsReady ? (
|
||||||
<AdminLoadingState />
|
<AdminLoadingState />
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
/** 结算金额正负着色:负红、正绿、零灰 */
|
export { signedMoneyClass as signedSettlementMoneyClass } from "@/lib/admin-signed-money";
|
||||||
export function signedSettlementMoneyClass(amount: number, emphasize = false): string {
|
|
||||||
if (amount < 0) {
|
|
||||||
return cn("text-destructive", emphasize && "font-medium");
|
|
||||||
}
|
|
||||||
if (amount > 0) {
|
|
||||||
return cn("text-emerald-700 dark:text-emerald-400", emphasize && "font-medium");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "text-muted-foreground";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatSignedSettlementMoney(amount: number, currencyCode: string): string {
|
export function formatSignedSettlementMoney(amount: number, currencyCode: string): string {
|
||||||
if (amount === 0) {
|
if (amount === 0) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export type AdminAgentLineProvisionPayload = {
|
export type AdminAgentLineProvisionPayload = {
|
||||||
site_code: string;
|
site_code: string;
|
||||||
code: string;
|
code?: string;
|
||||||
name: string;
|
name: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export type AdminProfile = {
|
|||||||
delegation_ceiling?: string[];
|
delegation_ceiling?: string[];
|
||||||
/** 平台账号可访问站点;代理账号为 undefined,见 agent.site_code */
|
/** 平台账号可访问站点;代理账号为 undefined,见 agent.site_code */
|
||||||
accessible_sites?: { id: number; code: string; name: string }[];
|
accessible_sites?: { id: number; code: string; name: string }[];
|
||||||
|
/** 站点管理员主站点上下文;代理/超管为 null */
|
||||||
|
site?: { id: number; code: string; name: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** `POST /api/v1/admin/auth/login` 成功信封内的 `data` */
|
/** `POST /api/v1/admin/auth/login` 成功信封内的 `data` */
|
||||||
|
|||||||
@@ -49,6 +49,36 @@ export type AdminDashboardCapabilities = {
|
|||||||
wallet_transfer_view: boolean;
|
wallet_transfer_view: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 站点管理员首页摘要(`GET /api/v1/admin/dashboard` → `site_overview`) */
|
||||||
|
export type AdminDashboardSiteOverview = {
|
||||||
|
admin_site_id: number;
|
||||||
|
site_code: string;
|
||||||
|
site_name: string;
|
||||||
|
agent_count: number;
|
||||||
|
player_count: number;
|
||||||
|
active_player_count_today: number;
|
||||||
|
bet_order_count_today: number;
|
||||||
|
today_bet_minor: number;
|
||||||
|
today_payout_minor: number;
|
||||||
|
today_profit_minor: number;
|
||||||
|
seven_day_bet_minor: number;
|
||||||
|
seven_day_payout_minor: number;
|
||||||
|
seven_day_profit_minor: number;
|
||||||
|
profit_scope?: "house_gross";
|
||||||
|
currency_code: string | null;
|
||||||
|
pending_bill_count: number;
|
||||||
|
pending_unpaid_minor: number;
|
||||||
|
latest_bet_at: string | null;
|
||||||
|
top_agent_today: {
|
||||||
|
agent_node_id: number;
|
||||||
|
agent_code: string;
|
||||||
|
agent_name: string;
|
||||||
|
total_bet_minor: number;
|
||||||
|
total_payout_minor: number;
|
||||||
|
approx_house_gross_minor: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
/** 代理经营账号首页摘要(`GET /api/v1/admin/dashboard` → `agent_overview`) */
|
/** 代理经营账号首页摘要(`GET /api/v1/admin/dashboard` → `agent_overview`) */
|
||||||
export type AdminDashboardAgentOverview = {
|
export type AdminDashboardAgentOverview = {
|
||||||
agent_node_id: number;
|
agent_node_id: number;
|
||||||
@@ -147,4 +177,5 @@ export type AdminDashboardData = {
|
|||||||
warnings: AdminDashboardWarning[];
|
warnings: AdminDashboardWarning[];
|
||||||
capabilities: AdminDashboardCapabilities;
|
capabilities: AdminDashboardCapabilities;
|
||||||
agent_overview: AdminDashboardAgentOverview | null;
|
agent_overview: AdminDashboardAgentOverview | null;
|
||||||
|
site_overview: AdminDashboardSiteOverview | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type AdminIntegrationSiteRow = {
|
|||||||
has_wallet_api_key: boolean;
|
has_wallet_api_key: boolean;
|
||||||
sso_secret_masked: string | null;
|
sso_secret_masked: string | null;
|
||||||
wallet_api_key_masked: string | null;
|
wallet_api_key_masked: string | null;
|
||||||
|
is_default: boolean;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user