feat: 添加货币管理功能,更新国际化支持,移除报表相关代码
This commit is contained in:
11
src/modules/settings/currency-management-screen.tsx
Normal file
11
src/modules/settings/currency-management-screen.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { CurrencySettingsPanel } from "@/modules/settings/currency-settings-panel";
|
||||
|
||||
export function CurrencyManagementScreen() {
|
||||
return (
|
||||
<div className="flex w-full max-w-none flex-col gap-6">
|
||||
<CurrencySettingsPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
377
src/modules/settings/currency-settings-panel.tsx
Normal file
377
src/modules/settings/currency-settings-panel.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
deleteAdminCurrency,
|
||||
getAdminCurrencies,
|
||||
postAdminCurrency,
|
||||
putAdminCurrency,
|
||||
} from "@/api/admin-currencies";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminCurrencyRow } from "@/types/api/admin-currency";
|
||||
|
||||
type CurrencyFormState = {
|
||||
code: string;
|
||||
name: string;
|
||||
decimal_places: string;
|
||||
is_enabled: boolean;
|
||||
is_bettable: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_FORM: CurrencyFormState = {
|
||||
code: "",
|
||||
name: "",
|
||||
decimal_places: "2",
|
||||
is_enabled: true,
|
||||
is_bettable: false,
|
||||
};
|
||||
|
||||
function toFormState(row: AdminCurrencyRow): CurrencyFormState {
|
||||
return {
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
decimal_places: String(row.decimal_places),
|
||||
is_enabled: row.is_enabled,
|
||||
is_bettable: row.is_enabled && row.is_bettable,
|
||||
};
|
||||
}
|
||||
|
||||
export function CurrencySettingsPanel() {
|
||||
const { t } = useTranslation(["config", "adminUsers"]);
|
||||
const profile = useAdminProfile();
|
||||
const canManage = adminHasAnyPermission(profile?.permissions, ["prd.currency.manage"]);
|
||||
const [items, setItems] = useState<AdminCurrencyRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [mode, setMode] = useState<"create" | "edit">("create");
|
||||
const [editingCode, setEditingCode] = useState<string | null>(null);
|
||||
const [form, setForm] = useState<CurrencyFormState>(EMPTY_FORM);
|
||||
const [deleteTarget, setDeleteTarget] = useState<AdminCurrencyRow | null>(null);
|
||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!canManage) {
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getAdminCurrencies();
|
||||
setItems(data.items);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof LotteryApiBizError
|
||||
? error.message
|
||||
: t("currencies.loadFailed", { ns: "config" }),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canManage, t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void load();
|
||||
});
|
||||
}, [load]);
|
||||
|
||||
function openCreate(): void {
|
||||
setMode("create");
|
||||
setEditingCode(null);
|
||||
setForm(EMPTY_FORM);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function openEdit(row: AdminCurrencyRow): void {
|
||||
setMode("edit");
|
||||
setEditingCode(row.code);
|
||||
setForm(toFormState(row));
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function updateForm<K extends keyof CurrencyFormState>(key: K, value: CurrencyFormState[K]): void {
|
||||
setForm((prev) => {
|
||||
const next = { ...prev, [key]: value };
|
||||
if (key === "is_enabled" && value === false) {
|
||||
next.is_bettable = false;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSubmit(): Promise<void> {
|
||||
const payload = {
|
||||
name: form.name.trim(),
|
||||
decimal_places: Number.parseInt(form.decimal_places || "0", 10),
|
||||
is_enabled: form.is_enabled,
|
||||
is_bettable: form.is_enabled && form.is_bettable,
|
||||
};
|
||||
|
||||
if (mode === "create") {
|
||||
if (form.code.trim() === "" || payload.name === "") {
|
||||
toast.error(t("currencies.form.required", { ns: "config" }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(payload.decimal_places) || payload.decimal_places < 0) {
|
||||
toast.error(t("currencies.form.decimalInvalid", { ns: "config" }));
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (mode === "create") {
|
||||
await postAdminCurrency({
|
||||
code: form.code.trim().toUpperCase(),
|
||||
...payload,
|
||||
});
|
||||
toast.success(t("currencies.createSuccess", { ns: "config" }));
|
||||
} else if (editingCode !== null) {
|
||||
await putAdminCurrency(editingCode, payload);
|
||||
toast.success(t("currencies.updateSuccess", { ns: "config" }));
|
||||
}
|
||||
|
||||
setDialogOpen(false);
|
||||
await load();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof LotteryApiBizError
|
||||
? error.message
|
||||
: t(mode === "create" ? "currencies.createFailed" : "currencies.updateFailed", { ns: "config" }),
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete(): Promise<void> {
|
||||
if (deleteTarget === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleteBusy(true);
|
||||
try {
|
||||
await deleteAdminCurrency(deleteTarget.code);
|
||||
toast.success(t("currencies.deleteSuccess", { ns: "config", code: deleteTarget.code }));
|
||||
setDeleteTarget(null);
|
||||
await load();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof LotteryApiBizError
|
||||
? error.message
|
||||
: t("currencies.deleteFailed", { ns: "config" }),
|
||||
);
|
||||
} finally {
|
||||
setDeleteBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!canManage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="admin-list-card">
|
||||
<CardHeader className="admin-list-header flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="admin-list-title">{t("currencies.title", { ns: "config" })}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("currencies.description", { ns: "config" })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AdminTableExportButton tableId="admin-currencies-table" filename="币种管理" sheetName="币种管理" />
|
||||
<Button onClick={openCreate}>{t("currencies.actions.create", { ns: "config" })}</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="admin-list-content">
|
||||
<div className="admin-table-shell">
|
||||
<Table id="admin-currencies-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="whitespace-nowrap">{t("currencies.table.code", { ns: "config" })}</TableHead>
|
||||
<TableHead>{t("currencies.table.name", { ns: "config" })}</TableHead>
|
||||
<TableHead className="whitespace-nowrap">{t("currencies.table.decimals", { ns: "config" })}</TableHead>
|
||||
<TableHead className="whitespace-nowrap">{t("currencies.table.enabled", { ns: "config" })}</TableHead>
|
||||
<TableHead className="whitespace-nowrap">{t("currencies.table.bettable", { ns: "config" })}</TableHead>
|
||||
<TableHead className="whitespace-nowrap text-center">{t("currencies.table.actions", { ns: "config" })}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-sm text-muted-foreground">
|
||||
{t("currencies.loading", { ns: "config" })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-sm text-muted-foreground">
|
||||
{t("currencies.empty", { ns: "config" })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
items.map((row) => (
|
||||
<TableRow key={row.code}>
|
||||
<TableCell className="font-mono">{row.code}</TableCell>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell>{row.decimal_places}</TableCell>
|
||||
<TableCell>{row.is_enabled ? t("system.states.enabled", { ns: "config" }) : t("system.states.disabled", { ns: "config" })}</TableCell>
|
||||
<TableCell>{row.is_bettable ? t("system.states.enabled", { ns: "config" }) : t("system.states.disabled", { ns: "config" })}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => openEdit(row)}>
|
||||
{t("currencies.actions.edit", { ns: "config" })}
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => setDeleteTarget(row)}>
|
||||
{t("currencies.actions.delete", { ns: "config" })}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent showCloseButton className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t(mode === "create" ? "currencies.dialog.createTitle" : "currencies.dialog.editTitle", {
|
||||
ns: "config",
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("currencies.dialog.description", { ns: "config" })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currency-code">{t("currencies.form.code", { ns: "config" })}</Label>
|
||||
<Input
|
||||
id="currency-code"
|
||||
value={form.code}
|
||||
onChange={(e) => updateForm("code", e.target.value.toUpperCase())}
|
||||
disabled={saving || mode === "edit"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currency-name">{t("currencies.form.name", { ns: "config" })}</Label>
|
||||
<Input
|
||||
id="currency-name"
|
||||
value={form.name}
|
||||
onChange={(e) => updateForm("name", e.target.value)}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currency-decimals">{t("currencies.form.decimals", { ns: "config" })}</Label>
|
||||
<Input
|
||||
id="currency-decimals"
|
||||
type="number"
|
||||
min="0"
|
||||
max="12"
|
||||
step="1"
|
||||
value={form.decimal_places}
|
||||
onChange={(e) => updateForm("decimal_places", e.target.value)}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-border/70 p-3">
|
||||
<div className="space-y-1">
|
||||
<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>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={form.is_enabled}
|
||||
onCheckedChange={(checked) => updateForm("is_enabled", checked === true)}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-border/70 p-3">
|
||||
<div className="space-y-1">
|
||||
<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>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={form.is_enabled && form.is_bettable}
|
||||
onCheckedChange={(checked) => updateForm("is_bettable", checked === true)}
|
||||
disabled={saving || !form.is_enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}>
|
||||
{t("actions.cancel", { ns: "adminUsers" })}
|
||||
</Button>
|
||||
<Button onClick={() => void handleSubmit()} disabled={saving}>
|
||||
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||
<DialogContent showCloseButton className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("currencies.deleteDialog.title", { ns: "config" })}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("currencies.deleteDialog.description", {
|
||||
ns: "config",
|
||||
code: deleteTarget?.code ?? "",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline" onClick={() => setDeleteTarget(null)} disabled={deleteBusy}>
|
||||
{t("actions.cancel", { ns: "adminUsers" })}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => void confirmDelete()} disabled={deleteBusy}>
|
||||
{deleteBusy ? t("deleting", { ns: "adminUsers" }) : t("currencies.actions.delete", { ns: "config" })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { WalletConfigDocScreen } from "@/modules/config/doc/wallet-config-doc-screen";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
@@ -31,6 +30,45 @@ interface RuntimeDraft {
|
||||
autoSettlement: boolean;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export function SystemSettingsScreen() {
|
||||
const { t } = useTranslation(["common", "config", "adminUsers"]);
|
||||
const [draft, setDraft] = useState<RuntimeDraft>({
|
||||
@@ -120,82 +158,89 @@ export function SystemSettingsScreen() {
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("system.title", { ns: "config" })}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-3 rounded-xl border border-border/70 p-4">
|
||||
<Label htmlFor="manual-review">{t("system.fields.manualReview", { ns: "config" })}</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id="manual-review"
|
||||
checked={draft.requireManualReview}
|
||||
onCheckedChange={(checked) => updateDraft("requireManualReview", checked === true)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
<Label htmlFor="manual-review" className="text-sm font-medium">
|
||||
{draft.requireManualReview
|
||||
? t("system.states.enabled", { ns: "config" })
|
||||
: t("system.states.disabled", { ns: "config" })}
|
||||
</Label>
|
||||
<CardContent className="space-y-8">
|
||||
<section className="space-y-4">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold">{t("system.title", { ns: "config" })}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-xl border border-border/70 p-4">
|
||||
<Label htmlFor="auto-settlement">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id="auto-settlement"
|
||||
checked={draft.autoSettlement}
|
||||
onCheckedChange={(checked) => updateDraft("autoSettlement", checked === true)}
|
||||
<div className="space-y-5 rounded-2xl border border-border/60 bg-muted/10 px-4 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm font-medium">{t("system.fields.manualReview", { ns: "config" })}</Label>
|
||||
</div>
|
||||
<BinaryChoice
|
||||
active={draft.requireManualReview}
|
||||
disabled={loading || saving}
|
||||
onChange={(value) => updateDraft("requireManualReview", value)}
|
||||
leftLabel={t("system.states.disabled", { ns: "config" })}
|
||||
rightLabel={t("system.states.enabled", { ns: "config" })}
|
||||
/>
|
||||
<Label htmlFor="auto-settlement" className="text-sm font-medium">
|
||||
{draft.autoSettlement
|
||||
? t("system.states.enabled", { ns: "config" })
|
||||
: t("system.states.disabled", { ns: "config" })}
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm font-medium">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
|
||||
</div>
|
||||
<BinaryChoice
|
||||
active={draft.autoSettlement}
|
||||
disabled={loading || saving}
|
||||
onChange={(value) => updateDraft("autoSettlement", value)}
|
||||
leftLabel={t("system.states.disabled", { ns: "config" })}
|
||||
rightLabel={t("system.states.enabled", { ns: "config" })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="cooldown-minutes" className="text-sm font-medium">
|
||||
{t("system.fields.cooldownMinutes", { ns: "config" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="cooldown-minutes"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={draft.cooldownMinutes}
|
||||
onChange={(e) => updateDraft("cooldownMinutes", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
className="max-w-[240px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
<Button onClick={() => void handleSave()} disabled={!dirty || loading || saving}>
|
||||
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
|
||||
</Button>
|
||||
{dirty && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDraft(saved);
|
||||
setDirty(false);
|
||||
}}
|
||||
>
|
||||
{t("system.discard", { ns: "config" })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cooldown-minutes">{t("system.fields.cooldownMinutes", { ns: "config" })}</Label>
|
||||
<Input
|
||||
id="cooldown-minutes"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={draft.cooldownMinutes}
|
||||
onChange={(e) => updateDraft("cooldownMinutes", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button onClick={() => void handleSave()} disabled={!dirty || loading || saving}>
|
||||
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
|
||||
</Button>
|
||||
{dirty && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDraft(saved);
|
||||
setDirty(false);
|
||||
}}
|
||||
>
|
||||
{t("system.discard", { ns: "config" })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<section className="space-y-4 border-t border-border/60 pt-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold">{t("wallet.title", { ns: "config" })}</h3>
|
||||
</div>
|
||||
<WalletConfigDocScreen embedded />
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<WalletConfigDocScreen />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user