Refactor agents console and related components
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
- Simplified share rate calculation in AgentsConsole by removing unnecessary checks and directly setting the profile share rate. - Updated the use of `profileParentCaps` to always return total share rate in the agent profile. - Removed unused variables and memoized calculations for improved performance. - Cleaned up imports in various files, removing unused components and optimizing the code structure. - Added `tRef` dependency to several useEffect hooks to ensure proper reactivity to translation changes. - Enhanced report preview tables with better label handling for various statuses and actions. - Updated wallet filter options to align with player-side transaction types. - Introduced new properties in types for better type safety and clarity.
This commit is contained in:
@@ -4,7 +4,7 @@ import { ChevronDown } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AdminPermissionCatalogData } from "@/types/api/admin-user";
|
||||
@@ -18,7 +18,6 @@ type PermissionSelectorProps = {
|
||||
selectableSlugs?: string[] | null;
|
||||
helperText?: string;
|
||||
summaryText?: string;
|
||||
emptyText: string;
|
||||
heightClassName?: string;
|
||||
};
|
||||
|
||||
@@ -31,7 +30,6 @@ export function AdminPermissionSelector({
|
||||
selectableSlugs = null,
|
||||
helperText,
|
||||
summaryText,
|
||||
emptyText,
|
||||
heightClassName = "h-[52vh]",
|
||||
}: PermissionSelectorProps): React.ReactElement {
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
|
||||
@@ -47,7 +47,6 @@ export function PlayerCreditLimitField({
|
||||
max={maxCredit}
|
||||
step={1000}
|
||||
integer
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
{parentAvailableCredit !== null ? (
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
|
||||
@@ -23,5 +23,6 @@ export function useAsyncEffect(
|
||||
return () => {
|
||||
cleanup?.();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
}
|
||||
|
||||
@@ -200,6 +200,8 @@
|
||||
"rebateLimit": "Rebate ceiling (%)",
|
||||
"relativeShareRate": "Share rate (% of parent)",
|
||||
"relativeShareRateValue": "{{rate}}% of parent",
|
||||
"actualShareRate": "Effective share {{rate}}%",
|
||||
"relativeShareCapHint": "Up to 100% of parent (parent total share {{parent}}%)",
|
||||
"riskTags": "Risk tags",
|
||||
"riskTagsPlaceholder": "Comma-separated, e.g. overdue, high_turnover",
|
||||
"saveSuccess": "Share and credit settings saved",
|
||||
|
||||
@@ -273,6 +273,24 @@
|
||||
"play": "Play name or code",
|
||||
"operator": "Admin account or name"
|
||||
},
|
||||
"labels": {
|
||||
"transferDirection": {
|
||||
"in": "Transfer in",
|
||||
"out": "Transfer out"
|
||||
},
|
||||
"transferStatus": {
|
||||
"processing": "Processing",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"pending_reconcile": "Pending reconcile",
|
||||
"reversed": "Reversed",
|
||||
"manually_processed": "Case closed"
|
||||
},
|
||||
"operatorType": {
|
||||
"admin": "Admin",
|
||||
"system": "System"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"draw_profit": {
|
||||
"title": "Draw bets / payout / P&L",
|
||||
|
||||
@@ -146,8 +146,10 @@
|
||||
},
|
||||
"reason": {
|
||||
"payment_record": "Bill payment",
|
||||
"bet": "Bet hold",
|
||||
"bet_hold": "Bet hold",
|
||||
"game_settlement": "Draw settlement",
|
||||
"bill_settlement": "Period payment",
|
||||
"game_settlement_reversal": "Draw reversal",
|
||||
"game_settlement_win": "Draw settlement credit",
|
||||
"bet_hold_release": "Hold release",
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
"transferOutRefund": "Transfer-out refund",
|
||||
"bizBetDeduct": "Bet debit",
|
||||
"bizBetReverse": "Bet reversal",
|
||||
"bizPrize": "Payout",
|
||||
"bizRefund": "Refund",
|
||||
"bizReversal": "Reversal",
|
||||
"bizSettlePayout": "Settlement payout",
|
||||
"bizJackpotPayout": "Jackpot payout",
|
||||
"bizSettlementAdjustment": "Settlement adjustment",
|
||||
|
||||
@@ -93,6 +93,24 @@
|
||||
"wallet_transfer_report": "वालेट ट्रान्सफर",
|
||||
"wallet_txns_daily": "वालेट लेनदेन (दैनिक)"
|
||||
},
|
||||
"labels": {
|
||||
"operatorType": {
|
||||
"admin": "प्रशासक",
|
||||
"system": "प्रणाली"
|
||||
},
|
||||
"transferDirection": {
|
||||
"in": "भित्र",
|
||||
"out": "बाहिर"
|
||||
},
|
||||
"transferStatus": {
|
||||
"failed": "असफल",
|
||||
"manually_processed": "केस बन्द",
|
||||
"pending_reconcile": "मिलान बाँकी",
|
||||
"processing": "प्रक्रियामा",
|
||||
"reversed": "रिभर्स भयो",
|
||||
"success": "सफल"
|
||||
}
|
||||
},
|
||||
"libraryTitle": "रिपोर्ट प्रकार",
|
||||
"loadFailed": "लोड असफल",
|
||||
"no": "होइन",
|
||||
|
||||
@@ -144,8 +144,10 @@
|
||||
},
|
||||
"reason": {
|
||||
"payment_record": "Bill payment",
|
||||
"bet": "Bet hold",
|
||||
"bet_hold": "Bet hold",
|
||||
"game_settlement": "Draw settlement",
|
||||
"bill_settlement": "Period payment",
|
||||
"game_settlement_reversal": "Draw reversal",
|
||||
"game_settlement_win": "Draw settlement credit",
|
||||
"bet_hold_release": "Hold release",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"bizBetDeduct": "बेट कटौती",
|
||||
"bizBetReverse": "बेट उल्टाउने",
|
||||
"bizJackpotPayout": "ज्याकपट भुक्तानी",
|
||||
"bizPrize": "भुक्तानी",
|
||||
"bizRefund": "फिर्ता",
|
||||
"bizReversal": "रिभर्सल",
|
||||
"bizSettlePayout": "बन्दोबस्त भुक्तानी",
|
||||
"bizSettlementAdjustment": "बन्दोबस्त समायोजन",
|
||||
"bizType": "व्यवसाय प्रकार",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"totalShareRate": "占成比例 (%)",
|
||||
"relativeShareRate": "占成比例(占上级 %)",
|
||||
"relativeShareRateValue": "占上级 {{rate}}%",
|
||||
"actualShareRate": "实际占成 {{rate}}%",
|
||||
"relativeShareCapHint": "占上级比例最高 100%(上级总占成 {{parent}}%)",
|
||||
"creditLimit": "授信额度",
|
||||
"rebateLimit": "回水上限 (%)",
|
||||
"defaultPlayerRebate": "默认玩家回水 (%)",
|
||||
|
||||
@@ -273,6 +273,24 @@
|
||||
"play": "玩法名称或编码",
|
||||
"operator": "管理员账号或昵称"
|
||||
},
|
||||
"labels": {
|
||||
"transferDirection": {
|
||||
"in": "转入",
|
||||
"out": "转出"
|
||||
},
|
||||
"transferStatus": {
|
||||
"processing": "处理中",
|
||||
"success": "成功",
|
||||
"failed": "失败",
|
||||
"pending_reconcile": "待对账",
|
||||
"reversed": "已冲正",
|
||||
"manually_processed": "已结案"
|
||||
},
|
||||
"operatorType": {
|
||||
"admin": "管理员",
|
||||
"system": "系统"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"draw_profit": {
|
||||
"title": "每期下注/派彩/盈亏",
|
||||
|
||||
@@ -131,9 +131,11 @@
|
||||
},
|
||||
"reason": {
|
||||
"payment_record": "账单收付",
|
||||
"bet": "下注冻结",
|
||||
"bet_hold": "下注冻结",
|
||||
"freezeAmount": "冻结 {{amount}}",
|
||||
"game_settlement": "开奖结算",
|
||||
"bill_settlement": "账期收付",
|
||||
"game_settlement_reversal": "开奖冲正",
|
||||
"game_settlement_win": "开奖结算入账",
|
||||
"bet_hold_release": "占用释放",
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
"transferOutRefund": "转出失败回补",
|
||||
"bizBetDeduct": "下注扣款",
|
||||
"bizBetReverse": "下注冲正",
|
||||
"bizPrize": "派彩",
|
||||
"bizRefund": "退本",
|
||||
"bizReversal": "冲正",
|
||||
"bizSettlePayout": "派彩入账",
|
||||
"bizJackpotPayout": "奖池派彩",
|
||||
"bizSettlementAdjustment": "结算调账",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { percentValueToUi } from "@/lib/admin-rate-percent";
|
||||
import type { AgentParentCaps } from "@/types/api/admin-agent";
|
||||
|
||||
/** 是否为站点一级代理(根节点)。 */
|
||||
@@ -65,6 +66,29 @@ export function maxCreditLimitFromParent(
|
||||
return baseline + extra;
|
||||
}
|
||||
|
||||
/** 实际占成 → 占上级比例(0–100);无上级或上级占成为 0 时返回 null。 */
|
||||
export function relativeSharePercentFromTotal(
|
||||
totalShareRate: number | null | undefined,
|
||||
parentTotalShareRate: number | null | undefined,
|
||||
): number | null {
|
||||
const total = Number(totalShareRate);
|
||||
const parent = Number(parentTotalShareRate);
|
||||
if (!Number.isFinite(total) || !Number.isFinite(parent) || parent <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (total / parent) * 100;
|
||||
}
|
||||
|
||||
/** 实际占成 → 占上级比例 UI 字符串(如 "50")。 */
|
||||
export function relativeShareRateUiFromTotal(
|
||||
totalShareRate: number | null | undefined,
|
||||
parentTotalShareRate: number | null | undefined,
|
||||
): string | null {
|
||||
const relative = relativeSharePercentFromTotal(totalShareRate, parentTotalShareRate);
|
||||
return relative === null ? null : percentValueToUi(relative);
|
||||
}
|
||||
|
||||
/** 相对占成(占上级 %)换算后的实际占成,不得超过上级总占成。 */
|
||||
export function actualShareRateFromRelative(
|
||||
relativePercent: number,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
CalendarClock,
|
||||
CircleDollarSign,
|
||||
ClipboardList,
|
||||
Coins,
|
||||
FileSpreadsheet,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Pencil, Plus, Trash2, Network } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AdminSubnav, AdminSubnavButton } from "@/components/admin/admin-subnav";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminLoadingInline } from "@/components/admin/admin-loading-state";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||
@@ -21,6 +21,7 @@ import { AgentProfileFields, type AgentProfileFieldsProps } from "@/modules/agen
|
||||
import { formatCredit } from "@/modules/agents/agent-line-sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { percentValueToUi } from "@/lib/admin-rate-percent";
|
||||
import { relativeShareRateUiFromTotal } from "@/lib/agent-profile-caps";
|
||||
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
|
||||
import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money";
|
||||
import type { AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent";
|
||||
@@ -41,7 +42,6 @@ export type AgentLineDetailPanelProps = {
|
||||
detailTab: AgentDetailTab;
|
||||
onDetailTabChange: (tab: AgentDetailTab) => void;
|
||||
canViewProfileTab: boolean;
|
||||
canEditProfileTab: boolean;
|
||||
canSaveProfileTab: boolean;
|
||||
canViewDownlineTab: boolean;
|
||||
canViewPlayersTab: boolean;
|
||||
@@ -73,7 +73,6 @@ export function AgentLineDetailPanel({
|
||||
detailTab,
|
||||
onDetailTabChange,
|
||||
canViewProfileTab,
|
||||
canEditProfileTab,
|
||||
canSaveProfileTab,
|
||||
canViewDownlineTab,
|
||||
canViewPlayersTab,
|
||||
@@ -149,7 +148,7 @@ export function AgentLineDetailPanel({
|
||||
{metaParts.length > 0 ? (
|
||||
<p className="truncate text-sm text-muted-foreground">{metaParts.join(" · ")}</p>
|
||||
) : null}
|
||||
<AgentHeaderMetrics profile={profile} loading={profileLoading} />
|
||||
<AgentHeaderMetrics node={node} profile={profile} loading={profileLoading} />
|
||||
</div>
|
||||
{canManageNode ? (
|
||||
<Button type="button" size="sm" variant="outline" onClick={onEditCurrent}>
|
||||
@@ -213,6 +212,9 @@ export function AgentLineDetailPanel({
|
||||
<DownlineSection
|
||||
childAgents={childAgents}
|
||||
childCountById={childCountById}
|
||||
parentTotalShareRate={
|
||||
profile?.total_share_rate ?? node.profile_summary?.total_share_rate ?? null
|
||||
}
|
||||
canManageNode={canManageNode}
|
||||
canCreateChild={canCreateChild}
|
||||
canDeleteChild={canDeleteChild}
|
||||
@@ -238,9 +240,11 @@ export function AgentLineDetailPanel({
|
||||
}
|
||||
|
||||
function AgentHeaderMetrics({
|
||||
node,
|
||||
profile,
|
||||
loading,
|
||||
}: {
|
||||
node: AgentNodeRow;
|
||||
profile: AgentProfileRow | null;
|
||||
loading: boolean;
|
||||
}): React.ReactElement {
|
||||
@@ -250,8 +254,22 @@ function AgentHeaderMetrics({
|
||||
return <p className="text-sm text-muted-foreground">…</p>;
|
||||
}
|
||||
|
||||
const totalShare = profile?.total_share_rate ?? node.profile_summary?.total_share_rate ?? 0;
|
||||
const parentShare = profile?.parent_caps?.total_share_rate;
|
||||
const relativeShareUi = relativeShareRateUiFromTotal(totalShare, parentShare);
|
||||
const shareMetric =
|
||||
relativeShareUi !== null
|
||||
? `${t("profile.relativeShareRateValue", {
|
||||
defaultValue: "占上级 {{rate}}%",
|
||||
rate: relativeShareUi,
|
||||
})}(${t("profile.actualShareRate", {
|
||||
defaultValue: "实际占成 {{rate}}%",
|
||||
rate: percentValueToUi(totalShare),
|
||||
})})`
|
||||
: `${t("profile.totalShareRate", { defaultValue: "占成" })} ${percentValueToUi(totalShare)}%`;
|
||||
|
||||
const items = [
|
||||
`${t("profile.totalShareRate", { defaultValue: "占成" })} ${profile?.total_share_rate ?? 0}%`,
|
||||
shareMetric,
|
||||
`${t("profile.creditLimit", { defaultValue: "授信" })} ${formatCredit(profile?.credit_limit ?? 0)}`,
|
||||
`${t("lineUi.availableCredit", { defaultValue: "可下发" })} ${formatCredit(profile?.available_credit ?? 0)}`,
|
||||
`${t("profile.rebateLimit", { defaultValue: "回水上限" })} ${percentValueToUi(profile?.rebate_limit ?? 0)}%`,
|
||||
@@ -267,6 +285,7 @@ function AgentHeaderMetrics({
|
||||
function DownlineSection({
|
||||
childAgents,
|
||||
childCountById,
|
||||
parentTotalShareRate,
|
||||
canManageNode,
|
||||
canCreateChild,
|
||||
canDeleteChild,
|
||||
@@ -277,6 +296,7 @@ function DownlineSection({
|
||||
}: {
|
||||
childAgents: AgentNodeRow[];
|
||||
childCountById: Map<number, number>;
|
||||
parentTotalShareRate: number | null;
|
||||
canManageNode: boolean;
|
||||
canCreateChild: boolean;
|
||||
canDeleteChild: (node: AgentNodeRow) => boolean;
|
||||
@@ -313,6 +333,7 @@ function DownlineSection({
|
||||
<DownlineTable
|
||||
childAgents={childAgents}
|
||||
childCountById={childCountById}
|
||||
parentTotalShareRate={parentTotalShareRate}
|
||||
canManageNode={canManageNode}
|
||||
onEditChild={onEditChild}
|
||||
onDeleteChild={onDeleteChild}
|
||||
@@ -326,6 +347,7 @@ function DownlineSection({
|
||||
function DownlineTable({
|
||||
childAgents,
|
||||
childCountById,
|
||||
parentTotalShareRate,
|
||||
canManageNode,
|
||||
canDeleteChild,
|
||||
onEditChild,
|
||||
@@ -334,6 +356,7 @@ function DownlineTable({
|
||||
}: {
|
||||
childAgents: AgentNodeRow[];
|
||||
childCountById: Map<number, number>;
|
||||
parentTotalShareRate: number | null;
|
||||
canManageNode: boolean;
|
||||
canDeleteChild: (node: AgentNodeRow) => boolean;
|
||||
onEditChild: (node: AgentNodeRow) => void;
|
||||
@@ -341,6 +364,8 @@ function DownlineTable({
|
||||
onSelectChild: (node: AgentNodeRow) => void;
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation(["agents", "common"]);
|
||||
const showRelativeShare =
|
||||
parentTotalShareRate !== null && Number(parentTotalShareRate) > 0;
|
||||
|
||||
return (
|
||||
<div className="admin-table-inset overflow-hidden">
|
||||
@@ -351,7 +376,9 @@ function DownlineTable({
|
||||
<TableHead>{t("agentName", { defaultValue: "名称" })}</TableHead>
|
||||
<TableHead>{t("loginUsername", { defaultValue: "登录名" })}</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap">
|
||||
{t("profile.totalShareRate", { defaultValue: "占成" })}
|
||||
{showRelativeShare
|
||||
? t("profile.totalShareWithRelative", { defaultValue: "实际占成 / 占上级" })
|
||||
: t("profile.totalShareRate", { defaultValue: "占成" })}
|
||||
</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap">
|
||||
{t("profile.creditLimit", { defaultValue: "授信" })}
|
||||
@@ -384,8 +411,15 @@ function DownlineTable({
|
||||
<div className="font-mono text-xs text-muted-foreground">{child.code}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{child.username ?? "—"}</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-sm">
|
||||
{summary ? `${summary.total_share_rate ?? 0}%` : "—"}
|
||||
<TableCell className="text-right text-sm">
|
||||
{summary ? (
|
||||
<DownlineShareCell
|
||||
totalShareRate={summary.total_share_rate}
|
||||
parentTotalShareRate={parentTotalShareRate}
|
||||
/>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={adminMoneyCellClassName("text-right text-sm")}>
|
||||
{summary ? <AdminTableMoney>{formatCredit(summary.credit_limit)}</AdminTableMoney> : "—"}
|
||||
@@ -440,4 +474,31 @@ function DownlineTable({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function DownlineShareCell({
|
||||
totalShareRate,
|
||||
parentTotalShareRate,
|
||||
}: {
|
||||
totalShareRate: number;
|
||||
parentTotalShareRate: number | null;
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation("agents");
|
||||
const relativeShareUi = relativeShareRateUiFromTotal(totalShareRate, parentTotalShareRate);
|
||||
|
||||
if (relativeShareUi !== null) {
|
||||
return (
|
||||
<div className="tabular-nums">
|
||||
<div>{percentValueToUi(totalShareRate)}%</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("profile.relativeShareRateValue", {
|
||||
defaultValue: "占上级 {{rate}}%",
|
||||
rate: relativeShareUi,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="tabular-nums">{percentValueToUi(totalShareRate)}%</div>;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,32 @@ type AgentLineProvisionWizardProps = {
|
||||
onSuccess?: (result: AdminAgentLineProvisionResult) => void | Promise<void>;
|
||||
};
|
||||
|
||||
function clampPercentInput(value: string): string {
|
||||
if (value.trim() === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parsed = Number.parseFloat(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
return String(Math.min(100, Math.max(0, parsed)));
|
||||
}
|
||||
|
||||
function clampIntegerInput(value: string): string {
|
||||
if (value.trim() === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
return String(Math.max(0, parsed));
|
||||
}
|
||||
|
||||
export function AgentLineProvisionWizard({
|
||||
embedded = false,
|
||||
defaultSiteCode,
|
||||
@@ -178,7 +204,7 @@ export function AgentLineProvisionWizard({
|
||||
can_grant_extra_rebate: form.can_grant_extra_rebate,
|
||||
});
|
||||
toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" }));
|
||||
setForm((f) => ({
|
||||
setForm(() => ({
|
||||
site_code: "",
|
||||
name: "",
|
||||
username: "",
|
||||
@@ -307,6 +333,9 @@ export function AgentLineProvisionWizard({
|
||||
step="0.01"
|
||||
value={form.total_share_rate}
|
||||
onChange={(e) => setForm((f) => ({ ...f, total_share_rate: e.target.value }))}
|
||||
onBlur={(e) =>
|
||||
setForm((f) => ({ ...f, total_share_rate: clampPercentInput(e.target.value) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
@@ -316,6 +345,9 @@ export function AgentLineProvisionWizard({
|
||||
min={0}
|
||||
value={form.credit_limit}
|
||||
onChange={(e) => setForm((f) => ({ ...f, credit_limit: e.target.value }))}
|
||||
onBlur={(e) =>
|
||||
setForm((f) => ({ ...f, credit_limit: clampIntegerInput(e.target.value) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
@@ -328,6 +360,9 @@ export function AgentLineProvisionWizard({
|
||||
value={form.rebate_limit}
|
||||
placeholder="50"
|
||||
onChange={(e) => setForm((f) => ({ ...f, rebate_limit: e.target.value }))}
|
||||
onBlur={(e) =>
|
||||
setForm((f) => ({ ...f, rebate_limit: clampPercentInput(e.target.value) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
@@ -340,6 +375,12 @@ export function AgentLineProvisionWizard({
|
||||
value={form.default_player_rebate}
|
||||
placeholder="50"
|
||||
onChange={(e) => setForm((f) => ({ ...f, default_player_rebate: e.target.value }))}
|
||||
onBlur={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
default_player_rebate: clampPercentInput(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChevronRight, Search } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminLoadingInline } from "@/components/admin/admin-loading-state";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -88,13 +88,11 @@ function collectSearchExpandIds(
|
||||
}
|
||||
|
||||
export type AgentLineSidebarProps = {
|
||||
siteLabel: string | null;
|
||||
/** API 返回的嵌套树(含 children) */
|
||||
tree: AgentNodeRow[];
|
||||
parentNameMap: Map<number, string>;
|
||||
selectedId: number | null;
|
||||
keyword: string;
|
||||
agentCount: number;
|
||||
loading?: boolean;
|
||||
onKeywordChange: (value: string) => void;
|
||||
onSelect: (node: AgentNodeRow) => void;
|
||||
@@ -187,12 +185,10 @@ function TreeRow({
|
||||
}
|
||||
|
||||
export function AgentLineSidebar({
|
||||
siteLabel,
|
||||
tree,
|
||||
parentNameMap,
|
||||
selectedId,
|
||||
keyword,
|
||||
agentCount,
|
||||
loading = false,
|
||||
onKeywordChange,
|
||||
onSelect,
|
||||
|
||||
@@ -40,7 +40,6 @@ export function isAgentLineSubnavTabVisible(
|
||||
): boolean {
|
||||
const perms = session?.permissions ?? [];
|
||||
const isSuper = session?.is_super_admin === true;
|
||||
const boundAgent = session?.agent ?? null;
|
||||
|
||||
if (isSuper) {
|
||||
return adminHasAnyPermission(perms, tabRequiredSlugs(href));
|
||||
|
||||
@@ -4,13 +4,6 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { formatAdminCreditMajorDecimal } from "@/lib/money";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -23,9 +16,7 @@ import { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import {
|
||||
AGENT_PERCENT_HARD_MAX,
|
||||
actualShareRateFromRelative,
|
||||
creditLimitRangeIssue,
|
||||
isNumericStepperOutOfRange,
|
||||
maxCreditLimitFromParent,
|
||||
maxDefaultRebatePercent,
|
||||
maxRebatePercentFromParent,
|
||||
@@ -105,7 +96,12 @@ export function AgentProfileFields({
|
||||
const maxRebatePercent = maxRebatePercentFromParent(parentCaps);
|
||||
const maxDefaultRebate = maxDefaultRebatePercent(rebateLimit, parentCaps);
|
||||
const maxCreditLimit = maxCreditLimitFromParent(parentCaps, baselineCreditLimit);
|
||||
const actualShare = actualShareRateFromRelative(Number.parseFloat(shareRate) || 0, parentCaps);
|
||||
const shareRateNumber = Number.parseFloat(shareRate) || 0;
|
||||
const parentShareRate = parentCaps !== null ? Number(parentCaps.total_share_rate) : null;
|
||||
const relativeShare =
|
||||
parentShareRate !== null && Number.isFinite(parentShareRate) && parentShareRate > 0
|
||||
? Number(((shareRateNumber / parentShareRate) * 100).toFixed(2))
|
||||
: null;
|
||||
const creditRangeIssue = creditLimitRangeIssue(creditLimit, {
|
||||
min: minCreditLimit,
|
||||
max: maxCreditLimit,
|
||||
@@ -176,9 +172,7 @@ export function AgentProfileFields({
|
||||
htmlFor={`${idPrefix}-share-rate`}
|
||||
className={showReadOnlyDisplay ? "text-foreground/85" : "text-muted-foreground"}
|
||||
>
|
||||
{parentCaps
|
||||
? t("profile.relativeShareRate", { defaultValue: "占成比例(占上级 %)" })
|
||||
: t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
||||
{t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
||||
</Label>
|
||||
{showReadOnlyDisplay ? (
|
||||
<ReadOnlyScalar id={`${idPrefix}-share-rate`} value={shareRate} suffix="%" flat={isCard} />
|
||||
@@ -191,13 +185,12 @@ export function AgentProfileFields({
|
||||
max={maxSharePercent}
|
||||
step={0.5}
|
||||
suffix="%"
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
)}
|
||||
{showFieldHints && parentCaps ? (
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
{t("profile.relativeShareCapHint", {
|
||||
defaultValue: "占上级比例最高 100%(上级总占成 {{parent}}%)",
|
||||
{t("profile.totalShareParentCapHint", {
|
||||
defaultValue: "实际占成不可超过上级总占成 {{parent}}%",
|
||||
parent: parentCaps.total_share_rate,
|
||||
})}
|
||||
</p>
|
||||
@@ -207,11 +200,11 @@ export function AgentProfileFields({
|
||||
{t("profile.totalShareCapHint", { defaultValue: "占成比例最高 100%" })}
|
||||
</p>
|
||||
) : null}
|
||||
{showFieldHints && parentCaps && shareRate && actualShare !== null ? (
|
||||
{parentCaps && shareRate && relativeShare !== null ? (
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
{t("profile.actualShareRate", {
|
||||
defaultValue: "实际占成 {{rate}}%",
|
||||
rate: actualShare,
|
||||
{t("profile.relativeShareRateValue", {
|
||||
defaultValue: "占上级 {{rate}}%",
|
||||
rate: relativeShare,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -234,7 +227,6 @@ export function AgentProfileFields({
|
||||
max={maxCreditLimit}
|
||||
step={1000}
|
||||
integer
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
)}
|
||||
{showFieldHints && parentCaps && maxCreditLimit !== undefined && profileScalarsEditable ? (
|
||||
@@ -289,7 +281,6 @@ export function AgentProfileFields({
|
||||
max={maxRebatePercent}
|
||||
step={0.5}
|
||||
suffix="%"
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
)}
|
||||
{parentCaps ? (
|
||||
@@ -323,7 +314,6 @@ export function AgentProfileFields({
|
||||
max={maxDefaultRebate}
|
||||
step={0.5}
|
||||
suffix="%"
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
|
||||
@@ -182,13 +182,7 @@ export function AgentsConsole(): React.ReactElement {
|
||||
};
|
||||
|
||||
const applyProfileRowToForm = (row: AgentProfileRow) => {
|
||||
const parentRate = row.parent_caps?.total_share_rate ?? 0;
|
||||
if (parentRate > 0 && row.total_share_rate != null) {
|
||||
const relative = (row.total_share_rate / parentRate) * 100;
|
||||
setProfileShareRate(percentValueToUi(relative));
|
||||
} else {
|
||||
setProfileShareRate(percentValueToUi(row.total_share_rate ?? 0));
|
||||
}
|
||||
setProfileShareRate(percentValueToUi(row.total_share_rate ?? 0));
|
||||
setProfileCreditLimit(String(row.credit_limit ?? 0));
|
||||
setProfileRebateLimit(percentValueToUi(row.rebate_limit ?? 0));
|
||||
setProfileDefaultRebate(percentValueToUi(row.default_player_rebate ?? 0));
|
||||
@@ -216,9 +210,6 @@ export function AgentsConsole(): React.ReactElement {
|
||||
can_create_player: profileCanCreatePlayer,
|
||||
risk_tags: parseRiskTagsInput(profileRiskTags),
|
||||
};
|
||||
if (profileParentCaps) {
|
||||
return { ...base, relative_share_rate: shareRate };
|
||||
}
|
||||
return { ...base, total_share_rate: shareRate };
|
||||
};
|
||||
|
||||
@@ -279,7 +270,7 @@ export function AgentsConsole(): React.ReactElement {
|
||||
parentCaps: profileParentCaps,
|
||||
baselineCreditLimit: profileBaselineCreditLimit,
|
||||
minCreditLimit: profileMinCreditLimit,
|
||||
usesRelativeShare: profileParentCaps !== null,
|
||||
usesRelativeShare: false,
|
||||
validateCredit: isRootProfileEditableByActor(creditNode, isSuperAdmin),
|
||||
});
|
||||
|
||||
@@ -297,16 +288,6 @@ export function AgentsConsole(): React.ReactElement {
|
||||
);
|
||||
const visibleAgentRows = flatNodes;
|
||||
const boundSite = profile?.site ?? null;
|
||||
const selectedSiteLabel = useMemo(() => {
|
||||
const fromOptions = siteOptions.find((site) => site.id === adminSiteId)?.name;
|
||||
if (fromOptions) {
|
||||
return fromOptions;
|
||||
}
|
||||
if (boundSite != null && boundSite.id === adminSiteId) {
|
||||
return boundSite.name;
|
||||
}
|
||||
return null;
|
||||
}, [adminSiteId, boundSite, siteOptions]);
|
||||
const rootNode = useMemo(
|
||||
() => flatNodes.find((node) => node.is_root || node.depth === 0) ?? null,
|
||||
[flatNodes],
|
||||
@@ -1090,12 +1071,10 @@ export function AgentsConsole(): React.ReactElement {
|
||||
<div className="flex max-h-[calc(100dvh-14rem)] min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border/70 bg-card shadow-sm lg:flex-row">
|
||||
{showAgentSidebar ? (
|
||||
<AgentLineSidebar
|
||||
siteLabel={selectedSiteLabel}
|
||||
tree={tree}
|
||||
parentNameMap={parentNameMap}
|
||||
selectedId={selectedNodeId}
|
||||
keyword={keyword}
|
||||
agentCount={visibleAgentRows.length}
|
||||
loading={loading && visibleAgentRows.length === 0}
|
||||
onKeywordChange={(value) => {
|
||||
setKeyword(value);
|
||||
@@ -1124,7 +1103,6 @@ export function AgentsConsole(): React.ReactElement {
|
||||
detailTab={detailTab}
|
||||
onDetailTabChange={setDetailTab}
|
||||
canViewProfileTab={canViewProfileTab}
|
||||
canEditProfileTab={canEditSelectedProfile && !isOwnAgentNode}
|
||||
canSaveProfileTab={canManageProfile && canEditSelectedProfile && !isOwnAgentNode}
|
||||
canViewDownlineTab={canShowDownlineTab}
|
||||
canViewPlayersTab={canShowPlayersTab}
|
||||
|
||||
@@ -949,7 +949,6 @@ export function AgentsPlayersPanel({
|
||||
max={maxPlayerRebatePercent}
|
||||
step={0.5}
|
||||
suffix="%"
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
{agentRebateLimitPercent !== null
|
||||
@@ -1036,7 +1035,6 @@ export function AgentsPlayersPanel({
|
||||
max={maxPlayerRebatePercent}
|
||||
step={0.5}
|
||||
suffix="%"
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
{agentRebateLimitPercent !== null
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AdminLoadingState, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -105,7 +105,7 @@ export function AuditLogsConsole(): React.ReactElement {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, perPage, appliedOperatorId, appliedModule, appliedOpType, appliedStartDate, appliedEndDate]);
|
||||
}, [page, perPage, appliedOperatorId, appliedModule, appliedOpType, appliedStartDate, appliedEndDate, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import { ratioToPercentUi } from "@/lib/admin-rate-percent";
|
||||
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||
@@ -94,8 +93,6 @@ type OddsConfigDocScreenProps = {
|
||||
embedded?: boolean;
|
||||
/** 合并页:左侧三步骤 + 右侧配置摘要(参考设计稿) */
|
||||
mergedLayout?: boolean;
|
||||
/** 合并页第 3 步:佣金 / 回水 */
|
||||
rebateSection?: ReactNode;
|
||||
/** 合并页共享数据层(避免与回水区块重复拉取版本详情) */
|
||||
workspace?: OddsConfigWorkspace;
|
||||
/** 与回水分区共用版本选择(无 workspace 时) */
|
||||
@@ -106,7 +103,6 @@ type OddsConfigDocScreenProps = {
|
||||
export function OddsConfigDocScreen({
|
||||
embedded = false,
|
||||
mergedLayout = false,
|
||||
rebateSection,
|
||||
workspace,
|
||||
versionId: controlledVersionId,
|
||||
onVersionIdChange,
|
||||
@@ -159,7 +155,7 @@ export function OddsConfigDocScreen({
|
||||
} finally {
|
||||
setLoadingTypes(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
const refreshList = useCallback(async () => {
|
||||
setLoadingList(true);
|
||||
@@ -175,7 +171,7 @@ export function OddsConfigDocScreen({
|
||||
} finally {
|
||||
setLoadingList(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
if (workspace) {
|
||||
@@ -199,7 +195,7 @@ export function OddsConfigDocScreen({
|
||||
} finally {
|
||||
setLoadingDetail(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
@@ -492,7 +488,7 @@ export function OddsConfigDocScreen({
|
||||
newValue: next?.odds_value ?? null,
|
||||
};
|
||||
});
|
||||
}, [activeCompareRows, resolvedDetail, resolvedDraftRows, resolvedPlayCode, t, i18n.language]);
|
||||
}, [activeCompareRows, resolvedDetail, resolvedDraftRows, resolvedPlayCode, t]);
|
||||
|
||||
const catTabs: { id: CatTab; label: string }[] = [
|
||||
{ id: "all", label: t("odds.tabs.all", { ns: "config" }) },
|
||||
|
||||
@@ -181,7 +181,7 @@ export function PlayConfigDocScreen() {
|
||||
} finally {
|
||||
setLoadingList(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void refreshList();
|
||||
@@ -214,7 +214,7 @@ export function PlayConfigDocScreen() {
|
||||
setLoadingDetail(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (list.length === 0) {
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
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 { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||
@@ -133,7 +133,6 @@ export function RebateConfigDocScreen({
|
||||
const resolvedList = workspace?.list ?? listRows;
|
||||
const resolvedDetail = workspace?.detail ?? detail;
|
||||
const resolvedDraftRows = workspace?.draftRows ?? draftRows;
|
||||
const setResolvedDraftRows = workspace?.setDraftRows ?? setDraftRows;
|
||||
const resolvedLoading = workspace ? workspace.loadingList || workspace.loadingTypes : loading;
|
||||
const resolvedLoadingDetail = workspace?.loadingDetail ?? loadingDetail;
|
||||
|
||||
@@ -152,7 +151,7 @@ export function RebateConfigDocScreen({
|
||||
);
|
||||
setTypes([]);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
const refreshList = useCallback(async () => {
|
||||
try {
|
||||
@@ -164,7 +163,7 @@ export function RebateConfigDocScreen({
|
||||
);
|
||||
setListRows([]);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
if (workspace) {
|
||||
@@ -239,7 +238,7 @@ export function RebateConfigDocScreen({
|
||||
} finally {
|
||||
setLoadingDetail(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
|
||||
@@ -70,7 +70,7 @@ export function useOddsConfigWorkspace(
|
||||
} finally {
|
||||
setLoadingTypes(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
const refreshList = useCallback(async () => {
|
||||
setLoadingList(true);
|
||||
@@ -85,7 +85,7 @@ export function useOddsConfigWorkspace(
|
||||
} finally {
|
||||
setLoadingList(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
const loadDetail = useCallback(
|
||||
async (id: number) => {
|
||||
@@ -110,7 +110,7 @@ export function useOddsConfigWorkspace(
|
||||
}
|
||||
}
|
||||
},
|
||||
[applyDetail],
|
||||
[applyDetail, tRef],
|
||||
);
|
||||
|
||||
const reloadDetail = useCallback(async () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { BarChart3, Gift, TrendingUp, Wallet } from "lucide-react";
|
||||
|
||||
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -82,7 +82,6 @@ export function DashboardAnalyticsMain({ analytics }: { analytics: DashboardAnal
|
||||
playOptions,
|
||||
sparklines,
|
||||
formatMoney,
|
||||
formatSignedMoney,
|
||||
profitScope,
|
||||
} = analytics;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react";
|
||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertTriangle, BarChart3, ClipboardList, RefreshCw, Shield, TrendingUp, Wallet } from "lucide-react";
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
formatDashboardMoneyMinor,
|
||||
} from "@/modules/dashboard/use-dashboard-analytics";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -241,7 +241,7 @@ export function DashboardConsole(): ReactElement {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
}, [tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load(false);
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
|
||||
@@ -132,7 +132,7 @@ export function useDashboardAnalytics({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [enabled, period, playCode, customFrom, customTo, scope.agentNodeId, scope.siteCode]);
|
||||
}, [enabled, period, playCode, customFrom, customTo, scope.agentNodeId, scope.siteCode, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getAdminDraw } from "@/api/admin-draws";
|
||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||
import { getAdminDrawResultBatches } from "@/api/admin-draws";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -47,7 +47,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idNum]);
|
||||
}, [idNum, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminLoadingState, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
||||
@@ -85,7 +85,7 @@ export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
|
||||
|
||||
return true;
|
||||
}),
|
||||
[canManageDraw, canViewDraw, canViewRisk],
|
||||
[canManageDraw, canViewDraw, canViewFinance, canViewRisk],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||
|
||||
import { getAdminJackpotContributions, getAdminJackpotPayoutLogs } from "@/api/admin-jackpot";
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminPlayerIdentityCells, AdminPlayerIdentityHeads } from "@/components/admin/admin-player-identity-columns";
|
||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||
import { AdminSubnav, AdminSubnavButton } from "@/components/admin/admin-subnav";
|
||||
@@ -17,7 +17,7 @@ import { Button } from "@/components/ui/button";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -66,8 +66,6 @@ function JackpotRecordTableSection({
|
||||
children: React.ReactNode;
|
||||
footer: React.ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<div className="admin-table-shell">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border/70 bg-muted/20 px-4 py-3">
|
||||
@@ -126,7 +124,7 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
|
||||
} finally {
|
||||
setLoadingP(false);
|
||||
}
|
||||
}, [pPage, pPer, appliedDrawNo]);
|
||||
}, [pPage, pPer, appliedDrawNo, tRef]);
|
||||
|
||||
const loadContribs = useCallback(async () => {
|
||||
setLoadingC(true);
|
||||
@@ -142,7 +140,7 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
|
||||
} finally {
|
||||
setLoadingC(false);
|
||||
}
|
||||
}, [cPage, cPer, appliedDrawNo]);
|
||||
}, [cPage, cPer, appliedDrawNo, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void loadPayouts();
|
||||
|
||||
@@ -146,7 +146,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
} finally {
|
||||
setPlayerLoading(false);
|
||||
}
|
||||
}, [playerId]);
|
||||
}, [playerId, tRef]);
|
||||
|
||||
const loadTickets = useCallback(async () => {
|
||||
setTicketsLoading(true);
|
||||
|
||||
@@ -227,7 +227,7 @@ export function PlayersConsole(): React.ReactElement {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, perPage, query, siteFilter]);
|
||||
}, [page, perPage, query, siteFilter, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -9,11 +9,11 @@ import { Download, RefreshCw } from "lucide-react";
|
||||
|
||||
import { downloadAdminReportJob, getAdminReportJobs } from "@/api/admin-report-jobs";
|
||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
||||
@@ -7,7 +7,8 @@ import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admi
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
|
||||
import { drawStatusLabel } from "@/modules/draws/draw-display";
|
||||
import { drawStatusLabel, settlementBatchStatusLabel } from "@/modules/draws/draw-display";
|
||||
import { riskActionTypeLabel } from "@/modules/risk/risk-display";
|
||||
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import {
|
||||
@@ -36,6 +37,18 @@ type ReportPreviewTableProps = {
|
||||
displayCurrency: string;
|
||||
};
|
||||
|
||||
function reportValueLabel(
|
||||
scope: string,
|
||||
value: string | null | undefined,
|
||||
t: (key: string, options?: { defaultValue?: string }) => string,
|
||||
): string {
|
||||
if (value == null || value === "") {
|
||||
return "—";
|
||||
}
|
||||
|
||||
return t(`labels.${scope}.${value}`, { defaultValue: value });
|
||||
}
|
||||
|
||||
export function ReportPreviewTable({
|
||||
report,
|
||||
result,
|
||||
@@ -135,7 +148,9 @@ export function ReportPreviewTable({
|
||||
<TableRow key={batch.id} className="bg-muted/10">
|
||||
<TableCell>#{batch.id}</TableCell>
|
||||
<TableCell>
|
||||
<AdminStatusBadge status={batch.status}>{batch.status}</AdminStatusBadge>
|
||||
<AdminStatusBadge status={batch.status}>
|
||||
{settlementBatchStatusLabel(batch.status, t)}
|
||||
</AdminStatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{batch.total_ticket_count}</TableCell>
|
||||
<TableCell className="text-right">{batch.total_win_count}</TableCell>
|
||||
@@ -269,9 +284,11 @@ export function ReportPreviewTable({
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-mono text-xs">{item.transfer_no}</TableCell>
|
||||
<TableCell>{optionText(item.username, item.nickname) || item.player_id}</TableCell>
|
||||
<TableCell>{item.direction}</TableCell>
|
||||
<TableCell>{reportValueLabel("transferDirection", item.direction, t)}</TableCell>
|
||||
<TableCell>
|
||||
<AdminStatusBadge status={item.status}>{item.status}</AdminStatusBadge>
|
||||
<AdminStatusBadge status={item.status}>
|
||||
{reportValueLabel("transferStatus", item.status, t)}
|
||||
</AdminStatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{item.currency_code} {item.amount}
|
||||
@@ -333,7 +350,7 @@ export function ReportPreviewTable({
|
||||
{result.raw.logs.items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-mono text-xs">#{item.id}</TableCell>
|
||||
<TableCell>{item.action_type}</TableCell>
|
||||
<TableCell>{riskActionTypeLabel(item.action_type, t)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.amount, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
@@ -402,11 +419,11 @@ export function ReportPreviewTable({
|
||||
{result.raw.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-mono text-xs">#{item.id}</TableCell>
|
||||
<TableCell>{item.operator_type}</TableCell>
|
||||
<TableCell>{reportValueLabel("operatorType", item.operator_type, t)}</TableCell>
|
||||
<TableCell>{item.operator_id}</TableCell>
|
||||
<TableCell>{item.module_code}</TableCell>
|
||||
<TableCell>{item.action_code}</TableCell>
|
||||
<TableCell>{item.target_type || "—"}</TableCell>
|
||||
<TableCell>{item.module_label || item.module_code}</TableCell>
|
||||
<TableCell>{item.action_label || item.action_code}</TableCell>
|
||||
<TableCell>{item.target_label || item.target_type || "—"}</TableCell>
|
||||
<TableCell>{formatTs(item.created_at)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -417,4 +434,4 @@ export function ReportPreviewTable({
|
||||
default:
|
||||
return <AdminLoadingInline />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ import {
|
||||
drawRowsFromSummary,
|
||||
emptyFilters,
|
||||
emptySearch,
|
||||
formatExportInstant,
|
||||
formatPlainMoney,
|
||||
formatUsagePercent,
|
||||
headlineStats,
|
||||
@@ -434,6 +433,7 @@ export function ReportsConsole({ initialCategory = "profit" }: ReportsConsolePro
|
||||
return;
|
||||
}
|
||||
void queryReport();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, perPage]);
|
||||
|
||||
function updateFilter<K extends keyof ReportFilters>(key: K, value: ReportFilters[K]): void {
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
AdminReportPlayerWinLossRow,
|
||||
} from "@/types/api/admin-reports";
|
||||
|
||||
import type { FieldKey, ReportDefinition, ReportKey } from "@/modules/reports/reports-definitions";
|
||||
import type { ReportDefinition, ReportKey } from "@/modules/reports/reports-definitions";
|
||||
|
||||
export type ExportCell = string | number | null;
|
||||
export type ExportRow = Record<string, ExportCell>;
|
||||
@@ -128,7 +128,8 @@ export function formatPlainMoney(value: number, currencyCode: string | null | un
|
||||
return formatAdminMinorUnits(value, currencyCode || "NPR");
|
||||
}
|
||||
|
||||
export function signedProfitCell(amount: number, currencyCode: string | null | undefined): string {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function signedProfitCell(amount: number, _currencyCode: string | null | undefined): string {
|
||||
return cn("tabular-nums", signedMoneyClass(amount, true));
|
||||
}
|
||||
|
||||
@@ -238,13 +239,9 @@ export function buildDailyProfitRowsAndSummary(
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
currencyCode: string,
|
||||
): Pick<Extract<ReportResult, { key: "daily_profit" }>, "rows" | "summary"> {
|
||||
let totalBet = 0;
|
||||
let totalPayout = 0;
|
||||
let totalGross = 0;
|
||||
|
||||
const rows = items.map((item) => {
|
||||
totalBet += item.total_bet_minor;
|
||||
totalPayout += item.total_payout_minor;
|
||||
totalGross += item.approx_house_gross_minor;
|
||||
return {
|
||||
business_date: item.business_date,
|
||||
@@ -306,11 +303,9 @@ export function buildPlayDimensionRowsAndSummary(
|
||||
t: (key: string) => string,
|
||||
currencyCode: string,
|
||||
): Pick<Extract<ReportResult, { key: "play_dimension" }>, "rows" | "summary"> {
|
||||
let totalBet = 0;
|
||||
let totalGross = 0;
|
||||
|
||||
const rows = items.map((item) => {
|
||||
totalBet += item.total_bet_minor;
|
||||
totalGross += item.approx_house_gross_minor;
|
||||
return {
|
||||
play_code: item.play_code,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AdminListPaginationFooter } from "@/components/admin/admin-list-paginat
|
||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -63,7 +63,7 @@ export function RiskPoolDetailConsole({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [drawId, number4d, page, perPage]);
|
||||
}, [drawId, number4d, page, perPage, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -102,7 +102,7 @@ export function CurrencySettingsPanel() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canManage]);
|
||||
}, [canManage, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { SignedMoney } from "@/lib/admin-signed-money";
|
||||
@@ -255,8 +255,6 @@ function ReportTable({
|
||||
columns: { key: string; header: string; money?: boolean; signed?: boolean; creditMajor?: boolean }[];
|
||||
currencyCode: string;
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <AdminNoResourceState className="text-sm text-muted-foreground" />;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminLoadingState, AdminLoadingInline } from "@/components/admin/admin-loading-state";
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
@@ -118,7 +118,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [batchId, page, perPage, appliedAgentNodeId]);
|
||||
}, [batchId, page, perPage, appliedAgentNodeId, tRef]);
|
||||
|
||||
async function runAction(label: string, action: () => Promise<unknown>): Promise<void> {
|
||||
setActing(label);
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
@@ -130,7 +130,7 @@ export function SettlementBatchesConsole() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, perPage, appliedDrawNo, appliedStatus]);
|
||||
}, [page, perPage, appliedDrawNo, appliedStatus, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -41,14 +41,9 @@ import { creditLedgerReasonLabel, settlementBillStatusLabel } from "@/modules/se
|
||||
|
||||
const REASON_FILTERS = [
|
||||
"all",
|
||||
"bet_hold",
|
||||
"bet_hold_release",
|
||||
"game_settlement",
|
||||
"game_settlement_win",
|
||||
"game_settlement_loss",
|
||||
"game_settlement_reversal",
|
||||
"settlement_confirm",
|
||||
"settlement_payout",
|
||||
"bet",
|
||||
"bill_settlement",
|
||||
"payment_record",
|
||||
"adjustment",
|
||||
"reversal",
|
||||
|
||||
@@ -52,9 +52,15 @@ export function creditLedgerReasonLabel(
|
||||
reason: string,
|
||||
t: TFunction<"settlementCenter">,
|
||||
): string {
|
||||
if (reason === "bet") {
|
||||
return t("creditLedger.reason.bet", { defaultValue: "下注冻结" });
|
||||
}
|
||||
if (reason === "game_settlement") {
|
||||
return t("creditLedger.reason.game_settlement", { defaultValue: "开奖结算" });
|
||||
}
|
||||
if (reason === "bill_settlement") {
|
||||
return t("creditLedger.reason.bill_settlement", { defaultValue: "账期收付" });
|
||||
}
|
||||
if (reason === "game_settlement_reversal") {
|
||||
return t("creditLedger.reason.game_settlement_reversal", { defaultValue: "开奖冲正" });
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -176,7 +176,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [applied, page, perPage]);
|
||||
}, [applied, page, perPage, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -68,7 +68,7 @@ export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) {
|
||||
void load();
|
||||
}, [ticketNo]);
|
||||
|
||||
const combinations = data?.combinations ?? [];
|
||||
const combinations = useMemo(() => data?.combinations ?? [], [data?.combinations]);
|
||||
const comboLastPage = Math.max(1, Math.ceil(combinations.length / comboPerPage));
|
||||
const comboPageSafe = Math.min(comboPage, comboLastPage);
|
||||
const comboSlice = useMemo(() => {
|
||||
|
||||
@@ -78,11 +78,14 @@ export function txnFiltersHaveUrlPreset(filters: TxnFilters): boolean {
|
||||
/** 下拉「不限」值;请求时转为空串不传参 */
|
||||
export const WALLET_FILTER_ALL = "__all__";
|
||||
|
||||
/** 与 {@see WalletTransactionListController}、{@see LotteryTransferService} 当前写入的 biz_type 一致 */
|
||||
/** 与玩家端资金流水公共类型一致;接口会展开到底层 wallet_txns.biz_type */
|
||||
export const WALLET_TXN_BIZ_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: "transfer_in", label: "transferIn" },
|
||||
{ value: "transfer_out", label: "transferOut" },
|
||||
{ value: "transfer_out_refund", label: "transferOutRefund" },
|
||||
{ value: "bet", label: "bizBetDeduct" },
|
||||
{ value: "prize", label: "bizPrize" },
|
||||
{ value: "refund", label: "bizRefund" },
|
||||
{ value: "reversal", label: "bizReversal" },
|
||||
];
|
||||
|
||||
/** 与 {@see WalletTransactionListController::ALLOWED_STATUS} 一致 */
|
||||
@@ -113,4 +116,4 @@ export function walletAdminSelectDisplayedLabel(
|
||||
}
|
||||
const key = options.find((o) => o.value === v)?.label;
|
||||
return key ? (t ? t(key) : key) : v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export type AgentParentCaps = {
|
||||
|
||||
export type AgentProfileRow = AgentProfilePayload & {
|
||||
agent_node_id: number;
|
||||
relative_share_rate?: number | null;
|
||||
allocated_credit: number;
|
||||
used_credit: number;
|
||||
available_credit: number;
|
||||
|
||||
Reference in New Issue
Block a user