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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user