Refactor agents console and related components
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:
2026-06-30 17:55:00 +08:00
parent 6c3ded5b75
commit 4484a7a77a
56 changed files with 320 additions and 158 deletions

View File

@@ -4,7 +4,7 @@ import { ChevronDown } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox"; 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 { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { AdminPermissionCatalogData } from "@/types/api/admin-user"; import type { AdminPermissionCatalogData } from "@/types/api/admin-user";
@@ -18,7 +18,6 @@ type PermissionSelectorProps = {
selectableSlugs?: string[] | null; selectableSlugs?: string[] | null;
helperText?: string; helperText?: string;
summaryText?: string; summaryText?: string;
emptyText: string;
heightClassName?: string; heightClassName?: string;
}; };
@@ -31,7 +30,6 @@ export function AdminPermissionSelector({
selectableSlugs = null, selectableSlugs = null,
helperText, helperText,
summaryText, summaryText,
emptyText,
heightClassName = "h-[52vh]", heightClassName = "h-[52vh]",
}: PermissionSelectorProps): React.ReactElement { }: PermissionSelectorProps): React.ReactElement {
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({}); const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});

View File

@@ -47,7 +47,6 @@ export function PlayerCreditLimitField({
max={maxCredit} max={maxCredit}
step={1000} step={1000}
integer integer
preserveOverMaxOnBlur
/> />
{parentAvailableCredit !== null ? ( {parentAvailableCredit !== null ? (
<p className="text-[11px] text-muted-foreground/80"> <p className="text-[11px] text-muted-foreground/80">

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useState } from "react";
import { getAdminIntegrationSites } from "@/api/admin-integration-sites"; import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { adminHasAnyPermission } from "@/lib/admin-permissions";

View File

@@ -23,5 +23,6 @@ export function useAsyncEffect(
return () => { return () => {
cleanup?.(); cleanup?.();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps); }, deps);
} }

View File

@@ -200,6 +200,8 @@
"rebateLimit": "Rebate ceiling (%)", "rebateLimit": "Rebate ceiling (%)",
"relativeShareRate": "Share rate (% of parent)", "relativeShareRate": "Share rate (% of parent)",
"relativeShareRateValue": "{{rate}}% of parent", "relativeShareRateValue": "{{rate}}% of parent",
"actualShareRate": "Effective share {{rate}}%",
"relativeShareCapHint": "Up to 100% of parent (parent total share {{parent}}%)",
"riskTags": "Risk tags", "riskTags": "Risk tags",
"riskTagsPlaceholder": "Comma-separated, e.g. overdue, high_turnover", "riskTagsPlaceholder": "Comma-separated, e.g. overdue, high_turnover",
"saveSuccess": "Share and credit settings saved", "saveSuccess": "Share and credit settings saved",

View File

@@ -273,6 +273,24 @@
"play": "Play name or code", "play": "Play name or code",
"operator": "Admin account or name" "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": { "items": {
"draw_profit": { "draw_profit": {
"title": "Draw bets / payout / P&L", "title": "Draw bets / payout / P&L",

View File

@@ -146,8 +146,10 @@
}, },
"reason": { "reason": {
"payment_record": "Bill payment", "payment_record": "Bill payment",
"bet": "Bet hold",
"bet_hold": "Bet hold", "bet_hold": "Bet hold",
"game_settlement": "Draw settlement", "game_settlement": "Draw settlement",
"bill_settlement": "Period payment",
"game_settlement_reversal": "Draw reversal", "game_settlement_reversal": "Draw reversal",
"game_settlement_win": "Draw settlement credit", "game_settlement_win": "Draw settlement credit",
"bet_hold_release": "Hold release", "bet_hold_release": "Hold release",

View File

@@ -24,6 +24,9 @@
"transferOutRefund": "Transfer-out refund", "transferOutRefund": "Transfer-out refund",
"bizBetDeduct": "Bet debit", "bizBetDeduct": "Bet debit",
"bizBetReverse": "Bet reversal", "bizBetReverse": "Bet reversal",
"bizPrize": "Payout",
"bizRefund": "Refund",
"bizReversal": "Reversal",
"bizSettlePayout": "Settlement payout", "bizSettlePayout": "Settlement payout",
"bizJackpotPayout": "Jackpot payout", "bizJackpotPayout": "Jackpot payout",
"bizSettlementAdjustment": "Settlement adjustment", "bizSettlementAdjustment": "Settlement adjustment",

View File

@@ -93,6 +93,24 @@
"wallet_transfer_report": "वालेट ट्रान्सफर", "wallet_transfer_report": "वालेट ट्रान्सफर",
"wallet_txns_daily": "वालेट लेनदेन (दैनिक)" "wallet_txns_daily": "वालेट लेनदेन (दैनिक)"
}, },
"labels": {
"operatorType": {
"admin": "प्रशासक",
"system": "प्रणाली"
},
"transferDirection": {
"in": "भित्र",
"out": "बाहिर"
},
"transferStatus": {
"failed": "असफल",
"manually_processed": "केस बन्द",
"pending_reconcile": "मिलान बाँकी",
"processing": "प्रक्रियामा",
"reversed": "रिभर्स भयो",
"success": "सफल"
}
},
"libraryTitle": "रिपोर्ट प्रकार", "libraryTitle": "रिपोर्ट प्रकार",
"loadFailed": "लोड असफल", "loadFailed": "लोड असफल",
"no": "होइन", "no": "होइन",

View File

@@ -144,8 +144,10 @@
}, },
"reason": { "reason": {
"payment_record": "Bill payment", "payment_record": "Bill payment",
"bet": "Bet hold",
"bet_hold": "Bet hold", "bet_hold": "Bet hold",
"game_settlement": "Draw settlement", "game_settlement": "Draw settlement",
"bill_settlement": "Period payment",
"game_settlement_reversal": "Draw reversal", "game_settlement_reversal": "Draw reversal",
"game_settlement_win": "Draw settlement credit", "game_settlement_win": "Draw settlement credit",
"bet_hold_release": "Hold release", "bet_hold_release": "Hold release",

View File

@@ -10,6 +10,9 @@
"bizBetDeduct": "बेट कटौती", "bizBetDeduct": "बेट कटौती",
"bizBetReverse": "बेट उल्टाउने", "bizBetReverse": "बेट उल्टाउने",
"bizJackpotPayout": "ज्याकपट भुक्तानी", "bizJackpotPayout": "ज्याकपट भुक्तानी",
"bizPrize": "भुक्तानी",
"bizRefund": "फिर्ता",
"bizReversal": "रिभर्सल",
"bizSettlePayout": "बन्दोबस्त भुक्तानी", "bizSettlePayout": "बन्दोबस्त भुक्तानी",
"bizSettlementAdjustment": "बन्दोबस्त समायोजन", "bizSettlementAdjustment": "बन्दोबस्त समायोजन",
"bizType": "व्यवसाय प्रकार", "bizType": "व्यवसाय प्रकार",

View File

@@ -160,6 +160,8 @@
"totalShareRate": "占成比例 (%)", "totalShareRate": "占成比例 (%)",
"relativeShareRate": "占成比例(占上级 %", "relativeShareRate": "占成比例(占上级 %",
"relativeShareRateValue": "占上级 {{rate}}%", "relativeShareRateValue": "占上级 {{rate}}%",
"actualShareRate": "实际占成 {{rate}}%",
"relativeShareCapHint": "占上级比例最高 100%(上级总占成 {{parent}}%",
"creditLimit": "授信额度", "creditLimit": "授信额度",
"rebateLimit": "回水上限 (%)", "rebateLimit": "回水上限 (%)",
"defaultPlayerRebate": "默认玩家回水 (%)", "defaultPlayerRebate": "默认玩家回水 (%)",

View File

@@ -273,6 +273,24 @@
"play": "玩法名称或编码", "play": "玩法名称或编码",
"operator": "管理员账号或昵称" "operator": "管理员账号或昵称"
}, },
"labels": {
"transferDirection": {
"in": "转入",
"out": "转出"
},
"transferStatus": {
"processing": "处理中",
"success": "成功",
"failed": "失败",
"pending_reconcile": "待对账",
"reversed": "已冲正",
"manually_processed": "已结案"
},
"operatorType": {
"admin": "管理员",
"system": "系统"
}
},
"items": { "items": {
"draw_profit": { "draw_profit": {
"title": "每期下注/派彩/盈亏", "title": "每期下注/派彩/盈亏",

View File

@@ -131,9 +131,11 @@
}, },
"reason": { "reason": {
"payment_record": "账单收付", "payment_record": "账单收付",
"bet": "下注冻结",
"bet_hold": "下注冻结", "bet_hold": "下注冻结",
"freezeAmount": "冻结 {{amount}}", "freezeAmount": "冻结 {{amount}}",
"game_settlement": "开奖结算", "game_settlement": "开奖结算",
"bill_settlement": "账期收付",
"game_settlement_reversal": "开奖冲正", "game_settlement_reversal": "开奖冲正",
"game_settlement_win": "开奖结算入账", "game_settlement_win": "开奖结算入账",
"bet_hold_release": "占用释放", "bet_hold_release": "占用释放",

View File

@@ -24,6 +24,9 @@
"transferOutRefund": "转出失败回补", "transferOutRefund": "转出失败回补",
"bizBetDeduct": "下注扣款", "bizBetDeduct": "下注扣款",
"bizBetReverse": "下注冲正", "bizBetReverse": "下注冲正",
"bizPrize": "派彩",
"bizRefund": "退本",
"bizReversal": "冲正",
"bizSettlePayout": "派彩入账", "bizSettlePayout": "派彩入账",
"bizJackpotPayout": "奖池派彩", "bizJackpotPayout": "奖池派彩",
"bizSettlementAdjustment": "结算调账", "bizSettlementAdjustment": "结算调账",

View File

@@ -1,3 +1,4 @@
import { percentValueToUi } from "@/lib/admin-rate-percent";
import type { AgentParentCaps } from "@/types/api/admin-agent"; import type { AgentParentCaps } from "@/types/api/admin-agent";
/** 是否为站点一级代理(根节点)。 */ /** 是否为站点一级代理(根节点)。 */
@@ -65,6 +66,29 @@ export function maxCreditLimitFromParent(
return baseline + extra; return baseline + extra;
} }
/** 实际占成 → 占上级比例0100无上级或上级占成为 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( export function actualShareRateFromRelative(
relativePercent: number, relativePercent: number,

View File

@@ -1,7 +1,6 @@
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import { import {
CalendarClock, CalendarClock,
CircleDollarSign,
ClipboardList, ClipboardList,
Coins, Coins,
FileSpreadsheet, FileSpreadsheet,

View File

@@ -4,7 +4,7 @@ import { Pencil, Plus, Trash2, Network } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AdminSubnav, AdminSubnavButton } from "@/components/admin/admin-subnav"; 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 { AdminLoadingInline } from "@/components/admin/admin-loading-state";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; 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 { formatCredit } from "@/modules/agents/agent-line-sidebar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { percentValueToUi } from "@/lib/admin-rate-percent"; import { percentValueToUi } from "@/lib/admin-rate-percent";
import { relativeShareRateUiFromTotal } from "@/lib/agent-profile-caps";
import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money"; import { AdminTableMoney, adminMoneyCellClassName } from "@/components/admin/admin-table-money";
import type { AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent"; import type { AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent";
@@ -41,7 +42,6 @@ export type AgentLineDetailPanelProps = {
detailTab: AgentDetailTab; detailTab: AgentDetailTab;
onDetailTabChange: (tab: AgentDetailTab) => void; onDetailTabChange: (tab: AgentDetailTab) => void;
canViewProfileTab: boolean; canViewProfileTab: boolean;
canEditProfileTab: boolean;
canSaveProfileTab: boolean; canSaveProfileTab: boolean;
canViewDownlineTab: boolean; canViewDownlineTab: boolean;
canViewPlayersTab: boolean; canViewPlayersTab: boolean;
@@ -73,7 +73,6 @@ export function AgentLineDetailPanel({
detailTab, detailTab,
onDetailTabChange, onDetailTabChange,
canViewProfileTab, canViewProfileTab,
canEditProfileTab,
canSaveProfileTab, canSaveProfileTab,
canViewDownlineTab, canViewDownlineTab,
canViewPlayersTab, canViewPlayersTab,
@@ -149,7 +148,7 @@ export function AgentLineDetailPanel({
{metaParts.length > 0 ? ( {metaParts.length > 0 ? (
<p className="truncate text-sm text-muted-foreground">{metaParts.join(" · ")}</p> <p className="truncate text-sm text-muted-foreground">{metaParts.join(" · ")}</p>
) : null} ) : null}
<AgentHeaderMetrics profile={profile} loading={profileLoading} /> <AgentHeaderMetrics node={node} profile={profile} loading={profileLoading} />
</div> </div>
{canManageNode ? ( {canManageNode ? (
<Button type="button" size="sm" variant="outline" onClick={onEditCurrent}> <Button type="button" size="sm" variant="outline" onClick={onEditCurrent}>
@@ -213,6 +212,9 @@ export function AgentLineDetailPanel({
<DownlineSection <DownlineSection
childAgents={childAgents} childAgents={childAgents}
childCountById={childCountById} childCountById={childCountById}
parentTotalShareRate={
profile?.total_share_rate ?? node.profile_summary?.total_share_rate ?? null
}
canManageNode={canManageNode} canManageNode={canManageNode}
canCreateChild={canCreateChild} canCreateChild={canCreateChild}
canDeleteChild={canDeleteChild} canDeleteChild={canDeleteChild}
@@ -238,9 +240,11 @@ export function AgentLineDetailPanel({
} }
function AgentHeaderMetrics({ function AgentHeaderMetrics({
node,
profile, profile,
loading, loading,
}: { }: {
node: AgentNodeRow;
profile: AgentProfileRow | null; profile: AgentProfileRow | null;
loading: boolean; loading: boolean;
}): React.ReactElement { }): React.ReactElement {
@@ -250,8 +254,22 @@ function AgentHeaderMetrics({
return <p className="text-sm text-muted-foreground"></p>; 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 = [ const items = [
`${t("profile.totalShareRate", { defaultValue: "占成" })} ${profile?.total_share_rate ?? 0}%`, shareMetric,
`${t("profile.creditLimit", { defaultValue: "授信" })} ${formatCredit(profile?.credit_limit ?? 0)}`, `${t("profile.creditLimit", { defaultValue: "授信" })} ${formatCredit(profile?.credit_limit ?? 0)}`,
`${t("lineUi.availableCredit", { defaultValue: "可下发" })} ${formatCredit(profile?.available_credit ?? 0)}`, `${t("lineUi.availableCredit", { defaultValue: "可下发" })} ${formatCredit(profile?.available_credit ?? 0)}`,
`${t("profile.rebateLimit", { defaultValue: "回水上限" })} ${percentValueToUi(profile?.rebate_limit ?? 0)}%`, `${t("profile.rebateLimit", { defaultValue: "回水上限" })} ${percentValueToUi(profile?.rebate_limit ?? 0)}%`,
@@ -267,6 +285,7 @@ function AgentHeaderMetrics({
function DownlineSection({ function DownlineSection({
childAgents, childAgents,
childCountById, childCountById,
parentTotalShareRate,
canManageNode, canManageNode,
canCreateChild, canCreateChild,
canDeleteChild, canDeleteChild,
@@ -277,6 +296,7 @@ function DownlineSection({
}: { }: {
childAgents: AgentNodeRow[]; childAgents: AgentNodeRow[];
childCountById: Map<number, number>; childCountById: Map<number, number>;
parentTotalShareRate: number | null;
canManageNode: boolean; canManageNode: boolean;
canCreateChild: boolean; canCreateChild: boolean;
canDeleteChild: (node: AgentNodeRow) => boolean; canDeleteChild: (node: AgentNodeRow) => boolean;
@@ -313,6 +333,7 @@ function DownlineSection({
<DownlineTable <DownlineTable
childAgents={childAgents} childAgents={childAgents}
childCountById={childCountById} childCountById={childCountById}
parentTotalShareRate={parentTotalShareRate}
canManageNode={canManageNode} canManageNode={canManageNode}
onEditChild={onEditChild} onEditChild={onEditChild}
onDeleteChild={onDeleteChild} onDeleteChild={onDeleteChild}
@@ -326,6 +347,7 @@ function DownlineSection({
function DownlineTable({ function DownlineTable({
childAgents, childAgents,
childCountById, childCountById,
parentTotalShareRate,
canManageNode, canManageNode,
canDeleteChild, canDeleteChild,
onEditChild, onEditChild,
@@ -334,6 +356,7 @@ function DownlineTable({
}: { }: {
childAgents: AgentNodeRow[]; childAgents: AgentNodeRow[];
childCountById: Map<number, number>; childCountById: Map<number, number>;
parentTotalShareRate: number | null;
canManageNode: boolean; canManageNode: boolean;
canDeleteChild: (node: AgentNodeRow) => boolean; canDeleteChild: (node: AgentNodeRow) => boolean;
onEditChild: (node: AgentNodeRow) => void; onEditChild: (node: AgentNodeRow) => void;
@@ -341,6 +364,8 @@ function DownlineTable({
onSelectChild: (node: AgentNodeRow) => void; onSelectChild: (node: AgentNodeRow) => void;
}): React.ReactElement { }): React.ReactElement {
const { t } = useTranslation(["agents", "common"]); const { t } = useTranslation(["agents", "common"]);
const showRelativeShare =
parentTotalShareRate !== null && Number(parentTotalShareRate) > 0;
return ( return (
<div className="admin-table-inset overflow-hidden"> <div className="admin-table-inset overflow-hidden">
@@ -351,7 +376,9 @@ function DownlineTable({
<TableHead>{t("agentName", { defaultValue: "名称" })}</TableHead> <TableHead>{t("agentName", { defaultValue: "名称" })}</TableHead>
<TableHead>{t("loginUsername", { defaultValue: "登录名" })}</TableHead> <TableHead>{t("loginUsername", { defaultValue: "登录名" })}</TableHead>
<TableHead className="text-right whitespace-nowrap"> <TableHead className="text-right whitespace-nowrap">
{t("profile.totalShareRate", { defaultValue: "占成" })} {showRelativeShare
? t("profile.totalShareWithRelative", { defaultValue: "实际占成 / 占上级" })
: t("profile.totalShareRate", { defaultValue: "占成" })}
</TableHead> </TableHead>
<TableHead className="text-right whitespace-nowrap"> <TableHead className="text-right whitespace-nowrap">
{t("profile.creditLimit", { defaultValue: "授信" })} {t("profile.creditLimit", { defaultValue: "授信" })}
@@ -384,8 +411,15 @@ function DownlineTable({
<div className="font-mono text-xs text-muted-foreground">{child.code}</div> <div className="font-mono text-xs text-muted-foreground">{child.code}</div>
</TableCell> </TableCell>
<TableCell className="text-sm">{child.username ?? "—"}</TableCell> <TableCell className="text-sm">{child.username ?? "—"}</TableCell>
<TableCell className="text-right tabular-nums text-sm"> <TableCell className="text-right text-sm">
{summary ? `${summary.total_share_rate ?? 0}%` : "—"} {summary ? (
<DownlineShareCell
totalShareRate={summary.total_share_rate}
parentTotalShareRate={parentTotalShareRate}
/>
) : (
"—"
)}
</TableCell> </TableCell>
<TableCell className={adminMoneyCellClassName("text-right text-sm")}> <TableCell className={adminMoneyCellClassName("text-right text-sm")}>
{summary ? <AdminTableMoney>{formatCredit(summary.credit_limit)}</AdminTableMoney> : "—"} {summary ? <AdminTableMoney>{formatCredit(summary.credit_limit)}</AdminTableMoney> : "—"}
@@ -440,4 +474,31 @@ function DownlineTable({
</div> </div>
</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>;
}

View File

@@ -37,6 +37,32 @@ type AgentLineProvisionWizardProps = {
onSuccess?: (result: AdminAgentLineProvisionResult) => void | Promise<void>; 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({ export function AgentLineProvisionWizard({
embedded = false, embedded = false,
defaultSiteCode, defaultSiteCode,
@@ -178,7 +204,7 @@ export function AgentLineProvisionWizard({
can_grant_extra_rebate: form.can_grant_extra_rebate, can_grant_extra_rebate: form.can_grant_extra_rebate,
}); });
toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" })); toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" }));
setForm((f) => ({ setForm(() => ({
site_code: "", site_code: "",
name: "", name: "",
username: "", username: "",
@@ -307,6 +333,9 @@ export function AgentLineProvisionWizard({
step="0.01" step="0.01"
value={form.total_share_rate} value={form.total_share_rate}
onChange={(e) => setForm((f) => ({ ...f, total_share_rate: e.target.value }))} 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>
<div className="grid gap-2"> <div className="grid gap-2">
@@ -316,6 +345,9 @@ export function AgentLineProvisionWizard({
min={0} min={0}
value={form.credit_limit} value={form.credit_limit}
onChange={(e) => setForm((f) => ({ ...f, credit_limit: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, credit_limit: e.target.value }))}
onBlur={(e) =>
setForm((f) => ({ ...f, credit_limit: clampIntegerInput(e.target.value) }))
}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
@@ -328,6 +360,9 @@ export function AgentLineProvisionWizard({
value={form.rebate_limit} value={form.rebate_limit}
placeholder="50" placeholder="50"
onChange={(e) => setForm((f) => ({ ...f, rebate_limit: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, rebate_limit: e.target.value }))}
onBlur={(e) =>
setForm((f) => ({ ...f, rebate_limit: clampPercentInput(e.target.value) }))
}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
@@ -340,6 +375,12 @@ export function AgentLineProvisionWizard({
value={form.default_player_rebate} value={form.default_player_rebate}
placeholder="50" placeholder="50"
onChange={(e) => setForm((f) => ({ ...f, default_player_rebate: e.target.value }))} 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>
</div> </div>

View File

@@ -4,7 +4,7 @@ import { ChevronRight, Search } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; 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 { AdminLoadingInline } from "@/components/admin/admin-loading-state";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -88,13 +88,11 @@ function collectSearchExpandIds(
} }
export type AgentLineSidebarProps = { export type AgentLineSidebarProps = {
siteLabel: string | null;
/** API 返回的嵌套树(含 children */ /** API 返回的嵌套树(含 children */
tree: AgentNodeRow[]; tree: AgentNodeRow[];
parentNameMap: Map<number, string>; parentNameMap: Map<number, string>;
selectedId: number | null; selectedId: number | null;
keyword: string; keyword: string;
agentCount: number;
loading?: boolean; loading?: boolean;
onKeywordChange: (value: string) => void; onKeywordChange: (value: string) => void;
onSelect: (node: AgentNodeRow) => void; onSelect: (node: AgentNodeRow) => void;
@@ -187,12 +185,10 @@ function TreeRow({
} }
export function AgentLineSidebar({ export function AgentLineSidebar({
siteLabel,
tree, tree,
parentNameMap, parentNameMap,
selectedId, selectedId,
keyword, keyword,
agentCount,
loading = false, loading = false,
onKeywordChange, onKeywordChange,
onSelect, onSelect,

View File

@@ -40,7 +40,6 @@ export function isAgentLineSubnavTabVisible(
): boolean { ): boolean {
const perms = session?.permissions ?? []; const perms = session?.permissions ?? [];
const isSuper = session?.is_super_admin === true; const isSuper = session?.is_super_admin === true;
const boundAgent = session?.agent ?? null;
if (isSuper) { if (isSuper) {
return adminHasAnyPermission(perms, tabRequiredSlugs(href)); return adminHasAnyPermission(perms, tabRequiredSlugs(href));

View File

@@ -4,13 +4,6 @@ import { useTranslation } from "react-i18next";
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { formatAdminCreditMajorDecimal } from "@/lib/money"; import { formatAdminCreditMajorDecimal } from "@/lib/money";
import { cn } from "@/lib/utils"; 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 { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { import {
AGENT_PERCENT_HARD_MAX, AGENT_PERCENT_HARD_MAX,
actualShareRateFromRelative,
creditLimitRangeIssue, creditLimitRangeIssue,
isNumericStepperOutOfRange,
maxCreditLimitFromParent, maxCreditLimitFromParent,
maxDefaultRebatePercent, maxDefaultRebatePercent,
maxRebatePercentFromParent, maxRebatePercentFromParent,
@@ -105,7 +96,12 @@ export function AgentProfileFields({
const maxRebatePercent = maxRebatePercentFromParent(parentCaps); const maxRebatePercent = maxRebatePercentFromParent(parentCaps);
const maxDefaultRebate = maxDefaultRebatePercent(rebateLimit, parentCaps); const maxDefaultRebate = maxDefaultRebatePercent(rebateLimit, parentCaps);
const maxCreditLimit = maxCreditLimitFromParent(parentCaps, baselineCreditLimit); 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, { const creditRangeIssue = creditLimitRangeIssue(creditLimit, {
min: minCreditLimit, min: minCreditLimit,
max: maxCreditLimit, max: maxCreditLimit,
@@ -176,9 +172,7 @@ export function AgentProfileFields({
htmlFor={`${idPrefix}-share-rate`} htmlFor={`${idPrefix}-share-rate`}
className={showReadOnlyDisplay ? "text-foreground/85" : "text-muted-foreground"} className={showReadOnlyDisplay ? "text-foreground/85" : "text-muted-foreground"}
> >
{parentCaps {t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
? t("profile.relativeShareRate", { defaultValue: "占成比例(占上级 %" })
: t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
</Label> </Label>
{showReadOnlyDisplay ? ( {showReadOnlyDisplay ? (
<ReadOnlyScalar id={`${idPrefix}-share-rate`} value={shareRate} suffix="%" flat={isCard} /> <ReadOnlyScalar id={`${idPrefix}-share-rate`} value={shareRate} suffix="%" flat={isCard} />
@@ -191,13 +185,12 @@ export function AgentProfileFields({
max={maxSharePercent} max={maxSharePercent}
step={0.5} step={0.5}
suffix="%" suffix="%"
preserveOverMaxOnBlur
/> />
)} )}
{showFieldHints && parentCaps ? ( {showFieldHints && parentCaps ? (
<p className="text-xs text-muted-foreground/80"> <p className="text-xs text-muted-foreground/80">
{t("profile.relativeShareCapHint", { {t("profile.totalShareParentCapHint", {
defaultValue: "占上级比例最高 100%上级总占成 {{parent}}%", defaultValue: "实际占成不可超过上级总占成 {{parent}}%",
parent: parentCaps.total_share_rate, parent: parentCaps.total_share_rate,
})} })}
</p> </p>
@@ -207,11 +200,11 @@ export function AgentProfileFields({
{t("profile.totalShareCapHint", { defaultValue: "占成比例最高 100%" })} {t("profile.totalShareCapHint", { defaultValue: "占成比例最高 100%" })}
</p> </p>
) : null} ) : null}
{showFieldHints && parentCaps && shareRate && actualShare !== null ? ( {parentCaps && shareRate && relativeShare !== null ? (
<p className="text-xs text-muted-foreground/80"> <p className="text-xs text-muted-foreground/80">
{t("profile.actualShareRate", { {t("profile.relativeShareRateValue", {
defaultValue: "实际占成 {{rate}}%", defaultValue: "占上级 {{rate}}%",
rate: actualShare, rate: relativeShare,
})} })}
</p> </p>
) : null} ) : null}
@@ -234,7 +227,6 @@ export function AgentProfileFields({
max={maxCreditLimit} max={maxCreditLimit}
step={1000} step={1000}
integer integer
preserveOverMaxOnBlur
/> />
)} )}
{showFieldHints && parentCaps && maxCreditLimit !== undefined && profileScalarsEditable ? ( {showFieldHints && parentCaps && maxCreditLimit !== undefined && profileScalarsEditable ? (
@@ -289,7 +281,6 @@ export function AgentProfileFields({
max={maxRebatePercent} max={maxRebatePercent}
step={0.5} step={0.5}
suffix="%" suffix="%"
preserveOverMaxOnBlur
/> />
)} )}
{parentCaps ? ( {parentCaps ? (
@@ -323,7 +314,6 @@ export function AgentProfileFields({
max={maxDefaultRebate} max={maxDefaultRebate}
step={0.5} step={0.5}
suffix="%" suffix="%"
preserveOverMaxOnBlur
/> />
)} )}
<p className="text-xs text-muted-foreground/80"> <p className="text-xs text-muted-foreground/80">

View File

@@ -182,13 +182,7 @@ export function AgentsConsole(): React.ReactElement {
}; };
const applyProfileRowToForm = (row: AgentProfileRow) => { const applyProfileRowToForm = (row: AgentProfileRow) => {
const parentRate = row.parent_caps?.total_share_rate ?? 0; setProfileShareRate(percentValueToUi(row.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));
}
setProfileCreditLimit(String(row.credit_limit ?? 0)); setProfileCreditLimit(String(row.credit_limit ?? 0));
setProfileRebateLimit(percentValueToUi(row.rebate_limit ?? 0)); setProfileRebateLimit(percentValueToUi(row.rebate_limit ?? 0));
setProfileDefaultRebate(percentValueToUi(row.default_player_rebate ?? 0)); setProfileDefaultRebate(percentValueToUi(row.default_player_rebate ?? 0));
@@ -216,9 +210,6 @@ export function AgentsConsole(): React.ReactElement {
can_create_player: profileCanCreatePlayer, can_create_player: profileCanCreatePlayer,
risk_tags: parseRiskTagsInput(profileRiskTags), risk_tags: parseRiskTagsInput(profileRiskTags),
}; };
if (profileParentCaps) {
return { ...base, relative_share_rate: shareRate };
}
return { ...base, total_share_rate: shareRate }; return { ...base, total_share_rate: shareRate };
}; };
@@ -279,7 +270,7 @@ export function AgentsConsole(): React.ReactElement {
parentCaps: profileParentCaps, parentCaps: profileParentCaps,
baselineCreditLimit: profileBaselineCreditLimit, baselineCreditLimit: profileBaselineCreditLimit,
minCreditLimit: profileMinCreditLimit, minCreditLimit: profileMinCreditLimit,
usesRelativeShare: profileParentCaps !== null, usesRelativeShare: false,
validateCredit: isRootProfileEditableByActor(creditNode, isSuperAdmin), validateCredit: isRootProfileEditableByActor(creditNode, isSuperAdmin),
}); });
@@ -297,16 +288,6 @@ export function AgentsConsole(): React.ReactElement {
); );
const visibleAgentRows = flatNodes; const visibleAgentRows = flatNodes;
const boundSite = profile?.site ?? null; 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( const rootNode = useMemo(
() => flatNodes.find((node) => node.is_root || node.depth === 0) ?? null, () => flatNodes.find((node) => node.is_root || node.depth === 0) ?? null,
[flatNodes], [flatNodes],
@@ -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"> <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 ? ( {showAgentSidebar ? (
<AgentLineSidebar <AgentLineSidebar
siteLabel={selectedSiteLabel}
tree={tree} tree={tree}
parentNameMap={parentNameMap} parentNameMap={parentNameMap}
selectedId={selectedNodeId} selectedId={selectedNodeId}
keyword={keyword} keyword={keyword}
agentCount={visibleAgentRows.length}
loading={loading && visibleAgentRows.length === 0} loading={loading && visibleAgentRows.length === 0}
onKeywordChange={(value) => { onKeywordChange={(value) => {
setKeyword(value); setKeyword(value);
@@ -1124,7 +1103,6 @@ export function AgentsConsole(): React.ReactElement {
detailTab={detailTab} detailTab={detailTab}
onDetailTabChange={setDetailTab} onDetailTabChange={setDetailTab}
canViewProfileTab={canViewProfileTab} canViewProfileTab={canViewProfileTab}
canEditProfileTab={canEditSelectedProfile && !isOwnAgentNode}
canSaveProfileTab={canManageProfile && canEditSelectedProfile && !isOwnAgentNode} canSaveProfileTab={canManageProfile && canEditSelectedProfile && !isOwnAgentNode}
canViewDownlineTab={canShowDownlineTab} canViewDownlineTab={canShowDownlineTab}
canViewPlayersTab={canShowPlayersTab} canViewPlayersTab={canShowPlayersTab}

View File

@@ -949,7 +949,6 @@ export function AgentsPlayersPanel({
max={maxPlayerRebatePercent} max={maxPlayerRebatePercent}
step={0.5} step={0.5}
suffix="%" suffix="%"
preserveOverMaxOnBlur
/> />
<p className="text-[11px] text-muted-foreground/80"> <p className="text-[11px] text-muted-foreground/80">
{agentRebateLimitPercent !== null {agentRebateLimitPercent !== null
@@ -1036,7 +1035,6 @@ export function AgentsPlayersPanel({
max={maxPlayerRebatePercent} max={maxPlayerRebatePercent}
step={0.5} step={0.5}
suffix="%" suffix="%"
preserveOverMaxOnBlur
/> />
<p className="text-[11px] text-muted-foreground/80"> <p className="text-[11px] text-muted-foreground/80">
{agentRebateLimitPercent !== null {agentRebateLimitPercent !== null

View File

@@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { 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, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -105,7 +105,7 @@ export function AuditLogsConsole(): React.ReactElement {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [page, perPage, appliedOperatorId, appliedModule, appliedOpType, appliedStartDate, appliedEndDate]); }, [page, perPage, appliedOperatorId, appliedModule, appliedOpType, appliedStartDate, appliedEndDate, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import type { ReactNode } from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -29,7 +28,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 { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { ratioToPercentUi } from "@/lib/admin-rate-percent"; import { ratioToPercentUi } from "@/lib/admin-rate-percent";
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";
@@ -94,8 +93,6 @@ type OddsConfigDocScreenProps = {
embedded?: boolean; embedded?: boolean;
/** 合并页:左侧三步骤 + 右侧配置摘要(参考设计稿) */ /** 合并页:左侧三步骤 + 右侧配置摘要(参考设计稿) */
mergedLayout?: boolean; mergedLayout?: boolean;
/** 合并页第 3 步:佣金 / 回水 */
rebateSection?: ReactNode;
/** 合并页共享数据层(避免与回水区块重复拉取版本详情) */ /** 合并页共享数据层(避免与回水区块重复拉取版本详情) */
workspace?: OddsConfigWorkspace; workspace?: OddsConfigWorkspace;
/** 与回水分区共用版本选择(无 workspace 时) */ /** 与回水分区共用版本选择(无 workspace 时) */
@@ -106,7 +103,6 @@ type OddsConfigDocScreenProps = {
export function OddsConfigDocScreen({ export function OddsConfigDocScreen({
embedded = false, embedded = false,
mergedLayout = false, mergedLayout = false,
rebateSection,
workspace, workspace,
versionId: controlledVersionId, versionId: controlledVersionId,
onVersionIdChange, onVersionIdChange,
@@ -159,7 +155,7 @@ export function OddsConfigDocScreen({
} finally { } finally {
setLoadingTypes(false); setLoadingTypes(false);
} }
}, []); }, [tRef]);
const refreshList = useCallback(async () => { const refreshList = useCallback(async () => {
setLoadingList(true); setLoadingList(true);
@@ -175,7 +171,7 @@ export function OddsConfigDocScreen({
} finally { } finally {
setLoadingList(false); setLoadingList(false);
} }
}, []); }, [tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
if (workspace) { if (workspace) {
@@ -199,7 +195,7 @@ export function OddsConfigDocScreen({
} finally { } finally {
setLoadingDetail(false); setLoadingDetail(false);
} }
}, []); }, [tRef]);
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {
@@ -492,7 +488,7 @@ export function OddsConfigDocScreen({
newValue: next?.odds_value ?? null, newValue: next?.odds_value ?? null,
}; };
}); });
}, [activeCompareRows, resolvedDetail, resolvedDraftRows, resolvedPlayCode, t, i18n.language]); }, [activeCompareRows, resolvedDetail, resolvedDraftRows, resolvedPlayCode, t]);
const catTabs: { id: CatTab; label: string }[] = [ const catTabs: { id: CatTab; label: string }[] = [
{ id: "all", label: t("odds.tabs.all", { ns: "config" }) }, { id: "all", label: t("odds.tabs.all", { ns: "config" }) },

View File

@@ -181,7 +181,7 @@ export function PlayConfigDocScreen() {
} finally { } finally {
setLoadingList(false); setLoadingList(false);
} }
}, []); }, [tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void refreshList(); void refreshList();
@@ -214,7 +214,7 @@ export function PlayConfigDocScreen() {
setLoadingDetail(false); setLoadingDetail(false);
} }
} }
}, []); }, [tRef]);
useEffect(() => { useEffect(() => {
if (list.length === 0) { if (list.length === 0) {

View File

@@ -40,7 +40,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 { 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";
@@ -133,7 +133,6 @@ export function RebateConfigDocScreen({
const resolvedList = workspace?.list ?? listRows; const resolvedList = workspace?.list ?? listRows;
const resolvedDetail = workspace?.detail ?? detail; const resolvedDetail = workspace?.detail ?? detail;
const resolvedDraftRows = workspace?.draftRows ?? draftRows; const resolvedDraftRows = workspace?.draftRows ?? draftRows;
const setResolvedDraftRows = workspace?.setDraftRows ?? setDraftRows;
const resolvedLoading = workspace ? workspace.loadingList || workspace.loadingTypes : loading; const resolvedLoading = workspace ? workspace.loadingList || workspace.loadingTypes : loading;
const resolvedLoadingDetail = workspace?.loadingDetail ?? loadingDetail; const resolvedLoadingDetail = workspace?.loadingDetail ?? loadingDetail;
@@ -152,7 +151,7 @@ export function RebateConfigDocScreen({
); );
setTypes([]); setTypes([]);
} }
}, []); }, [tRef]);
const refreshList = useCallback(async () => { const refreshList = useCallback(async () => {
try { try {
@@ -164,7 +163,7 @@ export function RebateConfigDocScreen({
); );
setListRows([]); setListRows([]);
} }
}, []); }, [tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
if (workspace) { if (workspace) {
@@ -239,7 +238,7 @@ export function RebateConfigDocScreen({
} finally { } finally {
setLoadingDetail(false); setLoadingDetail(false);
} }
}, []); }, [tRef]);
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {

View File

@@ -70,7 +70,7 @@ export function useOddsConfigWorkspace(
} finally { } finally {
setLoadingTypes(false); setLoadingTypes(false);
} }
}, []); }, [tRef]);
const refreshList = useCallback(async () => { const refreshList = useCallback(async () => {
setLoadingList(true); setLoadingList(true);
@@ -85,7 +85,7 @@ export function useOddsConfigWorkspace(
} finally { } finally {
setLoadingList(false); setLoadingList(false);
} }
}, []); }, [tRef]);
const loadDetail = useCallback( const loadDetail = useCallback(
async (id: number) => { async (id: number) => {
@@ -110,7 +110,7 @@ export function useOddsConfigWorkspace(
} }
} }
}, },
[applyDetail], [applyDetail, tRef],
); );
const reloadDetail = useCallback(async () => { const reloadDetail = useCallback(async () => {

View File

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
import { BarChart3, Gift, TrendingUp, Wallet } from "lucide-react"; import { BarChart3, Gift, TrendingUp, Wallet } from "lucide-react";
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field"; 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 { Alert, AlertDescription } from "@/components/ui/alert";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -82,7 +82,6 @@ export function DashboardAnalyticsMain({ analytics }: { analytics: DashboardAnal
playOptions, playOptions,
sparklines, sparklines,
formatMoney, formatMoney,
formatSignedMoney,
profitScope, profitScope,
} = analytics; } = analytics;

View File

@@ -2,7 +2,7 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react"; import { useCallback, useMemo, useState, type ReactElement } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AlertTriangle, BarChart3, ClipboardList, RefreshCw, Shield, TrendingUp, Wallet } from "lucide-react"; import { AlertTriangle, BarChart3, ClipboardList, RefreshCw, Shield, TrendingUp, Wallet } from "lucide-react";
@@ -22,7 +22,7 @@ import {
formatDashboardMoneyMinor, formatDashboardMoneyMinor,
} from "@/modules/dashboard/use-dashboard-analytics"; } from "@/modules/dashboard/use-dashboard-analytics";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 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 { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@@ -241,7 +241,7 @@ export function DashboardConsole(): ReactElement {
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
} }
}, []); }, [tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(false); void load(false);

View File

@@ -26,7 +26,7 @@ import {
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import {

View File

@@ -10,7 +10,7 @@ import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useTranslationRef } from "@/hooks/use-translation-ref"; import { useTranslationRef } from "@/hooks/use-translation-ref";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";

View File

@@ -132,7 +132,7 @@ export function useDashboardAnalytics({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [enabled, period, playCode, customFrom, customTo, scope.agentNodeId, scope.siteCode]); }, [enabled, period, playCode, customFrom, customTo, scope.agentNodeId, scope.siteCode, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -8,8 +8,6 @@ import {
useState, useState,
type ReactNode, type ReactNode,
} from "react"; } from "react";
import { useTranslation } from "react-i18next";
import { getAdminDraw } from "@/api/admin-draws"; import { getAdminDraw } from "@/api/admin-draws";
import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref"; import { useTranslationRef } from "@/hooks/use-translation-ref";

View File

@@ -8,7 +8,7 @@ import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminDrawResultBatches } from "@/api/admin-draws"; import { getAdminDrawResultBatches } from "@/api/admin-draws";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; 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 { import {
Table, Table,
TableBody, TableBody,
@@ -47,7 +47,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [idNum]); }, [idNum, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -16,7 +16,7 @@ import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
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 { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; 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 { import {
Table, Table,
TableBody, TableBody,

View File

@@ -85,7 +85,7 @@ export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
return true; return true;
}), }),
[canManageDraw, canViewDraw, canViewRisk], [canManageDraw, canViewDraw, canViewFinance, canViewRisk],
); );
return ( return (

View File

@@ -8,7 +8,7 @@ import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminJackpotContributions, getAdminJackpotPayoutLogs } from "@/api/admin-jackpot"; import { getAdminJackpotContributions, getAdminJackpotPayoutLogs } from "@/api/admin-jackpot";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; 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 { AdminPlayerIdentityCells, AdminPlayerIdentityHeads } from "@/components/admin/admin-player-identity-columns";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { AdminSubnav, AdminSubnavButton } from "@/components/admin/admin-subnav"; 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 { 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 { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { import {
Table, Table,
TableBody, TableBody,
@@ -66,8 +66,6 @@ function JackpotRecordTableSection({
children: React.ReactNode; children: React.ReactNode;
footer: React.ReactNode; footer: React.ReactNode;
}) { }) {
const { t } = useTranslation("common");
return ( return (
<div className="admin-table-shell"> <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"> <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 { } finally {
setLoadingP(false); setLoadingP(false);
} }
}, [pPage, pPer, appliedDrawNo]); }, [pPage, pPer, appliedDrawNo, tRef]);
const loadContribs = useCallback(async () => { const loadContribs = useCallback(async () => {
setLoadingC(true); setLoadingC(true);
@@ -142,7 +140,7 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
} finally { } finally {
setLoadingC(false); setLoadingC(false);
} }
}, [cPage, cPer, appliedDrawNo]); }, [cPage, cPer, appliedDrawNo, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void loadPayouts(); void loadPayouts();

View File

@@ -146,7 +146,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
} finally { } finally {
setPlayerLoading(false); setPlayerLoading(false);
} }
}, [playerId]); }, [playerId, tRef]);
const loadTickets = useCallback(async () => { const loadTickets = useCallback(async () => {
setTicketsLoading(true); setTicketsLoading(true);

View File

@@ -227,7 +227,7 @@ export function PlayersConsole(): React.ReactElement {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [page, perPage, query, siteFilter]); }, [page, perPage, query, siteFilter, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -9,11 +9,11 @@ import { Download, RefreshCw } from "lucide-react";
import { downloadAdminReportJob, getAdminReportJobs } from "@/api/admin-report-jobs"; import { downloadAdminReportJob, getAdminReportJobs } from "@/api/admin-report-jobs";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; 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 { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { import {
Table, Table,
TableBody, TableBody,

View File

@@ -7,7 +7,8 @@ import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admi
import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; import { AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge"; 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 { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { import {
@@ -36,6 +37,18 @@ type ReportPreviewTableProps = {
displayCurrency: string; 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({ export function ReportPreviewTable({
report, report,
result, result,
@@ -135,7 +148,9 @@ export function ReportPreviewTable({
<TableRow key={batch.id} className="bg-muted/10"> <TableRow key={batch.id} className="bg-muted/10">
<TableCell>#{batch.id}</TableCell> <TableCell>#{batch.id}</TableCell>
<TableCell> <TableCell>
<AdminStatusBadge status={batch.status}>{batch.status}</AdminStatusBadge> <AdminStatusBadge status={batch.status}>
{settlementBatchStatusLabel(batch.status, t)}
</AdminStatusBadge>
</TableCell> </TableCell>
<TableCell className="text-right">{batch.total_ticket_count}</TableCell> <TableCell className="text-right">{batch.total_ticket_count}</TableCell>
<TableCell className="text-right">{batch.total_win_count}</TableCell> <TableCell className="text-right">{batch.total_win_count}</TableCell>
@@ -269,9 +284,11 @@ export function ReportPreviewTable({
<TableRow key={item.id}> <TableRow key={item.id}>
<TableCell className="font-mono text-xs">{item.transfer_no}</TableCell> <TableCell className="font-mono text-xs">{item.transfer_no}</TableCell>
<TableCell>{optionText(item.username, item.nickname) || item.player_id}</TableCell> <TableCell>{optionText(item.username, item.nickname) || item.player_id}</TableCell>
<TableCell>{item.direction}</TableCell> <TableCell>{reportValueLabel("transferDirection", item.direction, t)}</TableCell>
<TableCell> <TableCell>
<AdminStatusBadge status={item.status}>{item.status}</AdminStatusBadge> <AdminStatusBadge status={item.status}>
{reportValueLabel("transferStatus", item.status, t)}
</AdminStatusBadge>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{item.currency_code} {item.amount} {item.currency_code} {item.amount}
@@ -333,7 +350,7 @@ export function ReportPreviewTable({
{result.raw.logs.items.map((item) => ( {result.raw.logs.items.map((item) => (
<TableRow key={item.id}> <TableRow key={item.id}>
<TableCell className="font-mono text-xs">#{item.id}</TableCell> <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"> <TableCell className="text-right">
{formatPlainMoney(item.amount, result.raw.currency_code)} {formatPlainMoney(item.amount, result.raw.currency_code)}
</TableCell> </TableCell>
@@ -402,11 +419,11 @@ export function ReportPreviewTable({
{result.raw.map((item) => ( {result.raw.map((item) => (
<TableRow key={item.id}> <TableRow key={item.id}>
<TableCell className="font-mono text-xs">#{item.id}</TableCell> <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.operator_id}</TableCell>
<TableCell>{item.module_code}</TableCell> <TableCell>{item.module_label || item.module_code}</TableCell>
<TableCell>{item.action_code}</TableCell> <TableCell>{item.action_label || item.action_code}</TableCell>
<TableCell>{item.target_type || "—"}</TableCell> <TableCell>{item.target_label || item.target_type || "—"}</TableCell>
<TableCell>{formatTs(item.created_at)}</TableCell> <TableCell>{formatTs(item.created_at)}</TableCell>
</TableRow> </TableRow>
))} ))}
@@ -417,4 +434,4 @@ export function ReportPreviewTable({
default: default:
return <AdminLoadingInline />; return <AdminLoadingInline />;
} }
} }

View File

@@ -71,7 +71,6 @@ import {
drawRowsFromSummary, drawRowsFromSummary,
emptyFilters, emptyFilters,
emptySearch, emptySearch,
formatExportInstant,
formatPlainMoney, formatPlainMoney,
formatUsagePercent, formatUsagePercent,
headlineStats, headlineStats,
@@ -434,6 +433,7 @@ export function ReportsConsole({ initialCategory = "profit" }: ReportsConsolePro
return; return;
} }
void queryReport(); void queryReport();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, perPage]); }, [page, perPage]);
function updateFilter<K extends keyof ReportFilters>(key: K, value: ReportFilters[K]): void { function updateFilter<K extends keyof ReportFilters>(key: K, value: ReportFilters[K]): void {

View File

@@ -19,7 +19,7 @@ import type {
AdminReportPlayerWinLossRow, AdminReportPlayerWinLossRow,
} from "@/types/api/admin-reports"; } 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 ExportCell = string | number | null;
export type ExportRow = Record<string, ExportCell>; export type ExportRow = Record<string, ExportCell>;
@@ -128,7 +128,8 @@ export function formatPlainMoney(value: number, currencyCode: string | null | un
return formatAdminMinorUnits(value, currencyCode || "NPR"); 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)); return cn("tabular-nums", signedMoneyClass(amount, true));
} }
@@ -238,13 +239,9 @@ export function buildDailyProfitRowsAndSummary(
pageScopedLabel: (statKey: string) => string, pageScopedLabel: (statKey: string) => string,
currencyCode: string, currencyCode: string,
): Pick<Extract<ReportResult, { key: "daily_profit" }>, "rows" | "summary"> { ): Pick<Extract<ReportResult, { key: "daily_profit" }>, "rows" | "summary"> {
let totalBet = 0;
let totalPayout = 0;
let totalGross = 0; let totalGross = 0;
const rows = items.map((item) => { const rows = items.map((item) => {
totalBet += item.total_bet_minor;
totalPayout += item.total_payout_minor;
totalGross += item.approx_house_gross_minor; totalGross += item.approx_house_gross_minor;
return { return {
business_date: item.business_date, business_date: item.business_date,
@@ -306,11 +303,9 @@ export function buildPlayDimensionRowsAndSummary(
t: (key: string) => string, t: (key: string) => string,
currencyCode: string, currencyCode: string,
): Pick<Extract<ReportResult, { key: "play_dimension" }>, "rows" | "summary"> { ): Pick<Extract<ReportResult, { key: "play_dimension" }>, "rows" | "summary"> {
let totalBet = 0;
let totalGross = 0; let totalGross = 0;
const rows = items.map((item) => { const rows = items.map((item) => {
totalBet += item.total_bet_minor;
totalGross += item.approx_house_gross_minor; totalGross += item.approx_house_gross_minor;
return { return {
play_code: item.play_code, play_code: item.play_code,

View File

@@ -11,7 +11,7 @@ import { AdminListPaginationFooter } from "@/components/admin/admin-list-paginat
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { import {
Table, Table,
TableBody, TableBody,
@@ -63,7 +63,7 @@ export function RiskPoolDetailConsole({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [drawId, number4d, page, perPage]); }, [drawId, number4d, page, perPage, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -102,7 +102,7 @@ export function CurrencySettingsPanel() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [canManage]); }, [canManage, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -1,6 +1,6 @@
"use client"; "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 { useTranslation } from "react-i18next";
import { SignedMoney } from "@/lib/admin-signed-money"; import { SignedMoney } from "@/lib/admin-signed-money";
@@ -255,8 +255,6 @@ function ReportTable({
columns: { key: string; header: string; money?: boolean; signed?: boolean; creditMajor?: boolean }[]; columns: { key: string; header: string; money?: boolean; signed?: boolean; creditMajor?: boolean }[];
currencyCode: string; currencyCode: string;
}): React.ReactElement { }): React.ReactElement {
const { t } = useTranslation("common");
if (rows.length === 0) { if (rows.length === 0) {
return <AdminNoResourceState className="text-sm text-muted-foreground" />; return <AdminNoResourceState className="text-sm text-muted-foreground" />;
} }

View File

@@ -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, AdminLoadingInline } from "@/components/admin/admin-loading-state";
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog"; import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
@@ -118,7 +118,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [batchId, page, perPage, appliedAgentNodeId]); }, [batchId, page, perPage, appliedAgentNodeId, tRef]);
async function runAction(label: string, action: () => Promise<unknown>): Promise<void> { async function runAction(label: string, action: () => Promise<unknown>): Promise<void> {
setActing(label); setActing(label);

View File

@@ -47,7 +47,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 { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { formatAdminMinorUnits } from "@/lib/money"; import { formatAdminMinorUnits } from "@/lib/money";
@@ -130,7 +130,7 @@ export function SettlementBatchesConsole() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [page, perPage, appliedDrawNo, appliedStatus]); }, [page, perPage, appliedDrawNo, appliedStatus, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -41,14 +41,9 @@ import { creditLedgerReasonLabel, settlementBillStatusLabel } from "@/modules/se
const REASON_FILTERS = [ const REASON_FILTERS = [
"all", "all",
"bet_hold",
"bet_hold_release",
"game_settlement", "game_settlement",
"game_settlement_win", "bet",
"game_settlement_loss", "bill_settlement",
"game_settlement_reversal",
"settlement_confirm",
"settlement_payout",
"payment_record", "payment_record",
"adjustment", "adjustment",
"reversal", "reversal",

View File

@@ -52,9 +52,15 @@ export function creditLedgerReasonLabel(
reason: string, reason: string,
t: TFunction<"settlementCenter">, t: TFunction<"settlementCenter">,
): string { ): string {
if (reason === "bet") {
return t("creditLedger.reason.bet", { defaultValue: "下注冻结" });
}
if (reason === "game_settlement") { if (reason === "game_settlement") {
return t("creditLedger.reason.game_settlement", { defaultValue: "开奖结算" }); return t("creditLedger.reason.game_settlement", { defaultValue: "开奖结算" });
} }
if (reason === "bill_settlement") {
return t("creditLedger.reason.bill_settlement", { defaultValue: "账期收付" });
}
if (reason === "game_settlement_reversal") { if (reason === "game_settlement_reversal") {
return t("creditLedger.reason.game_settlement_reversal", { defaultValue: "开奖冲正" }); return t("creditLedger.reason.game_settlement_reversal", { defaultValue: "开奖冲正" });
} }

View File

@@ -26,7 +26,7 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
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 { import {
Table, Table,
TableBody, TableBody,
@@ -176,7 +176,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [applied, page, perPage]); }, [applied, page, perPage, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();

View File

@@ -68,7 +68,7 @@ export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) {
void load(); void load();
}, [ticketNo]); }, [ticketNo]);
const combinations = data?.combinations ?? []; const combinations = useMemo(() => data?.combinations ?? [], [data?.combinations]);
const comboLastPage = Math.max(1, Math.ceil(combinations.length / comboPerPage)); const comboLastPage = Math.max(1, Math.ceil(combinations.length / comboPerPage));
const comboPageSafe = Math.min(comboPage, comboLastPage); const comboPageSafe = Math.min(comboPage, comboLastPage);
const comboSlice = useMemo(() => { const comboSlice = useMemo(() => {

View File

@@ -78,11 +78,14 @@ export function txnFiltersHaveUrlPreset(filters: TxnFilters): boolean {
/** 下拉「不限」值;请求时转为空串不传参 */ /** 下拉「不限」值;请求时转为空串不传参 */
export const WALLET_FILTER_ALL = "__all__"; 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 }[] = [ export const WALLET_TXN_BIZ_OPTIONS: { value: string; label: string }[] = [
{ value: "transfer_in", label: "transferIn" }, { value: "transfer_in", label: "transferIn" },
{ value: "transfer_out", label: "transferOut" }, { 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} 一致 */ /** 与 {@see WalletTransactionListController::ALLOWED_STATUS} 一致 */
@@ -113,4 +116,4 @@ export function walletAdminSelectDisplayedLabel(
} }
const key = options.find((o) => o.value === v)?.label; const key = options.find((o) => o.value === v)?.label;
return key ? (t ? t(key) : key) : v; return key ? (t ? t(key) : key) : v;
} }

View File

@@ -73,6 +73,7 @@ export type AgentParentCaps = {
export type AgentProfileRow = AgentProfilePayload & { export type AgentProfileRow = AgentProfilePayload & {
agent_node_id: number; agent_node_id: number;
relative_share_rate?: number | null;
allocated_credit: number; allocated_credit: number;
used_credit: number; used_credit: number;
available_credit: number; available_credit: number;