From 641c87ff503357fed802358c003017540fe2afe0 Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 15 Jun 2026 17:21:50 +0800 Subject: [PATCH] feat(docs, agents, risk): enhance documentation, API queries, and UI components Updated the public documentation site with improved layout and accessibility, including new sections for client integration and admin guides. Enhanced API queries by adding 'active_only' and 'group_by' parameters for better data filtering in risk management. Refined UI components for agent management, ensuring consistent styling and improved user experience across the application. Added localization support for new documentation content in English and Nepali. --- AGENTS.md | 10 +- src/api/admin-risk.ts | 6 + src/api/admin-ticket-detail.ts | 10 + src/app/admin/(shell)/agents/layout.tsx | 2 +- src/app/admin/(shell)/agents/list/page.tsx | 2 +- .../draws/[drawId]/risk/pools/page.tsx | 6 +- .../admin/(shell)/tickets/[ticketNo]/page.tsx | 22 + src/app/docs/admin/agents/page.tsx | 5 + src/app/docs/admin/config/page.tsx | 5 + src/app/docs/admin/draws/page.tsx | 5 + src/app/docs/admin/faq/page.tsx | 5 + src/app/docs/admin/fund-operations/page.tsx | 5 + src/app/docs/admin/layout.tsx | 13 + src/app/docs/admin/manual-review/page.tsx | 5 + src/app/docs/admin/page.tsx | 5 + src/app/docs/admin/players/page.tsx | 5 + src/app/docs/admin/reports/page.tsx | 5 + src/app/docs/admin/roles/page.tsx | 5 + src/app/docs/admin/settlement-center/page.tsx | 5 + src/app/docs/admin/site-setup/page.tsx | 5 + src/app/docs/admin/tickets/page.tsx | 5 + src/app/docs/admin/wallet/page.tsx | 5 + src/app/docs/integration/admin-guide/page.tsx | 5 + .../docs/integration/api-reference/page.tsx | 6 + src/app/docs/integration/delivery/page.tsx | 5 + .../docs/integration/troubleshooting/page.tsx | 5 + src/app/docs/layout.tsx | 6 +- src/app/globals.css | 18 + src/components/admin/admin-breadcrumb.tsx | 2 +- src/components/admin/admin-page-guide.tsx | 37 + src/components/admin/admin-shell.tsx | 2 +- src/components/admin/module-scaffold.tsx | 2 +- src/components/admin/toolbar.tsx | 8 + src/components/docs/doc-code.tsx | 13 +- src/components/docs/doc-ui.tsx | 78 ++- .../docs/docs-admin-console-link.tsx | 31 + src/components/docs/docs-shell.tsx | 38 +- src/components/docs/docs-sidebar.tsx | 36 +- src/components/docs/docs-top-nav.tsx | 54 ++ src/hooks/use-export-labels.ts | 1 + src/i18n/index.ts | 8 +- src/i18n/locales/en/adminDocs.json | 636 +++++++++++++++++ src/i18n/locales/en/agents.json | 2 + src/i18n/locales/en/common.json | 5 + src/i18n/locales/en/config.json | 1 + src/i18n/locales/en/draws.json | 1 + src/i18n/locales/en/integrationDocs.json | 612 ++++++++++------- src/i18n/locales/en/players.json | 1 + src/i18n/locales/en/reports.json | 1 + src/i18n/locales/en/risk.json | 12 + src/i18n/locales/en/settlementCenter.json | 1 + src/i18n/locales/en/tickets.json | 13 +- src/i18n/locales/ne/adminDocs.json | 636 +++++++++++++++++ src/i18n/locales/ne/agents.json | 1 + src/i18n/locales/ne/common.json | 4 + src/i18n/locales/ne/config.json | 1 + src/i18n/locales/ne/draws.json | 1 + src/i18n/locales/ne/integrationDocs.json | 566 +++++++++------- src/i18n/locales/ne/players.json | 1 + src/i18n/locales/ne/reports.json | 1 + src/i18n/locales/ne/risk.json | 12 + src/i18n/locales/ne/settlementCenter.json | 1 + src/i18n/locales/ne/tickets.json | 13 +- src/i18n/locales/zh/adminDocs.json | 637 ++++++++++++++++++ src/i18n/locales/zh/adminUsers.json | 2 +- src/i18n/locales/zh/agents.json | 2 + src/i18n/locales/zh/common.json | 5 + src/i18n/locales/zh/config.json | 1 + src/i18n/locales/zh/draws.json | 1 + src/i18n/locales/zh/integrationDocs.json | 442 +++++++----- src/i18n/locales/zh/players.json | 1 + src/i18n/locales/zh/reports.json | 1 + src/i18n/locales/zh/risk.json | 12 + src/i18n/locales/zh/settlementCenter.json | 1 + src/i18n/locales/zh/tickets.json | 13 +- src/lib/admin-doc-links.ts | 11 + src/lib/admin-docs-nav.ts | 44 ++ src/lib/admin-page-title.ts | 11 +- src/lib/agent-profile-caps.ts | 26 + src/lib/docs-nav-labels.ts | 23 +- src/lib/docs-nav.ts | 6 +- src/lib/highlight-code.ts | 4 +- .../admin-roles/admin-roles-console.tsx | 53 +- .../agents/agent-line-detail-panel.tsx | 89 ++- src/modules/agents/agent-line-sidebar.tsx | 2 +- src/modules/agents/agent-profile-fields.tsx | 25 +- src/modules/agents/agents-console.tsx | 9 +- src/modules/docs/admin/admin-doc-screens.tsx | 418 ++++++++++++ src/modules/docs/admin/use-admin-doc.ts | 46 ++ .../docs/integration/api-reference-screen.tsx | 373 ++++++++++ .../docs/integration/integration-doc-data.ts | 68 +- .../integration/integration-doc-screens.tsx | 81 ++- .../docs/integration/use-integration-doc.ts | 27 +- src/modules/draws/draws-index-console.tsx | 3 + .../integration/integration-sites-console.tsx | 7 + src/modules/players/players-console.tsx | 3 + src/modules/reports/report-jobs-panel.tsx | 2 +- src/modules/reports/reports-console.tsx | 3 + src/modules/risk/risk-index-console.tsx | 2 +- src/modules/risk/risk-lock-logs-console.tsx | 201 ++++-- src/modules/risk/risk-pools-console.tsx | 16 +- .../settlement/settlement-center-shell.tsx | 56 +- .../settlement-credit-ledger-panel.tsx | 2 +- .../tickets/player-tickets-console.tsx | 16 +- src/modules/tickets/ticket-detail-console.tsx | 230 +++++++ src/types/api/admin-risk.ts | 18 +- src/types/api/admin-tickets.ts | 46 ++ 107 files changed, 5114 insertions(+), 943 deletions(-) create mode 100644 src/api/admin-ticket-detail.ts create mode 100644 src/app/admin/(shell)/tickets/[ticketNo]/page.tsx create mode 100644 src/app/docs/admin/agents/page.tsx create mode 100644 src/app/docs/admin/config/page.tsx create mode 100644 src/app/docs/admin/draws/page.tsx create mode 100644 src/app/docs/admin/faq/page.tsx create mode 100644 src/app/docs/admin/fund-operations/page.tsx create mode 100644 src/app/docs/admin/layout.tsx create mode 100644 src/app/docs/admin/manual-review/page.tsx create mode 100644 src/app/docs/admin/page.tsx create mode 100644 src/app/docs/admin/players/page.tsx create mode 100644 src/app/docs/admin/reports/page.tsx create mode 100644 src/app/docs/admin/roles/page.tsx create mode 100644 src/app/docs/admin/settlement-center/page.tsx create mode 100644 src/app/docs/admin/site-setup/page.tsx create mode 100644 src/app/docs/admin/tickets/page.tsx create mode 100644 src/app/docs/admin/wallet/page.tsx create mode 100644 src/app/docs/integration/admin-guide/page.tsx create mode 100644 src/app/docs/integration/api-reference/page.tsx create mode 100644 src/app/docs/integration/delivery/page.tsx create mode 100644 src/app/docs/integration/troubleshooting/page.tsx create mode 100644 src/components/admin/admin-page-guide.tsx create mode 100644 src/components/docs/docs-admin-console-link.tsx create mode 100644 src/components/docs/docs-top-nav.tsx create mode 100644 src/i18n/locales/en/adminDocs.json create mode 100644 src/i18n/locales/ne/adminDocs.json create mode 100644 src/i18n/locales/zh/adminDocs.json create mode 100644 src/lib/admin-doc-links.ts create mode 100644 src/lib/admin-docs-nav.ts create mode 100644 src/modules/docs/admin/admin-doc-screens.tsx create mode 100644 src/modules/docs/admin/use-admin-doc.ts create mode 100644 src/modules/docs/integration/api-reference-screen.tsx create mode 100644 src/modules/tickets/ticket-detail-console.tsx diff --git a/AGENTS.md b/AGENTS.md index 6214b9a..240fdcf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,8 @@ This version has breaking changes — APIs, conventions, and file structure may ## Learned User Preferences - 占成/授信/回水/上限等数值字段用 `AdminNumericStepper`(± 步进 + 可手输),勿单独裸 `input[type=number]`。 +- 对外文档(接入 + 后台运营手册)禁用 RBAC slug(如 `prd.settlement.agent.manage`);对客户称「贵司」;排版忌 AI 感,正文对比度与字号可读优先。 +- 文档 i18n:`useTranslation` 须显式 `ns`;`returnObjects` 列表用 `Array.isArray` 守卫;避免节名与表头 key 冲突(如 `billStatus`)。 ## Learned Workspace Facts @@ -39,5 +41,11 @@ This version has breaking changes — APIs, conventions, and file structure may - 超管判定用登录态 `is_super_admin`,勿用站点角色或 `admin_user_site_roles` 绑定推断。 - 站点管理员(`profile.site != null`)代理 UI 绕过选中代理的 `can_create_*` 门控,按自身 manage 权限展示 Tab/操作。 - 站点管理员在代理下创建玩家须传 `agent_node_id`(与超管同逻辑),勿默认挂根代理。 -- 客户接入文档公开站:`/docs`(首页)、`/docs/integration`(接入正文),无需 `/admin` 登录;旧 `/admin/docs/integration-guide` 重定向至此。 +- 客户对外文档:`/docs`(首页)、`/docs/integration`(API 接入)、`/docs/admin`(后台运营手册),均公开免 `/admin` 登录;读者为接入方技术与站点运营/代理,非内部运维;旧 `/admin/docs/integration-guide` 重定向 `/docs/integration`;顶栏「管理后台」链 `/admin`(勿 `/admin/login`)。 - `SettlementBillRow` 无 `currency_code`;账单金额展示用玩家 `default_currency`。 +- 浏览器 `/api/v1/*` 由 Next 转发到 `LOTTERY_API_UPSTREAM`;与本地 Postgres 对账前须确认 upstream 与所查库一致。 +- 接入文档页静态内容校验用 `document.body.innerText`;`DocCode` 非裸 `
/`,勿只查 code 选择器。
+- Docs 侧栏粘性定位用 CSS 变量 `--docs-sticky-top`(含顶栏/header 偏移)。
+- Tanumo 联调/生产默认:H5 `https://front.tanumo.com`、API `https://lotterylaravel.tanumo.com`、管理端/文档 `https://lotteryadmin.tanumo.com`。
+- 接入文档 SSO:无「登录换票」;主站 JWT 直传 H5/iframe,`GET /api/v1/player/me` 自动建档;iframe 约定 token 在顶层 `data.token`;勿引用 main-site/monorepo 等内部仓库路径。
+- 风控页默认:占用流水按注单聚合;风险池仅显示有占用/高风险;组合明细放注单详情二级页。
diff --git a/src/api/admin-risk.ts b/src/api/admin-risk.ts
index e506e88..b329bad 100644
--- a/src/api/admin-risk.ts
+++ b/src/api/admin-risk.ts
@@ -15,6 +15,7 @@ export type AdminRiskPoolListQuery = {
   per_page?: number;
   sold_out_only?: boolean;
   high_risk_only?: boolean;
+  active_only?: boolean;
   normalized_number?: string;
   sort?: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc";
 };
@@ -29,6 +30,7 @@ export async function getAdminRiskPools(
       per_page: q.per_page,
       sold_out_only: q.sold_out_only === true ? 1 : undefined,
       high_risk_only: q.high_risk_only === true ? 1 : undefined,
+      active_only: q.active_only === true ? 1 : undefined,
       normalized_number: q.normalized_number || undefined,
       sort: q.sort,
     },
@@ -58,8 +60,10 @@ export async function postAdminRiskPoolRecover(
 export type AdminRiskLockLogQuery = {
   page?: number;
   per_page?: number;
+  group_by?: "ticket" | "entry";
   action_type?: "lock" | "release";
   normalized_number?: string;
+  ticket_item_id?: number;
 };
 
 export async function getAdminRiskPoolLockLogs(
@@ -72,8 +76,10 @@ export async function getAdminRiskPoolLockLogs(
       params: {
         page: q.page,
         per_page: q.per_page,
+        group_by: q.group_by ?? "ticket",
         action_type: q.action_type,
         normalized_number: q.normalized_number,
+        ticket_item_id: q.ticket_item_id,
       },
     },
   );
diff --git a/src/api/admin-ticket-detail.ts b/src/api/admin-ticket-detail.ts
new file mode 100644
index 0000000..69a255b
--- /dev/null
+++ b/src/api/admin-ticket-detail.ts
@@ -0,0 +1,10 @@
+import { adminRequest } from "@/lib/admin-http";
+
+import type { AdminTicketItemDetail } from "@/types/api/admin-tickets";
+
+const A = `/admin`;
+
+export async function getAdminTicketItem(ticketNo: string): Promise {
+  const encoded = encodeURIComponent(ticketNo);
+  return adminRequest.get(`${A}/tickets/${encoded}`);
+}
diff --git a/src/app/admin/(shell)/agents/layout.tsx b/src/app/admin/(shell)/agents/layout.tsx
index a8f835e..a479a9b 100644
--- a/src/app/admin/(shell)/agents/layout.tsx
+++ b/src/app/admin/(shell)/agents/layout.tsx
@@ -4,7 +4,7 @@ import { AgentsSubnav } from "@/modules/agents/agents-subnav";
 
 export default function AdminAgentsLayout({ children }: { children: ReactNode }) {
   return (
-    
+
{children}
diff --git a/src/app/admin/(shell)/agents/list/page.tsx b/src/app/admin/(shell)/agents/list/page.tsx index 9bdecd8..72544f4 100644 --- a/src/app/admin/(shell)/agents/list/page.tsx +++ b/src/app/admin/(shell)/agents/list/page.tsx @@ -3,7 +3,7 @@ import type { Metadata } from "next"; import { AgentsDirectoryConsole } from "@/modules/agents/agents-directory-console"; import { buildPageMetadata } from "@/lib/page-metadata"; -export const metadata: Metadata = buildPageMetadata("agents", "directoryTitle"); +export const metadata: Metadata = buildPageMetadata("agents", "listTitle"); export default function AgentsListPage() { return ; diff --git a/src/app/admin/(shell)/draws/[drawId]/risk/pools/page.tsx b/src/app/admin/(shell)/draws/[drawId]/risk/pools/page.tsx index 42b8691..fa4c686 100644 --- a/src/app/admin/(shell)/draws/[drawId]/risk/pools/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/risk/pools/page.tsx @@ -4,10 +4,10 @@ import { } from "@/modules/risk/risk-pools-console"; function parsePoolFilter(raw: string | undefined): RiskPoolListFilter { - if (raw === "sold_out" || raw === "high_risk") { + if (raw === "sold_out" || raw === "high_risk" || raw === "all" || raw === "active") { return raw; } - return "all"; + return "active"; } export default async function AdminDrawRiskPoolsPage(props: { @@ -24,7 +24,7 @@ export default async function AdminDrawRiskPoolsPage(props: { drawId={id} titleKey="allPoolsPageTitle" initialFilter={filter} - defaultSort={filter === "high_risk" ? "usage_desc" : "number_asc"} + defaultSort={filter === "high_risk" || filter === "active" ? "usage_desc" : "number_asc"} allowSortChange /> ); diff --git a/src/app/admin/(shell)/tickets/[ticketNo]/page.tsx b/src/app/admin/(shell)/tickets/[ticketNo]/page.tsx new file mode 100644 index 0000000..5c1b818 --- /dev/null +++ b/src/app/admin/(shell)/tickets/[ticketNo]/page.tsx @@ -0,0 +1,22 @@ +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { PRD_TICKETS_ACCESS_ANY } from "@/lib/admin-prd"; +import { TicketDetailConsole } from "@/modules/tickets/ticket-detail-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("tickets", "detailTitle"); + +export default async function AdminTicketDetailPage(props: { + params: Promise<{ ticketNo: string }>; +}) { + const { ticketNo } = await props.params; + + return ( + + + + + + ); +} diff --git a/src/app/docs/admin/agents/page.tsx b/src/app/docs/admin/agents/page.tsx new file mode 100644 index 0000000..dfd0436 --- /dev/null +++ b/src/app/docs/admin/agents/page.tsx @@ -0,0 +1,5 @@ +import { AdminAgentsDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsAgentsPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/config/page.tsx b/src/app/docs/admin/config/page.tsx new file mode 100644 index 0000000..ff4bcae --- /dev/null +++ b/src/app/docs/admin/config/page.tsx @@ -0,0 +1,5 @@ +import { AdminConfigDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsConfigPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/draws/page.tsx b/src/app/docs/admin/draws/page.tsx new file mode 100644 index 0000000..7e05acc --- /dev/null +++ b/src/app/docs/admin/draws/page.tsx @@ -0,0 +1,5 @@ +import { AdminDrawsDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsDrawsPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/faq/page.tsx b/src/app/docs/admin/faq/page.tsx new file mode 100644 index 0000000..7505822 --- /dev/null +++ b/src/app/docs/admin/faq/page.tsx @@ -0,0 +1,5 @@ +import { AdminFaqDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsFaqPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/fund-operations/page.tsx b/src/app/docs/admin/fund-operations/page.tsx new file mode 100644 index 0000000..df26b84 --- /dev/null +++ b/src/app/docs/admin/fund-operations/page.tsx @@ -0,0 +1,5 @@ +import { AdminFundOperationsDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsFundOperationsPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/layout.tsx b/src/app/docs/admin/layout.tsx new file mode 100644 index 0000000..8d84329 --- /dev/null +++ b/src/app/docs/admin/layout.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; + +import { DocsSidebar } from "@/components/docs/docs-sidebar"; +import { DocsBody } from "@/components/docs/docs-shell"; +import { ADMIN_DOCS_NAV_GROUPS } from "@/lib/admin-docs-nav"; + +export default function AdminDocsLayout({ children }: { children: ReactNode }): React.ReactElement { + return ( + }> + {children} + + ); +} diff --git a/src/app/docs/admin/manual-review/page.tsx b/src/app/docs/admin/manual-review/page.tsx new file mode 100644 index 0000000..259ce4b --- /dev/null +++ b/src/app/docs/admin/manual-review/page.tsx @@ -0,0 +1,5 @@ +import { AdminManualReviewDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsManualReviewPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/page.tsx b/src/app/docs/admin/page.tsx new file mode 100644 index 0000000..0a59006 --- /dev/null +++ b/src/app/docs/admin/page.tsx @@ -0,0 +1,5 @@ +import { AdminOverviewDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsOverviewPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/players/page.tsx b/src/app/docs/admin/players/page.tsx new file mode 100644 index 0000000..58940e1 --- /dev/null +++ b/src/app/docs/admin/players/page.tsx @@ -0,0 +1,5 @@ +import { AdminPlayersDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsPlayersPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/reports/page.tsx b/src/app/docs/admin/reports/page.tsx new file mode 100644 index 0000000..6fc59f5 --- /dev/null +++ b/src/app/docs/admin/reports/page.tsx @@ -0,0 +1,5 @@ +import { AdminReportsDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsReportsPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/roles/page.tsx b/src/app/docs/admin/roles/page.tsx new file mode 100644 index 0000000..a2e50aa --- /dev/null +++ b/src/app/docs/admin/roles/page.tsx @@ -0,0 +1,5 @@ +import { AdminRolesDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsRolesPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/settlement-center/page.tsx b/src/app/docs/admin/settlement-center/page.tsx new file mode 100644 index 0000000..585b3cf --- /dev/null +++ b/src/app/docs/admin/settlement-center/page.tsx @@ -0,0 +1,5 @@ +import { AdminSettlementCenterDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsSettlementCenterPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/site-setup/page.tsx b/src/app/docs/admin/site-setup/page.tsx new file mode 100644 index 0000000..1dddb4c --- /dev/null +++ b/src/app/docs/admin/site-setup/page.tsx @@ -0,0 +1,5 @@ +import { AdminSiteSetupDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsSiteSetupPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/tickets/page.tsx b/src/app/docs/admin/tickets/page.tsx new file mode 100644 index 0000000..ba47722 --- /dev/null +++ b/src/app/docs/admin/tickets/page.tsx @@ -0,0 +1,5 @@ +import { AdminTicketsDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsTicketsPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/admin/wallet/page.tsx b/src/app/docs/admin/wallet/page.tsx new file mode 100644 index 0000000..6692e62 --- /dev/null +++ b/src/app/docs/admin/wallet/page.tsx @@ -0,0 +1,5 @@ +import { AdminWalletDocScreen } from "@/modules/docs/admin/admin-doc-screens"; + +export default function AdminDocsWalletPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/integration/admin-guide/page.tsx b/src/app/docs/integration/admin-guide/page.tsx new file mode 100644 index 0000000..7d59c99 --- /dev/null +++ b/src/app/docs/integration/admin-guide/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function LegacyAdminGuidePage(): never { + redirect("/docs/admin"); +} diff --git a/src/app/docs/integration/api-reference/page.tsx b/src/app/docs/integration/api-reference/page.tsx new file mode 100644 index 0000000..6537928 --- /dev/null +++ b/src/app/docs/integration/api-reference/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; + +/** 旧链接兼容:完整内容已拆至左侧分章导航 */ +export default function ApiReferencePage(): never { + redirect("/docs/integration"); +} diff --git a/src/app/docs/integration/delivery/page.tsx b/src/app/docs/integration/delivery/page.tsx new file mode 100644 index 0000000..9477937 --- /dev/null +++ b/src/app/docs/integration/delivery/page.tsx @@ -0,0 +1,5 @@ +import { DeliveryDocScreen } from "@/modules/docs/integration/integration-doc-screens"; + +export default function IntegrationDeliveryPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/integration/troubleshooting/page.tsx b/src/app/docs/integration/troubleshooting/page.tsx new file mode 100644 index 0000000..d4bae2e --- /dev/null +++ b/src/app/docs/integration/troubleshooting/page.tsx @@ -0,0 +1,5 @@ +import { TroubleshootingDocScreen } from "@/modules/docs/integration/integration-doc-screens"; + +export default function IntegrationTroubleshootingPage(): React.ReactElement { + return ; +} diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx index 41d5188..8b2e994 100644 --- a/src/app/docs/layout.tsx +++ b/src/app/docs/layout.tsx @@ -5,10 +5,10 @@ import { DocsShell } from "@/components/docs/docs-shell"; export const metadata: Metadata = { title: { - template: "%s · Integration API", - default: "Integration API", + template: "%s · Docs", + default: "Documentation", }, - description: "Lottery integration docs: SSO, wallet gateway, transfers.", + description: "Lottery admin user guide and API integration documentation.", }; export default function DocsLayout({ children }: { children: ReactNode }): React.ReactElement { diff --git a/src/app/globals.css b/src/app/globals.css index 48df7e2..a8a8232 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -238,3 +238,21 @@ } } +/* 公开文档站:正文链接与强调,避免继承后台 muted 色 */ +.docs-site .doc-content a:not([class]) { + color: #0f172a; + font-weight: 500; + text-decoration: underline; + text-decoration-color: #cbd5e1; + text-underline-offset: 2px; +} + +.docs-site .doc-content a:not([class]):hover { + text-decoration-color: #64748b; +} + +.docs-site .doc-content strong { + color: #0f172a; + font-weight: 600; +} + diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index 233354e..84e0eba 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -41,7 +41,7 @@ const TOP_ROUTE_LABELS: Record = { }; const AGENT_ROUTE_LABELS: Record = { - list: "agents.directoryTitle", + list: "agents.listTitle", provision: "agents.subnav.provision", "settlement-bills": "settlementCenter.title", }; diff --git a/src/components/admin/admin-page-guide.tsx b/src/components/admin/admin-page-guide.tsx new file mode 100644 index 0000000..52a6da5 --- /dev/null +++ b/src/components/admin/admin-page-guide.tsx @@ -0,0 +1,37 @@ +"use client"; + +import Link from "next/link"; +import { BookOpenIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; + +type AdminPageGuideProps = { + guide: string; + docHref?: string; + className?: string; +}; + +export function AdminPageGuide({ guide, docHref, className }: AdminPageGuideProps): React.ReactElement { + const { t } = useTranslation("common"); + + return ( +
+

{guide}

+ {docHref ? ( + + + {t("docs.learnMore")} + + ) : null} +
+ ); +} diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index d29a551..365869d 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -48,7 +48,7 @@ export function AdminShell({ children }: { children: ReactNode }) { )}
-
+
{children}
diff --git a/src/components/admin/module-scaffold.tsx b/src/components/admin/module-scaffold.tsx index a6d0d08..0d6be69 100644 --- a/src/components/admin/module-scaffold.tsx +++ b/src/components/admin/module-scaffold.tsx @@ -15,7 +15,7 @@ export function ModuleScaffold({ children, className, embedded = false }: Module
{t("toolbar.accountSettings")} + router.push("/docs/admin")} + > + + {t("toolbar.relatedDocs")} + diff --git a/src/components/docs/doc-code.tsx b/src/components/docs/doc-code.tsx index b29a3d7..3622372 100644 --- a/src/components/docs/doc-code.tsx +++ b/src/components/docs/doc-code.tsx @@ -9,12 +9,14 @@ type DocCodeProps = { children: string; language?: HighlightLang; className?: string; + title?: string; }; export function DocCode({ children, language = "json", className, + title, }: DocCodeProps): React.ReactElement { const [html, setHtml] = useState(null); const code = children.trimEnd(); @@ -36,17 +38,22 @@ export function DocCode({ return (
+ {title ? ( +
+ {title} +
+ ) : null} {html ? (
) : ( -
+        
           {code}
         
)} diff --git a/src/components/docs/doc-ui.tsx b/src/components/docs/doc-ui.tsx index 0736512..0cb46eb 100644 --- a/src/components/docs/doc-ui.tsx +++ b/src/components/docs/doc-ui.tsx @@ -4,6 +4,11 @@ import { cn } from "@/lib/utils"; export { DocCode } from "@/components/docs/doc-code"; +/** 文档正文容器:统一字号与段落间距 */ +export function DocPage({ children }: { children: ReactNode }): React.ReactElement { + return
{children}
; +} + export function DocPageHeader({ title, description, @@ -12,10 +17,14 @@ export function DocPageHeader({ description?: string; }): React.ReactElement { return ( -
-

{title}

+
+

+ {title} +

{description ? ( -

{description}

+

+ {description} +

) : null}
); @@ -31,26 +40,39 @@ export function DocSection({ className?: string; }): React.ReactElement { return ( -
- {title ?

{title}

: null} +
+ {title ? ( +

+ {title} +

+ ) : null} {children}
); } +export function DocParagraph({ children }: { children: ReactNode }): React.ReactElement { + return

{children}

; +} + export function DocNote({ children }: { children: ReactNode }): React.ReactElement { return ( -

{children}

+ ); } export function DocList({ items }: { items: readonly string[] }): React.ReactElement { return ( -
    +
      {items.map((item) => ( -
    • - · - {item} +
    • + + {item}
    • ))}
    @@ -59,9 +81,11 @@ export function DocList({ items }: { items: readonly string[] }): React.ReactEle export function DocOrderedList({ items }: { items: readonly string[] }): React.ReactElement { return ( -
      +
        {items.map((item) => ( -
      1. {item}
      2. +
      3. + {item} +
      4. ))}
      ); @@ -77,14 +101,14 @@ export function DocTable({ compact?: boolean; }): React.ReactElement { return ( -
      - +
      +
      - + {headers.map((header) => ( @@ -93,13 +117,17 @@ export function DocTable({ {rows.map((row, rowIndex) => ( - + {row.map((cell, cellIndex) => (
      {header}
      {cell} @@ -115,15 +143,19 @@ export function DocTable({ export function DocEndpoint({ method, path }: { method: string; path: string }): React.ReactElement { return ( -
      - {method} - {path} +
      + + {method} + + {path}
      ); } export function DocInlineCode({ children }: { children: ReactNode }): React.ReactElement { return ( - {children} + + {children} + ); } diff --git a/src/components/docs/docs-admin-console-link.tsx b/src/components/docs/docs-admin-console-link.tsx new file mode 100644 index 0000000..5355cc7 --- /dev/null +++ b/src/components/docs/docs-admin-console-link.tsx @@ -0,0 +1,31 @@ +"use client"; + +import Link from "next/link"; +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; + +type DocsAdminConsoleLinkProps = { + namespace: "adminDocs" | "integrationDocs"; + className?: string; +}; + +export function DocsAdminConsoleLink({ + namespace, + className, +}: DocsAdminConsoleLinkProps): React.ReactElement { + const { t } = useTranslation(namespace); + const label = t("shell.adminLogin"); + + return ( + + {label} + + ); +} diff --git a/src/components/docs/docs-shell.tsx b/src/components/docs/docs-shell.tsx index 209ba6c..46d16cb 100644 --- a/src/components/docs/docs-shell.tsx +++ b/src/components/docs/docs-shell.tsx @@ -1,9 +1,12 @@ "use client"; import Link from "next/link"; +import { usePathname } from "next/navigation"; import { useTranslation } from "react-i18next"; import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher"; +import { DocsAdminConsoleLink } from "@/components/docs/docs-admin-console-link"; +import { DocsTopNav } from "@/components/docs/docs-top-nav"; import { cn } from "@/lib/utils"; type DocsShellProps = { @@ -12,23 +15,28 @@ type DocsShellProps = { }; export function DocsShell({ children, className }: DocsShellProps): React.ReactElement { - const { t } = useTranslation("integrationDocs"); + const pathname = usePathname(); + const isAdminDocs = pathname === "/docs/admin" || pathname.startsWith("/docs/admin/"); + const namespace = isAdminDocs ? "adminDocs" : "integrationDocs"; + const homeHref = isAdminDocs ? "/docs/admin" : "/docs/integration"; + const { t } = useTranslation(namespace); return ( -
      -
      -
      - - {t("shell.title")} - -
      - +
      +
      +
      +
      - {t("shell.admin")} + {t("shell.title")} + +
      +
      + +
      @@ -45,9 +53,11 @@ export function DocsBody({ children: React.ReactNode; }): React.ReactElement { return ( -
      +
      {sidebar} -
      {children}
      +
      + {children} +
      ); } diff --git a/src/components/docs/docs-sidebar.tsx b/src/components/docs/docs-sidebar.tsx index fddbc66..ddff37c 100644 --- a/src/components/docs/docs-sidebar.tsx +++ b/src/components/docs/docs-sidebar.tsx @@ -8,43 +8,49 @@ import type { DocsNavGroup } from "@/lib/docs-nav"; import { resolveDocsNavLabel } from "@/lib/docs-nav-labels"; import { cn } from "@/lib/utils"; -export function DocsSidebar({ groups }: { groups: readonly DocsNavGroup[] }): React.ReactElement { +export function DocsSidebar({ + groups, + namespace = "integrationDocs", +}: { + groups: readonly DocsNavGroup[]; + namespace?: "integrationDocs" | "adminDocs"; +}): React.ReactElement { const pathname = usePathname(); - const { t, i18n } = useTranslation("integrationDocs"); + const { t, i18n } = useTranslation(namespace); + + const homeHref = namespace === "adminDocs" ? "/docs/admin" : "/docs/integration"; const label = (key: string): string => { const translated = t(key); if (translated !== key) { return translated; } - return resolveDocsNavLabel(key, i18n.resolvedLanguage ?? i18n.language); + return resolveDocsNavLabel(key, i18n.resolvedLanguage ?? i18n.language, namespace); }; return ( -
      +
      - {data ? ( - { - setPerPage(n); - setPage(1); - }} - onPageChange={setPage} - /> - ) : null} - + {data ? ( + { + setPerPage(n); + setPage(1); + }} + onPageChange={setPage} + /> + ) : null} ); diff --git a/src/modules/risk/risk-pools-console.tsx b/src/modules/risk/risk-pools-console.tsx index 4e13134..94a7aba 100644 --- a/src/modules/risk/risk-pools-console.tsx +++ b/src/modules/risk/risk-pools-console.tsx @@ -63,7 +63,7 @@ function riskSortLabel( return option ? t(option.label) : value; } -export type RiskPoolListFilter = "all" | "sold_out" | "high_risk"; +export type RiskPoolListFilter = "all" | "active" | "sold_out" | "high_risk"; type RiskPoolsConsoleProps = { drawId: number; @@ -87,7 +87,7 @@ function resolveInitialFilter( if (soldOutOnly) { return "sold_out"; } - return "all"; + return "active"; } export function RiskPoolsConsole({ @@ -96,7 +96,7 @@ export function RiskPoolsConsole({ titleKey, soldOutOnly, initialFilter: initialFilterProp, - defaultSort = "number_asc", + defaultSort = "usage_desc", allowSortChange = true, }: RiskPoolsConsoleProps) { const { t } = useTranslation(["risk", "common"]); @@ -130,6 +130,7 @@ export function RiskPoolsConsole({ per_page: perPage, sold_out_only: filter === "sold_out", high_risk_only: filter === "high_risk", + active_only: filter === "active" && number.trim() === "", normalized_number: number.trim(), sort, }); @@ -216,12 +217,15 @@ export function RiskPoolsConsole({ {filter === "all" ? t("filterAll") - : filter === "sold_out" - ? t("filterSoldOut") - : t("filterHighRisk")} + : filter === "active" + ? t("filterActive") + : filter === "sold_out" + ? t("filterSoldOut") + : t("filterHighRisk")} + {t("filterActive")} {t("filterAll")} {t("filterSoldOut")} {t("filterHighRisk")} diff --git a/src/modules/settlement/settlement-center-shell.tsx b/src/modules/settlement/settlement-center-shell.tsx index 8504a04..d53d319 100644 --- a/src/modules/settlement/settlement-center-shell.tsx +++ b/src/modules/settlement/settlement-center-shell.tsx @@ -10,6 +10,8 @@ import { toast } from "sonner"; import { getSettlementPeriods, type SettlementPeriodRow } from "@/api/admin-agent-settlement"; import { getAdminIntegrationSites } from "@/api/admin-integration-sites"; import { AdminLoadingState } from "@/components/admin/admin-loading-state"; +import { AdminPageGuide } from "@/components/admin/admin-page-guide"; +import { ADMIN_DOC_LINKS } from "@/lib/admin-doc-links"; import { AdminNoIntegrationSiteState } from "@/components/admin/admin-no-integration-site-state"; import { AgentBillDetail } from "@/modules/settlement/agent-bill-detail"; import { SettlementCenterPeriodDetail } from "@/modules/settlement/settlement-center-period-detail"; @@ -58,6 +60,7 @@ export function SettlementCenterShell(): React.ReactElement { const canFinanceAdjustments = canOperateBills && boundAgent === null; const [siteOptions, setSiteOptions] = useState([]); + const [sitesReady, setSitesReady] = useState(() => boundAgent?.admin_site_id != null); const [adminSiteId, setAdminSiteId] = useState(null); const [sitePickerOpen, setSitePickerOpen] = useState(false); const [siteKeyword, setSiteKeyword] = useState(""); @@ -80,27 +83,36 @@ export function SettlementCenterShell(): React.ReactElement { currency_code: "NPR", }]); setAdminSiteId(boundAgent.admin_site_id); + setSitesReady(true); return; } - void getAdminIntegrationSites().then((sites) => { - const options = (sites.items ?? []).map((site) => ({ - id: site.id, - label: formatAdminSiteLabel(site.name, site.code), - code: site.code, - currency_code: site.currency_code ?? "NPR", - })); - setSiteOptions(options); - setAdminSiteId((current) => { - if (siteFromUrl !== null && options.some((site) => site.id === siteFromUrl)) { - return siteFromUrl; - } - if (current !== null && options.some((site) => site.id === current)) { - return current; - } - return options[0]?.id ?? null; + setSitesReady(false); + void getAdminIntegrationSites() + .then((sites) => { + const options = (sites.items ?? []).map((site) => ({ + id: site.id, + label: formatAdminSiteLabel(site.name, site.code), + code: site.code, + currency_code: site.currency_code ?? "NPR", + })); + setSiteOptions(options); + setAdminSiteId((current) => { + if (siteFromUrl !== null && options.some((site) => site.id === siteFromUrl)) { + return siteFromUrl; + } + if (current !== null && options.some((site) => site.id === current)) { + return current; + } + return options[0]?.id ?? null; + }); + }) + .catch(() => { + setSiteOptions([]); + }) + .finally(() => { + setSitesReady(true); }); - }); }, [boundAgent, siteFromUrl]); const siteId = adminSiteId ?? siteOptions[0]?.id ?? null; @@ -283,14 +295,18 @@ export function SettlementCenterShell(): React.ReactElement { }; }, [activePeriod, activePeriodId, activeView, periodsReady, router, siteId]); + const shellBootstrapping = + !sitesReady || (siteId !== null && !periodsReady); + return (
      - {siteId === null && siteOptions.length === 0 && boundAgent === null ? ( + + {shellBootstrapping ? ( + + ) : siteId === null && siteOptions.length === 0 && boundAgent === null ? ( ) : siteId === null ? (

      {t("empty.noSite", { defaultValue: "请选择站点。" })}

      - ) : !periodsReady ? ( - ) : isListMode ? ( { setLoading(true); diff --git a/src/modules/tickets/player-tickets-console.tsx b/src/modules/tickets/player-tickets-console.tsx index 33eb601..541d836 100644 --- a/src/modules/tickets/player-tickets-console.tsx +++ b/src/modules/tickets/player-tickets-console.tsx @@ -401,18 +401,24 @@ export function PlayerTicketsConsole(): React.ReactElement { {formatTs(row.updated_at)} diff --git a/src/modules/tickets/ticket-detail-console.tsx b/src/modules/tickets/ticket-detail-console.tsx new file mode 100644 index 0000000..007cdc7 --- /dev/null +++ b/src/modules/tickets/ticket-detail-console.tsx @@ -0,0 +1,230 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useAsyncEffect } from "@/hooks/use-async-effect"; +import { useTranslationRef } from "@/hooks/use-translation-ref"; + +import { getAdminTicketItem } from "@/api/admin-ticket-detail"; +import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; +import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; +import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; +import { PlayerFundingModeBadge } from "@/components/admin/player-funding-badges"; +import { buttonVariants } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; +import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog"; +import { useExportLabels } from "@/hooks/use-export-labels"; +import { adminPlayerDetailPath } from "@/lib/admin-player-paths"; +import { formatAdminMinorUnits } from "@/lib/money"; +import { cn } from "@/lib/utils"; +import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminTicketItemDetail } from "@/types/api/admin-tickets"; + +function ticketStatusText(value: string, t: (key: string) => string): string { + const key = `statusOptions.${value}`; + const translated = t(key); + return translated === key ? value : translated; +} + +export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) { + const { t } = useTranslation(["tickets", "common"]); + const tRef = useTranslationRef(["tickets", "common"]); + const playCodeLabel = useAdminPlayCodeLabel(); + const formatDt = useAdminDateTimeFormatter(); + const exportLabels = useExportLabels("ticketCombinations"); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [comboPage, setComboPage] = useState(1); + const [comboPerPage, setComboPerPage] = useState(20); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const d = await getAdminTicketItem(ticketNo); + setData(d); + } catch (e) { + const msg = e instanceof LotteryApiBizError ? e.message : tRef.current("detailLoadFailed"); + setError(msg); + setData(null); + } finally { + setLoading(false); + } + }, [ticketNo, tRef]); + + useAsyncEffect(() => { + void load(); + }, [ticketNo]); + + const combinations = data?.combinations ?? []; + const comboLastPage = Math.max(1, Math.ceil(combinations.length / comboPerPage)); + const comboPageSafe = Math.min(comboPage, comboLastPage); + const comboSlice = useMemo(() => { + const start = (comboPageSafe - 1) * comboPerPage; + return combinations.slice(start, start + comboPerPage); + }, [combinations, comboPageSafe, comboPerPage]); + + if (error && !data) { + return ( + + + {t("detailTitle")} + + +

      {error}

      + + {t("backToList")} + +
      +
      + ); + } + + const currencyCode = data?.currency_code ?? "NPR"; + + return ( +
      + + +
      + {t("detailTitle")} + + {t("backToList")} + +
      + + {data?.ticket_no ?? ticketNo} + {data?.order_no ? ` · ${data.order_no}` : ""} + +
      + + {loading && !data ? ( +

      {t("loadingDetail")}

      + ) : data ? ( +
      +

      + {t("drawNo")}: + {data.draw_no ?? "—"} +

      +

      + {t("playCode")}: + {playCodeLabel(data.play_code)} +

      +

      + {t("number")}: + {data.original_number ?? "—"} +

      +

      + {t("combinationCount")}: + {data.combination_count} +

      +

      + {t("betAmount")}: + {data.total_bet_amount_formatted} +

      +

      + {t("actualDeduct")}: + {data.actual_deduct_amount_formatted} +

      +

      + {t("status")}: + {ticketStatusText(data.status, t)} +

      +

      + {t("player", { ns: "tickets" })}: + + {data.player_id ? ( + + {data.username ?? data.site_player_id ?? data.player_id} + + ) : ( + "—" + )} +

      +

      + {t("placedAt")}: + {data.placed_at ? formatDt(data.placed_at) : "—"} +

      + {data.fail_reason_text || data.fail_reason_code ? ( +

      + {t("failReason")}: + {data.fail_reason_text ?? data.fail_reason_code} +

      + ) : null} +
      + ) : null} +
      +
      + + + + {t("combinationsTitle")} + {t("combinationsHint")} + + +
      + +
      +
      + + + + No. + {t("number4d")} + {t("comboBetAmount")} + {t("comboEstimatedPayout")} + + + + {loading && !data ? : null} + {comboSlice.map((row) => ( + + {row.combination_no} + {row.number_4d} + + {formatAdminMinorUnits(row.bet_amount, currencyCode)} + + + {formatAdminMinorUnits(row.estimated_payout, currencyCode)} + + + ))} + +
      +
      + {combinations.length > 0 ? ( + { + setComboPerPage(n); + setComboPage(1); + }} + onPageChange={setComboPage} + /> + ) : null} +
      +
      +
      + ); +} diff --git a/src/types/api/admin-risk.ts b/src/types/api/admin-risk.ts index 043540b..afedb0c 100644 --- a/src/types/api/admin-risk.ts +++ b/src/types/api/admin-risk.ts @@ -37,11 +37,27 @@ export type AdminRiskLockLogRow = { created_at: string | null; }; +export type AdminRiskLockLogTicketRow = { + ticket_item_id: number; + ticket_no: string; + play_code: string; + original_number: string; + combination_count: number; + player_id: number; + number_count: number; + lock_entry_count: number; + release_entry_count: number; + total_lock_amount: number; + total_release_amount: number; + last_at: string | null; +}; + export type AdminRiskLockLogListData = { draw_id: number; draw_no: string; currency_code: string | null; - items: AdminRiskLockLogRow[]; + group_by: "ticket" | "entry"; + items: AdminRiskLockLogRow[] | AdminRiskLockLogTicketRow[]; meta: AdminRiskPoolListMeta; }; diff --git a/src/types/api/admin-tickets.ts b/src/types/api/admin-tickets.ts index c6435da..2afe8fb 100644 --- a/src/types/api/admin-tickets.ts +++ b/src/types/api/admin-tickets.ts @@ -39,6 +39,52 @@ export type AdminTicketItemRow = { updated_at: string | null; }; +export type AdminTicketCombinationRow = { + combination_no: number; + number_4d: string; + bet_amount: number; + estimated_payout: number; +}; + +export type AdminTicketItemDetail = { + id: number; + ticket_no: string; + order_no: string | null; + order_status: string | null; + draw_id: number; + draw_no: string | null; + currency_code: string | null; + player_id: number; + site_code: string | null; + site_player_id: string | null; + username: string | null; + nickname: string | null; + funding_mode: string | null; + agent_node_id: number | null; + agent_code: string | null; + agent_name: string | null; + play_code: string; + dimension: number | null; + digit_slot: number | null; + original_number: string | null; + normalized_number: string | null; + combination_count: number; + unit_bet_amount: number; + total_bet_amount_minor: number; + total_bet_amount_formatted: string; + actual_deduct_amount_minor: number; + actual_deduct_amount_formatted: string; + risk_locked_amount: number; + status: string; + fail_reason_code: string | null; + fail_reason_text: string | null; + win_amount_minor: number; + win_amount_formatted: string; + placed_at: string | null; + updated_at: string | null; + combinations: AdminTicketCombinationRow[]; +}; + export type AdminTicketItemsData = { items: AdminTicketItemRow[]; total: number;