diff --git a/next.config.ts b/next.config.ts index 9b8dcd1..cd9f51a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + allowedDevOrigins: ["192.168.0.101"], reactCompiler: true, async redirects() { return [ diff --git a/src/api/admin-currencies.ts b/src/api/admin-currencies.ts new file mode 100644 index 0000000..11c8bf2 --- /dev/null +++ b/src/api/admin-currencies.ts @@ -0,0 +1,32 @@ +import { adminRequest } from "@/lib/admin-http"; + +import { API_V1_PREFIX } from "./paths"; + +import type { + AdminCurrencyCreatePayload, + AdminCurrencyDeleteResult, + AdminCurrencyListData, + AdminCurrencyRow, + AdminCurrencyUpdatePayload, +} from "@/types/api/admin-currency"; + +const A = `${API_V1_PREFIX}/admin`; + +export async function getAdminCurrencies(): Promise { + return adminRequest.get(`${A}/currencies`); +} + +export async function postAdminCurrency(body: AdminCurrencyCreatePayload): Promise { + return adminRequest.post(`${A}/currencies`, body); +} + +export async function putAdminCurrency( + code: string, + body: AdminCurrencyUpdatePayload, +): Promise { + return adminRequest.put(`${A}/currencies/${encodeURIComponent(code)}`, body); +} + +export async function deleteAdminCurrency(code: string): Promise { + return adminRequest.delete(`${A}/currencies/${encodeURIComponent(code)}`); +} diff --git a/src/api/admin-reconcile.ts b/src/api/admin-reconcile.ts index 7d10a7d..6d1e762 100644 --- a/src/api/admin-reconcile.ts +++ b/src/api/admin-reconcile.ts @@ -22,8 +22,9 @@ export async function getAdminReconcileJobs(params?: { export async function postAdminReconcileJob(body: { reconcile_type: string; - period_start?: string | null; - period_end?: string | null; + date_from?: string | null; + date_to?: string | null; + player_id?: number | null; items?: { side_a_ref?: string | null; side_b_ref?: string | null; diff --git a/src/api/admin-reports.ts b/src/api/admin-reports.ts deleted file mode 100644 index 325f9d0..0000000 --- a/src/api/admin-reports.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { adminHttp, adminRequest } from "@/lib/admin-http"; -import { withAdminAuthHeader } from "@/lib/admin-auth"; -import { withAdminLocaleHeaders } from "@/lib/admin-locale"; - -import { API_V1_PREFIX } from "./paths"; - -import type { - AdminReportJobCreateResponse, - AdminReportJobListData, -} from "@/types/api/admin-reports"; - -const A = `${API_V1_PREFIX}/admin`; - -export async function getAdminReportJobs(params?: { - page?: number; - per_page?: number; -}): Promise { - return adminRequest.get(`${A}/report-jobs`, { - params, - }); -} - -export async function postAdminReportJob(body: { - report_type: string; - export_format?: "csv" | "xlsx"; - parameters?: Record | null; - filter_json?: Record | null; -}): Promise { - return adminRequest.post( - `${A}/report-jobs`, - body, - ); -} - -export async function downloadAdminReportJob(jobId: number): Promise { - const res = await adminHttp.request( - withAdminAuthHeader(withAdminLocaleHeaders({ - url: `${A}/report-jobs/${jobId}/download`, - method: "GET", - responseType: "blob", - })), - ); - return res.data; -} diff --git a/src/api/index.ts b/src/api/index.ts index 17e85bd..6026b65 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,7 +8,6 @@ export { getAdminTransferOrders, getAdminWalletTransactions, } from "@/api/admin-wallet"; -export { getAdminReportJobs, postAdminReportJob } from "@/api/admin-reports"; export { getAdminReconcileJobItems, getAdminReconcileJobs, diff --git a/src/app/admin/(shell)/admin-roles/page.tsx b/src/app/admin/(shell)/admin-roles/page.tsx index c2981e9..0fac604 100644 --- a/src/app/admin/(shell)/admin-roles/page.tsx +++ b/src/app/admin/(shell)/admin-roles/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function AdminRolesPage() { return ( - + ); diff --git a/src/app/admin/(shell)/admin-users/page.tsx b/src/app/admin/(shell)/admin-users/page.tsx index 634b189..56f030e 100644 --- a/src/app/admin/(shell)/admin-users/page.tsx +++ b/src/app/admin/(shell)/admin-users/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function AdminUsersPage() { return ( - + ); diff --git a/src/app/admin/(shell)/audit-logs/page.tsx b/src/app/admin/(shell)/audit-logs/page.tsx index 80279c3..520eada 100644 --- a/src/app/admin/(shell)/audit-logs/page.tsx +++ b/src/app/admin/(shell)/audit-logs/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function AdminAuditLogsPage() { return ( - + ); diff --git a/src/app/admin/(shell)/currencies/page.tsx b/src/app/admin/(shell)/currencies/page.tsx new file mode 100644 index 0000000..e891fbb --- /dev/null +++ b/src/app/admin/(shell)/currencies/page.tsx @@ -0,0 +1,15 @@ +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { CurrencyManagementScreen } from "@/modules/settings/currency-management-screen"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "币种管理", +}; + +export default function AdminCurrenciesPage() { + return ( + + + + ); +} diff --git a/src/app/admin/(shell)/draws/[drawId]/layout.tsx b/src/app/admin/(shell)/draws/[drawId]/layout.tsx index 949ea0a..fd22817 100644 --- a/src/app/admin/(shell)/draws/[drawId]/layout.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/layout.tsx @@ -8,7 +8,7 @@ export default async function AdminDrawSegmentLayout(props: { const { drawId } = await props.params; return ( - + {props.children} diff --git a/src/app/admin/(shell)/draws/page.tsx b/src/app/admin/(shell)/draws/page.tsx index 82866c0..89096ef 100644 --- a/src/app/admin/(shell)/draws/page.tsx +++ b/src/app/admin/(shell)/draws/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function AdminDrawsPage() { return ( - + ); diff --git a/src/app/admin/(shell)/players/page.tsx b/src/app/admin/(shell)/players/page.tsx index 028bab0..a20a2f7 100644 --- a/src/app/admin/(shell)/players/page.tsx +++ b/src/app/admin/(shell)/players/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function AdminPlayersPage() { return ( - + ); diff --git a/src/app/admin/(shell)/reconcile/page.tsx b/src/app/admin/(shell)/reconcile/page.tsx index bec0e7d..de64a97 100644 --- a/src/app/admin/(shell)/reconcile/page.tsx +++ b/src/app/admin/(shell)/reconcile/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function AdminReconcilePage() { return ( - + ); diff --git a/src/app/admin/(shell)/reports/page.tsx b/src/app/admin/(shell)/reports/page.tsx deleted file mode 100644 index da8c512..0000000 --- a/src/app/admin/(shell)/reports/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { reportsModuleMeta } from "@/modules/reports/meta"; -import { ReportsConsole } from "@/modules/reports/reports-console"; -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: reportsModuleMeta.title, -}; - -export default function AdminReportsPage() { - return ( - - - - ); -} diff --git a/src/app/admin/(shell)/risk/draws/[drawId]/layout.tsx b/src/app/admin/(shell)/risk/draws/[drawId]/layout.tsx index 17048c7..72363c0 100644 --- a/src/app/admin/(shell)/risk/draws/[drawId]/layout.tsx +++ b/src/app/admin/(shell)/risk/draws/[drawId]/layout.tsx @@ -11,7 +11,7 @@ export default async function AdminRiskDrawLayout(props: { const safeId = Number.isFinite(id) ? id : 0; return ( - + {props.children} diff --git a/src/app/admin/(shell)/settings/currencies/page.tsx b/src/app/admin/(shell)/settings/currencies/page.tsx new file mode 100644 index 0000000..6c7bca9 --- /dev/null +++ b/src/app/admin/(shell)/settings/currencies/page.tsx @@ -0,0 +1,10 @@ +import { redirect } from "next/navigation"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "币种管理", +}; + +export default function AdminCurrencySettingsPage() { + redirect("/admin/currencies"); +} diff --git a/src/app/admin/(shell)/tickets/page.tsx b/src/app/admin/(shell)/tickets/page.tsx index 4e30089..c717c08 100644 --- a/src/app/admin/(shell)/tickets/page.tsx +++ b/src/app/admin/(shell)/tickets/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function AdminTicketsPage() { return ( - + ); diff --git a/src/app/admin/(shell)/wallet/layout.tsx b/src/app/admin/(shell)/wallet/layout.tsx index ca1480e..94e0f19 100644 --- a/src/app/admin/(shell)/wallet/layout.tsx +++ b/src/app/admin/(shell)/wallet/layout.tsx @@ -4,8 +4,10 @@ import { WalletSubnav } from "@/modules/wallet/wallet-subnav"; export default function AdminWalletLayout({ children }: { children: ReactNode }) { return ( -
- +
+
+ +
{children}
); diff --git a/src/app/admin/(shell)/wallet/player/page.tsx b/src/app/admin/(shell)/wallet/player/page.tsx index 208ce1f..3e5fc9d 100644 --- a/src/app/admin/(shell)/wallet/player/page.tsx +++ b/src/app/admin/(shell)/wallet/player/page.tsx @@ -1,4 +1,3 @@ -import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { walletModuleMeta } from "@/modules/wallet/meta"; import { PlayerWalletPanel } from "@/modules/wallet/wallet-console"; import type { Metadata } from "next"; @@ -8,9 +7,5 @@ export const metadata: Metadata = { }; export default function AdminWalletPlayerPage() { - return ( - - - - ); + return ; } diff --git a/src/app/admin/(shell)/wallet/transactions/page.tsx b/src/app/admin/(shell)/wallet/transactions/page.tsx index fe67200..569710a 100644 --- a/src/app/admin/(shell)/wallet/transactions/page.tsx +++ b/src/app/admin/(shell)/wallet/transactions/page.tsx @@ -1,4 +1,3 @@ -import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { walletModuleMeta } from "@/modules/wallet/meta"; import { WalletTxnsPanel } from "@/modules/wallet/wallet-console"; import type { Metadata } from "next"; @@ -8,9 +7,5 @@ export const metadata: Metadata = { }; export default function AdminWalletTransactionsPage() { - return ( - - - - ); + return ; } diff --git a/src/app/admin/(shell)/wallet/transfer-orders/page.tsx b/src/app/admin/(shell)/wallet/transfer-orders/page.tsx index 3be128f..18c0325 100644 --- a/src/app/admin/(shell)/wallet/transfer-orders/page.tsx +++ b/src/app/admin/(shell)/wallet/transfer-orders/page.tsx @@ -1,4 +1,3 @@ -import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { walletModuleMeta } from "@/modules/wallet/meta"; import { TransferOrdersPanel } from "@/modules/wallet/wallet-console"; import type { Metadata } from "next"; @@ -8,9 +7,5 @@ export const metadata: Metadata = { }; export default function AdminWalletTransferOrdersPage() { - return ( - - - - ); + return ; } diff --git a/src/app/globals.css b/src/app/globals.css index f7cf144..03cce09 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -131,27 +131,27 @@ @layer components { .admin-list-card { - @apply overflow-hidden border-border/80 shadow-sm; + @apply overflow-hidden border-border/80 bg-card shadow-sm; } .admin-list-header { - @apply border-b bg-muted/20 pb-4; + @apply border-b border-border/70 bg-gradient-to-b from-muted/25 to-transparent pb-5; } .admin-list-title { - @apply text-lg font-semibold tracking-tight; + @apply text-[1.05rem] font-semibold tracking-tight text-[#13315f]; } .admin-list-content { - @apply space-y-4; + @apply space-y-5; } .admin-list-toolbar { - @apply flex w-full flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center; + @apply flex w-full flex-col gap-3 border-t border-border/60 pt-4 xl:flex-row xl:flex-wrap xl:items-center; } .admin-list-field { - @apply flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:shrink-0 sm:gap-1.5; + @apply flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:gap-2; } .admin-list-field > label { @@ -159,16 +159,15 @@ } .admin-list-actions { - @apply flex shrink-0 flex-wrap gap-2; - margin-left: auto; + @apply flex shrink-0 flex-wrap items-center gap-2 xl:ml-auto xl:justify-end; } .admin-table-shell { - @apply overflow-x-auto rounded-xl border border-border bg-background; + @apply overflow-x-auto rounded-2xl border border-border/80 bg-card shadow-sm; } .admin-table-toolbar { - @apply flex items-center justify-end border-b border-border bg-muted/15 px-3 py-2.5; + @apply flex items-center justify-end border-b border-border/70 bg-muted/20 px-4 py-2.5; } .admin-inline-note { diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index 7b0a9a8..c8e97b9 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -26,6 +26,7 @@ const NAV_TRANSLATION_KEYS: Record = { admin_users: "admin_users", admin_roles: "admin_roles", players: "players", + currencies: "currencies", wallet: "wallet", draws: "draws", config: "config", @@ -33,11 +34,14 @@ const NAV_TRANSLATION_KEYS: Record = { settlement: "settlement", reconcile: "reconcile", tickets: "tickets", - reports: "reports", audit: "audit", settings: "settings", }; +const SETTINGS_ROUTE_LABELS: Record = { + currencies: "currencies.title", +}; + function titleCase(value: string): string { return value .split("-") @@ -52,7 +56,7 @@ type BreadcrumbCrumb = { }; export function AdminBreadcrumb() { - const { t } = useTranslation(["common", "dashboard", "reports", "audit", "config", "draws"]); + const { t } = useTranslation(["common", "dashboard", "audit", "config", "draws"]); const pathname = usePathname(); const profile = useAdminProfile(); const navItems = profile?.navigation ?? []; @@ -105,6 +109,11 @@ export function AdminBreadcrumb() { let subLabel = ""; if (businessSegment === "config" && subSegment) { subLabel = t(`nav.items.${subSegment}`, { ns: "config", defaultValue: titleCase(subSegment) }); + } else if (businessSegment === "settings" && subSegment) { + subLabel = t(SETTINGS_ROUTE_LABELS[subSegment] ?? `settings.${subSegment}`, { + ns: "config", + defaultValue: titleCase(subSegment), + }); } else { subLabel = subSegment ? t(`subnav.${subSegment}`, { diff --git a/src/components/admin/admin-list-pagination-footer.tsx b/src/components/admin/admin-list-pagination-footer.tsx index af62765..45c6eba 100644 --- a/src/components/admin/admin-list-pagination-footer.tsx +++ b/src/components/admin/admin-list-pagination-footer.tsx @@ -117,8 +117,8 @@ export function AdminListPaginationFooter({ }) { const { t } = useTranslation(["common"]); return ( -
-

+

+

{t("pagination.summary", { total, page, diff --git a/src/components/admin/admin-sidebar.tsx b/src/components/admin/admin-sidebar.tsx index f7d07aa..35ecfd9 100644 --- a/src/components/admin/admin-sidebar.tsx +++ b/src/components/admin/admin-sidebar.tsx @@ -22,17 +22,21 @@ import { adminNavIconBySegment } from "@/modules/_config/admin-nav-icons"; import { ADMIN_BASE } from "@/modules/_config/admin-nav"; import { useAdminProfile } from "@/stores/admin-session"; -function isActive(pathname: string, item: { href: string; activeMatchPrefix?: string }): boolean { - const { href, activeMatchPrefix } = item; +function isActive(pathname: string, item: { href: string; activeMatchPrefix?: string; segment?: string }): boolean { + const { href, activeMatchPrefix, segment } = item; const prefix = activeMatchPrefix ?? href; if (prefix === ADMIN_BASE || prefix === `${ADMIN_BASE}/`) { return pathname === ADMIN_BASE || pathname === `${ADMIN_BASE}/`; } + // Keep "settings" independent from its child routes like /admin/settings/currencies. + if (segment === "settings") { + return pathname === href; + } return pathname === prefix || pathname.startsWith(`${prefix}/`); } export function AdminAppSidebar() { - const { t } = useTranslation(["common", "dashboard", "players", "draws", "config", "wallet", "risk", "settlement", "jackpot", "reconcile", "tickets", "reports", "audit"]); + const { t } = useTranslation(["common", "dashboard", "players", "draws", "config", "wallet", "risk", "settlement", "jackpot", "reconcile", "tickets", "audit"]); const pathname = usePathname(); const profile = useAdminProfile(); const visibleNav = useMemo( diff --git a/src/components/admin/module-scaffold.tsx b/src/components/admin/module-scaffold.tsx index 640138d..e523ac3 100644 --- a/src/components/admin/module-scaffold.tsx +++ b/src/components/admin/module-scaffold.tsx @@ -9,5 +9,14 @@ type ModuleScaffoldProps = { /** 内容区容器;模块标题由侧栏导航体现,此处不再重复大标题与说明。 */ export function ModuleScaffold({ children, className }: ModuleScaffoldProps) { - return

{children}
; + return ( +
+ {children} +
+ ); } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 3b10e8d..eefe171 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -12,7 +12,7 @@ function Card({ data-slot="card" data-size={size} className={cn( - "group/card flex flex-col gap-4 overflow-hidden rounded-lg border border-border bg-card py-4 text-sm text-card-foreground shadow-[0_6px_18px_rgb(15_48_96_/_5%)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg", + "group/card flex flex-col gap-4 overflow-hidden rounded-2xl border border-border/60 bg-card py-5 text-sm text-card-foreground shadow-none has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-2xl *:[img:last-child]:rounded-b-2xl", className )} {...props} @@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
) { return (
) @@ -84,7 +84,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
) { return (
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { return ( ) @@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) { ) {
) { | null = null; + +export function getCachedAdminCurrencies(): AdminCurrencyRow[] { + return cachedAdminCurrencies; +} + +export function useAdminCurrencyCatalog() { + useEffect(() => { + if (cachedAdminCurrencies.length > 0 || inflightAdminCurrencyLoad !== null) { + return; + } + + inflightAdminCurrencyLoad = getAdminCurrencies() + .then((data) => { + cachedAdminCurrencies = data.items; + }) + .catch(() => { + // 币种目录失败时回退默认 2 位小数,不阻断后台页面。 + }) + .finally(() => { + inflightAdminCurrencyLoad = null; + }); + }, []); +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index cffb99f..45de9b3 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -12,7 +12,6 @@ import enConfig from "@/i18n/locales/en/config.json"; import enDashboard from "@/i18n/locales/en/dashboard.json"; import enDraws from "@/i18n/locales/en/draws.json"; import enJackpot from "@/i18n/locales/en/jackpot.json"; -import enReports from "@/i18n/locales/en/reports.json"; import enRisk from "@/i18n/locales/en/risk.json"; import enSettlement from "@/i18n/locales/en/settlement.json"; import enPlayers from "@/i18n/locales/en/players.json"; @@ -27,7 +26,6 @@ import neConfig from "@/i18n/locales/ne/config.json"; import neDashboard from "@/i18n/locales/ne/dashboard.json"; import neDraws from "@/i18n/locales/ne/draws.json"; import neJackpot from "@/i18n/locales/ne/jackpot.json"; -import neReports from "@/i18n/locales/ne/reports.json"; import neRisk from "@/i18n/locales/ne/risk.json"; import neSettlement from "@/i18n/locales/ne/settlement.json"; import nePlayers from "@/i18n/locales/ne/players.json"; @@ -42,7 +40,6 @@ import zhConfig from "@/i18n/locales/zh/config.json"; import zhDashboard from "@/i18n/locales/zh/dashboard.json"; import zhDraws from "@/i18n/locales/zh/draws.json"; import zhJackpot from "@/i18n/locales/zh/jackpot.json"; -import zhReports from "@/i18n/locales/zh/reports.json"; import zhRisk from "@/i18n/locales/zh/risk.json"; import zhSettlement from "@/i18n/locales/zh/settlement.json"; import zhPlayers from "@/i18n/locales/zh/players.json"; @@ -54,7 +51,7 @@ export const ADMIN_SUPPORTED_LANGUAGES = ["en", "ne", "zh"] as const; export type AdminLanguage = (typeof ADMIN_SUPPORTED_LANGUAGES)[number]; export const ADMIN_DEFAULT_LANGUAGE: AdminLanguage = "zh"; -const namespaces = ["common", "auth", "dashboard", "reports", "audit", "draws", "settlement", "risk", "jackpot", "players", "tickets", "reconcile", "wallet", "adminUsers", "config"] as const; +const namespaces = ["common", "auth", "dashboard", "audit", "draws", "settlement", "risk", "jackpot", "players", "tickets", "reconcile", "wallet", "adminUsers", "config"] as const; const resources = { en: { @@ -68,7 +65,6 @@ const resources = { players: enPlayers, tickets: enTickets, reconcile: enReconcile, - reports: enReports, risk: enRisk, audit: enAudit, settlement: enSettlement, @@ -85,7 +81,6 @@ const resources = { players: nePlayers, tickets: neTickets, reconcile: neReconcile, - reports: neReports, risk: neRisk, audit: neAudit, settlement: neSettlement, @@ -102,7 +97,6 @@ const resources = { players: zhPlayers, tickets: zhTickets, reconcile: zhReconcile, - reports: zhReports, risk: zhRisk, audit: zhAudit, settlement: zhSettlement, diff --git a/src/i18n/locales/en/adminUsers.json b/src/i18n/locales/en/adminUsers.json index 17f3df9..ff50dff 100644 --- a/src/i18n/locales/en/adminUsers.json +++ b/src/i18n/locales/en/adminUsers.json @@ -133,7 +133,6 @@ "jackpot": "Jackpot", "reconcile": "Reconcile", "tickets": "Tickets", - "reports": "Reports", "audit": "Audit Logs", "settings": "Settings" }, @@ -141,6 +140,7 @@ "prd.admin_user.manage": "Admin Users · Manage", "prd.admin_role.manage": "Role Management · Manage", "prd.users.manage": "Players · Manage", + "prd.currency.manage": "Currency Management · Manage", "prd.users.view_finance": "Players · View Finance", "prd.users.view_cs": "Players · View Customer Service Cases", "prd.player_freeze.manage": "Freeze/Unfreeze Player · Manage", @@ -162,10 +162,6 @@ "prd.payout.manage": "Payout Confirmation · Manage", "prd.payout.review": "Payout Confirmation · Review", "prd.payout.view": "Payout Confirmation · View", - "prd.report.all": "Reports · All", - "prd.report.risk": "Reports · Risk", - "prd.report.finance": "Reports · Finance", - "prd.report.player": "Reports · Single Player", "prd.audit.all": "Audit Logs · All", "prd.audit.self": "Audit Logs · Related to Self", "prd.audit.finance": "Audit Logs · Finance Related" diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index a39f1b5..7afec99 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -62,6 +62,7 @@ "admin_users": "Admin Users", "admin_roles": "Role Management", "players": "Players", + "currencies": "Currencies", "wallet": "Wallet", "draws": "Draws", "config": "Configuration", @@ -70,7 +71,6 @@ "jackpot": "Jackpot", "reconcile": "Reconcile", "tickets": "Ticket list", - "reports": "Reports", "audit": "Audit Logs", "settings": "Settings" }, diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index 771c9e3..eb378d5 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -94,6 +94,54 @@ }, "discard": "Discard changes" }, + "currencies": { + "title": "Currency management", + "description": "Maintain currency master data for admin operations and control whether a currency is enabled or allowed for betting.", + "loading": "Loading currencies…", + "empty": "No currencies yet.", + "loadFailed": "Failed to load currencies", + "createSuccess": "Currency created", + "createFailed": "Failed to create currency", + "updateSuccess": "Currency updated", + "updateFailed": "Failed to update currency", + "deleteSuccess": "Currency {{code}} deleted", + "deleteFailed": "Failed to delete currency", + "actions": { + "create": "Add currency", + "edit": "Edit", + "delete": "Delete", + "openStandalone": "Open dedicated page", + "backToSettings": "Back to settings" + }, + "table": { + "code": "Code", + "name": "Name", + "decimals": "Decimals", + "enabled": "Enabled", + "bettable": "Bettable", + "actions": "Actions" + }, + "dialog": { + "createTitle": "Add currency", + "editTitle": "Edit currency", + "description": "Currency code is immutable after creation. Disabling a currency also turns off bettable status." + }, + "deleteDialog": { + "title": "Delete currency?", + "description": "Delete currency {{code}}? The system blocks deletion when it is still referenced by defaults, wallets, tickets, odds, or jackpot data." + }, + "form": { + "code": "Currency code", + "name": "Currency name", + "decimals": "Decimal places", + "enabled": "Enabled status", + "enabledHint": "Disabled currencies should not be used for new business.", + "bettable": "Allow betting", + "bettableHint": "Only enabled currencies can be marked as bettable.", + "required": "Please fill in the required fields", + "decimalInvalid": "Enter a valid decimal place value" + } + }, "play": { "batchGroups": { "d2": "2D Global", diff --git a/src/i18n/locales/en/dashboard.json b/src/i18n/locales/en/dashboard.json index 08da297..3ced7a3 100644 --- a/src/i18n/locales/en/dashboard.json +++ b/src/i18n/locales/en/dashboard.json @@ -46,7 +46,6 @@ "results": "Results", "tickets": "Ticket management", "walletTransactions": "Wallet transactions", - "reports": "Reports", "auditLogs": "Audit logs" }, "warnings": { diff --git a/src/i18n/locales/en/reconcile.json b/src/i18n/locales/en/reconcile.json index ba27ac0..5ae8ac1 100644 --- a/src/i18n/locales/en/reconcile.json +++ b/src/i18n/locales/en/reconcile.json @@ -1,36 +1,46 @@ { "title": "Reconcile", "createTitle": "Create reconcile job", - "createDesc": "Manually check abnormal transfers by period or selected references. Scheduled reconciliation still runs automatically.", + "createDesc": "Manually check abnormal transfers by date range and optional player. Scheduled reconciliation still runs automatically.", "reconcileType": "Reconcile type", - "walletTransfer": "Wallet transfer (main site ⇄ lottery)", - "startTime": "Start time", - "endTime": "End time", - "scope": "Targets (optional)", - "scopePlaceholder": "One per line: player ID, transfer number, or main-site transaction number.\nLeave empty to check abnormal transfers in the selected period.", + "reconcileTypeFixed": "Wallet transfer (main site ⇄ lottery)", + "reconcileTypeHint": "Only wallet transfer is currently supported.", + "dateRange": "Reconcile date range", "createTask": "Create reconcile job", "submitting": "Submitting…", "loadFailed": "Failed to load", "loadItemsFailed": "Failed to load details", - "periodRequired": "Enter both reconcile start and end time", - "periodInvalid": "Invalid time range", + "periodRequired": "Enter both reconcile start and end dates", + "periodInvalid": "Invalid date range", "periodOrderInvalid": "End time must be later than or equal to start time", "createSuccess": "Reconcile job created", "createFailed": "Failed to create job", "noCreatePermission": "Current account cannot create reconcile jobs.", "jobsTitle": "Reconcile jobs", - "jobsDesc": "Click a row to view paginated item details.", + "jobsDesc": "Use the action on the right to open paginated item details.", "refresh": "Refresh", "jobNo": "Job no.", "type": "Type", "status": "Status", "period": "Period", "createdAt": "Created at", + "operate": "Action", + "view": "View", "detailsTitle": "Job details", "sideARef": "Lottery ref", "sideBRef": "Main site ref", "differenceAmount": "Difference (cent)", "noDetails": "No details", + "playerSearch": "Player (optional)", + "playerSearchPlaceholder": "Search by player ID / username / nickname", + "playerSearchHint": "After selection, reconciliation is limited to this player in the chosen date range.", + "playerSearchEmpty": "Enter a keyword to search players.", + "playerNoResults": "No matching players", + "playerChoose": "Choose", + "playerSelected": "Selected player", + "playerSelectedShort": "Selected", + "playerClear": "Clear", + "loadingPlayers": "Searching players…", "statusCompleted": "Completed", "statusRunning": "Running", "statusFailed": "Failed", diff --git a/src/i18n/locales/en/reports.json b/src/i18n/locales/en/reports.json deleted file mode 100644 index 321c9a0..0000000 --- a/src/i18n/locales/en/reports.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "title": "Reports", - "createExport": "Create export", - "reportType": "Report type", - "exportFormat": "Export format", - "filterJson": "filter_json (optional)", - "parseFilterFailed": "Failed to parse filter JSON", - "createSuccess": "Export job created", - "createFailed": "Failed to create job", - "downloadFailed": "Download failed", - "taskList": "Job list", - "jobId": "Job no.", - "type": "Type", - "format": "Format", - "status": "Status", - "output": "Output", - "download": "Download", - "createdAt": "Created at", - "id": "ID", - "empty": "No data", - "formatOptions": { - "csv": "CSV", - "xlsx": "Excel" - }, - "statusOptions": { - "pending": "Pending", - "queued": "Queued", - "running": "Running", - "completed": "Completed", - "failed": "Failed" - }, - "reportTypes": { - "draw_profit_summary": "Draw profit summary", - "daily_profit_summary": "Daily profit summary", - "player_win_loss": "Player win/loss report", - "wallet_transfer_report": "Wallet transfer report", - "hot_number_risk_report": "Hot number risk report", - "play_dimension_report": "Play dimension report", - "sold_out_number_report": "Sold-out number report", - "rebate_commission_report": "Rebate and commission report", - "audit_operation_report": "Audit operation report", - "wallet_txns_daily": "Wallet transactions daily", - "transfer_orders_daily": "Transfer orders daily" - } -} diff --git a/src/i18n/locales/ne/adminUsers.json b/src/i18n/locales/ne/adminUsers.json index a6ed515..f918055 100644 --- a/src/i18n/locales/ne/adminUsers.json +++ b/src/i18n/locales/ne/adminUsers.json @@ -133,7 +133,6 @@ "jackpot": "ज्याकपोट", "reconcile": "मिलान", "tickets": "टिकटहरू", - "reports": "रिपोर्टहरू", "audit": "अडिट लग", "settings": "सेटिङ" }, @@ -141,6 +140,7 @@ "prd.admin_user.manage": "प्रशासक सूची · व्यवस्थापन", "prd.admin_role.manage": "भूमिका व्यवस्थापन · व्यवस्थापन", "prd.users.manage": "खेलाडी व्यवस्थापन · व्यवस्थापन", + "prd.currency.manage": "मुद्रा व्यवस्थापन · व्यवस्थापन", "prd.users.view_finance": "खेलाडी व्यवस्थापन · वित्त हेर्नुहोस्", "prd.users.view_cs": "खेलाडी व्यवस्थापन · ग्राहक सेवा एकल प्रयोगकर्ता", "prd.player_freeze.manage": "खेलाडी रोक्ने/फुकाउने · व्यवस्थापन", @@ -162,10 +162,6 @@ "prd.payout.manage": "भुक्तानी पुष्टि · व्यवस्थापन", "prd.payout.review": "भुक्तानी पुष्टि · समीक्षा", "prd.payout.view": "भुक्तानी पुष्टि · हेर्नुहोस्", - "prd.report.all": "रिपोर्ट · सबै", - "prd.report.risk": "रिपोर्ट · जोखिम", - "prd.report.finance": "रिपोर्ट · वित्त", - "prd.report.player": "रिपोर्ट · एकल खेलाडी", "prd.audit.all": "अडिट लग · सबै", "prd.audit.self": "अडिट लग · आफूसँग सम्बन्धित", "prd.audit.finance": "अडिट लग · वित्त सम्बन्धित" diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index e7567ff..a550d93 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -62,6 +62,7 @@ "admin_users": "प्रशासक सूची", "admin_roles": "भूमिका व्यवस्थापन", "players": "खेलाडी सूची", + "currencies": "मुद्रा व्यवस्थापन", "wallet": "वालेट", "draws": "ड्रअहरू", "config": "कन्फिगरेसन", @@ -70,7 +71,6 @@ "jackpot": "Jackpot", "reconcile": "मिलान", "tickets": "टिकट सूची", - "reports": "रिपोर्टहरू", "audit": "अडिट लग", "settings": "सेटिङ" }, diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index fd4b60d..4002bee 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -94,6 +94,54 @@ }, "discard": "परिवर्तन त्याग्नुहोस्" }, + "currencies": { + "title": "मुद्रा व्यवस्थापन", + "description": "एडमिन सञ्चालनका लागि मुद्रा master data राख्नुहोस् र मुद्रा सक्रिय वा बेटिङका लागि उपलब्ध छ कि छैन नियन्त्रण गर्नुहोस्।", + "loading": "मुद्रा सूची लोड हुँदैछ…", + "empty": "अहिलेसम्म मुद्रा छैन।", + "loadFailed": "मुद्रा सूची लोड गर्न असफल", + "createSuccess": "मुद्रा सिर्जना भयो", + "createFailed": "मुद्रा सिर्जना असफल भयो", + "updateSuccess": "मुद्रा अद्यावधिक भयो", + "updateFailed": "मुद्रा अद्यावधिक गर्न असफल", + "deleteSuccess": "मुद्रा {{code}} मेटाइयो", + "deleteFailed": "मुद्रा मेटाउन असफल", + "actions": { + "create": "मुद्रा थप्नुहोस्", + "edit": "सम्पादन", + "delete": "मेटाउनुहोस्", + "openStandalone": "अलग पृष्ठ खोल्नुहोस्", + "backToSettings": "सेटिङमा फर्कनुहोस्" + }, + "table": { + "code": "कोड", + "name": "नाम", + "decimals": "दशमलव स्थान", + "enabled": "सक्रिय", + "bettable": "बेटिङयोग्य", + "actions": "कार्य" + }, + "dialog": { + "createTitle": "मुद्रा थप्नुहोस्", + "editTitle": "मुद्रा सम्पादन गर्नुहोस्", + "description": "मुद्रा कोड सिर्जना भएपछि परिवर्तन गर्न मिल्दैन। मुद्रा निष्क्रिय गर्दा bettable पनि स्वतः बन्द हुन्छ।" + }, + "deleteDialog": { + "title": "मुद्रा मेटाउने पुष्टि", + "description": "मुद्रा {{code}} मेटाउने? यदि यो मुद्रा default, wallet, ticket, odds वा jackpot डाटामा प्रयोग भएको छ भने प्रणालीले मेटाउन दिँदैन।" + }, + "form": { + "code": "मुद्रा कोड", + "name": "मुद्रा नाम", + "decimals": "दशमलव स्थान", + "enabled": "सक्रिय स्थिति", + "enabledHint": "निष्क्रिय मुद्रा नयाँ व्यवसायमा प्रयोग गर्नु हुँदैन।", + "bettable": "बेटिङ अनुमति", + "bettableHint": "सक्रिय मुद्रा मात्र bettable बनाउन सकिन्छ।", + "required": "कृपया आवश्यक फिल्ड भर्नुहोस्", + "decimalInvalid": "मान्य दशमलव स्थान प्रविष्ट गर्नुहोस्" + } + }, "play": { "batchGroups": { "d2": "2D ग्लोबल", diff --git a/src/i18n/locales/ne/dashboard.json b/src/i18n/locales/ne/dashboard.json index 64e39d9..ff90def 100644 --- a/src/i18n/locales/ne/dashboard.json +++ b/src/i18n/locales/ne/dashboard.json @@ -46,7 +46,6 @@ "results": "परिणाम", "tickets": "टिकट व्यवस्थापन", "walletTransactions": "वालेट कारोबार", - "reports": "रिपोर्ट", "auditLogs": "अडिट लग" }, "warnings": { diff --git a/src/i18n/locales/ne/reconcile.json b/src/i18n/locales/ne/reconcile.json index 56d56a3..05bfb47 100644 --- a/src/i18n/locales/ne/reconcile.json +++ b/src/i18n/locales/ne/reconcile.json @@ -1,36 +1,46 @@ { "title": "मिलान", "createTitle": "म्यानुअल मिलान कार्य", - "createDesc": "समय अवधि वा छानिएका सन्दर्भहरूबाट असामान्य ट्रान्सफर म्यानुअल रूपमा जाँच गर्नुहोस्। scheduled reconciliation स्वतः चलिरहन्छ।", + "createDesc": "मिति दायरा र वैकल्पिक खेलाडी चयनबाट असामान्य ट्रान्सफर म्यानुअल रूपमा जाँच गर्नुहोस्। scheduled reconciliation स्वतः चलिरहन्छ।", "reconcileType": "मिलान प्रकार", - "walletTransfer": "वालेट ट्रान्सफर (मुख्य साइट ⇄ लटरी)", - "startTime": "सुरु समय", - "endTime": "अन्त्य समय", - "scope": "लक्षित सन्दर्भ (वैकल्पिक)", - "scopePlaceholder": "प्रति लाइन एउटा: player ID, transfer no, वा main-site transaction no.\nखाली छोडेमा चयन गरिएको अवधिका असामान्य ट्रान्सफर जाँच हुन्छ।", + "reconcileTypeFixed": "वालेट ट्रान्सफर (मुख्य साइट ⇄ लटरी)", + "reconcileTypeHint": "हाल वालेट ट्रान्सफर मात्र समर्थित छ।", + "dateRange": "मिलान मिति दायरा", "createTask": "मिलान कार्य सिर्जना", "submitting": "पेश हुँदैछ…", "loadFailed": "लोड असफल भयो", "loadItemsFailed": "विवरण लोड असफल भयो", - "periodRequired": "सुरु र अन्त्य समय दुवै लेख्नुहोस्", - "periodInvalid": "अवैध समय दायरा", + "periodRequired": "सुरु र अन्त्य मिति दुवै लेख्नुहोस्", + "periodInvalid": "अवैध मिति दायरा", "periodOrderInvalid": "अन्त्य समय सुरु समयभन्दा पछाडि वा बराबर हुनुपर्छ", "createSuccess": "मिलान कार्य सिर्जना भयो", "createFailed": "कार्य सिर्जना असफल भयो", "noCreatePermission": "हालको खातासँग मिलान कार्य सिर्जना गर्ने अनुमति छैन।", "jobsTitle": "मिलान कार्यहरू", - "jobsDesc": "विवरण हेर्न row क्लिक गर्नुहोस्।", + "jobsDesc": "दायाँपट्टिको कार्यबाट विवरण खोल्नुहोस्।", "refresh": "रिफ्रेस", "jobNo": "कार्य नं.", "type": "प्रकार", "status": "स्थिति", "period": "अवधि", "createdAt": "सिर्जना समय", + "operate": "कार्य", + "view": "हेर्नुहोस्", "detailsTitle": "कार्य विवरण", "sideARef": "लटरी साइड सन्दर्भ", "sideBRef": "मुख्य साइट सन्दर्भ", "differenceAmount": "अन्तर (cent)", "noDetails": "विवरण छैन", + "playerSearch": "खेलाडी (वैकल्पिक)", + "playerSearchPlaceholder": "player ID / username / nickname बाट खोज्नुहोस्", + "playerSearchHint": "चयनपछि छनोट गरिएको मिति दायरामा सो खेलाडी मात्र मिलान हुन्छ।", + "playerSearchEmpty": "खेलाडी खोज्न कुञ्जी शब्द लेख्नुहोस्।", + "playerNoResults": "मिल्ने खेलाडी भेटिएन", + "playerChoose": "छान्नुहोस्", + "playerSelected": "छानिएको खेलाडी", + "playerSelectedShort": "छानियो", + "playerClear": "खाली गर्नुहोस्", + "loadingPlayers": "खेलाडी खोजिँदै…", "statusCompleted": "सम्पन्न", "statusRunning": "चलिरहेको", "statusFailed": "असफल", diff --git a/src/i18n/locales/ne/reports.json b/src/i18n/locales/ne/reports.json deleted file mode 100644 index 19d1ad9..0000000 --- a/src/i18n/locales/ne/reports.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "title": "रिपोर्ट", - "createExport": "निर्यात सिर्जना", - "reportType": "रिपोर्ट प्रकार", - "exportFormat": "निर्यात ढाँचा", - "filterJson": "फिल्टर JSON (वैकल्पिक)", - "parseFilterFailed": "फिल्टर JSON पार्स गर्न सकिएन", - "createSuccess": "निर्यात कार्य सिर्जना भयो", - "createFailed": "कार्य सिर्जना असफल भयो", - "downloadFailed": "डाउनलोड असफल भयो", - "taskList": "कार्य सूची", - "jobId": "कार्य नं.", - "type": "प्रकार", - "format": "ढाँचा", - "status": "स्थिति", - "output": "आउटपुट", - "download": "डाउनलोड", - "createdAt": "सिर्जना समय", - "id": "ID", - "empty": "डाटा छैन", - "formatOptions": { - "csv": "CSV", - "xlsx": "Excel" - }, - "statusOptions": { - "pending": "पेन्डिङ", - "queued": "पर्खाइमा", - "running": "चल्दैछ", - "completed": "सम्पन्न", - "failed": "असफल" - }, - "reportTypes": { - "draw_profit_summary": "ड्रअ नाफा सारांश", - "daily_profit_summary": "दैनिक नाफा सारांश", - "player_win_loss": "खेलाडी जित/हार रिपोर्ट", - "wallet_transfer_report": "वालेट ट्रान्सफर रिपोर्ट", - "hot_number_risk_report": "हट नम्बर जोखिम रिपोर्ट", - "play_dimension_report": "प्ले डाइमेन्सन रिपोर्ट", - "sold_out_number_report": "बिक्री समाप्त नम्बर रिपोर्ट", - "rebate_commission_report": "रिबेट र कमिसन रिपोर्ट", - "audit_operation_report": "अडिट अपरेशन रिपोर्ट", - "wallet_txns_daily": "वालेट कारोबार दैनिक", - "transfer_orders_daily": "ट्रान्सफर अर्डर दैनिक" - } -} diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json index 25231a8..161c8d9 100644 --- a/src/i18n/locales/zh/adminUsers.json +++ b/src/i18n/locales/zh/adminUsers.json @@ -133,7 +133,6 @@ "jackpot": "奖池", "reconcile": "对账", "tickets": "玩家注单", - "reports": "报表导出", "audit": "审计日志", "settings": "系统设置" }, @@ -141,6 +140,7 @@ "prd.admin_user.manage": "管理员列表·可管理", "prd.admin_role.manage": "角色管理·可管理", "prd.users.manage": "用户管理·可管理", + "prd.currency.manage": "币种管理·可管理", "prd.users.view_finance": "用户管理·财务查看", "prd.users.view_cs": "用户管理·客服单用户", "prd.player_freeze.manage": "冻结/解冻玩家·可管理", @@ -162,10 +162,6 @@ "prd.payout.manage": "派彩确认·可管理", "prd.payout.review": "派彩确认·可审核", "prd.payout.view": "派彩确认·查看", - "prd.report.all": "报表·全部", - "prd.report.risk": "报表·风控", - "prd.report.finance": "报表·财务", - "prd.report.player": "报表·单用户", "prd.audit.all": "审计日志·全部", "prd.audit.self": "审计日志·自身相关", "prd.audit.finance": "审计日志·资金相关" diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index fe7fba5..0e1881f 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -62,6 +62,7 @@ "admin_users": "管理列表", "admin_roles": "角色管理", "players": "玩家列表", + "currencies": "币种管理", "wallet": "钱包流水", "draws": "期号列表", "config": "运营配置", @@ -70,7 +71,6 @@ "jackpot": "奖池", "reconcile": "对账", "tickets": "注单列表", - "reports": "报表导出", "audit": "审计日志", "settings": "系统设置" }, diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index 4db609b..4050df1 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -94,6 +94,54 @@ }, "discard": "放弃更改" }, + "currencies": { + "title": "币种管理", + "description": "维护后台可用的币种主数据,控制是否启用以及是否允许用于下注。", + "loading": "正在加载币种列表…", + "empty": "暂无币种。", + "loadFailed": "币种列表加载失败", + "createSuccess": "币种已创建", + "createFailed": "币种创建失败", + "updateSuccess": "币种已更新", + "updateFailed": "币种更新失败", + "deleteSuccess": "币种 {{code}} 已删除", + "deleteFailed": "币种删除失败", + "actions": { + "create": "新增币种", + "edit": "编辑", + "delete": "删除", + "openStandalone": "进入独立页面", + "backToSettings": "返回系统设置" + }, + "table": { + "code": "代码", + "name": "名称", + "decimals": "小数位", + "enabled": "启用", + "bettable": "可下注", + "actions": "操作" + }, + "dialog": { + "createTitle": "新增币种", + "editTitle": "编辑币种", + "description": "币种代码创建后不可修改;禁用币种时会自动关闭“可下注”。" + }, + "deleteDialog": { + "title": "确认删除币种", + "description": "确定删除币种 {{code}} 吗?如果该币种已被默认配置、钱包、注单、赔率或奖池引用,系统会阻止删除。" + }, + "form": { + "code": "币种代码", + "name": "币种名称", + "decimals": "小数位", + "enabled": "启用状态", + "enabledHint": "关闭后,新业务不应继续使用该币种。", + "bettable": "允许下注", + "bettableHint": "仅启用中的币种才可设置为可下注。", + "required": "请先填写必填字段", + "decimalInvalid": "请输入合法的小数位" + } + }, "play": { "batchGroups": { "d2": "2D 全局", diff --git a/src/i18n/locales/zh/dashboard.json b/src/i18n/locales/zh/dashboard.json index 5be5169..0e45b4b 100644 --- a/src/i18n/locales/zh/dashboard.json +++ b/src/i18n/locales/zh/dashboard.json @@ -46,7 +46,6 @@ "results": "开奖结果", "tickets": "注单管理", "walletTransactions": "钱包流水", - "reports": "报表中心", "auditLogs": "审计日志" }, "warnings": { diff --git a/src/i18n/locales/zh/reconcile.json b/src/i18n/locales/zh/reconcile.json index 44e16a1..92ebf50 100644 --- a/src/i18n/locales/zh/reconcile.json +++ b/src/i18n/locales/zh/reconcile.json @@ -1,36 +1,46 @@ { "title": "对账", "createTitle": "人工发起对账", - "createDesc": "用于按时间段或指定单据人工核对异常转账。系统定时对账仍会自动执行。", + "createDesc": "用于按日期范围并可选指定玩家,人工核对异常转账。系统定时对账仍会自动执行。", "reconcileType": "对账类型", - "walletTransfer": "钱包划转(主站 ⇄ 彩票)", - "startTime": "对账开始时间", - "endTime": "对账结束时间", - "scope": "指定对象(可选)", - "scopePlaceholder": "每行一条:玩家 ID、划转单号或主站流水号。\n留空则核对所选时间段内的异常转账。", + "reconcileTypeFixed": "钱包划转(主站 ⇄ 彩票)", + "reconcileTypeHint": "当前仅支持钱包划转。", + "dateRange": "对账日期范围", "createTask": "创建对账任务", "submitting": "提交中…", "loadFailed": "加载失败", "loadItemsFailed": "加载明细失败", - "periodRequired": "请填写对账时间范围(开始与结束)", - "periodInvalid": "时间无效,请检查所选日期与时间", + "periodRequired": "请填写对账日期范围(开始与结束)", + "periodInvalid": "日期无效,请检查所选日期", "periodOrderInvalid": "结束时间需晚于或等于开始时间", "createSuccess": "已创建对账任务", "createFailed": "创建失败", "noCreatePermission": "当前账号无新建对账任务权限。", "jobsTitle": "对账任务", - "jobsDesc": "点击一行查看差异明细与分页。", + "jobsDesc": "在右侧操作中查看差异明细与分页。", "refresh": "刷新", "jobNo": "任务号", "type": "类型", "status": "状态", "period": "对账周期", "createdAt": "创建时间", + "operate": "操作", + "view": "查看", "detailsTitle": "任务明细", "sideARef": "彩票侧引用", "sideBRef": "主站侧引用", "differenceAmount": "差额(分)", "noDetails": "无明细", + "playerSearch": "指定玩家(可选)", + "playerSearchPlaceholder": "输入玩家 ID / 用户名 / 昵称搜索", + "playerSearchHint": "选择后只按该玩家核对所选日期范围内的异常转账。", + "playerSearchEmpty": "请输入关键词后选择玩家。", + "playerNoResults": "暂无匹配玩家", + "playerChoose": "选择", + "playerSelected": "已选玩家", + "playerSelectedShort": "已选", + "playerClear": "清除", + "loadingPlayers": "玩家搜索中…", "statusCompleted": "已完成", "statusRunning": "执行中", "statusFailed": "失败", diff --git a/src/i18n/locales/zh/reports.json b/src/i18n/locales/zh/reports.json deleted file mode 100644 index 5622605..0000000 --- a/src/i18n/locales/zh/reports.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "title": "报表", - "createExport": "新建导出", - "reportType": "报表类型", - "exportFormat": "导出格式", - "filterJson": "筛选条件 JSON(可选)", - "parseFilterFailed": "筛选 JSON 无法解析", - "createSuccess": "已创建导出任务", - "createFailed": "创建失败", - "downloadFailed": "下载失败", - "taskList": "任务列表", - "jobId": "任务号", - "type": "类型", - "format": "格式", - "status": "状态", - "output": "输出", - "download": "下载", - "createdAt": "创建时间", - "id": "ID", - "empty": "无数据", - "formatOptions": { - "csv": "CSV", - "xlsx": "Excel" - }, - "statusOptions": { - "pending": "待处理", - "queued": "排队中", - "running": "执行中", - "completed": "已完成", - "failed": "失败" - }, - "reportTypes": { - "draw_profit_summary": "期号盈亏", - "daily_profit_summary": "每日盈亏汇总", - "player_win_loss": "玩家输赢报表", - "wallet_transfer_report": "玩家转入转出报表", - "hot_number_risk_report": "热门号码风险报表", - "play_dimension_report": "玩法维度报表", - "sold_out_number_report": "售罄号码报表", - "rebate_commission_report": "佣金回水报表", - "audit_operation_report": "后台操作审计报表", - "wallet_txns_daily": "钱包流水日报", - "transfer_orders_daily": "转账单日报" - } -} diff --git a/src/lib/money.ts b/src/lib/money.ts index 241dae5..e148a1b 100644 --- a/src/lib/money.ts +++ b/src/lib/money.ts @@ -1,8 +1,35 @@ +import { getCachedAdminCurrencies } from "@/hooks/use-admin-currency-catalog"; + +const DEFAULT_DECIMAL_PLACES = 2; + +export function getAdminCurrencyDecimalPlaces(currencyCode: string | null | undefined): number { + const code = currencyCode?.trim().toUpperCase(); + if (!code) { + return DEFAULT_DECIMAL_PLACES; + } + + const row = getCachedAdminCurrencies().find((item) => item.code === code); + const decimals = row?.decimal_places; + if (typeof decimals === "number" && Number.isFinite(decimals) && decimals >= 0) { + return decimals; + } + + return DEFAULT_DECIMAL_PLACES; +} + /** 后台列表统一:最小货币单位 → 主货币展示(默认 2 位小数,与钱包一致) */ -export function formatAdminMinorUnits(minor: number, currencyCode = "NPR"): string { - const major = minor / 100; +export function formatAdminMinorUnits( + minor: number, + currencyCode = "NPR", + decimalPlaces?: number, +): string { + const resolvedDecimalPlaces = + typeof decimalPlaces === "number" && Number.isFinite(decimalPlaces) && decimalPlaces >= 0 + ? decimalPlaces + : getAdminCurrencyDecimalPlaces(currencyCode); + const major = minor / 10 ** resolvedDecimalPlaces; return `${currencyCode} ${major.toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, + minimumFractionDigits: resolvedDecimalPlaces, + maximumFractionDigits: resolvedDecimalPlaces, })}`; } diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index 1b370f0..dad3077 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -1,7 +1,7 @@ import type { LucideIcon } from "lucide-react"; import { CalendarClock, - FileSpreadsheet, + CircleDollarSign, Landmark, LayoutDashboard, LogIn, @@ -29,11 +29,11 @@ export const adminNavIconBySegment: Record wallet: Wallet, risk: ShieldAlert, settlement: Landmark, - reports: FileSpreadsheet, reconcile: Scale, audit: ScrollText, admin_users: ShieldCheck, admin_roles: ShieldCheck, + currencies: CircleDollarSign, settings: Settings, }; diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index 5538c5b..e080a90 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -10,11 +10,11 @@ export type AdminNavSegment = | "risk" | "settings" | "settlement" - | "reports" | "reconcile" | "audit" | "admin_users" - | "admin_roles"; + | "admin_roles" + | "currencies"; export type AdminNavItem = { label: string; diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx index 6813559..0bac59c 100644 --- a/src/modules/admin-roles/admin-roles-console.tsx +++ b/src/modules/admin-roles/admin-roles-console.tsx @@ -77,6 +77,10 @@ export function AdminRolesConsole(): React.ReactElement { () => roles.find((role) => role.id === selectedRoleId) ?? null, [roles, selectedRoleId], ); + const selectedPermissionSet = useMemo( + () => new Set(draftRolePermissions), + [draftRolePermissions], + ); const selectClassName = cn( "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors", @@ -141,6 +145,16 @@ export function AdminRolesConsole(): React.ReactElement { }); } + function toggleGroupPermissions(slugs: string[], checked: boolean): void { + setDraftRolePermissions((prev) => { + if (checked) { + return Array.from(new Set([...prev, ...slugs])).sort(); + } + const remove = new Set(slugs); + return prev.filter((value) => !remove.has(value)); + }); + } + function openCreateRole(): void { setRoleMode("create"); setEditingRoleId(null); @@ -182,6 +196,20 @@ export function AdminRolesConsole(): React.ReactElement { } } + function getGroupSelectionState(slugs: string[]): boolean | "indeterminate" { + if (slugs.length === 0) { + return false; + } + const selectedCount = slugs.filter((slug) => selectedPermissionSet.has(slug)).length; + if (selectedCount === 0) { + return false; + } + if (selectedCount === slugs.length) { + return true; + } + return "indeterminate"; + } + async function saveRolePermissions(): Promise { if (!selectedRole) { return; @@ -381,68 +409,94 @@ export function AdminRolesConsole(): React.ReactElement { - {t("rolePermissionDialog.title")} - + + {t("rolePermissionDialog.title")} + + {selectedRole ? selectedRole.name : null} -
-
+
+
{directPermissionGroups.map((group) => { const isOpen = isDirectGroupOpen(group.key); + const groupSlugs = group.permissions.map((permission) => permission.slug); const selectedCount = group.permissions.filter((permission) => - draftRolePermissions.includes(permission.slug), + selectedPermissionSet.has(permission.slug), ).length; + const checkedState = getGroupSelectionState(groupSlugs); return ( -
- + toggleGroupPermissions(groupSlugs, value === true)} /> - - {permissionGroupLabel(group.key, group.label, t)} - - + + {selectedCount}/{group.permissions.length} - +
{isOpen ? ( -
- {group.permissions.map((permission) => ( +
+ {group.permissions.map((permission, index) => ( ))}
) : null} - +
); })}
diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx index 6546c61..2dccebb 100644 --- a/src/modules/admin-users/admin-users-console.tsx +++ b/src/modules/admin-users/admin-users-console.tsx @@ -44,7 +44,7 @@ export function AdminUsersConsole(): React.ReactElement { const { t } = useTranslation(["adminUsers", "common"]); const profile = useAdminProfile(); const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(25); + const [perPage, setPerPage] = useState(10); const [keyword, setKeyword] = useState(""); const [query, setQuery] = useState(""); @@ -365,7 +365,7 @@ export function AdminUsersConsole(): React.ReactElement { {t("table.status")} {t("table.roles")} {t("table.effective")} - {t("table.actions")} + {t("table.actions")} @@ -414,8 +414,8 @@ export function AdminUsersConsole(): React.ReactElement {
{row.effective_permissions.length} - -
+ +
-
+
{statusCounts.map((s) => ( -
-

{s.label}

-

- {s.count} -

+
+ {s.label} + {s.count}
))}
-
+
{sortedVersions.length === 0 ? ( - +
{t("versionSwitcher.empty", { ns: "config" })} - +
) : ( STATUS_ORDER.map((status) => { const rows = groupedVersions.get(status) ?? []; @@ -188,8 +188,8 @@ export function ConfigVersionSwitcher({ return null; } return ( -
-
+
+
-

+

{t(`versionStatus.${status}`, { ns: "config" })}

-

+

{t("versionSwitcher.count", { ns: "config", count: rows.length })}

-
+
{rows.map((v) => { const isCurrent = selectedId === String(v.id); return ( - -
-
-
-
-
-
- - v{v.version_no} - - - - #{v.id} - -
-

- {t("versionSwitcher.effectiveAt", { - ns: "config", - value: v.effective_at ? formatDt(v.effective_at) : "—", - })} - {v.reason - ? ` · ${t("versionSwitcher.note", { - ns: "config", - value: v.reason, - })}` - : ""} -

-
- {isCurrent ? ( - - {t("versionSwitcher.current", { ns: "config" })} +
+
+
+
+
+ + v{v.version_no} - ) : null} + + + #{v.id} + +
+

+ {t("versionSwitcher.effectiveAt", { + ns: "config", + value: v.effective_at ? formatDt(v.effective_at) : "—", + })} + {v.reason + ? ` · ${t("versionSwitcher.note", { + ns: "config", + value: v.reason, + })}` + : ""} +

-
+ {isCurrent ? ( + + {t("versionSwitcher.current", { ns: "config" })} + + ) : null} +
+
+ + {onRollbackVersion && v.status !== "draft" ? ( - {onRollbackVersion && v.status !== "draft" ? ( - - ) : null} - {onDeleteVersion && v.status !== "active" ? ( - - ) : null} -
+ ) : null} + {onDeleteVersion && v.status !== "active" ? ( + + ) : null}
- +
); })}
diff --git a/src/modules/config/config-workspace-shell.tsx b/src/modules/config/config-workspace-shell.tsx index 8767bee..65082f3 100644 --- a/src/modules/config/config-workspace-shell.tsx +++ b/src/modules/config/config-workspace-shell.tsx @@ -1,88 +1,16 @@ "use client"; import type { ReactNode } from "react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { useTranslation } from "react-i18next"; -import { cn } from "@/lib/utils"; -import { CONFIG_NAV_GROUPS } from "@/modules/config/config-nav-model"; - -function navLinkActive(pathname: string, href: string): boolean { - return pathname === href || pathname.startsWith(`${href}/`); -} +import { ConfigSubNav } from "@/modules/config/config-subnav"; export function ConfigWorkspaceShell({ children }: { children: ReactNode }) { - const { t } = useTranslation("config"); - const pathname = usePathname() ?? ""; - return ( -
-
- - -
{children}
+
+
+
+
{children}
); } diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx index e79d018..59c33f6 100644 --- a/src/modules/config/doc/odds-config-doc-screen.tsx +++ b/src/modules/config/doc/odds-config-doc-screen.tsx @@ -445,32 +445,30 @@ export function OddsConfigDocScreen() {
-
-
- +
+ - void refreshList()} - onNewDraft={() => void handleNewDraft()} - onSaveDraft={() => void handleSave()} - onPublish={() => void requestPublishConfirm()} - /> -
+ void refreshList()} + onNewDraft={() => void handleNewDraft()} + onSaveDraft={() => void handleSave()} + onPublish={() => void requestPublishConfirm()} + />
{detail ? ( diff --git a/src/modules/config/doc/play-config-doc-screen.tsx b/src/modules/config/doc/play-config-doc-screen.tsx index 54f9b65..10d8d30 100644 --- a/src/modules/config/doc/play-config-doc-screen.tsx +++ b/src/modules/config/doc/play-config-doc-screen.tsx @@ -380,29 +380,27 @@ export function PlayConfigDocScreen() { {t("nav.items.plays", { ns: "config" })} -
-
- +
+ - void refreshList()} - onNewDraft={() => void handleNewDraft()} - onSaveDraft={() => void handleSaveDraft()} - onPublish={() => void handlePublish()} - /> -
+ void refreshList()} + onNewDraft={() => void handleNewDraft()} + onSaveDraft={() => void handleSaveDraft()} + onPublish={() => void handlePublish()} + />
{detail ? ( @@ -423,11 +421,9 @@ export function PlayConfigDocScreen() { ) : null} {detail ? ( -
-
-
-

{t("play.batchSwitchesTitle", { ns: "config" })}

-
+
+
+

{t("play.batchSwitchesTitle", { ns: "config" })}

{!isDraft ? ( {t("play.readOnlyDraftHint", { ns: "config" })} @@ -438,7 +434,7 @@ export function PlayConfigDocScreen() { {batchSwitchStates.map((group) => (

{group.label}

@@ -474,8 +470,7 @@ export function PlayConfigDocScreen() { {loadingDetail ? (

{t("states.loading", { ns: "common" })}

) : ( -
- +
{t("play.table.playCode", { ns: "config" })} @@ -606,8 +601,7 @@ export function PlayConfigDocScreen() { ))} -
-
+
)} diff --git a/src/modules/config/doc/rebate-config-doc-screen.tsx b/src/modules/config/doc/rebate-config-doc-screen.tsx index d5ca12a..d05833b 100644 --- a/src/modules/config/doc/rebate-config-doc-screen.tsx +++ b/src/modules/config/doc/rebate-config-doc-screen.tsx @@ -365,7 +365,7 @@ export function RebateConfigDocScreen() {
-
+
{error}

: null} -
+

{t("riskCap.defaultCap.title", { ns: "config" })}

@@ -422,8 +422,7 @@ export function RiskCapDocScreen() { ) : specialRows.length === 0 ? (

{t("riskCap.noDetailRows", { ns: "config" })}

) : ( -
- +
{t("riskCap.table.number", { ns: "config" })} @@ -492,9 +491,8 @@ export function RiskCapDocScreen() { ))} - -
-
+ + )}
@@ -522,8 +520,7 @@ export function RiskCapDocScreen() { {t("riskCap.actions.exportCsv", { ns: "config" })}
-
- +
{t("riskCap.table.number", { ns: "config" })} @@ -551,7 +548,6 @@ export function RiskCapDocScreen() { ))}
-
diff --git a/src/modules/config/doc/wallet-config-doc-screen.tsx b/src/modules/config/doc/wallet-config-doc-screen.tsx index 143420c..e71a272 100644 --- a/src/modules/config/doc/wallet-config-doc-screen.tsx +++ b/src/modules/config/doc/wallet-config-doc-screen.tsx @@ -42,7 +42,11 @@ interface Draft { outMax: string; } -export function WalletConfigDocScreen() { +type WalletConfigDocScreenProps = { + embedded?: boolean; +}; + +export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScreenProps) { const { t } = useTranslation(["config", "adminUsers"]); const [draft, setDraft] = useState({ inMin: "", @@ -109,83 +113,91 @@ export function WalletConfigDocScreen() { } }; + const content = ( + <> +
+
+ + handleChange("inMin", e.target.value)} + disabled={loading || saving} + /> +
+
+ + handleChange("inMax", e.target.value)} + disabled={loading || saving} + /> +
+
+ + handleChange("outMin", e.target.value)} + disabled={loading || saving} + /> +
+
+ + handleChange("outMax", e.target.value)} + disabled={loading || saving} + /> +
+
+
+ + {dirty && ( + + )} +
+ + ); + + if (embedded) { + return content; + } + return ( {t("wallet.title", { ns: "config" })} - -
-
- - handleChange("inMin", e.target.value)} - disabled={loading || saving} - /> -
-
- - handleChange("inMax", e.target.value)} - disabled={loading || saving} - /> -
-
- - handleChange("outMin", e.target.value)} - disabled={loading || saving} - /> -
-
- - handleChange("outMax", e.target.value)} - disabled={loading || saving} - /> -
-
-
- - {dirty && ( - - )} -
-
+ {content}
); } diff --git a/src/modules/dashboard/dashboard-console.tsx b/src/modules/dashboard/dashboard-console.tsx index 62d020d..2be2f27 100644 --- a/src/modules/dashboard/dashboard-console.tsx +++ b/src/modules/dashboard/dashboard-console.tsx @@ -10,7 +10,6 @@ import { ClipboardList, Diamond, FileSearch, - FileSpreadsheet, Gift, RefreshCw, ScrollText, @@ -25,6 +24,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; +import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; +import { formatAdminMinorUnits, getAdminCurrencyDecimalPlaces } from "@/lib/money"; import { cn } from "@/lib/utils"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; @@ -42,20 +43,18 @@ type SoldOutBuckets = { }; function formatMoneyMinor(minor: number, currencyCode: string | null): string { - const major = minor / 100; - const code = (currencyCode ?? "CNY").toUpperCase(); + const code = (currencyCode ?? "NPR").toUpperCase(); + const decimals = getAdminCurrencyDecimalPlaces(code); + const major = minor / 10 ** decimals; try { return new Intl.NumberFormat("zh-CN", { style: "currency", currency: code, - maximumFractionDigits: 2, + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, }).format(major); } catch { - return new Intl.NumberFormat("zh-CN", { - style: "currency", - currency: "CNY", - maximumFractionDigits: 2, - }).format(major); + return formatAdminMinorUnits(minor, code, decimals); } } @@ -251,6 +250,7 @@ function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement { export function DashboardConsole(): ReactElement { const { t } = useTranslation(["dashboard", "common"]); + useAdminCurrencyCatalog(); const [todayLabel] = useState(() => format(new Date(), "yyyy-MM-dd EEEE", { locale: zhCN })); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -352,7 +352,6 @@ export function DashboardConsole(): ReactElement { }, { href: "/admin/tickets", label: t("quickLinks.tickets"), icon: }, { href: "/admin/wallet/transactions", label: t("quickLinks.walletTransactions"), icon: }, - { href: "/admin/reports", label: t("quickLinks.reports"), icon: }, { href: "/admin/audit-logs", label: t("quickLinks.auditLogs"), icon: }, ]; diff --git a/src/modules/draws/draws-index-console.tsx b/src/modules/draws/draws-index-console.tsx index 2614100..d98dc86 100644 --- a/src/modules/draws/draws-index-console.tsx +++ b/src/modules/draws/draws-index-console.tsx @@ -76,7 +76,7 @@ export function DrawsIndexConsole() { const [appliedDrawNo, setAppliedDrawNo] = useState(""); const [appliedStatus, setAppliedStatus] = useState(""); const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(20); + const [perPage, setPerPage] = useState(10); const [generating, setGenerating] = useState(false); const drawStatusTriggerLabel = useMemo( diff --git a/src/modules/jackpot/jackpot-records-console.tsx b/src/modules/jackpot/jackpot-records-console.tsx index 5a741c6..dd778ae 100644 --- a/src/modules/jackpot/jackpot-records-console.tsx +++ b/src/modules/jackpot/jackpot-records-console.tsx @@ -35,11 +35,11 @@ export function JackpotRecordsConsole() { const [payouts, setPayouts] = useState(null); const [pPage, setPPage] = useState(1); - const [pPer, setPPer] = useState(15); + const [pPer, setPPer] = useState(10); const [contribs, setContribs] = useState(null); const [cPage, setCPage] = useState(1); - const [cPer, setCPer] = useState(15); + const [cPer, setCPer] = useState(10); const [loadingP, setLoadingP] = useState(true); const [loadingC, setLoadingC] = useState(true); diff --git a/src/modules/players/players-console.tsx b/src/modules/players/players-console.tsx index 576b373..0eb08c2 100644 --- a/src/modules/players/players-console.tsx +++ b/src/modules/players/players-console.tsx @@ -41,6 +41,8 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; +import { formatAdminMinorUnits } from "@/lib/money"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminPlayerRow } from "@/types/api/admin-player"; @@ -60,11 +62,6 @@ function playerStatusVariant( return "default"; } -function formatMinorUnits(minor: number, currencyCode: string): string { - const major = minor / 100; - return `${major.toFixed(2)} ${currencyCode}`; -} - const PLAYER_STATUS_OPTIONS = [ { value: 0, label: "statusNormal" }, { value: 1, label: "statusFrozen" }, @@ -74,9 +71,10 @@ const PLAYER_STATUS_OPTIONS = [ export function PlayersConsole(): React.ReactElement { const { t } = useTranslation(["players", "common"]); const profile = useAdminProfile(); + useAdminCurrencyCatalog(); const canManagePlayers = adminHasAnyPermission(profile?.permissions, ["prd.users.manage"]); const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(25); + const [perPage, setPerPage] = useState(10); const [keyword, setKeyword] = useState(""); const [query, setQuery] = useState(""); @@ -206,6 +204,9 @@ export function PlayersConsole(): React.ReactElement { if (formNickname !== editingPlayer?.nickname) { body.nickname = formNickname.trim() || null; } + if (formDefaultCurrency !== editingPlayer?.default_currency) { + body.default_currency = formDefaultCurrency.trim().toUpperCase(); + } if (formStatus !== editingPlayer?.status) { body.status = formStatus; } @@ -344,12 +345,12 @@ export function PlayersConsole(): React.ReactElement { {row.default_currency} {row.wallets.length > 0 - ? formatMinorUnits(row.wallets[0].balance, row.wallets[0].currency_code) + ? formatAdminMinorUnits(row.wallets[0].balance, row.wallets[0].currency_code) : "—"} {row.wallets.length > 0 - ? formatMinorUnits(row.wallets[0].available_balance, row.wallets[0].currency_code) + ? formatAdminMinorUnits(row.wallets[0].available_balance, row.wallets[0].currency_code) : "—"} diff --git a/src/modules/reconcile/reconcile-console.tsx b/src/modules/reconcile/reconcile-console.tsx index 04bd60f..a661468 100644 --- a/src/modules/reconcile/reconcile-console.tsx +++ b/src/modules/reconcile/reconcile-console.tsx @@ -9,19 +9,15 @@ import { getAdminReconcileJobs, postAdminReconcileJob, } from "@/api/admin-reconcile"; +import { getAdminPlayers } from "@/api/admin-player"; +import { AdminDateRangeField } from "@/components/admin/admin-date-range-field"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Table, TableBody, @@ -30,11 +26,12 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Textarea } from "@/components/ui/textarea"; import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; +import { cn } from "@/lib/utils"; import { useAdminProfile } from "@/stores/admin-session"; import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminPlayerRow } from "@/types/api/admin-player"; import type { AdminReconcileItemsData, AdminReconcileJobListData, @@ -43,12 +40,7 @@ import type { const MANAGE = ["prd.wallet_reconcile.manage"] as const; /** 与后端 reconcile_type 对齐;扩展时在 API 与下拉同步增加 */ -const RECONCILE_TYPE_OPTIONS = [{ value: "wallet_transfer", label: "walletTransfer" }] as const; - -function reconcileTypeLabel(slug: string, t: (key: string) => string): string { - const hit = RECONCILE_TYPE_OPTIONS.find((o) => o.value === slug); - return hit ? t(hit.label) : slug; -} +const RECONCILE_TYPE = "wallet_transfer" as const; function jobStatusLabel(status: string, t: (key: string) => string): string { switch (status) { @@ -76,34 +68,13 @@ function itemStatusLabel(status: string, t: (key: string) => string): string { } } -function toIsoFromDatetimeLocal(local: string): string | null { - const t = local.trim(); - if (t === "") { - return null; +function reconcileTypeLabel(type: string, t: (key: string) => string): string { + switch (type) { + case "wallet_transfer": + return t("reconcileTypeWalletTransfer"); + default: + return type; } - const d = new Date(t); - if (Number.isNaN(d.getTime())) { - return null; - } - return d.toISOString(); -} - -function scopeLinesToItems( - raw: string, -): NonNullable[0]["items"]> | undefined { - const lines = raw - .split(/\r?\n/) - .map((s) => s.trim()) - .filter(Boolean); - if (lines.length === 0) { - return undefined; - } - return lines.map((side_a_ref) => ({ - side_a_ref, - side_b_ref: null, - difference_amount: 0, - status: "pending_check", - })); } export function ReconcileConsole(): React.ReactElement { @@ -116,18 +87,21 @@ export function ReconcileConsole(): React.ReactElement { const [jobsLoading, setJobsLoading] = useState(true); const [jobsErr, setJobsErr] = useState(null); const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(25); + const [perPage, setPerPage] = useState(10); const [selectedId, setSelectedId] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); const [items, setItems] = useState(null); const [itemsPage, setItemsPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(50); + const [itemsPerPage, setItemsPerPage] = useState(10); const [itemsLoading, setItemsLoading] = useState(false); - const [reconcileType, setReconcileType] = useState(RECONCILE_TYPE_OPTIONS[0].value); - const [periodStartLocal, setPeriodStartLocal] = useState(""); - const [periodEndLocal, setPeriodEndLocal] = useState(""); - const [scopeLines, setScopeLines] = useState(""); + const [dateFrom, setDateFrom] = useState(""); + const [dateTo, setDateTo] = useState(""); + const [playerSearch, setPlayerSearch] = useState(""); + const [playerResults, setPlayerResults] = useState([]); + const [playerLoading, setPlayerLoading] = useState(false); + const [selectedPlayer, setSelectedPlayer] = useState(null); const [submitting, setSubmitting] = useState(false); const loadJobs = useCallback(async () => { @@ -176,35 +150,59 @@ export function ReconcileConsole(): React.ReactElement { }); }, [loadItems]); + const loadPlayers = useCallback(async (keyword: string) => { + const q = keyword.trim(); + if (q === "") { + setPlayerResults([]); + return; + } + setPlayerLoading(true); + try { + const data = await getAdminPlayers({ page: 1, per_page: 8, keyword: q }); + setPlayerResults(data.items); + } catch { + setPlayerResults([]); + } finally { + setPlayerLoading(false); + } + }, []); + + useEffect(() => { + const q = playerSearch.trim(); + if (q === "") { + return; + } + const timer = window.setTimeout(() => { + void loadPlayers(q); + }, 250); + return () => window.clearTimeout(timer); + }, [loadPlayers, playerSearch]); + async function onCreate(): Promise { - if (!periodStartLocal.trim() || !periodEndLocal.trim()) { + if (!dateFrom.trim() || !dateTo.trim()) { toast.error(t("periodRequired")); return; } - const periodStartIso = toIsoFromDatetimeLocal(periodStartLocal); - const periodEndIso = toIsoFromDatetimeLocal(periodEndLocal); - if (periodStartIso == null || periodEndIso == null) { - toast.error(t("periodInvalid")); - return; - } - if (new Date(periodStartIso).getTime() > new Date(periodEndIso).getTime()) { + if (dateFrom > dateTo) { toast.error(t("periodOrderInvalid")); return; } - const itemsPayload = scopeLinesToItems(scopeLines); - setSubmitting(true); try { await postAdminReconcileJob({ - reconcile_type: reconcileType, - period_start: periodStartIso, - period_end: periodEndIso, - items: itemsPayload, + reconcile_type: RECONCILE_TYPE, + date_from: dateFrom, + date_to: dateTo, + player_id: selectedPlayer ? selectedPlayer.id : null, }); toast.success(t("createSuccess")); setPage(1); - setScopeLines(""); + setDateFrom(""); + setDateTo(""); + setPlayerSearch(""); + setSelectedPlayer(null); + setPlayerResults([]); await loadJobs(); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("createFailed")); @@ -215,6 +213,7 @@ export function ReconcileConsole(): React.ReactElement { const jm = jobs?.meta; const im = items?.meta; + const selectedJob = jobs?.items.find((job) => job.id === selectedId) ?? null; return (
@@ -222,65 +221,104 @@ export function ReconcileConsole(): React.ReactElement { {t("createTitle")} - {t("createDesc")} -
+
- - + +
- - setPeriodStartLocal(e.target.value)} - /> -
-
- - setPeriodEndLocal(e.target.value)} + { + setDateFrom(from); + setDateTo(to); + }} />
-
- -