-
-
- {t("play.filters.keyword", { ns: "config" })}
- setKeyword(e.target.value)}
- placeholder={t("play.filters.keywordPlaceholder", { ns: "config" })}
- className="h-8"
- />
-
-
- {t("play.filters.category", { ns: "config" })}
-
-
-
- {t("play.filters.status", { ns: "config" })}
-
-
-
-
+
+
+ setKeyword(e.target.value)}
+ placeholder={t("play.filters.keywordPlaceholder", { ns: "config" })}
+ className="h-8 w-full min-w-[12rem] max-w-xs"
+ />
+
+
+ {(keyword || categoryFilter !== "all" || statusFilter !== "all") ? (
-
+ ) : null}
{isDraft ? (
-
-
- {t("play.batchSwitchesTitle", { ns: "config" })}
-
-
- {batchSwitchStates.map((group) => {
- const groupOn = group.allEnabled;
- const isPartial =
- group.total > 0 && group.enabledCount > 0 && group.enabledCount < group.total;
- return (
-
-
-
{group.label}
-
- {group.total > 0
- ? isPartial
- ? t("play.batchPartialEnabled", {
- ns: "config",
- enabledCount: group.enabledCount,
- total: group.total,
- })
- : t("play.batchEnabledCount", {
- ns: "config",
- enabledCount: group.enabledCount,
- total: group.total,
- })
- : t("play.noPlayTypes", { ns: "config" })}
-
-
-
-
+ {batchSwitchStates.map((group) => {
+ const groupOn = group.allEnabled;
+ const isPartial =
+ group.total > 0 && group.enabledCount > 0 && group.enabledCount < group.total;
+ return (
+
-
- );
- })}
-
-
+ count: group.total,
+ }),
+ confirmVariant: enable ? "default" : "destructive",
+ onConfirm: () => applyBatchSwitch(group, enable),
+ });
+ }}
+ />
+
+ );
+ })}
+
) : null}
-
+
) : null}
{error ?
{error}
: null}
diff --git a/src/modules/config/doc/rebate-config-doc-screen.tsx b/src/modules/config/doc/rebate-config-doc-screen.tsx
index 4f03dbe..58ca66b 100644
--- a/src/modules/config/doc/rebate-config-doc-screen.tsx
+++ b/src/modules/config/doc/rebate-config-doc-screen.tsx
@@ -481,8 +481,7 @@ export function RebateConfigDocScreen({
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}
diff --git a/src/modules/config/doc/risk-cap-doc-screen.tsx b/src/modules/config/doc/risk-cap-doc-screen.tsx
index fe7afb6..66bc34f 100644
--- a/src/modules/config/doc/risk-cap-doc-screen.tsx
+++ b/src/modules/config/doc/risk-cap-doc-screen.tsx
@@ -15,14 +15,11 @@ import {
putRiskCapItems,
} from "@/api/admin-config";
import { getAdminDraws } from "@/api/admin-draws";
+import { AdminPageCard } from "@/components/admin/admin-page-card";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { Button } from "@/components/ui/button";
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
-import {
- ConfigVersionToolbarMeta,
- ConfigVersionToolbarMetaEmphasis,
-} from "@/modules/config/config-version-toolbar-meta";
-import { ConfigSection } from "@/modules/config/config-section";
+import { ConfigVersionToolbarMeta } from "@/modules/config/config-version-toolbar-meta";
import {
Dialog,
DialogContent,
@@ -35,7 +32,6 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
-import { RiskCapRuntimePanel } from "@/modules/config/risk-cap-runtime-panel";
import {
Table,
TableBody,
@@ -44,13 +40,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
+
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
@@ -63,9 +53,7 @@ import { PRD_RISK_CAP_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 {
- AdminDrawListItem,
-} from "@/types/api/admin-draws";
+import type { AdminDrawListItem } from "@/types/api/admin-draws";
import type {
ConfigVersionSummary,
RiskCapItemRow,
@@ -102,6 +90,14 @@ function formatMinorToEditableMajor(minor: number, currencyCode: string): string
return formatAdminMinorDecimal(minor, currencyCode).replace(/,/g, "");
}
+function syncDefaultCapInput(rows: DraftRiskRow[], currencyCode: string): string {
+ const defaultRow = rows.find(isDefaultRiskRow);
+ if (!defaultRow) {
+ return "";
+ }
+ return formatMinorToEditableMajor(defaultRow.cap_amount, currencyCode);
+}
+
export function RiskCapDocScreen() {
const { t } = useTranslation(["config", "adminUsers", "common"]);
const tRef = useTranslationRef(["config", "common"]);
@@ -113,16 +109,15 @@ export function RiskCapDocScreen() {
const [selectedId, setSelectedId] = useState("");
const [detail, setDetail] = useState
(null);
const [draftRows, setDraftRows] = useState([]);
+ const [defaultCapInput, setDefaultCapInput] = useState("");
const [loadingList, setLoadingList] = useState(true);
const [loadingDetail, setLoadingDetail] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const [drawOptions, setDrawOptions] = useState([]);
-
- const [defaultCapStr, setDefaultCapStr] = useState("");
- const [syncOpen, setSyncOpen] = useState(false);
const [rollbackOpen, setRollbackOpen] = useState(false);
const [rollbackTarget, setRollbackTarget] = useState(null);
+ const [showNumberCaps, setShowNumberCaps] = useState(false);
const amountCurrencyCode = "NPR";
@@ -159,40 +154,49 @@ export function RiskCapDocScreen() {
void loadDrawOptions();
}, []);
- function syncDefaultCapFromRows(rows: DraftRiskRow[]) {
- const defaultRow = rows.find(isDefaultRiskRow);
- if (!defaultRow) {
- setDefaultCapStr("");
- return;
- }
- setDefaultCapStr(formatMinorToEditableMajor(defaultRow.cap_amount, amountCurrencyCode));
- }
+ const applyDefaultCapInput = useCallback(
+ (value: string) =>
+ (prev: DraftRiskRow[]): DraftRiskRow[] => {
+ const n = parseAdminMajorToMinor(value, amountCurrencyCode);
+ const rest = prev.filter((row) => !isDefaultRiskRow(row));
+ if (n != null && n > 0) {
+ return [defaultRiskRowFromAmount(n), ...rest];
+ }
+ return rest;
+ },
+ [amountCurrencyCode],
+ );
- const loadDetail = useCallback(async (id: number) => {
- setLoadingDetail(true);
- try {
- const d = await getRiskCapVersion(id);
- setDetail(d);
- const mapped = d.items.map((it) => ({
- clientKey: `srv-${it.id}`,
- draw_id: it.draw_id,
- normalized_number: it.normalized_number,
- cap_amount: it.cap_amount,
- cap_type: it.cap_type,
- }));
- setDraftRows(mapped);
- syncDefaultCapFromRows(mapped);
- } catch (e) {
- toast.error(
- e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
- );
- setDetail(null);
- setDraftRows([]);
- syncDefaultCapFromRows([]);
- } finally {
- setLoadingDetail(false);
- }
- }, []);
+ const loadDetail = useCallback(
+ async (id: number) => {
+ setLoadingDetail(true);
+ try {
+ const d = await getRiskCapVersion(id);
+ setDetail(d);
+ const mapped = d.items.map((it) => ({
+ clientKey: `srv-${it.id}`,
+ draw_id: it.draw_id,
+ normalized_number: it.normalized_number,
+ cap_amount: it.cap_amount,
+ cap_type: it.cap_type,
+ }));
+ setDraftRows(mapped);
+ setDefaultCapInput(syncDefaultCapInput(mapped, amountCurrencyCode));
+ setShowNumberCaps(mapped.some((row) => !isDefaultRiskRow(row)));
+ } catch (e) {
+ toast.error(
+ e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }),
+ );
+ setDetail(null);
+ setDraftRows([]);
+ setDefaultCapInput("");
+ setShowNumberCaps(false);
+ } finally {
+ setLoadingDetail(false);
+ }
+ },
+ [amountCurrencyCode, tRef],
+ );
useEffect(() => {
if (list.length === 0) {
@@ -201,7 +205,7 @@ export function RiskCapDocScreen() {
setSelectedId("");
setDetail(null);
setDraftRows([]);
- syncDefaultCapFromRows([]);
+ setDefaultCapInput("");
});
}
return;
@@ -251,11 +255,13 @@ export function RiskCapDocScreen() {
if (!detail || !canEditDraft) {
return;
}
- if (draftRows.length === 0) {
+ const rowsToSave = applyDefaultCapInput(defaultCapInput)(draftRows);
+ setDraftRows(rowsToSave);
+ if (rowsToSave.length === 0) {
toast.error(t("riskCap.validation.requireAtLeastOne", { ns: "config" }));
return;
}
- for (const r of draftRows) {
+ for (const r of rowsToSave) {
if (isDefaultRiskRow(r)) {
if (r.cap_amount <= 0) {
toast.error(t("riskCap.validation.defaultGreaterThanZero", { ns: "config" }));
@@ -278,7 +284,7 @@ export function RiskCapDocScreen() {
}
setSaving(true);
try {
- const payload = draftRows.map((r) => ({
+ const payload = rowsToSave.map((r) => ({
draw_id: r.draw_id && r.draw_id > 0 ? r.draw_id : null,
normalized_number: r.normalized_number,
cap_amount: r.cap_amount,
@@ -294,7 +300,7 @@ export function RiskCapDocScreen() {
cap_type: it.cap_type,
}));
setDraftRows(saved);
- syncDefaultCapFromRows(saved);
+ setDefaultCapInput(syncDefaultCapInput(saved, amountCurrencyCode));
toast.success(t("versionActions.saveDraft", { ns: "config" }));
void refreshList();
} catch (e) {
@@ -320,7 +326,7 @@ export function RiskCapDocScreen() {
cap_type: it.cap_type,
}));
setDraftRows(pub);
- syncDefaultCapFromRows(pub);
+ setDefaultCapInput(syncDefaultCapInput(pub, amountCurrencyCode));
toast.success(t("versionActions.publishCurrent", { ns: "config" }));
void refreshList();
setSelectedId(String(d.id));
@@ -351,7 +357,7 @@ export function RiskCapDocScreen() {
cap_type: it.cap_type,
}));
setDraftRows(nd);
- syncDefaultCapFromRows(nd);
+ setDefaultCapInput(syncDefaultCapInput(nd, amountCurrencyCode));
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("riskCap.createDraftFailed", { ns: "config" }));
} finally {
@@ -359,37 +365,25 @@ export function RiskCapDocScreen() {
}
}
- function applyDefaultCap() {
- const n = parseAdminMajorToMinor(defaultCapStr, amountCurrencyCode);
- if (n == null || !Number.isFinite(n) || n <= 0) {
- toast.error(t("riskCap.validation.enterValidCapAmount", { ns: "config" }));
- return;
- }
- setDraftRows((prev) => {
- const next = prev.filter((row) => !isDefaultRiskRow(row));
- return [defaultRiskRowFromAmount(n), ...next];
- });
- setSyncOpen(false);
- toast.message(t("riskCap.savedLocalDraft", { ns: "config" }));
- }
-
const specialRows = useMemo(
- () => draftRows.map((row, index) => ({ row, index })).filter(({ row }) => !isDefaultRiskRow(row)),
+ () =>
+ draftRows
+ .map((row, index) => ({ row, index }))
+ .filter(({ row }) => !isDefaultRiskRow(row) && row.draw_id == null),
[draftRows],
);
- const globalRows = useMemo(
- () => specialRows.filter(({ row }) => row.draw_id == null),
- [specialRows],
- );
- const drawRows = useMemo(
- () => specialRows.filter(({ row }) => row.draw_id != null),
- [specialRows],
+
+ const drawBoundRows = useMemo(
+ () => draftRows.filter((row) => !isDefaultRiskRow(row) && row.draw_id != null),
+ [draftRows],
);
+
+ function drawLabel(drawId: number): string {
+ return drawOptions.find((draw) => draw.id === drawId)?.draw_no ?? String(drawId);
+ }
+
const defaultCapDisplay = detail
- ? formatAdminMinorDecimal(
- draftRows.find(isDefaultRiskRow)?.cap_amount ?? 0,
- amountCurrencyCode,
- )
+ ? formatAdminMinorDecimal(draftRows.find(isDefaultRiskRow)?.cap_amount ?? 0, amountCurrencyCode)
: formatAdminMinorDecimal(0, amountCurrencyCode);
async function handleDeleteVersion(row: ConfigVersionSummary) {
@@ -436,7 +430,7 @@ export function RiskCapDocScreen() {
cap_type: it.cap_type,
}));
setDraftRows(mapped);
- syncDefaultCapFromRows(mapped);
+ setDefaultCapInput(syncDefaultCapInput(mapped, amountCurrencyCode));
setRollbackOpen(false);
setRollbackTarget(null);
} catch (e) {
@@ -458,7 +452,7 @@ export function RiskCapDocScreen() {
selectedId={selectedId}
onSelectedIdChange={setSelectedId}
loading={loadingList}
- sheetTitle={`${t("nav.items.risk-cap", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
+
onDeleteVersion={handleDeleteVersion}
onRollbackVersion={requestRollback}
rollbackBusy={saving}
@@ -486,276 +480,191 @@ export function RiskCapDocScreen() {
/>
}
footer={
- detail ? (
-
+ detail?.effective_at ? (
+
{t("riskCap.effectiveAt", {
ns: "config",
- value: detail.effective_at ? formatDt(detail.effective_at) : "—",
+ value: formatDt(detail.effective_at),
})}
- {!isDraft ? (
-
- {t("riskCap.readOnlyHint", { ns: "config" })}
-
- ) : (
- {t("versionToolbar.draftEditing", { ns: "config" })}
- )}
) : null
}
/>
}
- contentClassName="space-y-8"
+ contentClassName="space-y-6"
>
{error ? {error}
: null}
-
- {[
- {
- key: "default",
- label: t("riskCap.summary.defaultCap", { ns: "config" }),
- value: defaultCapDisplay,
- hint: t("riskCap.summary.defaultHint", { ns: "config" }),
- },
- {
- key: "global",
- label: t("riskCap.summary.globalCaps", { ns: "config" }),
- value: t("riskCap.groups.count", { ns: "config", count: globalRows.length }),
- hint: t("riskCap.summary.globalHint", { ns: "config" }),
- },
- {
- key: "draw",
- label: t("riskCap.summary.drawCaps", { ns: "config" }),
- value: t("riskCap.groups.count", { ns: "config", count: drawRows.length }),
- hint: t("riskCap.summary.drawHint", { ns: "config" }),
- },
- ].map((card) => (
-
-
{card.label}
-
{card.value}
-
{card.hint}
-
- ))}
-
-
-
-
-
-
- {canEditDraft ? (
- setDefaultCapStr(e.target.value)}
- />
- ) : (
-
- {defaultCapDisplay}
-
- )}
-
- {canEditDraft ? (
-
- ) : null}
-
-
-
-
+
+
+ {canEditDraft ? (
+ setDraftRows((prev) => [...prev, newRow()])}
- >
- {t("riskCap.actions.addSpecialCap", { ns: "config" })}
-
- ) : null
- }
- >
+ value={defaultCapInput}
+ placeholder={t("riskCap.placeholders.defaultCap", { ns: "config" })}
+ onChange={(e) => setDefaultCapInput(e.target.value)}
+ onBlur={() => setDraftRows(applyDefaultCapInput(defaultCapInput))}
+ />
+ ) : (
+ {defaultCapDisplay}
+ )}
+
+
+
+ {canEditDraft && !showNumberCaps && specialRows.length === 0 && drawBoundRows.length === 0 ? (
+
+ ) : null}
+
+ {showNumberCaps || specialRows.length > 0 || drawBoundRows.length > 0 ? (
+ setDraftRows((prev) => [...prev, newRow()])}
+ >
+ {t("riskCap.actions.addSpecialCap", { ns: "config" })}
+
+ ) : null
+ }
+ >
{loadingDetail ? (
-
- ) : specialRows.length === 0 ? (
+
+ ) : specialRows.length === 0 && drawBoundRows.length === 0 ? (
{t("riskCap.noDetailRows", { ns: "config" })}
) : (
- {[
- {
- key: "global",
- title: t("riskCap.groups.globalTitle", { ns: "config" }),
- description: t("riskCap.groups.globalDescription", { ns: "config" }),
- rows: globalRows,
- emptyText: t("riskCap.groups.globalEmpty", { ns: "config" }),
- },
- {
- key: "draw",
- title: t("riskCap.groups.drawTitle", { ns: "config" }),
- description: t("riskCap.groups.drawDescription", { ns: "config" }),
- rows: drawRows,
- emptyText: t("riskCap.groups.drawEmpty", { ns: "config" }),
- },
- ].map((group) => (
-
-
-
-
{group.title}
-
{group.description}
-
-
- {t("riskCap.groups.count", { ns: "config", count: group.rows.length })}
-
-
-
- {group.rows.length === 0 ? (
-
{group.emptyText}
- ) : (
-
-
-
- {t("riskCap.table.scope", { ns: "config" })}
- {t("riskCap.table.number", { ns: "config" })}
- {t("riskCap.table.capAmount", { ns: "config" })}
-
- {t("riskCap.table.actions", { ns: "config" })}
-
+ {specialRows.length > 0 ? (
+
+
+
+
+ {t("riskCap.table.number", { ns: "config" })}
+ {t("riskCap.table.capAmount", { ns: "config" })}
+
+ {t("riskCap.table.actions", { ns: "config" })}
+
+
+
+
+ {specialRows.map(({ row: r, index: idx }) => (
+
+
+ {canEditDraft ? (
+
+ updateRow(idx, {
+ normalized_number: e.target.value.replace(/\D/g, "").slice(0, 4),
+ })
+ }
+ />
+ ) : (
+ {r.normalized_number}
+ )}
+
+
+ {canEditDraft ? (
+
+ updateRow(idx, {
+ cap_amount: parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
+ })
+ }
+ />
+ ) : (
+
+ {formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)}
+
+ )}
+
+
+ {canEditDraft ? (
+ removeRow(idx),
+ },
+ ]}
+ />
+ ) : (
+ —
+ )}
+
-
-
- {group.rows.map(({ row: r, index: idx }) => (
-
-
- {canEditDraft ? (
-
- ) : (
-
- {r.draw_id == null
- ? t("riskCap.scope.global", { ns: "config" })
- : drawOptions.find((draw) => draw.id === r.draw_id)?.draw_no ??
- t("riskCap.scope.drawId", { ns: "config", id: r.draw_id })}
-
- )}
-
-
- {canEditDraft ? (
-
- updateRow(idx, {
- normalized_number: e.target.value.replace(/\D/g, "").slice(0, 4),
- })
- }
- />
- ) : (
- {r.normalized_number}
- )}
-
-
- {canEditDraft ? (
-
- updateRow(idx, {
- cap_amount:
- parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
- })
- }
- />
- ) : (
-
- {formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)}
-
- )}
-
-
- {canEditDraft ? (
- removeRow(idx),
- },
- ]}
- />
- ) : (
-
- {t("riskCap.readOnly", { ns: "config" })}
-
- )}
-
-
- ))}
-
-
- )}
+ ))}
+
+
- ))}
+ ) : null}
+
+ {drawBoundRows.length > 0 ? (
+
+
+
+
+ {t("riskCap.table.draw", { ns: "config" })}
+ {t("riskCap.table.number", { ns: "config" })}
+ {t("riskCap.table.capAmount", { ns: "config" })}
+
+
+
+ {drawBoundRows.map((row) => (
+
+ {drawLabel(row.draw_id ?? 0)}
+ {row.normalized_number}
+ {formatAdminMinorDecimal(row.cap_amount, amountCurrencyCode)}
+
+ ))}
+
+
+
+ ) : null}
)}
-
-
-
-
-
+
+ ) : null}
@@ -289,6 +309,8 @@ export function AgentDashboardConsole(): ReactElement {