Compare commits
2 Commits
788c7998eb
...
0bfcf6c59c
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bfcf6c59c | |||
| b76ade9608 |
@@ -1,7 +1,9 @@
|
||||
# =============================================================================
|
||||
# 管理端本地配置示例:复制为 .env.local 后按需修改
|
||||
# =============================================================================
|
||||
# 手动切换环境:保留一个生效,另一个注释掉
|
||||
|
||||
# 必填:Laravel 应用根 URL(无尾部斜杠)。axios 会请求 {此地址}/api/v1/...
|
||||
# 需保证 Laravel 已允许该来源的 CORS(本地管理端为 http://localhost:3801 等)。
|
||||
NEXT_PUBLIC_LOTTERY_API_BASE_URL=http://127.0.0.1:8000
|
||||
# 测试
|
||||
API_BASE_URL=http://127.0.0.1:8000
|
||||
# 线上
|
||||
# API_BASE_URL=https://api.your-production-domain.com
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const apiBaseUrl = process.env.API_BASE_URL?.trim() || "http://127.0.0.1:8000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
allowedDevOrigins: ["192.168.0.101"],
|
||||
reactCompiler: true,
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: `${apiBaseUrl}/api/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ShieldCheckIcon, TriangleAlertIcon } from "lucide-react";
|
||||
import { ShieldCheckIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -25,10 +24,6 @@ export function LoginForm() {
|
||||
const setBearerToken = useAdminSessionStore((s) => s.setBearerToken);
|
||||
const setAdminProfile = useAdminSessionStore((s) => s.setAdminProfile);
|
||||
|
||||
const apiConfigured =
|
||||
process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim() !== "" &&
|
||||
process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL !== undefined;
|
||||
|
||||
const [account, setAccount] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [captchaCode, setCaptchaCode] = useState("");
|
||||
@@ -39,9 +34,6 @@ export function LoginForm() {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const loadCaptcha = useCallback(async () => {
|
||||
if (!apiConfigured) {
|
||||
return;
|
||||
}
|
||||
setLoadingCaptcha(true);
|
||||
try {
|
||||
const data = await getAdminCaptcha();
|
||||
@@ -58,7 +50,7 @@ export function LoginForm() {
|
||||
} finally {
|
||||
setLoadingCaptcha(false);
|
||||
}
|
||||
}, [apiConfigured, t]);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (readToken()) {
|
||||
@@ -75,11 +67,6 @@ export function LoginForm() {
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
if (!apiConfigured) {
|
||||
toast.error(t("apiBaseMissingToast"));
|
||||
|
||||
return;
|
||||
}
|
||||
if (!captchaKey || !captchaSrc) {
|
||||
toast.error(t("captchaRequired"));
|
||||
void loadCaptcha();
|
||||
@@ -156,19 +143,6 @@ export function LoginForm() {
|
||||
</CardHeader>
|
||||
<form onSubmit={onSubmit}>
|
||||
<CardContent className="flex flex-col gap-5 sm:px-8">
|
||||
{!apiConfigured ? (
|
||||
<Alert variant="destructive" className="text-left">
|
||||
<TriangleAlertIcon />
|
||||
<AlertTitle>{t("apiMissingTitle")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("apiMissingDescriptionPrefix")}{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-xs">
|
||||
NEXT_PUBLIC_LOTTERY_API_BASE_URL
|
||||
</code>{" "}
|
||||
{t("apiMissingDescriptionSuffix")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="admin-account" className="text-sm font-medium">
|
||||
{t("account")}
|
||||
@@ -224,7 +198,7 @@ export function LoginForm() {
|
||||
className="flex h-11 min-w-[156px] shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl border border-border/80 bg-muted/35 px-2 shadow-[inset_0_1px_2px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.03] transition-[box-shadow,transform] hover:bg-muted/45 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 dark:bg-muted/25 dark:shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)] dark:ring-white/[0.06] dark:hover:bg-muted/35"
|
||||
onClick={() => void loadCaptcha()}
|
||||
disabled={
|
||||
loadingCaptcha || !apiConfigured || submitting
|
||||
loadingCaptcha || submitting
|
||||
}
|
||||
aria-label={loadingCaptcha ? t("captchaLoading") : t("captchaRefresh")}
|
||||
>
|
||||
@@ -251,7 +225,7 @@ export function LoginForm() {
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="h-11 w-full text-base font-medium shadow-sm"
|
||||
disabled={submitting || !apiConfigured}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? t("submitting") : t("submit")}
|
||||
</Button>
|
||||
|
||||
@@ -182,6 +182,14 @@
|
||||
"fields": {
|
||||
"manualReview": "Require manual review for draw results",
|
||||
"cooldownMinutes": "Cooldown duration (minutes)",
|
||||
"defaultCurrency": "Default currency code",
|
||||
"drawIntervalMinutes": "Draw interval (minutes)",
|
||||
"drawBettingWindowSeconds": "Betting window (seconds)",
|
||||
"drawCloseBeforeDrawSeconds": "Close before draw (seconds)",
|
||||
"drawBufferDrawsAhead": "Pre-generated future draws",
|
||||
"currencyDisplayDecimals": "Display decimals",
|
||||
"currencyDecimalSeparator": "Decimal separator",
|
||||
"currencyThousandsSeparator": "Thousands separator",
|
||||
"autoSettlement": "Run settlement automatically",
|
||||
"autoApprove": "Auto-approve settlement batches",
|
||||
"autoPayout": "Auto-credit winnings to wallets",
|
||||
|
||||
@@ -182,6 +182,14 @@
|
||||
"fields": {
|
||||
"manualReview": "ड्रअ परिणामका लागि म्यानुअल समीक्षा चाहिने",
|
||||
"cooldownMinutes": "कूलडाउन अवधि (मिनेट)",
|
||||
"defaultCurrency": "पूर्वनिर्धारित मुद्रा कोड",
|
||||
"drawIntervalMinutes": "ड्रअ अन्तराल (मिनेट)",
|
||||
"drawBettingWindowSeconds": "बेटिङ विन्डो (सेकेन्ड)",
|
||||
"drawCloseBeforeDrawSeconds": "ड्रअ अघि बन्द (सेकेन्ड)",
|
||||
"drawBufferDrawsAhead": "अग्रिम सिर्जना गरिने ड्रअ संख्या",
|
||||
"currencyDisplayDecimals": "प्रदर्शन दशमलव स्थान",
|
||||
"currencyDecimalSeparator": "दशमलव विभाजक",
|
||||
"currencyThousandsSeparator": "हजार विभाजक",
|
||||
"autoSettlement": "सेटलमेन्ट स्वतः चलाउने",
|
||||
"autoApprove": "सेटलमेन्ट ब्याच स्वतः स्वीकृत",
|
||||
"autoPayout": "जित रकम स्वतः वालेटमा जम्मा",
|
||||
|
||||
@@ -182,6 +182,14 @@
|
||||
"fields": {
|
||||
"manualReview": "开奖结果必须人工审核",
|
||||
"cooldownMinutes": "冷静期时长(分钟)",
|
||||
"defaultCurrency": "默认币种代码",
|
||||
"drawIntervalMinutes": "开奖间隔(分钟)",
|
||||
"drawBettingWindowSeconds": "下注窗口(秒)",
|
||||
"drawCloseBeforeDrawSeconds": "封盘提前(秒)",
|
||||
"drawBufferDrawsAhead": "预生成未来期数",
|
||||
"currencyDisplayDecimals": "金额显示小数位",
|
||||
"currencyDecimalSeparator": "小数分隔符",
|
||||
"currencyThousandsSeparator": "千位分隔符",
|
||||
"autoSettlement": "自动执行结算",
|
||||
"autoApprove": "自动审核结算批次",
|
||||
"autoPayout": "自动派彩入账",
|
||||
|
||||
@@ -9,15 +9,12 @@ import { withAdminLocaleHeaders } from "@/lib/admin-locale";
|
||||
import { LotteryApiBizError, LotteryApiEnvelopeError } from "@/types/api/errors";
|
||||
import { isApiEnvelope } from "@/types/api/envelope";
|
||||
|
||||
const baseURL = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim();
|
||||
|
||||
/** 是否已配置后台 API 根地址(客户端/服务端均可用 `NEXT_PUBLIC_*`) */
|
||||
export function hasLotteryAdminApiBaseUrl(): boolean {
|
||||
return baseURL !== undefined && baseURL !== "";
|
||||
return true;
|
||||
}
|
||||
|
||||
export const adminHttp = axios.create({
|
||||
baseURL: baseURL && baseURL !== "" ? baseURL : undefined,
|
||||
// API 路径统一由调用方传 `/api/v1/...`,避免与前缀重复拼接成 `/api/api/v1/...`。
|
||||
timeout: 30_000,
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
|
||||
@@ -310,7 +310,16 @@ export function PlayersConsole(): React.ReactElement {
|
||||
onValueChange={(v) => setSiteCode(v === "__all__" ? "" : v ?? "")}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[12rem]">
|
||||
<SelectValue placeholder={t("filterAllSites")} />
|
||||
<SelectValue>
|
||||
{(v) => {
|
||||
const value = String(v ?? "__all__");
|
||||
if (value === "__all__") {
|
||||
return t("filterAllSites");
|
||||
}
|
||||
const site = siteOptions.find((item) => item.code === value);
|
||||
return site ? `${site.code} — ${site.name}` : value;
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">{t("filterAllSites")}</SelectItem>
|
||||
|
||||
@@ -24,8 +24,16 @@ const DRAW_GROUP = "draw";
|
||||
const SETTLEMENT_GROUP = "settlement";
|
||||
|
||||
const DRAW_KEYS = {
|
||||
DEFAULT_CURRENCY: "currency.default_code",
|
||||
DRAW_INTERVAL_MINUTES: "draw.interval_minutes",
|
||||
DRAW_BETTING_WINDOW_SECONDS: "draw.betting_window_seconds",
|
||||
DRAW_CLOSE_BEFORE_DRAW_SECONDS: "draw.close_before_draw_seconds",
|
||||
DRAW_BUFFER_DRAWS_AHEAD: "draw.buffer_draws_ahead",
|
||||
REQUIRE_MANUAL_REVIEW: "draw.require_manual_review",
|
||||
COOLDOWN_MINUTES: "draw.cooldown_minutes",
|
||||
CURRENCY_DISPLAY_DECIMALS: "currency.display_decimals",
|
||||
CURRENCY_DECIMAL_SEPARATOR: "currency.decimal_separator",
|
||||
CURRENCY_THOUSANDS_SEPARATOR: "currency.thousands_separator",
|
||||
AUTO_SETTLEMENT: "settlement.auto_run_on_tick",
|
||||
AUTO_APPROVE: "settlement.auto_approve_on_tick",
|
||||
AUTO_PAYOUT: "settlement.auto_payout_on_tick",
|
||||
@@ -41,8 +49,16 @@ const FRONTEND_KEYS = {
|
||||
} as const;
|
||||
|
||||
interface RuntimeDraft {
|
||||
defaultCurrency: string;
|
||||
drawIntervalMinutes: string;
|
||||
drawBettingWindowSeconds: string;
|
||||
drawCloseBeforeDrawSeconds: string;
|
||||
drawBufferDrawsAhead: string;
|
||||
requireManualReview: boolean;
|
||||
cooldownMinutes: string;
|
||||
currencyDisplayDecimals: string;
|
||||
currencyDecimalSeparator: string;
|
||||
currencyThousandsSeparator: string;
|
||||
autoSettlement: boolean;
|
||||
autoApprove: boolean;
|
||||
autoPayout: boolean;
|
||||
@@ -89,8 +105,16 @@ export function SystemSettingsScreen() {
|
||||
const { t } = useTranslation(["common", "config", "adminUsers"]);
|
||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||
const [draft, setDraft] = useState<RuntimeDraft>({
|
||||
defaultCurrency: "NPR",
|
||||
drawIntervalMinutes: "5",
|
||||
drawBettingWindowSeconds: "270",
|
||||
drawCloseBeforeDrawSeconds: "30",
|
||||
drawBufferDrawsAhead: "8",
|
||||
requireManualReview: false,
|
||||
cooldownMinutes: "15",
|
||||
currencyDisplayDecimals: "2",
|
||||
currencyDecimalSeparator: ".",
|
||||
currencyThousandsSeparator: ",",
|
||||
autoSettlement: true,
|
||||
autoApprove: true,
|
||||
autoPayout: true,
|
||||
@@ -100,8 +124,16 @@ export function SystemSettingsScreen() {
|
||||
playRulesHtmlNe: "",
|
||||
});
|
||||
const [saved, setSaved] = useState<RuntimeDraft>({
|
||||
defaultCurrency: "NPR",
|
||||
drawIntervalMinutes: "5",
|
||||
drawBettingWindowSeconds: "270",
|
||||
drawCloseBeforeDrawSeconds: "30",
|
||||
drawBufferDrawsAhead: "8",
|
||||
requireManualReview: false,
|
||||
cooldownMinutes: "15",
|
||||
currencyDisplayDecimals: "2",
|
||||
currencyDecimalSeparator: ".",
|
||||
currencyThousandsSeparator: ",",
|
||||
autoSettlement: true,
|
||||
autoApprove: true,
|
||||
autoPayout: true,
|
||||
@@ -130,8 +162,16 @@ export function SystemSettingsScreen() {
|
||||
|
||||
const legacyHtml = String(kv[FRONTEND_KEYS.PLAY_RULES_HTML] ?? "");
|
||||
const nextDraft: RuntimeDraft = {
|
||||
defaultCurrency: String(kv[DRAW_KEYS.DEFAULT_CURRENCY] ?? "NPR"),
|
||||
drawIntervalMinutes: String(kv[DRAW_KEYS.DRAW_INTERVAL_MINUTES] ?? 5),
|
||||
drawBettingWindowSeconds: String(kv[DRAW_KEYS.DRAW_BETTING_WINDOW_SECONDS] ?? 270),
|
||||
drawCloseBeforeDrawSeconds: String(kv[DRAW_KEYS.DRAW_CLOSE_BEFORE_DRAW_SECONDS] ?? 30),
|
||||
drawBufferDrawsAhead: String(kv[DRAW_KEYS.DRAW_BUFFER_DRAWS_AHEAD] ?? 8),
|
||||
requireManualReview: Boolean(kv[DRAW_KEYS.REQUIRE_MANUAL_REVIEW] ?? false),
|
||||
cooldownMinutes: String(kv[DRAW_KEYS.COOLDOWN_MINUTES] ?? 15),
|
||||
currencyDisplayDecimals: String(kv[DRAW_KEYS.CURRENCY_DISPLAY_DECIMALS] ?? 2),
|
||||
currencyDecimalSeparator: String(kv[DRAW_KEYS.CURRENCY_DECIMAL_SEPARATOR] ?? "."),
|
||||
currencyThousandsSeparator: String(kv[DRAW_KEYS.CURRENCY_THOUSANDS_SEPARATOR] ?? ","),
|
||||
autoSettlement: Boolean(kv[DRAW_KEYS.AUTO_SETTLEMENT] ?? true),
|
||||
autoApprove: Boolean(kv[DRAW_KEYS.AUTO_APPROVE] ?? true),
|
||||
autoPayout: Boolean(kv[DRAW_KEYS.AUTO_PAYOUT] ?? true),
|
||||
@@ -164,11 +204,43 @@ export function SystemSettingsScreen() {
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.DEFAULT_CURRENCY,
|
||||
draft.defaultCurrency.trim().toUpperCase() || "NPR",
|
||||
);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.DRAW_INTERVAL_MINUTES,
|
||||
Math.max(1, Number.parseInt(draft.drawIntervalMinutes || "5", 10) || 5),
|
||||
);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.DRAW_BETTING_WINDOW_SECONDS,
|
||||
Math.max(10, Number.parseInt(draft.drawBettingWindowSeconds || "270", 10) || 270),
|
||||
);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.DRAW_CLOSE_BEFORE_DRAW_SECONDS,
|
||||
Math.max(5, Number.parseInt(draft.drawCloseBeforeDrawSeconds || "30", 10) || 30),
|
||||
);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.DRAW_BUFFER_DRAWS_AHEAD,
|
||||
Math.max(1, Number.parseInt(draft.drawBufferDrawsAhead || "8", 10) || 8),
|
||||
);
|
||||
await updateAdminSetting(DRAW_KEYS.REQUIRE_MANUAL_REVIEW, draft.requireManualReview);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.COOLDOWN_MINUTES,
|
||||
Math.max(0, Number.parseInt(draft.cooldownMinutes || "0", 10) || 0),
|
||||
);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.CURRENCY_DISPLAY_DECIMALS,
|
||||
Math.max(0, Math.min(12, Number.parseInt(draft.currencyDisplayDecimals || "2", 10) || 2)),
|
||||
);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.CURRENCY_DECIMAL_SEPARATOR,
|
||||
(draft.currencyDecimalSeparator || ".").slice(0, 1),
|
||||
);
|
||||
await updateAdminSetting(
|
||||
DRAW_KEYS.CURRENCY_THOUSANDS_SEPARATOR,
|
||||
(draft.currencyThousandsSeparator || ",").slice(0, 1),
|
||||
);
|
||||
await updateAdminSetting(DRAW_KEYS.AUTO_SETTLEMENT, draft.autoSettlement);
|
||||
await updateAdminSetting(DRAW_KEYS.AUTO_APPROVE, draft.autoApprove);
|
||||
await updateAdminSetting(DRAW_KEYS.AUTO_PAYOUT, draft.autoPayout);
|
||||
@@ -212,6 +284,119 @@ export function SystemSettingsScreen() {
|
||||
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="default-currency" className="text-sm font-medium">
|
||||
{t("system.fields.defaultCurrency", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="default-currency"
|
||||
value={draft.defaultCurrency}
|
||||
onChange={(e) => updateDraft("defaultCurrency", e.target.value.toUpperCase())}
|
||||
disabled={loading || saving}
|
||||
maxLength={16}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="draw-interval-minutes" className="text-sm font-medium">
|
||||
{t("system.fields.drawIntervalMinutes", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="draw-interval-minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
step="1"
|
||||
value={draft.drawIntervalMinutes}
|
||||
onChange={(e) => updateDraft("drawIntervalMinutes", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="draw-betting-window-seconds" className="text-sm font-medium">
|
||||
{t("system.fields.drawBettingWindowSeconds", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="draw-betting-window-seconds"
|
||||
type="number"
|
||||
min="10"
|
||||
step="1"
|
||||
value={draft.drawBettingWindowSeconds}
|
||||
onChange={(e) => updateDraft("drawBettingWindowSeconds", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="draw-close-before-seconds" className="text-sm font-medium">
|
||||
{t("system.fields.drawCloseBeforeDrawSeconds", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="draw-close-before-seconds"
|
||||
type="number"
|
||||
min="5"
|
||||
step="1"
|
||||
value={draft.drawCloseBeforeDrawSeconds}
|
||||
onChange={(e) => updateDraft("drawCloseBeforeDrawSeconds", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="draw-buffer-ahead" className="text-sm font-medium">
|
||||
{t("system.fields.drawBufferDrawsAhead", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="draw-buffer-ahead"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={draft.drawBufferDrawsAhead}
|
||||
onChange={(e) => updateDraft("drawBufferDrawsAhead", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="currency-display-decimals" className="text-sm font-medium">
|
||||
{t("system.fields.currencyDisplayDecimals", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="currency-display-decimals"
|
||||
type="number"
|
||||
min="0"
|
||||
max="12"
|
||||
step="1"
|
||||
value={draft.currencyDisplayDecimals}
|
||||
onChange={(e) => updateDraft("currencyDisplayDecimals", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="currency-decimal-separator" className="text-sm font-medium">
|
||||
{t("system.fields.currencyDecimalSeparator", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="currency-decimal-separator"
|
||||
value={draft.currencyDecimalSeparator}
|
||||
onChange={(e) => updateDraft("currencyDecimalSeparator", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
maxLength={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="currency-thousands-separator" className="text-sm font-medium">
|
||||
{t("system.fields.currencyThousandsSeparator", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="currency-thousands-separator"
|
||||
value={draft.currencyThousandsSeparator}
|
||||
onChange={(e) => updateDraft("currencyThousandsSeparator", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
maxLength={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Label className="text-sm font-medium">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
|
||||
<Switch
|
||||
|
||||
@@ -199,7 +199,16 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[12rem]">
|
||||
<SelectValue placeholder={t("filterAllSites")} />
|
||||
<SelectValue>
|
||||
{(v) => {
|
||||
const value = String(v ?? "__all__");
|
||||
if (value === "__all__") {
|
||||
return t("filterAllSites");
|
||||
}
|
||||
const site = siteOptions.find((item) => item.code === value);
|
||||
return site ? `${site.code} — ${site.name}` : value;
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">{t("filterAllSites")}</SelectItem>
|
||||
|
||||
Reference in New Issue
Block a user