refactor: 优化配置与奖池页面多语言编辑及管理端列表布局
This commit is contained in:
@@ -147,7 +147,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-list-toolbar {
|
.admin-list-toolbar {
|
||||||
@apply flex w-full flex-col gap-3 border-t border-border/60 pt-4 xl:flex-row xl:flex-wrap xl:items-center;
|
@apply flex w-full flex-row flex-wrap items-center gap-3 border-t border-border/60 pt-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-list-field {
|
.admin-list-field {
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-list-actions {
|
.admin-list-actions {
|
||||||
@apply flex shrink-0 flex-wrap items-center gap-2 xl:ml-auto xl:justify-end;
|
@apply ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-table-shell {
|
.admin-table-shell {
|
||||||
|
|||||||
@@ -94,10 +94,13 @@
|
|||||||
"loadFailed": "Failed to load system settings",
|
"loadFailed": "Failed to load system settings",
|
||||||
"saveSuccess": "System settings saved",
|
"saveSuccess": "System settings saved",
|
||||||
"saveFailed": "Failed to save system settings",
|
"saveFailed": "Failed to save system settings",
|
||||||
|
"frontendConfig": "Front-end configuration",
|
||||||
"fields": {
|
"fields": {
|
||||||
"manualReview": "Require manual review for draw results",
|
"manualReview": "Require manual review for draw results",
|
||||||
"cooldownMinutes": "Cooldown duration (minutes)",
|
"cooldownMinutes": "Cooldown duration (minutes)",
|
||||||
"autoSettlement": "Run settlement automatically"
|
"autoSettlement": "Run settlement automatically",
|
||||||
|
"playRulesHtml": "Play rules HTML (i18n)",
|
||||||
|
"playRulesHtmlDesc": "Rendered on the player play-rules page per locale. Leave empty to fall back to another language or the default empty state."
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"manualReview": "When enabled, RNG draw results enter pending review and must be published manually in admin.",
|
"manualReview": "When enabled, RNG draw results enter pending review and must be published manually in admin.",
|
||||||
@@ -169,7 +172,8 @@
|
|||||||
"jackpot": "Jackpot"
|
"jackpot": "Jackpot"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"minMaxInvalid": "{{playCode}}: min bet cannot exceed max bet"
|
"minMaxInvalid": "{{playCode}}: min bet cannot exceed max bet",
|
||||||
|
"nameZhRequired": "Chinese display name is required"
|
||||||
},
|
},
|
||||||
"publishFailed": "Publish failed",
|
"publishFailed": "Publish failed",
|
||||||
"createDraftSuccess": "Created draft v{{version}}",
|
"createDraftSuccess": "Created draft v{{version}}",
|
||||||
@@ -186,7 +190,13 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
"ruleText": "Rule text"
|
"ruleText": "Rule text",
|
||||||
|
"displayNames": "Display names"
|
||||||
|
},
|
||||||
|
"locales": {
|
||||||
|
"zh": "Chinese",
|
||||||
|
"en": "English",
|
||||||
|
"ne": "Nepali"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"playCode": "Play code",
|
"playCode": "Play code",
|
||||||
@@ -206,10 +216,15 @@
|
|||||||
"aria": {
|
"aria": {
|
||||||
"enablePlay": "Enable {{playCode}}"
|
"enablePlay": "Enable {{playCode}}"
|
||||||
},
|
},
|
||||||
|
"nameDialog": {
|
||||||
|
"title": "Display names (i18n)",
|
||||||
|
"description": "Play {{playCode}}. Chinese is required; English and Nepali are optional. The player site picks the label by locale after publish.",
|
||||||
|
"apply": "Apply to draft",
|
||||||
|
"savedLocal": "Display names were saved into the local draft. Save the draft to persist them."
|
||||||
|
},
|
||||||
"ruleDialog": {
|
"ruleDialog": {
|
||||||
"title": "Rule text (Chinese)",
|
"title": "Rule text (i18n)",
|
||||||
"description": "Play {{playCode}}. Changes stay in the draft until you save and publish it.",
|
"description": "Play {{playCode}}. Changes stay in the draft until you save and publish it.",
|
||||||
"fieldLabel": "rule_text_zh",
|
|
||||||
"apply": "Apply to draft"
|
"apply": "Apply to draft"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,10 +94,13 @@
|
|||||||
"loadFailed": "प्रणाली सेटिङ लोड असफल भयो",
|
"loadFailed": "प्रणाली सेटिङ लोड असफल भयो",
|
||||||
"saveSuccess": "प्रणाली सेटिङ सुरक्षित भयो",
|
"saveSuccess": "प्रणाली सेटिङ सुरक्षित भयो",
|
||||||
"saveFailed": "प्रणाली सेटिङ सुरक्षित गर्न असफल",
|
"saveFailed": "प्रणाली सेटिङ सुरक्षित गर्न असफल",
|
||||||
|
"frontendConfig": "फ्रन्ट-एन्ड कन्फिग",
|
||||||
"fields": {
|
"fields": {
|
||||||
"manualReview": "ड्रअ परिणामका लागि म्यानुअल समीक्षा चाहिने",
|
"manualReview": "ड्रअ परिणामका लागि म्यानुअल समीक्षा चाहिने",
|
||||||
"cooldownMinutes": "कूलडाउन अवधि (मिनेट)",
|
"cooldownMinutes": "कूलडाउन अवधि (मिनेट)",
|
||||||
"autoSettlement": "सेटलमेन्ट स्वतः चलाउने"
|
"autoSettlement": "सेटलमेन्ट स्वतः चलाउने",
|
||||||
|
"playRulesHtml": "खेल नियम HTML (बहुभाषी)",
|
||||||
|
"playRulesHtmlDesc": "खेलाडीको नियम पृष्ठमा भाषा अनुसार HTML देखिन्छ। खाली छोड्दा अर्को भाषा वा पूर्वनिर्धारित खाली सूचना देखिन्छ।"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"manualReview": "सक्रिय हुँदा RNG ड्रअ परिणाम pending review मा जान्छ र एडमिनबाट म्यानुअल रूपमा प्रकाशित गर्नुपर्छ।",
|
"manualReview": "सक्रिय हुँदा RNG ड्रअ परिणाम pending review मा जान्छ र एडमिनबाट म्यानुअल रूपमा प्रकाशित गर्नुपर्छ।",
|
||||||
@@ -169,7 +172,8 @@
|
|||||||
"jackpot": "Jackpot"
|
"jackpot": "Jackpot"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"minMaxInvalid": "{{playCode}}: न्यूनतम बेट अधिकतम बेटभन्दा ठूलो हुन सक्दैन"
|
"minMaxInvalid": "{{playCode}}: न्यूनतम बेट अधिकतम बेटभन्दा ठूलो हुन सक्दैन",
|
||||||
|
"nameZhRequired": "चिनियाँ प्रदर्शित नाम अनिवार्य छ"
|
||||||
},
|
},
|
||||||
"publishFailed": "प्रकाशन असफल भयो",
|
"publishFailed": "प्रकाशन असफल भयो",
|
||||||
"createDraftSuccess": "ड्राफ्ट v{{version}} सिर्जना भयो",
|
"createDraftSuccess": "ड्राफ्ट v{{version}} सिर्जना भयो",
|
||||||
@@ -186,7 +190,13 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"enable": "सक्रिय",
|
"enable": "सक्रिय",
|
||||||
"disable": "निष्क्रिय",
|
"disable": "निष्क्रिय",
|
||||||
"ruleText": "नियम पाठ"
|
"ruleText": "नियम पाठ",
|
||||||
|
"displayNames": "बहुभाषी नाम"
|
||||||
|
},
|
||||||
|
"locales": {
|
||||||
|
"zh": "चिनियाँ",
|
||||||
|
"en": "English",
|
||||||
|
"ne": "नेपाली"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"playCode": "खेल कोड",
|
"playCode": "खेल कोड",
|
||||||
@@ -206,10 +216,15 @@
|
|||||||
"aria": {
|
"aria": {
|
||||||
"enablePlay": "{{playCode}} सक्रिय गर्ने"
|
"enablePlay": "{{playCode}} सक्रिय गर्ने"
|
||||||
},
|
},
|
||||||
|
"nameDialog": {
|
||||||
|
"title": "प्रदर्शित नाम (बहुभाषी)",
|
||||||
|
"description": "खेल {{playCode}}। चिनियाँ अनिवार्य; अंग्रेजी र नेपाली वैकल्पिक। प्रकाशनपछि खेलाडीको भाषा अनुसार देखिन्छ।",
|
||||||
|
"apply": "ड्राफ्टमा लागू गर्नुहोस्",
|
||||||
|
"savedLocal": "प्रदर्शित नाम स्थानीय ड्राफ्टमा सुरक्षित भयो। स्थायी बनाउन ड्राफ्ट सेभ गर्नुहोस्।"
|
||||||
|
},
|
||||||
"ruleDialog": {
|
"ruleDialog": {
|
||||||
"title": "नियम पाठ (Chinese)",
|
"title": "नियम पाठ (बहुभाषी)",
|
||||||
"description": "खेल {{playCode}}। परिवर्तनहरू सेभ र प्रकाशित नगरेसम्म ड्राफ्टमै रहन्छन्।",
|
"description": "खेल {{playCode}}। परिवर्तनहरू सेभ र प्रकाशित नगरेसम्म ड्राफ्टमै रहन्छन्।",
|
||||||
"fieldLabel": "rule_text_zh",
|
|
||||||
"apply": "ड्राफ्टमा लागू गर्नुहोस्"
|
"apply": "ड्राफ्टमा लागू गर्नुहोस्"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,10 +94,13 @@
|
|||||||
"loadFailed": "系统设置加载失败",
|
"loadFailed": "系统设置加载失败",
|
||||||
"saveSuccess": "系统设置已保存",
|
"saveSuccess": "系统设置已保存",
|
||||||
"saveFailed": "系统设置保存失败",
|
"saveFailed": "系统设置保存失败",
|
||||||
|
"frontendConfig": "前端配置",
|
||||||
"fields": {
|
"fields": {
|
||||||
"manualReview": "开奖结果必须人工审核",
|
"manualReview": "开奖结果必须人工审核",
|
||||||
"cooldownMinutes": "冷静期时长(分钟)",
|
"cooldownMinutes": "冷静期时长(分钟)",
|
||||||
"autoSettlement": "自动执行结算"
|
"autoSettlement": "自动执行结算",
|
||||||
|
"playRulesHtml": "玩法规则 HTML(多语言)",
|
||||||
|
"playRulesHtmlDesc": "该内容将直接在玩家端的玩法规则页面作为 HTML 渲染。按语言分别配置;留空则回退其它语言或显示默认提示。"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"manualReview": "开启后,RNG 开奖结果会先进入待审核,必须由后台人工发布。",
|
"manualReview": "开启后,RNG 开奖结果会先进入待审核,必须由后台人工发布。",
|
||||||
@@ -169,7 +172,8 @@
|
|||||||
"jackpot": "奖池"
|
"jackpot": "奖池"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"minMaxInvalid": "{{playCode}}:最小下注额不能大于最大下注额"
|
"minMaxInvalid": "{{playCode}}:最小下注额不能大于最大下注额",
|
||||||
|
"nameZhRequired": "中文显示名称不能为空"
|
||||||
},
|
},
|
||||||
"publishFailed": "发布失败",
|
"publishFailed": "发布失败",
|
||||||
"createDraftSuccess": "已创建草稿 v{{version}}",
|
"createDraftSuccess": "已创建草稿 v{{version}}",
|
||||||
@@ -186,7 +190,13 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"enable": "开启",
|
"enable": "开启",
|
||||||
"disable": "关闭",
|
"disable": "关闭",
|
||||||
"ruleText": "规则文案"
|
"ruleText": "规则文案",
|
||||||
|
"displayNames": "多语言名称"
|
||||||
|
},
|
||||||
|
"locales": {
|
||||||
|
"zh": "中文",
|
||||||
|
"en": "English",
|
||||||
|
"ne": "नेपाली"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"playCode": "玩法编码",
|
"playCode": "玩法编码",
|
||||||
@@ -206,10 +216,15 @@
|
|||||||
"aria": {
|
"aria": {
|
||||||
"enablePlay": "切换 {{playCode}} 启用状态"
|
"enablePlay": "切换 {{playCode}} 启用状态"
|
||||||
},
|
},
|
||||||
|
"nameDialog": {
|
||||||
|
"title": "显示名称(多语言)",
|
||||||
|
"description": "玩法 {{playCode}};中文必填,英文与尼泊尔语可选。保存草稿并发布后,前台按玩家语言展示。",
|
||||||
|
"apply": "应用到草稿",
|
||||||
|
"savedLocal": "显示名称已写入本地草稿,记得保存草稿后再发布。"
|
||||||
|
},
|
||||||
"ruleDialog": {
|
"ruleDialog": {
|
||||||
"title": "规则文案(中文)",
|
"title": "规则文案(多语言)",
|
||||||
"description": "玩法 {{playCode}};修改内容只会暂存到草稿,保存并发布后才会生效。",
|
"description": "玩法 {{playCode}};修改内容只会暂存到草稿,保存并发布后才会生效。",
|
||||||
"fieldLabel": "中文规则文案",
|
|
||||||
"apply": "应用到草稿"
|
"apply": "应用到草稿"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -86,11 +86,13 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="admin-list-card w-full max-w-none">
|
<Card className="admin-list-card w-full max-w-none">
|
||||||
<CardHeader className="admin-list-header flex flex-col gap-5">
|
<CardHeader className="admin-list-header">
|
||||||
<CardTitle className="admin-list-title">{t("title")}</CardTitle>
|
<CardTitle className="admin-list-title">{t("title")}</CardTitle>
|
||||||
<div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-5">
|
</CardHeader>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<CardContent className="admin-list-content">
|
||||||
<Label htmlFor="aud-operator-id" className="shrink-0 whitespace-nowrap">
|
<div className="admin-list-toolbar">
|
||||||
|
<div className="admin-list-field">
|
||||||
|
<Label htmlFor="aud-operator-id" className="sm:shrink-0">
|
||||||
{t("operator")}
|
{t("operator")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -98,12 +100,12 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
value={operatorId}
|
value={operatorId}
|
||||||
onChange={(e) => setOperatorId(e.target.value)}
|
onChange={(e) => setOperatorId(e.target.value)}
|
||||||
placeholder={t("operatorIdPlaceholder")}
|
placeholder={t("operatorIdPlaceholder")}
|
||||||
className="w-full"
|
className="w-full sm:w-36"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="admin-list-field">
|
||||||
<Label htmlFor="aud-mod" className="shrink-0 whitespace-nowrap">
|
<Label htmlFor="aud-mod" className="sm:shrink-0">
|
||||||
{t("moduleCode")}
|
{t("moduleCode")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -111,11 +113,11 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
value={moduleCode}
|
value={moduleCode}
|
||||||
onChange={(e) => setModuleCode(e.target.value)}
|
onChange={(e) => setModuleCode(e.target.value)}
|
||||||
placeholder={t("exactMatch")}
|
placeholder={t("exactMatch")}
|
||||||
className="w-full"
|
className="w-full sm:w-40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="admin-list-field">
|
||||||
<Label htmlFor="aud-act" className="shrink-0 whitespace-nowrap">
|
<Label htmlFor="aud-act" className="sm:shrink-0">
|
||||||
{t("actionCode")}
|
{t("actionCode")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -123,11 +125,11 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
value={actionCode}
|
value={actionCode}
|
||||||
onChange={(e) => setActionCode(e.target.value)}
|
onChange={(e) => setActionCode(e.target.value)}
|
||||||
placeholder={t("exactMatch")}
|
placeholder={t("exactMatch")}
|
||||||
className="w-full"
|
className="w-full sm:w-40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="admin-list-field">
|
||||||
<Label htmlFor="aud-op" className="shrink-0 whitespace-nowrap">
|
<Label htmlFor="aud-op" className="sm:shrink-0">
|
||||||
{t("operatorType")}
|
{t("operatorType")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -135,13 +137,16 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
value={operatorType}
|
value={operatorType}
|
||||||
onChange={(e) => setOperatorType(e.target.value)}
|
onChange={(e) => setOperatorType(e.target.value)}
|
||||||
placeholder={t("operatorTypePlaceholder")}
|
placeholder={t("operatorTypePlaceholder")}
|
||||||
className="w-full"
|
className="w-full sm:w-40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 xl:col-span-2">
|
<div className="admin-list-field">
|
||||||
|
<Label htmlFor="aud-date-range" className="sm:shrink-0">
|
||||||
|
{t("time")}
|
||||||
|
</Label>
|
||||||
|
<div className="min-w-0 w-full sm:w-56">
|
||||||
<AdminDateRangeField
|
<AdminDateRangeField
|
||||||
id="aud-date-range"
|
id="aud-date-range"
|
||||||
label={t("time")}
|
|
||||||
from={startDate}
|
from={startDate}
|
||||||
to={endDate}
|
to={endDate}
|
||||||
onRangeChange={(range) => {
|
onRangeChange={(range) => {
|
||||||
@@ -151,7 +156,7 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap justify-end gap-2">
|
<div className="admin-list-actions">
|
||||||
<AdminTableExportButton
|
<AdminTableExportButton
|
||||||
tableId="audit-logs-table"
|
tableId="audit-logs-table"
|
||||||
filename={exportLabels.filename}
|
filename={exportLabels.filename}
|
||||||
@@ -193,8 +198,7 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
{t("actions.reset", { ns: "common" })}
|
{t("actions.reset", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent className="admin-list-content">
|
|
||||||
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
|
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
|
||||||
{loading && !data ? (
|
{loading && !data ? (
|
||||||
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
|
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
|
||||||
@@ -202,7 +206,7 @@ export function AuditLogsConsole(): React.ReactElement {
|
|||||||
|
|
||||||
{data ? (
|
{data ? (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-md border">
|
<div className="admin-table-shell">
|
||||||
<Table id="audit-logs-table">
|
<Table id="audit-logs-table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||||
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||||
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||||
@@ -146,9 +147,16 @@ 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 [nameDraftZh, setNameDraftZh] = useState("");
|
||||||
|
const [nameDraftEn, setNameDraftEn] = useState("");
|
||||||
|
const [nameDraftNe, setNameDraftNe] = 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("");
|
||||||
|
const [ruleDraftEn, setRuleDraftEn] = useState("");
|
||||||
|
const [ruleDraftNe, setRuleDraftNe] = useState("");
|
||||||
|
|
||||||
const refreshList = useCallback(async () => {
|
const refreshList = useCallback(async () => {
|
||||||
setLoadingList(true);
|
setLoadingList(true);
|
||||||
@@ -348,23 +356,80 @@ export function PlayConfigDocScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openNameEditor(play_code: string) {
|
||||||
|
const item = draftRows.find((row) => row.play_code === play_code);
|
||||||
|
setNamePlayCode(play_code);
|
||||||
|
setNameDraftZh(item?.display_name_zh ?? item?.play_code ?? "");
|
||||||
|
setNameDraftEn(item?.display_name_en ?? "");
|
||||||
|
setNameDraftNe(item?.display_name_ne ?? "");
|
||||||
|
setNameDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNameDraft() {
|
||||||
|
if (!namePlayCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const zh = nameDraftZh.trim();
|
||||||
|
if (!zh) {
|
||||||
|
toast.error(t("play.validation.nameZhRequired", { ns: "config" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateConfigRow(namePlayCode, {
|
||||||
|
display_name_zh: zh,
|
||||||
|
display_name_en: nameDraftEn.trim() || null,
|
||||||
|
display_name_ne: nameDraftNe.trim() || null,
|
||||||
|
});
|
||||||
|
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);
|
||||||
setRuleDraftZh(item?.rule_text_zh ?? "");
|
setRuleDraftZh(item?.rule_text_zh ?? "");
|
||||||
|
setRuleDraftEn(item?.rule_text_en ?? "");
|
||||||
|
setRuleDraftNe(item?.rule_text_ne ?? "");
|
||||||
setRuleDialogOpen(true);
|
setRuleDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveRuleZh() {
|
function saveRuleDraft() {
|
||||||
if (!rulePlayCode) {
|
if (!rulePlayCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateConfigRow(rulePlayCode, { rule_text_zh: ruleDraftZh.trim() || null });
|
updateConfigRow(rulePlayCode, {
|
||||||
|
rule_text_zh: ruleDraftZh.trim() || null,
|
||||||
|
rule_text_en: ruleDraftEn.trim() || null,
|
||||||
|
rule_text_ne: ruleDraftNe.trim() || null,
|
||||||
|
});
|
||||||
setRuleDialogOpen(false);
|
setRuleDialogOpen(false);
|
||||||
setRulePlayCode(null);
|
setRulePlayCode(null);
|
||||||
toast.message(t("play.ruleSavedLocal", { ns: "config" }));
|
toast.message(t("play.ruleSavedLocal", { ns: "config" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderDisplayNameReadonly(row: PlayConfigItemRow) {
|
||||||
|
const lines = [
|
||||||
|
{ label: t("play.locales.zh", { ns: "config" }), value: row.display_name_zh },
|
||||||
|
{ label: t("play.locales.en", { ns: "config" }), value: row.display_name_en },
|
||||||
|
{ label: t("play.locales.ne", { ns: "config" }), value: row.display_name_ne },
|
||||||
|
].filter((line) => line.value?.trim());
|
||||||
|
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return <span>—</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-0.5 text-center text-sm">
|
||||||
|
{lines.map((line) => (
|
||||||
|
<p key={line.label}>
|
||||||
|
<span className="text-muted-foreground text-xs">{line.label}: </span>
|
||||||
|
{line.value}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const activeHead = list.find((x) => x.status === "active");
|
const activeHead = list.find((x) => x.status === "active");
|
||||||
|
|
||||||
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
||||||
@@ -511,18 +576,24 @@ export function PlayConfigDocScreen() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{isDraft ? (
|
{isDraft ? (
|
||||||
<Input
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
className="h-8 text-center text-sm"
|
<p className="max-w-[10rem] truncate text-sm font-medium">
|
||||||
value={row.display_name_zh ?? ""}
|
{row.display_name_zh ?? row.play_code}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
onChange={(e) => {
|
onClick={() => openNameEditor(row.play_code)}
|
||||||
const next = e.target.value === "" ? null : e.target.value;
|
>
|
||||||
updateConfigRow(row.play_code, { display_name_zh: next });
|
{t("play.actions.displayNames", { ns: "config" })}
|
||||||
}}
|
</Button>
|
||||||
/>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ConfigReadonlyValue className="justify-center">
|
<ConfigReadonlyValue className="justify-center">
|
||||||
{row.display_name_zh ?? "—"}
|
{renderDisplayNameReadonly(row)}
|
||||||
</ConfigReadonlyValue>
|
</ConfigReadonlyValue>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -609,6 +680,51 @@ 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-3">
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
<Label htmlFor="name-zh">{t("play.locales.zh", { ns: "config" })}</Label>
|
||||||
|
<Input
|
||||||
|
id="name-zh"
|
||||||
|
value={nameDraftZh}
|
||||||
|
onChange={(e) => setNameDraftZh(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
<Label htmlFor="name-en">{t("play.locales.en", { ns: "config" })}</Label>
|
||||||
|
<Input
|
||||||
|
id="name-en"
|
||||||
|
value={nameDraftEn}
|
||||||
|
onChange={(e) => setNameDraftEn(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
<Label htmlFor="name-ne">{t("play.locales.ne", { ns: "config" })}</Label>
|
||||||
|
<Input
|
||||||
|
id="name-ne"
|
||||||
|
value={nameDraftNe}
|
||||||
|
onChange={(e) => setNameDraftNe(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
@@ -617,20 +733,42 @@ export function PlayConfigDocScreen() {
|
|||||||
{t("play.ruleDialog.description", { ns: "config", playCode: rulePlayCode ?? "—" })}
|
{t("play.ruleDialog.description", { ns: "config", playCode: rulePlayCode ?? "—" })}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-2">
|
<Tabs defaultValue="zh" className="w-full">
|
||||||
<Label htmlFor="rule-zh">{t("play.ruleDialog.fieldLabel", { ns: "config" })}</Label>
|
<TabsList className="w-full">
|
||||||
|
<TabsTrigger value="zh">{t("play.locales.zh", { ns: "config" })}</TabsTrigger>
|
||||||
|
<TabsTrigger value="en">{t("play.locales.en", { ns: "config" })}</TabsTrigger>
|
||||||
|
<TabsTrigger value="ne">{t("play.locales.ne", { ns: "config" })}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="zh" className="mt-3">
|
||||||
<textarea
|
<textarea
|
||||||
id="rule-zh"
|
id="rule-zh"
|
||||||
className="border-input bg-background ring-ring/24 focus-visible:ring-[3px] min-h-[140px] w-full rounded-lg border px-3 py-2 text-sm outline-none"
|
className="border-input bg-background ring-ring/24 focus-visible:ring-[3px] min-h-[140px] w-full rounded-lg border px-3 py-2 text-sm outline-none"
|
||||||
value={ruleDraftZh}
|
value={ruleDraftZh}
|
||||||
onChange={(e) => setRuleDraftZh(e.target.value)}
|
onChange={(e) => setRuleDraftZh(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</TabsContent>
|
||||||
|
<TabsContent value="en" className="mt-3">
|
||||||
|
<textarea
|
||||||
|
id="rule-en"
|
||||||
|
className="border-input bg-background ring-ring/24 focus-visible:ring-[3px] min-h-[140px] w-full rounded-lg border px-3 py-2 text-sm outline-none"
|
||||||
|
value={ruleDraftEn}
|
||||||
|
onChange={(e) => setRuleDraftEn(e.target.value)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="ne" className="mt-3">
|
||||||
|
<textarea
|
||||||
|
id="rule-ne"
|
||||||
|
className="border-input bg-background ring-ring/24 focus-visible:ring-[3px] min-h-[140px] w-full rounded-lg border px-3 py-2 text-sm outline-none"
|
||||||
|
value={ruleDraftNe}
|
||||||
|
onChange={(e) => setRuleDraftNe(e.target.value)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="outline" onClick={() => setRuleDialogOpen(false)}>
|
<Button type="button" variant="outline" onClick={() => setRuleDialogOpen(false)}>
|
||||||
{t("actions.cancel", { ns: "adminUsers" })}
|
{t("actions.cancel", { ns: "adminUsers" })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" onClick={saveRuleZh}>
|
<Button type="button" onClick={saveRuleDraft}>
|
||||||
{t("play.ruleDialog.apply", { ns: "config" })}
|
{t("play.ruleDialog.apply", { ns: "config" })}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -3,14 +3,10 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ConfigDocPage } from "@/modules/config/config-doc-page";
|
|
||||||
import { ConfigSection } from "@/modules/config/config-section";
|
|
||||||
import { JackpotPoolsConsole } from "@/modules/jackpot/jackpot-pools-console";
|
import { JackpotPoolsConsole } from "@/modules/jackpot/jackpot-pools-console";
|
||||||
import { JackpotRecordsConsole } from "@/modules/jackpot/jackpot-records-console";
|
import { JackpotRecordsConsole } from "@/modules/jackpot/jackpot-records-console";
|
||||||
|
|
||||||
/**
|
/** 奖池单页:池参数 + 流水记录,避免 ConfigDocPage / 内层 Card 重复套娃。 */
|
||||||
* 奖池:仅保留「侧栏 + 运营配置顶栏」两层导航;池参数与流水在同一页用分区展示。
|
|
||||||
*/
|
|
||||||
export function JackpotConfigScreen() {
|
export function JackpotConfigScreen() {
|
||||||
const { t } = useTranslation("jackpot");
|
const { t } = useTranslation("jackpot");
|
||||||
|
|
||||||
@@ -27,13 +23,20 @@ export function JackpotConfigScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigDocPage title={t("configTitle")}>
|
<div className="flex flex-col gap-10">
|
||||||
<ConfigSection title={t("poolsSectionTitle")}>
|
<section className="space-y-4">
|
||||||
|
<h2 className="border-b border-border/60 pb-3 text-base font-semibold text-foreground">
|
||||||
|
{t("poolsSectionTitle")}
|
||||||
|
</h2>
|
||||||
<JackpotPoolsConsole embedded />
|
<JackpotPoolsConsole embedded />
|
||||||
</ConfigSection>
|
</section>
|
||||||
<ConfigSection id="jackpot-records" title={t("recordsSectionTitle")}>
|
|
||||||
|
<section id="jackpot-records" className="scroll-mt-24 space-y-4">
|
||||||
|
<h2 className="border-b border-border/60 pb-3 text-base font-semibold text-foreground">
|
||||||
|
{t("recordsSectionTitle")}
|
||||||
|
</h2>
|
||||||
<JackpotRecordsConsole embedded />
|
<JackpotRecordsConsole embedded />
|
||||||
</ConfigSection>
|
</section>
|
||||||
</ConfigDocPage>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,14 +150,8 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const body = (
|
const poolList = (
|
||||||
<Card className={embedded ? "border-border/60 shadow-none" : undefined}>
|
<div className={embedded ? "space-y-4" : "space-y-8"}>
|
||||||
{!embedded ? (
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">{t("configTitle")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
) : null}
|
|
||||||
<CardContent className="space-y-8">
|
|
||||||
{loading ? <p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p> : null}
|
{loading ? <p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p> : null}
|
||||||
{!loading && items.length === 0 ? (
|
{!loading && items.length === 0 ? (
|
||||||
<p className="text-muted-foreground text-sm">{t("noPoolData")}</p>
|
<p className="text-muted-foreground text-sm">{t("noPoolData")}</p>
|
||||||
@@ -165,9 +159,12 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
{items.map((p) => {
|
{items.map((p) => {
|
||||||
const d = drafts[p.id] ?? toDraft(p);
|
const d = drafts[p.id] ?? toDraft(p);
|
||||||
return (
|
return (
|
||||||
<div key={p.id} className="space-y-4 rounded-lg border border-border p-4">
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="space-y-4 rounded-xl border border-border/60 bg-muted/10 p-4"
|
||||||
|
>
|
||||||
<h3 className="font-mono text-sm font-semibold">{p.currency_code}</h3>
|
<h3 className="font-mono text-sm font-semibold">{p.currency_code}</h3>
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor={`amt-${p.id}`}>{t("currentAmount")}</Label>
|
<Label htmlFor={`amt-${p.id}`}>{t("currentAmount")}</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -233,12 +230,12 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>{t("status")}</Label>
|
<Label htmlFor={`status-${p.id}`}>{t("status")}</Label>
|
||||||
<Select
|
<Select
|
||||||
value={d.status}
|
value={d.status}
|
||||||
onValueChange={(v) => updateDraft(p.id, { status: v ?? "0" })}
|
onValueChange={(v) => updateDraft(p.id, { status: v ?? "0" })}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger id={`status-${p.id}`} className="w-full">
|
||||||
<SelectValue>{d.status === "1" ? t("enabled") : t("disabled")}</SelectValue>
|
<SelectValue>{d.status === "1" ? t("enabled") : t("disabled")}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -248,14 +245,17 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end border-t border-border/60 pt-3">
|
||||||
<Button type="button" disabled={savingId === p.id} onClick={() => void save(p)}>
|
<Button type="button" disabled={savingId === p.id} onClick={() => void save(p)}>
|
||||||
{savingId === p.id ? t("saving") : t("save")}
|
{savingId === p.id ? t("saving") : t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-3">
|
<div className="rounded-lg border border-amber-200/80 bg-amber-50/80 p-4 dark:border-amber-900/50 dark:bg-amber-950/30">
|
||||||
<div className="grid gap-3 sm:grid-cols-[1fr_1fr_auto] sm:items-end">
|
<p className="mb-3 text-xs font-medium text-amber-900 dark:text-amber-200">
|
||||||
<div className="space-y-1.5">
|
{t("manualBurst")}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end">
|
||||||
|
<div className="min-w-0 flex-1 space-y-1.5 sm:max-w-xs">
|
||||||
<Label htmlFor={`burst-draw-${p.id}`}>{t("manualBurstDrawId")}</Label>
|
<Label htmlFor={`burst-draw-${p.id}`}>{t("manualBurstDrawId")}</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`burst-draw-${p.id}`}
|
id={`burst-draw-${p.id}`}
|
||||||
@@ -264,7 +264,7 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
onChange={(e) => updateDraft(p.id, { manual_burst_draw_id: e.target.value })}
|
onChange={(e) => updateDraft(p.id, { manual_burst_draw_id: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="min-w-0 flex-1 space-y-1.5 sm:max-w-xs">
|
||||||
<Label htmlFor={`burst-amount-${p.id}`}>{t("manualBurstAmount")}</Label>
|
<Label htmlFor={`burst-amount-${p.id}`}>{t("manualBurstAmount")}</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`burst-amount-${p.id}`}
|
id={`burst-amount-${p.id}`}
|
||||||
@@ -276,6 +276,7 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
className="shrink-0 sm:ml-auto"
|
||||||
disabled={burstingId === p.id}
|
disabled={burstingId === p.id}
|
||||||
onClick={() => void manualBurst(p)}
|
onClick={() => void manualBurst(p)}
|
||||||
>
|
>
|
||||||
@@ -286,13 +287,21 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
return body;
|
return poolList;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ModuleScaffold>{body}</ModuleScaffold>;
|
return (
|
||||||
|
<ModuleScaffold>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">{t("configTitle")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>{poolList}</CardContent>
|
||||||
|
</Card>
|
||||||
|
</ModuleScaffold>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -32,6 +33,51 @@ type JackpotRecordsConsoleProps = {
|
|||||||
embedded?: boolean;
|
embedded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 表格在 admin-table-shell 内时去掉 Table 组件自带的第二层边框 */
|
||||||
|
const TABLE_IN_SHELL_CLASS =
|
||||||
|
"[&_[data-slot=table-container]]:rounded-none [&_[data-slot=table-container]]:border-0 [&_[data-slot=table-container]]:bg-transparent [&_[data-slot=table-container]]:shadow-none";
|
||||||
|
|
||||||
|
function JackpotRecordTableSection({
|
||||||
|
title,
|
||||||
|
tableId,
|
||||||
|
exportFilename,
|
||||||
|
exportSheetName,
|
||||||
|
loading,
|
||||||
|
hasData,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
tableId: string;
|
||||||
|
exportFilename: string;
|
||||||
|
exportSheetName: string;
|
||||||
|
loading: boolean;
|
||||||
|
hasData: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
footer: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation("common");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-table-shell">
|
||||||
|
<div className="admin-table-toolbar flex items-center justify-between gap-3">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
|
||||||
|
<AdminTableExportButton
|
||||||
|
tableId={tableId}
|
||||||
|
filename={exportFilename}
|
||||||
|
sheetName={exportSheetName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{loading && !hasData ? (
|
||||||
|
<p className="px-4 py-6 text-sm text-muted-foreground">{t("states.loading")}</p>
|
||||||
|
) : (
|
||||||
|
<div className={TABLE_IN_SHELL_CLASS}>{children}</div>
|
||||||
|
)}
|
||||||
|
{footer ? <div className="px-4 pb-4">{footer}</div> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsoleProps) {
|
export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsoleProps) {
|
||||||
const { t } = useTranslation(["jackpot", "common"]);
|
const { t } = useTranslation(["jackpot", "common"]);
|
||||||
const payoutExport = useExportLabels("jackpotPayouts");
|
const payoutExport = useExportLabels("jackpotPayouts");
|
||||||
@@ -147,74 +193,7 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const payoutHeader = embedded ? (
|
const payoutFooter = payouts ? (
|
||||||
<p className="mb-3 text-sm font-semibold">{t("payoutRecords")}</p>
|
|
||||||
) : (
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">{t("payoutRecords")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
const contributionHeader = embedded ? (
|
|
||||||
<p className="mb-3 text-sm font-semibold">{t("contributionRecords")}</p>
|
|
||||||
) : (
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">{t("contributionRecords")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<>
|
|
||||||
{filterBlock}
|
|
||||||
|
|
||||||
{err ? <p className="text-destructive mb-4 text-sm">{err}</p> : null}
|
|
||||||
|
|
||||||
<Card className={embedded ? "mb-6 border-border/60 shadow-none" : "mb-8"}>
|
|
||||||
{!embedded ? payoutHeader : null}
|
|
||||||
<CardContent className={embedded ? "p-0" : undefined}>
|
|
||||||
{embedded ? payoutHeader : null}
|
|
||||||
{loadingP && !payouts ? (
|
|
||||||
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="admin-table-toolbar">
|
|
||||||
<AdminTableExportButton
|
|
||||||
tableId="jackpot-payout-table"
|
|
||||||
filename={payoutExport.filename}
|
|
||||||
sheetName={payoutExport.sheetName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Table id="jackpot-payout-table">
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>{t("table.id", { ns: "common" })}</TableHead>
|
|
||||||
<TableHead>{t("drawNo")}</TableHead>
|
|
||||||
<TableHead>{t("trigger")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("payoutAmount")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("winnerCount")}</TableHead>
|
|
||||||
<TableHead>{t("time")}</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{(payouts?.items ?? []).map((r) => (
|
|
||||||
<TableRow key={r.id}>
|
|
||||||
<TableCell className="font-mono text-xs">{r.id}</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs">{r.draw_no ?? "—"}</TableCell>
|
|
||||||
<TableCell className="text-xs">{triggerTypeText(r.trigger_type)}</TableCell>
|
|
||||||
<TableCell className="text-right font-mono text-xs tabular-nums">
|
|
||||||
{formatAdminMinorUnits(r.total_payout_amount, r.currency_code ?? "NPR")}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right tabular-nums">{r.winner_count}</TableCell>
|
|
||||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
|
||||||
{formatDt(r.created_at)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{payouts ? (
|
|
||||||
<AdminListPaginationFooter
|
<AdminListPaginationFooter
|
||||||
selectId="jk-payout-per"
|
selectId="jk-payout-per"
|
||||||
total={payouts.meta.total}
|
total={payouts.meta.total}
|
||||||
@@ -228,58 +207,9 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
|
|||||||
}}
|
}}
|
||||||
onPageChange={setPPage}
|
onPageChange={setPPage}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null;
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className={embedded ? "border-border/60 shadow-none" : undefined}>
|
const contributionFooter = contribs ? (
|
||||||
{!embedded ? contributionHeader : null}
|
|
||||||
<CardContent className={embedded ? "p-0" : undefined}>
|
|
||||||
{embedded ? contributionHeader : null}
|
|
||||||
{loadingC && !contribs ? (
|
|
||||||
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="admin-table-toolbar">
|
|
||||||
<AdminTableExportButton
|
|
||||||
tableId="jackpot-contribution-table"
|
|
||||||
filename={contributionExport.filename}
|
|
||||||
sheetName={contributionExport.sheetName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Table id="jackpot-contribution-table">
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>{t("table.id", { ns: "common" })}</TableHead>
|
|
||||||
<TableHead>{t("drawNo")}</TableHead>
|
|
||||||
<TableHead>{t("ticketNo")}</TableHead>
|
|
||||||
<TableHead>{t("player")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("contributionAmount")}</TableHead>
|
|
||||||
<TableHead>{t("time")}</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{(contribs?.items ?? []).map((r) => (
|
|
||||||
<TableRow key={r.id}>
|
|
||||||
<TableCell className="font-mono text-xs">{r.id}</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs">{r.draw_no ?? "—"}</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs">{r.ticket_no ?? "—"}</TableCell>
|
|
||||||
<TableCell className="max-w-[10rem] truncate text-xs">
|
|
||||||
{r.player_username ?? "—"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right font-mono text-xs tabular-nums">
|
|
||||||
{formatAdminMinorUnits(r.contribution_amount, r.currency_code ?? "NPR")}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
|
||||||
{formatDt(r.created_at)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{contribs ? (
|
|
||||||
<AdminListPaginationFooter
|
<AdminListPaginationFooter
|
||||||
selectId="jk-contrib-per"
|
selectId="jk-contrib-per"
|
||||||
total={contribs.meta.total}
|
total={contribs.meta.total}
|
||||||
@@ -293,9 +223,122 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
|
|||||||
}}
|
}}
|
||||||
onPageChange={setCPage}
|
onPageChange={setCPage}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null;
|
||||||
</CardContent>
|
|
||||||
</Card>
|
const payoutTable = (
|
||||||
|
<JackpotRecordTableSection
|
||||||
|
title={t("payoutRecords")}
|
||||||
|
tableId="jackpot-payout-table"
|
||||||
|
exportFilename={payoutExport.filename}
|
||||||
|
exportSheetName={payoutExport.sheetName}
|
||||||
|
loading={loadingP}
|
||||||
|
hasData={payouts != null}
|
||||||
|
footer={payoutFooter}
|
||||||
|
>
|
||||||
|
<Table id="jackpot-payout-table" className="table-fixed">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-14">{t("table.id", { ns: "common" })}</TableHead>
|
||||||
|
<TableHead className="w-[11rem]">{t("drawNo")}</TableHead>
|
||||||
|
<TableHead className="w-28">{t("trigger")}</TableHead>
|
||||||
|
<TableHead className="w-32 text-right">{t("payoutAmount")}</TableHead>
|
||||||
|
<TableHead className="w-24 text-right">{t("winnerCount")}</TableHead>
|
||||||
|
<TableHead className="w-[11rem]">{t("time")}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{(payouts?.items ?? []).length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-muted-foreground">
|
||||||
|
{t("states.noData", { ns: "common" })}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
(payouts?.items ?? []).map((r) => (
|
||||||
|
<TableRow key={r.id}>
|
||||||
|
<TableCell className="font-mono text-xs">{r.id}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">{r.draw_no ?? "—"}</TableCell>
|
||||||
|
<TableCell className="text-xs">{triggerTypeText(r.trigger_type)}</TableCell>
|
||||||
|
<TableCell className="text-right font-mono text-xs tabular-nums">
|
||||||
|
{formatAdminMinorUnits(r.total_payout_amount, r.currency_code ?? "NPR")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">{r.winner_count}</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{formatDt(r.created_at)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</JackpotRecordTableSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contributionTable = (
|
||||||
|
<JackpotRecordTableSection
|
||||||
|
title={t("contributionRecords")}
|
||||||
|
tableId="jackpot-contribution-table"
|
||||||
|
exportFilename={contributionExport.filename}
|
||||||
|
exportSheetName={contributionExport.sheetName}
|
||||||
|
loading={loadingC}
|
||||||
|
hasData={contribs != null}
|
||||||
|
footer={contributionFooter}
|
||||||
|
>
|
||||||
|
<Table id="jackpot-contribution-table" className="table-fixed">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-14">{t("table.id", { ns: "common" })}</TableHead>
|
||||||
|
<TableHead className="w-[11rem]">{t("drawNo")}</TableHead>
|
||||||
|
<TableHead className="w-[11rem]">{t("ticketNo")}</TableHead>
|
||||||
|
<TableHead>{t("player")}</TableHead>
|
||||||
|
<TableHead className="w-32 text-right">{t("contributionAmount")}</TableHead>
|
||||||
|
<TableHead className="w-[11rem]">{t("time")}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{(contribs?.items ?? []).length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-muted-foreground">
|
||||||
|
{t("states.noData", { ns: "common" })}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
(contribs?.items ?? []).map((r) => (
|
||||||
|
<TableRow key={r.id}>
|
||||||
|
<TableCell className="font-mono text-xs">{r.id}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">{r.draw_no ?? "—"}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">{r.ticket_no ?? "—"}</TableCell>
|
||||||
|
<TableCell className="max-w-[12rem] truncate text-xs">{r.player_username ?? "—"}</TableCell>
|
||||||
|
<TableCell className="text-right font-mono text-xs tabular-nums">
|
||||||
|
{formatAdminMinorUnits(r.contribution_amount, r.currency_code ?? "NPR")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{formatDt(r.created_at)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</JackpotRecordTableSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
{filterBlock}
|
||||||
|
{err ? <p className="text-destructive text-sm">{err}</p> : null}
|
||||||
|
|
||||||
|
{embedded ? (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{payoutTable}
|
||||||
|
{contributionTable}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{payoutTable}
|
||||||
|
{contributionTable}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
postAdminCurrency,
|
postAdminCurrency,
|
||||||
putAdminCurrency,
|
putAdminCurrency,
|
||||||
} from "@/api/admin-currencies";
|
} from "@/api/admin-currencies";
|
||||||
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -205,12 +206,7 @@ export function CurrencySettingsPanel() {
|
|||||||
return (
|
return (
|
||||||
<Card className="admin-list-card">
|
<Card className="admin-list-card">
|
||||||
<CardHeader className="admin-list-header flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<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>
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<AdminTableExportButton tableId="admin-currencies-table" filename={exportLabels.filename}
|
<AdminTableExportButton tableId="admin-currencies-table" filename={exportLabels.filename}
|
||||||
sheetName={exportLabels.sheetName} />
|
sheetName={exportLabels.sheetName} />
|
||||||
@@ -249,8 +245,22 @@ export function CurrencySettingsPanel() {
|
|||||||
<TableCell className="font-mono">{row.code}</TableCell>
|
<TableCell className="font-mono">{row.code}</TableCell>
|
||||||
<TableCell>{row.name}</TableCell>
|
<TableCell>{row.name}</TableCell>
|
||||||
<TableCell>{row.decimal_places}</TableCell>
|
<TableCell>{row.decimal_places}</TableCell>
|
||||||
<TableCell>{row.is_enabled ? t("system.states.enabled", { ns: "config" }) : t("system.states.disabled", { ns: "config" })}</TableCell>
|
<TableCell>
|
||||||
<TableCell>{row.is_bettable ? t("system.states.enabled", { ns: "config" }) : t("system.states.disabled", { ns: "config" })}</TableCell>
|
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
|
||||||
|
{row.is_enabled
|
||||||
|
? t("system.states.enabled", { ns: "config" })
|
||||||
|
: t("system.states.disabled", { ns: "config" })}
|
||||||
|
</AdminStatusBadge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<AdminStatusBadge
|
||||||
|
status={row.is_enabled && row.is_bettable ? "enabled" : "disabled"}
|
||||||
|
>
|
||||||
|
{row.is_enabled && row.is_bettable
|
||||||
|
? t("system.states.enabled", { ns: "config" })
|
||||||
|
: t("system.states.disabled", { ns: "config" })}
|
||||||
|
</AdminStatusBadge>
|
||||||
|
</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">
|
||||||
<Button variant="outline" size="sm" onClick={() => openEdit(row)}>
|
<Button variant="outline" size="sm" onClick={() => openEdit(row)}>
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
} from "@/api/admin-settings";
|
} from "@/api/admin-settings";
|
||||||
import { WalletConfigDocScreen } from "@/modules/config/doc/wallet-config-doc-screen";
|
import { WalletConfigDocScreen } from "@/modules/config/doc/wallet-config-doc-screen";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
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 { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
|
||||||
const DRAW_GROUP = "draw";
|
const DRAW_GROUP = "draw";
|
||||||
@@ -28,13 +28,18 @@ const DRAW_KEYS = {
|
|||||||
const FRONTEND_GROUP = "frontend";
|
const FRONTEND_GROUP = "frontend";
|
||||||
const FRONTEND_KEYS = {
|
const FRONTEND_KEYS = {
|
||||||
PLAY_RULES_HTML: "frontend.play_rules_html",
|
PLAY_RULES_HTML: "frontend.play_rules_html",
|
||||||
|
PLAY_RULES_HTML_ZH: "frontend.play_rules_html_zh",
|
||||||
|
PLAY_RULES_HTML_EN: "frontend.play_rules_html_en",
|
||||||
|
PLAY_RULES_HTML_NE: "frontend.play_rules_html_ne",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
interface RuntimeDraft {
|
interface RuntimeDraft {
|
||||||
requireManualReview: boolean;
|
requireManualReview: boolean;
|
||||||
cooldownMinutes: string;
|
cooldownMinutes: string;
|
||||||
autoSettlement: boolean;
|
autoSettlement: boolean;
|
||||||
playRulesHtml: string;
|
playRulesHtmlZh: string;
|
||||||
|
playRulesHtmlEn: string;
|
||||||
|
playRulesHtmlNe: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BinaryChoice({
|
function BinaryChoice({
|
||||||
@@ -76,19 +81,56 @@ function BinaryChoice({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SaveActions({
|
||||||
|
dirty,
|
||||||
|
loading,
|
||||||
|
saving,
|
||||||
|
onSave,
|
||||||
|
onDiscard,
|
||||||
|
saveLabel,
|
||||||
|
savingLabel,
|
||||||
|
discardLabel,
|
||||||
|
}: {
|
||||||
|
dirty: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
saving: boolean;
|
||||||
|
onSave: () => void;
|
||||||
|
onDiscard: () => void;
|
||||||
|
saveLabel: string;
|
||||||
|
savingLabel: string;
|
||||||
|
discardLabel: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center gap-3 pt-2">
|
||||||
|
<Button type="button" onClick={onSave} disabled={!dirty || loading || saving}>
|
||||||
|
{saving ? savingLabel : saveLabel}
|
||||||
|
</Button>
|
||||||
|
{dirty ? (
|
||||||
|
<Button type="button" variant="outline" onClick={onDiscard}>
|
||||||
|
{discardLabel}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SystemSettingsScreen() {
|
export function SystemSettingsScreen() {
|
||||||
const { t } = useTranslation(["common", "config", "adminUsers"]);
|
const { t } = useTranslation(["common", "config", "adminUsers"]);
|
||||||
const [draft, setDraft] = useState<RuntimeDraft>({
|
const [draft, setDraft] = useState<RuntimeDraft>({
|
||||||
requireManualReview: false,
|
requireManualReview: false,
|
||||||
cooldownMinutes: "15",
|
cooldownMinutes: "15",
|
||||||
autoSettlement: true,
|
autoSettlement: true,
|
||||||
playRulesHtml: "",
|
playRulesHtmlZh: "",
|
||||||
|
playRulesHtmlEn: "",
|
||||||
|
playRulesHtmlNe: "",
|
||||||
});
|
});
|
||||||
const [saved, setSaved] = useState<RuntimeDraft>({
|
const [saved, setSaved] = useState<RuntimeDraft>({
|
||||||
requireManualReview: false,
|
requireManualReview: false,
|
||||||
cooldownMinutes: "15",
|
cooldownMinutes: "15",
|
||||||
autoSettlement: true,
|
autoSettlement: true,
|
||||||
playRulesHtml: "",
|
playRulesHtmlZh: "",
|
||||||
|
playRulesHtmlEn: "",
|
||||||
|
playRulesHtmlNe: "",
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -108,11 +150,14 @@ export function SystemSettingsScreen() {
|
|||||||
kv[item.key] = item.value;
|
kv[item.key] = item.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyHtml = String(kv[FRONTEND_KEYS.PLAY_RULES_HTML] ?? "");
|
||||||
const nextDraft: RuntimeDraft = {
|
const nextDraft: RuntimeDraft = {
|
||||||
requireManualReview: Boolean(kv[DRAW_KEYS.REQUIRE_MANUAL_REVIEW] ?? false),
|
requireManualReview: Boolean(kv[DRAW_KEYS.REQUIRE_MANUAL_REVIEW] ?? false),
|
||||||
cooldownMinutes: String(kv[DRAW_KEYS.COOLDOWN_MINUTES] ?? 15),
|
cooldownMinutes: String(kv[DRAW_KEYS.COOLDOWN_MINUTES] ?? 15),
|
||||||
autoSettlement: Boolean(kv[DRAW_KEYS.AUTO_SETTLEMENT] ?? true),
|
autoSettlement: Boolean(kv[DRAW_KEYS.AUTO_SETTLEMENT] ?? true),
|
||||||
playRulesHtml: String(kv[FRONTEND_KEYS.PLAY_RULES_HTML] ?? ""),
|
playRulesHtmlZh: String(kv[FRONTEND_KEYS.PLAY_RULES_HTML_ZH] ?? legacyHtml),
|
||||||
|
playRulesHtmlEn: String(kv[FRONTEND_KEYS.PLAY_RULES_HTML_EN] ?? ""),
|
||||||
|
playRulesHtmlNe: String(kv[FRONTEND_KEYS.PLAY_RULES_HTML_NE] ?? ""),
|
||||||
};
|
};
|
||||||
setDraft(nextDraft);
|
setDraft(nextDraft);
|
||||||
setSaved(nextDraft);
|
setSaved(nextDraft);
|
||||||
@@ -144,7 +189,10 @@ export function SystemSettingsScreen() {
|
|||||||
Math.max(0, Number.parseInt(draft.cooldownMinutes || "0", 10) || 0),
|
Math.max(0, Number.parseInt(draft.cooldownMinutes || "0", 10) || 0),
|
||||||
);
|
);
|
||||||
await updateAdminSetting(DRAW_KEYS.AUTO_SETTLEMENT, draft.autoSettlement);
|
await updateAdminSetting(DRAW_KEYS.AUTO_SETTLEMENT, draft.autoSettlement);
|
||||||
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML, draft.playRulesHtml);
|
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML_ZH, draft.playRulesHtmlZh);
|
||||||
|
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML_EN, draft.playRulesHtmlEn);
|
||||||
|
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML_NE, draft.playRulesHtmlNe);
|
||||||
|
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML, draft.playRulesHtmlZh);
|
||||||
toast.success(t("system.saveSuccess", { ns: "config" }));
|
toast.success(t("system.saveSuccess", { ns: "config" }));
|
||||||
setSaved(draft);
|
setSaved(draft);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
@@ -157,33 +205,20 @@ export function SystemSettingsScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveLabel = t("actions.save", { ns: "adminUsers" });
|
||||||
|
const savingLabel = t("saving", { ns: "adminUsers" });
|
||||||
|
const discardLabel = t("system.discard", { ns: "config" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
|
<div className="flex flex-col gap-10">
|
||||||
<Card>
|
|
||||||
<CardHeader className="space-y-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
||||||
{t("nav.settings", { ns: "common", defaultValue: "System Settings" })}
|
|
||||||
</p>
|
|
||||||
<CardTitle className="text-2xl">
|
|
||||||
{t("system.runtimeTitle", { ns: "config" })}
|
|
||||||
</CardTitle>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="space-y-8">
|
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
<h2 className="border-b border-border/60 pb-3 text-base font-semibold text-foreground">
|
||||||
<div className="space-y-1">
|
{t("system.title", { ns: "config" })}
|
||||||
<h3 className="text-base font-semibold">{t("system.title", { ns: "config" })}</h3>
|
</h2>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-5 rounded-2xl border border-border/60 bg-muted/10 px-4 py-4">
|
<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">
|
||||||
<div className="space-y-1">
|
|
||||||
<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>
|
||||||
</div>
|
|
||||||
<BinaryChoice
|
<BinaryChoice
|
||||||
active={draft.requireManualReview}
|
active={draft.requireManualReview}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
@@ -196,9 +231,7 @@ export function SystemSettingsScreen() {
|
|||||||
<div className="h-px bg-border/60" />
|
<div className="h-px bg-border/60" />
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<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>
|
<Label className="text-sm font-medium">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
|
||||||
</div>
|
|
||||||
<BinaryChoice
|
<BinaryChoice
|
||||||
active={draft.autoSettlement}
|
active={draft.autoSettlement}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
@@ -210,7 +243,7 @@ export function SystemSettingsScreen() {
|
|||||||
|
|
||||||
<div className="h-px bg-border/60" />
|
<div className="h-px bg-border/60" />
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid max-w-xs gap-2">
|
||||||
<Label htmlFor="cooldown-minutes" className="text-sm font-medium">
|
<Label htmlFor="cooldown-minutes" className="text-sm font-medium">
|
||||||
{t("system.fields.cooldownMinutes", { ns: "config" })}
|
{t("system.fields.cooldownMinutes", { ns: "config" })}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -222,79 +255,85 @@ export function SystemSettingsScreen() {
|
|||||||
value={draft.cooldownMinutes}
|
value={draft.cooldownMinutes}
|
||||||
onChange={(e) => updateDraft("cooldownMinutes", e.target.value)}
|
onChange={(e) => updateDraft("cooldownMinutes", e.target.value)}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
className="max-w-[240px]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</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>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-4 border-t border-border/60 pt-6">
|
<section className="space-y-4">
|
||||||
<div className="space-y-1">
|
<h2 className="border-b border-border/60 pb-3 text-base font-semibold text-foreground">
|
||||||
<h3 className="text-base font-semibold">{t("system.frontendConfig", { ns: "config", defaultValue: "前端配置" })}</h3>
|
{t("wallet.title", { ns: "config" })}
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-5 rounded-2xl border border-border/60 bg-muted/10 px-4 py-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="play-rules-html" className="text-sm font-medium">
|
|
||||||
{t("system.fields.playRulesHtml", { ns: "config", defaultValue: "玩法规则 HTML 内容" })}
|
|
||||||
</Label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("system.fields.playRulesHtmlDesc", { ns: "config", defaultValue: "该内容将直接在玩家端的玩法规则页面作为 HTML 渲染。留空则显示前端默认提示。" })}
|
|
||||||
</p>
|
|
||||||
<Textarea
|
|
||||||
id="play-rules-html"
|
|
||||||
value={draft.playRulesHtml}
|
|
||||||
onChange={(e) => updateDraft("playRulesHtml", e.target.value)}
|
|
||||||
disabled={loading || saving}
|
|
||||||
className="font-mono text-xs min-h-[200px]"
|
|
||||||
placeholder="<div>...</div>"
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<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 />
|
<WalletConfigDocScreen embedded />
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
<section className="space-y-4">
|
||||||
|
<h2 className="border-b border-border/60 pb-3 text-base font-semibold text-foreground">
|
||||||
|
{t("system.frontendConfig", { ns: "config" })}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
{t("system.fields.playRulesHtml", { ns: "config" })}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("system.fields.playRulesHtmlDesc", { ns: "config" })}
|
||||||
|
</p>
|
||||||
|
<Tabs defaultValue="zh" className="w-full">
|
||||||
|
<TabsList className="w-full max-w-md">
|
||||||
|
<TabsTrigger value="zh">{t("play.locales.zh", { ns: "config" })}</TabsTrigger>
|
||||||
|
<TabsTrigger value="en">{t("play.locales.en", { ns: "config" })}</TabsTrigger>
|
||||||
|
<TabsTrigger value="ne">{t("play.locales.ne", { ns: "config" })}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="zh" className="mt-3">
|
||||||
|
<Textarea
|
||||||
|
id="play-rules-html-zh"
|
||||||
|
value={draft.playRulesHtmlZh}
|
||||||
|
onChange={(e) => updateDraft("playRulesHtmlZh", e.target.value)}
|
||||||
|
disabled={loading || saving}
|
||||||
|
className="min-h-[200px] font-mono text-xs"
|
||||||
|
placeholder="<div>...</div>"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="en" className="mt-3">
|
||||||
|
<Textarea
|
||||||
|
id="play-rules-html-en"
|
||||||
|
value={draft.playRulesHtmlEn}
|
||||||
|
onChange={(e) => updateDraft("playRulesHtmlEn", e.target.value)}
|
||||||
|
disabled={loading || saving}
|
||||||
|
className="min-h-[200px] font-mono text-xs"
|
||||||
|
placeholder="<div>...</div>"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="ne" className="mt-3">
|
||||||
|
<Textarea
|
||||||
|
id="play-rules-html-ne"
|
||||||
|
value={draft.playRulesHtmlNe}
|
||||||
|
onChange={(e) => updateDraft("playRulesHtmlNe", e.target.value)}
|
||||||
|
disabled={loading || saving}
|
||||||
|
className="min-h-[200px] font-mono text-xs"
|
||||||
|
placeholder="<div>...</div>"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<SaveActions
|
||||||
|
dirty={dirty}
|
||||||
|
loading={loading}
|
||||||
|
saving={saving}
|
||||||
|
onSave={() => void handleSave()}
|
||||||
|
onDiscard={() => {
|
||||||
|
setDraft(saved);
|
||||||
|
setDirty(false);
|
||||||
|
}}
|
||||||
|
saveLabel={saveLabel}
|
||||||
|
savingLabel={savingLabel}
|
||||||
|
discardLabel={discardLabel}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,9 +168,11 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
<CardTitle className="admin-list-title">{t("playerTicketQuery")}</CardTitle>
|
<CardTitle className="admin-list-title">{t("playerTicketQuery")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="admin-list-content">
|
<CardContent className="admin-list-content">
|
||||||
<div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-4">
|
<div className="admin-list-toolbar">
|
||||||
<div className="grid gap-1.5">
|
<div className="admin-list-field min-w-[12rem] flex-1 sm:max-w-md">
|
||||||
<Label htmlFor="pt-player">{t("playerId")}</Label>
|
<Label htmlFor="pt-player" className="sm:shrink-0">
|
||||||
|
{t("playerId")}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="pt-player"
|
id="pt-player"
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
@@ -181,18 +183,22 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="admin-list-field">
|
||||||
<Label htmlFor="pt-draw">{t("drawNoOptional")}</Label>
|
<Label htmlFor="pt-draw" className="sm:shrink-0">
|
||||||
|
{t("drawNoOptional")}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="pt-draw"
|
id="pt-draw"
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm sm:w-44"
|
||||||
placeholder={t("drawNoPlaceholder")}
|
placeholder={t("drawNoPlaceholder")}
|
||||||
value={draft.drawNo}
|
value={draft.drawNo}
|
||||||
onChange={(e) => setDraft((current) => ({ ...current, drawNo: e.target.value }))}
|
onChange={(e) => setDraft((current) => ({ ...current, drawNo: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="admin-list-field min-w-[12rem] flex-1 sm:max-w-md">
|
||||||
<Label htmlFor="pt-number">{t("numberKeyword")}</Label>
|
<Label htmlFor="pt-number" className="sm:shrink-0">
|
||||||
|
{t("numberKeyword")}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="pt-number"
|
id="pt-number"
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
@@ -203,10 +209,13 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="admin-list-field">
|
||||||
|
<Label htmlFor="pt-date-range" className="sm:shrink-0">
|
||||||
|
{t("placedDateRange")}
|
||||||
|
</Label>
|
||||||
|
<div className="min-w-0 w-full sm:w-56">
|
||||||
<AdminDateRangeField
|
<AdminDateRangeField
|
||||||
id="pt-date-range"
|
id="pt-date-range"
|
||||||
label={t("placedDateRange")}
|
|
||||||
from={draft.startDate}
|
from={draft.startDate}
|
||||||
to={draft.endDate}
|
to={draft.endDate}
|
||||||
onRangeChange={(range) =>
|
onRangeChange={(range) =>
|
||||||
@@ -219,18 +228,20 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-list-field">
|
||||||
<div className="grid gap-2">
|
<Label htmlFor="pt-status" className="sm:shrink-0">
|
||||||
<div className="flex items-center justify-between gap-3">
|
{t("statusFilterLabel")}
|
||||||
<span className="text-sm font-medium leading-none">{t("statusFilterLabel")}</span>
|
</Label>
|
||||||
<span className="text-muted-foreground text-xs">{t("statusHint")}</span>
|
|
||||||
</div>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger className="inline-flex h-11 w-full items-center justify-between rounded-md border border-border bg-card px-4 text-left text-sm font-normal text-primary shadow-sm outline-none transition-all hover:bg-accent hover:text-primary focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50">
|
<DropdownMenuTrigger
|
||||||
|
id="pt-status"
|
||||||
|
title={t("statusHint")}
|
||||||
|
className="inline-flex h-8 w-full min-w-0 items-center justify-between rounded-md border border-border bg-card px-3 text-left text-sm font-normal shadow-sm outline-none transition-all hover:bg-accent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 sm:w-44"
|
||||||
|
>
|
||||||
<span className="truncate">{ticketStatusSummary(draft.statuses, t)}</span>
|
<span className="truncate">{ticketStatusSummary(draft.statuses, t)}</span>
|
||||||
<ChevronDown className="size-4 shrink-0 opacity-60" />
|
<ChevronDown className="size-4 shrink-0 opacity-60" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-[min(28rem,calc(100vw-2rem))]">
|
<DropdownMenuContent align="start" className="w-56">
|
||||||
{TICKET_STATUS_OPTIONS.map((status) => (
|
{TICKET_STATUS_OPTIONS.map((status) => (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
key={status}
|
key={status}
|
||||||
@@ -243,8 +254,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-list-actions">
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<AdminTableExportButton
|
<AdminTableExportButton
|
||||||
tableId="tickets-table"
|
tableId="tickets-table"
|
||||||
filename={exportLabels.filename}
|
filename={exportLabels.filename}
|
||||||
@@ -260,6 +270,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
{t("refreshCurrentPage")}
|
{t("refreshCurrentPage")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{applied.playerQuery || applied.drawNo || applied.numberKeyword || applied.startDate || applied.endDate || applied.statuses.length > 0 ? (
|
{applied.playerQuery || applied.drawNo || applied.numberKeyword || applied.startDate || applied.endDate || applied.statuses.length > 0 ? (
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user