refactor(reports): 优化报表导出和数据处理逻辑
- 异步动态引入 XLSX 库以优化加载性能 - 抽取导出表格为工作簿的异步函数,提高代码复用性 - 使用构建函数封装报表数据行和汇总的生成逻辑 - 替换硬编码数据聚合为调用构建函数,简化代码结构 - 在导出功能中支持异步操作,增强用户体验 - 统一格式化和统计报表数据,提升代码可维护性
This commit is contained in:
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import * as XLSX from "xlsx";
|
||||
import {
|
||||
CalendarDays,
|
||||
CircleDollarSign,
|
||||
@@ -279,7 +278,7 @@ function toCsvValue(value: ExportCell): string {
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
function exportRows(rows: ExportRow[], filename: string, sheetName: string, format: ExportFormat): void {
|
||||
async function exportRows(rows: ExportRow[], filename: string, sheetName: string, format: ExportFormat): Promise<void> {
|
||||
if (rows.length === 0) {
|
||||
throw new LotteryApiBizError("no_data", -1, null);
|
||||
}
|
||||
@@ -295,12 +294,160 @@ function exportRows(rows: ExportRow[], filename: string, sheetName: string, form
|
||||
return;
|
||||
}
|
||||
|
||||
const XLSX = await import("xlsx");
|
||||
const worksheet = XLSX.utils.json_to_sheet(rows);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
XLSX.writeFile(workbook, `${filename}.xlsx`);
|
||||
}
|
||||
|
||||
function buildDailyProfitRowsAndSummary(
|
||||
items: AdminReportDailyProfitRow[],
|
||||
total: number,
|
||||
t: (key: string) => string,
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
): Pick<Extract<ReportResult, { key: "daily_profit" }>, "rows" | "summary"> {
|
||||
let totalBet = 0;
|
||||
let totalPayout = 0;
|
||||
let totalGross = 0;
|
||||
|
||||
const rows = items.map((item) => {
|
||||
totalBet += item.total_bet_minor;
|
||||
totalPayout += item.total_payout_minor;
|
||||
totalGross += item.approx_house_gross_minor;
|
||||
return {
|
||||
business_date: item.business_date,
|
||||
total_bet_minor: item.total_bet_minor,
|
||||
total_payout_minor: item.total_payout_minor,
|
||||
approx_house_gross_minor: item.approx_house_gross_minor,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
rows,
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(total) },
|
||||
{ label: pageScopedLabel("bet"), value: formatPlainMoney(totalBet, "NPR") },
|
||||
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, "NPR") },
|
||||
{
|
||||
label: pageScopedLabel("houseGross"),
|
||||
value: formatPlainMoney(totalGross, "NPR"),
|
||||
tone: totalGross >= 0 ? "good" : "bad",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlayerTransferRowsAndSummary(
|
||||
items: AdminTransferOrderItem[],
|
||||
total: number,
|
||||
page: number,
|
||||
perPage: number,
|
||||
t: (key: string) => string,
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
): Pick<Extract<ReportResult, { key: "player_transfer" }>, "rows" | "summary" | "meta"> {
|
||||
let transferInCount = 0;
|
||||
let transferOutCount = 0;
|
||||
|
||||
const rows = items.map((item) => {
|
||||
if (item.direction === "in") {
|
||||
transferInCount += 1;
|
||||
} else if (item.direction === "out") {
|
||||
transferOutCount += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
transfer_no: item.transfer_no,
|
||||
player_id: item.player_id,
|
||||
username: item.username,
|
||||
nickname: item.nickname,
|
||||
direction: item.direction,
|
||||
currency_code: item.currency_code,
|
||||
amount: item.amount,
|
||||
status: item.status,
|
||||
external_ref_no: item.external_ref_no,
|
||||
fail_reason: item.fail_reason,
|
||||
created_at: formatExportInstant(item.created_at),
|
||||
finished_at: formatExportInstant(item.finished_at),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
rows,
|
||||
meta: { total, page, perPage, lastPage: Math.max(1, Math.ceil(total / perPage)) },
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(total) },
|
||||
{ label: t("preview.stats.currentPage"), value: String(items.length) },
|
||||
{ label: pageScopedLabel("transferIn"), value: String(transferInCount), tone: "good" },
|
||||
{ label: pageScopedLabel("transferOut"), value: String(transferOutCount), tone: "warn" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlayDimensionRowsAndSummary(
|
||||
items: AdminReportPlayDimensionRow[],
|
||||
total: number,
|
||||
t: (key: string) => string,
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
): Pick<Extract<ReportResult, { key: "play_dimension" }>, "rows" | "summary"> {
|
||||
let totalBet = 0;
|
||||
let totalPayout = 0;
|
||||
|
||||
const rows = items.map((item) => {
|
||||
totalBet += item.total_bet_minor;
|
||||
totalPayout += item.total_payout_minor;
|
||||
return {
|
||||
play_code: item.play_code,
|
||||
dimension: item.dimension,
|
||||
total_bet_minor: item.total_bet_minor,
|
||||
total_payout_minor: item.total_payout_minor,
|
||||
approx_house_gross_minor: item.approx_house_gross_minor,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
rows,
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(total) },
|
||||
{ label: t("preview.stats.currentPage"), value: String(items.length) },
|
||||
{ label: pageScopedLabel("bet"), value: formatPlainMoney(totalBet, "NPR") },
|
||||
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, "NPR") },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function buildRebateCommissionRowsAndSummary(
|
||||
items: AdminReportRebateCommissionRow[],
|
||||
total: number,
|
||||
t: (key: string) => string,
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
): Pick<Extract<ReportResult, { key: "rebate_commission" }>, "rows" | "summary"> {
|
||||
let totalRebate = 0;
|
||||
let totalOrders = 0;
|
||||
|
||||
const rows = items.map((item) => {
|
||||
totalRebate += item.total_rebate_minor;
|
||||
totalOrders += item.order_count;
|
||||
return {
|
||||
play_code: item.play_code,
|
||||
total_rebate_minor: item.total_rebate_minor,
|
||||
order_count: item.order_count,
|
||||
ticket_item_count: item.ticket_item_count,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
rows,
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(total) },
|
||||
{ label: t("preview.stats.currentPage"), value: String(items.length) },
|
||||
{ label: pageScopedLabel("rebate"), value: formatPlainMoney(totalRebate, "NPR") },
|
||||
{ label: pageScopedLabel("orders"), value: String(totalOrders) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function metaFromList(meta: { current_page: number; per_page: number; total: number; last_page: number }): ReportMeta {
|
||||
return {
|
||||
total: meta.total,
|
||||
@@ -724,30 +871,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const payload = await getAdminReportDailyProfit(
|
||||
reportListParams(filters, page, perPage),
|
||||
);
|
||||
const rows = payload.items.map((item) => ({
|
||||
business_date: item.business_date,
|
||||
total_bet_minor: item.total_bet_minor,
|
||||
total_payout_minor: item.total_payout_minor,
|
||||
approx_house_gross_minor: item.approx_house_gross_minor,
|
||||
}));
|
||||
const totalBet = payload.items.reduce((sum, item) => sum + item.total_bet_minor, 0);
|
||||
const totalPayout = payload.items.reduce((sum, item) => sum + item.total_payout_minor, 0);
|
||||
const totalGross = payload.items.reduce((sum, item) => sum + item.approx_house_gross_minor, 0);
|
||||
const next = buildDailyProfitRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
|
||||
setResult({
|
||||
key: "daily_profit",
|
||||
raw: payload.items,
|
||||
rows,
|
||||
rows: next.rows,
|
||||
meta: metaFromList(payload.meta),
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
|
||||
{ label: pageScopedLabel("bet"), value: formatPlainMoney(totalBet, "NPR") },
|
||||
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, "NPR") },
|
||||
{
|
||||
label: pageScopedLabel("houseGross"),
|
||||
value: formatPlainMoney(totalGross, "NPR"),
|
||||
tone: totalGross >= 0 ? "good" : "bad",
|
||||
},
|
||||
],
|
||||
summary: next.summary,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -792,32 +922,20 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
created_from: filters.dateFrom || undefined,
|
||||
created_to: filters.dateTo || undefined,
|
||||
});
|
||||
const rows = payload.items.map((item) => ({
|
||||
id: item.id,
|
||||
transfer_no: item.transfer_no,
|
||||
player_id: item.player_id,
|
||||
username: item.username,
|
||||
nickname: item.nickname,
|
||||
direction: item.direction,
|
||||
currency_code: item.currency_code,
|
||||
amount: item.amount,
|
||||
status: item.status,
|
||||
external_ref_no: item.external_ref_no,
|
||||
fail_reason: item.fail_reason,
|
||||
created_at: formatExportInstant(item.created_at),
|
||||
finished_at: formatExportInstant(item.finished_at),
|
||||
}));
|
||||
const next = buildPlayerTransferRowsAndSummary(
|
||||
payload.items,
|
||||
payload.total,
|
||||
payload.page,
|
||||
payload.per_page,
|
||||
t,
|
||||
pageScopedLabel,
|
||||
);
|
||||
setResult({
|
||||
key: "player_transfer",
|
||||
raw: payload.items,
|
||||
rows,
|
||||
meta: { total: payload.total, page: payload.page, perPage: payload.per_page, lastPage: Math.max(1, Math.ceil(payload.total / payload.per_page)) },
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(payload.total) },
|
||||
{ label: t("preview.stats.currentPage"), value: String(payload.items.length) },
|
||||
{ label: pageScopedLabel("transferIn"), value: String(payload.items.filter((item) => item.direction === "in").length), tone: "good" },
|
||||
{ label: pageScopedLabel("transferOut"), value: String(payload.items.filter((item) => item.direction === "out").length), tone: "warn" },
|
||||
],
|
||||
rows: next.rows,
|
||||
meta: next.meta,
|
||||
summary: next.summary,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -913,24 +1031,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const payload = await getAdminReportPlayDimension(
|
||||
reportListParams(filters, page, perPage),
|
||||
);
|
||||
const rows = payload.items.map((item) => ({
|
||||
play_code: item.play_code,
|
||||
dimension: item.dimension,
|
||||
total_bet_minor: item.total_bet_minor,
|
||||
total_payout_minor: item.total_payout_minor,
|
||||
approx_house_gross_minor: item.approx_house_gross_minor,
|
||||
}));
|
||||
const next = buildPlayDimensionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
|
||||
setResult({
|
||||
key: "play_dimension",
|
||||
raw: payload.items,
|
||||
rows,
|
||||
rows: next.rows,
|
||||
meta: metaFromList(payload.meta),
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
|
||||
{ label: t("preview.stats.currentPage"), value: String(payload.items.length) },
|
||||
{ label: pageScopedLabel("bet"), value: formatPlainMoney(payload.items.reduce((s, i) => s + i.total_bet_minor, 0), "NPR") },
|
||||
{ label: pageScopedLabel("payout"), value: formatPlainMoney(payload.items.reduce((s, i) => s + i.total_payout_minor, 0), "NPR") },
|
||||
],
|
||||
summary: next.summary,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -938,23 +1045,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const payload = await getAdminReportRebateCommission(
|
||||
reportListParams(filters, page, perPage),
|
||||
);
|
||||
const rows = payload.items.map((item) => ({
|
||||
play_code: item.play_code,
|
||||
total_rebate_minor: item.total_rebate_minor,
|
||||
order_count: item.order_count,
|
||||
ticket_item_count: item.ticket_item_count,
|
||||
}));
|
||||
const next = buildRebateCommissionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
|
||||
setResult({
|
||||
key: "rebate_commission",
|
||||
raw: payload.items,
|
||||
rows,
|
||||
rows: next.rows,
|
||||
meta: metaFromList(payload.meta),
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
|
||||
{ label: t("preview.stats.currentPage"), value: String(payload.items.length) },
|
||||
{ label: pageScopedLabel("rebate"), value: formatPlainMoney(payload.items.reduce((s, i) => s + i.total_rebate_minor, 0), "NPR") },
|
||||
{ label: pageScopedLabel("orders"), value: String(payload.items.reduce((s, i) => s + i.order_count, 0)) },
|
||||
],
|
||||
summary: next.summary,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user