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.
692 lines
24 KiB
TypeScript
692 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
|
|
import {
|
|
deleteOddsVersion,
|
|
getAdminPlayTypes,
|
|
getAllConfigVersions,
|
|
getOddsVersion,
|
|
getOddsVersions,
|
|
postOddsVersion,
|
|
publishOddsVersion,
|
|
putOddsItems,
|
|
} from "@/api/admin-config";
|
|
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
|
|
import {
|
|
ConfigVersionToolbarMeta,
|
|
ConfigVersionToolbarMetaEmphasis,
|
|
} from "@/modules/config/config-version-toolbar-meta";
|
|
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";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} 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 { 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";
|
|
import type {
|
|
AdminPlayTypeRow,
|
|
ConfigVersionSummary,
|
|
OddsItemRow,
|
|
OddsVersionDetail,
|
|
} from "@/types/api/admin-config";
|
|
|
|
import { ConfigWorkflowSection } from "@/modules/config/config-workflow-section";
|
|
import {
|
|
inferRebatePercentFromDimension,
|
|
rateToPercentUi,
|
|
} from "@/modules/config/doc/odds-rebate-rates";
|
|
import { PRIZE_SCOPE_ORDER } from "@/modules/config/doc/prize-scopes";
|
|
|
|
const APPLY_REBATE_TO_PAYOUT_KEY = "settlement.apply_rebate_to_payout";
|
|
|
|
function dimensionDistinctPrimaryScopePercents(
|
|
dim: 2 | 3 | 4,
|
|
rows: OddsItemRow[],
|
|
typeList: AdminPlayTypeRow[],
|
|
): Set<string> {
|
|
const codes = typeList
|
|
.filter((t) => (t.dimension ?? 2) === dim)
|
|
.map((t) => t.play_code)
|
|
.sort((a, b) => a.localeCompare(b));
|
|
const scope = PRIZE_SCOPE_ORDER[0];
|
|
const percents = new Set<string>();
|
|
for (const code of codes) {
|
|
const hit = rows.find((r) => r.play_code === code && r.prize_scope === scope);
|
|
if (hit) {
|
|
percents.add(rateToPercentUi(String(hit.rebate_rate)));
|
|
}
|
|
}
|
|
return percents;
|
|
}
|
|
|
|
type RebateConfigDocScreenProps = {
|
|
embedded?: boolean;
|
|
/** 合并页第 3 步卡片 */
|
|
mergedSection?: boolean;
|
|
workspace?: OddsConfigWorkspace;
|
|
versionId?: string;
|
|
onVersionIdChange?: (id: string) => void;
|
|
};
|
|
|
|
export function RebateConfigDocScreen({
|
|
embedded = false,
|
|
mergedSection = false,
|
|
workspace,
|
|
versionId: controlledVersionId,
|
|
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]);
|
|
const canEditWinEnjoy = adminHasAnyPermission(profile?.permissions, [
|
|
PRD_REBATE_MANAGE,
|
|
PRD_WALLET_RECONCILE_MANAGE,
|
|
]);
|
|
const [applyRebateToPayout, setApplyRebateToPayout] = useState(false);
|
|
const [winEnjoyLoading, setWinEnjoyLoading] = useState(true);
|
|
const [winEnjoySaving, setWinEnjoySaving] = useState(false);
|
|
const formatDt = useAdminDateTimeFormatter();
|
|
const [types, setTypes] = useState<AdminPlayTypeRow[]>([]);
|
|
const [listRows, setListRows] = useState<ConfigVersionSummary[]>([]);
|
|
|
|
const [internalSelectedId, setInternalSelectedId] = useState("");
|
|
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");
|
|
const [rollbackOpen, setRollbackOpen] = useState(false);
|
|
const [rollbackTarget, setRollbackTarget] = useState<ConfigVersionSummary | null>(null);
|
|
|
|
const refreshTypes = useCallback(async () => {
|
|
try {
|
|
setTypes(await ensureAdminPlayTypesLoaded(getAdminPlayTypes));
|
|
} catch (e) {
|
|
toast.error(
|
|
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
|
|
);
|
|
setTypes([]);
|
|
}
|
|
}, []);
|
|
|
|
const refreshList = useCallback(async () => {
|
|
try {
|
|
const d = await getAllConfigVersions(getOddsVersions);
|
|
setListRows(d.items);
|
|
} catch (e) {
|
|
toast.error(
|
|
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
|
|
);
|
|
setListRows([]);
|
|
}
|
|
}, []);
|
|
|
|
useAsyncEffect(() => {
|
|
if (workspace) {
|
|
return;
|
|
}
|
|
void (async () => {
|
|
setLoading(true);
|
|
await Promise.all([refreshTypes(), refreshList()]);
|
|
setLoading(false);
|
|
})();
|
|
}, [workspace]);
|
|
|
|
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) {
|
|
return;
|
|
}
|
|
setP2(inferRebatePercentFromDimension(2, workspace.draftRows, workspace.types));
|
|
setP3(inferRebatePercentFromDimension(3, workspace.draftRows, workspace.types));
|
|
setP4(inferRebatePercentFromDimension(4, workspace.draftRows, workspace.types));
|
|
}, [workspace?.draftRows, workspace?.types, workspace]);
|
|
|
|
async function handleWinEnjoyChange(checked: boolean): Promise<void> {
|
|
if (!canEditWinEnjoy) {
|
|
return;
|
|
}
|
|
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) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.winEnjoy.saveFailed", { ns: "config" }));
|
|
} finally {
|
|
setWinEnjoySaving(false);
|
|
}
|
|
}
|
|
|
|
const loadDetail = useCallback(async (id: number) => {
|
|
setLoadingDetail(true);
|
|
try {
|
|
const typeList = await ensureAdminPlayTypesLoaded(getAdminPlayTypes);
|
|
setTypes(typeList);
|
|
const d = await getOddsVersion(id);
|
|
const rows = d.items.map((it) => ({ ...it }));
|
|
setDetail(d);
|
|
setDraftRows(rows);
|
|
setP2(inferRebatePercentFromDimension(2, rows, typeList));
|
|
setP3(inferRebatePercentFromDimension(3, rows, typeList));
|
|
setP4(inferRebatePercentFromDimension(4, rows, typeList));
|
|
} catch (e) {
|
|
toast.error(
|
|
e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
|
|
);
|
|
setDetail(null);
|
|
setDraftRows([]);
|
|
} finally {
|
|
setLoadingDetail(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (workspace) {
|
|
return;
|
|
}
|
|
if (listRows.length === 0) {
|
|
if (selectedId !== "") {
|
|
queueMicrotask(() => {
|
|
setSelectedId("");
|
|
setDetail(null);
|
|
setDraftRows([]);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (selectedId !== "" && listRows.some((x) => String(x.id) === selectedId)) {
|
|
return;
|
|
}
|
|
queueMicrotask(() => {
|
|
const pickId = pickDefaultConfigVersionId(listRows);
|
|
if (pickId) {
|
|
setSelectedId(pickId);
|
|
}
|
|
});
|
|
}, [listRows, selectedId, setSelectedId, workspace]);
|
|
|
|
useEffect(() => {
|
|
if (workspace) {
|
|
return;
|
|
}
|
|
if (selectedId === "") {
|
|
return;
|
|
}
|
|
const id = Number(selectedId);
|
|
if (!Number.isFinite(id)) {
|
|
return;
|
|
}
|
|
queueMicrotask(() => {
|
|
void loadDetail(id);
|
|
});
|
|
}, [selectedId, loadDetail, workspace]);
|
|
|
|
const typesByCode = useMemo(() => {
|
|
const m = new Map<string, AdminPlayTypeRow>();
|
|
for (const row of resolvedTypes) {
|
|
m.set(row.play_code, row);
|
|
}
|
|
return m;
|
|
}, [resolvedTypes]);
|
|
|
|
const rebateBulkPercentsMixed = useMemo(() => {
|
|
if (resolvedTypes.length === 0 || resolvedDraftRows.length === 0) {
|
|
return false;
|
|
}
|
|
for (const dim of [2, 3, 4] as const) {
|
|
if (dimensionDistinctPrimaryScopePercents(dim, resolvedDraftRows, resolvedTypes).size > 1) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}, [resolvedTypes, resolvedDraftRows]);
|
|
|
|
const selectedVersionSummary = useMemo(
|
|
() => resolvedList.find((x) => String(x.id) === selectedId) ?? null,
|
|
[resolvedList, selectedId],
|
|
);
|
|
const isSelectedDetail = resolvedDetail !== null && String(resolvedDetail.id) === selectedId;
|
|
const selectedStatus = isSelectedDetail ? resolvedDetail.status : selectedVersionSummary?.status;
|
|
const isDraft = selectedStatus === "draft";
|
|
const canEditDraft = isDraft && canManage;
|
|
|
|
function applyDimensionPercentsToRows(rows: OddsItemRow[]): OddsItemRow[] {
|
|
const r2 = Number.parseFloat(p2);
|
|
const r3 = Number.parseFloat(p3);
|
|
const r4 = Number.parseFloat(p4);
|
|
const rate2 = Number.isFinite(r2) ? r2 / 100 : 0;
|
|
const rate3 = Number.isFinite(r3) ? r3 / 100 : 0;
|
|
const rate4 = Number.isFinite(r4) ? r4 / 100 : 0;
|
|
return rows.map((row) => {
|
|
const t = typesByCode.get(row.play_code);
|
|
const dim = (t?.dimension ?? 2) as 2 | 3 | 4;
|
|
const rate = dim === 4 ? rate4 : dim === 3 ? rate3 : rate2;
|
|
return { ...row, rebate_rate: String(rate) };
|
|
});
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (!resolvedDetail || !canEditDraft) {
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const nextRows = applyDimensionPercentsToRows(resolvedDraftRows);
|
|
const payload = nextRows.map((r) => ({
|
|
play_code: r.play_code,
|
|
prize_scope: r.prize_scope,
|
|
odds_value: r.odds_value,
|
|
rebate_rate: Number.parseFloat(String(r.rebate_rate)) || 0,
|
|
commission_rate: Number.parseFloat(String(r.commission_rate)) || 0,
|
|
currency_code: r.currency_code,
|
|
extra_config_json: r.extra_config_json,
|
|
}));
|
|
const d = await putOddsItems(resolvedDetail.id, payload);
|
|
const rows = d.items.map((it) => ({ ...it }));
|
|
if (workspace) {
|
|
workspace.applyDetail(d);
|
|
} else {
|
|
setDetail(d);
|
|
setDraftRows(rows);
|
|
}
|
|
setP2(inferRebatePercentFromDimension(2, rows, resolvedTypes));
|
|
setP3(inferRebatePercentFromDimension(3, rows, resolvedTypes));
|
|
setP4(inferRebatePercentFromDimension(4, rows, resolvedTypes));
|
|
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
|
void (workspace?.refreshList() ?? refreshList());
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" }));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handlePublish() {
|
|
if (!resolvedDetail || !canEditDraft) {
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const d = await publishOddsVersion(resolvedDetail.id);
|
|
const rows = d.items.map((it) => ({ ...it }));
|
|
if (workspace) {
|
|
workspace.applyDetail(d);
|
|
} else {
|
|
setDetail(d);
|
|
setDraftRows(rows);
|
|
}
|
|
setP2(inferRebatePercentFromDimension(2, rows, resolvedTypes));
|
|
setP3(inferRebatePercentFromDimension(3, rows, resolvedTypes));
|
|
setP4(inferRebatePercentFromDimension(4, rows, resolvedTypes));
|
|
toast.success(t("rebate.publishSuccess", { ns: "config" }));
|
|
void (workspace?.refreshList() ?? refreshList());
|
|
setSelectedId(String(d.id));
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.publishFailed", { ns: "config" }));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleNewDraft() {
|
|
setSaving(true);
|
|
try {
|
|
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 (workspace?.refreshList() ?? refreshList());
|
|
setSelectedId(String(d.id));
|
|
const rows = d.items.map((it) => ({ ...it }));
|
|
if (workspace) {
|
|
workspace.applyDetail(d);
|
|
} else {
|
|
setDetail(d);
|
|
setDraftRows(rows);
|
|
}
|
|
setP2(inferRebatePercentFromDimension(2, rows, resolvedTypes));
|
|
setP3(inferRebatePercentFromDimension(3, rows, resolvedTypes));
|
|
setP4(inferRebatePercentFromDimension(4, rows, resolvedTypes));
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.createDraftFailed", { ns: "config" }));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
const activeHead = resolvedList.find((x) => x.status === "active");
|
|
|
|
function requestRollback(row: ConfigVersionSummary) {
|
|
setRollbackTarget(row);
|
|
setRollbackOpen(true);
|
|
}
|
|
|
|
async function handleRollback() {
|
|
if (!rollbackTarget) {
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const d = await postOddsVersion({
|
|
reason: `rollback from v${rollbackTarget.version_no}`,
|
|
clone_from_version_id: rollbackTarget.id,
|
|
});
|
|
toast.success(
|
|
t("versionActions.rollbackSuccess", {
|
|
ns: "config",
|
|
fromVersion: rollbackTarget.version_no,
|
|
version: d.version_no,
|
|
}),
|
|
);
|
|
await (workspace?.refreshList() ?? refreshList());
|
|
setSelectedId(String(d.id));
|
|
const rows = d.items.map((it) => ({ ...it }));
|
|
if (workspace) {
|
|
workspace.applyDetail(d);
|
|
} else {
|
|
setDetail(d);
|
|
setDraftRows(rows);
|
|
}
|
|
setP2(inferRebatePercentFromDimension(2, rows, resolvedTypes));
|
|
setP3(inferRebatePercentFromDimension(3, rows, resolvedTypes));
|
|
setP4(inferRebatePercentFromDimension(4, rows, resolvedTypes));
|
|
setRollbackOpen(false);
|
|
setRollbackTarget(null);
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.rollbackFailed", { ns: "config" }));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
|
try {
|
|
await deleteOddsVersion(row.id);
|
|
toast.success(t("versionSwitcher.delete", { ns: "config" }));
|
|
await (workspace?.refreshList() ?? refreshList());
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.deleteFailed", { ns: "config" }));
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const toolbarBlock = embedded ? null : (
|
|
<ConfigDocToolbar
|
|
switcher={
|
|
<ConfigVersionSwitcher
|
|
versions={resolvedList}
|
|
selectedId={selectedId}
|
|
onSelectedIdChange={setSelectedId}
|
|
loading={resolvedLoading}
|
|
sheetTitle={`${t("nav.items.rebate", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
|
|
sheetDescription={t("rebate.sheetDescription", { ns: "config" })}
|
|
onDeleteVersion={handleDeleteVersion}
|
|
onRollbackVersion={requestRollback}
|
|
rollbackBusy={saving}
|
|
/>
|
|
}
|
|
actions={
|
|
<ConfigVersionActions
|
|
isDraft={isDraft}
|
|
canManage={canManage}
|
|
loadingList={resolvedLoading}
|
|
loadingDetail={resolvedLoadingDetail}
|
|
saving={saving}
|
|
publishLabel={t("rebate.publishLabel", { ns: "config" })}
|
|
onRefresh={() => void refreshList()}
|
|
onNewDraft={() => void handleNewDraft()}
|
|
onSaveDraft={() => void handleSave()}
|
|
onPublish={() =>
|
|
requestConfirm({
|
|
title: t("rebate.publishDialog.title", { ns: "config" }),
|
|
description: t("rebate.publishDialog.description", { ns: "config" }),
|
|
confirmLabel: t("rebate.publishDialog.confirm", { ns: "config" }),
|
|
confirmVariant: "destructive",
|
|
onConfirm: () => handlePublish(),
|
|
})
|
|
}
|
|
/>
|
|
}
|
|
footer={
|
|
embedded || !resolvedDetail ? null : (
|
|
<ConfigVersionToolbarMeta emphasis={!isDraft}>
|
|
<span>
|
|
{t("rebate.editingVersion", {
|
|
ns: "config",
|
|
version: resolvedDetail.version_no,
|
|
status:
|
|
resolvedDetail.status === "draft"
|
|
? t("versionStatus.draft", { ns: "config" })
|
|
: resolvedDetail.status === "active"
|
|
? t("versionStatus.active", { ns: "config" })
|
|
: t("versionStatus.archived", { ns: "config" }),
|
|
})}
|
|
</span>
|
|
{!isDraft ? (
|
|
<ConfigVersionToolbarMetaEmphasis>
|
|
{t("rebate.readOnlyHint", { ns: "config" })}
|
|
</ConfigVersionToolbarMetaEmphasis>
|
|
) : (
|
|
<span>{t("versionToolbar.draftEditing", { ns: "config" })}</span>
|
|
)}
|
|
</ConfigVersionToolbarMeta>
|
|
)
|
|
}
|
|
/>
|
|
);
|
|
|
|
const fieldsBlock = (
|
|
<>
|
|
{rebateBulkPercentsMixed ? (
|
|
<Alert className="border-amber-500/35 bg-amber-500/10 text-amber-950 dark:text-amber-100">
|
|
<AlertDescription>{t("rebate.dimensionRatesMixedHint", { ns: "config" })}</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
<div className="grid gap-5 sm:grid-cols-3">
|
|
<div className="grid gap-2">
|
|
<Label>{t("rebate.fields.d2", { ns: "config" })}</Label>
|
|
{canEditDraft ? (
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
min={0}
|
|
className="font-mono tabular-nums"
|
|
disabled={saving}
|
|
value={p2}
|
|
onChange={(e) => setP2(e.target.value)}
|
|
/>
|
|
) : (
|
|
<ConfigReadonlyValue mono>{p2}</ConfigReadonlyValue>
|
|
)}
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("rebate.fields.d3", { ns: "config" })}</Label>
|
|
{canEditDraft ? (
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
min={0}
|
|
className="font-mono tabular-nums"
|
|
disabled={saving}
|
|
value={p3}
|
|
onChange={(e) => setP3(e.target.value)}
|
|
/>
|
|
) : (
|
|
<ConfigReadonlyValue mono>{p3}</ConfigReadonlyValue>
|
|
)}
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("rebate.fields.d4", { ns: "config" })}</Label>
|
|
{canEditDraft ? (
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
min={0}
|
|
className="font-mono tabular-nums"
|
|
disabled={saving}
|
|
value={p4}
|
|
onChange={(e) => setP4(e.target.value)}
|
|
/>
|
|
) : (
|
|
<ConfigReadonlyValue mono>{p4}</ConfigReadonlyValue>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-border/60 px-4 py-3">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div className="min-w-0 space-y-1">
|
|
<p className="text-sm font-medium">{t("rebate.winEnjoy.label", { ns: "config" })}</p>
|
|
<p className="text-xs text-muted-foreground">{t("rebate.winEnjoy.description", { ns: "config" })}</p>
|
|
</div>
|
|
<Switch
|
|
checked={applyRebateToPayout}
|
|
disabled={winEnjoyLoading || winEnjoySaving || !canEditWinEnjoy}
|
|
aria-label={t("rebate.winEnjoy.label", { ns: "config" })}
|
|
onCheckedChange={(value) => void handleWinEnjoyChange(value)}
|
|
/>
|
|
</div>
|
|
<p className="mt-2 text-xs text-muted-foreground">{t("rebate.winEnjoy.hint", { ns: "config" })}</p>
|
|
</div>
|
|
|
|
{!embedded ? (
|
|
<div className="grid gap-1 text-sm">
|
|
<span className="text-muted-foreground">{t("rebate.effectiveTime", { ns: "config" })}</span>
|
|
<span className="font-mono text-sm">
|
|
{activeHead?.effective_at ? formatDt(activeHead.effective_at) : "—"}
|
|
</span>
|
|
</div>
|
|
) : null}
|
|
|
|
{resolvedLoading || resolvedLoadingDetail ? (
|
|
<AdminLoadingState minHeight="6rem" className="py-6" />
|
|
) : null}
|
|
</>
|
|
);
|
|
|
|
const rollbackDialog = (
|
|
<Dialog open={rollbackOpen} onOpenChange={setRollbackOpen}>
|
|
<DialogContent showCloseButton className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("versionActions.rollbackDialog.title", { ns: "config" })}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("versionActions.rollbackDialog.description", {
|
|
ns: "config",
|
|
version: rollbackTarget?.version_no ?? "—",
|
|
})}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => setRollbackOpen(false)}>
|
|
{t("actions.cancel", { ns: "adminUsers" })}
|
|
</Button>
|
|
<Button type="button" onClick={() => void handleRollback()} disabled={!rollbackTarget || saving}>
|
|
{t("versionActions.rollbackDialog.confirm", { ns: "config" })}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
|
|
if (embedded && mergedSection) {
|
|
return (
|
|
<>
|
|
<ConfigWorkflowSection
|
|
step={3}
|
|
title={t("nav.items.rebate", { ns: "config" })}
|
|
description={t("rebate.sectionHint", { ns: "config" })}
|
|
contentClassName="space-y-5"
|
|
>
|
|
{fieldsBlock}
|
|
</ConfigWorkflowSection>
|
|
{rollbackDialog}
|
|
<ConfirmDialog />
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (embedded) {
|
|
return (
|
|
<div className="space-y-4">
|
|
{fieldsBlock}
|
|
{rollbackDialog}
|
|
<ConfirmDialog />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ConfigDocPage
|
|
title={t("nav.items.rebate", { ns: "config" })}
|
|
toolbar={toolbarBlock}
|
|
>
|
|
{fieldsBlock}
|
|
{rollbackDialog}
|
|
<ConfirmDialog />
|
|
</ConfigDocPage>
|
|
);
|
|
}
|