feat(admin, settlement, dashboard): strengthen permission gating and billing workflows
This commit is contained in:
@@ -141,6 +141,8 @@ export type SettlementCreditLedgerRow = {
|
|||||||
play_code?: string | null;
|
play_code?: string | null;
|
||||||
draw_no?: string | null;
|
draw_no?: string | null;
|
||||||
ticket_item_id?: number | null;
|
ticket_item_id?: number | null;
|
||||||
|
settlement_bill_id?: number | null;
|
||||||
|
bill_status?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getCreditLedger(params: {
|
export async function getCreditLedger(params: {
|
||||||
@@ -202,6 +204,15 @@ export type SettlementPaymentRow = {
|
|||||||
created_at?: string;
|
created_at?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SettlementBillPaymentRow = {
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
status: string;
|
||||||
|
method?: string | null;
|
||||||
|
proof?: string | null;
|
||||||
|
remark?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getSettlementPayments(params?: {
|
export async function getSettlementPayments(params?: {
|
||||||
settlement_period_id?: number;
|
settlement_period_id?: number;
|
||||||
admin_site_id?: number;
|
admin_site_id?: number;
|
||||||
@@ -245,18 +256,9 @@ export type RebateAllocationRow = {
|
|||||||
allocation_rule: string;
|
allocation_rule: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettlementPaymentRow = {
|
|
||||||
id: number;
|
|
||||||
amount: number;
|
|
||||||
status: string;
|
|
||||||
method?: string | null;
|
|
||||||
proof?: string | null;
|
|
||||||
remark?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getSettlementBill(billId: number): Promise<{
|
export async function getSettlementBill(billId: number): Promise<{
|
||||||
bill: SettlementBillRow;
|
bill: SettlementBillRow;
|
||||||
payments: SettlementPaymentRow[];
|
payments: SettlementBillPaymentRow[];
|
||||||
rebate_allocations: RebateAllocationRow[];
|
rebate_allocations: RebateAllocationRow[];
|
||||||
adjustments: Array<{ id: number; amount: number; adjustment_type: string; reason: string | null }>;
|
adjustments: Array<{ id: number; amount: number; adjustment_type: string; reason: string | null }>;
|
||||||
tier_edge?: string | null;
|
tier_edge?: string | null;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { AdminRolesConsole } from "@/modules/admin-roles/admin-roles-console";
|
import { AdminRolesConsole } from "@/modules/admin-roles/admin-roles-console";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_ADMIN_ROLE_MANAGE } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("adminRoles", "title");
|
export const metadata: Metadata = buildPageMetadata("adminRoles", "title");
|
||||||
@@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("adminRoles", "title");
|
|||||||
export default function AdminRolesPage() {
|
export default function AdminRolesPage() {
|
||||||
return (
|
return (
|
||||||
<ModuleScaffold>
|
<ModuleScaffold>
|
||||||
|
<AdminPermissionGate requiredAny={[PRD_ADMIN_ROLE_MANAGE]}>
|
||||||
<AdminRolesConsole />
|
<AdminRolesConsole />
|
||||||
|
</AdminPermissionGate>
|
||||||
</ModuleScaffold>
|
</ModuleScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { AdminUsersConsole } from "@/modules/admin-users/admin-users-console";
|
import { AdminUsersConsole } from "@/modules/admin-users/admin-users-console";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_ADMIN_USER_MANAGE } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("adminUsers", "title");
|
export const metadata: Metadata = buildPageMetadata("adminUsers", "title");
|
||||||
@@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("adminUsers", "title");
|
|||||||
export default function AdminUsersPage() {
|
export default function AdminUsersPage() {
|
||||||
return (
|
return (
|
||||||
<ModuleScaffold>
|
<ModuleScaffold>
|
||||||
|
<AdminPermissionGate requiredAny={[PRD_ADMIN_USER_MANAGE]}>
|
||||||
<AdminUsersConsole />
|
<AdminUsersConsole />
|
||||||
|
</AdminPermissionGate>
|
||||||
</ModuleScaffold>
|
</ModuleScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { AuditLogsConsole } from "@/modules/audit/audit-logs-console";
|
import { AuditLogsConsole } from "@/modules/audit/audit-logs-console";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_AUDIT_VIEW } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("audit", "title");
|
export const metadata: Metadata = buildPageMetadata("audit", "title");
|
||||||
@@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("audit", "title");
|
|||||||
export default function AdminAuditLogsPage() {
|
export default function AdminAuditLogsPage() {
|
||||||
return (
|
return (
|
||||||
<ModuleScaffold>
|
<ModuleScaffold>
|
||||||
|
<AdminPermissionGate requiredAny={[PRD_AUDIT_VIEW]}>
|
||||||
<AuditLogsConsole />
|
<AuditLogsConsole />
|
||||||
|
</AdminPermissionGate>
|
||||||
</ModuleScaffold>
|
</ModuleScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { ConfigHubScreen } from "@/modules/config/config-hub-screen";
|
import { ConfigHubScreen } from "@/modules/config/config-hub-screen";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_CONFIG_HUB_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("config", "hub.title");
|
export const metadata: Metadata = buildPageMetadata("config", "hub.title");
|
||||||
|
|
||||||
export default function AdminConfigHubPage() {
|
export default function AdminConfigHubPage() {
|
||||||
return <ConfigHubScreen />;
|
return (
|
||||||
|
<AdminPermissionGate requiredAny={PRD_CONFIG_HUB_ACCESS_ANY}>
|
||||||
|
<ConfigHubScreen />
|
||||||
|
</AdminPermissionGate>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { CurrencyManagementScreen } from "@/modules/settings/currency-management-screen";
|
import { CurrencyManagementScreen } from "@/modules/settings/currency-management-screen";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_CURRENCY_MANAGE } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("config", "currencies.title");
|
export const metadata: Metadata = buildPageMetadata("config", "currencies.title");
|
||||||
@@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("config", "currencies.title"
|
|||||||
export default function AdminCurrenciesPage() {
|
export default function AdminCurrenciesPage() {
|
||||||
return (
|
return (
|
||||||
<ModuleScaffold>
|
<ModuleScaffold>
|
||||||
|
<AdminPermissionGate requiredAny={[PRD_CURRENCY_MANAGE]}>
|
||||||
<CurrencyManagementScreen />
|
<CurrencyManagementScreen />
|
||||||
|
</AdminPermissionGate>
|
||||||
</ModuleScaffold>
|
</ModuleScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { ReconcileConsole } from "@/modules/reconcile/reconcile-console";
|
import { ReconcileConsole } from "@/modules/reconcile/reconcile-console";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_WALLET_TX_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("reconcile", "title");
|
export const metadata: Metadata = buildPageMetadata("reconcile", "title");
|
||||||
@@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("reconcile", "title");
|
|||||||
export default function AdminReconcilePage() {
|
export default function AdminReconcilePage() {
|
||||||
return (
|
return (
|
||||||
<ModuleScaffold>
|
<ModuleScaffold>
|
||||||
|
<AdminPermissionGate requiredAny={PRD_WALLET_TX_ACCESS_ANY}>
|
||||||
<ReconcileConsole />
|
<ReconcileConsole />
|
||||||
|
</AdminPermissionGate>
|
||||||
</ModuleScaffold>
|
</ModuleScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { RulesOddsConfigScreen } from "@/modules/rules/rules-odds-config-screen";
|
import { RulesOddsConfigScreen } from "@/modules/rules/rules-odds-config-screen";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_RULES_ODDS_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("config", "nav.rulesOddsTitle");
|
export const metadata: Metadata = buildPageMetadata("config", "nav.rulesOddsTitle");
|
||||||
|
|
||||||
export default function AdminRulesOddsPage() {
|
export default function AdminRulesOddsPage() {
|
||||||
return <RulesOddsConfigScreen />;
|
return (
|
||||||
|
<AdminPermissionGate requiredAny={PRD_RULES_ODDS_ACCESS_ANY}>
|
||||||
|
<RulesOddsConfigScreen />
|
||||||
|
</AdminPermissionGate>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { PlayConfigDocScreen } from "@/modules/config/doc/play-config-doc-screen";
|
import { PlayConfigDocScreen } from "@/modules/config/doc/play-config-doc-screen";
|
||||||
import { RulesPageShell } from "@/modules/rules/rules-page-shell";
|
import { RulesPageShell } from "@/modules/rules/rules-page-shell";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_RULES_PLAYS_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("config", "nav.rulesPlaysTitle");
|
export const metadata: Metadata = buildPageMetadata("config", "nav.rulesPlaysTitle");
|
||||||
|
|
||||||
export default function AdminRulesPlaysPage() {
|
export default function AdminRulesPlaysPage() {
|
||||||
return (
|
return (
|
||||||
|
<AdminPermissionGate requiredAny={PRD_RULES_PLAYS_ACCESS_ANY}>
|
||||||
<RulesPageShell>
|
<RulesPageShell>
|
||||||
<PlayConfigDocScreen />
|
<PlayConfigDocScreen />
|
||||||
</RulesPageShell>
|
</RulesPageShell>
|
||||||
|
</AdminPermissionGate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { SystemSettingsScreen } from "@/modules/settings/system-settings-screen";
|
import { SystemSettingsScreen } from "@/modules/settings/system-settings-screen";
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_SETTINGS_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = buildPageMetadata("common", "nav.settings");
|
export const metadata: Metadata = buildPageMetadata("common", "nav.settings");
|
||||||
@@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("common", "nav.settings");
|
|||||||
export default function AdminSettingsPage() {
|
export default function AdminSettingsPage() {
|
||||||
return (
|
return (
|
||||||
<ModuleScaffold>
|
<ModuleScaffold>
|
||||||
|
<AdminPermissionGate requiredAny={PRD_SETTINGS_ACCESS_ANY}>
|
||||||
<SystemSettingsScreen />
|
<SystemSettingsScreen />
|
||||||
|
</AdminPermissionGate>
|
||||||
</ModuleScaffold>
|
</ModuleScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<CardContent className="flex flex-col gap-5 px-6 pt-6 sm:px-8">
|
<CardContent className="flex flex-col gap-5 px-6 py-6 sm:px-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="admin-account" className="text-sm font-medium">
|
<Label htmlFor="admin-account" className="text-sm font-medium">
|
||||||
{t("account")}
|
{t("account")}
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { playerFundingModeLabel } from "@/lib/admin-player-display";
|
import { playerFundingModeLabel } from "@/lib/admin-player-display";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { AdminPlayerRow } from "@/types/api/admin-player";
|
|
||||||
|
|
||||||
type FundingRow = Pick<AdminPlayerRow, "funding_mode" | "uses_credit" | "auth_source">;
|
type FundingRow = {
|
||||||
|
funding_mode?: string | null;
|
||||||
|
uses_credit?: boolean;
|
||||||
|
auth_source?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export function PlayerFundingModeBadge({
|
export function PlayerFundingModeBadge({
|
||||||
row,
|
row,
|
||||||
@@ -41,25 +44,47 @@ export function PlayerLedgerSourceBadge({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}): React.ReactElement | null {
|
}): React.ReactElement | null {
|
||||||
const { t } = useTranslation("wallet");
|
const { t } = useTranslation("wallet");
|
||||||
if (ledgerSource !== "credit_ledger" && ledgerSource !== "wallet_txn") {
|
if (
|
||||||
|
ledgerSource !== "credit_ledger" &&
|
||||||
|
ledgerSource !== "wallet_txn" &&
|
||||||
|
ledgerSource !== "payment_record" &&
|
||||||
|
ledgerSource !== "settlement_adjustment" &&
|
||||||
|
ledgerSource !== "share_ledger"
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCredit = ledgerSource === "credit_ledger";
|
const sourceClass =
|
||||||
|
ledgerSource === "wallet_txn"
|
||||||
|
? "border-sky-200 bg-sky-50 text-sky-900"
|
||||||
|
: ledgerSource === "payment_record"
|
||||||
|
? "border-emerald-200 bg-emerald-50 text-emerald-900"
|
||||||
|
: ledgerSource === "settlement_adjustment"
|
||||||
|
? "border-amber-200 bg-amber-50 text-amber-900"
|
||||||
|
: ledgerSource === "share_ledger"
|
||||||
|
? "border-indigo-200 bg-indigo-50 text-indigo-900"
|
||||||
|
: "border-violet-200 bg-violet-50 text-violet-900";
|
||||||
|
|
||||||
|
const label =
|
||||||
|
ledgerSource === "wallet_txn"
|
||||||
|
? t("ledgerWallet", { defaultValue: "钱包流水" })
|
||||||
|
: ledgerSource === "payment_record"
|
||||||
|
? t("ledgerPayment", { defaultValue: "收付记录" })
|
||||||
|
: ledgerSource === "settlement_adjustment"
|
||||||
|
? t("ledgerAdjustment", { defaultValue: "调账记录" })
|
||||||
|
: ledgerSource === "share_ledger"
|
||||||
|
? t("ledgerShare", { defaultValue: "分账" })
|
||||||
|
: t("ledgerCredit", { defaultValue: "信用流水" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex rounded-md border px-2 py-0.5 text-xs font-medium",
|
"inline-flex rounded-md border px-2 py-0.5 text-xs font-medium",
|
||||||
isCredit
|
sourceClass,
|
||||||
? "border-violet-200 bg-violet-50 text-violet-900"
|
|
||||||
: "border-sky-200 bg-sky-50 text-sky-900",
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCredit
|
{label}
|
||||||
? t("ledgerCredit", { defaultValue: "信用流水" })
|
|
||||||
: t("ledgerWallet", { defaultValue: "钱包流水" })}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,22 @@ import { useEffect, useRef, type DependencyList } from "react";
|
|||||||
* 在依赖变化时执行异步副作用;factory 始终用最新闭包,但不必把 `t` 等不稳定引用放进 deps。
|
* 在依赖变化时执行异步副作用;factory 始终用最新闭包,但不必把 `t` 等不稳定引用放进 deps。
|
||||||
*/
|
*/
|
||||||
export function useAsyncEffect(
|
export function useAsyncEffect(
|
||||||
factory: () => void | Promise<void>,
|
factory: () => void | (() => void) | Promise<void>,
|
||||||
deps: DependencyList,
|
deps: DependencyList,
|
||||||
): void {
|
): void {
|
||||||
const factoryRef = useRef(factory);
|
const factoryRef = useRef(factory);
|
||||||
factoryRef.current = factory;
|
factoryRef.current = factory;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cleanup: void | (() => void);
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
void factoryRef.current();
|
void Promise.resolve(factoryRef.current()).then((result) => {
|
||||||
|
cleanup = result;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanup?.();
|
||||||
|
};
|
||||||
}, deps);
|
}, deps);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,11 @@
|
|||||||
"outMin": "Per-order minimum from lottery wallet to main wallet",
|
"outMin": "Per-order minimum from lottery wallet to main wallet",
|
||||||
"outMax": "Per-order maximum from lottery wallet to main wallet"
|
"outMax": "Per-order maximum from lottery wallet to main wallet"
|
||||||
},
|
},
|
||||||
|
"validation": {
|
||||||
|
"amountAtLeastMinorUnit": "{{field}} must be a valid amount and at least 0.01.",
|
||||||
|
"inRangeInvalid": "Maximum transfer-in amount cannot be less than the minimum transfer-in amount.",
|
||||||
|
"outRangeInvalid": "Maximum transfer-out amount cannot be less than the minimum transfer-out amount."
|
||||||
|
},
|
||||||
"discard": "Discard changes",
|
"discard": "Discard changes",
|
||||||
"confirmSaveTitle": "Save wallet limits?",
|
"confirmSaveTitle": "Save wallet limits?",
|
||||||
"confirmSaveDescription": "This updates per-order transfer-in/out limits and immediately affects player wallet transfers."
|
"confirmSaveDescription": "This updates per-order transfer-in/out limits and immediately affects player wallet transfers."
|
||||||
|
|||||||
@@ -157,18 +157,45 @@
|
|||||||
"agent": {
|
"agent": {
|
||||||
"title": "Operations overview",
|
"title": "Operations overview",
|
||||||
"subtitle": "Your line scope · {{name}}",
|
"subtitle": "Your line scope · {{name}}",
|
||||||
|
"heroEyebrow": "Today's line cockpit",
|
||||||
|
"heroTitle": "{{name}} live operations",
|
||||||
"creditTitle": "Credit limit",
|
"creditTitle": "Credit limit",
|
||||||
"creditAvailable": "Available {{amount}}",
|
"creditAvailable": "Available {{amount}}",
|
||||||
"creditAllocated": "Allocated {{amount}}",
|
"creditAllocated": "Allocated {{amount}}",
|
||||||
"creditUsed": "Used {{amount}}",
|
"creditUsed": "Used {{amount}}",
|
||||||
|
"creditAllocatedLabel": "Allocated credit",
|
||||||
|
"creditUsedLabel": "Used credit",
|
||||||
"shareRate": "Total share {{rate}}%",
|
"shareRate": "Total share {{rate}}%",
|
||||||
"settlementCycle": "Cycle {{cycle}}",
|
"settlementCycle": "Cycle {{cycle}}",
|
||||||
"teamTitle": "Team size",
|
"teamTitle": "Team size",
|
||||||
"directChildren": "Direct child agents",
|
"directChildren": "Direct child agents",
|
||||||
"directPlayers": "Direct players",
|
"directPlayers": "Direct players",
|
||||||
"subtreeAgents": "Agents in line",
|
"subtreeAgents": "Agents in line",
|
||||||
|
"teamPlayers": "Players in line",
|
||||||
|
"activePlayersToday": "Active players today",
|
||||||
|
"betOrdersToday": "Bet orders today",
|
||||||
|
"todayBet": "Today's bet",
|
||||||
|
"todayPayout": "Today's payout",
|
||||||
|
"todayProfit": "Today's profit",
|
||||||
|
"sevenDayTitle": "Last 7 days",
|
||||||
|
"sevenDayPayout": "Payout {{amount}}",
|
||||||
|
"sevenDayProfit": "Profit {{amount}}",
|
||||||
"pendingBills": "Open agent bills",
|
"pendingBills": "Open agent bills",
|
||||||
"pendingUnpaid": "Unpaid total {{amount}}",
|
"pendingUnpaid": "Unpaid total {{amount}}",
|
||||||
|
"latestBetAt": "Latest bet {{time}}",
|
||||||
|
"noBetToday": "No bets yet today",
|
||||||
|
"topMomentum": "Today's bet focus",
|
||||||
|
"topMomentumHint": "Profit {{profit}}",
|
||||||
|
"managementFocus": "Management focus",
|
||||||
|
"focusBet": "Watch today's bet volume",
|
||||||
|
"focusPlayers": "Today's active players",
|
||||||
|
"focusBills": "Bills to follow up",
|
||||||
|
"quickStatsTitle": "Line permission snapshot",
|
||||||
|
"canCreateChildAgent": "Can create child agent",
|
||||||
|
"canCreatePlayer": "Can create player",
|
||||||
|
"lineDepth": "Line depth",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
"viewBills": "View bills",
|
"viewBills": "View bills",
|
||||||
"viewLine": "Agent line",
|
"viewLine": "Agent line",
|
||||||
"quickLinks": {
|
"quickLinks": {
|
||||||
|
|||||||
@@ -116,6 +116,9 @@
|
|||||||
"entryKind": {
|
"entryKind": {
|
||||||
"adjustment": "Adjustment"
|
"adjustment": "Adjustment"
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
"posted": "Posted"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"viewPlayer": "Player detail",
|
"viewPlayer": "Player detail",
|
||||||
"viewBill": "Bill detail",
|
"viewBill": "Bill detail",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"viewTicketInList": "View this ticket",
|
"viewTicketInList": "View this ticket",
|
||||||
|
"viewPlayer": "View player",
|
||||||
"failReason": "Fail reason",
|
"failReason": "Fail reason",
|
||||||
"winAmount": "Win amount",
|
"winAmount": "Win amount",
|
||||||
"placedAt": "Placed at",
|
"placedAt": "Placed at",
|
||||||
|
|||||||
@@ -176,6 +176,11 @@
|
|||||||
"outMin": "लटरी वालेटबाट मुख्य वालेटमा प्रति अर्डर न्यूनतम",
|
"outMin": "लटरी वालेटबाट मुख्य वालेटमा प्रति अर्डर न्यूनतम",
|
||||||
"outMax": "लटरी वालेटबाट मुख्य वालेटमा प्रति अर्डर अधिकतम"
|
"outMax": "लटरी वालेटबाट मुख्य वालेटमा प्रति अर्डर अधिकतम"
|
||||||
},
|
},
|
||||||
|
"validation": {
|
||||||
|
"amountAtLeastMinorUnit": "{{field}} मान्य रकम हुनुपर्छ र कम्तीमा 0.01 हुनुपर्छ।",
|
||||||
|
"inRangeInvalid": "अधिकतम ट्रान्सफर-इन रकम न्यूनतम ट्रान्सफर-इन रकमभन्दा कम हुन सक्दैन।",
|
||||||
|
"outRangeInvalid": "अधिकतम ट्रान्सफर-आउट रकम न्यूनतम ट्रान्सफर-आउट रकमभन्दा कम हुन सक्दैन।"
|
||||||
|
},
|
||||||
"discard": "परिवर्तन त्याग्नुहोस्",
|
"discard": "परिवर्तन त्याग्नुहोस्",
|
||||||
"confirmSaveTitle": "वालेट सीमा सुरक्षित गर्ने?",
|
"confirmSaveTitle": "वालेट सीमा सुरक्षित गर्ने?",
|
||||||
"confirmSaveDescription": "ट्रान्सफर-इन/आउटको प्रति अर्डर सीमा अद्यावधिक हुन्छ र खेलाडीको वालेट ट्रान्सफरमा तुरुन्त असर पर्छ।"
|
"confirmSaveDescription": "ट्रान्सफर-इन/आउटको प्रति अर्डर सीमा अद्यावधिक हुन्छ र खेलाडीको वालेट ट्रान्सफरमा तुरुन्त असर पर्छ।"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"actualDeduct": "कटौती",
|
"actualDeduct": "कटौती",
|
||||||
"status": "स्थिति",
|
"status": "स्थिति",
|
||||||
"actions": "कार्य",
|
"actions": "कार्य",
|
||||||
|
"viewPlayer": "खेलाडी हेर्नुहोस्",
|
||||||
"failReason": "असफल कारण",
|
"failReason": "असफल कारण",
|
||||||
"winAmount": "जित रकम",
|
"winAmount": "जित रकम",
|
||||||
"placedAt": "बेट समय",
|
"placedAt": "बेट समय",
|
||||||
|
|||||||
@@ -185,6 +185,11 @@
|
|||||||
"outMin": "彩票钱包转出主站钱包的单笔下限",
|
"outMin": "彩票钱包转出主站钱包的单笔下限",
|
||||||
"outMax": "彩票钱包转出主站钱包的单笔上限"
|
"outMax": "彩票钱包转出主站钱包的单笔上限"
|
||||||
},
|
},
|
||||||
|
"validation": {
|
||||||
|
"amountAtLeastMinorUnit": "{{field}} 必须是有效金额,且至少为 0.01。",
|
||||||
|
"inRangeInvalid": "转入最大金额不能小于转入最小金额。",
|
||||||
|
"outRangeInvalid": "转出最大金额不能小于转出最小金额。"
|
||||||
|
},
|
||||||
"discard": "放弃更改",
|
"discard": "放弃更改",
|
||||||
"confirmSaveTitle": "确认保存钱包限额?",
|
"confirmSaveTitle": "确认保存钱包限额?",
|
||||||
"confirmSaveDescription": "将更新转入/转出单笔限额,立即影响玩家钱包转账。"
|
"confirmSaveDescription": "将更新转入/转出单笔限额,立即影响玩家钱包转账。"
|
||||||
|
|||||||
@@ -157,18 +157,45 @@
|
|||||||
"agent": {
|
"agent": {
|
||||||
"title": "经营概览",
|
"title": "经营概览",
|
||||||
"subtitle": "本线路数据范围 · {{name}}",
|
"subtitle": "本线路数据范围 · {{name}}",
|
||||||
|
"heroEyebrow": "今日经营驾驶舱",
|
||||||
|
"heroTitle": "{{name}} 的线路动态",
|
||||||
"creditTitle": "授信额度",
|
"creditTitle": "授信额度",
|
||||||
"creditAvailable": "可下发 {{amount}}",
|
"creditAvailable": "可下发 {{amount}}",
|
||||||
"creditAllocated": "已下发 {{amount}}",
|
"creditAllocated": "已下发 {{amount}}",
|
||||||
"creditUsed": "已占用 {{amount}}",
|
"creditUsed": "已占用 {{amount}}",
|
||||||
|
"creditAllocatedLabel": "已下发额度",
|
||||||
|
"creditUsedLabel": "已占用额度",
|
||||||
"shareRate": "总占成 {{rate}}%",
|
"shareRate": "总占成 {{rate}}%",
|
||||||
"settlementCycle": "账期 {{cycle}}",
|
"settlementCycle": "账期 {{cycle}}",
|
||||||
"teamTitle": "团队规模",
|
"teamTitle": "团队规模",
|
||||||
"directChildren": "直属下级代理",
|
"directChildren": "直属下级代理",
|
||||||
"directPlayers": "直属玩家",
|
"directPlayers": "直属玩家",
|
||||||
"subtreeAgents": "线路代理数",
|
"subtreeAgents": "线路代理数",
|
||||||
|
"teamPlayers": "线路玩家数",
|
||||||
|
"activePlayersToday": "今日活跃玩家",
|
||||||
|
"betOrdersToday": "今日下注单数",
|
||||||
|
"todayBet": "今日下注",
|
||||||
|
"todayPayout": "今日派彩",
|
||||||
|
"todayProfit": "今日盈亏",
|
||||||
|
"sevenDayTitle": "近 7 天走势",
|
||||||
|
"sevenDayPayout": "派彩 {{amount}}",
|
||||||
|
"sevenDayProfit": "盈亏 {{amount}}",
|
||||||
"pendingBills": "待结代理账单",
|
"pendingBills": "待结代理账单",
|
||||||
"pendingUnpaid": "未结合计 {{amount}}",
|
"pendingUnpaid": "未结合计 {{amount}}",
|
||||||
|
"latestBetAt": "最近下注 {{time}}",
|
||||||
|
"noBetToday": "今日暂时没有下注",
|
||||||
|
"topMomentum": "今日投注焦点",
|
||||||
|
"topMomentumHint": "对应盈亏 {{profit}}",
|
||||||
|
"managementFocus": "经营重点",
|
||||||
|
"focusBet": "今天先盯下注额",
|
||||||
|
"focusPlayers": "今天活跃人数",
|
||||||
|
"focusBills": "待跟进账单",
|
||||||
|
"quickStatsTitle": "线路权限快照",
|
||||||
|
"canCreateChildAgent": "可开下级代理",
|
||||||
|
"canCreatePlayer": "可开玩家",
|
||||||
|
"lineDepth": "线路层级",
|
||||||
|
"yes": "是",
|
||||||
|
"no": "否",
|
||||||
"viewBills": "查看账单",
|
"viewBills": "查看账单",
|
||||||
"viewLine": "代理线路",
|
"viewLine": "代理线路",
|
||||||
"quickLinks": {
|
"quickLinks": {
|
||||||
|
|||||||
@@ -102,6 +102,9 @@
|
|||||||
"entryKind": {
|
"entryKind": {
|
||||||
"adjustment": "调账流水"
|
"adjustment": "调账流水"
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
"posted": "已入账"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"viewPlayer": "玩家详情",
|
"viewPlayer": "玩家详情",
|
||||||
"viewBill": "账单详情",
|
"viewBill": "账单详情",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"status": "状态",
|
"status": "状态",
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"viewTicketInList": "查看该注单",
|
"viewTicketInList": "查看该注单",
|
||||||
|
"viewPlayer": "查看玩家",
|
||||||
"failReason": "失败原因",
|
"failReason": "失败原因",
|
||||||
"winAmount": "中奖",
|
"winAmount": "中奖",
|
||||||
"placedAt": "下单时间",
|
"placedAt": "下单时间",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function playerBalanceCells(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function playerFundingModeLabel(
|
export function playerFundingModeLabel(
|
||||||
row: Pick<AdminPlayerRow, "funding_mode">,
|
row: { funding_mode?: string | null },
|
||||||
t: (key: string, opts?: { defaultValue?: string }) => string,
|
t: (key: string, opts?: { defaultValue?: string }) => string,
|
||||||
): string {
|
): string {
|
||||||
if (row.funding_mode === "credit") {
|
if (row.funding_mode === "credit") {
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ export const PRD_RISK_ACCESS_ANY = [
|
|||||||
export const PRD_REPORTS_VIEW_ACCESS_ANY = [PRD_REPORT_VIEW] as const;
|
export const PRD_REPORTS_VIEW_ACCESS_ANY = [PRD_REPORT_VIEW] as const;
|
||||||
export const PRD_REPORTS_EXPORT_ACCESS_ANY = [PRD_REPORT_EXPORT] as const;
|
export const PRD_REPORTS_EXPORT_ACCESS_ANY = [PRD_REPORT_EXPORT] as const;
|
||||||
|
|
||||||
|
/** 系统设置(与后端 admin.settings.* 资源口径一致) */
|
||||||
|
export const PRD_SETTINGS_ACCESS_ANY = [
|
||||||
|
PRD_WALLET_RECONCILE_MANAGE,
|
||||||
|
PRD_REBATE_MANAGE,
|
||||||
|
PRD_REBATE_VIEW,
|
||||||
|
PRD_PAYOUT_MANAGE,
|
||||||
|
] as const;
|
||||||
|
|
||||||
/** 钱包流水 */
|
/** 钱包流水 */
|
||||||
export const PRD_WALLET_TX_ACCESS_ANY = [
|
export const PRD_WALLET_TX_ACCESS_ANY = [
|
||||||
PRD_WALLET_RECONCILE_MANAGE,
|
PRD_WALLET_RECONCILE_MANAGE,
|
||||||
@@ -90,13 +98,10 @@ export const PRD_WALLET_TX_ACCESS_ANY = [
|
|||||||
export const PRD_WALLET_TRANSFER_ACCESS_ANY = [
|
export const PRD_WALLET_TRANSFER_ACCESS_ANY = [
|
||||||
...PRD_WALLET_TX_ACCESS_ANY,
|
...PRD_WALLET_TX_ACCESS_ANY,
|
||||||
PRD_WALLET_ADJUST_MANAGE,
|
PRD_WALLET_ADJUST_MANAGE,
|
||||||
PRD_USERS_MANAGE,
|
|
||||||
PRD_USERS_VIEW_FINANCE,
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** 单玩家钱包查询 */
|
/** 单玩家钱包查询 */
|
||||||
export const PRD_WALLET_PLAYER_ACCESS_ANY = [
|
export const PRD_WALLET_PLAYER_ACCESS_ANY = [
|
||||||
PRD_USERS_MANAGE,
|
|
||||||
PRD_USERS_VIEW_FINANCE,
|
PRD_USERS_VIEW_FINANCE,
|
||||||
...PRD_WALLET_TX_ACCESS_ANY,
|
...PRD_WALLET_TX_ACCESS_ANY,
|
||||||
] as const;
|
] as const;
|
||||||
@@ -109,6 +114,13 @@ export const PRD_RULES_ODDS_ACCESS_ANY = [
|
|||||||
PRD_REBATE_VIEW,
|
PRD_REBATE_VIEW,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/** 投注规则配置页 */
|
||||||
|
export const PRD_RULES_PLAYS_ACCESS_ANY = [
|
||||||
|
PRD_PLAY_SWITCH_MANAGE,
|
||||||
|
PRD_ODDS_MANAGE,
|
||||||
|
PRD_ODDS_VIEW,
|
||||||
|
] as const;
|
||||||
|
|
||||||
/** 开奖页面入口 */
|
/** 开奖页面入口 */
|
||||||
export const PRD_DRAW_ACCESS_ANY = [
|
export const PRD_DRAW_ACCESS_ANY = [
|
||||||
PRD_DRAW_RESULT_VIEW,
|
PRD_DRAW_RESULT_VIEW,
|
||||||
@@ -142,6 +154,15 @@ export const PRD_DRAW_FINANCE_ACCESS_ANY = [
|
|||||||
/** 接入站点配置页 */
|
/** 接入站点配置页 */
|
||||||
export const PRD_INTEGRATION_ACCESS_ANY = [PRD_INTEGRATION_VIEW, PRD_INTEGRATION_MANAGE] as const;
|
export const PRD_INTEGRATION_ACCESS_ANY = [PRD_INTEGRATION_VIEW, PRD_INTEGRATION_MANAGE] as const;
|
||||||
|
|
||||||
|
/** 运营配置首页 */
|
||||||
|
export const PRD_CONFIG_HUB_ACCESS_ANY = [
|
||||||
|
...PRD_RULES_PLAYS_ACCESS_ANY,
|
||||||
|
...PRD_RULES_ODDS_ACCESS_ANY,
|
||||||
|
...PRD_JACKPOT_ACCESS_ANY,
|
||||||
|
...PRD_RISK_CAP_ACCESS_ANY,
|
||||||
|
...PRD_INTEGRATION_ACCESS_ANY,
|
||||||
|
] as const;
|
||||||
|
|
||||||
/** 代理管理 */
|
/** 代理管理 */
|
||||||
export const PRD_AGENT_VIEW = "prd.agent.view" as const;
|
export const PRD_AGENT_VIEW = "prd.agent.view" as const;
|
||||||
export const PRD_AGENT_MANAGE = "prd.agent.manage" as const;
|
export const PRD_AGENT_MANAGE = "prd.agent.manage" as const;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const AGENT_OWNER_LINE_ROOT_EXTRA_SLUGS = [
|
|||||||
"prd.agent.role.manage",
|
"prd.agent.role.manage",
|
||||||
"prd.agent.user.manage",
|
"prd.agent.user.manage",
|
||||||
"prd.users.manage",
|
"prd.users.manage",
|
||||||
"prd.users.view_finance",
|
|
||||||
"prd.users.view_cs",
|
"prd.users.view_cs",
|
||||||
"prd.settlement.agent.manage",
|
"prd.settlement.agent.manage",
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -165,9 +165,6 @@ export function AgentsPlayersPanel({
|
|||||||
const viewPlayerLabel = t("players:viewDetail", { defaultValue: "查看玩家详情" });
|
const viewPlayerLabel = t("players:viewDetail", { defaultValue: "查看玩家详情" });
|
||||||
const editPlayerLabel = t("players:editPlayer", { defaultValue: "编辑玩家" });
|
const editPlayerLabel = t("players:editPlayer", { defaultValue: "编辑玩家" });
|
||||||
const deletePlayerLabel = t("players:deletePlayer", { defaultValue: "删除玩家" });
|
const deletePlayerLabel = t("players:deletePlayer", { defaultValue: "删除玩家" });
|
||||||
const settlementCenterLabel = t("playersPanel.gotoSettlementCenter", {
|
|
||||||
defaultValue: "去结算中心",
|
|
||||||
});
|
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const boundAgent = profile?.agent ?? null;
|
const boundAgent = profile?.agent ?? null;
|
||||||
const isSuperAdmin = profile?.is_super_admin === true;
|
const isSuperAdmin = profile?.is_super_admin === true;
|
||||||
@@ -521,10 +518,15 @@ export function AgentsPlayersPanel({
|
|||||||
|
|
||||||
async function handlePayBill(): Promise<void> {
|
async function handlePayBill(): Promise<void> {
|
||||||
if (selectedBill === null) return;
|
if (selectedBill === null) return;
|
||||||
|
const amount = parseBillingAmount(payAmount || String(selectedBill.unpaid_amount ?? 0));
|
||||||
|
if (amount === null || amount <= 0 || amount > Number(selectedBill.unpaid_amount ?? 0)) {
|
||||||
|
toast.error(t("playersPanel.paymentAmountInvalid", { defaultValue: "请输入有效的收付金额" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBillingBusy(true);
|
setBillingBusy(true);
|
||||||
try {
|
try {
|
||||||
await postSettlementBillPayment(selectedBill.id, {
|
await postSettlementBillPayment(selectedBill.id, {
|
||||||
amount: Number(payAmount || selectedBill.unpaid_amount || 0),
|
amount,
|
||||||
method: payMethod.trim() || undefined,
|
method: payMethod.trim() || undefined,
|
||||||
proof: payProof.trim() || undefined,
|
proof: payProof.trim() || undefined,
|
||||||
});
|
});
|
||||||
@@ -546,10 +548,15 @@ export function AgentsPlayersPanel({
|
|||||||
|
|
||||||
async function handleWriteOffBill(): Promise<void> {
|
async function handleWriteOffBill(): Promise<void> {
|
||||||
if (selectedBill === null) return;
|
if (selectedBill === null) return;
|
||||||
|
const reason = badDebtReason.trim();
|
||||||
|
if (!reason) {
|
||||||
|
toast.error(t("playersPanel.badDebtReasonRequired", { defaultValue: "请填写核销原因" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBillingBusy(true);
|
setBillingBusy(true);
|
||||||
try {
|
try {
|
||||||
await postSettlementBillBadDebtWriteOff(selectedBill.id, {
|
await postSettlementBillBadDebtWriteOff(selectedBill.id, {
|
||||||
reason: badDebtReason.trim() || undefined,
|
reason,
|
||||||
});
|
});
|
||||||
toast.success(t("playersPanel.billWrittenOff", { defaultValue: "已核销坏账" }));
|
toast.success(t("playersPanel.billWrittenOff", { defaultValue: "已核销坏账" }));
|
||||||
await load();
|
await load();
|
||||||
@@ -567,6 +574,68 @@ export function AgentsPlayersPanel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBillingAmount(raw: string): number | null {
|
||||||
|
const value = Number(raw);
|
||||||
|
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestConfirmBillAction(): void {
|
||||||
|
if (selectedBill === null) return;
|
||||||
|
requestConfirm({
|
||||||
|
title: t("playersPanel.confirmBillTitle", { defaultValue: "确认账单?" }),
|
||||||
|
description: t("playersPanel.confirmBillDescription", {
|
||||||
|
defaultValue: "确认后账单会进入待收付状态,请确认金额与玩家无误。",
|
||||||
|
}),
|
||||||
|
confirmLabel: t("agents:settlementBills.confirm", { defaultValue: "确认账单" }),
|
||||||
|
confirmVariant: "default",
|
||||||
|
onConfirm: handleConfirmBill,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPayBillAction(): void {
|
||||||
|
if (selectedBill === null) return;
|
||||||
|
const amount = parseBillingAmount(payAmount || String(selectedBill.unpaid_amount ?? 0));
|
||||||
|
if (amount === null || amount <= 0) {
|
||||||
|
toast.error(t("playersPanel.paymentAmountInvalid", { defaultValue: "请输入大于 0 的整数金额" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount > Number(selectedBill.unpaid_amount ?? 0)) {
|
||||||
|
toast.error(t("playersPanel.paymentAmountTooLarge", { defaultValue: "收付金额不能超过未结金额" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestConfirm({
|
||||||
|
title: t("playersPanel.payBillConfirmTitle", { defaultValue: "确认登记收付?" }),
|
||||||
|
description: t("playersPanel.payBillConfirmDescription", {
|
||||||
|
defaultValue: "这会写入收付记录并更新玩家账单金额,请确认金额与凭证无误。",
|
||||||
|
}),
|
||||||
|
confirmLabel: t("agents:settlementBills.paid", { defaultValue: "登记收付" }),
|
||||||
|
confirmVariant: "default",
|
||||||
|
onConfirm: handlePayBill,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestWriteOffBillAction(): void {
|
||||||
|
if (selectedBill === null) return;
|
||||||
|
if (!badDebtReason.trim()) {
|
||||||
|
toast.error(t("playersPanel.badDebtReasonRequired", { defaultValue: "请填写核销原因" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestConfirm({
|
||||||
|
title: t("playersPanel.writeOffBillConfirmTitle", { defaultValue: "确认核销坏账?" }),
|
||||||
|
description: t("playersPanel.writeOffBillConfirmDescription", {
|
||||||
|
defaultValue: "核销会把该玩家账单未结金额归档为坏账记录,请确认已无法收回。",
|
||||||
|
}),
|
||||||
|
confirmLabel: t("agents:settlementBills.confirmBadDebt", { defaultValue: "确认核销" }),
|
||||||
|
confirmVariant: "destructive",
|
||||||
|
onConfirm: handleWriteOffBill,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
@@ -936,7 +1005,7 @@ export function AgentsPlayersPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedBill.status === "pending_confirm" ? (
|
{selectedBill.status === "pending_confirm" ? (
|
||||||
<Button type="button" className="w-full" disabled={billingBusy} onClick={() => void handleConfirmBill()}>
|
<Button type="button" className="w-full" disabled={billingBusy || confirmBusy} onClick={requestConfirmBillAction}>
|
||||||
{t("agents:settlementBills.confirm", { defaultValue: "确认账单" })}
|
{t("agents:settlementBills.confirm", { defaultValue: "确认账单" })}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -967,7 +1036,7 @@ export function AgentsPlayersPanel({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" className="w-full" disabled={billingBusy} onClick={() => void handlePayBill()}>
|
<Button type="button" className="w-full" disabled={billingBusy || confirmBusy} onClick={requestPayBillAction}>
|
||||||
{t("agents:settlementBills.paid", { defaultValue: "登记收付" })}
|
{t("agents:settlementBills.paid", { defaultValue: "登记收付" })}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -981,7 +1050,7 @@ export function AgentsPlayersPanel({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="destructive" className="w-full" disabled={billingBusy} onClick={() => void handleWriteOffBill()}>
|
<Button type="button" variant="destructive" className="w-full" disabled={billingBusy || confirmBusy} onClick={requestWriteOffBillAction}>
|
||||||
{t("agents:settlementBills.confirmBadDebt", { defaultValue: "确认核销" })}
|
{t("agents:settlementBills.confirmBadDebt", { defaultValue: "确认核销" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||||
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||||
@@ -560,7 +560,7 @@ export function PlayConfigDocScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5 md:w-[140px]">
|
<div className="flex flex-col gap-1.5 md:w-[140px]">
|
||||||
<span className="text-sm font-medium">{t("play.filters.category", { ns: "config" })}</span>
|
<span className="text-sm font-medium">{t("play.filters.category", { ns: "config" })}</span>
|
||||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
<Select value={categoryFilter} onValueChange={(value) => setCategoryFilter(value ?? "all")}>
|
||||||
<SelectTrigger className="h-8">
|
<SelectTrigger className="h-8">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{categoryFilter === "all"
|
{categoryFilter === "all"
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { ConfigDocPage } from "@/modules/config/config-doc-page";
|
import { ConfigDocPage } from "@/modules/config/config-doc-page";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { PRD_WALLET_ADJUST_MANAGE, PRD_WALLET_RECONCILE_MANAGE } from "@/lib/admin-prd";
|
||||||
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
|
||||||
function minorUnitsToDisplay(n: unknown, decimals = 2): string {
|
function minorUnitsToDisplay(n: unknown, decimals = 2): string {
|
||||||
@@ -20,9 +23,11 @@ function minorUnitsToDisplay(n: unknown, decimals = 2): string {
|
|||||||
return (num / 100).toFixed(decimals);
|
return (num / 100).toFixed(decimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayToMinorUnits(s: string): number {
|
function displayToMinorUnits(s: string): number | null {
|
||||||
const n = parseFloat(s);
|
const normalized = s.trim();
|
||||||
if (Number.isNaN(n) || n < 0) return 0;
|
if (normalized === "") return null;
|
||||||
|
const n = Number(normalized);
|
||||||
|
if (!Number.isFinite(n)) return null;
|
||||||
return Math.round(n * 100);
|
return Math.round(n * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,11 +51,44 @@ type WalletConfigDocScreenProps = {
|
|||||||
embedded?: boolean;
|
embedded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function validateDraft(draft: Draft, t: ReturnType<typeof useTranslation<["config", "adminUsers", "common"]>>["t"]): string[] {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const values = {
|
||||||
|
inMin: displayToMinorUnits(draft.inMin),
|
||||||
|
inMax: displayToMinorUnits(draft.inMax),
|
||||||
|
outMin: displayToMinorUnits(draft.outMin),
|
||||||
|
outMax: displayToMinorUnits(draft.outMax),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const field of ["inMin", "inMax", "outMin", "outMax"] as const) {
|
||||||
|
if (values[field] === null || values[field] < 1) {
|
||||||
|
errors.push(t("wallet.validation.amountAtLeastMinorUnit", {
|
||||||
|
ns: "config",
|
||||||
|
field: t(`wallet.fields.${field}`, { ns: "config" }),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.inMin !== null && values.inMax !== null && values.inMax < values.inMin) {
|
||||||
|
errors.push(t("wallet.validation.inRangeInvalid", { ns: "config" }));
|
||||||
|
}
|
||||||
|
if (values.outMin !== null && values.outMax !== null && values.outMax < values.outMin) {
|
||||||
|
errors.push(t("wallet.validation.outRangeInvalid", { ns: "config" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(errors)];
|
||||||
|
}
|
||||||
|
|
||||||
export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScreenProps) {
|
export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScreenProps) {
|
||||||
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
||||||
const tRef = useRef(t);
|
const tRef = useRef(t);
|
||||||
tRef.current = t;
|
tRef.current = t;
|
||||||
const shared = useOptionalAdminSettingsData();
|
const shared = useOptionalAdminSettingsData();
|
||||||
|
const profile = useAdminProfile();
|
||||||
|
const canManage = adminHasAnyPermission(profile?.permissions, [
|
||||||
|
PRD_WALLET_RECONCILE_MANAGE,
|
||||||
|
PRD_WALLET_ADJUST_MANAGE,
|
||||||
|
]);
|
||||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||||
const [draft, setDraft] = useState<Draft>({
|
const [draft, setDraft] = useState<Draft>({
|
||||||
inMin: "",
|
inMin: "",
|
||||||
@@ -66,6 +104,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
|
|||||||
draft.inMax !== saved.inMax ||
|
draft.inMax !== saved.inMax ||
|
||||||
draft.outMin !== saved.outMin ||
|
draft.outMin !== saved.outMin ||
|
||||||
draft.outMax !== saved.outMax;
|
draft.outMax !== saved.outMax;
|
||||||
|
const validationErrors = validateDraft(draft, t);
|
||||||
|
const hasValidationError = validationErrors.length > 0;
|
||||||
|
|
||||||
const loading = embedded ? (shared?.loading ?? true) : standaloneLoading;
|
const loading = embedded ? (shared?.loading ?? true) : standaloneLoading;
|
||||||
|
|
||||||
@@ -107,6 +147,11 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
if (hasValidationError) {
|
||||||
|
toast.error(validationErrors[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
if (draft.inMin !== saved.inMin) {
|
if (draft.inMin !== saved.inMin) {
|
||||||
items.push({ key: WALLET_KEYS.IN_MIN, value: displayToMinorUnits(draft.inMin) });
|
items.push({ key: WALLET_KEYS.IN_MIN, value: displayToMinorUnits(draft.inMin) });
|
||||||
@@ -156,7 +201,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
|
|||||||
placeholder={t("wallet.placeholders.min", { ns: "config" })}
|
placeholder={t("wallet.placeholders.min", { ns: "config" })}
|
||||||
value={draft.inMin}
|
value={draft.inMin}
|
||||||
onChange={(e) => handleChange("inMin", e.target.value)}
|
onChange={(e) => handleChange("inMin", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
|
aria-invalid={hasValidationError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -169,7 +215,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
|
|||||||
placeholder={t("wallet.placeholders.max", { ns: "config" })}
|
placeholder={t("wallet.placeholders.max", { ns: "config" })}
|
||||||
value={draft.inMax}
|
value={draft.inMax}
|
||||||
onChange={(e) => handleChange("inMax", e.target.value)}
|
onChange={(e) => handleChange("inMax", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
|
aria-invalid={hasValidationError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -182,7 +229,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
|
|||||||
placeholder={t("wallet.placeholders.min", { ns: "config" })}
|
placeholder={t("wallet.placeholders.min", { ns: "config" })}
|
||||||
value={draft.outMin}
|
value={draft.outMin}
|
||||||
onChange={(e) => handleChange("outMin", e.target.value)}
|
onChange={(e) => handleChange("outMin", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
|
aria-invalid={hasValidationError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -195,10 +243,18 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
|
|||||||
placeholder={t("wallet.placeholders.max", { ns: "config" })}
|
placeholder={t("wallet.placeholders.max", { ns: "config" })}
|
||||||
value={draft.outMax}
|
value={draft.outMax}
|
||||||
onChange={(e) => handleChange("outMax", e.target.value)}
|
onChange={(e) => handleChange("outMax", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
|
aria-invalid={hasValidationError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{validationErrors.length > 0 && (
|
||||||
|
<div className="space-y-1 text-xs text-destructive" role="alert">
|
||||||
|
{validationErrors.map((error) => (
|
||||||
|
<p key={error}>{error}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center gap-4 pt-2">
|
<div className="flex items-center gap-4 pt-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -209,7 +265,7 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
|
|||||||
onConfirm: () => handleSave(),
|
onConfirm: () => handleSave(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={!dirty || loading || saving}
|
disabled={!canManage || !dirty || hasValidationError || loading || saving}
|
||||||
>
|
>
|
||||||
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
|
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import { useCallback, useMemo, useState, type ReactElement } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
|
Flame,
|
||||||
|
Landmark,
|
||||||
Network,
|
Network,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Sparkles,
|
||||||
Ticket,
|
Ticket,
|
||||||
|
TrendingUp,
|
||||||
Users,
|
Users,
|
||||||
Wallet,
|
Wallet,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -48,7 +52,7 @@ export function AgentDashboardConsole(): ReactElement {
|
|||||||
const tRef = useTranslationRef(["dashboard", "common"]);
|
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const agent = profile?.agent ?? null;
|
const agent = profile?.agent ?? null;
|
||||||
const permissions = profile?.permissions ?? [];
|
const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]);
|
||||||
|
|
||||||
const todayLabel = useMemo(() => {
|
const todayLabel = useMemo(() => {
|
||||||
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
||||||
@@ -109,6 +113,7 @@ export function AgentDashboardConsole(): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const currency = "NPR";
|
const currency = "NPR";
|
||||||
|
const displayCurrency = overview?.currency_code ?? currency;
|
||||||
|
|
||||||
const quickLinks = useMemo(() => {
|
const quickLinks = useMemo(() => {
|
||||||
const links: { href: string; label: string; icon: ReactElement }[] = [];
|
const links: { href: string; label: string; icon: ReactElement }[] = [];
|
||||||
@@ -189,78 +194,268 @@ export function AgentDashboardConsole(): ReactElement {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : overview ? (
|
) : overview ? (
|
||||||
<section className="grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
<section className="space-y-4">
|
||||||
<Card>
|
<div className="grid min-w-0 grid-cols-1 gap-4 xl:grid-cols-[1.35fr_0.95fr]">
|
||||||
<CardHeader className="pb-2">
|
<Card className="overflow-hidden border-slate-200 bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.18),_transparent_42%),linear-gradient(135deg,_#0f172a,_#111827_55%,_#1f2937)] text-white shadow-[0_20px_60px_-25px_rgba(15,23,42,0.65)]">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.24em] text-emerald-200/80">
|
||||||
|
{t("agent.heroEyebrow")}
|
||||||
|
</p>
|
||||||
|
<CardTitle className="mt-2 text-2xl font-semibold">
|
||||||
|
{t("agent.heroTitle", { name: overview.agent_name || overview.agent_code })}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-full border border-white/15 bg-white/10 p-2">
|
||||||
|
<Sparkles className="size-4 text-emerald-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
||||||
|
<p className="text-xs text-slate-300">{t("agent.todayBet")}</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
||||||
|
{formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
||||||
|
<p className="text-xs text-slate-300">{t("agent.todayPayout")}</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
||||||
|
{formatDashboardMoneyMinor(overview.today_payout_minor, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
||||||
|
<p className="text-xs text-slate-300">{t("agent.todayProfit")}</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
||||||
|
{formatDashboardMoneyMinor(overview.today_profit_minor, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div className="rounded-xl border border-white/10 px-4 py-3">
|
||||||
|
<p className="text-[11px] text-slate-300">{t("agent.activePlayersToday")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.active_player_count_today}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/10 px-4 py-3">
|
||||||
|
<p className="text-[11px] text-slate-300">{t("agent.betOrdersToday")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.bet_order_count_today}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/10 px-4 py-3">
|
||||||
|
<p className="text-[11px] text-slate-300">{t("agent.pendingBills")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-slate-300">
|
||||||
|
<span>{t("agent.shareRate", { rate: overview.total_share_rate })}</span>
|
||||||
|
<span>{t("agent.settlementCycle", { cycle: overview.settlement_cycle })}</span>
|
||||||
|
<span>
|
||||||
|
{overview.latest_bet_at
|
||||||
|
? t("agent.latestBetAt", { time: new Date(overview.latest_bet_at).toLocaleString() })
|
||||||
|
: t("agent.noBetToday")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-slate-200 bg-[linear-gradient(180deg,_rgba(248,250,252,0.98),_rgba(241,245,249,0.92))]">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-sm font-semibold">{t("agent.creditTitle")}</CardTitle>
|
<CardTitle className="text-sm font-semibold">{t("agent.creditTitle")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-1 text-sm">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-2xl font-semibold tabular-nums">
|
<div>
|
||||||
{formatDashboardCreditMajor(overview.credit_limit, currency)}
|
<p className="text-3xl font-semibold tabular-nums text-slate-900">
|
||||||
|
{formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-slate-500">
|
||||||
{t("agent.creditAvailable", {
|
{t("agent.creditAvailable", {
|
||||||
amount: formatDashboardCreditMajor(overview.available_credit, currency),
|
amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div className="rounded-2xl border bg-white px-4 py-3">
|
||||||
|
<p className="text-xs text-slate-500">{t("agent.creditAllocatedLabel")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">
|
||||||
|
{formatDashboardCreditMajor(overview.allocated_credit, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border bg-white px-4 py-3">
|
||||||
|
<p className="text-xs text-slate-500">{t("agent.creditUsedLabel")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">
|
||||||
|
{formatDashboardCreditMajor(overview.used_credit, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
{t("agent.pendingUnpaid", {
|
||||||
|
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<TrendingUp className="size-4 text-emerald-600" />
|
||||||
|
{t("agent.sevenDayTitle")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-1 text-sm">
|
||||||
|
<p className="text-xl font-semibold tabular-nums">
|
||||||
|
{formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("agent.sevenDayPayout", {
|
||||||
|
amount: formatDashboardMoneyMinor(overview.seven_day_payout_minor, displayCurrency),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("agent.creditAllocated", {
|
{t("agent.sevenDayProfit", {
|
||||||
amount: formatDashboardCreditMajor(overview.allocated_credit, currency),
|
amount: formatDashboardMoneyMinor(overview.seven_day_profit_minor, displayCurrency),
|
||||||
})}
|
})}
|
||||||
{" · "}
|
|
||||||
{t("agent.creditUsed", {
|
|
||||||
amount: formatDashboardCreditMajor(overview.used_credit, currency),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("agent.shareRate", { rate: overview.total_share_rate })}
|
|
||||||
{" · "}
|
|
||||||
{t("agent.settlementCycle", { cycle: overview.settlement_cycle })}
|
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-semibold">{t("agent.teamTitle")}</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<Users className="size-4 text-sky-600" />
|
||||||
|
{t("agent.teamTitle")}
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">{t("agent.directChildren")}</p>
|
<p className="text-xs text-muted-foreground">{t("agent.directChildren")}</p>
|
||||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_child_count}</p>
|
<p className="text-xl font-semibold tabular-nums">{overview.direct_child_count}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("agent.subtreeAgents")}</p>
|
||||||
|
<p className="text-xl font-semibold tabular-nums">{overview.subtree_agent_count}</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">{t("agent.directPlayers")}</p>
|
<p className="text-xs text-muted-foreground">{t("agent.directPlayers")}</p>
|
||||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_player_count}</p>
|
<p className="text-xl font-semibold tabular-nums">{overview.direct_player_count}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">{t("agent.subtreeAgents")}</p>
|
<p className="text-xs text-muted-foreground">{t("agent.teamPlayers")}</p>
|
||||||
<p className="text-lg font-semibold tabular-nums">{overview.subtree_agent_count}</p>
|
<p className="text-xl font-semibold tabular-nums">{overview.team_player_count}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="sm:col-span-2 xl:col-span-2">
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-semibold">{t("agent.pendingBills")}</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||||
{adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? (
|
<Wallet className="size-4 text-amber-600" />
|
||||||
<Link
|
{t("agent.pendingBills")}
|
||||||
href="/admin/settlement-center"
|
</CardTitle>
|
||||||
className={cn(buttonVariants({ variant: "link", size: "sm" }), "h-auto px-0 text-xs")}
|
|
||||||
>
|
|
||||||
{t("agent.viewBills")}
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-1">
|
||||||
<p className="text-2xl font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
<p className="text-2xl font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("agent.pendingUnpaid", {
|
{t("agent.pendingUnpaid", {
|
||||||
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, currency),
|
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<Flame className="size-4 text-rose-600" />
|
||||||
|
{t("agent.topMomentum")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-1">
|
||||||
|
{overview.top_agent_today ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm font-semibold text-slate-900">
|
||||||
|
{overview.top_agent_today.agent_name || overview.top_agent_today.agent_code}
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-semibold tabular-nums">
|
||||||
|
{formatDashboardMoneyMinor(overview.top_agent_today.total_bet_minor, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("agent.topMomentumHint", {
|
||||||
|
profit: formatDashboardMoneyMinor(
|
||||||
|
overview.top_agent_today.approx_house_gross_minor,
|
||||||
|
displayCurrency,
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">{t("agent.noBetToday")}</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||||
|
<Landmark className="size-4 text-slate-700" />
|
||||||
|
{t("agent.managementFocus")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
||||||
|
<p className="text-xs text-slate-500">{t("agent.focusBet")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">
|
||||||
|
{formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
||||||
|
<p className="text-xs text-slate-500">{t("agent.focusPlayers")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.active_player_count_today}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
||||||
|
<p className="text-xs text-slate-500">{t("agent.focusBills")}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-semibold">{t("agent.quickStatsTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">{t("agent.canCreateChildAgent")}</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{overview.can_create_child_agent ? t("agent.yes") : t("agent.no")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">{t("agent.canCreatePlayer")}</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{overview.can_create_player ? t("agent.yes") : t("agent.no")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">{t("agent.lineDepth")}</span>
|
||||||
|
<span className="font-medium tabular-nums">{overview.depth}</span>
|
||||||
|
</div>
|
||||||
|
{adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? (
|
||||||
|
<Link
|
||||||
|
href="/admin/settlement-center"
|
||||||
|
className={cn(buttonVariants({ variant: "link", size: "sm" }), "h-auto px-0")}
|
||||||
|
>
|
||||||
|
{t("agent.viewBills")}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import { drawStatusLabel, hallPreviewDiffersFromDbStatus } from "./draw-display"
|
|||||||
import { DrawStatusBadge } from "./draw-status-badge";
|
import { DrawStatusBadge } from "./draw-status-badge";
|
||||||
import {
|
import {
|
||||||
PRD_DRAW_REOPEN_MANAGE,
|
PRD_DRAW_REOPEN_MANAGE,
|
||||||
PRD_DRAW_RESULT_MANAGE,
|
|
||||||
PRD_PAYOUT_MANAGE,
|
PRD_PAYOUT_MANAGE,
|
||||||
PRD_PAYOUT_REVIEW,
|
PRD_PAYOUT_REVIEW,
|
||||||
} from "./draw-prd";
|
} from "./draw-prd";
|
||||||
@@ -173,7 +172,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
label: t("manualClose"),
|
label: t("manualClose"),
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
enabled: ["pending", "open"].includes(data.status),
|
enabled: ["pending", "open"].includes(data.status),
|
||||||
onConfirm: () => postAdminManualCloseDraw(idNum),
|
onConfirm: async () => { await postAdminManualCloseDraw(idNum); },
|
||||||
confirmTitle: t("confirm.manualCloseTitle"),
|
confirmTitle: t("confirm.manualCloseTitle"),
|
||||||
confirmDescription: t("confirm.manualCloseDescription"),
|
confirmDescription: t("confirm.manualCloseDescription"),
|
||||||
},
|
},
|
||||||
@@ -182,7 +181,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
label: t("cancelBeforeDraw"),
|
label: t("cancelBeforeDraw"),
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
enabled: ["pending", "open", "closing", "closed"].includes(data.status),
|
enabled: ["pending", "open", "closing", "closed"].includes(data.status),
|
||||||
onConfirm: () => postAdminCancelDraw(idNum),
|
onConfirm: async () => { await postAdminCancelDraw(idNum); },
|
||||||
confirmTitle: t("confirm.cancelDrawTitle"),
|
confirmTitle: t("confirm.cancelDrawTitle"),
|
||||||
confirmDescription: t("confirm.cancelDrawDescription"),
|
confirmDescription: t("confirm.cancelDrawDescription"),
|
||||||
},
|
},
|
||||||
@@ -191,7 +190,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
label: t("rngAutoGenerate"),
|
label: t("rngAutoGenerate"),
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
enabled: data.status === "closed",
|
enabled: data.status === "closed",
|
||||||
onConfirm: () => postAdminRunDrawRng(idNum),
|
onConfirm: async () => { await postAdminRunDrawRng(idNum); },
|
||||||
confirmTitle: t("confirm.rngDrawTitle"),
|
confirmTitle: t("confirm.rngDrawTitle"),
|
||||||
confirmDescription: t("confirm.rngDrawDescription"),
|
confirmDescription: t("confirm.rngDrawDescription"),
|
||||||
},
|
},
|
||||||
@@ -204,7 +203,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
label: t("cooldownReopen"),
|
label: t("cooldownReopen"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
enabled: data.status === "cooldown",
|
enabled: data.status === "cooldown",
|
||||||
onConfirm: () => postAdminReopenDraw(idNum),
|
onConfirm: async () => { await postAdminReopenDraw(idNum); },
|
||||||
confirmTitle: t("confirm.reopenTitle"),
|
confirmTitle: t("confirm.reopenTitle"),
|
||||||
confirmDescription: t("confirm.reopenDescription"),
|
confirmDescription: t("confirm.reopenDescription"),
|
||||||
confirmVariant: "destructive",
|
confirmVariant: "destructive",
|
||||||
@@ -217,7 +216,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
label: t("runSettlement"),
|
label: t("runSettlement"),
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
enabled: data.status === "settling",
|
enabled: data.status === "settling",
|
||||||
onConfirm: () => postAdminRunDrawSettlement(idNum),
|
onConfirm: async () => { await postAdminRunDrawSettlement(idNum); },
|
||||||
confirmTitle: t("confirm.runSettlementTitle"),
|
confirmTitle: t("confirm.runSettlementTitle"),
|
||||||
confirmDescription: t("confirm.runSettlementDescription"),
|
confirmDescription: t("confirm.runSettlementDescription"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
putAdminIntegrationSite,
|
putAdminIntegrationSite,
|
||||||
} from "@/api/admin-integration-sites";
|
} from "@/api/admin-integration-sites";
|
||||||
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
||||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -41,7 +41,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { getAdminPageBundle } from "@/lib/admin-permission-bundles";
|
import { getAdminPageBundle } from "@/lib/admin-permission-bundles";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
@@ -252,7 +252,7 @@ export function IntegrationSitesConsole({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [tRef]);
|
||||||
|
|
||||||
useAsyncEffect(() => {
|
useAsyncEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
@@ -572,6 +572,7 @@ export function IntegrationSitesConsole({
|
|||||||
key: "connectivity",
|
key: "connectivity",
|
||||||
label: t("integrationSites.connectivityTest"),
|
label: t("integrationSites.connectivityTest"),
|
||||||
icon: Link2,
|
icon: Link2,
|
||||||
|
hidden: !canManage,
|
||||||
onClick: () => openConnectivity(row),
|
onClick: () => openConnectivity(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -790,7 +791,7 @@ export function IntegrationSitesConsole({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={connectivityBusy || connectivityPlayerId.trim() === ""}
|
disabled={!canManage || connectivityBusy || connectivityPlayerId.trim() === ""}
|
||||||
onClick={() => void runConnectivityTest()}
|
onClick={() => void runConnectivityTest()}
|
||||||
>
|
>
|
||||||
{connectivityBusy
|
{connectivityBusy
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
import { flattenAgentTree, type FlatAgentOption } from "@/lib/admin-agent-tree";
|
import { flattenAgentTree, type FlatAgentOption } from "@/lib/admin-agent-tree";
|
||||||
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
|
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
|
||||||
import { AdminAgentCell, AdminAgentHead } from "@/components/admin/admin-agent-columns";
|
import { AdminAgentCell, AdminAgentHead } from "@/components/admin/admin-agent-columns";
|
||||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||||
@@ -41,7 +41,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { PRD_PLAYER_FREEZE_MANAGE, PRD_USERS_MANAGE } from "@/lib/admin-prd";
|
import { PRD_PLAYER_FREEZE_MANAGE, PRD_USERS_MANAGE } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
@@ -705,7 +705,7 @@ export function PlayersConsole(): React.ReactElement {
|
|||||||
<Select
|
<Select
|
||||||
value={formSiteCode}
|
value={formSiteCode}
|
||||||
onValueChange={(code) => {
|
onValueChange={(code) => {
|
||||||
setFormSiteCode(code);
|
setFormSiteCode(code ?? "");
|
||||||
setFormAgentNodeId(undefined);
|
setFormAgentNodeId(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import type { AdminSettingBatchItem } from "@/api/admin-settings";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd";
|
||||||
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
|
||||||
interface DrawDraft {
|
interface DrawDraft {
|
||||||
defaultCurrency: string;
|
defaultCurrency: string;
|
||||||
@@ -90,6 +93,8 @@ function buildDirtyItems(draft: DrawDraft, saved: DrawDraft): AdminSettingBatchI
|
|||||||
|
|
||||||
export function DrawSettingsPanel() {
|
export function DrawSettingsPanel() {
|
||||||
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
||||||
|
const profile = useAdminProfile();
|
||||||
|
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
|
||||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||||
const buildItems = useCallback(buildDirtyItems, []);
|
const buildItems = useCallback(buildDirtyItems, []);
|
||||||
const section = useSettingsSection({
|
const section = useSettingsSection({
|
||||||
@@ -113,7 +118,7 @@ export function DrawSettingsPanel() {
|
|||||||
<Label className="text-sm font-medium">{t("system.fields.manualReview", { ns: "config" })}</Label>
|
<Label className="text-sm font-medium">{t("system.fields.manualReview", { ns: "config" })}</Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={draft.requireManualReview}
|
checked={draft.requireManualReview}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
aria-label={t("system.fields.manualReview", { ns: "config" })}
|
aria-label={t("system.fields.manualReview", { ns: "config" })}
|
||||||
onCheckedChange={(value) => updateField("requireManualReview", value)}
|
onCheckedChange={(value) => updateField("requireManualReview", value)}
|
||||||
/>
|
/>
|
||||||
@@ -131,7 +136,7 @@ export function DrawSettingsPanel() {
|
|||||||
value={draft.defaultCurrency}
|
value={draft.defaultCurrency}
|
||||||
placeholder={t("system.placeholders.defaultCurrency", { ns: "config" })}
|
placeholder={t("system.placeholders.defaultCurrency", { ns: "config" })}
|
||||||
onChange={(e) => updateField("defaultCurrency", e.target.value.toUpperCase())}
|
onChange={(e) => updateField("defaultCurrency", e.target.value.toUpperCase())}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
maxLength={16}
|
maxLength={16}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +153,7 @@ export function DrawSettingsPanel() {
|
|||||||
value={draft.drawIntervalMinutes}
|
value={draft.drawIntervalMinutes}
|
||||||
placeholder={t("system.placeholders.drawIntervalMinutes", { ns: "config" })}
|
placeholder={t("system.placeholders.drawIntervalMinutes", { ns: "config" })}
|
||||||
onChange={(e) => updateField("drawIntervalMinutes", e.target.value)}
|
onChange={(e) => updateField("drawIntervalMinutes", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
@@ -163,7 +168,7 @@ export function DrawSettingsPanel() {
|
|||||||
value={draft.drawBettingWindowSeconds}
|
value={draft.drawBettingWindowSeconds}
|
||||||
placeholder={t("system.placeholders.drawBettingWindowSeconds", { ns: "config" })}
|
placeholder={t("system.placeholders.drawBettingWindowSeconds", { ns: "config" })}
|
||||||
onChange={(e) => updateField("drawBettingWindowSeconds", e.target.value)}
|
onChange={(e) => updateField("drawBettingWindowSeconds", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
@@ -178,7 +183,7 @@ export function DrawSettingsPanel() {
|
|||||||
value={draft.drawCloseBeforeDrawSeconds}
|
value={draft.drawCloseBeforeDrawSeconds}
|
||||||
placeholder={t("system.placeholders.drawCloseBeforeDrawSeconds", { ns: "config" })}
|
placeholder={t("system.placeholders.drawCloseBeforeDrawSeconds", { ns: "config" })}
|
||||||
onChange={(e) => updateField("drawCloseBeforeDrawSeconds", e.target.value)}
|
onChange={(e) => updateField("drawCloseBeforeDrawSeconds", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
@@ -193,7 +198,7 @@ export function DrawSettingsPanel() {
|
|||||||
value={draft.drawBufferDrawsAhead}
|
value={draft.drawBufferDrawsAhead}
|
||||||
placeholder={t("system.placeholders.drawBufferDrawsAhead", { ns: "config" })}
|
placeholder={t("system.placeholders.drawBufferDrawsAhead", { ns: "config" })}
|
||||||
onChange={(e) => updateField("drawBufferDrawsAhead", e.target.value)}
|
onChange={(e) => updateField("drawBufferDrawsAhead", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
@@ -208,14 +213,14 @@ export function DrawSettingsPanel() {
|
|||||||
value={draft.cooldownMinutes}
|
value={draft.cooldownMinutes}
|
||||||
placeholder={t("system.placeholders.cooldownMinutes", { ns: "config" })}
|
placeholder={t("system.placeholders.cooldownMinutes", { ns: "config" })}
|
||||||
onChange={(e) => updateField("cooldownMinutes", e.target.value)}
|
onChange={(e) => updateField("cooldownMinutes", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsSectionActions
|
<SettingsSectionActions
|
||||||
dirty={dirty}
|
dirty={dirty}
|
||||||
loading={loading}
|
loading={!canManage || loading}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onSave={() =>
|
onSave={() =>
|
||||||
requestConfirm({
|
requestConfirm({
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import type { AdminSettingBatchItem } from "@/api/admin-settings";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { PRD_ODDS_MANAGE, PRD_REBATE_MANAGE } from "@/lib/admin-prd";
|
||||||
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
|
||||||
interface FrontendDraft {
|
interface FrontendDraft {
|
||||||
playRulesHtmlZh: string;
|
playRulesHtmlZh: string;
|
||||||
@@ -51,6 +54,8 @@ function buildDirtyItems(draft: FrontendDraft, saved: FrontendDraft): AdminSetti
|
|||||||
|
|
||||||
export function FrontendSettingsPanel() {
|
export function FrontendSettingsPanel() {
|
||||||
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
||||||
|
const profile = useAdminProfile();
|
||||||
|
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_ODDS_MANAGE, PRD_REBATE_MANAGE]);
|
||||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||||
const buildItems = useCallback(buildDirtyItems, []);
|
const buildItems = useCallback(buildDirtyItems, []);
|
||||||
const section = useSettingsSection({
|
const section = useSettingsSection({
|
||||||
@@ -84,7 +89,7 @@ export function FrontendSettingsPanel() {
|
|||||||
id="play-rules-html-zh"
|
id="play-rules-html-zh"
|
||||||
value={draft.playRulesHtmlZh}
|
value={draft.playRulesHtmlZh}
|
||||||
onChange={(e) => updateField("playRulesHtmlZh", e.target.value)}
|
onChange={(e) => updateField("playRulesHtmlZh", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
className="min-h-[200px] font-mono text-xs"
|
className="min-h-[200px] font-mono text-xs"
|
||||||
placeholder="<div>...</div>"
|
placeholder="<div>...</div>"
|
||||||
/>
|
/>
|
||||||
@@ -94,7 +99,7 @@ export function FrontendSettingsPanel() {
|
|||||||
id="play-rules-html-en"
|
id="play-rules-html-en"
|
||||||
value={draft.playRulesHtmlEn}
|
value={draft.playRulesHtmlEn}
|
||||||
onChange={(e) => updateField("playRulesHtmlEn", e.target.value)}
|
onChange={(e) => updateField("playRulesHtmlEn", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
className="min-h-[200px] font-mono text-xs"
|
className="min-h-[200px] font-mono text-xs"
|
||||||
placeholder="<div>...</div>"
|
placeholder="<div>...</div>"
|
||||||
/>
|
/>
|
||||||
@@ -104,7 +109,7 @@ export function FrontendSettingsPanel() {
|
|||||||
id="play-rules-html-ne"
|
id="play-rules-html-ne"
|
||||||
value={draft.playRulesHtmlNe}
|
value={draft.playRulesHtmlNe}
|
||||||
onChange={(e) => updateField("playRulesHtmlNe", e.target.value)}
|
onChange={(e) => updateField("playRulesHtmlNe", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={!canManage || loading || saving}
|
||||||
className="min-h-[200px] font-mono text-xs"
|
className="min-h-[200px] font-mono text-xs"
|
||||||
placeholder="<div>...</div>"
|
placeholder="<div>...</div>"
|
||||||
/>
|
/>
|
||||||
@@ -113,7 +118,7 @@ export function FrontendSettingsPanel() {
|
|||||||
|
|
||||||
<SettingsSectionActions
|
<SettingsSectionActions
|
||||||
dirty={dirty}
|
dirty={dirty}
|
||||||
loading={loading}
|
loading={!canManage || loading}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onSave={() =>
|
onSave={() =>
|
||||||
requestConfirm({
|
requestConfirm({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { AdminSettingBatchItem } from "@/api/admin-settings";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { PRD_PAYOUT_MANAGE } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
|
||||||
interface SettlementDraft {
|
interface SettlementDraft {
|
||||||
@@ -57,7 +58,7 @@ function buildDirtyItems(draft: SettlementDraft, saved: SettlementDraft): AdminS
|
|||||||
export function SettlementSettingsPanel() {
|
export function SettlementSettingsPanel() {
|
||||||
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const canManage = adminHasAnyPermission(profile?.permissions, ["prd.payout.manage"]);
|
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_MANAGE]);
|
||||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||||
const buildItems = useCallback(buildDirtyItems, []);
|
const buildItems = useCallback(buildDirtyItems, []);
|
||||||
const section = useSettingsSection({
|
const section = useSettingsSection({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
postSettlementBillPayment,
|
postSettlementBillPayment,
|
||||||
type RebateAllocationRow,
|
type RebateAllocationRow,
|
||||||
type SettlementBillRow,
|
type SettlementBillRow,
|
||||||
type SettlementPaymentRow,
|
type SettlementBillPaymentRow,
|
||||||
} from "@/api/admin-agent-settlement";
|
} from "@/api/admin-agent-settlement";
|
||||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||||
@@ -26,6 +26,8 @@ import { describeBillPaymentDirection } from "@/modules/settlement/settlement-bi
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
|
||||||
type AgentBillDetailProps = {
|
type AgentBillDetailProps = {
|
||||||
billId: number;
|
billId: number;
|
||||||
@@ -42,15 +44,17 @@ export function AgentBillDetail({
|
|||||||
}: AgentBillDetailProps): React.ReactElement {
|
}: AgentBillDetailProps): React.ReactElement {
|
||||||
const { t } = useTranslation(["agents", "settlementCenter", "common"]);
|
const { t } = useTranslation(["agents", "settlementCenter", "common"]);
|
||||||
const [bill, setBill] = useState<SettlementBillRow | null>(null);
|
const [bill, setBill] = useState<SettlementBillRow | null>(null);
|
||||||
const [payments, setPayments] = useState<SettlementPaymentRow[]>([]);
|
const [payments, setPayments] = useState<SettlementBillPaymentRow[]>([]);
|
||||||
const [rebateAllocations, setRebateAllocations] = useState<RebateAllocationRow[]>([]);
|
const [rebateAllocations, setRebateAllocations] = useState<RebateAllocationRow[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [payAmount, setPayAmount] = useState("");
|
const [payAmount, setPayAmount] = useState("");
|
||||||
const [payMethod, setPayMethod] = useState("");
|
const [payMethod, setPayMethod] = useState("");
|
||||||
const [payProof, setPayProof] = useState("");
|
const [payProof, setPayProof] = useState("");
|
||||||
const [adjustAmount, setAdjustAmount] = useState("");
|
const [adjustAmount, setAdjustAmount] = useState("");
|
||||||
|
const [adjustReason, setAdjustReason] = useState("");
|
||||||
const [badDebtReason, setBadDebtReason] = useState("");
|
const [badDebtReason, setBadDebtReason] = useState("");
|
||||||
const [rebateDetailsOpen, setRebateDetailsOpen] = useState(false);
|
const [rebateDetailsOpen, setRebateDetailsOpen] = useState(false);
|
||||||
|
const { request: requestConfirm, ConfirmDialog, busy: confirmBusy } = useConfirmAction();
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -111,7 +115,153 @@ export function AgentBillDetail({
|
|||||||
),
|
),
|
||||||
).sort((a, b) => b.amount - a.amount || a.label.localeCompare(b.label, "zh-CN"));
|
).sort((a, b) => b.amount - a.amount || a.label.localeCompare(b.label, "zh-CN"));
|
||||||
|
|
||||||
|
const showActionError = (err: unknown, fallback: string): void => {
|
||||||
|
toast.error(err instanceof LotteryApiBizError ? err.message : fallback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseWholeAmount = (raw: string): number | null => {
|
||||||
|
const value = Number(raw);
|
||||||
|
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestConfirmBill = (): void => {
|
||||||
|
requestConfirm({
|
||||||
|
title: t("settlementBills.confirmBillTitle", { defaultValue: "确认账单?" }),
|
||||||
|
description: t("settlementBills.confirmBillDescription", {
|
||||||
|
defaultValue: "确认后账单会进入待收付状态,请确认金额与双方无误。",
|
||||||
|
}),
|
||||||
|
confirmLabel: t("settlementBills.confirm", { defaultValue: "确认账单" }),
|
||||||
|
confirmVariant: "default",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await postSettlementBillConfirm(billId);
|
||||||
|
await load();
|
||||||
|
await onUpdated?.();
|
||||||
|
toast.success(t("settlementBills.confirmed", { defaultValue: "已确认" }));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
showActionError(
|
||||||
|
err,
|
||||||
|
t("settlementBills.confirmFailed", { defaultValue: "确认账单失败" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPayment = (): void => {
|
||||||
|
const amount = parseWholeAmount(payAmount);
|
||||||
|
if (amount === null || amount <= 0) {
|
||||||
|
toast.error(t("settlementBills.paymentAmountInvalid", { defaultValue: "请输入大于 0 的整数金额" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount > bill.unpaid_amount) {
|
||||||
|
toast.error(t("settlementBills.paymentAmountTooLarge", { defaultValue: "收付金额不能超过未结金额" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestConfirm({
|
||||||
|
title: t("settlementBills.paymentConfirmTitle", { defaultValue: "确认登记收付?" }),
|
||||||
|
description: t("settlementBills.paymentConfirmDescription", {
|
||||||
|
defaultValue: "这会写入收付记录并更新账单已付/未结金额,请确认金额与方向无误。",
|
||||||
|
}),
|
||||||
|
confirmLabel: paymentSubmit,
|
||||||
|
confirmVariant: "default",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await postSettlementBillPayment(billId, {
|
||||||
|
amount,
|
||||||
|
method: payMethod.trim() || undefined,
|
||||||
|
proof: payProof.trim() || undefined,
|
||||||
|
});
|
||||||
|
await load();
|
||||||
|
await onUpdated?.();
|
||||||
|
toast.success(t("settlementBills.paid", { defaultValue: "已登记收付" }));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
showActionError(
|
||||||
|
err,
|
||||||
|
t("settlementBills.paymentFailed", { defaultValue: "登记收付失败" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestBadDebtWriteOff = (): void => {
|
||||||
|
const reason = badDebtReason.trim();
|
||||||
|
if (!reason) {
|
||||||
|
toast.error(t("settlementBills.badDebtReasonRequired", { defaultValue: "请填写核销原因" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestConfirm({
|
||||||
|
title: t("settlementBills.badDebtConfirmTitle", { defaultValue: "确认核销坏账?" }),
|
||||||
|
description: t("settlementBills.badDebtConfirmDescription", {
|
||||||
|
defaultValue: "核销会把未结金额归档为坏账记录,这类资金动作需要确认原因无误。",
|
||||||
|
}),
|
||||||
|
confirmLabel: t("settlementBills.confirmBadDebt", { defaultValue: "确认核销" }),
|
||||||
|
confirmVariant: "destructive",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await postSettlementBillBadDebtWriteOff(billId, { reason });
|
||||||
|
await load();
|
||||||
|
await onUpdated?.();
|
||||||
|
toast.success(t("settlementBills.badDebtDone", { defaultValue: "已核销坏账" }));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
showActionError(
|
||||||
|
err,
|
||||||
|
t("settlementBills.badDebtFailed", { defaultValue: "核销坏账失败" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestAdjustment = (): void => {
|
||||||
|
const amount = parseWholeAmount(adjustAmount);
|
||||||
|
const reason = adjustReason.trim();
|
||||||
|
if (amount === null || amount === 0) {
|
||||||
|
toast.error(t("settlementBills.adjustmentAmountInvalid", { defaultValue: "请输入非 0 的整数调整金额" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!reason) {
|
||||||
|
toast.error(t("settlementBills.adjustmentReasonRequired", { defaultValue: "请填写补差/冲正原因" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestConfirm({
|
||||||
|
title: t("settlementBills.adjustmentConfirmTitle", { defaultValue: "确认创建补差/冲正单?" }),
|
||||||
|
description: t("settlementBills.adjustmentConfirmDescription", {
|
||||||
|
defaultValue: "提交后会生成独立调账单,需要后续确认与收付;请确认正负方向和原因无误。",
|
||||||
|
}),
|
||||||
|
confirmLabel: t("settlementBills.createAdjustment", { defaultValue: "创建补差单" }),
|
||||||
|
confirmVariant: amount < 0 ? "destructive" : "default",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await postSettlementBillAdjustment(billId, {
|
||||||
|
amount,
|
||||||
|
adjustment_type: amount < 0 ? "reversal" : "adjustment",
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
setAdjustAmount("");
|
||||||
|
setAdjustReason("");
|
||||||
|
toast.success(t("settlementBills.adjustmentCreated", { defaultValue: "已创建补差单" }));
|
||||||
|
await onUpdated?.();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
showActionError(
|
||||||
|
err,
|
||||||
|
t("settlementBills.adjustmentFailed", { defaultValue: "创建补差单失败" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<ConfirmDialog />
|
||||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.35fr)_minmax(340px,0.95fr)]">
|
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.35fr)_minmax(340px,0.95fr)]">
|
||||||
<div className="space-y-5 text-sm">
|
<div className="space-y-5 text-sm">
|
||||||
<SettlementBillSummaryHeader bill={bill} currencyCode={currencyCode} />
|
<SettlementBillSummaryHeader bill={bill} currencyCode={currencyCode} />
|
||||||
@@ -218,12 +368,8 @@ export function AgentBillDetail({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() =>
|
disabled={confirmBusy}
|
||||||
void postSettlementBillConfirm(billId)
|
onClick={requestConfirmBill}
|
||||||
.then(load)
|
|
||||||
.then(onUpdated)
|
|
||||||
.then(() => toast.success(t("settlementBills.confirmed", { defaultValue: "已确认" })))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t("settlementBills.confirm", { defaultValue: "确认账单" })}
|
{t("settlementBills.confirm", { defaultValue: "确认账单" })}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -279,16 +425,8 @@ export function AgentBillDetail({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() =>
|
disabled={confirmBusy}
|
||||||
void postSettlementBillPayment(billId, {
|
onClick={requestPayment}
|
||||||
amount: Number(payAmount),
|
|
||||||
method: payMethod.trim() || undefined,
|
|
||||||
proof: payProof.trim() || undefined,
|
|
||||||
})
|
|
||||||
.then(load)
|
|
||||||
.then(onUpdated)
|
|
||||||
.then(() => toast.success(t("settlementBills.paid", { defaultValue: "已登记收付" })))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{paymentSubmit}
|
{paymentSubmit}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -321,16 +459,8 @@ export function AgentBillDetail({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() =>
|
disabled={confirmBusy}
|
||||||
void postSettlementBillBadDebtWriteOff(billId, {
|
onClick={requestBadDebtWriteOff}
|
||||||
reason: badDebtReason.trim() || undefined,
|
|
||||||
})
|
|
||||||
.then(load)
|
|
||||||
.then(onUpdated)
|
|
||||||
.then(() =>
|
|
||||||
toast.success(t("settlementBills.badDebtDone", { defaultValue: "已核销坏账" })),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t("settlementBills.confirmBadDebt", { defaultValue: "确认核销" })}
|
{t("settlementBills.confirmBadDebt", { defaultValue: "确认核销" })}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -360,20 +490,22 @@ export function AgentBillDetail({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>{t("settlementBills.adjustmentReason", { defaultValue: "调整原因" })}</Label>
|
||||||
|
<Input
|
||||||
|
value={adjustReason}
|
||||||
|
onChange={(e) => setAdjustReason(e.target.value)}
|
||||||
|
placeholder={t("settlementBills.adjustmentReasonPlaceholder", {
|
||||||
|
defaultValue: "例如:人工复核补差、冲正错账",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() =>
|
disabled={confirmBusy}
|
||||||
void postSettlementBillAdjustment(billId, {
|
onClick={requestAdjustment}
|
||||||
amount: Number(adjustAmount),
|
|
||||||
reason: "manual_adjustment",
|
|
||||||
})
|
|
||||||
.then(() =>
|
|
||||||
toast.success(t("settlementBills.adjustmentCreated", { defaultValue: "已创建补差单" })),
|
|
||||||
)
|
|
||||||
.then(onUpdated)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t("settlementBills.createAdjustment", { defaultValue: "创建补差单" })}
|
{t("settlementBills.createAdjustment", { defaultValue: "创建补差单" })}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -381,5 +513,6 @@ export function AgentBillDetail({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ export function SettlementCreditLedgerPanel({
|
|||||||
|
|
||||||
const refLabel = (row: SettlementCreditLedgerRow): string => {
|
const refLabel = (row: SettlementCreditLedgerRow): string => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
const ticketLabel = row.ticket_item_id ? `#${row.ticket_item_id}` : null;
|
||||||
|
const billLabel = row.settlement_bill_id ? `#${row.settlement_bill_id}` : null;
|
||||||
if (row.biz_no) {
|
if (row.biz_no) {
|
||||||
parts.push(row.biz_no);
|
parts.push(row.biz_no);
|
||||||
}
|
}
|
||||||
@@ -152,14 +154,57 @@ export function SettlementCreditLedgerPanel({
|
|||||||
if (row.play_code) {
|
if (row.play_code) {
|
||||||
parts.push(row.play_code);
|
parts.push(row.play_code);
|
||||||
}
|
}
|
||||||
if (row.ticket_item_id) {
|
if (ticketLabel) {
|
||||||
parts.push(`#${row.ticket_item_id}`);
|
parts.push(ticketLabel);
|
||||||
}
|
}
|
||||||
if (row.settlement_bill_id) {
|
if (billLabel) {
|
||||||
parts.push(`#${row.settlement_bill_id}`);
|
parts.push(billLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.length > 0 ? parts.join(" · ") : "—";
|
const normalizedParts = parts
|
||||||
|
.map((part) => {
|
||||||
|
let normalized = part;
|
||||||
|
if (billLabel) {
|
||||||
|
normalized = normalized.replace(new RegExp(`\\bbill${billLabel.replace("#", "\\#")}\\b`, "g"), "");
|
||||||
|
}
|
||||||
|
if (ticketLabel) {
|
||||||
|
normalized = normalized.replace(new RegExp(`\\bticket_item${ticketLabel.replace("#", "\\#")}\\b`, "g"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
.split(" · ")
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ");
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const uniqueParts = normalizedParts.filter((part, index) => {
|
||||||
|
if (normalizedParts.indexOf(part) !== index) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (billLabel && part !== billLabel && part.endsWith(billLabel)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ticketLabel && part !== ticketLabel && part.endsWith(ticketLabel)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueParts.length > 0 ? uniqueParts.join(" · ") : "—";
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel = (row: SettlementCreditLedgerRow): string => {
|
||||||
|
if (row.bill_status) {
|
||||||
|
return settlementBillStatusLabel(row.bill_status, t);
|
||||||
|
}
|
||||||
|
if (row.status === "posted") {
|
||||||
|
return t("creditLedger.status.posted", { defaultValue: "已入账" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return row.status || "—";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -306,7 +351,7 @@ export function SettlementCreditLedgerPanel({
|
|||||||
{row.settlement_bill_id ? `#${row.settlement_bill_id}` : "—"}
|
{row.settlement_bill_id ? `#${row.settlement_bill_id}` : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs">
|
||||||
{row.bill_status ? settlementBillStatusLabel(row.bill_status, t) : row.status}
|
{statusLabel(row)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="tabular-nums text-right text-xs">
|
<TableCell className="tabular-nums text-right text-xs">
|
||||||
<span className={cn(signedLedgerAmountClass(signed))}>
|
<span className={cn(signedLedgerAmountClass(signed))}>
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: "view-player",
|
key: "view-player",
|
||||||
label: t("viewDetail", { ns: "players" }),
|
label: t("viewPlayer", { ns: "tickets" }),
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
href: adminPlayerDetailPath(row.player_id),
|
href: adminPlayerDetailPath(row.player_id),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,8 +66,27 @@ export type AdminDashboardAgentOverview = {
|
|||||||
direct_child_count: number;
|
direct_child_count: number;
|
||||||
subtree_agent_count: number;
|
subtree_agent_count: number;
|
||||||
direct_player_count: number;
|
direct_player_count: number;
|
||||||
|
team_player_count: number;
|
||||||
|
active_player_count_today: number;
|
||||||
|
bet_order_count_today: number;
|
||||||
|
today_bet_minor: number;
|
||||||
|
today_payout_minor: number;
|
||||||
|
today_profit_minor: number;
|
||||||
|
seven_day_bet_minor: number;
|
||||||
|
seven_day_payout_minor: number;
|
||||||
|
seven_day_profit_minor: number;
|
||||||
|
currency_code: string | null;
|
||||||
pending_bill_count: number;
|
pending_bill_count: number;
|
||||||
pending_unpaid_minor: number;
|
pending_unpaid_minor: number;
|
||||||
|
latest_bet_at: string | null;
|
||||||
|
top_agent_today: {
|
||||||
|
agent_node_id: number;
|
||||||
|
agent_code: string;
|
||||||
|
agent_name: string;
|
||||||
|
total_bet_minor: number;
|
||||||
|
total_payout_minor: number;
|
||||||
|
approx_house_gross_minor: number;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 全站待审核开奖批次队列(不限于大厅当前期) */
|
/** 全站待审核开奖批次队列(不限于大厅当前期) */
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export type {
|
|||||||
AdminRoleRow,
|
AdminRoleRow,
|
||||||
AdminRoleUpdatePayload,
|
AdminRoleUpdatePayload,
|
||||||
AdminPermissionCatalogData,
|
AdminPermissionCatalogData,
|
||||||
|
AdminUserSiteBinding,
|
||||||
AdminUserPermissionListData,
|
AdminUserPermissionListData,
|
||||||
AdminUserPermissionRow,
|
AdminUserPermissionRow,
|
||||||
AdminUserPermissionSyncData,
|
AdminUserPermissionSyncData,
|
||||||
|
|||||||
Reference in New Issue
Block a user