feat(api, ui, i18n): 增强奖池管理与钱包功能

新增奖池余额调整与调整记录查询相关 API,提升后台对奖池的管理与控制能力。
更新奖池与钱包相关多语言文案,新增余额调整与转账完成提示信息,提升用户理解与反馈体验。
优化奖池管理相关 UI 组件,新增余额调整功能并改进页面布局,提升操作易用性。
重构相关组件以整合新功能,并进一步优化后台管理界面的整体用户体验。
This commit is contained in:
2026-05-26 14:59:41 +08:00
parent 60271d87fb
commit eb83bcf360
23 changed files with 881 additions and 228 deletions

View File

@@ -42,6 +42,7 @@ 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 { OddsConfigWorkspace } from "@/modules/config/use-odds-config-workspace";
import type {
AdminPlayTypeRow,
ConfigVersionSummary,
@@ -82,13 +83,16 @@ function filterTypes(tab: CatTab, types: AdminPlayTypeRow[]): AdminPlayTypeRow[]
type OddsConfigDocScreenProps = {
/** 嵌入「赔率与回水」合并页时去掉外层 ConfigDocPage */
embedded?: boolean;
/** 与回水分区共用版本选择(合并页) */
/** 合并页共享数据层(避免与回水区块重复拉取版本详情 */
workspace?: OddsConfigWorkspace;
/** 与回水分区共用版本选择(无 workspace 时) */
versionId?: string;
onVersionIdChange?: (id: string) => void;
};
export function OddsConfigDocScreen({
embedded = false,
workspace,
versionId: controlledVersionId,
onVersionIdChange,
}: OddsConfigDocScreenProps) {
@@ -99,8 +103,8 @@ export function OddsConfigDocScreen({
const [types, setTypes] = useState<AdminPlayTypeRow[]>([]);
const [list, setList] = useState<ConfigVersionSummary[]>([]);
const [internalSelectedId, setInternalSelectedId] = useState("");
const selectedId = controlledVersionId ?? internalSelectedId;
const setSelectedId = onVersionIdChange ?? setInternalSelectedId;
const selectedId = workspace?.selectedId ?? controlledVersionId ?? internalSelectedId;
const setSelectedId = workspace?.setSelectedId ?? onVersionIdChange ?? setInternalSelectedId;
const [detail, setDetail] = useState<OddsVersionDetail | null>(null);
const [draftRows, setDraftRows] = useState<OddsItemRow[]>([]);
const [loadingTypes, setLoadingTypes] = useState(true);
@@ -109,6 +113,16 @@ export function OddsConfigDocScreen({
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const resolvedTypes = workspace?.types ?? types;
const resolvedList = workspace?.list ?? list;
const resolvedDetail = workspace?.detail ?? detail;
const resolvedDraftRows = workspace?.draftRows ?? draftRows;
const setResolvedDraftRows = workspace?.setDraftRows ?? setDraftRows;
const resolvedLoadingTypes = workspace?.loadingTypes ?? loadingTypes;
const resolvedLoadingList = workspace?.loadingList ?? loadingList;
const resolvedLoadingDetail = workspace?.loadingDetail ?? loadingDetail;
const resolvedError = workspace?.error ?? error;
const [catTab, setCatTab] = useState<CatTab>("all");
/** User-selected play type. Empty means none selected yet and falls back to the first item in the category. */
const [playCode, setPlayCode] = useState<string>("");
@@ -147,11 +161,14 @@ export function OddsConfigDocScreen({
}, [t]);
useEffect(() => {
if (workspace) {
return;
}
queueMicrotask(() => {
void refreshTypes();
void refreshList();
});
}, [refreshTypes, refreshList]);
}, [refreshTypes, refreshList, workspace]);
const loadDetail = useCallback(async (id: number) => {
setLoadingDetail(true);
@@ -169,6 +186,9 @@ export function OddsConfigDocScreen({
}, [t]);
useEffect(() => {
if (workspace) {
return;
}
if (list.length === 0) {
if (selectedId !== "") {
queueMicrotask(() => {
@@ -188,9 +208,12 @@ export function OddsConfigDocScreen({
setSelectedId(pickId);
}
});
}, [list, selectedId, setSelectedId]);
}, [list, selectedId, setSelectedId, workspace]);
useEffect(() => {
if (workspace) {
return;
}
if (selectedId === "") {
return;
}
@@ -201,11 +224,14 @@ export function OddsConfigDocScreen({
queueMicrotask(() => {
void loadDetail(id);
});
}, [selectedId, loadDetail]);
}, [selectedId, loadDetail, workspace]);
const sortedTypes = useMemo(
() => [...types].sort((a, b) => a.sort_order - b.sort_order || a.play_code.localeCompare(b.play_code)),
[types],
() =>
[...resolvedTypes].sort(
(a, b) => a.sort_order - b.sort_order || a.play_code.localeCompare(b.play_code),
),
[resolvedTypes],
);
const filteredTypes = useMemo(() => filterTypes(catTab, sortedTypes), [catTab, sortedTypes]);
@@ -221,11 +247,11 @@ export function OddsConfigDocScreen({
}, [filteredTypes, playCode]);
const selectedVersionSummary = useMemo(
() => list.find((x) => String(x.id) === selectedId) ?? null,
[list, selectedId],
() => resolvedList.find((x) => String(x.id) === selectedId) ?? null,
[resolvedList, selectedId],
);
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
const isSelectedDetail = resolvedDetail !== null && String(resolvedDetail.id) === selectedId;
const selectedStatus = isSelectedDetail ? resolvedDetail.status : selectedVersionSummary?.status;
const isDraft = selectedStatus === "draft";
const canEditDraft = isDraft && canManage;
@@ -234,14 +260,17 @@ export function OddsConfigDocScreen({
if (!resolvedPlayCode) {
return rows;
}
for (const scope of PRIZE_SCOPE_ORDER) {
const hit = draftRows.find((r) => r.play_code === resolvedPlayCode && r.prize_scope === scope);
if (hit) {
rows[scope] = hit;
for (const row of resolvedDraftRows) {
if (row.play_code !== resolvedPlayCode) {
continue;
}
const scope = row.prize_scope as PrizeScopeCode;
if (PRIZE_SCOPE_ORDER.includes(scope)) {
rows[scope] = row;
}
}
return rows;
}, [draftRows, resolvedPlayCode]);
}, [resolvedDraftRows, resolvedPlayCode]);
const rebatePercentUi = useMemo(() => {
const first = PRIZE_SCOPE_ORDER.map((s) => scopeRows[s]).find(Boolean);
@@ -256,11 +285,11 @@ export function OddsConfigDocScreen({
}, [scopeRows]);
function rowIndex(play_code: string, prize_scope: string): number {
return draftRows.findIndex((r) => r.play_code === play_code && r.prize_scope === prize_scope);
return resolvedDraftRows.findIndex((r) => r.play_code === play_code && r.prize_scope === prize_scope);
}
function updateOddsRow(idx: number, patch: Partial<OddsItemRow>) {
setDraftRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r)));
setResolvedDraftRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r)));
}
function updateOddsForScope(scope: PrizeScopeCode, patch: Partial<OddsItemRow>) {
@@ -273,7 +302,7 @@ export function OddsConfigDocScreen({
function setRebateForPlayPercent(percentStr: string) {
const p = Number.parseFloat(percentStr);
const rate = Number.isFinite(p) ? p / 100 : 0;
setDraftRows((prev) =>
setResolvedDraftRows((prev) =>
prev.map((r) =>
r.play_code === resolvedPlayCode ? { ...r, rebate_rate: String(rate) } : r,
),
@@ -281,12 +310,12 @@ export function OddsConfigDocScreen({
}
async function handleSave() {
if (!detail || !canEditDraft) {
if (!resolvedDetail || !canEditDraft) {
return;
}
setSaving(true);
try {
const payload = draftRows.map((r) => ({
const payload = resolvedDraftRows.map((r) => ({
play_code: r.play_code,
prize_scope: r.prize_scope,
odds_value: r.odds_value,
@@ -295,11 +324,15 @@ export function OddsConfigDocScreen({
currency_code: r.currency_code,
extra_config_json: r.extra_config_json,
}));
const d = await putOddsItems(detail.id, payload);
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
const d = await putOddsItems(resolvedDetail.id, payload);
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
}
toast.success(t("versionActions.saveDraft", { ns: "config" }));
void refreshList();
void (workspace?.refreshList() ?? refreshList());
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" }));
} finally {
@@ -308,16 +341,20 @@ export function OddsConfigDocScreen({
}
async function handlePublish() {
if (!detail || !canEditDraft) {
if (!resolvedDetail || !canEditDraft) {
return;
}
setSaving(true);
try {
const d = await publishOddsVersion(detail.id);
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
const d = await publishOddsVersion(resolvedDetail.id);
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
}
toast.success(t("versionActions.publishCurrent", { ns: "config" }));
void refreshList();
void (workspace?.refreshList() ?? refreshList());
setSelectedId(String(d.id));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.publishFailed", { ns: "config" }));
@@ -327,11 +364,11 @@ export function OddsConfigDocScreen({
}
async function requestPublishConfirm() {
if (!detail || !canEditDraft) {
if (!resolvedDetail || !canEditDraft) {
return;
}
const active = list.find((x) => x.status === "active");
if (active && active.id !== detail.id) {
const active = resolvedList.find((x) => x.status === "active");
if (active && active.id !== resolvedDetail.id) {
try {
const d = await getOddsVersion(active.id);
setActiveCompareRows(d.items);
@@ -347,16 +384,20 @@ export function OddsConfigDocScreen({
async function handleNewDraft() {
setSaving(true);
try {
const active = list.find((x) => x.status === "active");
const active = resolvedList.find((x) => x.status === "active");
const d = await postOddsVersion({
reason: `draft ${new Date().toISOString()}`,
clone_from_version_id: active?.id ?? null,
});
toast.success(t("odds.createDraftSuccess", { ns: "config", version: d.version_no }));
await refreshList();
await (workspace?.refreshList() ?? refreshList());
setSelectedId(String(d.id));
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
}
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.createDraftFailed", { ns: "config" }));
} finally {
@@ -381,10 +422,14 @@ export function OddsConfigDocScreen({
version: d.version_no,
}),
);
await refreshList();
await (workspace?.refreshList() ?? refreshList());
setSelectedId(String(d.id));
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(d.items.map((it) => ({ ...it })));
}
setRollbackOpen(false);
setRollbackTarget(null);
} catch (e) {
@@ -394,13 +439,13 @@ export function OddsConfigDocScreen({
}
}
const activeHead = list.find((x) => x.status === "active");
const activeHead = resolvedList.find((x) => x.status === "active");
async function handleDeleteVersion(row: ConfigVersionSummary) {
try {
await deleteOddsVersion(row.id);
toast.success(t("versionSwitcher.delete", { ns: "config" }));
await refreshList();
await (workspace?.refreshList() ?? refreshList());
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.deleteFailed", { ns: "config" }));
throw e;
@@ -413,14 +458,14 @@ export function OddsConfigDocScreen({
}
const publishDiffRows = useMemo(() => {
if (!detail) {
if (!resolvedDetail) {
return [];
}
const selectedPlay = resolvedPlayCode;
return PRIZE_SCOPE_ORDER.map((scope) => {
const next = draftRows.find((r) => r.play_code === selectedPlay && r.prize_scope === scope);
const next = resolvedDraftRows.find((r) => r.play_code === selectedPlay && r.prize_scope === scope);
const old = activeCompareRows.find((r) => r.play_code === selectedPlay && r.prize_scope === scope);
return {
scope,
@@ -429,7 +474,7 @@ export function OddsConfigDocScreen({
newValue: next?.odds_value ?? null,
};
});
}, [activeCompareRows, detail, draftRows, resolvedPlayCode, t, i18n.language]);
}, [activeCompareRows, resolvedDetail, resolvedDraftRows, resolvedPlayCode, t, i18n.language]);
const catTabs: { id: CatTab; label: string }[] = [
{ id: "all", label: t("odds.tabs.all", { ns: "config" }) },
@@ -477,10 +522,10 @@ export function OddsConfigDocScreen({
className={embedded ? "rounded-none border-0 shadow-none" : undefined}
switcher={
<ConfigVersionSwitcher
versions={list}
versions={resolvedList}
selectedId={selectedId}
onSelectedIdChange={setSelectedId}
loading={loadingList}
loading={resolvedLoadingList}
sheetTitle={`${t("nav.items.odds", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
sheetDescription={embedded ? undefined : t("odds.sheetDescription", { ns: "config" })}
onDeleteVersion={handleDeleteVersion}
@@ -492,8 +537,8 @@ export function OddsConfigDocScreen({
<ConfigVersionActions
isDraft={isDraft}
canManage={canManage}
loadingList={loadingList}
loadingDetail={loadingDetail}
loadingList={resolvedLoadingList}
loadingDetail={resolvedLoadingDetail}
saving={saving}
onRefresh={() => void refreshList()}
onNewDraft={() => void handleNewDraft()}
@@ -502,7 +547,7 @@ export function OddsConfigDocScreen({
/>
}
footer={
!detail ? null : (
!resolvedDetail ? null : (
<ConfigVersionToolbarMeta emphasis={!isDraft}>
<span>
{t("odds.activeVersionPrefix", { ns: "config" })}
@@ -530,9 +575,9 @@ export function OddsConfigDocScreen({
const mainBlock = (
<>
{error ? <p className="text-sm text-destructive">{error}</p> : null}
{resolvedError ? <p className="text-sm text-destructive">{resolvedError}</p> : null}
{loadingDetail || loadingTypes ? (
{resolvedLoadingDetail || resolvedLoadingTypes ? (
<p className="py-8 text-center text-sm text-muted-foreground">
{t("odds.loadingDetails", { ns: "config" })}
</p>

View File

@@ -40,6 +40,7 @@ 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 type { OddsConfigWorkspace } from "@/modules/config/use-odds-config-workspace";
import { PRD_REBATE_MANAGE, PRD_WALLET_RECONCILE_MANAGE } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
@@ -100,12 +101,14 @@ function dimensionDistinctPrimaryScopePercents(
type RebateConfigDocScreenProps = {
embedded?: boolean;
workspace?: OddsConfigWorkspace;
versionId?: string;
onVersionIdChange?: (id: string) => void;
};
export function RebateConfigDocScreen({
embedded = false,
workspace,
versionId: controlledVersionId,
onVersionIdChange,
}: RebateConfigDocScreenProps) {
@@ -125,14 +128,22 @@ export function RebateConfigDocScreen({
const [listRows, setListRows] = useState<ConfigVersionSummary[]>([]);
const [internalSelectedId, setInternalSelectedId] = useState("");
const selectedId = controlledVersionId ?? internalSelectedId;
const setSelectedId = onVersionIdChange ?? setInternalSelectedId;
const selectedId = workspace?.selectedId ?? controlledVersionId ?? internalSelectedId;
const setSelectedId = workspace?.setSelectedId ?? onVersionIdChange ?? setInternalSelectedId;
const [detail, setDetail] = useState<OddsVersionDetail | null>(null);
const [draftRows, setDraftRows] = useState<OddsItemRow[]>([]);
const [loading, setLoading] = useState(true);
const [loadingDetail, setLoadingDetail] = useState(false);
const [saving, setSaving] = useState(false);
const resolvedTypes = workspace?.types ?? types;
const resolvedList = workspace?.list ?? listRows;
const resolvedDetail = workspace?.detail ?? detail;
const resolvedDraftRows = workspace?.draftRows ?? draftRows;
const setResolvedDraftRows = workspace?.setDraftRows ?? setDraftRows;
const resolvedLoading = workspace ? workspace.loadingList || workspace.loadingTypes : loading;
const resolvedLoadingDetail = workspace?.loadingDetail ?? loadingDetail;
const [p2, setP2] = useState("0");
const [p3, setP3] = useState("0");
const [p4, setP4] = useState("0");
@@ -173,13 +184,16 @@ export function RebateConfigDocScreen({
}, [t]);
useEffect(() => {
if (workspace) {
return;
}
queueMicrotask(async () => {
setLoading(true);
await refreshTypes();
await refreshList();
setLoading(false);
});
}, [refreshTypes, refreshList]);
}, [refreshTypes, refreshList, workspace]);
useEffect(() => {
queueMicrotask(() => {
@@ -187,6 +201,15 @@ export function RebateConfigDocScreen({
});
}, [loadWinEnjoySetting]);
useEffect(() => {
if (!workspace) {
return;
}
setP2(inferPercentFrom(2, workspace.draftRows, workspace.types));
setP3(inferPercentFrom(3, workspace.draftRows, workspace.types));
setP4(inferPercentFrom(4, workspace.draftRows, workspace.types));
}, [workspace?.draftRows, workspace?.types, workspace]);
async function handleWinEnjoyChange(checked: boolean): Promise<void> {
if (!canEditWinEnjoy) {
return;
@@ -226,6 +249,9 @@ export function RebateConfigDocScreen({
}, [t]);
useEffect(() => {
if (workspace) {
return;
}
if (listRows.length === 0) {
if (selectedId !== "") {
queueMicrotask(() => {
@@ -245,9 +271,12 @@ export function RebateConfigDocScreen({
setSelectedId(pickId);
}
});
}, [listRows, selectedId, setSelectedId]);
}, [listRows, selectedId, setSelectedId, workspace]);
useEffect(() => {
if (workspace) {
return;
}
if (selectedId === "") {
return;
}
@@ -258,34 +287,34 @@ export function RebateConfigDocScreen({
queueMicrotask(() => {
void loadDetail(id);
});
}, [selectedId, loadDetail]);
}, [selectedId, loadDetail, workspace]);
const typesByCode = useMemo(() => {
const m = new Map<string, AdminPlayTypeRow>();
for (const t of types) {
m.set(t.play_code, t);
for (const row of resolvedTypes) {
m.set(row.play_code, row);
}
return m;
}, [types]);
}, [resolvedTypes]);
const rebateBulkPercentsMixed = useMemo(() => {
if (types.length === 0 || draftRows.length === 0) {
if (resolvedTypes.length === 0 || resolvedDraftRows.length === 0) {
return false;
}
for (const dim of [2, 3, 4] as const) {
if (dimensionDistinctPrimaryScopePercents(dim, draftRows, types).size > 1) {
if (dimensionDistinctPrimaryScopePercents(dim, resolvedDraftRows, resolvedTypes).size > 1) {
return true;
}
}
return false;
}, [types, draftRows]);
}, [resolvedTypes, resolvedDraftRows]);
const selectedVersionSummary = useMemo(
() => listRows.find((x) => String(x.id) === selectedId) ?? null,
[listRows, selectedId],
() => resolvedList.find((x) => String(x.id) === selectedId) ?? null,
[resolvedList, selectedId],
);
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
const isSelectedDetail = resolvedDetail !== null && String(resolvedDetail.id) === selectedId;
const selectedStatus = isSelectedDetail ? resolvedDetail.status : selectedVersionSummary?.status;
const isDraft = selectedStatus === "draft";
const canEditDraft = isDraft && canManage;
@@ -305,12 +334,12 @@ export function RebateConfigDocScreen({
}
async function handleSave() {
if (!detail || !canEditDraft) {
if (!resolvedDetail || !canEditDraft) {
return;
}
setSaving(true);
try {
const nextRows = applyDimensionPercentsToRows(draftRows);
const nextRows = applyDimensionPercentsToRows(resolvedDraftRows);
const payload = nextRows.map((r) => ({
play_code: r.play_code,
prize_scope: r.prize_scope,
@@ -320,15 +349,19 @@ export function RebateConfigDocScreen({
currency_code: r.currency_code,
extra_config_json: r.extra_config_json,
}));
const d = await putOddsItems(detail.id, payload);
const d = await putOddsItems(resolvedDetail.id, payload);
const rows = d.items.map((it) => ({ ...it }));
setDetail(d);
setDraftRows(rows);
setP2(inferPercentFrom(2, rows, types));
setP3(inferPercentFrom(3, rows, types));
setP4(inferPercentFrom(4, rows, types));
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(rows);
}
setP2(inferPercentFrom(2, rows, resolvedTypes));
setP3(inferPercentFrom(3, rows, resolvedTypes));
setP4(inferPercentFrom(4, rows, resolvedTypes));
toast.success(t("versionActions.saveDraft", { ns: "config" }));
void refreshList();
void (workspace?.refreshList() ?? refreshList());
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" }));
} finally {
@@ -337,20 +370,24 @@ export function RebateConfigDocScreen({
}
async function handlePublish() {
if (!detail || !canEditDraft) {
if (!resolvedDetail || !canEditDraft) {
return;
}
setSaving(true);
try {
const d = await publishOddsVersion(detail.id);
const d = await publishOddsVersion(resolvedDetail.id);
const rows = d.items.map((it) => ({ ...it }));
setDetail(d);
setDraftRows(rows);
setP2(inferPercentFrom(2, rows, types));
setP3(inferPercentFrom(3, rows, types));
setP4(inferPercentFrom(4, rows, types));
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(rows);
}
setP2(inferPercentFrom(2, rows, resolvedTypes));
setP3(inferPercentFrom(3, rows, resolvedTypes));
setP4(inferPercentFrom(4, rows, resolvedTypes));
toast.success(t("rebate.publishSuccess", { ns: "config" }));
void refreshList();
void (workspace?.refreshList() ?? refreshList());
setSelectedId(String(d.id));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.publishFailed", { ns: "config" }));
@@ -362,20 +399,24 @@ export function RebateConfigDocScreen({
async function handleNewDraft() {
setSaving(true);
try {
const active = listRows.find((x) => x.status === "active");
const active = resolvedList.find((x) => x.status === "active");
const d = await postOddsVersion({
reason: `rebate draft ${new Date().toISOString()}`,
clone_from_version_id: active?.id ?? null,
});
toast.success(t("rebate.createDraftSuccess", { ns: "config", version: d.version_no }));
await refreshList();
await (workspace?.refreshList() ?? refreshList());
setSelectedId(String(d.id));
const rows = d.items.map((it) => ({ ...it }));
setDetail(d);
setDraftRows(rows);
setP2(inferPercentFrom(2, rows, types));
setP3(inferPercentFrom(3, rows, types));
setP4(inferPercentFrom(4, rows, types));
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(rows);
}
setP2(inferPercentFrom(2, rows, resolvedTypes));
setP3(inferPercentFrom(3, rows, resolvedTypes));
setP4(inferPercentFrom(4, rows, resolvedTypes));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.createDraftFailed", { ns: "config" }));
} finally {
@@ -383,7 +424,7 @@ export function RebateConfigDocScreen({
}
}
const activeHead = listRows.find((x) => x.status === "active");
const activeHead = resolvedList.find((x) => x.status === "active");
function requestRollback(row: ConfigVersionSummary) {
setRollbackTarget(row);
@@ -407,14 +448,18 @@ export function RebateConfigDocScreen({
version: d.version_no,
}),
);
await refreshList();
await (workspace?.refreshList() ?? refreshList());
setSelectedId(String(d.id));
const rows = d.items.map((it) => ({ ...it }));
setDetail(d);
setDraftRows(rows);
setP2(inferPercentFrom(2, rows, types));
setP3(inferPercentFrom(3, rows, types));
setP4(inferPercentFrom(4, rows, types));
if (workspace) {
workspace.applyDetail(d);
} else {
setDetail(d);
setDraftRows(rows);
}
setP2(inferPercentFrom(2, rows, resolvedTypes));
setP3(inferPercentFrom(3, rows, resolvedTypes));
setP4(inferPercentFrom(4, rows, resolvedTypes));
setRollbackOpen(false);
setRollbackTarget(null);
} catch (e) {
@@ -428,7 +473,7 @@ export function RebateConfigDocScreen({
try {
await deleteOddsVersion(row.id);
toast.success(t("versionSwitcher.delete", { ns: "config" }));
await refreshList();
await (workspace?.refreshList() ?? refreshList());
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.deleteFailed", { ns: "config" }));
throw e;
@@ -439,10 +484,10 @@ export function RebateConfigDocScreen({
<ConfigDocToolbar
switcher={
<ConfigVersionSwitcher
versions={listRows}
versions={resolvedList}
selectedId={selectedId}
onSelectedIdChange={setSelectedId}
loading={loading}
loading={resolvedLoading}
sheetTitle={`${t("nav.items.rebate", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
sheetDescription={t("rebate.sheetDescription", { ns: "config" })}
onDeleteVersion={handleDeleteVersion}
@@ -454,8 +499,8 @@ export function RebateConfigDocScreen({
<ConfigVersionActions
isDraft={isDraft}
canManage={canManage}
loadingList={loading}
loadingDetail={loadingDetail}
loadingList={resolvedLoading}
loadingDetail={resolvedLoadingDetail}
saving={saving}
publishLabel={t("rebate.publishLabel", { ns: "config" })}
onRefresh={() => void refreshList()}
@@ -473,16 +518,16 @@ export function RebateConfigDocScreen({
/>
}
footer={
embedded || !detail ? null : (
embedded || !resolvedDetail ? null : (
<ConfigVersionToolbarMeta emphasis={!isDraft}>
<span>
{t("rebate.editingVersion", {
ns: "config",
version: detail.version_no,
version: resolvedDetail.version_no,
status:
detail.status === "draft"
resolvedDetail.status === "draft"
? t("versionStatus.draft", { ns: "config" })
: detail.status === "active"
: resolvedDetail.status === "active"
? t("versionStatus.active", { ns: "config" })
: t("versionStatus.archived", { ns: "config" }),
})}
@@ -583,7 +628,7 @@ export function RebateConfigDocScreen({
</div>
) : null}
{loading || loadingDetail ? (
{resolvedLoading || resolvedLoadingDetail ? (
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
) : null}
</>