diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000..9b8b44b
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
+
+import { cn } from "@/lib/utils"
+
+function Switch({
+ className,
+ size = "default",
+ ...props
+}: SwitchPrimitive.Root.Props & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+
+
+ )
+}
+
+export { Switch }
diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json
index 5f2b924..51ba7a8 100644
--- a/src/i18n/locales/en/config.json
+++ b/src/i18n/locales/en/config.json
@@ -236,7 +236,8 @@
"readOnly": "Read only"
},
"aria": {
- "enablePlay": "Enable {{playCode}}"
+ "enablePlay": "Enable {{playCode}}",
+ "batchGroupSwitch": "Toggle batch switch for {{group}}"
},
"nameDialog": {
"title": "Edit display name",
diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json
index ba89bf9..c45c332 100644
--- a/src/i18n/locales/ne/config.json
+++ b/src/i18n/locales/ne/config.json
@@ -236,7 +236,8 @@
"readOnly": "केवल पढ्न मिल्ने"
},
"aria": {
- "enablePlay": "{{playCode}} सक्रिय गर्ने"
+ "enablePlay": "{{playCode}} सक्रिय गर्ने",
+ "batchGroupSwitch": "«{{group}}» समूह स्विच टगल गर्नुहोस्"
},
"nameDialog": {
"title": "प्रदर्शित नाम (बहुभाषी)",
diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json
index 357a6ad..553e9eb 100644
--- a/src/i18n/locales/zh/config.json
+++ b/src/i18n/locales/zh/config.json
@@ -236,7 +236,8 @@
"readOnly": "只读"
},
"aria": {
- "enablePlay": "切换 {{playCode}} 启用状态"
+ "enablePlay": "切换 {{playCode}} 启用状态",
+ "batchGroupSwitch": "切换「{{group}}」批量开关"
},
"nameDialog": {
"title": "编辑显示名称",
diff --git a/src/lib/money.ts b/src/lib/money.ts
index e148a1b..16621d5 100644
--- a/src/lib/money.ts
+++ b/src/lib/money.ts
@@ -33,3 +33,37 @@ export function formatAdminMinorUnits(
maximumFractionDigits: resolvedDecimalPlaces,
})}`;
}
+
+export function formatAdminMinorDecimal(
+ minor: number,
+ currencyCode = "NPR",
+ decimalPlaces?: number,
+): string {
+ const resolvedDecimalPlaces =
+ typeof decimalPlaces === "number" && Number.isFinite(decimalPlaces) && decimalPlaces >= 0
+ ? decimalPlaces
+ : getAdminCurrencyDecimalPlaces(currencyCode);
+ const major = minor / 10 ** resolvedDecimalPlaces;
+ return major.toLocaleString(undefined, {
+ minimumFractionDigits: resolvedDecimalPlaces,
+ maximumFractionDigits: resolvedDecimalPlaces,
+ });
+}
+
+export function parseAdminMajorToMinor(
+ raw: string,
+ currencyCode = "NPR",
+ decimalPlaces?: number,
+): number | null {
+ const resolvedDecimalPlaces =
+ typeof decimalPlaces === "number" && Number.isFinite(decimalPlaces) && decimalPlaces >= 0
+ ? decimalPlaces
+ : getAdminCurrencyDecimalPlaces(currencyCode);
+ const cleaned = raw.replace(/,/g, "").trim();
+ if (!cleaned) return null;
+ const n = Number(cleaned);
+ if (!Number.isFinite(n) || n < 0) return null;
+ const factor = 10 ** resolvedDecimalPlaces;
+ const minor = Math.round(n * factor);
+ return Number.isSafeInteger(minor) ? minor : null;
+}
diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx
index 396d6fa..6e7212a 100644
--- a/src/modules/admin-roles/admin-roles-console.tsx
+++ b/src/modules/admin-roles/admin-roles-console.tsx
@@ -15,11 +15,10 @@ import {
putAdminRole,
putAdminRolePermissions,
} from "@/api/admin-users";
-import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Badge } from "@/components/ui/badge";
-import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Button } from "@/components/ui/button";
+import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
@@ -93,12 +92,6 @@ export function AdminRolesConsole(): React.ReactElement {
[draftRolePermissions],
);
- const selectClassName = cn(
- "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors",
- "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:text-sm",
- "dark:bg-input/30 disabled:cursor-not-allowed disabled:opacity-50",
- );
-
const directPermissionGroups = useMemo(() => {
const groups = catalog?.permission_menu_groups;
if (groups && groups.length > 0) {
@@ -376,9 +369,13 @@ export function AdminRolesConsole(): React.ReactElement {
)}
-
- {role.status === 1 ? t("status.enabled") : t("status.disabled")}
-
+
+
+
{role.user_count}
{role.permission_slugs.length}
@@ -554,12 +551,19 @@ export function AdminRolesConsole(): React.ReactElement {
{t("roleDialog.descriptionLabel")}
setRoleDescription(e.target.value)} />
-
-
{t("roleDialog.status")}
-
+
+
+
{t("roleDialog.status")}
+
+ {roleStatus === 1 ? t("status.enabled") : t("status.disabled")}
+
+
+
setRoleStatus(checked ? 1 : 0)}
+ />
diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx
index 2e7aaa6..eaf0848 100644
--- a/src/modules/admin-users/admin-users-console.tsx
+++ b/src/modules/admin-users/admin-users-console.tsx
@@ -16,10 +16,9 @@ import {
} from "@/api/admin-users";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
-import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Badge } from "@/components/ui/badge";
-import { resolveAdminUserStatusTone } from "@/lib/admin-status-tone";
import { Button } from "@/components/ui/button";
+import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
@@ -92,12 +91,6 @@ export function AdminUsersConsole(): React.ReactElement {
[catalog],
);
- const selectClassName = cn(
- "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors",
- "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:text-sm",
- "dark:bg-input/30 disabled:cursor-not-allowed disabled:opacity-50",
- );
-
const load = useCallback(async () => {
setLoading(true);
setErr(null);
@@ -398,9 +391,13 @@ export function AdminUsersConsole(): React.ReactElement {
{row.nickname ?? ""}
-
- {row.status === 0 ? t("status.enabled") : t("status.disabled")}
-
+
+
+
@@ -634,16 +631,19 @@ export function AdminUsersConsole(): React.ReactElement {
) : null}
-
-
{t("table.status")}
-
+
+
+
{t("table.status")}
+
+ {formStatus === 0 ? t("status.enabled") : t("status.disabled")}
+
+
+
setFormStatus(checked ? 0 : 1)}
+ />
diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx
index c3da3a6..6b8ea59 100644
--- a/src/modules/config/doc/odds-config-doc-screen.tsx
+++ b/src/modules/config/doc/odds-config-doc-screen.tsx
@@ -57,6 +57,15 @@ function oddsMultiplierLabel(oddsValue: number): string {
return (oddsValue / 10000).toFixed(4);
}
+function parseOddsMultiplierInput(raw: string): number {
+ const n = Number.parseFloat(raw);
+ if (!Number.isFinite(n) || n < 0) {
+ return 0;
+ }
+ const scaled = Math.round(n * 10000);
+ return Number.isSafeInteger(scaled) ? scaled : 0;
+}
+
function filterTypes(tab: CatTab, types: AdminPlayTypeRow[]): AdminPlayTypeRow[] {
if (tab === "all") {
return types;
@@ -515,19 +524,19 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro
{canEditDraft ? (
updateOddsForScope(scope, {
- odds_value: Number.parseInt(e.target.value, 10) || 0,
+ odds_value: parseOddsMultiplierInput(e.target.value),
})
}
/>
) : (
- {row.odds_value}
+ {oddsMultiplierLabel(row.odds_value)}
)}
{!embedded ? (
@@ -610,9 +619,11 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro
{row.label}
- {row.oldValue === null ? "—" : row.oldValue}
+ {row.oldValue === null ? "—" : oddsMultiplierLabel(row.oldValue)}
+
+
+ {row.newValue === null ? "—" : oddsMultiplierLabel(row.newValue)}
- {row.newValue ?? "—"}
))}
diff --git a/src/modules/config/doc/play-config-doc-screen.tsx b/src/modules/config/doc/play-config-doc-screen.tsx
index 0ece77a..8b9f7ea 100644
--- a/src/modules/config/doc/play-config-doc-screen.tsx
+++ b/src/modules/config/doc/play-config-doc-screen.tsx
@@ -19,7 +19,7 @@ import { ConfigChipGroup } from "@/modules/config/config-chip-group";
import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner";
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
import { ConfigSection } from "@/modules/config/config-section";
-import { Checkbox } from "@/components/ui/checkbox";
+import { Switch } from "@/components/ui/switch";
import {
Dialog,
DialogContent,
@@ -29,7 +29,6 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
@@ -46,6 +45,7 @@ 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 { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
import { PRD_PLAY_SWITCH_MANAGE } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
@@ -151,9 +151,6 @@ export function PlayConfigDocScreen() {
const [error, setError] = useState(null);
const detailRequestSeq = useRef(0);
- const [nameDialogOpen, setNameDialogOpen] = useState(false);
- const [namePlayCode, setNamePlayCode] = useState(null);
- const [nameDraft, setNameDraft] = useState("");
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [rulePlayCode, setRulePlayCode] = useState(null);
const [ruleDraftZh, setRuleDraftZh] = useState("");
@@ -258,6 +255,7 @@ export function PlayConfigDocScreen() {
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
const isDraft = selectedStatus === "draft";
+ const amountCurrencyCode = "NPR";
const orderedRows = useMemo(
() =>
@@ -373,30 +371,6 @@ export function PlayConfigDocScreen() {
}
}
- function openNameEditor(play_code: string) {
- const item = draftRows.find((row) => row.play_code === play_code);
- setNamePlayCode(play_code);
- setNameDraft(item?.display_name ?? item?.play_code ?? "");
- setNameDialogOpen(true);
- }
-
- function saveNameDraft() {
- if (!namePlayCode) {
- return;
- }
- const name = nameDraft.trim();
- if (!name) {
- toast.error(t("play.validation.displayNameRequired", { ns: "config" }));
- return;
- }
- updateConfigRow(namePlayCode, {
- display_name: name,
- });
- setNameDialogOpen(false);
- setNamePlayCode(null);
- toast.message(t("play.nameDialog.savedLocal", { ns: "config" }));
- }
-
function openRuleEditor(play_code: string) {
const item = draftRows.find((row) => row.play_code === play_code);
setRulePlayCode(play_code);
@@ -501,52 +475,53 @@ export function PlayConfigDocScreen() {
description={!isDraft ? t("play.readOnlyDraftHint", { ns: "config" }) : undefined}
>
- {batchSwitchStates.map((group) => (
-
-
-
{group.label}
-
- {group.total > 0
- ? t("play.batchEnabledCount", {
- ns: "config",
- enabledCount: group.enabledCount,
- total: group.total,
- })
- : t("play.noPlayTypes", { ns: "config" })}
-
-
-
+ );
+ })}
) : null}
@@ -562,7 +537,7 @@ export function PlayConfigDocScreen() {
{t("play.table.playCode", { ns: "config" })}
{t("play.table.category", { ns: "config" })}
{t("play.table.status", { ns: "config" })}
- {t("play.table.displayName", { ns: "config" })}
+ {t("play.table.displayName", { ns: "config" })}
{t("play.table.order", { ns: "config" })}
{t("play.table.minBet", { ns: "config" })}
{t("play.table.maxBet", { ns: "config" })}
@@ -575,12 +550,16 @@ export function PlayConfigDocScreen() {
{row.play_code}
{row.category ?? "—"}
- {isDraft ? (
-
+ {
- const enabled = v === true;
+ disabled={!isDraft || saving}
+ aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
+ onCheckedChange={(checked) => {
+ if (!isDraft) {
+ return;
+ }
+ const enabled = checked;
const action = enabled
? t("play.toggleEnable", { ns: "config" })
: t("play.toggleDisable", { ns: "config" });
@@ -598,35 +577,27 @@ export function PlayConfigDocScreen() {
},
});
}}
- aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
/>
- ) : (
-
-
- {row.is_enabled
- ? t("play.states.enabled", { ns: "config" })
- : t("play.states.disabled", { ns: "config" })}
-
-
- )}
+
-
+
{isDraft ? (
-
-
- {row.display_name ?? row.play_code}
-
-
-
+
+ updateConfigRow(row.play_code, { display_name: e.target.value })
+ }
+ onBlur={(e) => {
+ const trimmed = e.target.value.trim();
+ updateConfigRow(row.play_code, {
+ display_name: trimmed || row.play_code,
+ });
+ }}
+ />
) : (
{renderDisplayNameReadonly(row)}
@@ -658,19 +629,20 @@ export function PlayConfigDocScreen() {
{isDraft ? (
updateConfigRow(row.play_code, {
- min_bet_amount: Number.parseInt(e.target.value, 10) || 0,
+ min_bet_amount:
+ parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
})
}
/>
) : (
- {row.min_bet_amount}
+ {formatAdminMinorDecimal(row.min_bet_amount, amountCurrencyCode)}
)}
@@ -678,19 +650,20 @@ export function PlayConfigDocScreen() {
{isDraft ? (
updateConfigRow(row.play_code, {
- max_bet_amount: Number.parseInt(e.target.value, 10) || 0,
+ max_bet_amount:
+ parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
})
}
/>
) : (
- {row.max_bet_amount}
+ {formatAdminMinorDecimal(row.max_bet_amount, amountCurrencyCode)}
)}
@@ -716,33 +689,6 @@ export function PlayConfigDocScreen() {
)}
-
-