feat(settlement): 支持结算时区与已关账漏账流水显示
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
- 為結算期間相關接口與前端組件新增 settlement_timezone 字段 - 在账期范围格式化函数中支持传入时区参数显示当地日期 - 在开账面板和账期视图中增加漏账流水(orphanActivity)高亮显示 - 统一调整账期视图默认子页面为“账单处理”而非“待处理” - 在开奖管理中添加开奖提供商(provider)信息展示和选择功能 - 结算中心增加账单状态筛选项支持待确认、待收付、已结清、调账/冲正 - 移除部分账期页状态筛选标签,改用文本方式显示待确认与待收付数量 - 集成站点管理中新增结算时区字段,支持编辑与保存 - 多语言新增与更新与结算时区和漏账流水相关的文案提示及翻译
This commit is contained in:
@@ -102,8 +102,10 @@ export async function postSettlementPeriod(body: {
|
|||||||
export type SettlementPeriodOpenHints = {
|
export type SettlementPeriodOpenHints = {
|
||||||
suggested_start: string;
|
suggested_start: string;
|
||||||
suggested_end: string;
|
suggested_end: string;
|
||||||
|
settlement_timezone: string;
|
||||||
occupied_period_dates: string[];
|
occupied_period_dates: string[];
|
||||||
pending_activity_dates: string[];
|
pending_activity_dates: string[];
|
||||||
|
orphan_activity_dates?: string[];
|
||||||
unpaid_bill_dates: string[];
|
unpaid_bill_dates: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export type AdminDateRangeCalendarMarkers = {
|
|||||||
occupiedPeriod?: string[];
|
occupiedPeriod?: string[];
|
||||||
/** 本地日历 `yyyy-MM-dd` */
|
/** 本地日历 `yyyy-MM-dd` */
|
||||||
pendingActivity?: string[];
|
pendingActivity?: string[];
|
||||||
|
/** 本地日历 `yyyy-MM-dd` — 已关账账期内仍有未归属流水 */
|
||||||
|
orphanActivity?: string[];
|
||||||
/** 本地日历 `yyyy-MM-dd` — 已有账期内的未结清日 */
|
/** 本地日历 `yyyy-MM-dd` — 已有账期内的未结清日 */
|
||||||
unpaidBill?: string[];
|
unpaidBill?: string[];
|
||||||
};
|
};
|
||||||
@@ -113,6 +115,13 @@ export function AdminDateRangeField({
|
|||||||
.filter((d): d is Date => d instanceof Date),
|
.filter((d): d is Date => d instanceof Date),
|
||||||
[calendarMarkers?.unpaidBill],
|
[calendarMarkers?.unpaidBill],
|
||||||
);
|
);
|
||||||
|
const orphanActivityDates = React.useMemo(
|
||||||
|
() =>
|
||||||
|
(calendarMarkers?.orphanActivity ?? [])
|
||||||
|
.map((ymd) => parseYmd(ymd))
|
||||||
|
.filter((d): d is Date => d instanceof Date),
|
||||||
|
[calendarMarkers?.orphanActivity],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
@@ -166,6 +175,7 @@ export function AdminDateRangeField({
|
|||||||
modifiers={{
|
modifiers={{
|
||||||
occupiedPeriod: occupiedPeriodDates,
|
occupiedPeriod: occupiedPeriodDates,
|
||||||
pendingActivity: pendingActivityDates,
|
pendingActivity: pendingActivityDates,
|
||||||
|
orphanActivity: orphanActivityDates,
|
||||||
unpaidBill: unpaidBillDates,
|
unpaidBill: unpaidBillDates,
|
||||||
}}
|
}}
|
||||||
modifiersClassNames={{
|
modifiersClassNames={{
|
||||||
@@ -173,6 +183,8 @@ export function AdminDateRangeField({
|
|||||||
"[&>button]:cursor-not-allowed [&>button]:bg-muted/80 [&>button]:text-muted-foreground [&>button]:line-through [&>button]:opacity-70",
|
"[&>button]:cursor-not-allowed [&>button]:bg-muted/80 [&>button]:text-muted-foreground [&>button]:line-through [&>button]:opacity-70",
|
||||||
pendingActivity:
|
pendingActivity:
|
||||||
"[&:not([data-disabled=true])>button]:relative [&:not([data-disabled=true])>button]:after:absolute [&:not([data-disabled=true])>button]:after:bottom-0.5 [&:not([data-disabled=true])>button]:after:left-1/2 [&:not([data-disabled=true])>button]:after:size-1 [&:not([data-disabled=true])>button]:after:-translate-x-1/2 [&:not([data-disabled=true])>button]:after:rounded-full [&:not([data-disabled=true])>button]:after:bg-amber-500 [&:not([data-disabled=true])>button]:after:content-['']",
|
"[&:not([data-disabled=true])>button]:relative [&:not([data-disabled=true])>button]:after:absolute [&:not([data-disabled=true])>button]:after:bottom-0.5 [&:not([data-disabled=true])>button]:after:left-1/2 [&:not([data-disabled=true])>button]:after:size-1 [&:not([data-disabled=true])>button]:after:-translate-x-1/2 [&:not([data-disabled=true])>button]:after:rounded-full [&:not([data-disabled=true])>button]:after:bg-amber-500 [&:not([data-disabled=true])>button]:after:content-['']",
|
||||||
|
orphanActivity:
|
||||||
|
"[&>button]:relative [&>button]:after:absolute [&>button]:after:bottom-0.5 [&>button]:after:left-1/2 [&>button]:after:size-1.5 [&>button]:after:-translate-x-1/2 [&>button]:after:rounded-full [&>button]:after:bg-violet-600 [&>button]:after:content-['']",
|
||||||
unpaidBill:
|
unpaidBill:
|
||||||
"[&>button]:relative [&>button]:before:absolute [&>button]:before:top-0.5 [&>button]:before:right-0.5 [&>button]:before:size-1.5 [&>button]:before:rounded-full [&>button]:before:bg-rose-500 [&>button]:before:content-['']",
|
"[&>button]:relative [&>button]:before:absolute [&>button]:before:top-0.5 [&>button]:before:right-0.5 [&>button]:before:size-1.5 [&>button]:before:rounded-full [&>button]:before:bg-rose-500 [&>button]:before:content-['']",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -295,11 +295,13 @@
|
|||||||
"manageTitle": "Period management",
|
"manageTitle": "Period management",
|
||||||
"markerOccupied": "Occupied period",
|
"markerOccupied": "Occupied period",
|
||||||
"markerPending": "Pending turnover",
|
"markerPending": "Pending turnover",
|
||||||
|
"markerOrphan": "Closed-period orphan turnover",
|
||||||
"markerUnpaid": "Unsettled period",
|
"markerUnpaid": "Unsettled period",
|
||||||
"open": "Open period",
|
"open": "Open period",
|
||||||
"openDesc": "Select the start and end dates; gray strikethrough dates are occupied, amber dots indicate pending turnover, red dots at top-right indicate unsettled periods.",
|
"openDesc": "Select the start and end dates; gray strikethrough dates are occupied, amber dots indicate pending turnover, violet dots indicate closed-period orphan turnover, red dots at top-right indicate unsettled periods.",
|
||||||
"openFailed": "Failed to open period",
|
"openFailed": "Failed to open period",
|
||||||
"openHint": "Local dates ({{tz}})",
|
"openHint": "Local dates ({{tz}})",
|
||||||
|
"timezoneHint": "Period dates are calculated in site timezone {{tz}}.",
|
||||||
"openWithPreset": "Apply quick preset",
|
"openWithPreset": "Apply quick preset",
|
||||||
"opened": "Period opened",
|
"opened": "Period opened",
|
||||||
"overlapsOccupied": "The selected range overlaps with existing periods. Please avoid dates with gray strikethrough lines.",
|
"overlapsOccupied": "The selected range overlaps with existing periods. Please avoid dates with gray strikethrough lines.",
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"lotteryH5BaseUrl": "Lottery H5 base URL (optional)",
|
"lotteryH5BaseUrl": "Lottery H5 base URL (optional)",
|
||||||
"name": "Site name",
|
"name": "Site name",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
|
"settlementTimezone": "Settlement timezone",
|
||||||
"ssoSecret": "SSO secret",
|
"ssoSecret": "SSO secret",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"walletApiKey": "Wallet API key",
|
"walletApiKey": "Wallet API key",
|
||||||
@@ -161,6 +162,7 @@
|
|||||||
"lotteryH5BaseUrl": "Enter H5 URL",
|
"lotteryH5BaseUrl": "Enter H5 URL",
|
||||||
"name": "Enter site name",
|
"name": "Enter site name",
|
||||||
"notes": "Enter notes",
|
"notes": "Enter notes",
|
||||||
|
"settlementTimezone": "Enter IANA timezone, for example Asia/Kathmandu",
|
||||||
"walletApiUrl": "Enter wallet API URL"
|
"walletApiUrl": "Enter wallet API URL"
|
||||||
},
|
},
|
||||||
"rotateConfirm": "Rotate",
|
"rotateConfirm": "Rotate",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"failed": "Create failed"
|
"failed": "Create failed"
|
||||||
},
|
},
|
||||||
"drawNo": "Draw no.",
|
"drawNo": "Draw no.",
|
||||||
|
"provider": "Provider",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"startTime": "Start time",
|
"startTime": "Start time",
|
||||||
"closeTime": "Close time",
|
"closeTime": "Close time",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"closeDialogTitle": "Close period",
|
"closeDialogTitle": "Close period",
|
||||||
"closeDialogDesc": "Summarize {{range}} and generate bills.",
|
"closeDialogDesc": "Summarize {{range}} and generate bills.",
|
||||||
"closeDialogShare": "{{count}} ledger entries",
|
"closeDialogShare": "{{count}} ledger entries",
|
||||||
"closeDialogUnsettled": "{{count}} tickets still unsettled",
|
"closeDialogUnsettled": "{{count}} ticket(s) still unsettled. Finish draw settlement before closing.",
|
||||||
"closeDialogIrreversible": "Cannot undo. Use adjustments or reversals to fix errors.",
|
"closeDialogIrreversible": "Cannot undo. Use adjustments or reversals to fix errors.",
|
||||||
"closeDialogConfirm": "Close period",
|
"closeDialogConfirm": "Close period",
|
||||||
"closeDialogEmpty": "No share ledger this period; closing will not generate bills."
|
"closeDialogEmpty": "No share ledger this period; closing will not generate bills."
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"player": "Player",
|
"player": "Player",
|
||||||
"orderNo": "Order no.",
|
"orderNo": "Order no.",
|
||||||
"drawNo": "Draw no.",
|
"drawNo": "Draw no.",
|
||||||
|
"provider": "Provider",
|
||||||
|
"allProviders": "All providers",
|
||||||
"playCode": "Play",
|
"playCode": "Play",
|
||||||
"number": "Number",
|
"number": "Number",
|
||||||
"betAmount": "Bet amount",
|
"betAmount": "Bet amount",
|
||||||
|
|||||||
@@ -311,11 +311,13 @@
|
|||||||
"closed": "अवधि बन्द भयो, बिल सिर्जना भयो",
|
"closed": "अवधि बन्द भयो, बिल सिर्जना भयो",
|
||||||
"closeFailed": "अवधि बन्द गर्न असफल",
|
"closeFailed": "अवधि बन्द गर्न असफल",
|
||||||
"hintsFailed": "खोल्ने सुझाव लोड गर्न असफल",
|
"hintsFailed": "खोल्ने सुझाव लोड गर्न असफल",
|
||||||
"openDesc": "सुरु र अन्त्य मिति चयन गर्नुहोस्; खैरो स्ट्राइकथ्रु भएका मितिहरू अधिकृत अवधि हुन्, एम्बर बिन्दुहरू पेन्डिङ कारोबार हुन्, माथि-दायाँ रातो बिन्दुहरू नबुझिएको अवधि हुन्।",
|
"openDesc": "सुरु र अन्त्य मिति चयन गर्नुहोस्; खैरो स्ट्राइकथ्रु भएका मितिहरू अधिकृत अवधि हुन्, एम्बर बिन्दुहरू पेन्डिङ कारोबार हुन्, बैजनी बिन्दुहरू बन्द अवधिमा छुटेका कारोबार हुन्, माथि-दायाँ रातो बिन्दुहरू नबुझिएको अवधि हुन्।",
|
||||||
|
"timezoneHint": "अवधि मिति साइट समयक्षेत्र {{tz}} अनुसार गणना हुन्छ।",
|
||||||
"rangeLabel": "अवधि दायरा",
|
"rangeLabel": "अवधि दायरा",
|
||||||
"rangeHint": "पहिले सुरु मिति चयन गर्नुहोस्, त्यसपछि अन्त्य मिति; एकल-दिन अवधिको लागि एउटै मिति दुई पटक क्लिक गर्नुहोस्।",
|
"rangeHint": "पहिले सुरु मिति चयन गर्नुहोस्, त्यसपछि अन्त्य मिति; एकल-दिन अवधिको लागि एउटै मिति दुई पटक क्लिक गर्नुहोस्।",
|
||||||
"markerOccupied": "अधिकृत अवधि",
|
"markerOccupied": "अधिकृत अवधि",
|
||||||
"markerPending": "पेन्डिङ कारोबार",
|
"markerPending": "पेन्डिङ कारोबार",
|
||||||
|
"markerOrphan": "बन्द अवधिमा छुटेको कारोबार",
|
||||||
"markerUnpaid": "नबुझिएको अवधि",
|
"markerUnpaid": "नबुझिएको अवधि",
|
||||||
"overlapsOccupied": "चयन गरिएको दायरा अवस्थित अवधिसँग ओभरल्याप हुन्छ। कृपया खैरो स्ट्राइकथ्रु भएका मितिहरू नछान्नुहोस्।"
|
"overlapsOccupied": "चयन गरिएको दायरा अवस्थित अवधिसँग ओभरल्याप हुन्छ। कृपया खैरो स्ट्राइकथ्रु भएका मितिहरू नछान्नुहोस्।"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"lotteryH5BaseUrl": "लटरी H5 आधार URL (वैकल्पिक)",
|
"lotteryH5BaseUrl": "लटरी H5 आधार URL (वैकल्पिक)",
|
||||||
"name": "साइट नाम",
|
"name": "साइट नाम",
|
||||||
"notes": "टिप्पणी",
|
"notes": "टिप्पणी",
|
||||||
|
"settlementTimezone": "सेटलमेन्ट समयक्षेत्र",
|
||||||
"ssoSecret": "SSO गोप्य",
|
"ssoSecret": "SSO गोप्य",
|
||||||
"status": "स्थिति",
|
"status": "स्थिति",
|
||||||
"walletApiKey": "वालेट API कुञ्जी",
|
"walletApiKey": "वालेट API कुञ्जी",
|
||||||
@@ -159,6 +160,7 @@
|
|||||||
"lotteryH5BaseUrl": "H5 ठेगाना प्रविष्ट गर्नुहोस्",
|
"lotteryH5BaseUrl": "H5 ठेगाना प्रविष्ट गर्नुहोस्",
|
||||||
"name": "साइट नाम प्रविष्ट गर्नुहोस्",
|
"name": "साइट नाम प्रविष्ट गर्नुहोस्",
|
||||||
"notes": "टिप्पणी प्रविष्ट गर्नुहोस्",
|
"notes": "टिप्पणी प्रविष्ट गर्नुहोस्",
|
||||||
|
"settlementTimezone": "IANA समयक्षेत्र प्रविष्ट गर्नुहोस्, जस्तै Asia/Kathmandu",
|
||||||
"walletApiUrl": "वालेट API ठेगाना प्रविष्ट गर्नुहोस्"
|
"walletApiUrl": "वालेट API ठेगाना प्रविष्ट गर्नुहोस्"
|
||||||
},
|
},
|
||||||
"rotateConfirm": "पुष्टि",
|
"rotateConfirm": "पुष्टि",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"drawDetail": "ड्रअ विवरण",
|
"drawDetail": "ड्रअ विवरण",
|
||||||
"drawNo": "ड्रअ नं.",
|
"drawNo": "ड्रअ नं.",
|
||||||
"drawTime": "ड्रअ समय",
|
"drawTime": "ड्रअ समय",
|
||||||
|
"provider": "Provider",
|
||||||
"editDraw": {
|
"editDraw": {
|
||||||
"action": "सम्पादन",
|
"action": "सम्पादन",
|
||||||
"description": "ड्रअ {{drawNo}} · स्थानीय समयक्षेत्र {{tz}} मा सम्पादन",
|
"description": "ड्रअ {{drawNo}} · स्थानीय समयक्षेत्र {{tz}} मा सम्पादन",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"closeDialogTitle": "अवधि बन्द गर्नुहोस्",
|
"closeDialogTitle": "अवधि बन्द गर्नुहोस्",
|
||||||
"closeDialogDesc": "{{range}} भित्रको लेजर संक्षेप गरी बिल सिर्जना गर्नुहोस्।",
|
"closeDialogDesc": "{{range}} भित्रको लेजर संक्षेप गरी बिल सिर्जना गर्नुहोस्।",
|
||||||
"closeDialogShare": "{{count}} लेजर प्रविष्टिहरू",
|
"closeDialogShare": "{{count}} लेजर प्रविष्टिहरू",
|
||||||
"closeDialogUnsettled": "{{count}} टिकटहरू अझै नसेटल",
|
"closeDialogUnsettled": "{{count}} टिकटहरू अझै नसेटल छन्। बन्द गर्नु अघि ड्र सेटलमेन्ट पूरा गर्नुहोस्।",
|
||||||
"closeDialogIrreversible": "पूर्ववत गर्न सकिँदैन। त्रुटि सच्याउन समायोजन वा रिभर्सल प्रयोग गर्नुहोस्।",
|
"closeDialogIrreversible": "पूर्ववत गर्न सकिँदैन। त्रुटि सच्याउन समायोजन वा रिभर्सल प्रयोग गर्नुहोस्।",
|
||||||
"closeDialogConfirm": "अवधि बन्द गर्नुहोस्",
|
"closeDialogConfirm": "अवधि बन्द गर्नुहोस्",
|
||||||
"closeDialogEmpty": "यस अवधिमा शेयर लेजर छैन; बन्द गर्दा बिल सिर्जना हुँदैन।"
|
"closeDialogEmpty": "यस अवधिमा शेयर लेजर छैन; बन्द गर्दा बिल सिर्जना हुँदैन।"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"detailLoadFailed": "टिकट विवरण लोड असफल",
|
"detailLoadFailed": "टिकट विवरण लोड असफल",
|
||||||
"detailTitle": "टिकट विवरण",
|
"detailTitle": "टिकट विवरण",
|
||||||
"drawNo": "ड्रअ नं.",
|
"drawNo": "ड्रअ नं.",
|
||||||
|
"provider": "Provider",
|
||||||
|
"allProviders": "सबै provider",
|
||||||
"drawNoOptional": "ड्रअ नम्बर (वैकल्पिक)",
|
"drawNoOptional": "ड्रअ नम्बर (वैकल्पिक)",
|
||||||
"drawNoPlaceholder": "जस्तै 20260520-001",
|
"drawNoPlaceholder": "जस्तै 20260520-001",
|
||||||
"failReason": "असफल कारण",
|
"failReason": "असफल कारण",
|
||||||
|
|||||||
@@ -326,11 +326,13 @@
|
|||||||
"closeFailed": "关账失败",
|
"closeFailed": "关账失败",
|
||||||
"datesRequired": "请填写账期起止日期",
|
"datesRequired": "请填写账期起止日期",
|
||||||
"invalidRange": "结束日期不能早于开始日期",
|
"invalidRange": "结束日期不能早于开始日期",
|
||||||
"openDesc": "选择账期起止日期;灰色删除线为已有账期不可再开,琥珀点为待入账,右上角红点为未结清。",
|
"openDesc": "选择账期起止日期;灰色删除线为已有账期不可再开,琥珀点为待入账,紫点为已关账漏账,右上角红点为未结清。",
|
||||||
|
"timezoneHint": "账期日期按站点时区 {{tz}} 计算。",
|
||||||
"rangeLabel": "账期范围",
|
"rangeLabel": "账期范围",
|
||||||
"rangeHint": "先选开始日期,再选结束日期;单日账期可点同一天两次。",
|
"rangeHint": "先选开始日期,再选结束日期;单日账期可点同一天两次。",
|
||||||
"markerOccupied": "已有账期",
|
"markerOccupied": "已有账期",
|
||||||
"markerPending": "待入账流水",
|
"markerPending": "待入账流水",
|
||||||
|
"markerOrphan": "已关账漏账",
|
||||||
"markerUnpaid": "未结清账期",
|
"markerUnpaid": "未结清账期",
|
||||||
"overlapsOccupied": "所选范围与已有账期重叠,请避开灰色删除线的日期。",
|
"overlapsOccupied": "所选范围与已有账期重叠,请避开灰色删除线的日期。",
|
||||||
"hintsFailed": "无法加载开账建议"
|
"hintsFailed": "无法加载开账建议"
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
"adminPassword": "初始密码",
|
"adminPassword": "初始密码",
|
||||||
"adminEmail": "邮箱(可选)",
|
"adminEmail": "邮箱(可选)",
|
||||||
"currency": "默认币种",
|
"currency": "默认币种",
|
||||||
|
"settlementTimezone": "结算时区",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"walletApiUrl": "主站钱包根 URL",
|
"walletApiUrl": "主站钱包根 URL",
|
||||||
"lotteryH5BaseUrl": "彩票 H5 基址(可选)",
|
"lotteryH5BaseUrl": "彩票 H5 基址(可选)",
|
||||||
@@ -143,6 +144,7 @@
|
|||||||
"adminPassword": "至少 6 位",
|
"adminPassword": "至少 6 位",
|
||||||
"adminEmail": "请输入邮箱",
|
"adminEmail": "请输入邮箱",
|
||||||
"currency": "请输入币种代码,如 NPR",
|
"currency": "请输入币种代码,如 NPR",
|
||||||
|
"settlementTimezone": "例如 Asia/Kathmandu",
|
||||||
"walletApiUrl": "请输入钱包接口地址",
|
"walletApiUrl": "请输入钱包接口地址",
|
||||||
"lotteryH5BaseUrl": "请输入 H5 地址",
|
"lotteryH5BaseUrl": "请输入 H5 地址",
|
||||||
"iframeOrigins": "请输入允许的来源地址,如 https://www.example.com",
|
"iframeOrigins": "请输入允许的来源地址,如 https://www.example.com",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"failed": "创建失败"
|
"failed": "创建失败"
|
||||||
},
|
},
|
||||||
"drawNo": "期号",
|
"drawNo": "期号",
|
||||||
|
"provider": "开注商",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"startTime": "开始时间",
|
"startTime": "开始时间",
|
||||||
"closeTime": "封盘时间",
|
"closeTime": "封盘时间",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"closeDialogDesc": "将汇总 {{range}} 内的流水并生成账单。",
|
"closeDialogDesc": "将汇总 {{range}} 内的流水并生成账单。",
|
||||||
"closeDialogShare": "流水 {{count}} 笔",
|
"closeDialogShare": "流水 {{count}} 笔",
|
||||||
"closeDialogEmpty": "本期暂无占成流水,关账后不会生成账单。",
|
"closeDialogEmpty": "本期暂无占成流水,关账后不会生成账单。",
|
||||||
"closeDialogUnsettled": "仍有 {{count}} 笔注单未结算",
|
"closeDialogUnsettled": "仍有 {{count}} 笔注单未结算,需先完成开奖结算后才能关账",
|
||||||
"closeDialogIrreversible": "关账后不可撤销,差错请通过调账或冲正处理。",
|
"closeDialogIrreversible": "关账后不可撤销,差错请通过调账或冲正处理。",
|
||||||
"closeDialogConfirm": "确认关账"
|
"closeDialogConfirm": "确认关账"
|
||||||
},
|
},
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"periods": "账期",
|
"periods": "账期",
|
||||||
"pendingWorkbench": "待处理",
|
"pendingWorkbench": "待处理",
|
||||||
"allBills": "全部账单",
|
"allBills": "全部账单",
|
||||||
|
"billWorkbench": "账单处理",
|
||||||
"bills": "账单",
|
"bills": "账单",
|
||||||
"operations": "收付与调账",
|
"operations": "收付与调账",
|
||||||
"aria": "账期视图",
|
"aria": "账期视图",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"player": "玩家",
|
"player": "玩家",
|
||||||
"orderNo": "订单号",
|
"orderNo": "订单号",
|
||||||
"drawNo": "期号",
|
"drawNo": "期号",
|
||||||
|
"provider": "开注商",
|
||||||
|
"allProviders": "全部开注商",
|
||||||
"playCode": "玩法",
|
"playCode": "玩法",
|
||||||
"number": "号码",
|
"number": "号码",
|
||||||
"betAmount": "下注",
|
"betAmount": "下注",
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ export function localDateRangeToUtcPeriodBounds(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** UTC 账期时刻 → 本地日历 `YYYY-MM-DD`(列表展示) */
|
/** UTC 账期时刻 → 本地日历 `YYYY-MM-DD`(列表展示) */
|
||||||
export function utcPeriodInstantToLocalYmd(iso: string | null | undefined): string {
|
export function utcPeriodInstantToLocalYmd(
|
||||||
|
iso: string | null | undefined,
|
||||||
|
timeZone?: string | null,
|
||||||
|
): string {
|
||||||
if (iso == null || iso === "") {
|
if (iso == null || iso === "") {
|
||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
@@ -81,6 +84,24 @@ export function utcPeriodInstantToLocalYmd(iso: string | null | undefined): stri
|
|||||||
if (Number.isNaN(ms)) {
|
if (Number.isNaN(ms)) {
|
||||||
return iso.slice(0, 10);
|
return iso.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
if (timeZone && typeof Intl !== "undefined") {
|
||||||
|
try {
|
||||||
|
const parts = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).formatToParts(new Date(ms));
|
||||||
|
const y = parts.find((part) => part.type === "year")?.value;
|
||||||
|
const m = parts.find((part) => part.type === "month")?.value;
|
||||||
|
const d = parts.find((part) => part.type === "day")?.value;
|
||||||
|
if (y && m && d) {
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to browser-local formatting below.
|
||||||
|
}
|
||||||
|
}
|
||||||
return toDateYmdValue(new Date(ms));
|
return toDateYmdValue(new Date(ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,9 +128,10 @@ export function utcStorageDateToLocalFormYmd(utcYmd: string): string {
|
|||||||
export function formatSettlementPeriodSpan(
|
export function formatSettlementPeriodSpan(
|
||||||
periodStart: string | undefined,
|
periodStart: string | undefined,
|
||||||
periodEnd: string | undefined,
|
periodEnd: string | undefined,
|
||||||
|
timeZone?: string | null,
|
||||||
): string {
|
): string {
|
||||||
const start = utcPeriodInstantToLocalYmd(periodStart);
|
const start = utcPeriodInstantToLocalYmd(periodStart, timeZone);
|
||||||
const end = utcPeriodInstantToLocalYmd(periodEnd);
|
const end = utcPeriodInstantToLocalYmd(periodEnd, timeZone);
|
||||||
|
|
||||||
return `${start} ~ ${end}`;
|
return `${start} ~ ${end}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function DrawPublishDialog({
|
|||||||
<DialogTitle className="text-base">
|
<DialogTitle className="text-base">
|
||||||
{t("publishTitle")}
|
{t("publishTitle")}
|
||||||
{batch ? ` · v${batch.result_version}` : ""}
|
{batch ? ` · v${batch.result_version}` : ""}
|
||||||
|
{batch?.provider_name || batch?.provider_code ? ` · ${batch.provider_name || batch.provider_code}` : ""}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||||
@@ -58,4 +59,4 @@ export function DrawPublishDialog({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ export function DrawPublishPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="admin-table-shell overflow-x-auto rounded-md border">
|
<div className="admin-table-shell overflow-x-auto rounded-md border">
|
||||||
|
<div className="border-b px-3 py-2 text-xs text-muted-foreground">
|
||||||
|
{t("provider")}:
|
||||||
|
<span className="font-mono text-foreground">
|
||||||
|
{batch.provider_name || batch.provider_code || "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -144,4 +150,4 @@ export function DrawPublishPanel({
|
|||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
|||||||
<AdminNoResourceState message={t("noPublishedBatch")} />
|
<AdminNoResourceState message={t("noPublishedBatch")} />
|
||||||
) : (
|
) : (
|
||||||
published.map((batch) => (
|
published.map((batch) => (
|
||||||
<BatchTable key={batch.id} batch={batch} />
|
<BatchTable key={`${batch.provider_code ?? "GLOBAL"}-${batch.id}`} batch={batch} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +85,9 @@ function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm">{t("version", { version: batch.result_version })}</CardTitle>
|
<CardTitle className="text-sm">
|
||||||
|
{batch.provider_name || batch.provider_code || t("provider")} · {t("version", { version: batch.result_version })}
|
||||||
|
</CardTitle>
|
||||||
<p className="font-mono text-xs text-muted-foreground">
|
<p className="font-mono text-xs text-muted-foreground">
|
||||||
{formatDt(batch.confirmed_at)}
|
{formatDt(batch.confirmed_at)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -12,9 +12,17 @@ import {
|
|||||||
getAdminDrawResultBatches,
|
getAdminDrawResultBatches,
|
||||||
postAdminCreateManualResultBatch,
|
postAdminCreateManualResultBatch,
|
||||||
} from "@/api/admin-draws";
|
} from "@/api/admin-draws";
|
||||||
|
import { getAdminBetProviders } from "@/api/admin-bet-providers";
|
||||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +39,7 @@ import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
|||||||
import { useAdminProfile } from "@/stores/admin-session";
|
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 type { AdminBetProviderRow } from "@/types/api/admin-bet-provider";
|
||||||
import { DrawPublishDialog } from "@/modules/draws/draw-publish-dialog";
|
import { DrawPublishDialog } from "@/modules/draws/draw-publish-dialog";
|
||||||
import { useDrawDetail } from "@/modules/draws/draw-detail-context";
|
import { useDrawDetail } from "@/modules/draws/draw-detail-context";
|
||||||
|
|
||||||
@@ -69,6 +78,9 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [savingManual, setSavingManual] = useState(false);
|
const [savingManual, setSavingManual] = useState(false);
|
||||||
|
const [providersLoading, setProvidersLoading] = useState(false);
|
||||||
|
const [betProviders, setBetProviders] = useState<AdminBetProviderRow[]>([]);
|
||||||
|
const [manualProviderCode, setManualProviderCode] = useState("");
|
||||||
const [discardingBatchId, setDiscardingBatchId] = useState<number | null>(null);
|
const [discardingBatchId, setDiscardingBatchId] = useState<number | null>(null);
|
||||||
const [manualOpen, setManualOpen] = useState(false);
|
const [manualOpen, setManualOpen] = useState(false);
|
||||||
const [publishBatch, setPublishBatch] = useState<AdminDrawBatchRow | null>(null);
|
const [publishBatch, setPublishBatch] = useState<AdminDrawBatchRow | null>(null);
|
||||||
@@ -97,6 +109,19 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
void load();
|
void load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
setProvidersLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getAdminBetProviders();
|
||||||
|
setBetProviders(res.items);
|
||||||
|
setManualProviderCode((current) => current || res.items.find((item) => item.is_default)?.code || res.items[0]?.code || "");
|
||||||
|
} catch {
|
||||||
|
setBetProviders([]);
|
||||||
|
} finally {
|
||||||
|
setProvidersLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const pending = useMemo(() => data?.batches.filter((b) => b.status === "pending_review") ?? [], [data]);
|
const pending = useMemo(() => data?.batches.filter((b) => b.status === "pending_review") ?? [], [data]);
|
||||||
|
|
||||||
function fillRandomManualNumbers(): void {
|
function fillRandomManualNumbers(): void {
|
||||||
@@ -129,6 +154,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
setSavingManual(true);
|
setSavingManual(true);
|
||||||
try {
|
try {
|
||||||
const res = await postAdminCreateManualResultBatch(idNum, {
|
const res = await postAdminCreateManualResultBatch(idNum, {
|
||||||
|
provider_code: manualProviderCode || undefined,
|
||||||
items: RESULT_SLOTS.map((slot, i) => ({
|
items: RESULT_SLOTS.map((slot, i) => ({
|
||||||
prize_type: slot.prize_type,
|
prize_type: slot.prize_type,
|
||||||
prize_index: slot.prize_index,
|
prize_index: slot.prize_index,
|
||||||
@@ -177,6 +203,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t("version", { version: "" }).replace(" v", "").trim()}</TableHead>
|
<TableHead>{t("version", { version: "" }).replace(" v", "").trim()}</TableHead>
|
||||||
|
<TableHead>{t("provider")}</TableHead>
|
||||||
<TableHead>{t("numberCount")}</TableHead>
|
<TableHead>{t("numberCount")}</TableHead>
|
||||||
<TableHead className="w-14 text-center">{t("table.actions", { ns: "common" })}</TableHead>
|
<TableHead className="w-14 text-center">{t("table.actions", { ns: "common" })}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -185,6 +212,9 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
{pending.map((b) => (
|
{pending.map((b) => (
|
||||||
<TableRow key={b.id}>
|
<TableRow key={b.id}>
|
||||||
<TableCell>v{b.result_version}</TableCell>
|
<TableCell>v{b.result_version}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
{b.provider_name || b.provider_code || "—"}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="tabular-nums">{b.items.length}</TableCell>
|
<TableCell className="tabular-nums">{b.items.length}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{canManageDraw ? (
|
{canManageDraw ? (
|
||||||
@@ -241,6 +271,28 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
</button>
|
</button>
|
||||||
{manualOpen ? (
|
{manualOpen ? (
|
||||||
<div className="space-y-3 border-t border-border/60 px-3 py-3">
|
<div className="space-y-3 border-t border-border/60 px-3 py-3">
|
||||||
|
<div className="max-w-xs space-y-1.5">
|
||||||
|
<span className="text-xs text-muted-foreground">{t("provider")}</span>
|
||||||
|
<Select
|
||||||
|
modal={false}
|
||||||
|
value={manualProviderCode}
|
||||||
|
disabled={savingManual || providersLoading || betProviders.length === 0}
|
||||||
|
onValueChange={(value) => setManualProviderCode(String(value ?? ""))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-full">
|
||||||
|
<SelectValue>
|
||||||
|
{betProviders.find((item) => item.code === manualProviderCode)?.name ?? manualProviderCode}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent align="start" sideOffset={6}>
|
||||||
|
{betProviders.map((provider) => (
|
||||||
|
<SelectItem key={provider.code} value={provider.code}>
|
||||||
|
{provider.name} ({provider.code})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{RESULT_SLOTS.map((slot, i) => (
|
{RESULT_SLOTS.map((slot, i) => (
|
||||||
<label key={`${slot.prize_type}-${slot.prize_index}`} className="space-y-1">
|
<label key={`${slot.prize_type}-${slot.prize_index}`} className="space-y-1">
|
||||||
@@ -288,7 +340,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8"
|
className="h-8"
|
||||||
disabled={savingManual || !["closed", "review"].includes(data.draw_status)}
|
disabled={savingManual || !manualProviderCode || !["closed", "review"].includes(data.draw_status)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
requestConfirm({
|
requestConfirm({
|
||||||
title: t("confirm.saveManualDraftTitle"),
|
title: t("confirm.saveManualDraftTitle"),
|
||||||
@@ -320,4 +372,4 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ type FormState = {
|
|||||||
admin_password: string;
|
admin_password: string;
|
||||||
admin_email: string;
|
admin_email: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
settlement_timezone: string;
|
||||||
status: number;
|
status: number;
|
||||||
wallet_api_url: string;
|
wallet_api_url: string;
|
||||||
wallet_debit_path: string;
|
wallet_debit_path: string;
|
||||||
@@ -139,6 +140,7 @@ const EMPTY_FORM: FormState = {
|
|||||||
admin_password: "",
|
admin_password: "",
|
||||||
admin_email: "",
|
admin_email: "",
|
||||||
currency_code: "NPR",
|
currency_code: "NPR",
|
||||||
|
settlement_timezone: "UTC",
|
||||||
status: 1,
|
status: 1,
|
||||||
wallet_api_url: "",
|
wallet_api_url: "",
|
||||||
wallet_debit_path: "/wallet/debit-for-lottery",
|
wallet_debit_path: "/wallet/debit-for-lottery",
|
||||||
@@ -170,6 +172,7 @@ function rowToForm(row: AdminIntegrationSiteDetail): FormState {
|
|||||||
admin_password: "",
|
admin_password: "",
|
||||||
admin_email: "",
|
admin_email: "",
|
||||||
currency_code: row.currency_code,
|
currency_code: row.currency_code,
|
||||||
|
settlement_timezone: row.settlement_timezone || "UTC",
|
||||||
status: row.status,
|
status: row.status,
|
||||||
wallet_api_url: row.wallet_api_url ?? "",
|
wallet_api_url: row.wallet_api_url ?? "",
|
||||||
wallet_debit_path: row.wallet_debit_path ?? "/wallet/debit-for-lottery",
|
wallet_debit_path: row.wallet_debit_path ?? "/wallet/debit-for-lottery",
|
||||||
@@ -191,6 +194,7 @@ function formToPayload(
|
|||||||
const base = {
|
const base = {
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
currency_code: form.currency_code.trim() || "NPR",
|
currency_code: form.currency_code.trim() || "NPR",
|
||||||
|
settlement_timezone: form.settlement_timezone.trim() || "UTC",
|
||||||
status: form.status,
|
status: form.status,
|
||||||
wallet_api_url: form.wallet_api_url.trim() || null,
|
wallet_api_url: form.wallet_api_url.trim() || null,
|
||||||
wallet_debit_path: form.wallet_debit_path.trim(),
|
wallet_debit_path: form.wallet_debit_path.trim(),
|
||||||
@@ -795,7 +799,7 @@ export function IntegrationSitesConsole({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="is-currency">{t("integrationSites.fields.currency")}</Label>
|
<Label htmlFor="is-currency">{t("integrationSites.fields.currency")}</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -805,6 +809,21 @@ export function IntegrationSitesConsole({
|
|||||||
onChange={(e) => updateForm("currency_code", e.target.value)}
|
onChange={(e) => updateForm("currency_code", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="is-settlement-timezone">
|
||||||
|
{t("integrationSites.fields.settlementTimezone", {
|
||||||
|
defaultValue: "结算时区",
|
||||||
|
})}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="is-settlement-timezone"
|
||||||
|
value={form.settlement_timezone}
|
||||||
|
placeholder={t("integrationSites.placeholders.settlementTimezone", {
|
||||||
|
defaultValue: "例如 Asia/Kathmandu",
|
||||||
|
})}
|
||||||
|
onChange={(e) => updateForm("settlement_timezone", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="is-status">{t("integrationSites.fields.status")}</Label>
|
<Label htmlFor="is-status">{t("integrationSites.fields.status")}</Label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function settlementCenterScopeHref(
|
|||||||
|
|
||||||
export function settlementPeriodViewHref(
|
export function settlementPeriodViewHref(
|
||||||
periodId: number,
|
periodId: number,
|
||||||
view: SettlementPeriodView = "pending",
|
view: SettlementPeriodView = "bills",
|
||||||
adminSiteId?: number | null,
|
adminSiteId?: number | null,
|
||||||
billScope?: SettlementBillScopeFilter | null,
|
billScope?: SettlementBillScopeFilter | null,
|
||||||
): string {
|
): string {
|
||||||
@@ -88,11 +88,11 @@ export function parseSettlementCenterView(
|
|||||||
billScope: SettlementBillScopeFilter;
|
billScope: SettlementBillScopeFilter;
|
||||||
} {
|
} {
|
||||||
const normalizedView =
|
const normalizedView =
|
||||||
viewRaw === "reports" || viewRaw === "operations" ? "bills" : viewRaw;
|
viewRaw === "reports" || viewRaw === "operations" || viewRaw === "pending" ? "bills" : viewRaw;
|
||||||
const view =
|
const view =
|
||||||
normalizedView !== null && VALID_VIEWS.includes(normalizedView as SettlementPeriodView)
|
normalizedView !== null && VALID_VIEWS.includes(normalizedView as SettlementPeriodView)
|
||||||
? (normalizedView as SettlementPeriodView)
|
? (normalizedView as SettlementPeriodView)
|
||||||
: "pending";
|
: "bills";
|
||||||
|
|
||||||
const billScope =
|
const billScope =
|
||||||
scopeRaw !== null &&
|
scopeRaw !== null &&
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type SettlementCenterPeriodDetailProps = {
|
|||||||
billScope: SettlementBillScopeFilter;
|
billScope: SettlementBillScopeFilter;
|
||||||
adminSiteId: number;
|
adminSiteId: number;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
settlementTimezone: string;
|
||||||
canOperateBills: boolean;
|
canOperateBills: boolean;
|
||||||
canManagePeriods: boolean;
|
canManagePeriods: boolean;
|
||||||
boundAgentId?: number | null;
|
boundAgentId?: number | null;
|
||||||
@@ -34,6 +35,7 @@ export function SettlementCenterPeriodDetail({
|
|||||||
billScope,
|
billScope,
|
||||||
adminSiteId,
|
adminSiteId,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
|
settlementTimezone,
|
||||||
canOperateBills,
|
canOperateBills,
|
||||||
canManagePeriods,
|
canManagePeriods,
|
||||||
boundAgentId = null,
|
boundAgentId = null,
|
||||||
@@ -45,8 +47,7 @@ export function SettlementCenterPeriodDetail({
|
|||||||
const { t } = useTranslation("settlementCenter");
|
const { t } = useTranslation("settlementCenter");
|
||||||
|
|
||||||
const subViews: { key: SettlementPeriodView; label: string }[] = [
|
const subViews: { key: SettlementPeriodView; label: string }[] = [
|
||||||
{ key: "pending", label: t("nav.pendingWorkbench", { defaultValue: "待处理" }) },
|
{ key: "bills", label: t("nav.billWorkbench", { defaultValue: "账单处理" }) },
|
||||||
{ key: "bills", label: t("nav.allBills", { defaultValue: "全部账单" }) },
|
|
||||||
{ key: "ledger", label: t("nav.ledgerAudit", { defaultValue: "流水审计" }) },
|
{ key: "ledger", label: t("nav.ledgerAudit", { defaultValue: "流水审计" }) },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ export function SettlementCenterPeriodDetail({
|
|||||||
period={period}
|
period={period}
|
||||||
adminSiteId={adminSiteId}
|
adminSiteId={adminSiteId}
|
||||||
currencyCode={currencyCode}
|
currencyCode={currencyCode}
|
||||||
billScope={billScope}
|
settlementTimezone={settlementTimezone}
|
||||||
canManage={canManagePeriods}
|
canManage={canManagePeriods}
|
||||||
onPeriodClosed={onPeriodClosed}
|
onPeriodClosed={onPeriodClosed}
|
||||||
onReloadPeriods={onReloadPeriods}
|
onReloadPeriods={onReloadPeriods}
|
||||||
@@ -80,7 +81,6 @@ export function SettlementCenterPeriodDetail({
|
|||||||
adminSiteId={adminSiteId}
|
adminSiteId={adminSiteId}
|
||||||
currencyCode={currencyCode}
|
currencyCode={currencyCode}
|
||||||
periodFilter={period.id}
|
periodFilter={period.id}
|
||||||
mode="workbench"
|
|
||||||
onOpenBillDetail={onOpenBillDetail}
|
onOpenBillDetail={onOpenBillDetail}
|
||||||
refreshKey={refreshKey}
|
refreshKey={refreshKey}
|
||||||
canOperateBills={canOperateBills}
|
canOperateBills={canOperateBills}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
|
||||||
type SiteOption = { id: number; label: string; code: string; currency_code: string };
|
type SiteOption = { id: number; label: string; code: string; currency_code: string; settlement_timezone: string };
|
||||||
|
|
||||||
function siteOptionsFromProfile(profile: ReturnType<typeof useAdminProfile>): SiteOption[] {
|
function siteOptionsFromProfile(profile: ReturnType<typeof useAdminProfile>): SiteOption[] {
|
||||||
const byId = new Map<number, SiteOption>();
|
const byId = new Map<number, SiteOption>();
|
||||||
@@ -52,6 +52,7 @@ function siteOptionsFromProfile(profile: ReturnType<typeof useAdminProfile>): Si
|
|||||||
label: formatAdminSiteLabel(profile.site.name, profile.site.code),
|
label: formatAdminSiteLabel(profile.site.name, profile.site.code),
|
||||||
code: profile.site.code,
|
code: profile.site.code,
|
||||||
currency_code: "NPR",
|
currency_code: "NPR",
|
||||||
|
settlement_timezone: "UTC",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ function siteOptionsFromProfile(profile: ReturnType<typeof useAdminProfile>): Si
|
|||||||
label: formatAdminSiteLabel(site.name, site.code),
|
label: formatAdminSiteLabel(site.name, site.code),
|
||||||
code: site.code,
|
code: site.code,
|
||||||
currency_code: "NPR",
|
currency_code: "NPR",
|
||||||
|
settlement_timezone: "UTC",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +137,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
label,
|
label,
|
||||||
code: siteCode,
|
code: siteCode,
|
||||||
currency_code: match?.currency_code ?? "NPR",
|
currency_code: match?.currency_code ?? "NPR",
|
||||||
|
settlement_timezone: match?.settlement_timezone ?? "UTC",
|
||||||
}]);
|
}]);
|
||||||
setAdminSiteId(siteId);
|
setAdminSiteId(siteId);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -143,6 +146,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
label,
|
label,
|
||||||
code: siteCode,
|
code: siteCode,
|
||||||
currency_code: "NPR",
|
currency_code: "NPR",
|
||||||
|
settlement_timezone: "UTC",
|
||||||
}]);
|
}]);
|
||||||
setAdminSiteId(siteId);
|
setAdminSiteId(siteId);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -177,6 +181,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
label: formatAdminSiteLabel(site.name, site.code),
|
label: formatAdminSiteLabel(site.name, site.code),
|
||||||
code: site.code,
|
code: site.code,
|
||||||
currency_code: site.currency_code ?? "NPR",
|
currency_code: site.currency_code ?? "NPR",
|
||||||
|
settlement_timezone: site.settlement_timezone ?? "UTC",
|
||||||
}));
|
}));
|
||||||
setSiteOptions(options);
|
setSiteOptions(options);
|
||||||
setAdminSiteId((current) => {
|
setAdminSiteId((current) => {
|
||||||
@@ -206,6 +211,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
const selectedSite = siteOptions.find((s) => s.id === siteId) ?? null;
|
const selectedSite = siteOptions.find((s) => s.id === siteId) ?? null;
|
||||||
const siteLabel = selectedSite?.label ?? null;
|
const siteLabel = selectedSite?.label ?? null;
|
||||||
const currency = siteOptions.find((s) => s.id === siteId)?.currency_code ?? "NPR";
|
const currency = siteOptions.find((s) => s.id === siteId)?.currency_code ?? "NPR";
|
||||||
|
const settlementTimezone = siteOptions.find((s) => s.id === siteId)?.settlement_timezone ?? "UTC";
|
||||||
const filteredSites = siteKeyword.trim().toLowerCase()
|
const filteredSites = siteKeyword.trim().toLowerCase()
|
||||||
? siteOptions.filter((site) => site.label.toLowerCase().includes(siteKeyword.trim().toLowerCase()))
|
? siteOptions.filter((site) => site.label.toLowerCase().includes(siteKeyword.trim().toLowerCase()))
|
||||||
: siteOptions;
|
: siteOptions;
|
||||||
@@ -325,7 +331,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
const activePeriod =
|
const activePeriod =
|
||||||
activePeriodId !== null ? (periods.find((row) => row.id === activePeriodId) ?? null) : null;
|
activePeriodId !== null ? (periods.find((row) => row.id === activePeriodId) ?? null) : null;
|
||||||
|
|
||||||
const openPeriodView = (periodId: number, view: SettlementPeriodView = "pending"): void => {
|
const openPeriodView = (periodId: number, view: SettlementPeriodView = "bills"): void => {
|
||||||
const period = periods.find((row) => row.id === periodId);
|
const period = periods.find((row) => row.id === periodId);
|
||||||
const scope = preferredBillScopeForPeriod(period?.summary);
|
const scope = preferredBillScopeForPeriod(period?.summary);
|
||||||
router.push(settlementPeriodViewHref(periodId, view, siteId, scope));
|
router.push(settlementPeriodViewHref(periodId, view, siteId, scope));
|
||||||
@@ -432,8 +438,9 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
canManage={canManagePeriods}
|
canManage={canManagePeriods}
|
||||||
periods={periods}
|
periods={periods}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
onViewDetail={(id) => openPeriodView(id, "pending")}
|
onViewDetail={(id) => openPeriodView(id, "bills")}
|
||||||
onReloadPeriods={loadPeriods}
|
onReloadPeriods={loadPeriods}
|
||||||
|
settlementTimezone={settlementTimezone}
|
||||||
onPeriodOpened={() => {
|
onPeriodOpened={() => {
|
||||||
setRefreshKey((n) => n + 1);
|
setRefreshKey((n) => n + 1);
|
||||||
}}
|
}}
|
||||||
@@ -473,6 +480,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
|||||||
billScope={activeBillScope}
|
billScope={activeBillScope}
|
||||||
adminSiteId={siteId}
|
adminSiteId={siteId}
|
||||||
currencyCode={currency}
|
currencyCode={currency}
|
||||||
|
settlementTimezone={settlementTimezone}
|
||||||
canOperateBills={canOperateBills}
|
canOperateBills={canOperateBills}
|
||||||
canManagePeriods={canManagePeriods}
|
canManagePeriods={canManagePeriods}
|
||||||
boundAgentId={boundAgent?.id ?? null}
|
boundAgentId={boundAgent?.id ?? null}
|
||||||
|
|||||||
@@ -198,6 +198,29 @@ export function SettlementMainPanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statusScopeOptions: SettlementBillScopeFilter[] = [
|
||||||
|
"all",
|
||||||
|
"pending_confirm",
|
||||||
|
"awaiting_payment",
|
||||||
|
"settled",
|
||||||
|
"adjustment",
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusScopeLabel = (value: SettlementBillScopeFilter): string => {
|
||||||
|
switch (value) {
|
||||||
|
case "pending_confirm":
|
||||||
|
return t("billsPanel.category.pendingConfirm", { defaultValue: "待确认" });
|
||||||
|
case "awaiting_payment":
|
||||||
|
return t("billsPanel.category.awaitingPayment", { defaultValue: "待收付" });
|
||||||
|
case "settled":
|
||||||
|
return t("billStatus.settled", { defaultValue: "已结清" });
|
||||||
|
case "adjustment":
|
||||||
|
return t("billsPanel.filterAdjustment", { defaultValue: "调账 / 冲正" });
|
||||||
|
default:
|
||||||
|
return t("billsPanel.filterAll", { defaultValue: "全部状态" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const requestConfirmBill = (row: SettlementBillRow): void => {
|
const requestConfirmBill = (row: SettlementBillRow): void => {
|
||||||
requestConfirm({
|
requestConfirm({
|
||||||
title: t("billsPanel.confirmOneTitle", { defaultValue: "确认账单 #{{id}}?", id: row.id }),
|
title: t("billsPanel.confirmOneTitle", { defaultValue: "确认账单 #{{id}}?", id: row.id }),
|
||||||
@@ -358,12 +381,18 @@ export function SettlementMainPanel({
|
|||||||
<Select
|
<Select
|
||||||
modal={false}
|
modal={false}
|
||||||
value={draft.billType}
|
value={draft.billType}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) => {
|
||||||
|
const next = (v ?? "all") as BillTypeFilter;
|
||||||
setDraft((d) => ({
|
setDraft((d) => ({
|
||||||
...d,
|
...d,
|
||||||
billType: (v ?? "all") as BillTypeFilter,
|
billType: next,
|
||||||
}))
|
}));
|
||||||
}
|
setApplied((d) => ({
|
||||||
|
...d,
|
||||||
|
billType: next,
|
||||||
|
}));
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="sb-type" className="h-9 w-full sm:w-36">
|
<SelectTrigger id="sb-type" className="h-9 w-full sm:w-36">
|
||||||
<SelectValue>{() => billTypeLabel(draft.billType)}</SelectValue>
|
<SelectValue>{() => billTypeLabel(draft.billType)}</SelectValue>
|
||||||
@@ -378,6 +407,38 @@ export function SettlementMainPanel({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="admin-list-field">
|
||||||
|
<Label htmlFor="sb-status" className="sm:shrink-0">
|
||||||
|
{t("billsPanel.status", { defaultValue: "账单状态" })}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
modal={false}
|
||||||
|
value={draft.statusScope}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const next = (v ?? "all") as SettlementBillScopeFilter;
|
||||||
|
setDraft((d) => ({
|
||||||
|
...d,
|
||||||
|
statusScope: next,
|
||||||
|
}));
|
||||||
|
setApplied((d) => ({
|
||||||
|
...d,
|
||||||
|
statusScope: next,
|
||||||
|
}));
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="sb-status" className="h-9 w-full sm:w-36">
|
||||||
|
<SelectValue>{() => statusScopeLabel(draft.statusScope)}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{statusScopeOptions.map((value) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{statusScopeLabel(value)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
<div className="admin-list-actions">
|
<div className="admin-list-actions">
|
||||||
<Button type="button" size="sm" onClick={() => runSearch()}>
|
<Button type="button" size="sm" onClick={() => runSearch()}>
|
||||||
{t("billsPanel.searchBtn", { defaultValue: "搜索" })}
|
{t("billsPanel.searchBtn", { defaultValue: "搜索" })}
|
||||||
|
|||||||
@@ -22,39 +22,25 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
|
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
|
||||||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||||
import {
|
import { settlementCenterListHref } from "@/modules/settlement/settlement-center-nav";
|
||||||
settlementCenterListHref,
|
|
||||||
settlementPeriodViewHref,
|
|
||||||
type SettlementBillScopeFilter,
|
|
||||||
} from "@/modules/settlement/settlement-center-nav";
|
|
||||||
import { settlementPeriodStatusLabel } from "@/modules/settlement/settlement-status-label";
|
import { settlementPeriodStatusLabel } from "@/modules/settlement/settlement-status-label";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type SettlementPeriodHeaderProps = {
|
type SettlementPeriodHeaderProps = {
|
||||||
period: SettlementPeriodRow;
|
period: SettlementPeriodRow;
|
||||||
adminSiteId: number;
|
adminSiteId: number;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
billScope: SettlementBillScopeFilter;
|
settlementTimezone?: string;
|
||||||
canManage: boolean;
|
canManage: boolean;
|
||||||
onPeriodClosed?: (result: SettlementPeriodCloseResult) => void;
|
onPeriodClosed?: (result: SettlementPeriodCloseResult) => void;
|
||||||
onReloadPeriods?: () => Promise<unknown>;
|
onReloadPeriods?: () => Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function scopeChipClass(active: boolean): string {
|
|
||||||
return cn(
|
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium transition-colors",
|
|
||||||
active
|
|
||||||
? "border-primary/40 bg-primary/10 text-primary"
|
|
||||||
: "border-border/70 bg-muted/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettlementPeriodHeader({
|
export function SettlementPeriodHeader({
|
||||||
period,
|
period,
|
||||||
adminSiteId,
|
adminSiteId,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
billScope,
|
settlementTimezone = "UTC",
|
||||||
canManage,
|
canManage,
|
||||||
onPeriodClosed,
|
onPeriodClosed,
|
||||||
onReloadPeriods,
|
onReloadPeriods,
|
||||||
@@ -70,23 +56,6 @@ export function SettlementPeriodHeader({
|
|||||||
const unsettledCount = period.pipeline?.unsettled_ticket_count ?? 0;
|
const unsettledCount = period.pipeline?.unsettled_ticket_count ?? 0;
|
||||||
const isOpen = period.status === "open";
|
const isOpen = period.status === "open";
|
||||||
|
|
||||||
const scopeChips: { key: SettlementBillScopeFilter; label: string; count?: number }[] = [
|
|
||||||
{
|
|
||||||
key: "all",
|
|
||||||
label: t("billsPanel.filterAll", { defaultValue: "全部" }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "pending_confirm",
|
|
||||||
label: t("billsPanel.category.pendingConfirm", { defaultValue: "待确认" }),
|
|
||||||
count: pendingConfirm,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "awaiting_payment",
|
|
||||||
label: t("billsPanel.category.awaitingPayment", { defaultValue: "待收付" }),
|
|
||||||
count: awaitingPayment,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function confirmClose(): Promise<void> {
|
async function confirmClose(): Promise<void> {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
@@ -129,7 +98,7 @@ export function SettlementPeriodHeader({
|
|||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<h2 className="text-base font-semibold tracking-tight">
|
<h2 className="text-base font-semibold tracking-tight">
|
||||||
{formatSettlementPeriodSpan(period.period_start, period.period_end)}
|
{formatSettlementPeriodSpan(period.period_start, period.period_end, settlementTimezone)}
|
||||||
</h2>
|
</h2>
|
||||||
<AdminStatusBadge status={period.status}>
|
<AdminStatusBadge status={period.status}>
|
||||||
{settlementPeriodStatusLabel(period.status, t)}
|
{settlementPeriodStatusLabel(period.status, t)}
|
||||||
@@ -144,20 +113,17 @@ export function SettlementPeriodHeader({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isOpen ? (
|
{!isOpen ? (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||||
{scopeChips.map((chip) => {
|
<span>
|
||||||
const active = billScope === chip.key;
|
{t("overview.pendingConfirm", { defaultValue: "待确认" })}{" "}
|
||||||
const href = settlementPeriodViewHref(period.id, "bills", adminSiteId, chip.key);
|
<span className="font-medium tabular-nums text-foreground">{pendingConfirm}</span>
|
||||||
|
</span>
|
||||||
return (
|
<span>
|
||||||
<Link key={chip.key} href={href} className={scopeChipClass(active)}>
|
{t("overview.awaitingPayment", { defaultValue: "待收付" })}{" "}
|
||||||
{chip.label}
|
<span className="font-medium tabular-nums text-foreground">{awaitingPayment}</span>
|
||||||
{chip.count != null && chip.count > 0 ? ` ${chip.count}` : null}
|
</span>
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{totalUnpaid > 0 ? (
|
{totalUnpaid > 0 ? (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span>
|
||||||
{t("overview.totalUnpaid", { defaultValue: "未结合计" })}{" "}
|
{t("overview.totalUnpaid", { defaultValue: "未结合计" })}{" "}
|
||||||
<span className="font-medium tabular-nums text-foreground">
|
<span className="font-medium tabular-nums text-foreground">
|
||||||
{formatDashboardMoneyMinor(totalUnpaid, currencyCode)}
|
{formatDashboardMoneyMinor(totalUnpaid, currencyCode)}
|
||||||
@@ -184,7 +150,7 @@ export function SettlementPeriodHeader({
|
|||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("period.closeDialogDesc", {
|
{t("period.closeDialogDesc", {
|
||||||
defaultValue: "将汇总 {{range}} 内的流水并生成账单。",
|
defaultValue: "将汇总 {{range}} 内的流水并生成账单。",
|
||||||
range: formatSettlementPeriodSpan(period.period_start, period.period_end),
|
range: formatSettlementPeriodSpan(period.period_start, period.period_end, settlementTimezone),
|
||||||
})}
|
})}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -212,7 +178,7 @@ export function SettlementPeriodHeader({
|
|||||||
<Button type="button" variant="outline" disabled={busy} onClick={() => setCloseDialogOpen(false)}>
|
<Button type="button" variant="outline" disabled={busy} onClick={() => setCloseDialogOpen(false)}>
|
||||||
{t("common:cancel", { defaultValue: "取消" })}
|
{t("common:cancel", { defaultValue: "取消" })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" disabled={busy} onClick={() => void confirmClose()}>
|
<Button type="button" disabled={busy || unsettledCount > 0} onClick={() => void confirmClose()}>
|
||||||
{t("period.closeDialogConfirm", { defaultValue: "确认关账" })}
|
{t("period.closeDialogConfirm", { defaultValue: "确认关账" })}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -220,4 +186,4 @@ export function SettlementPeriodHeader({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
formatSettlementPeriodSpan,
|
formatSettlementPeriodSpan,
|
||||||
isSettlementLocalDateRangeValid,
|
isSettlementLocalDateRangeValid,
|
||||||
localDateRangeToUtcPeriodBounds,
|
|
||||||
settlementRangeOverlapsOccupiedDates,
|
settlementRangeOverlapsOccupiedDates,
|
||||||
utcStorageDateToLocalFormYmd,
|
|
||||||
utcStorageDatesToLocalMarks,
|
|
||||||
} from "@/lib/agent-settlement-period-range";
|
} from "@/lib/agent-settlement-period-range";
|
||||||
import { settlementPeriodStatusLabel } from "@/modules/settlement/settlement-status-label";
|
import { settlementPeriodStatusLabel } from "@/modules/settlement/settlement-status-label";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -59,6 +56,7 @@ type SettlementPeriodWorkbenchProps = {
|
|||||||
onReloadPeriods: () => Promise<SettlementPeriodRow[]>;
|
onReloadPeriods: () => Promise<SettlementPeriodRow[]>;
|
||||||
onPeriodOpened?: (periodId: number) => void;
|
onPeriodOpened?: (periodId: number) => void;
|
||||||
onPeriodClosed?: (result: SettlementPeriodCloseResult) => void;
|
onPeriodClosed?: (result: SettlementPeriodCloseResult) => void;
|
||||||
|
settlementTimezone?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SettlementPeriodWorkbench({
|
export function SettlementPeriodWorkbench({
|
||||||
@@ -71,6 +69,7 @@ export function SettlementPeriodWorkbench({
|
|||||||
onReloadPeriods,
|
onReloadPeriods,
|
||||||
onPeriodOpened,
|
onPeriodOpened,
|
||||||
onPeriodClosed,
|
onPeriodClosed,
|
||||||
|
settlementTimezone = "UTC",
|
||||||
}: SettlementPeriodWorkbenchProps): React.ReactElement {
|
}: SettlementPeriodWorkbenchProps): React.ReactElement {
|
||||||
const { t } = useTranslation(["settlementCenter", "agents", "common"]);
|
const { t } = useTranslation(["settlementCenter", "agents", "common"]);
|
||||||
const [draftStatus, setDraftStatus] = useState<PeriodStatusFilter>("all");
|
const [draftStatus, setDraftStatus] = useState<PeriodStatusFilter>("all");
|
||||||
@@ -122,14 +121,15 @@ export function SettlementPeriodWorkbench({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
occupiedPeriod: utcStorageDatesToLocalMarks(openHints.occupied_period_dates),
|
occupiedPeriod: openHints.occupied_period_dates,
|
||||||
pendingActivity: utcStorageDatesToLocalMarks(openHints.pending_activity_dates),
|
pendingActivity: openHints.pending_activity_dates,
|
||||||
unpaidBill: utcStorageDatesToLocalMarks(openHints.unpaid_bill_dates),
|
orphanActivity: openHints.orphan_activity_dates ?? [],
|
||||||
|
unpaidBill: openHints.unpaid_bill_dates,
|
||||||
};
|
};
|
||||||
}, [openHints]);
|
}, [openHints]);
|
||||||
|
|
||||||
const occupiedLocalDates = useMemo(
|
const occupiedLocalDates = useMemo(
|
||||||
() => utcStorageDatesToLocalMarks(openHints?.occupied_period_dates ?? []),
|
() => openHints?.occupied_period_dates ?? [],
|
||||||
[openHints],
|
[openHints],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -171,13 +171,12 @@ export function SettlementPeriodWorkbench({
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bounds = localDateRangeToUtcPeriodBounds(startYmd, endYmd);
|
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const row = await postSettlementPeriod({
|
const row = await postSettlementPeriod({
|
||||||
admin_site_id: adminSiteId,
|
admin_site_id: adminSiteId,
|
||||||
period_start: bounds.period_start,
|
period_start: startYmd,
|
||||||
period_end: bounds.period_end,
|
period_end: endYmd,
|
||||||
});
|
});
|
||||||
await onReloadPeriods();
|
await onReloadPeriods();
|
||||||
onPeriodOpened?.(row.id);
|
onPeriodOpened?.(row.id);
|
||||||
@@ -213,9 +212,9 @@ export function SettlementPeriodWorkbench({
|
|||||||
setCustomStart("");
|
setCustomStart("");
|
||||||
setCustomEnd("");
|
setCustomEnd("");
|
||||||
if (hints.suggested_start && hints.suggested_end) {
|
if (hints.suggested_start && hints.suggested_end) {
|
||||||
const from = utcStorageDateToLocalFormYmd(hints.suggested_start);
|
const from = hints.suggested_start;
|
||||||
const to = utcStorageDateToLocalFormYmd(hints.suggested_end);
|
const to = hints.suggested_end;
|
||||||
const occupied = utcStorageDatesToLocalMarks(hints.occupied_period_dates);
|
const occupied = hints.occupied_period_dates;
|
||||||
if (!settlementRangeOverlapsOccupiedDates(from, to, occupied)) {
|
if (!settlementRangeOverlapsOccupiedDates(from, to, occupied)) {
|
||||||
setCustomStart(from);
|
setCustomStart(from);
|
||||||
setCustomEnd(to);
|
setCustomEnd(to);
|
||||||
@@ -423,6 +422,7 @@ export function SettlementPeriodWorkbench({
|
|||||||
canManage={canManage}
|
canManage={canManage}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
currencyCode={currencyCode}
|
currencyCode={currencyCode}
|
||||||
|
settlementTimezone={settlementTimezone}
|
||||||
emptyMessage={tableEmptyMessage}
|
emptyMessage={tableEmptyMessage}
|
||||||
onViewDetail={onViewDetail}
|
onViewDetail={onViewDetail}
|
||||||
onRequestClose={requestClose}
|
onRequestClose={requestClose}
|
||||||
@@ -450,11 +450,17 @@ export function SettlementPeriodWorkbench({
|
|||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("agents:settlementPeriods.openDesc", {
|
{t("agents:settlementPeriods.openDesc", {
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"选择账期起止日期;灰色删除线为已有账期不可再开,琥珀点为待入账,右上角红点为未结清。",
|
"选择账期起止日期;灰色删除线为已有账期不可再开,琥珀点为待入账,紫点为已关账漏账,右上角红点为未结清。",
|
||||||
})}
|
})}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{t("agents:settlementPeriods.timezoneHint", {
|
||||||
|
defaultValue: "账期日期按站点时区 {{tz}} 计算。",
|
||||||
|
tz: openHints?.settlement_timezone ?? settlementTimezone,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
<AdminDateRangeField
|
<AdminDateRangeField
|
||||||
id="sp-dialog-range"
|
id="sp-dialog-range"
|
||||||
label={t("agents:settlementPeriods.rangeLabel", { defaultValue: "账期范围" })}
|
label={t("agents:settlementPeriods.rangeLabel", { defaultValue: "账期范围" })}
|
||||||
@@ -485,6 +491,13 @@ export function SettlementPeriodWorkbench({
|
|||||||
/>
|
/>
|
||||||
{t("agents:settlementPeriods.markerPending", { defaultValue: "待入账流水" })}
|
{t("agents:settlementPeriods.markerPending", { defaultValue: "待入账流水" })}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="inline-block size-1.5 rounded-full bg-violet-600"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
{t("agents:settlementPeriods.markerOrphan", { defaultValue: "已关账漏账" })}
|
||||||
|
</span>
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<span
|
<span
|
||||||
className="relative inline-block size-3 rounded ring-1 ring-border"
|
className="relative inline-block size-3 rounded ring-1 ring-border"
|
||||||
@@ -542,6 +555,7 @@ export function SettlementPeriodWorkbench({
|
|||||||
range: formatSettlementPeriodSpan(
|
range: formatSettlementPeriodSpan(
|
||||||
closeTarget.period_start,
|
closeTarget.period_start,
|
||||||
closeTarget.period_end,
|
closeTarget.period_end,
|
||||||
|
settlementTimezone,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
@@ -578,7 +592,7 @@ export function SettlementPeriodWorkbench({
|
|||||||
<Button type="button" variant="outline" disabled={busy} onClick={() => setCloseDialogOpen(false)}>
|
<Button type="button" variant="outline" disabled={busy} onClick={() => setCloseDialogOpen(false)}>
|
||||||
{t("common:cancel", { defaultValue: "取消" })}
|
{t("common:cancel", { defaultValue: "取消" })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" disabled={busy} onClick={() => void confirmClose()}>
|
<Button type="button" disabled={busy || unsettledCount > 0} onClick={() => void confirmClose()}>
|
||||||
{t("period.closeDialogConfirm", { defaultValue: "确认关账" })}
|
{t("period.closeDialogConfirm", { defaultValue: "确认关账" })}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type SettlementPeriodsTableProps = {
|
|||||||
canManage: boolean;
|
canManage: boolean;
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
currencyCode?: string;
|
currencyCode?: string;
|
||||||
|
settlementTimezone?: string;
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
onViewDetail: (periodId: number) => void;
|
onViewDetail: (periodId: number) => void;
|
||||||
onRequestClose: (row: SettlementPeriodRow) => void;
|
onRequestClose: (row: SettlementPeriodRow) => void;
|
||||||
@@ -41,6 +42,7 @@ export function SettlementPeriodsTable({
|
|||||||
canManage,
|
canManage,
|
||||||
busy = false,
|
busy = false,
|
||||||
currencyCode = "NPR",
|
currencyCode = "NPR",
|
||||||
|
settlementTimezone = "UTC",
|
||||||
emptyMessage,
|
emptyMessage,
|
||||||
onViewDetail,
|
onViewDetail,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
@@ -105,7 +107,7 @@ export function SettlementPeriodsTable({
|
|||||||
return (
|
return (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
<TableCell className="font-medium whitespace-nowrap">
|
<TableCell className="font-medium whitespace-nowrap">
|
||||||
{formatSettlementPeriodSpan(row.period_start, row.period_end)}
|
{formatSettlementPeriodSpan(row.period_start, row.period_end, settlementTimezone)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={row.status}>
|
<AdminStatusBadge status={row.status}>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAsyncEffect } from "@/hooks/use-async-effect";
|
|||||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||||
|
|
||||||
import { getAdminTicketItems } from "@/api/admin-tickets";
|
import { getAdminTicketItems } from "@/api/admin-tickets";
|
||||||
|
import { getAdminBetProviders } from "@/api/admin-bet-providers";
|
||||||
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
|
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
|
||||||
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||||
@@ -26,6 +27,13 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
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 {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -41,6 +49,7 @@ import { PlayerFundingModeBadge } from "@/components/admin/player-funding-badges
|
|||||||
import { adminPlayerDetailPath } from "@/lib/admin-player-paths";
|
import { adminPlayerDetailPath } from "@/lib/admin-player-paths";
|
||||||
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 type { AdminBetProviderRow } from "@/types/api/admin-bet-provider";
|
||||||
import { ChevronDown, Eye } from "lucide-react";
|
import { ChevronDown, Eye } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -78,6 +87,8 @@ const emptyTicketFilters: TicketFilters = {
|
|||||||
statuses: [],
|
statuses: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ALL_PROVIDERS_VALUE = "__all__";
|
||||||
|
|
||||||
type TicketTranslateFn = (
|
type TicketTranslateFn = (
|
||||||
key: string,
|
key: string,
|
||||||
options?: { count?: number },
|
options?: { count?: number },
|
||||||
@@ -138,16 +149,20 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
const playerIdFromUrl = (searchParams.get("player_id") ?? "").trim();
|
const playerIdFromUrl = (searchParams.get("player_id") ?? "").trim();
|
||||||
const drawNoFromUrl = (searchParams.get("draw_no") ?? "").trim();
|
const drawNoFromUrl = (searchParams.get("draw_no") ?? "").trim();
|
||||||
const numberKeywordFromUrl = (searchParams.get("number") ?? "").trim();
|
const numberKeywordFromUrl = (searchParams.get("number") ?? "").trim();
|
||||||
|
const providerCodeFromUrl = (searchParams.get("provider_code") ?? "").trim().toUpperCase();
|
||||||
const initialFilters: TicketFilters = {
|
const initialFilters: TicketFilters = {
|
||||||
...emptyTicketFilters,
|
...emptyTicketFilters,
|
||||||
playerQuery: playerIdFromUrl,
|
playerQuery: playerIdFromUrl,
|
||||||
drawNo: drawNoFromUrl,
|
drawNo: drawNoFromUrl,
|
||||||
|
providerCode: providerCodeFromUrl,
|
||||||
numberKeyword: numberKeywordFromUrl,
|
numberKeyword: numberKeywordFromUrl,
|
||||||
};
|
};
|
||||||
const [draft, setDraft] = useState<TicketFilters>(initialFilters);
|
const [draft, setDraft] = useState<TicketFilters>(initialFilters);
|
||||||
const [applied, setApplied] = useState<TicketFilters>(initialFilters);
|
const [applied, setApplied] = useState<TicketFilters>(initialFilters);
|
||||||
const [data, setData] = useState<AdminTicketItemsData | null>(null);
|
const [data, setData] = useState<AdminTicketItemsData | null>(null);
|
||||||
|
const [betProviders, setBetProviders] = useState<AdminBetProviderRow[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [providersLoading, setProvidersLoading] = useState(false);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(10);
|
const [perPage, setPerPage] = useState(10);
|
||||||
@@ -189,6 +204,18 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
void load();
|
void load();
|
||||||
}, [applied, page, perPage]);
|
}, [applied, page, perPage]);
|
||||||
|
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
setProvidersLoading(true);
|
||||||
|
try {
|
||||||
|
const d = await getAdminBetProviders();
|
||||||
|
setBetProviders(d.items);
|
||||||
|
} catch {
|
||||||
|
setBetProviders([]);
|
||||||
|
} finally {
|
||||||
|
setProvidersLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const runSearch = () => {
|
const runSearch = () => {
|
||||||
setErr(null);
|
setErr(null);
|
||||||
setApplied({
|
setApplied({
|
||||||
@@ -271,21 +298,35 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TicketFilterField>
|
</TicketFilterField>
|
||||||
<TicketFilterField id="pt-provider" label={t("provider", { defaultValue: "开注商" })}>
|
<TicketFilterField id="pt-provider" label={t("provider")}>
|
||||||
<Input
|
<Select
|
||||||
id="pt-provider"
|
modal={false}
|
||||||
className="h-8 w-full font-mono uppercase"
|
value={draft.providerCode || ALL_PROVIDERS_VALUE}
|
||||||
placeholder="SG / MY / TH"
|
disabled={providersLoading}
|
||||||
value={draft.providerCode}
|
onValueChange={(value) => {
|
||||||
onChange={(e) =>
|
const nextProviderCode = String(value ?? "");
|
||||||
setDraft((current) => ({ ...current, providerCode: e.target.value.toUpperCase() }))
|
setDraft((current) => ({
|
||||||
}
|
...current,
|
||||||
onKeyDown={(e) => {
|
providerCode: nextProviderCode === ALL_PROVIDERS_VALUE ? "" : nextProviderCode,
|
||||||
if (e.key === "Enter") {
|
}));
|
||||||
runSearch();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<SelectTrigger id="pt-provider" className="h-8 w-full">
|
||||||
|
<SelectValue>
|
||||||
|
{draft.providerCode
|
||||||
|
? (betProviders.find((item) => item.code === draft.providerCode)?.name ?? draft.providerCode)
|
||||||
|
: t("allProviders")}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent align="start" sideOffset={6}>
|
||||||
|
<SelectItem value={ALL_PROVIDERS_VALUE}>{t("allProviders")}</SelectItem>
|
||||||
|
{betProviders.map((provider) => (
|
||||||
|
<SelectItem key={provider.code} value={provider.code}>
|
||||||
|
{provider.name} ({provider.code})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</TicketFilterField>
|
</TicketFilterField>
|
||||||
<TicketFilterField id="pt-date-range" label={t("placedDateRange")}>
|
<TicketFilterField id="pt-date-range" label={t("placedDateRange")}>
|
||||||
<AdminDateRangeField
|
<AdminDateRangeField
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export type AdminDrawBatchItemRow = {
|
|||||||
|
|
||||||
export type AdminDrawBatchRow = {
|
export type AdminDrawBatchRow = {
|
||||||
id: number;
|
id: number;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
result_version: number;
|
result_version: number;
|
||||||
source_type?: string;
|
source_type?: string;
|
||||||
rng_seed_hash?: string | null;
|
rng_seed_hash?: string | null;
|
||||||
@@ -123,6 +125,8 @@ export type AdminDrawBatchesData = {
|
|||||||
export type AdminDrawPublishResponse = {
|
export type AdminDrawPublishResponse = {
|
||||||
draw_no: string;
|
draw_no: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
result_version: number;
|
result_version: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,6 +146,7 @@ export type AdminDrawPlanGenerateResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AdminDrawManualBatchPayload = {
|
export type AdminDrawManualBatchPayload = {
|
||||||
|
provider_code?: string;
|
||||||
items: Array<{
|
items: Array<{
|
||||||
prize_type: string;
|
prize_type: string;
|
||||||
prize_index: number;
|
prize_index: number;
|
||||||
@@ -154,9 +159,20 @@ export type AdminDrawBatchCreateResponse = {
|
|||||||
status: string;
|
status: string;
|
||||||
batch: {
|
batch: {
|
||||||
id: number;
|
id: number;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
result_version: number;
|
result_version: number;
|
||||||
source_type: string;
|
source_type: string;
|
||||||
status: string;
|
status: string;
|
||||||
items_count: number;
|
items_count: number;
|
||||||
};
|
};
|
||||||
|
batches?: Array<{
|
||||||
|
id: number;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
|
result_version: number;
|
||||||
|
source_type: string;
|
||||||
|
status: string;
|
||||||
|
items_count: number;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type AdminIntegrationSiteRow = {
|
|||||||
name: string;
|
name: string;
|
||||||
has_line_root: boolean;
|
has_line_root: boolean;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
settlement_timezone: string;
|
||||||
status: number;
|
status: number;
|
||||||
wallet_api_url: string | null;
|
wallet_api_url: string | null;
|
||||||
lottery_h5_base_url: string | null;
|
lottery_h5_base_url: string | null;
|
||||||
@@ -48,6 +49,7 @@ export type AdminIntegrationSiteCreatePayload = {
|
|||||||
name: string;
|
name: string;
|
||||||
admin_account: AdminIntegrationSiteAdminAccountPayload;
|
admin_account: AdminIntegrationSiteAdminAccountPayload;
|
||||||
currency_code?: string;
|
currency_code?: string;
|
||||||
|
settlement_timezone?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
wallet_api_url?: string | null;
|
wallet_api_url?: string | null;
|
||||||
wallet_debit_path?: string;
|
wallet_debit_path?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user