feat(api, i18n): add agent_node_id to various admin queries and enhance multi-language support

Introduced the agent_node_id field in AdminDrawListQuery, AdminPlayerListQuery, AdminSettlementBatchListQuery, TicketItemsListQuery, and TransferOrderListQuery to improve filtering capabilities. Updated the admin-breadcrumb and admin-sidebar components to include new translations for agent-related terms in English, Nepali, and Chinese, enhancing the overall user experience and multi-language support across the admin interface.
This commit is contained in:
2026-06-02 14:37:08 +08:00
parent a4e7a2d228
commit b15e377187
105 changed files with 5305 additions and 1596 deletions

View File

@@ -32,11 +32,14 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { resolveAdminPlayTypeDisplayName } from "@/lib/admin-play-types";
import { ensureAdminPlayTypesLoaded, resolveAdminPlayTypeDisplayName } from "@/lib/admin-play-types";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { cn } from "@/lib/utils";
import { PRD_ODDS_MANAGE, PRD_REBATE_MANAGE } from "@/lib/admin-prd";
@@ -106,6 +109,7 @@ export function OddsConfigDocScreen({
onVersionIdChange,
}: OddsConfigDocScreenProps) {
const { t, i18n } = useTranslation(["config", "adminUsers", "common"]);
const tRef = useTranslationRef(["config", "common"]);
const profile = useAdminProfile();
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_ODDS_MANAGE, PRD_REBATE_MANAGE]);
const formatDt = useAdminDateTimeFormatter();
@@ -144,15 +148,16 @@ export function OddsConfigDocScreen({
const refreshTypes = useCallback(async () => {
setLoadingTypes(true);
try {
const d = await getAdminPlayTypes();
setTypes(d.items);
setTypes(await ensureAdminPlayTypesLoaded(getAdminPlayTypes));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
setTypes([]);
} finally {
setLoadingTypes(false);
}
}, [t]);
}, []);
const refreshList = useCallback(async () => {
setLoadingList(true);
@@ -161,23 +166,21 @@ export function OddsConfigDocScreen({
const d = await getAllConfigVersions(getOddsVersions);
setList(d.items);
} catch (e) {
const msg = e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" });
const msg =
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" });
setError(msg);
setList([]);
} finally {
setLoadingList(false);
}
}, [t]);
}, []);
useEffect(() => {
useAsyncEffect(() => {
if (workspace) {
return;
}
queueMicrotask(() => {
void refreshTypes();
void refreshList();
});
}, [refreshTypes, refreshList, workspace]);
void Promise.all([refreshTypes(), refreshList()]);
}, [workspace]);
const loadDetail = useCallback(async (id: number) => {
setLoadingDetail(true);
@@ -186,13 +189,15 @@ export function OddsConfigDocScreen({
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
setDetail(null);
setDraftRows([]);
} finally {
setLoadingDetail(false);
}
}, [t]);
}, []);
useEffect(() => {
if (workspace) {
@@ -638,9 +643,11 @@ export function OddsConfigDocScreen({
{resolvedError ? <p className="text-sm text-destructive">{resolvedError}</p> : null}
{resolvedLoadingDetail || resolvedLoadingTypes ? (
<p className={cn("text-center text-sm text-muted-foreground", mergedLayout ? "py-6" : "py-8")}>
{t("odds.loadingDetails", { ns: "config" })}
</p>
<AdminLoadingState
className={cn(mergedLayout ? "py-6" : "py-8")}
minHeight="6rem"
label={t("odds.loadingDetails", { ns: "config" })}
/>
) : resolvedPlayCode ? (
<div className={cn(!mergedLayout && embedded ? "rounded-xl border border-border/60 bg-card p-4" : undefined)}>
<div className="grid grid-cols-2 gap-x-4 gap-y-4 sm:grid-cols-3">

View File

@@ -41,11 +41,14 @@ import {
TableRow,
} from "@/components/ui/table";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
import { PRD_PLAY_SWITCH_MANAGE } from "@/lib/admin-prd";
@@ -138,6 +141,7 @@ function buildPlayConfigSavePayload(
export function PlayConfigDocScreen() {
const { t } = useTranslation(["config", "adminUsers", "common"]);
const tRef = useTranslationRef(["config", "common"]);
const { request: requestConfirm, ConfirmDialog, busy: confirmBusy } = useConfirmAction();
const profile = useAdminProfile();
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_PLAY_SWITCH_MANAGE]);
@@ -165,19 +169,18 @@ export function PlayConfigDocScreen() {
draftId !== null && d.items.some((x) => String(x.id) === draftId) ? null : draftId,
);
} catch (e) {
const msg = e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" });
const msg =
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" });
setError(msg);
setList([]);
} finally {
setLoadingList(false);
}
}, [t]);
}, []);
useEffect(() => {
queueMicrotask(() => {
void refreshList();
});
}, [refreshList]);
useAsyncEffect(() => {
void refreshList();
}, []);
const loadDetail = useCallback(async (id: number) => {
const requestSeq = detailRequestSeq.current + 1;
@@ -196,7 +199,9 @@ export function PlayConfigDocScreen() {
if (detailRequestSeq.current !== requestSeq) {
return;
}
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
setDetail(null);
setDraftRows([]);
} finally {
@@ -204,7 +209,7 @@ export function PlayConfigDocScreen() {
setLoadingDetail(false);
}
}
}, [t]);
}, []);
useEffect(() => {
if (list.length === 0) {
@@ -538,7 +543,7 @@ export function PlayConfigDocScreen() {
{error ? <p className="text-sm text-destructive">{error}</p> : null}
{loadingDetail ? (
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
<AdminLoadingState minHeight="6rem" className="py-6" />
) : (
<Table>
<TableHeader>

View File

@@ -19,7 +19,14 @@ import {
ConfigVersionToolbarMeta,
ConfigVersionToolbarMetaEmphasis,
} from "@/modules/config/config-version-toolbar-meta";
import { getAdminSettings, updateAdminSetting } from "@/api/admin-settings";
import { updateAdminSetting } from "@/api/admin-settings";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import {
loadApplyRebateToPayoutSetting,
setCachedApplyRebateToPayoutSetting,
} from "@/lib/admin-settlement-settings-cache";
import { ensureAdminPlayTypesLoaded } from "@/lib/admin-play-types";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
@@ -33,6 +40,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
@@ -58,7 +66,6 @@ import {
} from "@/modules/config/doc/odds-rebate-rates";
import { PRIZE_SCOPE_ORDER } from "@/modules/config/doc/prize-scopes";
const SETTLEMENT_GROUP = "settlement";
const APPLY_REBATE_TO_PAYOUT_KEY = "settlement.apply_rebate_to_payout";
function dimensionDistinctPrimaryScopePercents(
@@ -98,6 +105,7 @@ export function RebateConfigDocScreen({
onVersionIdChange,
}: RebateConfigDocScreenProps) {
const { t } = useTranslation(["config", "common"]);
const tRef = useTranslationRef(["config", "common"]);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const profile = useAdminProfile();
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_REBATE_MANAGE]);
@@ -137,54 +145,52 @@ export function RebateConfigDocScreen({
const refreshTypes = useCallback(async () => {
try {
const d = await getAdminPlayTypes();
setTypes(d.items);
setTypes(await ensureAdminPlayTypesLoaded(getAdminPlayTypes));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
setTypes([]);
}
}, [t]);
}, []);
const refreshList = useCallback(async () => {
try {
const d = await getAllConfigVersions(getOddsVersions);
setListRows(d.items);
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
setListRows([]);
}
}, [t]);
}, []);
const loadWinEnjoySetting = useCallback(async () => {
setWinEnjoyLoading(true);
try {
const res = await getAdminSettings(SETTLEMENT_GROUP);
const hit = res.items.find((item) => item.key === APPLY_REBATE_TO_PAYOUT_KEY);
setApplyRebateToPayout(Boolean(hit?.value));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
} finally {
setWinEnjoyLoading(false);
}
}, [t]);
useEffect(() => {
useAsyncEffect(() => {
if (workspace) {
return;
}
queueMicrotask(async () => {
void (async () => {
setLoading(true);
await refreshTypes();
await refreshList();
await Promise.all([refreshTypes(), refreshList()]);
setLoading(false);
});
}, [refreshTypes, refreshList, workspace]);
})();
}, [workspace]);
useEffect(() => {
queueMicrotask(() => {
void loadWinEnjoySetting();
});
}, [loadWinEnjoySetting]);
useAsyncEffect(() => {
void (async () => {
setWinEnjoyLoading(true);
try {
setApplyRebateToPayout(await loadApplyRebateToPayoutSetting());
} catch (e) {
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
} finally {
setWinEnjoyLoading(false);
}
})();
}, []);
useEffect(() => {
if (!workspace) {
@@ -202,6 +208,7 @@ export function RebateConfigDocScreen({
setWinEnjoySaving(true);
try {
await updateAdminSetting(APPLY_REBATE_TO_PAYOUT_KEY, checked);
setCachedApplyRebateToPayoutSetting(checked);
setApplyRebateToPayout(checked);
toast.success(t("rebate.winEnjoy.saveSuccess", { ns: "config" }));
} catch (e) {
@@ -214,8 +221,7 @@ export function RebateConfigDocScreen({
const loadDetail = useCallback(async (id: number) => {
setLoadingDetail(true);
try {
const pt = await getAdminPlayTypes();
const typeList = pt.items;
const typeList = await ensureAdminPlayTypesLoaded(getAdminPlayTypes);
setTypes(typeList);
const d = await getOddsVersion(id);
const rows = d.items.map((it) => ({ ...it }));
@@ -225,13 +231,15 @@ export function RebateConfigDocScreen({
setP3(inferRebatePercentFromDimension(3, rows, typeList));
setP4(inferRebatePercentFromDimension(4, rows, typeList));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
setDetail(null);
setDraftRows([]);
} finally {
setLoadingDetail(false);
}
}, [t]);
}, []);
useEffect(() => {
if (workspace) {
@@ -614,7 +622,7 @@ export function RebateConfigDocScreen({
) : null}
{resolvedLoading || resolvedLoadingDetail ? (
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
<AdminLoadingState minHeight="6rem" className="py-6" />
) : null}
</>
);

View File

@@ -32,6 +32,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
import { RiskCapRuntimePanel } from "@/modules/config/risk-cap-runtime-panel";
import {
@@ -45,7 +46,9 @@ import {
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
import { PRD_RISK_CAP_MANAGE, PRD_RISK_CAP_VIEW } from "@/lib/admin-prd";
@@ -86,6 +89,7 @@ function defaultRiskRowFromAmount(amount: number): DraftRiskRow {
export function RiskCapDocScreen() {
const { t } = useTranslation(["config", "adminUsers", "common"]);
const tRef = useTranslationRef(["config", "common"]);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const profile = useAdminProfile();
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_RISK_CAP_MANAGE]);
@@ -113,19 +117,18 @@ export function RiskCapDocScreen() {
const d = await getAllConfigVersions(getRiskCapVersions);
setList(d.items);
} catch (e) {
const msg = e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" });
const msg =
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" });
setError(msg);
setList([]);
} finally {
setLoadingList(false);
}
}, [t]);
}, []);
useEffect(() => {
queueMicrotask(() => {
void refreshList();
});
}, [refreshList]);
useAsyncEffect(() => {
void refreshList();
}, []);
function syncDefaultCapFromRows(rows: DraftRiskRow[]) {
const defaultRow = rows.find(isDefaultRiskRow);
@@ -151,14 +154,16 @@ export function RiskCapDocScreen() {
setDraftRows(mapped);
syncDefaultCapFromRows(mapped);
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
toast.error(
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
);
setDetail(null);
setDraftRows([]);
syncDefaultCapFromRows([]);
} finally {
setLoadingDetail(false);
}
}, [t]);
}, []);
useEffect(() => {
if (list.length === 0) {
@@ -498,7 +503,7 @@ export function RiskCapDocScreen() {
}
>
{loadingDetail ? (
<p className="text-sm text-muted-foreground">{t("riskCap.loadingDetails", { ns: "config" })}</p>
<AdminLoadingState minHeight="6rem" className="py-4" label={t("riskCap.loadingDetails", { ns: "config" })} />
) : specialRows.length === 0 ? (
<p className="text-sm text-muted-foreground">{t("riskCap.noDetailRows", { ns: "config" })}</p>
) : (

View File

@@ -1,13 +1,12 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
getAdminSettings,
updateAdminSetting,
} from "@/api/admin-settings";
import { getAdminSettings, updateAdminSettingsBatch } from "@/api/admin-settings";
import { useOptionalAdminSettingsData } from "@/modules/settings/admin-settings-data-context";
import { WALLET_GROUP, WALLET_KEYS } from "@/modules/settings/settings-keys";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { Button } from "@/components/ui/button";
import { ConfigDocPage } from "@/modules/config/config-doc-page";
@@ -15,15 +14,6 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { LotteryApiBizError } from "@/types/api/errors";
const WALLET_GROUP = "wallet";
const KEYS = {
IN_MIN: "wallet.transfer_in_min_minor",
IN_MAX: "wallet.transfer_in_max_minor",
OUT_MIN: "wallet.transfer_out_min_minor",
OUT_MAX: "wallet.transfer_out_max_minor",
} as const;
function minorUnitsToDisplay(n: unknown, decimals = 2): string {
const num = Number(n);
if (!Number.isFinite(num)) return "";
@@ -43,12 +33,24 @@ interface Draft {
outMax: string;
}
function draftFromKv(kv: Record<string, unknown>): Draft {
return {
inMin: minorUnitsToDisplay(kv[WALLET_KEYS.IN_MIN] ?? 100),
inMax: minorUnitsToDisplay(kv[WALLET_KEYS.IN_MAX] ?? 0),
outMin: minorUnitsToDisplay(kv[WALLET_KEYS.OUT_MIN] ?? 100),
outMax: minorUnitsToDisplay(kv[WALLET_KEYS.OUT_MAX] ?? 0),
};
}
type WalletConfigDocScreenProps = {
embedded?: boolean;
};
export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScreenProps) {
const { t } = useTranslation(["config", "adminUsers", "common"]);
const tRef = useRef(t);
tRef.current = t;
const shared = useOptionalAdminSettingsData();
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const [draft, setDraft] = useState<Draft>({
inMin: "",
@@ -57,55 +59,81 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
outMax: "",
});
const [saved, setSaved] = useState<Draft>({ inMin: "", inMax: "", outMin: "", outMax: "" });
const [loading, setLoading] = useState(true);
const [standaloneLoading, setStandaloneLoading] = useState(!embedded);
const [saving, setSaving] = useState(false);
const [dirty, setDirty] = useState(false);
const dirty =
draft.inMin !== saved.inMin ||
draft.inMax !== saved.inMax ||
draft.outMin !== saved.outMin ||
draft.outMax !== saved.outMax;
const load = useCallback(async () => {
setLoading(true);
const loading = embedded ? (shared?.loading ?? true) : standaloneLoading;
const loadStandalone = useCallback(async () => {
setStandaloneLoading(true);
try {
const res = await getAdminSettings(WALLET_GROUP);
const kv: Record<string, unknown> = {};
for (const item of res.items) {
kv[item.key] = item.value;
}
const d: Draft = {
inMin: minorUnitsToDisplay(kv[KEYS.IN_MIN] ?? 100),
inMax: minorUnitsToDisplay(kv[KEYS.IN_MAX] ?? 0),
outMin: minorUnitsToDisplay(kv[KEYS.OUT_MIN] ?? 100),
outMax: minorUnitsToDisplay(kv[KEYS.OUT_MAX] ?? 0),
};
const d = draftFromKv(kv);
setDraft(d);
setSaved(d);
setDirty(false);
} catch {
toast.error(t("wallet.loadFailed", { ns: "config" }));
toast.error(tRef.current("wallet.loadFailed", { ns: "config" }));
} finally {
setLoading(false);
setStandaloneLoading(false);
}
}, [t]);
}, []);
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
if (!embedded) {
void loadStandalone();
}
}, [embedded, loadStandalone]);
useEffect(() => {
if (!embedded || shared?.kv === null || shared?.kv === undefined) {
return;
}
const d = draftFromKv(shared.kv);
setDraft(d);
setSaved(d);
}, [embedded, shared?.kv]);
const handleChange = (field: keyof Draft, value: string) => {
setDraft((prev) => ({ ...prev, [field]: value }));
setDirty(true);
};
const handleSave = async () => {
const items = [];
if (draft.inMin !== saved.inMin) {
items.push({ key: WALLET_KEYS.IN_MIN, value: displayToMinorUnits(draft.inMin) });
}
if (draft.inMax !== saved.inMax) {
items.push({ key: WALLET_KEYS.IN_MAX, value: displayToMinorUnits(draft.inMax) });
}
if (draft.outMin !== saved.outMin) {
items.push({ key: WALLET_KEYS.OUT_MIN, value: displayToMinorUnits(draft.outMin) });
}
if (draft.outMax !== saved.outMax) {
items.push({ key: WALLET_KEYS.OUT_MAX, value: displayToMinorUnits(draft.outMax) });
}
if (items.length === 0) {
return;
}
setSaving(true);
try {
await updateAdminSetting(KEYS.IN_MIN, displayToMinorUnits(draft.inMin));
await updateAdminSetting(KEYS.IN_MAX, displayToMinorUnits(draft.inMax));
await updateAdminSetting(KEYS.OUT_MIN, displayToMinorUnits(draft.outMin));
await updateAdminSetting(KEYS.OUT_MAX, displayToMinorUnits(draft.outMax));
await updateAdminSettingsBatch(items);
const updates: Record<string, unknown> = {};
for (const item of items) {
updates[item.key] = item.value;
}
shared?.patchKv(updates);
toast.success(t("wallet.saveSuccess", { ns: "config" }));
setSaved(draft);
setDirty(false);
} catch (error) {
toast.error(
error instanceof LotteryApiBizError ? error.message : t("wallet.saveFailed", { ns: "config" }),
@@ -186,13 +214,7 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
</Button>
{dirty && (
<Button
variant="outline"
onClick={() => {
setDraft(saved);
setDirty(false);
}}
>
<Button variant="outline" onClick={() => setDraft(saved)} disabled={saving}>
{t("wallet.discard", { ns: "config" })}
</Button>
)}