feat(i18n): add batch group switch text to English, Nepali, and Chinese locales
- Updated the English, Nepali, and Chinese locale files to include a new translation for "Toggle batch switch for {{group}}".
- Enhanced internationalization support for the admin interface by adding relevant strings for improved user experience.
This commit is contained in:
32
src/components/ui/switch.tsx
Normal file
32
src/components/ui/switch.tsx
Normal file
@@ -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 (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
@@ -236,7 +236,8 @@
|
|||||||
"readOnly": "Read only"
|
"readOnly": "Read only"
|
||||||
},
|
},
|
||||||
"aria": {
|
"aria": {
|
||||||
"enablePlay": "Enable {{playCode}}"
|
"enablePlay": "Enable {{playCode}}",
|
||||||
|
"batchGroupSwitch": "Toggle batch switch for {{group}}"
|
||||||
},
|
},
|
||||||
"nameDialog": {
|
"nameDialog": {
|
||||||
"title": "Edit display name",
|
"title": "Edit display name",
|
||||||
|
|||||||
@@ -236,7 +236,8 @@
|
|||||||
"readOnly": "केवल पढ्न मिल्ने"
|
"readOnly": "केवल पढ्न मिल्ने"
|
||||||
},
|
},
|
||||||
"aria": {
|
"aria": {
|
||||||
"enablePlay": "{{playCode}} सक्रिय गर्ने"
|
"enablePlay": "{{playCode}} सक्रिय गर्ने",
|
||||||
|
"batchGroupSwitch": "«{{group}}» समूह स्विच टगल गर्नुहोस्"
|
||||||
},
|
},
|
||||||
"nameDialog": {
|
"nameDialog": {
|
||||||
"title": "प्रदर्शित नाम (बहुभाषी)",
|
"title": "प्रदर्शित नाम (बहुभाषी)",
|
||||||
|
|||||||
@@ -236,7 +236,8 @@
|
|||||||
"readOnly": "只读"
|
"readOnly": "只读"
|
||||||
},
|
},
|
||||||
"aria": {
|
"aria": {
|
||||||
"enablePlay": "切换 {{playCode}} 启用状态"
|
"enablePlay": "切换 {{playCode}} 启用状态",
|
||||||
|
"batchGroupSwitch": "切换「{{group}}」批量开关"
|
||||||
},
|
},
|
||||||
"nameDialog": {
|
"nameDialog": {
|
||||||
"title": "编辑显示名称",
|
"title": "编辑显示名称",
|
||||||
|
|||||||
@@ -33,3 +33,37 @@ export function formatAdminMinorUnits(
|
|||||||
maximumFractionDigits: resolvedDecimalPlaces,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ import {
|
|||||||
putAdminRole,
|
putAdminRole,
|
||||||
putAdminRolePermissions,
|
putAdminRolePermissions,
|
||||||
} from "@/api/admin-users";
|
} from "@/api/admin-users";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
|
|
||||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
@@ -93,12 +92,6 @@ export function AdminRolesConsole(): React.ReactElement {
|
|||||||
[draftRolePermissions],
|
[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 directPermissionGroups = useMemo(() => {
|
||||||
const groups = catalog?.permission_menu_groups;
|
const groups = catalog?.permission_menu_groups;
|
||||||
if (groups && groups.length > 0) {
|
if (groups && groups.length > 0) {
|
||||||
@@ -376,9 +369,13 @@ export function AdminRolesConsole(): React.ReactElement {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={role.status} tone={resolveRoleStatusTone(role.status)}>
|
<div className="flex justify-center">
|
||||||
{role.status === 1 ? t("status.enabled") : t("status.disabled")}
|
<Switch
|
||||||
</AdminStatusBadge>
|
checked={role.status === 1}
|
||||||
|
disabled
|
||||||
|
aria-label={t("roleDialog.status")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="tabular-nums">{role.user_count}</TableCell>
|
<TableCell className="tabular-nums">{role.user_count}</TableCell>
|
||||||
<TableCell className="tabular-nums">{role.permission_slugs.length}</TableCell>
|
<TableCell className="tabular-nums">{role.permission_slugs.length}</TableCell>
|
||||||
@@ -554,12 +551,19 @@ export function AdminRolesConsole(): React.ReactElement {
|
|||||||
<div className="text-sm font-medium leading-none">{t("roleDialog.descriptionLabel")}</div>
|
<div className="text-sm font-medium leading-none">{t("roleDialog.descriptionLabel")}</div>
|
||||||
<Input value={roleDescription} onChange={(e) => setRoleDescription(e.target.value)} />
|
<Input value={roleDescription} onChange={(e) => setRoleDescription(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="flex items-center justify-between rounded-xl border border-border/70 p-3">
|
||||||
<div className="text-sm font-medium leading-none">{t("roleDialog.status")}</div>
|
<div className="space-y-1">
|
||||||
<select className={selectClassName} value={roleStatus} onChange={(e) => setRoleStatus(Number(e.target.value))}>
|
<p className="text-sm font-medium">{t("roleDialog.status")}</p>
|
||||||
<option value={1}>{t("status.enabled")}</option>
|
<p className="text-xs text-muted-foreground">
|
||||||
<option value={0}>{t("status.disabled")}</option>
|
{roleStatus === 1 ? t("status.enabled") : t("status.disabled")}
|
||||||
</select>
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={roleStatus === 1}
|
||||||
|
disabled={roleFormSaving}
|
||||||
|
aria-label={t("roleDialog.status")}
|
||||||
|
onCheckedChange={(checked) => setRoleStatus(checked ? 1 : 0)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ import {
|
|||||||
} from "@/api/admin-users";
|
} from "@/api/admin-users";
|
||||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { resolveAdminUserStatusTone } from "@/lib/admin-status-tone";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
@@ -92,12 +91,6 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
[catalog],
|
[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 () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
@@ -398,9 +391,13 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.nickname ?? ""}</TableCell>
|
<TableCell>{row.nickname ?? ""}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={row.status} tone={resolveAdminUserStatusTone(row.status)}>
|
<div className="flex justify-center">
|
||||||
{row.status === 0 ? t("status.enabled") : t("status.disabled")}
|
<Switch
|
||||||
</AdminStatusBadge>
|
checked={row.status === 0}
|
||||||
|
disabled
|
||||||
|
aria-label={t("table.status")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
@@ -634,16 +631,19 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="space-y-1.5">
|
<div className="flex items-center justify-between rounded-xl border border-border/70 p-3">
|
||||||
<div className="text-sm font-medium leading-none">{t("table.status")}</div>
|
<div className="space-y-1">
|
||||||
<select
|
<p className="text-sm font-medium">{t("table.status")}</p>
|
||||||
className={selectClassName}
|
<p className="text-xs text-muted-foreground">
|
||||||
value={formStatus}
|
{formStatus === 0 ? t("status.enabled") : t("status.disabled")}
|
||||||
onChange={(e) => setFormStatus(Number(e.target.value))}
|
</p>
|
||||||
>
|
</div>
|
||||||
<option value={0}>{t("status.enabled")}</option>
|
<Switch
|
||||||
<option value={1}>{t("status.disabled")}</option>
|
checked={formStatus === 0}
|
||||||
</select>
|
disabled={accountSaving}
|
||||||
|
aria-label={t("table.status")}
|
||||||
|
onCheckedChange={(checked) => setFormStatus(checked ? 0 : 1)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ function oddsMultiplierLabel(oddsValue: number): string {
|
|||||||
return (oddsValue / 10000).toFixed(4);
|
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[] {
|
function filterTypes(tab: CatTab, types: AdminPlayTypeRow[]): AdminPlayTypeRow[] {
|
||||||
if (tab === "all") {
|
if (tab === "all") {
|
||||||
return types;
|
return types;
|
||||||
@@ -515,19 +524,19 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro
|
|||||||
{canEditDraft ? (
|
{canEditDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="decimal"
|
||||||
className="h-9 max-w-[200px] font-mono tabular-nums"
|
className="h-9 max-w-[200px] font-mono tabular-nums"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
value={row.odds_value}
|
value={oddsMultiplierLabel(row.odds_value)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateOddsForScope(scope, {
|
updateOddsForScope(scope, {
|
||||||
odds_value: Number.parseInt(e.target.value, 10) || 0,
|
odds_value: parseOddsMultiplierInput(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfigReadonlyValue mono className="max-w-[200px]">
|
<ConfigReadonlyValue mono className="max-w-[200px]">
|
||||||
{row.odds_value}
|
{oddsMultiplierLabel(row.odds_value)}
|
||||||
</ConfigReadonlyValue>
|
</ConfigReadonlyValue>
|
||||||
)}
|
)}
|
||||||
{!embedded ? (
|
{!embedded ? (
|
||||||
@@ -610,9 +619,11 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro
|
|||||||
<div key={row.scope} className="grid grid-cols-3 px-3 py-2 text-sm">
|
<div key={row.scope} className="grid grid-cols-3 px-3 py-2 text-sm">
|
||||||
<span>{row.label}</span>
|
<span>{row.label}</span>
|
||||||
<span className="text-right font-mono tabular-nums">
|
<span className="text-right font-mono tabular-nums">
|
||||||
{row.oldValue === null ? "—" : row.oldValue}
|
{row.oldValue === null ? "—" : oddsMultiplierLabel(row.oldValue)}
|
||||||
|
</span>
|
||||||
|
<span className="text-right font-mono tabular-nums">
|
||||||
|
{row.newValue === null ? "—" : oddsMultiplierLabel(row.newValue)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-right font-mono tabular-nums">{row.newValue ?? "—"}</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { ConfigChipGroup } from "@/modules/config/config-chip-group";
|
|||||||
import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner";
|
import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner";
|
||||||
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
|
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
|
||||||
import { ConfigSection } from "@/modules/config/config-section";
|
import { ConfigSection } from "@/modules/config/config-section";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -46,6 +45,7 @@ import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher"
|
|||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
|
||||||
import { PRD_PLAY_SWITCH_MANAGE } from "@/lib/admin-prd";
|
import { PRD_PLAY_SWITCH_MANAGE } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -151,9 +151,6 @@ export function PlayConfigDocScreen() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const detailRequestSeq = useRef(0);
|
const detailRequestSeq = useRef(0);
|
||||||
|
|
||||||
const [nameDialogOpen, setNameDialogOpen] = useState(false);
|
|
||||||
const [namePlayCode, setNamePlayCode] = useState<string | null>(null);
|
|
||||||
const [nameDraft, setNameDraft] = useState("");
|
|
||||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||||
const [rulePlayCode, setRulePlayCode] = useState<string | null>(null);
|
const [rulePlayCode, setRulePlayCode] = useState<string | null>(null);
|
||||||
const [ruleDraftZh, setRuleDraftZh] = useState("");
|
const [ruleDraftZh, setRuleDraftZh] = useState("");
|
||||||
@@ -258,6 +255,7 @@ export function PlayConfigDocScreen() {
|
|||||||
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
|
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
|
||||||
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
|
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
|
||||||
const isDraft = selectedStatus === "draft";
|
const isDraft = selectedStatus === "draft";
|
||||||
|
const amountCurrencyCode = "NPR";
|
||||||
|
|
||||||
const orderedRows = useMemo(
|
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) {
|
function openRuleEditor(play_code: string) {
|
||||||
const item = draftRows.find((row) => row.play_code === play_code);
|
const item = draftRows.find((row) => row.play_code === play_code);
|
||||||
setRulePlayCode(play_code);
|
setRulePlayCode(play_code);
|
||||||
@@ -501,52 +475,53 @@ export function PlayConfigDocScreen() {
|
|||||||
description={!isDraft ? t("play.readOnlyDraftHint", { ns: "config" }) : undefined}
|
description={!isDraft ? t("play.readOnlyDraftHint", { ns: "config" }) : undefined}
|
||||||
>
|
>
|
||||||
<ConfigChipGroup>
|
<ConfigChipGroup>
|
||||||
{batchSwitchStates.map((group) => (
|
{batchSwitchStates.map((group) => {
|
||||||
<div
|
const groupOn = group.enabledCount > 0;
|
||||||
key={group.key}
|
return (
|
||||||
className="flex items-center gap-3 rounded-xl border border-border/60 bg-card px-4 py-3"
|
<div
|
||||||
>
|
key={group.key}
|
||||||
<div className="min-w-[100px]">
|
className="flex items-center justify-between gap-4 rounded-xl border border-border/60 bg-card px-4 py-3"
|
||||||
<p className="text-sm font-medium text-foreground">{group.label}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{group.total > 0
|
|
||||||
? t("play.batchEnabledCount", {
|
|
||||||
ns: "config",
|
|
||||||
enabledCount: group.enabledCount,
|
|
||||||
total: group.total,
|
|
||||||
})
|
|
||||||
: t("play.noPlayTypes", { ns: "config" })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant={group.allEnabled ? "secondary" : "outline"}
|
|
||||||
disabled={!isDraft || saving || group.total === 0}
|
|
||||||
onClick={() => {
|
|
||||||
const enable = !group.allEnabled;
|
|
||||||
const action = enable
|
|
||||||
? t("play.batchSwitchEnable", { ns: "config" })
|
|
||||||
: t("play.batchSwitchDisable", { ns: "config" });
|
|
||||||
requestConfirm({
|
|
||||||
title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
|
|
||||||
description: t("play.batchSwitchConfirmDescription", {
|
|
||||||
ns: "config",
|
|
||||||
action,
|
|
||||||
group: group.label,
|
|
||||||
count: group.total,
|
|
||||||
}),
|
|
||||||
confirmVariant: enable ? "default" : "destructive",
|
|
||||||
onConfirm: () => applyBatchSwitch(group, enable),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{group.allEnabled
|
<div className="min-w-0">
|
||||||
? t("play.actions.disable", { ns: "config" })
|
<p className="text-sm font-medium text-foreground">{group.label}</p>
|
||||||
: t("play.actions.enable", { ns: "config" })}
|
<p className="text-sm text-muted-foreground">
|
||||||
</Button>
|
{group.total > 0
|
||||||
</div>
|
? t("play.batchEnabledCount", {
|
||||||
))}
|
ns: "config",
|
||||||
|
enabledCount: group.enabledCount,
|
||||||
|
total: group.total,
|
||||||
|
})
|
||||||
|
: t("play.noPlayTypes", { ns: "config" })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={groupOn}
|
||||||
|
disabled={!isDraft || saving || group.total === 0}
|
||||||
|
aria-label={t("play.aria.batchGroupSwitch", {
|
||||||
|
ns: "config",
|
||||||
|
group: group.label,
|
||||||
|
})}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const enable = checked;
|
||||||
|
const action = enable
|
||||||
|
? t("play.batchSwitchEnable", { ns: "config" })
|
||||||
|
: t("play.batchSwitchDisable", { ns: "config" });
|
||||||
|
requestConfirm({
|
||||||
|
title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
|
||||||
|
description: t("play.batchSwitchConfirmDescription", {
|
||||||
|
ns: "config",
|
||||||
|
action,
|
||||||
|
group: group.label,
|
||||||
|
count: group.total,
|
||||||
|
}),
|
||||||
|
confirmVariant: enable ? "default" : "destructive",
|
||||||
|
onConfirm: () => applyBatchSwitch(group, enable),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ConfigChipGroup>
|
</ConfigChipGroup>
|
||||||
</ConfigSection>
|
</ConfigSection>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -562,7 +537,7 @@ export function PlayConfigDocScreen() {
|
|||||||
<TableHead className="text-center">{t("play.table.playCode", { ns: "config" })}</TableHead>
|
<TableHead className="text-center">{t("play.table.playCode", { ns: "config" })}</TableHead>
|
||||||
<TableHead className="w-[100px] text-center">{t("play.table.category", { ns: "config" })}</TableHead>
|
<TableHead className="w-[100px] text-center">{t("play.table.category", { ns: "config" })}</TableHead>
|
||||||
<TableHead className="w-[88px] text-center">{t("play.table.status", { ns: "config" })}</TableHead>
|
<TableHead className="w-[88px] text-center">{t("play.table.status", { ns: "config" })}</TableHead>
|
||||||
<TableHead className="min-w-[120px] text-center">{t("play.table.displayName", { ns: "config" })}</TableHead>
|
<TableHead className="w-32 text-center">{t("play.table.displayName", { ns: "config" })}</TableHead>
|
||||||
<TableHead className="w-[120px] text-center">{t("play.table.order", { ns: "config" })}</TableHead>
|
<TableHead className="w-[120px] text-center">{t("play.table.order", { ns: "config" })}</TableHead>
|
||||||
<TableHead className="w-[110px] text-center">{t("play.table.minBet", { ns: "config" })}</TableHead>
|
<TableHead className="w-[110px] text-center">{t("play.table.minBet", { ns: "config" })}</TableHead>
|
||||||
<TableHead className="w-[110px] text-center">{t("play.table.maxBet", { ns: "config" })}</TableHead>
|
<TableHead className="w-[110px] text-center">{t("play.table.maxBet", { ns: "config" })}</TableHead>
|
||||||
@@ -575,12 +550,16 @@ export function PlayConfigDocScreen() {
|
|||||||
<TableCell className="text-center font-mono text-sm">{row.play_code}</TableCell>
|
<TableCell className="text-center font-mono text-sm">{row.play_code}</TableCell>
|
||||||
<TableCell className="text-center text-muted-foreground text-sm">{row.category ?? "—"}</TableCell>
|
<TableCell className="text-center text-muted-foreground text-sm">{row.category ?? "—"}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{isDraft ? (
|
<div className="flex justify-center">
|
||||||
<Checkbox
|
<Switch
|
||||||
checked={row.is_enabled}
|
checked={row.is_enabled}
|
||||||
disabled={saving}
|
disabled={!isDraft || saving}
|
||||||
onCheckedChange={(v) => {
|
aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
|
||||||
const enabled = v === true;
|
onCheckedChange={(checked) => {
|
||||||
|
if (!isDraft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const enabled = checked;
|
||||||
const action = enabled
|
const action = enabled
|
||||||
? t("play.toggleEnable", { ns: "config" })
|
? t("play.toggleEnable", { ns: "config" })
|
||||||
: t("play.toggleDisable", { 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 })}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<div className="flex justify-center">
|
|
||||||
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
|
|
||||||
{row.is_enabled
|
|
||||||
? t("play.states.enabled", { ns: "config" })
|
|
||||||
: t("play.states.disabled", { ns: "config" })}
|
|
||||||
</AdminStatusBadge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="w-32 text-center">
|
||||||
{isDraft ? (
|
{isDraft ? (
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
<Input
|
||||||
<p className="max-w-[10rem] truncate text-sm font-medium">
|
type="text"
|
||||||
{row.display_name ?? row.play_code}
|
className="mx-auto h-8 w-28 text-center text-sm"
|
||||||
</p>
|
disabled={saving}
|
||||||
<Button
|
value={row.display_name ?? ""}
|
||||||
type="button"
|
placeholder={row.play_code}
|
||||||
variant="outline"
|
onChange={(e) =>
|
||||||
size="sm"
|
updateConfigRow(row.play_code, { display_name: e.target.value })
|
||||||
className="h-7 text-xs"
|
}
|
||||||
disabled={saving}
|
onBlur={(e) => {
|
||||||
onClick={() => openNameEditor(row.play_code)}
|
const trimmed = e.target.value.trim();
|
||||||
>
|
updateConfigRow(row.play_code, {
|
||||||
{t("play.actions.editDisplayName", { ns: "config" })}
|
display_name: trimmed || row.play_code,
|
||||||
</Button>
|
});
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfigReadonlyValue className="justify-center">
|
<ConfigReadonlyValue className="justify-center">
|
||||||
{renderDisplayNameReadonly(row)}
|
{renderDisplayNameReadonly(row)}
|
||||||
@@ -658,19 +629,20 @@ export function PlayConfigDocScreen() {
|
|||||||
{isDraft ? (
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="decimal"
|
||||||
className="h-8 text-center font-mono tabular-nums"
|
className="h-8 text-center font-mono tabular-nums"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
value={row.min_bet_amount}
|
value={formatAdminMinorDecimal(row.min_bet_amount, amountCurrencyCode)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfigRow(row.play_code, {
|
updateConfigRow(row.play_code, {
|
||||||
min_bet_amount: Number.parseInt(e.target.value, 10) || 0,
|
min_bet_amount:
|
||||||
|
parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfigReadonlyValue mono className="justify-center">
|
<ConfigReadonlyValue mono className="justify-center">
|
||||||
{row.min_bet_amount}
|
{formatAdminMinorDecimal(row.min_bet_amount, amountCurrencyCode)}
|
||||||
</ConfigReadonlyValue>
|
</ConfigReadonlyValue>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -678,19 +650,20 @@ export function PlayConfigDocScreen() {
|
|||||||
{isDraft ? (
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="decimal"
|
||||||
className="h-8 text-center font-mono tabular-nums"
|
className="h-8 text-center font-mono tabular-nums"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
value={row.max_bet_amount}
|
value={formatAdminMinorDecimal(row.max_bet_amount, amountCurrencyCode)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfigRow(row.play_code, {
|
updateConfigRow(row.play_code, {
|
||||||
max_bet_amount: Number.parseInt(e.target.value, 10) || 0,
|
max_bet_amount:
|
||||||
|
parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfigReadonlyValue mono className="justify-center">
|
<ConfigReadonlyValue mono className="justify-center">
|
||||||
{row.max_bet_amount}
|
{formatAdminMinorDecimal(row.max_bet_amount, amountCurrencyCode)}
|
||||||
</ConfigReadonlyValue>
|
</ConfigReadonlyValue>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -716,33 +689,6 @@ export function PlayConfigDocScreen() {
|
|||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={nameDialogOpen} onOpenChange={setNameDialogOpen}>
|
|
||||||
<DialogContent showCloseButton className="sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("play.nameDialog.title", { ns: "config" })}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t("play.nameDialog.description", { ns: "config", playCode: namePlayCode ?? "—" })}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
<Label htmlFor="play-display-name">{t("play.table.displayName", { ns: "config" })}</Label>
|
|
||||||
<Input
|
|
||||||
id="play-display-name"
|
|
||||||
value={nameDraft}
|
|
||||||
onChange={(e) => setNameDraft(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" onClick={() => setNameDialogOpen(false)}>
|
|
||||||
{t("actions.cancel", { ns: "adminUsers" })}
|
|
||||||
</Button>
|
|
||||||
<Button type="button" onClick={saveNameDraft}>
|
|
||||||
{t("play.nameDialog.apply", { ns: "config" })}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog open={ruleDialogOpen} onOpenChange={setRuleDialogOpen}>
|
<Dialog open={ruleDialogOpen} onOpenChange={setRuleDialogOpen}>
|
||||||
<DialogContent showCloseButton className="sm:max-w-lg">
|
<DialogContent showCloseButton className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "@/api/admin-config";
|
} from "@/api/admin-config";
|
||||||
import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner";
|
import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner";
|
||||||
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
|
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||||
@@ -40,9 +40,9 @@ import { PRIZE_SCOPE_ORDER } from "@/modules/config/doc/prize-scopes";
|
|||||||
function rateToPercentUi(rateStr: string): string {
|
function rateToPercentUi(rateStr: string): string {
|
||||||
const n = Number.parseFloat(rateStr);
|
const n = Number.parseFloat(rateStr);
|
||||||
if (!Number.isFinite(n)) {
|
if (!Number.isFinite(n)) {
|
||||||
return "0";
|
return "0.00";
|
||||||
}
|
}
|
||||||
return String(Math.round(n * 10000) / 100);
|
return (Math.round(n * 10000) / 100).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPlayTypeRow[]): string {
|
function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPlayTypeRow[]): string {
|
||||||
@@ -391,19 +391,11 @@ export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScree
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3 px-1">
|
<div className="flex items-center justify-between gap-3 rounded-xl border border-border/60 px-4 py-3">
|
||||||
<Checkbox
|
<Label htmlFor="win-enjoy" className="font-medium leading-snug">
|
||||||
id="win-enjoy"
|
{t("rebate.winEnjoy.label", { ns: "config" })}
|
||||||
checked
|
</Label>
|
||||||
aria-disabled
|
<Switch id="win-enjoy" checked disabled aria-label={t("rebate.winEnjoy.label", { ns: "config" })} />
|
||||||
disabled
|
|
||||||
aria-label={t("rebate.winEnjoy.label", { ns: "config" })}
|
|
||||||
/>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="win-enjoy" className="font-medium leading-snug">
|
|
||||||
{t("rebate.winEnjoy.label", { ns: "config" })}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!embedded ? (
|
{!embedded ? (
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
|||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
|
||||||
import { PRD_RISK_CAP_MANAGE, PRD_RISK_CAP_VIEW } from "@/lib/admin-prd";
|
import { PRD_RISK_CAP_MANAGE, PRD_RISK_CAP_VIEW } from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -95,6 +96,7 @@ export function RiskCapDocScreen() {
|
|||||||
const [syncOpen, setSyncOpen] = useState(false);
|
const [syncOpen, setSyncOpen] = useState(false);
|
||||||
|
|
||||||
const [occSearch, setOccSearch] = useState("");
|
const [occSearch, setOccSearch] = useState("");
|
||||||
|
const amountCurrencyCode = "NPR";
|
||||||
|
|
||||||
const refreshList = useCallback(async () => {
|
const refreshList = useCallback(async () => {
|
||||||
setLoadingList(true);
|
setLoadingList(true);
|
||||||
@@ -123,7 +125,7 @@ export function RiskCapDocScreen() {
|
|||||||
setDefaultCapStr("");
|
setDefaultCapStr("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDefaultCapStr(String(defaultRow.cap_amount));
|
setDefaultCapStr(formatAdminMinorDecimal(defaultRow.cap_amount, amountCurrencyCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDetail = useCallback(async (id: number) => {
|
const loadDetail = useCallback(async (id: number) => {
|
||||||
@@ -299,7 +301,7 @@ export function RiskCapDocScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyDefaultCap() {
|
function applyDefaultCap() {
|
||||||
const n = Number.parseInt(defaultCapStr, 10);
|
const n = parseAdminMajorToMinor(defaultCapStr, amountCurrencyCode);
|
||||||
if (!Number.isFinite(n) || n <= 0) {
|
if (!Number.isFinite(n) || n <= 0) {
|
||||||
toast.error(t("riskCap.validation.enterValidCapAmount", { ns: "config" }));
|
toast.error(t("riskCap.validation.enterValidCapAmount", { ns: "config" }));
|
||||||
return;
|
return;
|
||||||
@@ -408,7 +410,7 @@ export function RiskCapDocScreen() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfigReadonlyValue mono className="w-[220px]">
|
<ConfigReadonlyValue mono className="w-[220px]">
|
||||||
{defaultCapStr || "—"}
|
{defaultCapStr || formatAdminMinorDecimal(0, amountCurrencyCode)}
|
||||||
</ConfigReadonlyValue>
|
</ConfigReadonlyValue>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -474,19 +476,22 @@ export function RiskCapDocScreen() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{canEditDraft ? (
|
{canEditDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
min={0}
|
inputMode="decimal"
|
||||||
className="h-8 font-mono tabular-nums"
|
className="h-8 font-mono tabular-nums"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
value={r.cap_amount}
|
value={formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateRow(idx, {
|
updateRow(idx, {
|
||||||
cap_amount: Number.parseInt(e.target.value, 10) || 0,
|
cap_amount:
|
||||||
|
parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfigReadonlyValue mono>{r.cap_amount}</ConfigReadonlyValue>
|
<ConfigReadonlyValue mono>
|
||||||
|
{formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-muted-foreground tabular-nums text-sm">—</TableCell>
|
<TableCell className="text-right text-muted-foreground tabular-nums text-sm">—</TableCell>
|
||||||
|
|||||||
@@ -10,19 +10,14 @@ import {
|
|||||||
} from "@/api/admin-jackpot";
|
} from "@/api/admin-jackpot";
|
||||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
|
||||||
import { PRD_JACKPOT_MANAGE, PRD_JACKPOT_MANUAL_BURST } from "@/lib/admin-prd";
|
import { PRD_JACKPOT_MANAGE, PRD_JACKPOT_MANUAL_BURST } from "@/lib/admin-prd";
|
||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import { Switch } from "@/components/ui/switch";
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -50,12 +45,12 @@ type Draft = {
|
|||||||
|
|
||||||
function toDraft(p: AdminJackpotPoolRow): Draft {
|
function toDraft(p: AdminJackpotPoolRow): Draft {
|
||||||
return {
|
return {
|
||||||
current_amount: String(p.current_amount),
|
current_amount: formatAdminMinorDecimal(p.current_amount, p.currency_code),
|
||||||
contribution_rate: String(p.contribution_rate),
|
contribution_rate: String(p.contribution_rate),
|
||||||
trigger_threshold: String(p.trigger_threshold),
|
trigger_threshold: formatAdminMinorDecimal(p.trigger_threshold, p.currency_code),
|
||||||
payout_rate: String(p.payout_rate),
|
payout_rate: String(p.payout_rate),
|
||||||
force_trigger_draw_gap: String(p.force_trigger_draw_gap),
|
force_trigger_draw_gap: String(p.force_trigger_draw_gap),
|
||||||
min_bet_amount: String(p.min_bet_amount),
|
min_bet_amount: formatAdminMinorDecimal(p.min_bet_amount, p.currency_code),
|
||||||
combo_trigger_play_codes: p.combo_trigger_play_codes.join(","),
|
combo_trigger_play_codes: p.combo_trigger_play_codes.join(","),
|
||||||
status: String(p.status),
|
status: String(p.status),
|
||||||
manual_burst_draw_id: "",
|
manual_burst_draw_id: "",
|
||||||
@@ -116,12 +111,12 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
setSavingId(p.id);
|
setSavingId(p.id);
|
||||||
try {
|
try {
|
||||||
await putAdminJackpotPool(p.id, {
|
await putAdminJackpotPool(p.id, {
|
||||||
current_amount: Number.parseInt(d.current_amount, 10),
|
current_amount: parseAdminMajorToMinor(d.current_amount, p.currency_code) ?? 0,
|
||||||
contribution_rate: Number(d.contribution_rate),
|
contribution_rate: Number(d.contribution_rate),
|
||||||
trigger_threshold: Number.parseInt(d.trigger_threshold, 10),
|
trigger_threshold: parseAdminMajorToMinor(d.trigger_threshold, p.currency_code) ?? 0,
|
||||||
payout_rate: Number(d.payout_rate),
|
payout_rate: Number(d.payout_rate),
|
||||||
force_trigger_draw_gap: Number.parseInt(d.force_trigger_draw_gap, 10),
|
force_trigger_draw_gap: Number.parseInt(d.force_trigger_draw_gap, 10),
|
||||||
min_bet_amount: Number.parseInt(d.min_bet_amount, 10),
|
min_bet_amount: parseAdminMajorToMinor(d.min_bet_amount, p.currency_code) ?? 0,
|
||||||
combo_trigger_play_codes: d.combo_trigger_play_codes
|
combo_trigger_play_codes: d.combo_trigger_play_codes
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((v) => v.trim().toLowerCase())
|
.map((v) => v.trim().toLowerCase())
|
||||||
@@ -240,20 +235,19 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
onChange={(e) => updateDraft(p.id, { combo_trigger_play_codes: e.target.value })}
|
onChange={(e) => updateDraft(p.id, { combo_trigger_play_codes: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="flex items-center justify-between gap-3 rounded-lg border border-border/60 px-3 py-2.5">
|
||||||
<Label htmlFor={`status-${p.id}`}>{t("status")}</Label>
|
<Label htmlFor={`status-${p.id}`} className="text-sm font-medium">
|
||||||
<Select
|
{t("status")}
|
||||||
value={d.status}
|
</Label>
|
||||||
onValueChange={(v) => updateDraft(p.id, { status: v ?? "0" })}
|
<Switch
|
||||||
>
|
id={`status-${p.id}`}
|
||||||
<SelectTrigger id={`status-${p.id}`} className="w-full">
|
checked={d.status === "1"}
|
||||||
<SelectValue>{d.status === "1" ? t("enabled") : t("disabled")}</SelectValue>
|
disabled={!canManageJackpot}
|
||||||
</SelectTrigger>
|
aria-label={t("status")}
|
||||||
<SelectContent>
|
onCheckedChange={(checked) =>
|
||||||
<SelectItem value="0">{t("disabled")}</SelectItem>
|
updateDraft(p.id, { status: checked ? "1" : "0" })
|
||||||
<SelectItem value="1">{t("enabled")}</SelectItem>
|
}
|
||||||
</SelectContent>
|
/>
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{canManageJackpot ? (
|
{canManageJackpot ? (
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ import {
|
|||||||
} from "@/api/admin-player";
|
} from "@/api/admin-player";
|
||||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
|
||||||
import { resolvePlayerStatusTone } from "@/lib/admin-status-tone";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -377,9 +376,35 @@ export function PlayersConsole(): React.ReactElement {
|
|||||||
: "—"}
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={row.status} tone={resolvePlayerStatusTone(row.status)}>
|
{canFreezePlayers ? (
|
||||||
{playerStatusLabelT(row.status, t)}
|
<div className="flex justify-center">
|
||||||
</AdminStatusBadge>
|
<Switch
|
||||||
|
checked={row.status === 0}
|
||||||
|
disabled={freezeBusyId === row.id}
|
||||||
|
aria-label={t("status")}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const name = row.username ?? row.site_player_id;
|
||||||
|
if (checked) {
|
||||||
|
requestConfirm({
|
||||||
|
title: t("confirmUnfreezeTitle"),
|
||||||
|
description: t("confirmUnfreezeDescription", { name }),
|
||||||
|
onConfirm: () => toggleFreeze(row, false),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestConfirm({
|
||||||
|
title: t("confirmFreezeTitle"),
|
||||||
|
description: t("confirmFreezeDescription", { name }),
|
||||||
|
onConfirm: () => toggleFreeze(row, true),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{playerStatusLabelT(row.status, t)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
||||||
{row.last_login_at
|
{row.last_login_at
|
||||||
@@ -395,42 +420,6 @@ export function PlayersConsole(): React.ReactElement {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{canManagePlayers || canFreezePlayers ? (
|
{canManagePlayers || canFreezePlayers ? (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{canFreezePlayers && row.status === 0 ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={freezeBusyId === row.id}
|
|
||||||
onClick={() => {
|
|
||||||
const name = row.username ?? row.site_player_id;
|
|
||||||
requestConfirm({
|
|
||||||
title: t("confirmFreezeTitle"),
|
|
||||||
description: t("confirmFreezeDescription", { name }),
|
|
||||||
onConfirm: () => toggleFreeze(row, true),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{freezeBusyId === row.id ? t("saving") : t("freeze")}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canFreezePlayers && row.status === 1 ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={freezeBusyId === row.id}
|
|
||||||
onClick={() => {
|
|
||||||
const name = row.username ?? row.site_player_id;
|
|
||||||
requestConfirm({
|
|
||||||
title: t("confirmUnfreezeTitle"),
|
|
||||||
description: t("confirmUnfreezeDescription", { name }),
|
|
||||||
onConfirm: () => toggleFreeze(row, false),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{freezeBusyId === row.id ? t("saving") : t("unfreeze")}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canManagePlayers ? (
|
{canManagePlayers ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import {
|
|||||||
putAdminCurrency,
|
putAdminCurrency,
|
||||||
} from "@/api/admin-currencies";
|
} from "@/api/admin-currencies";
|
||||||
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -251,20 +250,22 @@ export function CurrencySettingsPanel() {
|
|||||||
<TableCell>{row.name}</TableCell>
|
<TableCell>{row.name}</TableCell>
|
||||||
<TableCell>{row.decimal_places}</TableCell>
|
<TableCell>{row.decimal_places}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
|
<div className="flex justify-center">
|
||||||
{row.is_enabled
|
<Switch
|
||||||
? t("system.states.enabled", { ns: "config" })
|
checked={row.is_enabled}
|
||||||
: t("system.states.disabled", { ns: "config" })}
|
disabled
|
||||||
</AdminStatusBadge>
|
aria-label={t("currencies.form.enabled", { ns: "config", code: row.code })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge
|
<div className="flex justify-center">
|
||||||
status={row.is_enabled && row.is_bettable ? "enabled" : "disabled"}
|
<Switch
|
||||||
>
|
checked={row.is_enabled && row.is_bettable}
|
||||||
{row.is_enabled && row.is_bettable
|
disabled
|
||||||
? t("system.states.enabled", { ns: "config" })
|
aria-label={t("currencies.form.bettable", { ns: "config", code: row.code })}
|
||||||
: t("system.states.disabled", { ns: "config" })}
|
/>
|
||||||
</AdminStatusBadge>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
@@ -337,10 +338,11 @@ export function CurrencySettingsPanel() {
|
|||||||
<p className="text-sm font-medium">{t("currencies.form.enabled", { ns: "config" })}</p>
|
<p className="text-sm font-medium">{t("currencies.form.enabled", { ns: "config" })}</p>
|
||||||
<p className="text-xs text-muted-foreground">{t("currencies.form.enabledHint", { ns: "config" })}</p>
|
<p className="text-xs text-muted-foreground">{t("currencies.form.enabledHint", { ns: "config" })}</p>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Switch
|
||||||
checked={form.is_enabled}
|
checked={form.is_enabled}
|
||||||
onCheckedChange={(checked) => updateForm("is_enabled", checked === true)}
|
onCheckedChange={(checked) => updateForm("is_enabled", checked)}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
|
aria-label={t("currencies.form.enabled", { ns: "config" })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -349,10 +351,11 @@ export function CurrencySettingsPanel() {
|
|||||||
<p className="text-sm font-medium">{t("currencies.form.bettable", { ns: "config" })}</p>
|
<p className="text-sm font-medium">{t("currencies.form.bettable", { ns: "config" })}</p>
|
||||||
<p className="text-xs text-muted-foreground">{t("currencies.form.bettableHint", { ns: "config" })}</p>
|
<p className="text-xs text-muted-foreground">{t("currencies.form.bettableHint", { ns: "config" })}</p>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Switch
|
||||||
checked={form.is_enabled && form.is_bettable}
|
checked={form.is_enabled && form.is_bettable}
|
||||||
onCheckedChange={(checked) => updateForm("is_bettable", checked === true)}
|
onCheckedChange={(checked) => updateForm("is_bettable", checked)}
|
||||||
disabled={saving || !form.is_enabled}
|
disabled={saving || !form.is_enabled}
|
||||||
|
aria-label={t("currencies.form.bettable", { ns: "config" })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -49,45 +50,6 @@ interface RuntimeDraft {
|
|||||||
playRulesHtmlNe: string;
|
playRulesHtmlNe: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BinaryChoice({
|
|
||||||
active,
|
|
||||||
disabled,
|
|
||||||
onChange,
|
|
||||||
leftLabel,
|
|
||||||
rightLabel,
|
|
||||||
}: {
|
|
||||||
active: boolean;
|
|
||||||
disabled: boolean;
|
|
||||||
onChange: (value: boolean) => void;
|
|
||||||
leftLabel: string;
|
|
||||||
rightLabel: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="inline-flex rounded-full border border-border/60 bg-background p-1">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant={!active ? "default" : "ghost"}
|
|
||||||
className={!active ? "h-8 rounded-full px-3" : "h-8 rounded-full px-3 text-muted-foreground"}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => onChange(false)}
|
|
||||||
>
|
|
||||||
{leftLabel}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant={active ? "default" : "ghost"}
|
|
||||||
className={active ? "h-8 rounded-full px-3" : "h-8 rounded-full px-3 text-muted-foreground"}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => onChange(true)}
|
|
||||||
>
|
|
||||||
{rightLabel}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SaveActions({
|
function SaveActions({
|
||||||
dirty,
|
dirty,
|
||||||
loading,
|
loading,
|
||||||
@@ -234,12 +196,11 @@ export function SystemSettingsScreen() {
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Label className="text-sm font-medium">{t("system.fields.manualReview", { ns: "config" })}</Label>
|
<Label className="text-sm font-medium">{t("system.fields.manualReview", { ns: "config" })}</Label>
|
||||||
<BinaryChoice
|
<Switch
|
||||||
active={draft.requireManualReview}
|
checked={draft.requireManualReview}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
onChange={(value) => updateDraft("requireManualReview", value)}
|
aria-label={t("system.fields.manualReview", { ns: "config" })}
|
||||||
leftLabel={t("system.states.disabled", { ns: "config" })}
|
onCheckedChange={(value) => updateDraft("requireManualReview", value)}
|
||||||
rightLabel={t("system.states.enabled", { ns: "config" })}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -247,12 +208,11 @@ export function SystemSettingsScreen() {
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Label className="text-sm font-medium">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
|
<Label className="text-sm font-medium">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
|
||||||
<BinaryChoice
|
<Switch
|
||||||
active={draft.autoSettlement}
|
checked={draft.autoSettlement}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
onChange={(value) => updateDraft("autoSettlement", value)}
|
aria-label={t("system.fields.autoSettlement", { ns: "config" })}
|
||||||
leftLabel={t("system.states.disabled", { ns: "config" })}
|
onCheckedChange={(value) => updateDraft("autoSettlement", value)}
|
||||||
rightLabel={t("system.states.enabled", { ns: "config" })}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -260,12 +220,11 @@ export function SystemSettingsScreen() {
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Label className="text-sm font-medium">{t("system.fields.autoApprove", { ns: "config" })}</Label>
|
<Label className="text-sm font-medium">{t("system.fields.autoApprove", { ns: "config" })}</Label>
|
||||||
<BinaryChoice
|
<Switch
|
||||||
active={draft.autoApprove}
|
checked={draft.autoApprove}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
onChange={(value) => updateDraft("autoApprove", value)}
|
aria-label={t("system.fields.autoApprove", { ns: "config" })}
|
||||||
leftLabel={t("system.states.disabled", { ns: "config" })}
|
onCheckedChange={(value) => updateDraft("autoApprove", value)}
|
||||||
rightLabel={t("system.states.enabled", { ns: "config" })}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -273,12 +232,11 @@ export function SystemSettingsScreen() {
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<Label className="text-sm font-medium">{t("system.fields.autoPayout", { ns: "config" })}</Label>
|
<Label className="text-sm font-medium">{t("system.fields.autoPayout", { ns: "config" })}</Label>
|
||||||
<BinaryChoice
|
<Switch
|
||||||
active={draft.autoPayout}
|
checked={draft.autoPayout}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
onChange={(value) => updateDraft("autoPayout", value)}
|
aria-label={t("system.fields.autoPayout", { ns: "config" })}
|
||||||
leftLabel={t("system.states.disabled", { ns: "config" })}
|
onCheckedChange={(value) => updateDraft("autoPayout", value)}
|
||||||
rightLabel={t("system.states.enabled", { ns: "config" })}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user