feat(reports, risk, deployment): 支持开注商经营报表、风险池按开注商操作并调整 PM2 部署目录
Some checks failed
lotteryadmin CI / build (push) Has been cancelled

This commit is contained in:
2026-07-10 10:23:46 +08:00
parent 1164d9d728
commit d6826d5a1c
22 changed files with 147 additions and 33 deletions

View File

@@ -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 部署完成!"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,10 @@
"summary": "खेल र व्यावसायिक मितिअनुसार P&L; मिति नचयेमा पछिल्लो ३० दिन।",
"title": "खेल आयाम रिपोर्ट"
},
"provider_profit": {
"summary": "खुलाउने कम्पनी र व्यावसायिक मितिअनुसार टिकट, बेट, पेआउट र प्लेटफर्म नाफा; मिति नचयेमा पछिल्लो ३० दिन।",
"title": "खुलाउने कम्पनी सञ्चालन रिपोर्ट"
},
"player_transfer": {
"summary": "वालेट-मोड मुख्य साइट ट्रान्सफर, रेकर्ड समय अनुसार; मिति नचयेमा पछिल्लो ३० दिन।",
"title": "खेलाडी ट्रान्सफर रिपोर्ट"

View File

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

View File

@@ -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": "查看单期已售罄号码、售罄时间和风险封锁情况。"

View File

@@ -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": "加载风险池详情失败",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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