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 { 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>>({});
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ export function useAsyncEffect(
|
|||||||
return () => {
|
return () => {
|
||||||
cleanup?.();
|
cleanup?.();
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, deps);
|
}, deps);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "होइन",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
"bizBetDeduct": "बेट कटौती",
|
"bizBetDeduct": "बेट कटौती",
|
||||||
"bizBetReverse": "बेट उल्टाउने",
|
"bizBetReverse": "बेट उल्टाउने",
|
||||||
"bizJackpotPayout": "ज्याकपट भुक्तानी",
|
"bizJackpotPayout": "ज्याकपट भुक्तानी",
|
||||||
|
"bizPrize": "भुक्तानी",
|
||||||
|
"bizRefund": "फिर्ता",
|
||||||
|
"bizReversal": "रिभर्सल",
|
||||||
"bizSettlePayout": "बन्दोबस्त भुक्तानी",
|
"bizSettlePayout": "बन्दोबस्त भुक्तानी",
|
||||||
"bizSettlementAdjustment": "बन्दोबस्त समायोजन",
|
"bizSettlementAdjustment": "बन्दोबस्त समायोजन",
|
||||||
"bizType": "व्यवसाय प्रकार",
|
"bizType": "व्यवसाय प्रकार",
|
||||||
|
|||||||
@@ -160,6 +160,8 @@
|
|||||||
"totalShareRate": "占成比例 (%)",
|
"totalShareRate": "占成比例 (%)",
|
||||||
"relativeShareRate": "占成比例(占上级 %)",
|
"relativeShareRate": "占成比例(占上级 %)",
|
||||||
"relativeShareRateValue": "占上级 {{rate}}%",
|
"relativeShareRateValue": "占上级 {{rate}}%",
|
||||||
|
"actualShareRate": "实际占成 {{rate}}%",
|
||||||
|
"relativeShareCapHint": "占上级比例最高 100%(上级总占成 {{parent}}%)",
|
||||||
"creditLimit": "授信额度",
|
"creditLimit": "授信额度",
|
||||||
"rebateLimit": "回水上限 (%)",
|
"rebateLimit": "回水上限 (%)",
|
||||||
"defaultPlayerRebate": "默认玩家回水 (%)",
|
"defaultPlayerRebate": "默认玩家回水 (%)",
|
||||||
|
|||||||
@@ -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": "每期下注/派彩/盈亏",
|
||||||
|
|||||||
@@ -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": "占用释放",
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
"transferOutRefund": "转出失败回补",
|
"transferOutRefund": "转出失败回补",
|
||||||
"bizBetDeduct": "下注扣款",
|
"bizBetDeduct": "下注扣款",
|
||||||
"bizBetReverse": "下注冲正",
|
"bizBetReverse": "下注冲正",
|
||||||
|
"bizPrize": "派彩",
|
||||||
|
"bizRefund": "退本",
|
||||||
|
"bizReversal": "冲正",
|
||||||
"bizSettlePayout": "派彩入账",
|
"bizSettlePayout": "派彩入账",
|
||||||
"bizJackpotPayout": "奖池派彩",
|
"bizJackpotPayout": "奖池派彩",
|
||||||
"bizSettlementAdjustment": "结算调账",
|
"bizSettlementAdjustment": "结算调账",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 实际占成 → 占上级比例(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(
|
export function actualShareRateFromRelative(
|
||||||
relativePercent: number,
|
relativePercent: number,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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" }) },
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function CurrencySettingsPanel() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [canManage]);
|
}, [canManage, tRef]);
|
||||||
|
|
||||||
useAsyncEffect(() => {
|
useAsyncEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
|
|||||||
@@ -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" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: "开奖冲正" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user