diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json
index 94c9f1d..ee6dcdd 100644
--- a/src/i18n/locales/en/config.json
+++ b/src/i18n/locales/en/config.json
@@ -63,7 +63,8 @@
"refreshing": "Refreshing",
"refresh": "Refresh versions",
"newDraft": "New draft",
- "saveDraft": "Save draft"
+ "saveDraft": "Save draft",
+ "saveFailed": "Failed to save configuration"
},
"wallet": {
"title": "Wallet transfer limit settings",
@@ -318,6 +319,7 @@
"deleteFailed": "Delete failed",
"editingVersion": "Editing version v{{version}} · {{status}}",
"readOnlyHint": "Create a draft before editing rebate.",
+ "dimensionRatesMixedHint": "Rebate rates within the same dimension (2D/3D/4D) are not identical: the three percentage inputs show the first play (alphabetically) that has the primary prize scope; use the table as the source of truth. Bulk inputs will overwrite all plays in that dimension to one rate.",
"fields": {
"d2": "2D rebate rate (%)",
"d3": "3D rebate rate (%)",
diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json
index 222ca8f..c64cb88 100644
--- a/src/i18n/locales/ne/config.json
+++ b/src/i18n/locales/ne/config.json
@@ -63,7 +63,8 @@
"refreshing": "रिफ्रेस हुँदैछ",
"refresh": "संस्करण रिफ्रेस",
"newDraft": "नयाँ ड्राफ्ट",
- "saveDraft": "ड्राफ्ट सेभ गर्नुहोस्"
+ "saveDraft": "ड्राफ्ट सेभ गर्नुहोस्",
+ "saveFailed": "कन्फिगरेसन सुरक्षित गर्न असफल"
},
"wallet": {
"title": "वालेट ट्रान्सफर सीमा सेटिङ",
@@ -318,6 +319,7 @@
"deleteFailed": "मेटाउन असफल",
"editingVersion": "सम्पादन भइरहेको संस्करण v{{version}} · {{status}}",
"readOnlyHint": "रिबेट सम्पादन गर्नुअघि ड्राफ्ट बनाउनुहोस्।",
+ "dimensionRatesMixedHint": "एउटै डाइमेन्शनभित्रका खेलका रिबेट दरहरू उस्तै छैनन्: माथिको तीन फिल्डहरू मुख्य पुरस्कार स्कोपको पहिलो प्ले (क्रम)को उदाहरण मात्र देखाउँछन्; वास्तविक दर टेबुलको लाइन हेर्नुहोस्। थोक इनपुटले डाइमेन्शनभित्रका सबै प्लेहरू एउटै दरले अधिलेखन गर्छ।",
"fields": {
"d2": "2D रिबेट दर (%)",
"d3": "3D रिबेट दर (%)",
diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json
index e64b4f1..5424e27 100644
--- a/src/i18n/locales/zh/config.json
+++ b/src/i18n/locales/zh/config.json
@@ -63,7 +63,8 @@
"refreshing": "刷新中",
"refresh": "刷新版本",
"newDraft": "新建草稿",
- "saveDraft": "保存草稿"
+ "saveDraft": "保存草稿",
+ "saveFailed": "配置保存失败"
},
"wallet": {
"title": "钱包转账限额配置",
@@ -318,6 +319,7 @@
"deleteFailed": "删除失败",
"editingVersion": "当前编辑版本 v{{version}} · {{status}}",
"readOnlyHint": "修改回水前请先创建草稿。",
+ "dimensionRatesMixedHint": "检测到同一维度(2D/3D/4D)内各玩法的首奖级回水比例不完全相同:上方三个百分比输入仅展示按玩法编码排序后的第一个有值示例,实际回水请以下方表格各行数据为准;使用批量输入会先按维度覆盖为同一比例。",
"fields": {
"d2": "2D 回水比例 (%)",
"d3": "3D 回水比例 (%)",
diff --git a/src/lib/config-version-auto-pick.ts b/src/lib/config-version-auto-pick.ts
new file mode 100644
index 0000000..249688c
--- /dev/null
+++ b/src/lib/config-version-auto-pick.ts
@@ -0,0 +1,12 @@
+import type { ConfigVersionSummary } from "@/types/api/admin-config";
+
+/** Prefer newest draft, then active, otherwise highest numeric id — matches config doc UX. */
+export function pickDefaultConfigVersionId(list: ConfigVersionSummary[]): string | null {
+ if (list.length === 0) {
+ return null;
+ }
+ const drafts = list.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
+ const active = list.find((x) => x.status === "active");
+ const pick = drafts[0] ?? active ?? [...list].sort((a, b) => b.id - a.id)[0];
+ return pick ? String(pick.id) : null;
+}
diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx
index 9061f00..a2b843d 100644
--- a/src/modules/config/doc/odds-config-doc-screen.tsx
+++ b/src/modules/config/doc/odds-config-doc-screen.tsx
@@ -41,6 +41,7 @@ import { cn } from "@/lib/utils";
import { PRD_ODDS_MANAGE, PRD_REBATE_MANAGE } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
+import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick";
import type {
AdminPlayTypeRow,
ConfigVersionSummary,
@@ -168,18 +169,26 @@ export function OddsConfigDocScreen({
}, [t]);
useEffect(() => {
- if (list.length === 0 || selectedId !== "") {
+ if (list.length === 0) {
+ if (selectedId !== "") {
+ queueMicrotask(() => {
+ setSelectedId("");
+ setDetail(null);
+ setDraftRows([]);
+ });
+ }
+ return;
+ }
+ if (selectedId !== "" && list.some((x) => String(x.id) === selectedId)) {
return;
}
queueMicrotask(() => {
- const drafts = list.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
- const active = list.find((x) => x.status === "active");
- const pick = drafts[0] ?? active ?? [...list].sort((a, b) => b.id - a.id)[0];
- if (pick) {
- setSelectedId(String(pick.id));
+ const pickId = pickDefaultConfigVersionId(list);
+ if (pickId) {
+ setSelectedId(pickId);
}
});
- }, [list, selectedId]);
+ }, [list, selectedId, setSelectedId]);
useEffect(() => {
if (selectedId === "") {
@@ -292,7 +301,7 @@ export function OddsConfigDocScreen({
toast.success(t("versionActions.saveDraft", { ns: "config" }));
void refreshList();
} catch (e) {
- toast.error(e instanceof LotteryApiBizError ? e.message : t("wallet.saveFailed", { ns: "config" }));
+ toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" }));
} finally {
setSaving(false);
}
diff --git a/src/modules/config/doc/play-config-doc-screen.tsx b/src/modules/config/doc/play-config-doc-screen.tsx
index a1634df..f3d06bc 100644
--- a/src/modules/config/doc/play-config-doc-screen.tsx
+++ b/src/modules/config/doc/play-config-doc-screen.tsx
@@ -14,6 +14,7 @@ import {
putPlayConfigItems,
} from "@/api/admin-config";
import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
import { ConfigChipGroup } from "@/modules/config/config-chip-group";
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
import {
@@ -312,7 +313,7 @@ export function PlayConfigDocScreen() {
toast.success(t("versionActions.saveDraft", { ns: "config" }));
void refreshList();
} catch (e) {
- toast.error(e instanceof LotteryApiBizError ? e.message : t("wallet.saveFailed", { ns: "config" }));
+ toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" }));
} finally {
setSaving(false);
}
@@ -490,32 +491,34 @@ export function PlayConfigDocScreen() {
: t("play.noPlayTypes", { ns: "config" })}
- {
- const enable = checked;
- const action = enable
- ? t("play.batchSwitchEnable", { ns: "config" })
- : t("play.batchSwitchDisable", { ns: "config" });
- requestConfirm({
- title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
- description: t("play.batchSwitchConfirmDescription", {
- ns: "config",
- action,
- group: group.label,
- count: group.total,
- }),
- confirmVariant: enable ? "default" : "destructive",
- onConfirm: () => applyBatchSwitch(group, enable),
- });
- }}
- />
+
+ {
+ const enable = checked === true;
+ const action = enable
+ ? t("play.batchSwitchEnable", { ns: "config" })
+ : t("play.batchSwitchDisable", { ns: "config" });
+ requestConfirm({
+ title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
+ description: t("play.batchSwitchConfirmDescription", {
+ ns: "config",
+ action,
+ group: group.label,
+ count: group.total,
+ }),
+ confirmVariant: enable ? "default" : "destructive",
+ onConfirm: () => applyBatchSwitch(group, enable),
+ });
+ }}
+ />
+
);
})}
diff --git a/src/modules/config/doc/rebate-config-doc-screen.tsx b/src/modules/config/doc/rebate-config-doc-screen.tsx
index d249e97..b61244c 100644
--- a/src/modules/config/doc/rebate-config-doc-screen.tsx
+++ b/src/modules/config/doc/rebate-config-doc-screen.tsx
@@ -20,6 +20,7 @@ import {
ConfigVersionToolbarMetaEmphasis,
} from "@/modules/config/config-version-toolbar-meta";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
+import { Alert, AlertDescription } from "@/components/ui/alert";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
@@ -28,6 +29,7 @@ import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher"
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
+import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick";
import { PRD_REBATE_MANAGE } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
@@ -49,10 +51,38 @@ function rateToPercentUi(rateStr: string): string {
}
function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPlayTypeRow[]): string {
- const codes = typeList.filter((t) => (t.dimension ?? 2) === dim).map((t) => t.play_code);
+ const codes = typeList
+ .filter((t) => (t.dimension ?? 2) === dim)
+ .map((t) => t.play_code)
+ .sort((a, b) => a.localeCompare(b));
const scope = PRIZE_SCOPE_ORDER[0];
- const hit = rows.find((r) => codes.includes(r.play_code) && r.prize_scope === scope);
- return hit ? rateToPercentUi(String(hit.rebate_rate)) : "0";
+ for (const code of codes) {
+ const hit = rows.find((r) => r.play_code === code && r.prize_scope === scope);
+ if (hit) {
+ return rateToPercentUi(String(hit.rebate_rate));
+ }
+ }
+ return "0";
+}
+
+function dimensionDistinctPrimaryScopePercents(
+ dim: 2 | 3 | 4,
+ rows: OddsItemRow[],
+ typeList: AdminPlayTypeRow[],
+): Set {
+ const codes = typeList
+ .filter((t) => (t.dimension ?? 2) === dim)
+ .map((t) => t.play_code)
+ .sort((a, b) => a.localeCompare(b));
+ const scope = PRIZE_SCOPE_ORDER[0];
+ const percents = new Set();
+ for (const code of codes) {
+ const hit = rows.find((r) => r.play_code === code && r.prize_scope === scope);
+ if (hit) {
+ percents.add(rateToPercentUi(String(hit.rebate_rate)));
+ }
+ }
+ return percents;
}
type RebateConfigDocScreenProps = {
@@ -139,18 +169,26 @@ export function RebateConfigDocScreen({
}, [t]);
useEffect(() => {
- if (listRows.length === 0 || selectedId !== "") {
+ if (listRows.length === 0) {
+ if (selectedId !== "") {
+ queueMicrotask(() => {
+ setSelectedId("");
+ setDetail(null);
+ setDraftRows([]);
+ });
+ }
+ return;
+ }
+ if (selectedId !== "" && listRows.some((x) => String(x.id) === selectedId)) {
return;
}
queueMicrotask(() => {
- const drafts = listRows.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
- const active = listRows.find((x) => x.status === "active");
- const pick = drafts[0] ?? active ?? [...listRows].sort((a, b) => b.id - a.id)[0];
- if (pick) {
- setSelectedId(String(pick.id));
+ const pickId = pickDefaultConfigVersionId(listRows);
+ if (pickId) {
+ setSelectedId(pickId);
}
});
- }, [listRows, selectedId]);
+ }, [listRows, selectedId, setSelectedId]);
useEffect(() => {
if (selectedId === "") {
@@ -173,6 +211,18 @@ export function RebateConfigDocScreen({
return m;
}, [types]);
+ const rebateBulkPercentsMixed = useMemo(() => {
+ if (types.length === 0 || draftRows.length === 0) {
+ return false;
+ }
+ for (const dim of [2, 3, 4] as const) {
+ if (dimensionDistinctPrimaryScopePercents(dim, draftRows, types).size > 1) {
+ return true;
+ }
+ }
+ return false;
+ }, [types, draftRows]);
+
const selectedVersionSummary = useMemo(
() => listRows.find((x) => String(x.id) === selectedId) ?? null,
[listRows, selectedId],
@@ -223,7 +273,7 @@ export function RebateConfigDocScreen({
toast.success(t("versionActions.saveDraft", { ns: "config" }));
void refreshList();
} catch (e) {
- toast.error(e instanceof LotteryApiBizError ? e.message : t("wallet.saveFailed", { ns: "config" }));
+ toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" }));
} finally {
setSaving(false);
}
@@ -354,6 +404,11 @@ export function RebateConfigDocScreen({
const fieldsBlock = (
<>
+ {rebateBulkPercentsMixed ? (
+
+ {t("rebate.dimensionRatesMixedHint", { ns: "config" })}
+
+ ) : null}
diff --git a/src/modules/config/doc/risk-cap-doc-screen.tsx b/src/modules/config/doc/risk-cap-doc-screen.tsx
index fc75fe4..fbd8ddf 100644
--- a/src/modules/config/doc/risk-cap-doc-screen.tsx
+++ b/src/modules/config/doc/risk-cap-doc-screen.tsx
@@ -48,6 +48,7 @@ import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
import { PRD_RISK_CAP_MANAGE, PRD_RISK_CAP_VIEW } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
+import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick";
import type {
ConfigVersionSummary,
RiskCapItemRow,
@@ -156,15 +157,24 @@ export function RiskCapDocScreen() {
}, [t]);
useEffect(() => {
- if (list.length === 0 || selectedId !== "") {
+ if (list.length === 0) {
+ if (selectedId !== "") {
+ queueMicrotask(() => {
+ setSelectedId("");
+ setDetail(null);
+ setDraftRows([]);
+ syncDefaultCapFromRows([]);
+ });
+ }
+ return;
+ }
+ if (selectedId !== "" && list.some((x) => String(x.id) === selectedId)) {
return;
}
queueMicrotask(() => {
- const drafts = list.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
- const active = list.find((x) => x.status === "active");
- const pick = drafts[0] ?? active ?? [...list].sort((a, b) => b.id - a.id)[0];
- if (pick) {
- setSelectedId(String(pick.id));
+ const pickId = pickDefaultConfigVersionId(list);
+ if (pickId) {
+ setSelectedId(pickId);
}
});
}, [list, selectedId]);
@@ -242,7 +252,7 @@ export function RiskCapDocScreen() {
toast.success(t("versionActions.saveDraft", { ns: "config" }));
void refreshList();
} catch (e) {
- toast.error(e instanceof LotteryApiBizError ? e.message : t("wallet.saveFailed", { ns: "config" }));
+ toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" }));
} finally {
setSaving(false);
}
diff --git a/src/modules/draws/draws-index-console.tsx b/src/modules/draws/draws-index-console.tsx
index 26c1e31..58158b1 100644
--- a/src/modules/draws/draws-index-console.tsx
+++ b/src/modules/draws/draws-index-console.tsx
@@ -231,8 +231,14 @@ export function DrawsIndexConsole() {
t("batchDelete.success", { count: result.success.length }),
);
}
-
- setSelectedDrawIds(new Set());
+
+ setSelectedDrawIds((prev) => {
+ const next = new Set(prev);
+ for (const id of result.success) {
+ next.delete(id);
+ }
+ return next;
+ });
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("batchDelete.failed"));
diff --git a/src/modules/players/players-console.tsx b/src/modules/players/players-console.tsx
index 48c65c2..49b4bf0 100644
--- a/src/modules/players/players-console.tsx
+++ b/src/modules/players/players-console.tsx
@@ -52,7 +52,7 @@ import {
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { formatAdminMinorUnits } from "@/lib/money";
import { LotteryApiBizError } from "@/types/api/errors";
-import type { AdminPlayerRow } from "@/types/api/admin-player";
+import type { AdminPlayerRow, AdminPlayerWalletRow } from "@/types/api/admin-player";
function playerStatusLabelT(status: number, t: (key: string) => string): string {
if (status === 0) return t("statusNormal");
@@ -61,6 +61,15 @@ function playerStatusLabelT(status: number, t: (key: string) => string): string
return String(status);
}
+function preferredDisplayWallet(row: AdminPlayerRow): AdminPlayerWalletRow | null {
+ const { wallets, default_currency } = row;
+ if (wallets.length === 0) {
+ return null;
+ }
+ const code = default_currency.trim().toUpperCase();
+ return wallets.find((w) => w.currency_code.toUpperCase() === code) ?? wallets[0];
+}
+
const PLAYER_STATUS_OPTIONS = [
{ value: 0, label: "statusNormal" },
{ value: 1, label: "statusFrozen" },
@@ -357,7 +366,9 @@ export function PlayersConsole(): React.ReactElement {
) : (
- items.map((row) => (
+ items.map((row) => {
+ const displayWallet = preferredDisplayWallet(row);
+ return (
#{row.id}
@@ -370,13 +381,16 @@ export function PlayersConsole(): React.ReactElement {
{row.nickname ?? "—"}
{row.default_currency}
- {row.wallets.length > 0
- ? formatAdminMinorUnits(row.wallets[0].balance, row.wallets[0].currency_code)
+ {displayWallet
+ ? formatAdminMinorUnits(displayWallet.balance, displayWallet.currency_code)
: "—"}
- {row.wallets.length > 0
- ? formatAdminMinorUnits(row.wallets[0].available_balance, row.wallets[0].currency_code)
+ {displayWallet
+ ? formatAdminMinorUnits(
+ displayWallet.available_balance,
+ displayWallet.currency_code,
+ )
: "—"}
@@ -451,7 +465,8 @@ export function PlayersConsole(): React.ReactElement {
)}
- ))
+ );
+ })
)}