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