feat(i18n): add saveFailed message and dimensionRatesMixedHint to locale files
- Updated English, Nepali, and Chinese locale files to include a new message for "Failed to save configuration". - Added a hint for mixed rebate rates within the same dimension to enhance user understanding. - Improved internationalization support across multiple locales for better user experience in the admin interface.
This commit is contained in:
@@ -63,7 +63,8 @@
|
|||||||
"refreshing": "Refreshing",
|
"refreshing": "Refreshing",
|
||||||
"refresh": "Refresh versions",
|
"refresh": "Refresh versions",
|
||||||
"newDraft": "New draft",
|
"newDraft": "New draft",
|
||||||
"saveDraft": "Save draft"
|
"saveDraft": "Save draft",
|
||||||
|
"saveFailed": "Failed to save configuration"
|
||||||
},
|
},
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"title": "Wallet transfer limit settings",
|
"title": "Wallet transfer limit settings",
|
||||||
@@ -318,6 +319,7 @@
|
|||||||
"deleteFailed": "Delete failed",
|
"deleteFailed": "Delete failed",
|
||||||
"editingVersion": "Editing version v{{version}} · {{status}}",
|
"editingVersion": "Editing version v{{version}} · {{status}}",
|
||||||
"readOnlyHint": "Create a draft before editing rebate.",
|
"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": {
|
"fields": {
|
||||||
"d2": "2D rebate rate (%)",
|
"d2": "2D rebate rate (%)",
|
||||||
"d3": "3D rebate rate (%)",
|
"d3": "3D rebate rate (%)",
|
||||||
|
|||||||
@@ -63,7 +63,8 @@
|
|||||||
"refreshing": "रिफ्रेस हुँदैछ",
|
"refreshing": "रिफ्रेस हुँदैछ",
|
||||||
"refresh": "संस्करण रिफ्रेस",
|
"refresh": "संस्करण रिफ्रेस",
|
||||||
"newDraft": "नयाँ ड्राफ्ट",
|
"newDraft": "नयाँ ड्राफ्ट",
|
||||||
"saveDraft": "ड्राफ्ट सेभ गर्नुहोस्"
|
"saveDraft": "ड्राफ्ट सेभ गर्नुहोस्",
|
||||||
|
"saveFailed": "कन्फिगरेसन सुरक्षित गर्न असफल"
|
||||||
},
|
},
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"title": "वालेट ट्रान्सफर सीमा सेटिङ",
|
"title": "वालेट ट्रान्सफर सीमा सेटिङ",
|
||||||
@@ -318,6 +319,7 @@
|
|||||||
"deleteFailed": "मेटाउन असफल",
|
"deleteFailed": "मेटाउन असफल",
|
||||||
"editingVersion": "सम्पादन भइरहेको संस्करण v{{version}} · {{status}}",
|
"editingVersion": "सम्पादन भइरहेको संस्करण v{{version}} · {{status}}",
|
||||||
"readOnlyHint": "रिबेट सम्पादन गर्नुअघि ड्राफ्ट बनाउनुहोस्।",
|
"readOnlyHint": "रिबेट सम्पादन गर्नुअघि ड्राफ्ट बनाउनुहोस्।",
|
||||||
|
"dimensionRatesMixedHint": "एउटै डाइमेन्शनभित्रका खेलका रिबेट दरहरू उस्तै छैनन्: माथिको तीन फिल्डहरू मुख्य पुरस्कार स्कोपको पहिलो प्ले (क्रम)को उदाहरण मात्र देखाउँछन्; वास्तविक दर टेबुलको लाइन हेर्नुहोस्। थोक इनपुटले डाइमेन्शनभित्रका सबै प्लेहरू एउटै दरले अधिलेखन गर्छ।",
|
||||||
"fields": {
|
"fields": {
|
||||||
"d2": "2D रिबेट दर (%)",
|
"d2": "2D रिबेट दर (%)",
|
||||||
"d3": "3D रिबेट दर (%)",
|
"d3": "3D रिबेट दर (%)",
|
||||||
|
|||||||
@@ -63,7 +63,8 @@
|
|||||||
"refreshing": "刷新中",
|
"refreshing": "刷新中",
|
||||||
"refresh": "刷新版本",
|
"refresh": "刷新版本",
|
||||||
"newDraft": "新建草稿",
|
"newDraft": "新建草稿",
|
||||||
"saveDraft": "保存草稿"
|
"saveDraft": "保存草稿",
|
||||||
|
"saveFailed": "配置保存失败"
|
||||||
},
|
},
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"title": "钱包转账限额配置",
|
"title": "钱包转账限额配置",
|
||||||
@@ -318,6 +319,7 @@
|
|||||||
"deleteFailed": "删除失败",
|
"deleteFailed": "删除失败",
|
||||||
"editingVersion": "当前编辑版本 v{{version}} · {{status}}",
|
"editingVersion": "当前编辑版本 v{{version}} · {{status}}",
|
||||||
"readOnlyHint": "修改回水前请先创建草稿。",
|
"readOnlyHint": "修改回水前请先创建草稿。",
|
||||||
|
"dimensionRatesMixedHint": "检测到同一维度(2D/3D/4D)内各玩法的首奖级回水比例不完全相同:上方三个百分比输入仅展示按玩法编码排序后的第一个有值示例,实际回水请以下方表格各行数据为准;使用批量输入会先按维度覆盖为同一比例。",
|
||||||
"fields": {
|
"fields": {
|
||||||
"d2": "2D 回水比例 (%)",
|
"d2": "2D 回水比例 (%)",
|
||||||
"d3": "3D 回水比例 (%)",
|
"d3": "3D 回水比例 (%)",
|
||||||
|
|||||||
12
src/lib/config-version-auto-pick.ts
Normal file
12
src/lib/config-version-auto-pick.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { PRD_ODDS_MANAGE, PRD_REBATE_MANAGE } from "@/lib/admin-prd";
|
import { PRD_ODDS_MANAGE, PRD_REBATE_MANAGE } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick";
|
||||||
import type {
|
import type {
|
||||||
AdminPlayTypeRow,
|
AdminPlayTypeRow,
|
||||||
ConfigVersionSummary,
|
ConfigVersionSummary,
|
||||||
@@ -168,18 +169,26 @@ export function OddsConfigDocScreen({
|
|||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const drafts = list.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
|
const pickId = pickDefaultConfigVersionId(list);
|
||||||
const active = list.find((x) => x.status === "active");
|
if (pickId) {
|
||||||
const pick = drafts[0] ?? active ?? [...list].sort((a, b) => b.id - a.id)[0];
|
setSelectedId(pickId);
|
||||||
if (pick) {
|
|
||||||
setSelectedId(String(pick.id));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [list, selectedId]);
|
}, [list, selectedId, setSelectedId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedId === "") {
|
if (selectedId === "") {
|
||||||
@@ -292,7 +301,7 @@ export function OddsConfigDocScreen({
|
|||||||
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
||||||
void refreshList();
|
void refreshList();
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
putPlayConfigItems,
|
putPlayConfigItems,
|
||||||
} from "@/api/admin-config";
|
} from "@/api/admin-config";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { ConfigChipGroup } from "@/modules/config/config-chip-group";
|
import { ConfigChipGroup } from "@/modules/config/config-chip-group";
|
||||||
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
|
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
|
||||||
import {
|
import {
|
||||||
@@ -312,7 +313,7 @@ export function PlayConfigDocScreen() {
|
|||||||
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
||||||
void refreshList();
|
void refreshList();
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -490,32 +491,34 @@ export function PlayConfigDocScreen() {
|
|||||||
: t("play.noPlayTypes", { ns: "config" })}
|
: t("play.noPlayTypes", { ns: "config" })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmableSwitch
|
<div className="flex shrink-0 items-center justify-center">
|
||||||
checked={groupOn}
|
<Checkbox
|
||||||
confirmBusy={confirmBusy}
|
checked={groupOn}
|
||||||
disabled={!isDraft || saving || group.total === 0}
|
indeterminate={isPartial}
|
||||||
aria-label={t("play.aria.batchGroupSwitch", {
|
disabled={!isDraft || saving || group.total === 0 || confirmBusy}
|
||||||
ns: "config",
|
aria-label={t("play.aria.batchGroupSwitch", {
|
||||||
group: group.label,
|
ns: "config",
|
||||||
})}
|
group: group.label,
|
||||||
onCheckedChange={(checked) => {
|
})}
|
||||||
const enable = checked;
|
onCheckedChange={(checked) => {
|
||||||
const action = enable
|
const enable = checked === true;
|
||||||
? t("play.batchSwitchEnable", { ns: "config" })
|
const action = enable
|
||||||
: t("play.batchSwitchDisable", { ns: "config" });
|
? t("play.batchSwitchEnable", { ns: "config" })
|
||||||
requestConfirm({
|
: t("play.batchSwitchDisable", { ns: "config" });
|
||||||
title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
|
requestConfirm({
|
||||||
description: t("play.batchSwitchConfirmDescription", {
|
title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
|
||||||
ns: "config",
|
description: t("play.batchSwitchConfirmDescription", {
|
||||||
action,
|
ns: "config",
|
||||||
group: group.label,
|
action,
|
||||||
count: group.total,
|
group: group.label,
|
||||||
}),
|
count: group.total,
|
||||||
confirmVariant: enable ? "default" : "destructive",
|
}),
|
||||||
onConfirm: () => applyBatchSwitch(group, enable),
|
confirmVariant: enable ? "default" : "destructive",
|
||||||
});
|
onConfirm: () => applyBatchSwitch(group, enable),
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
ConfigVersionToolbarMetaEmphasis,
|
ConfigVersionToolbarMetaEmphasis,
|
||||||
} from "@/modules/config/config-version-toolbar-meta";
|
} from "@/modules/config/config-version-toolbar-meta";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
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 { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
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 { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick";
|
||||||
import { PRD_REBATE_MANAGE } from "@/lib/admin-prd";
|
import { PRD_REBATE_MANAGE } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
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 {
|
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 scope = PRIZE_SCOPE_ORDER[0];
|
||||||
const hit = rows.find((r) => codes.includes(r.play_code) && r.prize_scope === scope);
|
for (const code of codes) {
|
||||||
return hit ? rateToPercentUi(String(hit.rebate_rate)) : "0";
|
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<string> {
|
||||||
|
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<string>();
|
||||||
|
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 = {
|
type RebateConfigDocScreenProps = {
|
||||||
@@ -139,18 +169,26 @@ export function RebateConfigDocScreen({
|
|||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const drafts = listRows.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
|
const pickId = pickDefaultConfigVersionId(listRows);
|
||||||
const active = listRows.find((x) => x.status === "active");
|
if (pickId) {
|
||||||
const pick = drafts[0] ?? active ?? [...listRows].sort((a, b) => b.id - a.id)[0];
|
setSelectedId(pickId);
|
||||||
if (pick) {
|
|
||||||
setSelectedId(String(pick.id));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [listRows, selectedId]);
|
}, [listRows, selectedId, setSelectedId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedId === "") {
|
if (selectedId === "") {
|
||||||
@@ -173,6 +211,18 @@ export function RebateConfigDocScreen({
|
|||||||
return m;
|
return m;
|
||||||
}, [types]);
|
}, [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(
|
const selectedVersionSummary = useMemo(
|
||||||
() => listRows.find((x) => String(x.id) === selectedId) ?? null,
|
() => listRows.find((x) => String(x.id) === selectedId) ?? null,
|
||||||
[listRows, selectedId],
|
[listRows, selectedId],
|
||||||
@@ -223,7 +273,7 @@ export function RebateConfigDocScreen({
|
|||||||
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
||||||
void refreshList();
|
void refreshList();
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -354,6 +404,11 @@ export function RebateConfigDocScreen({
|
|||||||
|
|
||||||
const fieldsBlock = (
|
const fieldsBlock = (
|
||||||
<>
|
<>
|
||||||
|
{rebateBulkPercentsMixed ? (
|
||||||
|
<Alert className="border-amber-500/35 bg-amber-500/10 text-amber-950 dark:text-amber-100">
|
||||||
|
<AlertDescription>{t("rebate.dimensionRatesMixedHint", { ns: "config" })}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
<div className="grid gap-5 sm:grid-cols-3">
|
<div className="grid gap-5 sm:grid-cols-3">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>{t("rebate.fields.d2", { ns: "config" })}</Label>
|
<Label>{t("rebate.fields.d2", { ns: "config" })}</Label>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
|
|||||||
import { PRD_RISK_CAP_MANAGE, PRD_RISK_CAP_VIEW } from "@/lib/admin-prd";
|
import { PRD_RISK_CAP_MANAGE, PRD_RISK_CAP_VIEW } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick";
|
||||||
import type {
|
import type {
|
||||||
ConfigVersionSummary,
|
ConfigVersionSummary,
|
||||||
RiskCapItemRow,
|
RiskCapItemRow,
|
||||||
@@ -156,15 +157,24 @@ export function RiskCapDocScreen() {
|
|||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const drafts = list.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
|
const pickId = pickDefaultConfigVersionId(list);
|
||||||
const active = list.find((x) => x.status === "active");
|
if (pickId) {
|
||||||
const pick = drafts[0] ?? active ?? [...list].sort((a, b) => b.id - a.id)[0];
|
setSelectedId(pickId);
|
||||||
if (pick) {
|
|
||||||
setSelectedId(String(pick.id));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [list, selectedId]);
|
}, [list, selectedId]);
|
||||||
@@ -242,7 +252,7 @@ export function RiskCapDocScreen() {
|
|||||||
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
||||||
void refreshList();
|
void refreshList();
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,8 +231,14 @@ export function DrawsIndexConsole() {
|
|||||||
t("batchDelete.success", { count: result.success.length }),
|
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();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("batchDelete.failed"));
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("batchDelete.failed"));
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import {
|
|||||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
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 {
|
function playerStatusLabelT(status: number, t: (key: string) => string): string {
|
||||||
if (status === 0) return t("statusNormal");
|
if (status === 0) return t("statusNormal");
|
||||||
@@ -61,6 +61,15 @@ function playerStatusLabelT(status: number, t: (key: string) => string): string
|
|||||||
return String(status);
|
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 = [
|
const PLAYER_STATUS_OPTIONS = [
|
||||||
{ value: 0, label: "statusNormal" },
|
{ value: 0, label: "statusNormal" },
|
||||||
{ value: 1, label: "statusFrozen" },
|
{ value: 1, label: "statusFrozen" },
|
||||||
@@ -357,7 +366,9 @@ export function PlayersConsole(): React.ReactElement {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
items.map((row) => (
|
items.map((row) => {
|
||||||
|
const displayWallet = preferredDisplayWallet(row);
|
||||||
|
return (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
<TableCell className="tabular-nums">#{row.id}</TableCell>
|
<TableCell className="tabular-nums">#{row.id}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -370,13 +381,16 @@ export function PlayersConsole(): React.ReactElement {
|
|||||||
<TableCell>{row.nickname ?? "—"}</TableCell>
|
<TableCell>{row.nickname ?? "—"}</TableCell>
|
||||||
<TableCell>{row.default_currency}</TableCell>
|
<TableCell>{row.default_currency}</TableCell>
|
||||||
<TableCell className="whitespace-nowrap text-center tabular-nums text-xs">
|
<TableCell className="whitespace-nowrap text-center tabular-nums text-xs">
|
||||||
{row.wallets.length > 0
|
{displayWallet
|
||||||
? formatAdminMinorUnits(row.wallets[0].balance, row.wallets[0].currency_code)
|
? formatAdminMinorUnits(displayWallet.balance, displayWallet.currency_code)
|
||||||
: "—"}
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="whitespace-nowrap text-center tabular-nums text-xs">
|
<TableCell className="whitespace-nowrap text-center tabular-nums text-xs">
|
||||||
{row.wallets.length > 0
|
{displayWallet
|
||||||
? formatAdminMinorUnits(row.wallets[0].available_balance, row.wallets[0].currency_code)
|
? formatAdminMinorUnits(
|
||||||
|
displayWallet.available_balance,
|
||||||
|
displayWallet.currency_code,
|
||||||
|
)
|
||||||
: "—"}
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -451,7 +465,8 @@ export function PlayersConsole(): React.ReactElement {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
Reference in New Issue
Block a user