feat(reports, risk, deployment): 支持开注商经营报表、风险池按开注商操作并调整 PM2 部署目录
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
This commit is contained in:
@@ -10,12 +10,12 @@ REMOTE_DIR="/www/wwwroot/lotteryAdmin"
|
||||
echo "🔨 构建中..."
|
||||
npm run build
|
||||
|
||||
echo "📦 上传到服务器..."
|
||||
ssh ${SERVER} "mkdir -p ${REMOTE_DIR}/.next/standalone ${REMOTE_DIR}/logs"
|
||||
rsync -avz --delete .next/standalone/ ${SERVER}:${REMOTE_DIR}/.next/standalone/
|
||||
rsync -avz ecosystem.config.cjs ${SERVER}:${REMOTE_DIR}/ecosystem.config.cjs
|
||||
echo "📦 上传到服务器 app/ ..."
|
||||
ssh ${SERVER} "mkdir -p ${REMOTE_DIR}/app ${REMOTE_DIR}/logs"
|
||||
rsync -az --delete .next/standalone/ ${SERVER}:${REMOTE_DIR}/app/
|
||||
rsync -az ecosystem.config.cjs ${SERVER}:${REMOTE_DIR}/ecosystem.config.cjs
|
||||
|
||||
echo "🚀 服务器重启 PM2..."
|
||||
ssh ${SERVER} "source /etc/profile && source ~/.bashrc && mkdir -p ${REMOTE_DIR}/logs && cd ${REMOTE_DIR} && (pm2 restart lotteryadmin 2>/dev/null || pm2 start ecosystem.config.cjs) && pm2 save"
|
||||
echo "🚀 重启 PM2..."
|
||||
ssh ${SERVER} "source /etc/profile && source ~/.bashrc && cd ${REMOTE_DIR} && (pm2 delete lotteryadmin 2>/dev/null || true) && pm2 start ecosystem.config.cjs --only lotteryadmin --update-env && pm2 save"
|
||||
|
||||
echo "✅ lotteryAdmin 部署完成!"
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
AdminReportDailyProfitRow,
|
||||
AdminReportListData,
|
||||
AdminReportPlayDimensionRow,
|
||||
AdminReportProviderProfitRow,
|
||||
AdminReportPlayerWinLossRow,
|
||||
AdminReportQueryParams,
|
||||
} from "@/types/api/admin-reports";
|
||||
@@ -19,6 +20,12 @@ export async function getAdminReportDailyProfit(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAdminReportProviderProfit(
|
||||
params: AdminReportQueryParams,
|
||||
): Promise<AdminReportListData<AdminReportProviderProfitRow>> {
|
||||
return adminRequest.get<AdminReportListData<AdminReportProviderProfitRow>>(`${A}/reports/provider-profit`, { params });
|
||||
}
|
||||
|
||||
export async function getAdminReportPlayerWinLoss(
|
||||
params: AdminReportQueryParams,
|
||||
): Promise<AdminReportListData<AdminReportPlayerWinLossRow>> {
|
||||
|
||||
@@ -12,6 +12,7 @@ const A = `/admin`;
|
||||
export type AdminRiskPoolListQuery = {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
provider_code?: string;
|
||||
sold_out_only?: boolean;
|
||||
high_risk_only?: boolean;
|
||||
active_only?: boolean;
|
||||
@@ -27,6 +28,7 @@ export async function getAdminRiskPools(
|
||||
params: {
|
||||
page: q.page,
|
||||
per_page: q.per_page,
|
||||
provider_code: q.provider_code || undefined,
|
||||
sold_out_only: q.sold_out_only === true ? 1 : undefined,
|
||||
high_risk_only: q.high_risk_only === true ? 1 : undefined,
|
||||
active_only: q.active_only === true ? 1 : undefined,
|
||||
@@ -39,26 +41,33 @@ export async function getAdminRiskPools(
|
||||
export async function postAdminRiskPoolManualClose(
|
||||
drawId: number,
|
||||
number4d: string,
|
||||
providerCode: string,
|
||||
): Promise<AdminRiskPoolRow> {
|
||||
const encoded = encodeURIComponent(number4d);
|
||||
return adminRequest.post<AdminRiskPoolRow>(
|
||||
`${A}/draws/${drawId}/risk-pools/${encoded}/manual-close`,
|
||||
undefined,
|
||||
{ params: { provider_code: providerCode || undefined } },
|
||||
);
|
||||
}
|
||||
|
||||
export async function postAdminRiskPoolRecover(
|
||||
drawId: number,
|
||||
number4d: string,
|
||||
providerCode: string,
|
||||
): Promise<AdminRiskPoolRow> {
|
||||
const encoded = encodeURIComponent(number4d);
|
||||
return adminRequest.post<AdminRiskPoolRow>(
|
||||
`${A}/draws/${drawId}/risk-pools/${encoded}/recover`,
|
||||
undefined,
|
||||
{ params: { provider_code: providerCode || undefined } },
|
||||
);
|
||||
}
|
||||
|
||||
export type AdminRiskPoolShowQuery = {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
provider_code?: string;
|
||||
};
|
||||
|
||||
export async function getAdminRiskPoolDetail(
|
||||
@@ -69,6 +78,6 @@ export async function getAdminRiskPoolDetail(
|
||||
const encoded = encodeURIComponent(number4d);
|
||||
return adminRequest.get<AdminRiskPoolShowData>(
|
||||
`${A}/draws/${drawId}/risk-pools/${encoded}`,
|
||||
{ params: { page: q.page, per_page: q.per_page } },
|
||||
{ params: { page: q.page, per_page: q.per_page, provider_code: q.provider_code || undefined } },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import { RiskPoolDetailConsole } from "@/modules/risk/risk-pool-detail-console";
|
||||
|
||||
export default async function AdminDrawRiskPoolDetailPage(props: {
|
||||
params: Promise<{ drawId: string; number: string }>;
|
||||
searchParams: Promise<{ provider_code?: string }>;
|
||||
}) {
|
||||
const { drawId, number } = await props.params;
|
||||
const { provider_code } = await props.searchParams;
|
||||
const id = Number(drawId);
|
||||
const providerCode = (provider_code ?? "SG").trim().toUpperCase() || "SG";
|
||||
|
||||
return <RiskPoolDetailConsole drawId={id} number4d={number} />;
|
||||
return <RiskPoolDetailConsole drawId={id} number4d={number} providerCode={providerCode} />;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import { redirect } from "next/navigation";
|
||||
|
||||
export default async function AdminRiskPoolDetailPage(props: {
|
||||
params: Promise<{ drawId: string; number: string }>;
|
||||
searchParams: Promise<{ provider_code?: string }>;
|
||||
}) {
|
||||
const { drawId, number } = await props.params;
|
||||
redirect(`/admin/draws/${drawId}/risk/pools/${number}`);
|
||||
const { provider_code } = await props.searchParams;
|
||||
const suffix = provider_code ? `?provider_code=${encodeURIComponent(provider_code)}` : "";
|
||||
redirect(`/admin/draws/${drawId}/risk/pools/${number}${suffix}`);
|
||||
}
|
||||
|
||||
@@ -316,6 +316,10 @@
|
||||
"title": "Play dimension report",
|
||||
"summary": "P&L by play and business date; defaults to the last 30 days. Ticket amounts — not credit-line settlement."
|
||||
},
|
||||
"provider_profit": {
|
||||
"title": "Bet provider performance",
|
||||
"summary": "Ticket items, bets, payouts, and platform gross by provider and business date; defaults to the last 30 days."
|
||||
},
|
||||
"sold_out_number": {
|
||||
"title": "Sold-out number report",
|
||||
"summary": "Review sold-out numbers, sold-out time, and risk lock state by draw."
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"searchNumber": "Search number",
|
||||
"searchNumberPlaceholder": "For example 8888",
|
||||
"riskFilter": "Risk filter",
|
||||
"provider": "Provider",
|
||||
"sort": "Sort",
|
||||
"filterAll": "All",
|
||||
"filterActive": "Active / high risk",
|
||||
@@ -51,9 +52,9 @@
|
||||
"actionFailed": "Action failed",
|
||||
"confirm": {
|
||||
"closeTitle": "Confirm close number?",
|
||||
"closeDescription": "Number {{number}} will be blocked for this draw.",
|
||||
"closeDescription": "Number {{number}} ({{provider}}) will be blocked for this draw.",
|
||||
"recoverTitle": "Confirm recover number?",
|
||||
"recoverDescription": "Number {{number}} will be open for betting again."
|
||||
"recoverDescription": "Number {{number}} ({{provider}}) will be open for betting again."
|
||||
},
|
||||
"detailTitle": "Risk pool details",
|
||||
"loadDetailFailed": "Failed to load risk pool details",
|
||||
|
||||
@@ -68,6 +68,10 @@
|
||||
"summary": "खेल र व्यावसायिक मितिअनुसार P&L; मिति नचयेमा पछिल्लो ३० दिन।",
|
||||
"title": "खेल आयाम रिपोर्ट"
|
||||
},
|
||||
"provider_profit": {
|
||||
"summary": "खुलाउने कम्पनी र व्यावसायिक मितिअनुसार टिकट, बेट, पेआउट र प्लेटफर्म नाफा; मिति नचयेमा पछिल्लो ३० दिन।",
|
||||
"title": "खुलाउने कम्पनी सञ्चालन रिपोर्ट"
|
||||
},
|
||||
"player_transfer": {
|
||||
"summary": "वालेट-मोड मुख्य साइट ट्रान्सफर, रेकर्ड समय अनुसार; मिति नचयेमा पछिल्लो ३० दिन।",
|
||||
"title": "खेलाडी ट्रान्सफर रिपोर्ट"
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"closeTime": "बन्द समय",
|
||||
"combinationCount": "संयोजन",
|
||||
"confirm": {
|
||||
"closeDescription": "नम्बर {{number}} यो ड्रका लागि ब्लक हुनेछ।",
|
||||
"closeDescription": "नम्बर {{number}} ({{provider}}) यो ड्रका लागि ब्लक हुनेछ।",
|
||||
"closeTitle": "नम्बर बन्द गर्ने?",
|
||||
"recoverDescription": "नम्बर {{number}} फेरि बाजीका लागि खुल्नेछ।",
|
||||
"recoverDescription": "नम्बर {{number}} ({{provider}}) फेरि बाजीका लागि खुल्नेछ।",
|
||||
"recoverTitle": "नम्बर पुन: खोल्ने?"
|
||||
},
|
||||
"databaseStatus": "डेटाबेस स्थिति",
|
||||
@@ -69,6 +69,7 @@
|
||||
"remainingAmount": "बाँकी",
|
||||
"remainingSellable": "बाँकी बिक्रीयोग्य",
|
||||
"riskFilter": "जोखिम फिल्टर",
|
||||
"provider": "प्रोभाइडर",
|
||||
"search": "खोज",
|
||||
"searchNumber": "नम्बर खोज्नुहोस्",
|
||||
"searchNumberPlaceholder": "जस्तै 8888",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"transfer_orders_daily": "转账订单(日)",
|
||||
"hot_number_risk_report": "热门号码风险",
|
||||
"play_dimension_report": "玩法维度",
|
||||
"provider_profit_report": "开注商经营",
|
||||
"sold_out_number_report": "售罄号码",
|
||||
"audit_operation_report": "后台操作审计"
|
||||
},
|
||||
@@ -316,6 +317,10 @@
|
||||
"title": "玩法维度报表",
|
||||
"summary": "按玩法与业务日拆分盈亏;未选日期默认近 30 天。注单口径,非信用账期结算。"
|
||||
},
|
||||
"provider_profit": {
|
||||
"title": "开注商经营报表",
|
||||
"summary": "按开注商与业务日汇总注项、投注、派彩和平台毛利;未选日期默认近 30 天。"
|
||||
},
|
||||
"sold_out_number": {
|
||||
"title": "售罄号码报表",
|
||||
"summary": "查看单期已售罄号码、售罄时间和风险封锁情况。"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"searchNumber": "搜索号码",
|
||||
"searchNumberPlaceholder": "如 8888",
|
||||
"riskFilter": "风险筛选",
|
||||
"provider": "开注商",
|
||||
"sort": "排序",
|
||||
"filterAll": "全部",
|
||||
"filterActive": "有占用/高风险",
|
||||
@@ -51,9 +52,9 @@
|
||||
"actionFailed": "操作失败",
|
||||
"confirm": {
|
||||
"closeTitle": "确认关闭该号码下注?",
|
||||
"closeDescription": "号码 {{number}} 在本期将被禁止下注。",
|
||||
"closeDescription": "号码 {{number}}({{provider}})在本期将被禁止下注。",
|
||||
"recoverTitle": "确认恢复该号码下注?",
|
||||
"recoverDescription": "号码 {{number}} 将恢复为可下注状态。"
|
||||
"recoverDescription": "号码 {{number}}({{provider}})将恢复为可下注状态。"
|
||||
},
|
||||
"detailTitle": "风险池详情",
|
||||
"loadDetailFailed": "加载风险池详情失败",
|
||||
|
||||
@@ -17,6 +17,7 @@ export type ReportUiKey =
|
||||
| "player_transfer"
|
||||
| "hot_number_risk"
|
||||
| "play_dimension"
|
||||
| "provider_profit"
|
||||
| "sold_out_number"
|
||||
| "admin_audit";
|
||||
|
||||
@@ -28,6 +29,7 @@ export const REPORT_UI_TO_JOB_TYPE: Record<ReportUiKey, string> = {
|
||||
player_transfer: "wallet_transfer_report",
|
||||
hot_number_risk: "hot_number_risk_report",
|
||||
play_dimension: "play_dimension_report",
|
||||
provider_profit: "provider_profit_report",
|
||||
sold_out_number: "sold_out_number_report",
|
||||
admin_audit: "audit_operation_report",
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ type DraftRiskRow = Omit<RiskCapItemRow, "id"> & { clientKey: string };
|
||||
function newRow(): DraftRiskRow {
|
||||
return {
|
||||
clientKey: `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
provider_code: "GLOBAL",
|
||||
draw_id: null,
|
||||
normalized_number: "0000",
|
||||
cap_amount: 0,
|
||||
@@ -79,6 +80,7 @@ function isDefaultRiskRow(row: DraftRiskRow): boolean {
|
||||
function defaultRiskRowFromAmount(amount: number): DraftRiskRow {
|
||||
return {
|
||||
clientKey: `default-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
provider_code: "GLOBAL",
|
||||
draw_id: null,
|
||||
normalized_number: "0000",
|
||||
cap_amount: amount,
|
||||
@@ -175,6 +177,7 @@ export function RiskCapDocScreen() {
|
||||
setDetail(d);
|
||||
const mapped = d.items.map((it) => ({
|
||||
clientKey: `srv-${it.id}`,
|
||||
provider_code: it.provider_code ?? "GLOBAL",
|
||||
draw_id: it.draw_id,
|
||||
normalized_number: it.normalized_number,
|
||||
cap_amount: it.cap_amount,
|
||||
@@ -285,6 +288,7 @@ export function RiskCapDocScreen() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = rowsToSave.map((r) => ({
|
||||
provider_code: r.provider_code ?? "GLOBAL",
|
||||
draw_id: r.draw_id && r.draw_id > 0 ? r.draw_id : null,
|
||||
normalized_number: r.normalized_number,
|
||||
cap_amount: r.cap_amount,
|
||||
@@ -294,6 +298,7 @@ export function RiskCapDocScreen() {
|
||||
setDetail(d);
|
||||
const saved = d.items.map((it) => ({
|
||||
clientKey: `srv-${it.id}`,
|
||||
provider_code: it.provider_code ?? "GLOBAL",
|
||||
draw_id: it.draw_id,
|
||||
normalized_number: it.normalized_number,
|
||||
cap_amount: it.cap_amount,
|
||||
@@ -320,6 +325,7 @@ export function RiskCapDocScreen() {
|
||||
setDetail(d);
|
||||
const pub = d.items.map((it) => ({
|
||||
clientKey: `srv-${it.id}`,
|
||||
provider_code: it.provider_code ?? "GLOBAL",
|
||||
draw_id: it.draw_id,
|
||||
normalized_number: it.normalized_number,
|
||||
cap_amount: it.cap_amount,
|
||||
@@ -351,6 +357,7 @@ export function RiskCapDocScreen() {
|
||||
setDetail(d);
|
||||
const nd = d.items.map((it) => ({
|
||||
clientKey: `srv-${it.id}`,
|
||||
provider_code: it.provider_code ?? "GLOBAL",
|
||||
draw_id: it.draw_id,
|
||||
normalized_number: it.normalized_number,
|
||||
cap_amount: it.cap_amount,
|
||||
@@ -424,6 +431,7 @@ export function RiskCapDocScreen() {
|
||||
setDetail(d);
|
||||
const mapped = d.items.map((it) => ({
|
||||
clientKey: `srv-${it.id}`,
|
||||
provider_code: it.provider_code ?? "GLOBAL",
|
||||
draw_id: it.draw_id,
|
||||
normalized_number: it.normalized_number,
|
||||
cap_amount: it.cap_amount,
|
||||
@@ -691,4 +699,4 @@ export function RiskCapDocScreen() {
|
||||
<ConfirmDialog />
|
||||
</ConfigDocPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,13 @@ export function ReportPreviewTable({
|
||||
}
|
||||
|
||||
switch (result.key) {
|
||||
case "provider_profit":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader><TableRow><TableHead>开注商</TableHead><TableHead className="text-right">注项数</TableHead><TableHead className="text-right">下注</TableHead><TableHead className="text-right">派彩</TableHead><TableHead className="text-right">平台毛利</TableHead></TableRow></TableHeader>
|
||||
<TableBody>{result.raw.map((item) => <TableRow key={item.provider_code}><TableCell className="font-medium">{item.provider_name}<span className="ml-2 font-mono text-xs text-muted-foreground">{item.provider_code}</span></TableCell><TableCell className="text-right">{item.ticket_item_count}</TableCell><TableCell className="text-right">{formatPlainMoney(item.total_bet_minor, displayCurrency)}</TableCell><TableCell className="text-right">{formatPlainMoney(item.total_payout_minor, displayCurrency)}</TableCell><TableCell className={signedProfitCell(item.approx_house_gross_minor, displayCurrency)}>{formatPlainMoney(item.approx_house_gross_minor, displayCurrency)}</TableCell></TableRow>)}</TableBody>
|
||||
</Table>
|
||||
);
|
||||
case "draw_profit":
|
||||
return (
|
||||
<Table>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { downloadAdminReportJob, postAdminReportJob } from "@/api/admin-report-j
|
||||
import {
|
||||
getAdminReportDailyProfit,
|
||||
getAdminReportPlayDimension,
|
||||
getAdminReportProviderProfit,
|
||||
getAdminReportPlayerWinLoss,
|
||||
} from "@/api/admin-reports";
|
||||
import {
|
||||
@@ -375,6 +376,23 @@ export function ReportsConsole({ initialCategory = "profit" }: ReportsConsolePro
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "provider_profit": {
|
||||
const payload = await getAdminReportProviderProfit(reportListParams(filters, page, perPage));
|
||||
const currencyCode = resolveDisplayCurrency(payload.currency_code);
|
||||
setDisplayCurrency(currencyCode);
|
||||
const gross = payload.items.reduce((sum, item) => sum + item.approx_house_gross_minor, 0);
|
||||
setResult({
|
||||
key: "provider_profit",
|
||||
raw: payload.items,
|
||||
rows: payload.items,
|
||||
meta: metaFromList(payload.meta),
|
||||
summary: [
|
||||
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
|
||||
{ label: pageScopedLabel("houseGross"), value: formatPlainMoney(gross, currencyCode), tone: gross >= 0 ? "good" : "bad" },
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "admin_audit": {
|
||||
const operatorId = filters.operatorId ?? parsePositiveInteger(filters.operator);
|
||||
const payload = await getAdminAuditLogs({
|
||||
@@ -841,4 +859,4 @@ export function ReportsConsole({ initialCategory = "profit" }: ReportsConsolePro
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
CalendarDays,
|
||||
Building2,
|
||||
FileSpreadsheet,
|
||||
ListFilter,
|
||||
ShieldAlert,
|
||||
@@ -44,6 +45,7 @@ export type ReportKey =
|
||||
| "player_transfer"
|
||||
| "hot_number_risk"
|
||||
| "play_dimension"
|
||||
| "provider_profit"
|
||||
| "sold_out_number"
|
||||
| "admin_audit";
|
||||
|
||||
@@ -60,6 +62,15 @@ export type ReportDefinition = {
|
||||
const PRD_REPORTS_VIEW_ACCESS_ANY = [PRD_REPORT_VIEW] as const;
|
||||
|
||||
export const REPORT_DEFINITIONS: ReportDefinition[] = [
|
||||
{
|
||||
key: "provider_profit",
|
||||
category: "profit",
|
||||
icon: Building2,
|
||||
filterKind: "date",
|
||||
fields: ["period"],
|
||||
connected: true,
|
||||
requiredAny: PRD_REPORTS_VIEW_ACCESS_ANY,
|
||||
},
|
||||
{
|
||||
key: "draw_profit",
|
||||
category: "profit",
|
||||
@@ -138,4 +149,4 @@ export const CATEGORY_SHORTCUT_HREF: Partial<Record<ReportCategory, string>> = {
|
||||
wallet: "/admin/wallet/transfer-orders",
|
||||
risk: "/admin/risk",
|
||||
audit: "/admin/audit-logs",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { AdminTransferOrderItem } from "@/types/api/admin-wallet";
|
||||
import type {
|
||||
AdminReportDailyProfitRow,
|
||||
AdminReportPlayDimensionRow,
|
||||
AdminReportProviderProfitRow,
|
||||
AdminReportPlayerWinLossRow,
|
||||
} from "@/types/api/admin-reports";
|
||||
|
||||
@@ -59,6 +60,7 @@ export type ReportResult =
|
||||
| { key: "player_transfer"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminTransferOrderItem[] }
|
||||
| { key: "hot_number_risk"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta | null; raw: AdminRiskPoolShowData }
|
||||
| { key: "play_dimension"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportPlayDimensionRow[] }
|
||||
| { key: "provider_profit"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportProviderProfitRow[] }
|
||||
| { key: "sold_out_number"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminRiskPoolRow[] }
|
||||
| { key: "admin_audit"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminAuditLogRow[] };
|
||||
|
||||
@@ -350,4 +352,4 @@ export function headlineStats(result: ReportResult | null, reportKey: ReportKey)
|
||||
return [];
|
||||
}
|
||||
return result.summary.slice(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ import type { AdminRiskPoolDetailLogRow, AdminRiskPoolShowData } from "@/types/a
|
||||
export function RiskPoolDetailConsole({
|
||||
drawId,
|
||||
number4d,
|
||||
providerCode,
|
||||
}: {
|
||||
drawId: number;
|
||||
number4d: string;
|
||||
providerCode: string;
|
||||
}) {
|
||||
const { t } = useTranslation(["risk", "common"]);
|
||||
const tRef = useTranslationRef(["risk", "common"]);
|
||||
@@ -53,7 +55,11 @@ export function RiskPoolDetailConsole({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const d = await getAdminRiskPoolDetail(drawId, number4d, { page, per_page: perPage });
|
||||
const d = await getAdminRiskPoolDetail(drawId, number4d, {
|
||||
page,
|
||||
per_page: perPage,
|
||||
provider_code: providerCode,
|
||||
});
|
||||
setData(d);
|
||||
} catch (e) {
|
||||
const msg =
|
||||
@@ -63,11 +69,11 @@ export function RiskPoolDetailConsole({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [drawId, number4d, page, perPage, tRef]);
|
||||
}, [drawId, number4d, page, perPage, providerCode, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
}, [drawId, number4d, page, perPage]);
|
||||
}, [drawId, number4d, page, perPage, providerCode]);
|
||||
|
||||
if (error && !data) {
|
||||
return (
|
||||
@@ -115,7 +121,9 @@ export function RiskPoolDetailConsole({
|
||||
<CardTitle className="text-lg">
|
||||
{t("numberTitle", { number: pool.normalized_number })}
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{t("drawMeta", { drawNo: data.draw_no })}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("drawMeta", { drawNo: data.draw_no })} · {t("provider")} {pool.provider_code}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="rounded-lg border bg-muted/40 p-3">
|
||||
|
||||
@@ -119,7 +119,7 @@ export function RiskPoolsConsole({
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const [data, setData] = useState<AdminRiskPoolListData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actingNumber, setActingNumber] = useState<string | null>(null);
|
||||
const [actingKey, setActingKey] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
@@ -152,11 +152,12 @@ export function RiskPoolsConsole({
|
||||
|
||||
const handleManualStatus = useCallback(
|
||||
async (row: AdminRiskPoolRow) => {
|
||||
setActingNumber(row.normalized_number);
|
||||
const rowKey = `${row.provider_code}:${row.normalized_number}`;
|
||||
setActingKey(rowKey);
|
||||
try {
|
||||
const updated = row.is_sold_out
|
||||
? await postAdminRiskPoolRecover(drawId, row.normalized_number)
|
||||
: await postAdminRiskPoolManualClose(drawId, row.normalized_number);
|
||||
? await postAdminRiskPoolRecover(drawId, row.normalized_number, row.provider_code)
|
||||
: await postAdminRiskPoolManualClose(drawId, row.normalized_number, row.provider_code);
|
||||
|
||||
setData((current) => {
|
||||
if (!current) return current;
|
||||
@@ -164,7 +165,10 @@ export function RiskPoolsConsole({
|
||||
return {
|
||||
...current,
|
||||
items: current.items.map((item) =>
|
||||
item.normalized_number === updated.normalized_number ? updated : item,
|
||||
item.normalized_number === updated.normalized_number &&
|
||||
item.provider_code === updated.provider_code
|
||||
? updated
|
||||
: item,
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -172,7 +176,7 @@ export function RiskPoolsConsole({
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed"));
|
||||
} finally {
|
||||
setActingNumber(null);
|
||||
setActingKey(null);
|
||||
}
|
||||
},
|
||||
[drawId, t],
|
||||
@@ -275,6 +279,7 @@ export function RiskPoolsConsole({
|
||||
<Table id={`risk-pools-table-${drawId}`}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("provider")}</TableHead>
|
||||
<TableHead>{t("searchNumber")}</TableHead>
|
||||
<TableHead className="text-center">{t("capAmount")}</TableHead>
|
||||
<TableHead className="text-center">{t("lockedAmount")}</TableHead>
|
||||
@@ -285,15 +290,15 @@ export function RiskPoolsConsole({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading && !data ? <AdminTableLoadingRow colSpan={7} /> : null}
|
||||
{loading && !data ? <AdminTableLoadingRow colSpan={8} /> : null}
|
||||
{(data?.items ?? []).map((row: AdminRiskPoolRow) => {
|
||||
const highRisk = (row.usage_ratio ?? 0) >= 0.8;
|
||||
const acting = actingNumber === row.normalized_number;
|
||||
const acting = actingKey === `${row.provider_code}:${row.normalized_number}`;
|
||||
const currencyCode = data?.currency_code ?? "NPR";
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={row.normalized_number}
|
||||
key={`${row.provider_code}-${row.normalized_number}`}
|
||||
className={cn(
|
||||
row.is_sold_out
|
||||
? "bg-red-50/90 hover:bg-red-50 dark:bg-red-950/25 dark:hover:bg-red-950/35"
|
||||
@@ -302,6 +307,7 @@ export function RiskPoolsConsole({
|
||||
: null,
|
||||
)}
|
||||
>
|
||||
<TableCell className="font-mono text-xs">{row.provider_code}</TableCell>
|
||||
<TableCell className="font-mono font-medium">{row.normalized_number}</TableCell>
|
||||
<TableCell className="text-center text-sm font-semibold">
|
||||
{formatAdminMinorUnits(row.total_cap_amount, currencyCode)}
|
||||
@@ -331,7 +337,7 @@ export function RiskPoolsConsole({
|
||||
key: "view",
|
||||
label: t("view"),
|
||||
icon: Eye,
|
||||
href: `/admin/draws/${drawId}/risk/pools/${row.normalized_number}`,
|
||||
href: `/admin/draws/${drawId}/risk/pools/${row.normalized_number}?provider_code=${encodeURIComponent(row.provider_code)}`,
|
||||
},
|
||||
{
|
||||
key: "toggle",
|
||||
@@ -347,9 +353,11 @@ export function RiskPoolsConsole({
|
||||
description: row.is_sold_out
|
||||
? t("confirm.recoverDescription", {
|
||||
number: row.normalized_number,
|
||||
provider: row.provider_code,
|
||||
})
|
||||
: t("confirm.closeDescription", {
|
||||
number: row.normalized_number,
|
||||
provider: row.provider_code,
|
||||
}),
|
||||
confirmVariant: row.is_sold_out ? "default" : "destructive",
|
||||
onConfirm: () => handleManualStatus(row),
|
||||
|
||||
@@ -77,6 +77,7 @@ export type OddsVersionDetail = ConfigVersionSummary & {
|
||||
|
||||
export type RiskCapItemRow = {
|
||||
id: number;
|
||||
provider_code?: string;
|
||||
draw_id: number | null;
|
||||
normalized_number: string;
|
||||
cap_amount: number;
|
||||
|
||||
@@ -24,6 +24,15 @@ export type AdminReportPlayDimensionRow = {
|
||||
approx_house_gross_minor: number;
|
||||
};
|
||||
|
||||
export type AdminReportProviderProfitRow = {
|
||||
provider_code: string;
|
||||
provider_name: string;
|
||||
ticket_item_count: number;
|
||||
total_bet_minor: number;
|
||||
total_payout_minor: number;
|
||||
approx_house_gross_minor: number;
|
||||
};
|
||||
|
||||
export type AdminReportListData<T> = {
|
||||
items: T[];
|
||||
meta: {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type AdminRiskPoolListMeta = {
|
||||
};
|
||||
|
||||
export type AdminRiskPoolRow = {
|
||||
provider_code: string;
|
||||
normalized_number: string;
|
||||
total_cap_amount: number;
|
||||
locked_amount: number;
|
||||
@@ -26,6 +27,7 @@ export type AdminRiskPoolListData = {
|
||||
|
||||
export type AdminRiskPoolDetailLogRow = {
|
||||
id: number;
|
||||
provider_code: string;
|
||||
action_type: string;
|
||||
amount: number;
|
||||
source_reason: string | null;
|
||||
|
||||
Reference in New Issue
Block a user