refactor: 更新风险监控页面标题为国际化支持,优化风险池控制台的标题处理
This commit is contained in:
@@ -9,7 +9,7 @@ export default async function AdminDrawRiskHotPage(props: {
|
|||||||
return (
|
return (
|
||||||
<RiskPoolsConsole
|
<RiskPoolsConsole
|
||||||
drawId={id}
|
drawId={id}
|
||||||
title="热门号码监控"
|
titleKey="hotPageTitle"
|
||||||
soldOutOnly={false}
|
soldOutOnly={false}
|
||||||
defaultSort="usage_desc"
|
defaultSort="usage_desc"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function AdminDrawRiskPoolsPage(props: {
|
|||||||
return (
|
return (
|
||||||
<RiskPoolsConsole
|
<RiskPoolsConsole
|
||||||
drawId={id}
|
drawId={id}
|
||||||
title="全部风险池"
|
titleKey="allPoolsPageTitle"
|
||||||
soldOutOnly={false}
|
soldOutOnly={false}
|
||||||
defaultSort="number_asc"
|
defaultSort="number_asc"
|
||||||
allowSortChange
|
allowSortChange
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function AdminDrawRiskSoldOutPage(props: {
|
|||||||
return (
|
return (
|
||||||
<RiskPoolsConsole
|
<RiskPoolsConsole
|
||||||
drawId={id}
|
drawId={id}
|
||||||
title="售罄号码列表"
|
titleKey="soldOutPageTitle"
|
||||||
soldOutOnly
|
soldOutOnly
|
||||||
defaultSort="number_asc"
|
defaultSort="number_asc"
|
||||||
/>
|
/>
|
||||||
|
|||||||
40
src/hooks/use-admin-play-type-catalog.ts
Normal file
40
src/hooks/use-admin-play-type-catalog.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { getAdminPlayTypes } from "@/api/admin-config";
|
||||||
|
import {
|
||||||
|
formatAdminPlayCodeLabel,
|
||||||
|
getAdminPlayTypesLoadPromise,
|
||||||
|
resolveAdminPlayTypeDisplayName,
|
||||||
|
} from "@/lib/admin-play-types";
|
||||||
|
|
||||||
|
export function useAdminPlayTypeCatalog(): void {
|
||||||
|
useEffect(() => {
|
||||||
|
void getAdminPlayTypesLoadPromise(getAdminPlayTypes);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回按当前界面语言解析的玩法标签(显示名 + 编码) */
|
||||||
|
export function useAdminPlayCodeLabel(): (playCode: string | null | undefined) => string {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
useAdminPlayTypeCatalog();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(playCode: string | null | undefined) => formatAdminPlayCodeLabel(playCode, i18n.language),
|
||||||
|
[i18n.language],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅显示名,不含编码(用于下拉选项等) */
|
||||||
|
export function useAdminPlayCodeDisplayName(): (playCode: string | null | undefined) => string {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
useAdminPlayTypeCatalog();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(playCode: string | null | undefined) =>
|
||||||
|
resolveAdminPlayTypeDisplayName(playCode, i18n.language),
|
||||||
|
[i18n.language],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -162,8 +162,7 @@
|
|||||||
"prd.payout.manage": "Payout Confirmation · Manage",
|
"prd.payout.manage": "Payout Confirmation · Manage",
|
||||||
"prd.payout.review": "Payout Confirmation · Review",
|
"prd.payout.review": "Payout Confirmation · Review",
|
||||||
"prd.payout.view": "Payout Confirmation · View",
|
"prd.payout.view": "Payout Confirmation · View",
|
||||||
"prd.audit.all": "Audit Logs · All",
|
"prd.audit.view": "Audit Logs · View",
|
||||||
"prd.audit.self": "Audit Logs · Related to Self",
|
"prd.report.view": "Reports · View"
|
||||||
"prd.audit.finance": "Audit Logs · Finance Related"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@
|
|||||||
"plannedDraw": "Planned draw",
|
"plannedDraw": "Planned draw",
|
||||||
"coolingEndTime": "Cooling ends at",
|
"coolingEndTime": "Cooling ends at",
|
||||||
"resultSource": "Result source",
|
"resultSource": "Result source",
|
||||||
|
"resultSourceOptions": {
|
||||||
|
"rng": "RNG auto-generated",
|
||||||
|
"manual": "Manual entry"
|
||||||
|
},
|
||||||
|
"batchStatusOptions": {
|
||||||
|
"pending_review": "Pending review",
|
||||||
|
"published": "Published"
|
||||||
|
},
|
||||||
"currentResultVersion": "Current result version",
|
"currentResultVersion": "Current result version",
|
||||||
"settleVersion": "Settlement version",
|
"settleVersion": "Settlement version",
|
||||||
"isReopened": "Reopened",
|
"isReopened": "Reopened",
|
||||||
@@ -61,6 +69,7 @@
|
|||||||
"ticketCount": "Tickets",
|
"ticketCount": "Tickets",
|
||||||
"winCount": "Wins",
|
"winCount": "Wins",
|
||||||
"finishedAt": "Finished at",
|
"finishedAt": "Finished at",
|
||||||
|
"jackpotPayout": "Jackpot payout",
|
||||||
"resultsTitle": "Results",
|
"resultsTitle": "Results",
|
||||||
"reviewAndPublish": "Review / publish",
|
"reviewAndPublish": "Review / publish",
|
||||||
"viewReviewQueue": "View review queue",
|
"viewReviewQueue": "View review queue",
|
||||||
@@ -77,6 +86,9 @@
|
|||||||
"headTail": "Head/Tail",
|
"headTail": "Head/Tail",
|
||||||
"manualResultEntry": "Manual result entry",
|
"manualResultEntry": "Manual result entry",
|
||||||
"currentStatusAndDraft": "Current status {{status}}. Saving creates a pending batch and does not publish it.",
|
"currentStatusAndDraft": "Current status {{status}}. Saving creates a pending batch and does not publish it.",
|
||||||
|
"currentStatusLabel": "Current status",
|
||||||
|
"currentStatusDraftHint": "Saving creates a pending batch and does not publish it.",
|
||||||
|
"hallPreviewStatusLabel": "Hall preview",
|
||||||
"enter23Numbers": "Please enter all 23 groups of 4 digits",
|
"enter23Numbers": "Please enter all 23 groups of 4 digits",
|
||||||
"draftSaved": "Draft v{{version}} saved, waiting to be published",
|
"draftSaved": "Draft v{{version}} saved, waiting to be published",
|
||||||
"saveFailed": "Failed to save",
|
"saveFailed": "Failed to save",
|
||||||
@@ -84,7 +96,7 @@
|
|||||||
"saveDraft": "Save draft",
|
"saveDraft": "Save draft",
|
||||||
"saving": "Saving…",
|
"saving": "Saving…",
|
||||||
"pendingBatches": "Pending batches",
|
"pendingBatches": "Pending batches",
|
||||||
"noPendingBatches": "There are no pending_review batches.",
|
"noPendingBatches": "There are no batches pending review.",
|
||||||
"batchId": "Batch ID",
|
"batchId": "Batch ID",
|
||||||
"numberCount": "Number count",
|
"numberCount": "Number count",
|
||||||
"reviewAndPublishAction": "Review and publish",
|
"reviewAndPublishAction": "Review and publish",
|
||||||
@@ -107,7 +119,11 @@
|
|||||||
"status": "Draw status",
|
"status": "Draw status",
|
||||||
"results": "Results",
|
"results": "Results",
|
||||||
"finance": "Draw finance",
|
"finance": "Draw finance",
|
||||||
"review": "Review & publish"
|
"review": "Review & publish",
|
||||||
|
"riskOccupancy": "Risk occupancy",
|
||||||
|
"riskHot": "Hot numbers",
|
||||||
|
"riskSoldOut": "Sold-out numbers",
|
||||||
|
"riskPools": "Risk pools"
|
||||||
},
|
},
|
||||||
"statusOptions": {
|
"statusOptions": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
|
|||||||
@@ -12,6 +12,14 @@
|
|||||||
"loadDrawListFailed": "Failed to load draw list",
|
"loadDrawListFailed": "Failed to load draw list",
|
||||||
"enterRisk": "Enter risk",
|
"enterRisk": "Enter risk",
|
||||||
"poolsTitle": "Risk pools",
|
"poolsTitle": "Risk pools",
|
||||||
|
"hotPageTitle": "Hot numbers monitor",
|
||||||
|
"soldOutPageTitle": "Sold-out numbers",
|
||||||
|
"allPoolsPageTitle": "All risk pools",
|
||||||
|
"sourceReasonOptions": {
|
||||||
|
"ticket_place": "Bet placement",
|
||||||
|
"admin_manual_close": "Manual close",
|
||||||
|
"admin_manual_recover": "Manual recover"
|
||||||
|
},
|
||||||
"searchNumber": "Search number",
|
"searchNumber": "Search number",
|
||||||
"searchNumberPlaceholder": "For example 8888",
|
"searchNumberPlaceholder": "For example 8888",
|
||||||
"riskFilter": "Risk filter",
|
"riskFilter": "Risk filter",
|
||||||
|
|||||||
@@ -162,8 +162,7 @@
|
|||||||
"prd.payout.manage": "भुक्तानी पुष्टि · व्यवस्थापन",
|
"prd.payout.manage": "भुक्तानी पुष्टि · व्यवस्थापन",
|
||||||
"prd.payout.review": "भुक्तानी पुष्टि · समीक्षा",
|
"prd.payout.review": "भुक्तानी पुष्टि · समीक्षा",
|
||||||
"prd.payout.view": "भुक्तानी पुष्टि · हेर्नुहोस्",
|
"prd.payout.view": "भुक्तानी पुष्टि · हेर्नुहोस्",
|
||||||
"prd.audit.all": "अडिट लग · सबै",
|
"prd.audit.view": "अडिट लग · हेर्नुहोस्",
|
||||||
"prd.audit.self": "अडिट लग · आफूसँग सम्बन्धित",
|
"prd.report.view": "रिपोर्ट · हेर्नुहोस्"
|
||||||
"prd.audit.finance": "अडिट लग · वित्त सम्बन्धित"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@
|
|||||||
"plannedDraw": "योजनाबद्ध ड्रअ",
|
"plannedDraw": "योजनाबद्ध ड्रअ",
|
||||||
"coolingEndTime": "कुलिङ समाप्ति",
|
"coolingEndTime": "कुलिङ समाप्ति",
|
||||||
"resultSource": "नतिजा स्रोत",
|
"resultSource": "नतिजा स्रोत",
|
||||||
|
"resultSourceOptions": {
|
||||||
|
"rng": "RNG स्वचालित",
|
||||||
|
"manual": "म्यानुअल प्रविष्टि"
|
||||||
|
},
|
||||||
|
"batchStatusOptions": {
|
||||||
|
"pending_review": "समीक्षा बाँकी",
|
||||||
|
"published": "प्रकाशित"
|
||||||
|
},
|
||||||
"currentResultVersion": "हालको नतिजा संस्करण",
|
"currentResultVersion": "हालको नतिजा संस्करण",
|
||||||
"settleVersion": "सेटलमेन्ट संस्करण",
|
"settleVersion": "सेटलमेन्ट संस्करण",
|
||||||
"isReopened": "फेरि खोलिएको",
|
"isReopened": "फेरि खोलिएको",
|
||||||
@@ -61,6 +69,7 @@
|
|||||||
"ticketCount": "टिकट",
|
"ticketCount": "टिकट",
|
||||||
"winCount": "जित",
|
"winCount": "जित",
|
||||||
"finishedAt": "समाप्त समय",
|
"finishedAt": "समाप्त समय",
|
||||||
|
"jackpotPayout": "ज्याकपट भुक्तानी",
|
||||||
"resultsTitle": "परिणाम",
|
"resultsTitle": "परिणाम",
|
||||||
"reviewAndPublish": "समीक्षा / प्रकाशित",
|
"reviewAndPublish": "समीक्षा / प्रकाशित",
|
||||||
"viewReviewQueue": "समीक्षा सूची हेर्नुहोस्",
|
"viewReviewQueue": "समीक्षा सूची हेर्नुहोस्",
|
||||||
@@ -77,6 +86,9 @@
|
|||||||
"headTail": "हेड/टेल",
|
"headTail": "हेड/टेल",
|
||||||
"manualResultEntry": "म्यानुअल परिणाम प्रविष्टि",
|
"manualResultEntry": "म्यानुअल परिणाम प्रविष्टि",
|
||||||
"currentStatusAndDraft": "हालको स्थिति {{status}} · सेभ गरेपछि pending batch बन्छ, सिधै प्रकाशित हुँदैन",
|
"currentStatusAndDraft": "हालको स्थिति {{status}} · सेभ गरेपछि pending batch बन्छ, सिधै प्रकाशित हुँदैन",
|
||||||
|
"currentStatusLabel": "हालको स्थिति",
|
||||||
|
"currentStatusDraftHint": "सेभ गरेपछि समीक्षा बाँकी ब्याच बन्छ, सिधै प्रकाशित हुँदैन",
|
||||||
|
"hallPreviewStatusLabel": "हल पूर्वावलोकन",
|
||||||
"enter23Numbers": "कृपया 23 वटा 4-अङ्क समूह पूरा भर्नुहोस्",
|
"enter23Numbers": "कृपया 23 वटा 4-अङ्क समूह पूरा भर्नुहोस्",
|
||||||
"draftSaved": "ड्राफ्ट v{{version}} सुरक्षित भयो, प्रकाशनको प्रतिक्षामा",
|
"draftSaved": "ड्राफ्ट v{{version}} सुरक्षित भयो, प्रकाशनको प्रतिक्षामा",
|
||||||
"saveFailed": "सेभ असफल भयो",
|
"saveFailed": "सेभ असफल भयो",
|
||||||
@@ -84,7 +96,7 @@
|
|||||||
"saveDraft": "ड्राफ्ट सुरक्षित गर्नुहोस्",
|
"saveDraft": "ड्राफ्ट सुरक्षित गर्नुहोस्",
|
||||||
"saving": "सेभ हुँदैछ…",
|
"saving": "सेभ हुँदैछ…",
|
||||||
"pendingBatches": "बाँकी ब्याच",
|
"pendingBatches": "बाँकी ब्याच",
|
||||||
"noPendingBatches": "pending_review ब्याच छैन।",
|
"noPendingBatches": "समीक्षा बाँकी ब्याच छैन।",
|
||||||
"batchId": "ब्याच ID",
|
"batchId": "ब्याच ID",
|
||||||
"numberCount": "नम्बर संख्या",
|
"numberCount": "नम्बर संख्या",
|
||||||
"reviewAndPublishAction": "जाँचेर प्रकाशित गर्नुहोस्",
|
"reviewAndPublishAction": "जाँचेर प्रकाशित गर्नुहोस्",
|
||||||
@@ -107,7 +119,11 @@
|
|||||||
"status": "ड्रअ स्थिति",
|
"status": "ड्रअ स्थिति",
|
||||||
"results": "परिणाम",
|
"results": "परिणाम",
|
||||||
"finance": "ड्रअ वित्त",
|
"finance": "ड्रअ वित्त",
|
||||||
"review": "समीक्षा र प्रकाशन"
|
"review": "समीक्षा र प्रकाशन",
|
||||||
|
"riskOccupancy": "जोखिम अकुपेन्सी",
|
||||||
|
"riskHot": "हट नम्बर",
|
||||||
|
"riskSoldOut": "बिक्री समाप्त नम्बर",
|
||||||
|
"riskPools": "जोखिम पूल"
|
||||||
},
|
},
|
||||||
"statusOptions": {
|
"statusOptions": {
|
||||||
"all": "सबै",
|
"all": "सबै",
|
||||||
|
|||||||
@@ -12,6 +12,14 @@
|
|||||||
"loadDrawListFailed": "ड्रअ सूची लोड असफल भयो",
|
"loadDrawListFailed": "ड्रअ सूची लोड असफल भयो",
|
||||||
"enterRisk": "जोखिममा जानुहोस्",
|
"enterRisk": "जोखिममा जानुहोस्",
|
||||||
"poolsTitle": "जोखिम पूल",
|
"poolsTitle": "जोखिम पूल",
|
||||||
|
"hotPageTitle": "हट नम्बर निगरानी",
|
||||||
|
"soldOutPageTitle": "बिक्री समाप्त नम्बर सूची",
|
||||||
|
"allPoolsPageTitle": "सबै जोखिम पूल",
|
||||||
|
"sourceReasonOptions": {
|
||||||
|
"ticket_place": "बेट अकुपेन्सी",
|
||||||
|
"admin_manual_close": "म्यानुअल बन्द",
|
||||||
|
"admin_manual_recover": "म्यानुअल पुनर्स्थापना"
|
||||||
|
},
|
||||||
"searchNumber": "नम्बर खोज्नुहोस्",
|
"searchNumber": "नम्बर खोज्नुहोस्",
|
||||||
"searchNumberPlaceholder": "जस्तै 8888",
|
"searchNumberPlaceholder": "जस्तै 8888",
|
||||||
"riskFilter": "जोखिम फिल्टर",
|
"riskFilter": "जोखिम फिल्टर",
|
||||||
|
|||||||
@@ -162,8 +162,7 @@
|
|||||||
"prd.payout.manage": "派彩确认·可管理",
|
"prd.payout.manage": "派彩确认·可管理",
|
||||||
"prd.payout.review": "派彩确认·可审核",
|
"prd.payout.review": "派彩确认·可审核",
|
||||||
"prd.payout.view": "派彩确认·查看",
|
"prd.payout.view": "派彩确认·查看",
|
||||||
"prd.audit.all": "审计日志·全部",
|
"prd.audit.view": "审计日志·查看",
|
||||||
"prd.audit.self": "审计日志·自身相关",
|
"prd.report.view": "报表中心·查看"
|
||||||
"prd.audit.finance": "审计日志·资金相关"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@
|
|||||||
"plannedDraw": "计划开奖",
|
"plannedDraw": "计划开奖",
|
||||||
"coolingEndTime": "冷静期结束",
|
"coolingEndTime": "冷静期结束",
|
||||||
"resultSource": "结果来源",
|
"resultSource": "结果来源",
|
||||||
|
"resultSourceOptions": {
|
||||||
|
"rng": "RNG 自动生成",
|
||||||
|
"manual": "人工录入"
|
||||||
|
},
|
||||||
|
"batchStatusOptions": {
|
||||||
|
"pending_review": "待审核",
|
||||||
|
"published": "已发布"
|
||||||
|
},
|
||||||
"currentResultVersion": "当前结果版本",
|
"currentResultVersion": "当前结果版本",
|
||||||
"settleVersion": "结算版本",
|
"settleVersion": "结算版本",
|
||||||
"isReopened": "是否重开",
|
"isReopened": "是否重开",
|
||||||
@@ -61,6 +69,7 @@
|
|||||||
"ticketCount": "票数",
|
"ticketCount": "票数",
|
||||||
"winCount": "中奖数",
|
"winCount": "中奖数",
|
||||||
"finishedAt": "完成时间",
|
"finishedAt": "完成时间",
|
||||||
|
"jackpotPayout": "奖池派彩",
|
||||||
"resultsTitle": "开奖结果",
|
"resultsTitle": "开奖结果",
|
||||||
"reviewAndPublish": "去审核 / 发布",
|
"reviewAndPublish": "去审核 / 发布",
|
||||||
"viewReviewQueue": "查看审核队列",
|
"viewReviewQueue": "查看审核队列",
|
||||||
@@ -77,6 +86,9 @@
|
|||||||
"headTail": "头/尾",
|
"headTail": "头/尾",
|
||||||
"manualResultEntry": "人工录入开奖结果",
|
"manualResultEntry": "人工录入开奖结果",
|
||||||
"currentStatusAndDraft": "当前状态 {{status}} · 保存后生成待确认批次,不会直接发布",
|
"currentStatusAndDraft": "当前状态 {{status}} · 保存后生成待确认批次,不会直接发布",
|
||||||
|
"currentStatusLabel": "当前状态",
|
||||||
|
"currentStatusDraftHint": "保存后生成待确认批次,不会直接发布",
|
||||||
|
"hallPreviewStatusLabel": "大厅预览",
|
||||||
"enter23Numbers": "请完整输入 23 组 4 位数字",
|
"enter23Numbers": "请完整输入 23 组 4 位数字",
|
||||||
"draftSaved": "已保存草稿 v{{version}},等待确认发布",
|
"draftSaved": "已保存草稿 v{{version}},等待确认发布",
|
||||||
"saveFailed": "保存失败",
|
"saveFailed": "保存失败",
|
||||||
@@ -84,7 +96,7 @@
|
|||||||
"saveDraft": "保存草稿",
|
"saveDraft": "保存草稿",
|
||||||
"saving": "保存中…",
|
"saving": "保存中…",
|
||||||
"pendingBatches": "待确认批次",
|
"pendingBatches": "待确认批次",
|
||||||
"noPendingBatches": "当前没有待审核(pending_review)批次。",
|
"noPendingBatches": "当前没有待审核批次。",
|
||||||
"batchId": "批次 ID",
|
"batchId": "批次 ID",
|
||||||
"numberCount": "号码条数",
|
"numberCount": "号码条数",
|
||||||
"reviewAndPublishAction": "核对并发布",
|
"reviewAndPublishAction": "核对并发布",
|
||||||
|
|||||||
@@ -12,6 +12,14 @@
|
|||||||
"loadDrawListFailed": "加载期号列表失败",
|
"loadDrawListFailed": "加载期号列表失败",
|
||||||
"enterRisk": "进入风控",
|
"enterRisk": "进入风控",
|
||||||
"poolsTitle": "风险池",
|
"poolsTitle": "风险池",
|
||||||
|
"hotPageTitle": "热门号码监控",
|
||||||
|
"soldOutPageTitle": "售罄号码列表",
|
||||||
|
"allPoolsPageTitle": "全部风险池",
|
||||||
|
"sourceReasonOptions": {
|
||||||
|
"ticket_place": "下注占用",
|
||||||
|
"admin_manual_close": "人工关闭",
|
||||||
|
"admin_manual_recover": "人工恢复"
|
||||||
|
},
|
||||||
"searchNumber": "搜索号码",
|
"searchNumber": "搜索号码",
|
||||||
"searchNumberPlaceholder": "如 8888",
|
"searchNumberPlaceholder": "如 8888",
|
||||||
"riskFilter": "风险筛选",
|
"riskFilter": "风险筛选",
|
||||||
@@ -73,8 +81,8 @@
|
|||||||
"optional": "可选",
|
"optional": "可选",
|
||||||
"actionFilter": "动作",
|
"actionFilter": "动作",
|
||||||
"noLimit": "不限",
|
"noLimit": "不限",
|
||||||
"lock": "锁定 lock",
|
"lock": "锁定",
|
||||||
"release": "释放 release",
|
"release": "释放",
|
||||||
"applyFilter": "应用筛选",
|
"applyFilter": "应用筛选",
|
||||||
"statusOptions": {
|
"statusOptions": {
|
||||||
"pending": "未开始",
|
"pending": "未开始",
|
||||||
|
|||||||
102
src/lib/admin-play-types.ts
Normal file
102
src/lib/admin-play-types.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import type { AdminPlayTypeRow } from "@/types/api/admin-config";
|
||||||
|
|
||||||
|
let cachedByCode = new Map<string, AdminPlayTypeRow>();
|
||||||
|
let inflightLoad: Promise<void> | null = null;
|
||||||
|
|
||||||
|
export function getCachedAdminPlayType(playCode: string): AdminPlayTypeRow | undefined {
|
||||||
|
return cachedByCode.get(playCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCachedAdminPlayTypes(): AdminPlayTypeRow[] {
|
||||||
|
return [...cachedByCode.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCachedAdminPlayTypes(items: AdminPlayTypeRow[]): void {
|
||||||
|
cachedByCode = new Map(items.map((row) => [row.play_code, row]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCachedAdminPlayTypes(): void {
|
||||||
|
cachedByCode = new Map();
|
||||||
|
inflightLoad = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAdminPlayTypesLoadPromise(
|
||||||
|
loader: () => Promise<{ items: AdminPlayTypeRow[] }>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (cachedByCode.size > 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (inflightLoad !== null) {
|
||||||
|
return inflightLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
inflightLoad = loader()
|
||||||
|
.then((data) => {
|
||||||
|
setCachedAdminPlayTypes(data.items);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 玩法目录加载失败时回退展示 play_code,不阻断页面。
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
inflightLoad = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return inflightLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDisplayName(row: AdminPlayTypeRow, language: string): string | null {
|
||||||
|
const lang = language.split("-")[0]?.toLowerCase() ?? "zh";
|
||||||
|
|
||||||
|
if (lang === "en" && row.display_name_en?.trim()) {
|
||||||
|
return row.display_name_en.trim();
|
||||||
|
}
|
||||||
|
if (lang === "ne" && row.display_name_ne?.trim()) {
|
||||||
|
return row.display_name_ne.trim();
|
||||||
|
}
|
||||||
|
if (row.display_name_zh?.trim()) {
|
||||||
|
return row.display_name_zh.trim();
|
||||||
|
}
|
||||||
|
if (row.display_name_en?.trim()) {
|
||||||
|
return row.display_name_en.trim();
|
||||||
|
}
|
||||||
|
if (row.display_name_ne?.trim()) {
|
||||||
|
return row.display_name_ne.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按当前语言解析玩法显示名;无配置时回退 play_code */
|
||||||
|
export function resolveAdminPlayTypeDisplayName(
|
||||||
|
playCode: string | null | undefined,
|
||||||
|
language: string,
|
||||||
|
row?: AdminPlayTypeRow,
|
||||||
|
): string {
|
||||||
|
if (playCode == null || playCode === "") {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = row ?? cachedByCode.get(playCode);
|
||||||
|
if (!resolved) {
|
||||||
|
return playCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pickDisplayName(resolved, language) ?? playCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格展示:显示名 + 编码(与报表筛选一致) */
|
||||||
|
export function formatAdminPlayCodeLabel(
|
||||||
|
playCode: string | null | undefined,
|
||||||
|
language: string,
|
||||||
|
): string {
|
||||||
|
if (playCode == null || playCode === "") {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = resolveAdminPlayTypeDisplayName(playCode, language);
|
||||||
|
if (name === playCode) {
|
||||||
|
return playCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${name} (${playCode})`;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { useAdminProfile } from "@/stores/admin-session";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { drawResultSourceLabel, drawStatusLabel } from "./draw-display";
|
||||||
import { DrawStatusBadge } from "./draw-status-badge";
|
import { DrawStatusBadge } from "./draw-status-badge";
|
||||||
import {
|
import {
|
||||||
PRD_DRAW_REOPEN_MANAGE,
|
PRD_DRAW_REOPEN_MANAGE,
|
||||||
@@ -33,12 +34,6 @@ import {
|
|||||||
PRD_PAYOUT_REVIEW,
|
PRD_PAYOUT_REVIEW,
|
||||||
} from "./draw-prd";
|
} from "./draw-prd";
|
||||||
|
|
||||||
function drawStatusText(status: string, t: (key: string) => string): string {
|
|
||||||
const key = `statusOptions.${status}`;
|
|
||||||
const translated = t(key);
|
|
||||||
return translated === key ? status : translated;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-1 sm:grid-cols-[10rem_1fr] sm:items-start">
|
<div className="grid gap-1 sm:grid-cols-[10rem_1fr] sm:items-start">
|
||||||
@@ -123,13 +118,13 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
<div className="flex flex-col items-end gap-1 text-right">
|
<div className="flex flex-col items-end gap-1 text-right">
|
||||||
<DrawStatusBadge
|
<DrawStatusBadge
|
||||||
status={data.status}
|
status={data.status}
|
||||||
label={drawStatusText(data.status, t)}
|
label={drawStatusLabel(data.status, t)}
|
||||||
/>
|
/>
|
||||||
<p className="flex flex-wrap items-center justify-end gap-2 text-sm text-muted-foreground">
|
<p className="flex flex-wrap items-center justify-end gap-2 text-sm text-muted-foreground">
|
||||||
<span>{t("hallPreviewStatus", { status: "" }).replace(/\{\{status\}\}/, "").replace(/\s+$/, "")}</span>
|
<span>{t("hallPreviewStatusLabel")}</span>
|
||||||
<DrawStatusBadge
|
<DrawStatusBadge
|
||||||
status={data.hall_preview_status}
|
status={data.hall_preview_status}
|
||||||
label={drawStatusText(data.hall_preview_status, t)}
|
label={drawStatusLabel(data.hall_preview_status, t)}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +142,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Field label={t("resultSource")}>{data.result_source ?? "—"}</Field>
|
<Field label={t("resultSource")}>{drawResultSourceLabel(data.result_source, t)}</Field>
|
||||||
<Field label={t("currentResultVersion")}>{data.current_result_version}</Field>
|
<Field label={t("currentResultVersion")}>{data.current_result_version}</Field>
|
||||||
<Field label={t("settleVersion")}>{data.settle_version}</Field>
|
<Field label={t("settleVersion")}>{data.settle_version}</Field>
|
||||||
<Field label={t("isReopened")}>{data.is_reopened ? t("yes") : t("no")}</Field>
|
<Field label={t("isReopened")}>{data.is_reopened ? t("yes") : t("no")}</Field>
|
||||||
|
|||||||
62
src/modules/draws/draw-display.ts
Normal file
62
src/modules/draws/draw-display.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
type DrawTranslate = (
|
||||||
|
key: string,
|
||||||
|
options?: { ns?: string; index?: number },
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
/** 期号状态文案(draws.statusOptions) */
|
||||||
|
export function drawStatusLabel(status: string, t: DrawTranslate): string {
|
||||||
|
const key = `statusOptions.${status}`;
|
||||||
|
const translated = t(key, { ns: "draws" });
|
||||||
|
return translated === key ? status : translated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开奖结果来源(draws.resultSourceOptions) */
|
||||||
|
export function drawResultSourceLabel(
|
||||||
|
source: string | null | undefined,
|
||||||
|
t: DrawTranslate,
|
||||||
|
): string {
|
||||||
|
if (source == null || source === "") {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
const key = `resultSourceOptions.${source}`;
|
||||||
|
const translated = t(key, { ns: "draws" });
|
||||||
|
return translated === key ? source : translated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开奖结果批次状态(draws.batchStatusOptions) */
|
||||||
|
export function drawBatchStatusLabel(status: string, t: DrawTranslate): string {
|
||||||
|
const key = `batchStatusOptions.${status}`;
|
||||||
|
const translated = t(key, { ns: "draws" });
|
||||||
|
return translated === key ? status : translated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 结算批次状态(settlement.statusOptions) */
|
||||||
|
export function settlementBatchStatusLabel(status: string, t: DrawTranslate): string {
|
||||||
|
const key = `statusOptions.${status}`;
|
||||||
|
const translated = t(key, { ns: "settlement" });
|
||||||
|
return translated === key ? status : translated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 奖项类型 + 序号 → 展示名(draws.resultSlots) */
|
||||||
|
export function drawPrizeTypeLabel(
|
||||||
|
prizeType: string,
|
||||||
|
prizeIndex: number,
|
||||||
|
t: DrawTranslate,
|
||||||
|
): string {
|
||||||
|
if (prizeType === "first") {
|
||||||
|
return t("resultSlots.first", { ns: "draws" });
|
||||||
|
}
|
||||||
|
if (prizeType === "second") {
|
||||||
|
return t("resultSlots.second", { ns: "draws" });
|
||||||
|
}
|
||||||
|
if (prizeType === "third") {
|
||||||
|
return t("resultSlots.third", { ns: "draws" });
|
||||||
|
}
|
||||||
|
if (prizeType === "starter") {
|
||||||
|
return t("resultSlots.starter", { ns: "draws", index: prizeIndex + 1 });
|
||||||
|
}
|
||||||
|
if (prizeType === "consolation") {
|
||||||
|
return t("resultSlots.consolation", { ns: "draws", index: prizeIndex + 1 });
|
||||||
|
}
|
||||||
|
return prizeType;
|
||||||
|
}
|
||||||
@@ -26,18 +26,16 @@ import { LotteryApiBizError } from "@/types/api/errors";
|
|||||||
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||||
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
|
|
||||||
|
import { drawStatusLabel, settlementBatchStatusLabel } from "./draw-display";
|
||||||
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "./draw-prd";
|
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "./draw-prd";
|
||||||
|
|
||||||
function drawStatusText(status: string, t: (key: string) => string): string {
|
|
||||||
const key = `statusOptions.${status}`;
|
|
||||||
const translated = t(key);
|
|
||||||
return translated === key ? status : translated;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
|
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
|
||||||
const { t } = useTranslation(["draws", "common"]);
|
const { t } = useTranslation(["draws", "settlement", "common"]);
|
||||||
|
useAdminCurrencyCatalog();
|
||||||
const idNum = Number(drawId);
|
const idNum = Number(drawId);
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const canRunSettlement = adminHasAnyPermission(profile?.permissions, [
|
const canRunSettlement = adminHasAnyPermission(profile?.permissions, [
|
||||||
@@ -96,6 +94,9 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
return <p className="text-destructive text-sm">{err ?? t("states.noData", { ns: "common" })}</p>;
|
return <p className="text-destructive text-sm">{err ?? t("states.noData", { ns: "common" })}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currencyCode = data.currency_code ?? "NPR";
|
||||||
|
const formatMoney = (minor: number) => formatAdminMinorUnits(minor, currencyCode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -110,7 +111,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">{t("status")}</span>
|
<span className="text-muted-foreground">{t("status")}</span>
|
||||||
<p className="mt-1">
|
<p className="mt-1">
|
||||||
<DrawStatusBadge status={data.draw_status} label={drawStatusText(data.draw_status, t)} />
|
<DrawStatusBadge status={data.draw_status} label={drawStatusLabel(data.draw_status, t)} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -121,11 +122,11 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">{t("actualBet")}</span>
|
<span className="text-muted-foreground">{t("actualBet")}</span>
|
||||||
<p className="tabular-nums font-medium">{data.total_bet_minor}</p>
|
<p className="tabular-nums font-medium">{formatMoney(data.total_bet_minor)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">{t("currentPayout")}</span>
|
<span className="text-muted-foreground">{t("currentPayout")}</span>
|
||||||
<p className="tabular-nums font-medium">{data.total_payout_minor}</p>
|
<p className="tabular-nums font-medium">{formatMoney(data.total_payout_minor)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">{t("grossProfit")}</span>
|
<span className="text-muted-foreground">{t("grossProfit")}</span>
|
||||||
@@ -135,7 +136,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
data.approx_house_gross_minor >= 0 ? "text-emerald-600" : "text-destructive",
|
data.approx_house_gross_minor >= 0 ? "text-emerald-600" : "text-destructive",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{data.approx_house_gross_minor}
|
{formatMoney(data.approx_house_gross_minor)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -185,7 +186,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
<TableHead className="text-right">{t("ticketCount")}</TableHead>
|
<TableHead className="text-right">{t("ticketCount")}</TableHead>
|
||||||
<TableHead className="text-right">{t("winCount")}</TableHead>
|
<TableHead className="text-right">{t("winCount")}</TableHead>
|
||||||
<TableHead className="text-right">{t("payoutTotal")}</TableHead>
|
<TableHead className="text-right">{t("payoutTotal")}</TableHead>
|
||||||
<TableHead className="text-right">{t("jackpot")}</TableHead>
|
<TableHead className="text-right">{t("jackpotPayout")}</TableHead>
|
||||||
<TableHead>{t("finishedAt")}</TableHead>
|
<TableHead>{t("finishedAt")}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -194,7 +195,9 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
<TableRow key={b.id}>
|
<TableRow key={b.id}>
|
||||||
<TableCell className="font-mono text-xs">{b.id}</TableCell>
|
<TableCell className="font-mono text-xs">{b.id}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={b.status}>{drawStatusText(b.status, t)}</AdminStatusBadge>
|
<AdminStatusBadge status={b.status}>
|
||||||
|
{settlementBatchStatusLabel(b.status, t)}
|
||||||
|
</AdminStatusBadge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right tabular-nums text-xs">
|
<TableCell className="text-right tabular-nums text-xs">
|
||||||
{b.total_ticket_count}
|
{b.total_ticket_count}
|
||||||
@@ -203,10 +206,10 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
{b.total_win_count}
|
{b.total_win_count}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right tabular-nums text-xs">
|
<TableCell className="text-right tabular-nums text-xs">
|
||||||
{b.total_payout_amount}
|
{formatMoney(b.total_payout_amount)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right tabular-nums text-xs">
|
<TableCell className="text-right tabular-nums text-xs">
|
||||||
{b.total_jackpot_payout_amount}
|
{formatMoney(b.total_jackpot_payout_amount)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-[11px] text-muted-foreground">
|
<TableCell className="font-mono text-[11px] text-muted-foreground">
|
||||||
{b.finished_at ?? "—"}
|
{b.finished_at ?? "—"}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useAdminProfile } from "@/stores/admin-session";
|
|||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin-draws";
|
import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin-draws";
|
||||||
|
|
||||||
|
import { drawBatchStatusLabel, drawPrizeTypeLabel, drawStatusLabel } from "./draw-display";
|
||||||
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||||
|
|
||||||
export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchId: string }) {
|
export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchId: string }) {
|
||||||
@@ -73,7 +74,12 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
|||||||
setPublishing(true);
|
setPublishing(true);
|
||||||
try {
|
try {
|
||||||
const res = await postAdminPublishResultBatch(idNum, batchNum);
|
const res = await postAdminPublishResultBatch(idNum, batchNum);
|
||||||
toast.success(t("publishSuccess", { drawNo: res.draw_no, status: res.status }));
|
toast.success(
|
||||||
|
t("publishSuccess", {
|
||||||
|
drawNo: res.draw_no,
|
||||||
|
status: drawStatusLabel(res.status, t),
|
||||||
|
}),
|
||||||
|
);
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof LotteryApiBizError ? e.message : t("publishFailed");
|
const msg = e instanceof LotteryApiBizError ? e.message : t("publishFailed");
|
||||||
@@ -125,7 +131,9 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
|||||||
{!canPublish && canManageDraw ? (
|
{!canPublish && canManageDraw ? (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTitle>{t("cannotPublish")}</AlertTitle>
|
<AlertTitle>{t("cannotPublish")}</AlertTitle>
|
||||||
<AlertDescription>{t("cannotPublishDesc", { status: batch.status })}</AlertDescription>
|
<AlertDescription>
|
||||||
|
{t("cannotPublishDesc", { status: drawBatchStatusLabel(batch.status, t) })}
|
||||||
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
{canPublish ? (
|
{canPublish ? (
|
||||||
@@ -147,7 +155,9 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{batch.items.map((it) => (
|
{batch.items.map((it) => (
|
||||||
<TableRow key={`${it.prize_type}-${it.prize_index}`}>
|
<TableRow key={`${it.prize_type}-${it.prize_index}`}>
|
||||||
<TableCell className="text-xs">{it.prize_type}</TableCell>
|
<TableCell className="text-xs">
|
||||||
|
{drawPrizeTypeLabel(it.prize_type, it.prize_index, t)}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{it.prize_index}</TableCell>
|
<TableCell className="font-mono text-xs">{it.prize_index}</TableCell>
|
||||||
<TableCell className="font-mono text-sm font-semibold">{it.number_4d}</TableCell>
|
<TableCell className="font-mono text-sm font-semibold">{it.number_4d}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useAdminProfile } from "@/stores/admin-session";
|
|||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin-draws";
|
import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin-draws";
|
||||||
|
|
||||||
|
import { drawPrizeTypeLabel } from "./draw-display";
|
||||||
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||||
import { DrawStatusBadge } from "./draw-status-badge";
|
import { DrawStatusBadge } from "./draw-status-badge";
|
||||||
|
|
||||||
@@ -129,7 +130,9 @@ function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{batch.items.map((it) => (
|
{batch.items.map((it) => (
|
||||||
<TableRow key={`${it.prize_type}-${it.prize_index}`}>
|
<TableRow key={`${it.prize_type}-${it.prize_index}`}>
|
||||||
<TableCell className="text-xs">{it.prize_type}</TableCell>
|
<TableCell className="text-xs">
|
||||||
|
{drawPrizeTypeLabel(it.prize_type, it.prize_index, t)}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{it.prize_index}</TableCell>
|
<TableCell className="font-mono text-xs">{it.prize_index}</TableCell>
|
||||||
<TableCell className="font-mono text-sm font-semibold">{it.number_4d}</TableCell>
|
<TableCell className="font-mono text-sm font-semibold">{it.number_4d}</TableCell>
|
||||||
<TableCell className="hidden font-mono text-xs sm:table-cell">
|
<TableCell className="hidden font-mono text-xs sm:table-cell">
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useAdminProfile } from "@/stores/admin-session";
|
|||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminDrawBatchesData } from "@/types/api/admin-draws";
|
import type { AdminDrawBatchesData } from "@/types/api/admin-draws";
|
||||||
|
|
||||||
|
import { drawStatusLabel } from "./draw-display";
|
||||||
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||||
import { DrawStatusBadge } from "./draw-status-badge";
|
import { DrawStatusBadge } from "./draw-status-badge";
|
||||||
|
|
||||||
@@ -129,13 +130,12 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">{t("manualResultEntry")}</CardTitle>
|
<CardTitle className="text-lg">{t("manualResultEntry")}</CardTitle>
|
||||||
<p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
<p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||||
{t("currentStatusAndDraft", {
|
<span>{t("currentStatusLabel")}</span>
|
||||||
status: data.draw_status,
|
<DrawStatusBadge
|
||||||
}).split(data.draw_status)[0]}
|
status={data.draw_status}
|
||||||
<DrawStatusBadge status={data.draw_status} />
|
label={drawStatusLabel(data.draw_status, t)}
|
||||||
{t("currentStatusAndDraft", {
|
/>
|
||||||
status: data.draw_status,
|
<span>· {t("currentStatusDraftHint")}</span>
|
||||||
}).split(data.draw_status)[1] ?? ""}
|
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -35,6 +37,7 @@ import { useAdminProfile } from "@/stores/admin-session";
|
|||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-draws";
|
import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-draws";
|
||||||
|
|
||||||
|
import { drawStatusLabel } from "./draw-display";
|
||||||
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||||
import { DrawStatusBadge } from "./draw-status-badge";
|
import { DrawStatusBadge } from "./draw-status-badge";
|
||||||
|
|
||||||
@@ -67,7 +70,9 @@ function drawAdminStatusSelectLabel(raw: unknown, t: (key: string) => string): s
|
|||||||
export function DrawsIndexConsole() {
|
export function DrawsIndexConsole() {
|
||||||
const { t } = useTranslation(["draws", "common"]);
|
const { t } = useTranslation(["draws", "common"]);
|
||||||
const exportLabels = useExportLabels("drawsList");
|
const exportLabels = useExportLabels("drawsList");
|
||||||
|
useAdminCurrencyCatalog();
|
||||||
const formatDt = useAdminDateTimeFormatter();
|
const formatDt = useAdminDateTimeFormatter();
|
||||||
|
const defaultCurrency = "NPR";
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
|
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
|
||||||
const [data, setData] = useState<AdminDrawListData | null>(null);
|
const [data, setData] = useState<AdminDrawListData | null>(null);
|
||||||
@@ -271,22 +276,28 @@ export function DrawsIndexConsole() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<DrawStatusBadge
|
<DrawStatusBadge
|
||||||
status={row.status}
|
status={row.status}
|
||||||
label={drawAdminStatusSelectLabel(row.status, t)}
|
label={drawStatusLabel(row.status, t)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-mono text-xs tabular-nums">
|
<TableCell className="text-right text-xs tabular-nums">
|
||||||
{row.total_bet_minor ?? "—"}
|
{row.total_bet_minor != null
|
||||||
|
? formatAdminMinorUnits(row.total_bet_minor, defaultCurrency)
|
||||||
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-mono text-xs tabular-nums">
|
<TableCell className="text-right text-xs tabular-nums">
|
||||||
{row.total_payout_minor ?? "—"}
|
{row.total_payout_minor != null
|
||||||
|
? formatAdminMinorUnits(row.total_payout_minor, defaultCurrency)
|
||||||
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-right font-mono text-xs tabular-nums",
|
"text-right text-xs tabular-nums",
|
||||||
(row.profit_loss_minor ?? 0) < 0 ? "text-destructive" : "text-emerald-600",
|
(row.profit_loss_minor ?? 0) < 0 ? "text-destructive" : "text-emerald-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{row.profit_loss_minor ?? "—"}
|
{row.profit_loss_minor != null
|
||||||
|
? formatAdminMinorUnits(row.profit_loss_minor, defaultCurrency)
|
||||||
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ import {
|
|||||||
|
|
||||||
import { getAdminAuditLogs } from "@/api/admin-audit";
|
import { getAdminAuditLogs } from "@/api/admin-audit";
|
||||||
import { getAdminPlayTypes } from "@/api/admin-config";
|
import { getAdminPlayTypes } from "@/api/admin-config";
|
||||||
|
import { useAdminPlayCodeLabel, useAdminPlayTypeCatalog } from "@/hooks/use-admin-play-type-catalog";
|
||||||
|
import {
|
||||||
|
getAdminPlayTypesLoadPromise,
|
||||||
|
getCachedAdminPlayTypes,
|
||||||
|
resolveAdminPlayTypeDisplayName,
|
||||||
|
} from "@/lib/admin-play-types";
|
||||||
import { getAdminDraws, getAdminDrawFinanceSummary } from "@/api/admin-draws";
|
import { getAdminDraws, getAdminDrawFinanceSummary } from "@/api/admin-draws";
|
||||||
import { getAdminPlayers } from "@/api/admin-player";
|
import { getAdminPlayers } from "@/api/admin-player";
|
||||||
import {
|
import {
|
||||||
@@ -358,8 +364,10 @@ function resultRowCount(result: ReportResult | null): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ReportsConsole() {
|
export function ReportsConsole() {
|
||||||
const { t } = useTranslation(["reports", "common"]);
|
const { t, i18n } = useTranslation(["reports", "common"]);
|
||||||
useAdminCurrencyCatalog();
|
useAdminCurrencyCatalog();
|
||||||
|
useAdminPlayTypeCatalog();
|
||||||
|
const playCodeLabel = useAdminPlayCodeLabel();
|
||||||
const formatTs = useAdminDateTimeFormatter();
|
const formatTs = useAdminDateTimeFormatter();
|
||||||
const [selectedKey, setSelectedKey] = useState<ReportKey>(REPORTS[0].key);
|
const [selectedKey, setSelectedKey] = useState<ReportKey>(REPORTS[0].key);
|
||||||
const [filters, setFilters] = useState<ReportFilters>(emptyFilters);
|
const [filters, setFilters] = useState<ReportFilters>(emptyFilters);
|
||||||
@@ -388,17 +396,20 @@ export function ReportsConsole() {
|
|||||||
|
|
||||||
const loadPlayOptions = useCallback(async () => {
|
const loadPlayOptions = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const payload = await getAdminPlayTypes();
|
await getAdminPlayTypesLoadPromise(getAdminPlayTypes);
|
||||||
setPlayOptions(
|
setPlayOptions(
|
||||||
payload.items.map((item) => ({
|
getCachedAdminPlayTypes().map((item) => ({
|
||||||
code: item.play_code,
|
code: item.play_code,
|
||||||
label: optionText(item.display_name_zh, item.play_code),
|
label: optionText(
|
||||||
|
resolveAdminPlayTypeDisplayName(item.play_code, i18n.language, item),
|
||||||
|
item.play_code,
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
setPlayOptions([]);
|
setPlayOptions([]);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [i18n.language]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadPlayOptions();
|
void loadPlayOptions();
|
||||||
@@ -1056,7 +1067,7 @@ export function ReportsConsole() {
|
|||||||
<TableCell className="font-mono text-xs">#{item.id}</TableCell>
|
<TableCell className="font-mono text-xs">#{item.id}</TableCell>
|
||||||
<TableCell>{item.action_type}</TableCell>
|
<TableCell>{item.action_type}</TableCell>
|
||||||
<TableCell className="text-right">{formatPlainMoney(item.amount, result.raw.currency_code)}</TableCell>
|
<TableCell className="text-right">{formatPlainMoney(item.amount, result.raw.currency_code)}</TableCell>
|
||||||
<TableCell>{item.play_code || "-"}</TableCell>
|
<TableCell>{playCodeLabel(item.play_code)}</TableCell>
|
||||||
<TableCell>{item.ticket_no || "-"}</TableCell>
|
<TableCell>{item.ticket_no || "-"}</TableCell>
|
||||||
<TableCell>{item.player_id || "-"}</TableCell>
|
<TableCell>{item.player_id || "-"}</TableCell>
|
||||||
<TableCell>{item.source_reason || "-"}</TableCell>
|
<TableCell>{item.source_reason || "-"}</TableCell>
|
||||||
@@ -1115,7 +1126,7 @@ export function ReportsConsole() {
|
|||||||
if (result.key === "play_dimension") {
|
if (result.key === "play_dimension") {
|
||||||
return result.raw.map((item) => (
|
return result.raw.map((item) => (
|
||||||
<TableRow key={`${item.play_code}-${item.dimension}`}>
|
<TableRow key={`${item.play_code}-${item.dimension}`}>
|
||||||
<TableCell className="font-medium">{item.play_code}</TableCell>
|
<TableCell className="font-medium">{playCodeLabel(item.play_code)}</TableCell>
|
||||||
<TableCell>{item.dimension}D</TableCell>
|
<TableCell>{item.dimension}D</TableCell>
|
||||||
<TableCell className="text-right">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
<TableCell className="text-right">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-right">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
<TableCell className="text-right">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
||||||
@@ -1130,7 +1141,7 @@ export function ReportsConsole() {
|
|||||||
if (result.key === "rebate_commission") {
|
if (result.key === "rebate_commission") {
|
||||||
return result.raw.map((item) => (
|
return result.raw.map((item) => (
|
||||||
<TableRow key={item.play_code}>
|
<TableRow key={item.play_code}>
|
||||||
<TableCell className="font-medium">{item.play_code}</TableCell>
|
<TableCell className="font-medium">{playCodeLabel(item.play_code)}</TableCell>
|
||||||
<TableCell>{item.order_count}</TableCell>
|
<TableCell>{item.order_count}</TableCell>
|
||||||
<TableCell className="text-right">{formatPlainMoney(item.total_rebate_minor, "NPR")}</TableCell>
|
<TableCell className="text-right">{formatPlainMoney(item.total_rebate_minor, "NPR")}</TableCell>
|
||||||
<TableCell className="text-right">{item.ticket_item_count}</TableCell>
|
<TableCell className="text-right">{item.ticket_item_count}</TableCell>
|
||||||
|
|||||||
27
src/modules/risk/risk-display.ts
Normal file
27
src/modules/risk/risk-display.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
type RiskTranslate = (key: string, options?: { ns?: string }) => string;
|
||||||
|
|
||||||
|
/** 风控占用流水来源(risk.sourceReasonOptions) */
|
||||||
|
export function riskSourceReasonLabel(
|
||||||
|
reason: string | null | undefined,
|
||||||
|
t: RiskTranslate,
|
||||||
|
): string {
|
||||||
|
if (reason == null || reason === "") {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
const key = `sourceReasonOptions.${reason}`;
|
||||||
|
const translated = t(key, { ns: "risk" });
|
||||||
|
return translated === key ? reason : translated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RiskPoolsPageTitleKey = "hotPageTitle" | "soldOutPageTitle" | "allPoolsPageTitle";
|
||||||
|
|
||||||
|
/** 锁定 / 释放动作(risk.lock / risk.release) */
|
||||||
|
export function riskActionTypeLabel(action: string, t: RiskTranslate): string {
|
||||||
|
if (action === "lock") {
|
||||||
|
return t("lock", { ns: "risk" });
|
||||||
|
}
|
||||||
|
if (action === "release") {
|
||||||
|
return t("release", { ns: "risk" });
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
@@ -27,33 +27,30 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
|
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
|
import { riskActionTypeLabel, riskSourceReasonLabel } from "@/modules/risk/risk-display";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminRiskLockLogListData, AdminRiskLockLogRow } from "@/types/api/admin-risk";
|
import type { AdminRiskLockLogListData, AdminRiskLockLogRow } from "@/types/api/admin-risk";
|
||||||
|
|
||||||
const ACTION_ALL = "__all__";
|
const ACTION_ALL = "__all__";
|
||||||
|
|
||||||
function riskActionLabel(
|
function riskActionFilterLabel(
|
||||||
value: string,
|
value: string,
|
||||||
t: (key: string) => string,
|
t: (key: string) => string,
|
||||||
): string {
|
): string {
|
||||||
if (value === ACTION_ALL) {
|
if (value === ACTION_ALL) {
|
||||||
return t("noLimit");
|
return t("noLimit");
|
||||||
}
|
}
|
||||||
if (value === "lock") {
|
return riskActionTypeLabel(value, t);
|
||||||
return t("lock");
|
|
||||||
}
|
|
||||||
if (value === "release") {
|
|
||||||
return t("release");
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
|
export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
|
||||||
const { t } = useTranslation(["risk", "common"]);
|
const { t } = useTranslation(["risk", "common"]);
|
||||||
const exportLabels = useExportLabels("riskLockLogs");
|
const exportLabels = useExportLabels("riskLockLogs");
|
||||||
useAdminCurrencyCatalog();
|
useAdminCurrencyCatalog();
|
||||||
|
const playCodeLabel = useAdminPlayCodeLabel();
|
||||||
const formatDt = useAdminDateTimeFormatter();
|
const formatDt = useAdminDateTimeFormatter();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(10);
|
const [perPage, setPerPage] = useState(10);
|
||||||
@@ -129,7 +126,7 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="risk-log-action" size="sm" className="w-full sm:w-40">
|
<SelectTrigger id="risk-log-action" size="sm" className="w-full sm:w-40">
|
||||||
<SelectValue>{riskActionLabel(draftAction, t)}</SelectValue>
|
<SelectValue>{riskActionFilterLabel(draftAction, t)}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={ACTION_ALL}>{t("noLimit")}</SelectItem>
|
<SelectItem value={ACTION_ALL}>{t("noLimit")}</SelectItem>
|
||||||
@@ -185,16 +182,16 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{row.normalized_number}</TableCell>
|
<TableCell className="font-mono text-sm">{row.normalized_number}</TableCell>
|
||||||
<TableCell className="text-sm">
|
<TableCell className="text-sm">
|
||||||
{riskActionLabel(row.action_type, t)}
|
{riskActionTypeLabel(row.action_type, t)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-sm tabular-nums">
|
<TableCell className="text-right text-sm tabular-nums">
|
||||||
{formatAdminMinorUnits(row.amount, data?.currency_code ?? "NPR")}
|
{formatAdminMinorUnits(row.amount, data?.currency_code ?? "NPR")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs text-muted-foreground">
|
<TableCell className="text-xs text-muted-foreground">
|
||||||
{row.source_reason ?? "—"}
|
{riskSourceReasonLabel(row.source_reason, t)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.ticket_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.ticket_no ?? "—"}</TableCell>
|
||||||
<TableCell className="text-xs">{row.play_code ?? "—"}</TableCell>
|
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
|
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
|
import { riskActionTypeLabel, riskSourceReasonLabel } from "@/modules/risk/risk-display";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -35,6 +37,7 @@ export function RiskPoolDetailConsole({
|
|||||||
const { t } = useTranslation(["risk", "common"]);
|
const { t } = useTranslation(["risk", "common"]);
|
||||||
const exportLabels = useExportLabels("riskPoolDetail", { number: number4d });
|
const exportLabels = useExportLabels("riskPoolDetail", { number: number4d });
|
||||||
useAdminCurrencyCatalog();
|
useAdminCurrencyCatalog();
|
||||||
|
const playCodeLabel = useAdminPlayCodeLabel();
|
||||||
const formatDt = useAdminDateTimeFormatter();
|
const formatDt = useAdminDateTimeFormatter();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(10);
|
const [perPage, setPerPage] = useState(10);
|
||||||
@@ -172,15 +175,15 @@ export function RiskPoolDetailConsole({
|
|||||||
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
||||||
{row.created_at ? formatDt(row.created_at) : "—"}
|
{row.created_at ? formatDt(row.created_at) : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm">{row.action_type}</TableCell>
|
<TableCell className="text-sm">{riskActionTypeLabel(row.action_type, t)}</TableCell>
|
||||||
<TableCell className="text-right text-sm tabular-nums">
|
<TableCell className="text-right text-sm tabular-nums">
|
||||||
{formatAdminMinorUnits(row.amount, currencyCode)}
|
{formatAdminMinorUnits(row.amount, currencyCode)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs text-muted-foreground">
|
<TableCell className="text-xs text-muted-foreground">
|
||||||
{row.source_reason ?? "—"}
|
{riskSourceReasonLabel(row.source_reason, t)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.ticket_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.ticket_no ?? "—"}</TableCell>
|
||||||
<TableCell className="text-xs">{row.play_code ?? "—"}</TableCell>
|
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { useExportLabels } from "@/hooks/use-export-labels";
|
|||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
import type { RiskPoolsPageTitleKey } from "@/modules/risk/risk-display";
|
||||||
import type { AdminRiskPoolListData, AdminRiskPoolRow } from "@/types/api/admin-risk";
|
import type { AdminRiskPoolListData, AdminRiskPoolRow } from "@/types/api/admin-risk";
|
||||||
|
|
||||||
const SORT_OPTIONS: { value: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc"; label: string }[] =
|
const SORT_OPTIONS: { value: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc"; label: string }[] =
|
||||||
@@ -59,7 +60,9 @@ type RiskFilter = "all" | "sold_out" | "high_risk";
|
|||||||
|
|
||||||
type RiskPoolsConsoleProps = {
|
type RiskPoolsConsoleProps = {
|
||||||
drawId: number;
|
drawId: number;
|
||||||
title: string;
|
/** @deprecated 优先使用 titleKey */
|
||||||
|
title?: string;
|
||||||
|
titleKey?: RiskPoolsPageTitleKey;
|
||||||
soldOutOnly: boolean;
|
soldOutOnly: boolean;
|
||||||
defaultSort: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc";
|
defaultSort: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc";
|
||||||
allowSortChange?: boolean;
|
allowSortChange?: boolean;
|
||||||
@@ -68,11 +71,13 @@ type RiskPoolsConsoleProps = {
|
|||||||
export function RiskPoolsConsole({
|
export function RiskPoolsConsole({
|
||||||
drawId,
|
drawId,
|
||||||
title,
|
title,
|
||||||
|
titleKey,
|
||||||
soldOutOnly,
|
soldOutOnly,
|
||||||
defaultSort,
|
defaultSort,
|
||||||
allowSortChange = false,
|
allowSortChange = false,
|
||||||
}: RiskPoolsConsoleProps) {
|
}: RiskPoolsConsoleProps) {
|
||||||
const { t } = useTranslation(["risk", "common"]);
|
const { t } = useTranslation(["risk", "common"]);
|
||||||
|
const pageTitle = titleKey ? t(titleKey) : (title ?? t("poolsTitle"));
|
||||||
const exportLabels = useExportLabels("riskPools");
|
const exportLabels = useExportLabels("riskPools");
|
||||||
useAdminCurrencyCatalog();
|
useAdminCurrencyCatalog();
|
||||||
const [sort, setSort] = useState(defaultSort);
|
const [sort, setSort] = useState(defaultSort);
|
||||||
@@ -145,7 +150,7 @@ export function RiskPoolsConsole({
|
|||||||
return (
|
return (
|
||||||
<Card className="admin-list-card">
|
<Card className="admin-list-card">
|
||||||
<CardHeader className="admin-list-header space-y-3">
|
<CardHeader className="admin-list-header space-y-3">
|
||||||
<CardTitle className="admin-list-title">{title}</CardTitle>
|
<CardTitle className="admin-list-title">{pageTitle}</CardTitle>
|
||||||
<div className="admin-list-toolbar">
|
<div className="admin-list-toolbar">
|
||||||
<div className="admin-list-field">
|
<div className="admin-list-field">
|
||||||
<Label htmlFor="risk-pool-number" className="sm:w-16 sm:shrink-0">
|
<Label htmlFor="risk-pool-number" className="sm:w-16 sm:shrink-0">
|
||||||
@@ -216,7 +221,7 @@ export function RiskPoolsConsole({
|
|||||||
<div className="admin-list-actions">
|
<div className="admin-list-actions">
|
||||||
<AdminTableExportButton
|
<AdminTableExportButton
|
||||||
tableId={`risk-pools-table-${drawId}`}
|
tableId={`risk-pools-table-${drawId}`}
|
||||||
filename={title ?? exportLabels.filename}
|
filename={pageTitle ?? exportLabels.filename}
|
||||||
sheetName={exportLabels.sheetName}
|
sheetName={exportLabels.sheetName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
|
||||||
const DRAW_GROUP = "draw";
|
const DRAW_GROUP = "draw";
|
||||||
@@ -24,10 +25,16 @@ const DRAW_KEYS = {
|
|||||||
AUTO_SETTLEMENT: "settlement.auto_run_on_tick",
|
AUTO_SETTLEMENT: "settlement.auto_run_on_tick",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const FRONTEND_GROUP = "frontend";
|
||||||
|
const FRONTEND_KEYS = {
|
||||||
|
PLAY_RULES_HTML: "frontend.play_rules_html",
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface RuntimeDraft {
|
interface RuntimeDraft {
|
||||||
requireManualReview: boolean;
|
requireManualReview: boolean;
|
||||||
cooldownMinutes: string;
|
cooldownMinutes: string;
|
||||||
autoSettlement: boolean;
|
autoSettlement: boolean;
|
||||||
|
playRulesHtml: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BinaryChoice({
|
function BinaryChoice({
|
||||||
@@ -75,11 +82,13 @@ export function SystemSettingsScreen() {
|
|||||||
requireManualReview: false,
|
requireManualReview: false,
|
||||||
cooldownMinutes: "15",
|
cooldownMinutes: "15",
|
||||||
autoSettlement: true,
|
autoSettlement: true,
|
||||||
|
playRulesHtml: "",
|
||||||
});
|
});
|
||||||
const [saved, setSaved] = useState<RuntimeDraft>({
|
const [saved, setSaved] = useState<RuntimeDraft>({
|
||||||
requireManualReview: false,
|
requireManualReview: false,
|
||||||
cooldownMinutes: "15",
|
cooldownMinutes: "15",
|
||||||
autoSettlement: true,
|
autoSettlement: true,
|
||||||
|
playRulesHtml: "",
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -88,13 +97,14 @@ export function SystemSettingsScreen() {
|
|||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [drawRes, settlementRes] = await Promise.all([
|
const [drawRes, settlementRes, frontendRes] = await Promise.all([
|
||||||
getAdminSettings(DRAW_GROUP),
|
getAdminSettings(DRAW_GROUP),
|
||||||
getAdminSettings(SETTLEMENT_GROUP),
|
getAdminSettings(SETTLEMENT_GROUP),
|
||||||
|
getAdminSettings(FRONTEND_GROUP),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const kv: Record<string, unknown> = {};
|
const kv: Record<string, unknown> = {};
|
||||||
for (const item of [...drawRes.items, ...settlementRes.items]) {
|
for (const item of [...drawRes.items, ...settlementRes.items, ...frontendRes.items]) {
|
||||||
kv[item.key] = item.value;
|
kv[item.key] = item.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +112,7 @@ export function SystemSettingsScreen() {
|
|||||||
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] ?? ""),
|
||||||
};
|
};
|
||||||
setDraft(nextDraft);
|
setDraft(nextDraft);
|
||||||
setSaved(nextDraft);
|
setSaved(nextDraft);
|
||||||
@@ -133,6 +144,7 @@ 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);
|
||||||
toast.success(t("system.saveSuccess", { ns: "config" }));
|
toast.success(t("system.saveSuccess", { ns: "config" }));
|
||||||
setSaved(draft);
|
setSaved(draft);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
@@ -233,6 +245,48 @@ export function SystemSettingsScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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("system.frontendConfig", { ns: "config", defaultValue: "前端配置" })}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
<section className="space-y-4 border-t border-border/60 pt-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="text-base font-semibold">{t("wallet.title", { ns: "config" })}</h3>
|
<h3 className="text-base font-semibold">{t("wallet.title", { ns: "config" })}</h3>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
|
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { formatAdminMinorUnits } from "@/lib/money";
|
import { formatAdminMinorUnits } from "@/lib/money";
|
||||||
@@ -72,6 +73,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
|||||||
const { t } = useTranslation(["settlement", "common"]);
|
const { t } = useTranslation(["settlement", "common"]);
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
useAdminCurrencyCatalog();
|
useAdminCurrencyCatalog();
|
||||||
|
const playCodeLabel = useAdminPlayCodeLabel();
|
||||||
const canReviewSettlement = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_REVIEW]);
|
const canReviewSettlement = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_REVIEW]);
|
||||||
const canManagePayout = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_MANAGE]);
|
const canManagePayout = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_MANAGE]);
|
||||||
const formatDt = useAdminDateTimeFormatter();
|
const formatDt = useAdminDateTimeFormatter();
|
||||||
@@ -333,7 +335,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
|||||||
{details.items.map((r) => (
|
{details.items.map((r) => (
|
||||||
<TableRow key={r.id}>
|
<TableRow key={r.id}>
|
||||||
<TableCell className="font-mono text-xs">{r.ticket_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{r.ticket_no ?? "—"}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{r.play_code ?? "—"}</TableCell>
|
<TableCell className="text-xs">{playCodeLabel(r.play_code)}</TableCell>
|
||||||
<TableCell className="max-w-[10rem] truncate text-xs">
|
<TableCell className="max-w-[10rem] truncate text-xs">
|
||||||
{r.player_username ?? r.site_player_id ?? r.player_id ?? "—"}
|
{r.player_username ?? r.site_player_id ?? r.player_id ?? "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
|
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminTicketItemsData } from "@/types/api/admin-tickets";
|
import type { AdminTicketItemsData } from "@/types/api/admin-tickets";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
@@ -60,13 +61,18 @@ const emptyTicketFilters: TicketFilters = {
|
|||||||
statuses: [],
|
statuses: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function ticketStatusText(value: string, t: (key: string) => string): string {
|
type TicketTranslateFn = (
|
||||||
|
key: string,
|
||||||
|
options?: { count?: number },
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
function ticketStatusText(value: string, t: TicketTranslateFn): string {
|
||||||
const key = `statusOptions.${value}`;
|
const key = `statusOptions.${value}`;
|
||||||
const translated = t(key);
|
const translated = t(key);
|
||||||
return translated === key ? value : translated;
|
return translated === key ? value : translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ticketStatusSummary(statuses: string[], t: (key: string) => string): string {
|
function ticketStatusSummary(statuses: string[], t: TicketTranslateFn): string {
|
||||||
if (statuses.length === 0) {
|
if (statuses.length === 0) {
|
||||||
return t("statusOptions.all");
|
return t("statusOptions.all");
|
||||||
}
|
}
|
||||||
@@ -80,6 +86,7 @@ function ticketStatusSummary(statuses: string[], t: (key: string) => string): st
|
|||||||
|
|
||||||
export function PlayerTicketsConsole(): React.ReactElement {
|
export function PlayerTicketsConsole(): React.ReactElement {
|
||||||
const { t } = useTranslation(["tickets", "common"]);
|
const { t } = useTranslation(["tickets", "common"]);
|
||||||
|
const playCodeLabel = useAdminPlayCodeLabel();
|
||||||
const exportLabels = useExportLabels("tickets");
|
const exportLabels = useExportLabels("tickets");
|
||||||
const formatTs = useAdminDateTimeFormatter();
|
const formatTs = useAdminDateTimeFormatter();
|
||||||
const [draft, setDraft] = useState<TicketFilters>(emptyTicketFilters);
|
const [draft, setDraft] = useState<TicketFilters>(emptyTicketFilters);
|
||||||
@@ -328,7 +335,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.order_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.order_no ?? "—"}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.draw_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.draw_no ?? "—"}</TableCell>
|
||||||
<TableCell className="text-xs">{row.play_code}</TableCell>
|
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.original_number ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.original_number ?? "—"}</TableCell>
|
||||||
<TableCell className="text-right tabular-nums text-xs">
|
<TableCell className="text-right tabular-nums text-xs">
|
||||||
{row.total_bet_amount_formatted}
|
{row.total_bet_amount_formatted}
|
||||||
|
|||||||
Reference in New Issue
Block a user