feat(settlement): 支持结算时区与已关账漏账流水显示
Some checks failed
lotteryadmin CI / build (push) Has been cancelled

- 為結算期間相關接口與前端組件新增 settlement_timezone 字段
- 在账期范围格式化函数中支持传入时区参数显示当地日期
- 在开账面板和账期视图中增加漏账流水(orphanActivity)高亮显示
- 统一调整账期视图默认子页面为“账单处理”而非“待处理”
- 在开奖管理中添加开奖提供商(provider)信息展示和选择功能
- 结算中心增加账单状态筛选项支持待确认、待收付、已结清、调账/冲正
- 移除部分账期页状态筛选标签,改用文本方式显示待确认与待收付数量
- 集成站点管理中新增结算时区字段,支持编辑与保存
- 多语言新增与更新与结算时区和漏账流水相关的文案提示及翻译
This commit is contained in:
2026-07-09 15:48:03 +08:00
parent 330063abf6
commit 1164d9d728
33 changed files with 359 additions and 111 deletions

View File

@@ -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[];
}; };

View File

@@ -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-['']",
}} }}

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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."

View File

@@ -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",

View File

@@ -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": "चयन गरिएको दायरा अवस्थित अवधिसँग ओभरल्याप हुन्छ। कृपया खैरो स्ट्राइकथ्रु भएका मितिहरू नछान्नुहोस्।"
}, },

View File

@@ -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": "पुष्टि",

View File

@@ -100,6 +100,7 @@
"drawDetail": "ड्रअ विवरण", "drawDetail": "ड्रअ विवरण",
"drawNo": "ड्रअ नं.", "drawNo": "ड्रअ नं.",
"drawTime": "ड्रअ समय", "drawTime": "ड्रअ समय",
"provider": "Provider",
"editDraw": { "editDraw": {
"action": "सम्पादन", "action": "सम्पादन",
"description": "ड्रअ {{drawNo}} · स्थानीय समयक्षेत्र {{tz}} मा सम्पादन", "description": "ड्रअ {{drawNo}} · स्थानीय समयक्षेत्र {{tz}} मा सम्पादन",

View File

@@ -14,7 +14,7 @@
"closeDialogTitle": "अवधि बन्द गर्नुहोस्", "closeDialogTitle": "अवधि बन्द गर्नुहोस्",
"closeDialogDesc": "{{range}} भित्रको लेजर संक्षेप गरी बिल सिर्जना गर्नुहोस्।", "closeDialogDesc": "{{range}} भित्रको लेजर संक्षेप गरी बिल सिर्जना गर्नुहोस्।",
"closeDialogShare": "{{count}} लेजर प्रविष्टिहरू", "closeDialogShare": "{{count}} लेजर प्रविष्टिहरू",
"closeDialogUnsettled": "{{count}} टिकटहरू अझै नसेटल", "closeDialogUnsettled": "{{count}} टिकटहरू अझै नसेटल छन्। बन्द गर्नु अघि ड्र सेटलमेन्ट पूरा गर्नुहोस्।",
"closeDialogIrreversible": "पूर्ववत गर्न सकिँदैन। त्रुटि सच्याउन समायोजन वा रिभर्सल प्रयोग गर्नुहोस्।", "closeDialogIrreversible": "पूर्ववत गर्न सकिँदैन। त्रुटि सच्याउन समायोजन वा रिभर्सल प्रयोग गर्नुहोस्।",
"closeDialogConfirm": "अवधि बन्द गर्नुहोस्", "closeDialogConfirm": "अवधि बन्द गर्नुहोस्",
"closeDialogEmpty": "यस अवधिमा शेयर लेजर छैन; बन्द गर्दा बिल सिर्जना हुँदैन।" "closeDialogEmpty": "यस अवधिमा शेयर लेजर छैन; बन्द गर्दा बिल सिर्जना हुँदैन।"

View File

@@ -13,6 +13,8 @@
"detailLoadFailed": "टिकट विवरण लोड असफल", "detailLoadFailed": "टिकट विवरण लोड असफल",
"detailTitle": "टिकट विवरण", "detailTitle": "टिकट विवरण",
"drawNo": "ड्रअ नं.", "drawNo": "ड्रअ नं.",
"provider": "Provider",
"allProviders": "सबै provider",
"drawNoOptional": "ड्रअ नम्बर (वैकल्पिक)", "drawNoOptional": "ड्रअ नम्बर (वैकल्पिक)",
"drawNoPlaceholder": "जस्तै 20260520-001", "drawNoPlaceholder": "जस्तै 20260520-001",
"failReason": "असफल कारण", "failReason": "असफल कारण",

View File

@@ -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": "无法加载开账建议"

View File

@@ -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",

View File

@@ -20,6 +20,7 @@
"failed": "创建失败" "failed": "创建失败"
}, },
"drawNo": "期号", "drawNo": "期号",
"provider": "开注商",
"status": "状态", "status": "状态",
"startTime": "开始时间", "startTime": "开始时间",
"closeTime": "封盘时间", "closeTime": "封盘时间",

View File

@@ -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": "账期视图",

View File

@@ -19,6 +19,8 @@
"player": "玩家", "player": "玩家",
"orderNo": "订单号", "orderNo": "订单号",
"drawNo": "期号", "drawNo": "期号",
"provider": "开注商",
"allProviders": "全部开注商",
"playCode": "玩法", "playCode": "玩法",
"number": "号码", "number": "号码",
"betAmount": "下注", "betAmount": "下注",

View File

@@ -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}`;
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>

View File

@@ -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>
); );
} }

View File

@@ -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

View File

@@ -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 &&

View File

@@ -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}

View File

@@ -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}

View File

@@ -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: "搜索" })}

View File

@@ -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>
</> </>
); );
} }

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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

View File

@@ -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;
}>;
}; };

View File

@@ -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;