feat(api, ui, i18n): 增强奖池管理与钱包功能
新增奖池余额调整与调整记录查询相关 API,提升后台对奖池的管理与控制能力。 更新奖池与钱包相关多语言文案,新增余额调整与转账完成提示信息,提升用户理解与反馈体验。 优化奖池管理相关 UI 组件,新增余额调整功能并改进页面布局,提升操作易用性。 重构相关组件以整合新功能,并进一步优化后台管理界面的整体用户体验。
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user