refactor(risk, navigation): update risk management redirects and enhance loading states
Changed default redirects in risk management pages to point to the new risk pools section. Removed unused risk lock log components and streamlined the admin reports page with a loading state for better user experience. Added a new DocFigure component for improved documentation visuals and updated localization files to include new figure descriptions.
This commit is contained in:
@@ -70,7 +70,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { useAdminCurrencyCatalog, getCachedAdminCurrencies } from "@/hooks/use-admin-currency-catalog";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { formatAdminInstant } from "@/lib/admin-datetime";
|
||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||
@@ -208,6 +208,68 @@ const emptyFilters: ReportFilters = {
|
||||
dateTo: "",
|
||||
};
|
||||
|
||||
function isoDateLocal(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function defaultReportPeriod(): Pick<ReportFilters, "dateFrom" | "dateTo"> {
|
||||
const to = new Date();
|
||||
const from = new Date();
|
||||
from.setDate(from.getDate() - 29);
|
||||
return { dateFrom: isoDateLocal(from), dateTo: isoDateLocal(to) };
|
||||
}
|
||||
|
||||
function createDefaultFilters(): ReportFilters {
|
||||
return { ...emptyFilters, ...defaultReportPeriod() };
|
||||
}
|
||||
|
||||
function reportHasPeriodField(report: ReportDefinition): boolean {
|
||||
return report.fields.includes("period");
|
||||
}
|
||||
|
||||
function resolveDisplayCurrency(apiCode?: string | null): string {
|
||||
const trimmed = apiCode?.trim();
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
const fallback = getCachedAdminCurrencies().find((row) => row.is_default)?.code;
|
||||
return fallback?.trim() || "NPR";
|
||||
}
|
||||
|
||||
function reportTimeAxisKey(key: ReportKey): "businessDate" | "recordCreatedAt" | null {
|
||||
switch (key) {
|
||||
case "daily_profit":
|
||||
case "player_win_loss":
|
||||
case "play_dimension":
|
||||
case "rebate_commission":
|
||||
return "businessDate";
|
||||
case "player_transfer":
|
||||
case "admin_audit":
|
||||
return "recordCreatedAt";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function reportDisclaimerKey(key: ReportKey): string | null {
|
||||
switch (key) {
|
||||
case "draw_profit":
|
||||
case "daily_profit":
|
||||
case "player_win_loss":
|
||||
case "play_dimension":
|
||||
return "items.profit_reports.disclaimer";
|
||||
case "player_transfer":
|
||||
return "items.player_transfer.disclaimer";
|
||||
case "rebate_commission":
|
||||
return "items.rebate_commission.disclaimer";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const emptySearch: SearchState = {
|
||||
open: null,
|
||||
query: "",
|
||||
@@ -306,6 +368,7 @@ function buildDailyProfitRowsAndSummary(
|
||||
total: number,
|
||||
t: (key: string) => string,
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
currencyCode: string,
|
||||
): Pick<Extract<ReportResult, { key: "daily_profit" }>, "rows" | "summary"> {
|
||||
let totalBet = 0;
|
||||
let totalPayout = 0;
|
||||
@@ -327,11 +390,11 @@ function buildDailyProfitRowsAndSummary(
|
||||
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("bet"), value: formatPlainMoney(totalBet, currencyCode) },
|
||||
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, currencyCode) },
|
||||
{
|
||||
label: pageScopedLabel("houseGross"),
|
||||
value: formatPlainMoney(totalGross, "NPR"),
|
||||
value: formatPlainMoney(totalGross, currencyCode),
|
||||
tone: totalGross >= 0 ? "good" : "bad",
|
||||
},
|
||||
],
|
||||
@@ -390,6 +453,7 @@ function buildPlayDimensionRowsAndSummary(
|
||||
total: number,
|
||||
t: (key: string) => string,
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
currencyCode: string,
|
||||
): Pick<Extract<ReportResult, { key: "play_dimension" }>, "rows" | "summary"> {
|
||||
let totalBet = 0;
|
||||
let totalPayout = 0;
|
||||
@@ -411,8 +475,8 @@ function buildPlayDimensionRowsAndSummary(
|
||||
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") },
|
||||
{ label: pageScopedLabel("bet"), value: formatPlainMoney(totalBet, currencyCode) },
|
||||
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, currencyCode) },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -422,6 +486,7 @@ function buildRebateCommissionRowsAndSummary(
|
||||
total: number,
|
||||
t: (key: string) => string,
|
||||
pageScopedLabel: (statKey: string) => string,
|
||||
currencyCode: string,
|
||||
): Pick<Extract<ReportResult, { key: "rebate_commission" }>, "rows" | "summary"> {
|
||||
let totalRebate = 0;
|
||||
let totalOrders = 0;
|
||||
@@ -442,7 +507,7 @@ function buildRebateCommissionRowsAndSummary(
|
||||
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("rebate"), value: formatPlainMoney(totalRebate, currencyCode) },
|
||||
{ label: pageScopedLabel("orders"), value: String(totalOrders) },
|
||||
],
|
||||
};
|
||||
@@ -654,7 +719,8 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const [selectedKey, setSelectedKey] = useState<ReportKey>(
|
||||
filteredReports[0]?.key ?? REPORTS[0].key,
|
||||
);
|
||||
const [filters, setFilters] = useState<ReportFilters>(emptyFilters);
|
||||
const [filters, setFilters] = useState<ReportFilters>(createDefaultFilters);
|
||||
const [displayCurrency, setDisplayCurrency] = useState<string>(() => resolveDisplayCurrency(null));
|
||||
const [result, setResult] = useState<ReportResult | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -852,6 +918,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
tRef.current("validation.drawNoNotFound", { ns: "reports", drawNo }),
|
||||
});
|
||||
const summary = await getAdminDrawFinanceSummary(draw.id);
|
||||
setDisplayCurrency(resolveDisplayCurrency(summary.currency_code));
|
||||
setResult({
|
||||
key: "draw_profit",
|
||||
raw: summary,
|
||||
@@ -874,7 +941,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const payload = await getAdminReportDailyProfit(
|
||||
reportListParams(filters, page, perPage),
|
||||
);
|
||||
const next = buildDailyProfitRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
|
||||
const currencyCode = resolveDisplayCurrency(payload.currency_code);
|
||||
setDisplayCurrency(currencyCode);
|
||||
const next = buildDailyProfitRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel, currencyCode);
|
||||
setResult({
|
||||
key: "daily_profit",
|
||||
raw: payload.items,
|
||||
@@ -888,6 +957,8 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const payload = await getAdminReportPlayerWinLoss(
|
||||
reportListParams(filters, page, perPage),
|
||||
);
|
||||
const currencyCode = resolveDisplayCurrency(payload.currency_code);
|
||||
setDisplayCurrency(currencyCode);
|
||||
const rows = payload.items.map((item) => ({
|
||||
player_id: item.player_id,
|
||||
username: item.username,
|
||||
@@ -907,7 +978,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
label: pageScopedLabel("houseGross"),
|
||||
value: formatPlainMoney(
|
||||
payload.items.reduce((sum, item) => sum - item.net_win_loss_minor, 0),
|
||||
"NPR",
|
||||
currencyCode,
|
||||
),
|
||||
tone: (() => {
|
||||
const houseGross = payload.items.reduce((sum, item) => sum - item.net_win_loss_minor, 0);
|
||||
@@ -937,6 +1008,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
t,
|
||||
pageScopedLabel,
|
||||
);
|
||||
setDisplayCurrency(resolveDisplayCurrency(payload.items[0]?.currency_code));
|
||||
setResult({
|
||||
key: "player_transfer",
|
||||
raw: payload.items,
|
||||
@@ -956,6 +1028,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
tRef.current("validation.drawNoNotFound", { ns: "reports", drawNo }),
|
||||
});
|
||||
const detail = await getAdminRiskPoolDetail(draw.id, filters.number.trim(), { page, per_page: perPage });
|
||||
setDisplayCurrency(resolveDisplayCurrency(detail.currency_code));
|
||||
const rows: ExportRow[] = [
|
||||
{
|
||||
row_type: "risk_pool",
|
||||
@@ -1007,6 +1080,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
tRef.current("validation.drawNoNotFound", { ns: "reports", drawNo }),
|
||||
});
|
||||
const payload = await getAdminRiskPools(draw.id, { page, per_page: perPage, sold_out_only: true, sort: "number_asc" });
|
||||
setDisplayCurrency(resolveDisplayCurrency(payload.currency_code));
|
||||
const rows = payload.items.map((item) => ({
|
||||
draw_id: payload.draw_id,
|
||||
draw_no: payload.draw_no,
|
||||
@@ -1038,7 +1112,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const payload = await getAdminReportPlayDimension(
|
||||
reportListParams(filters, page, perPage),
|
||||
);
|
||||
const next = buildPlayDimensionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
|
||||
const currencyCode = resolveDisplayCurrency(payload.currency_code);
|
||||
setDisplayCurrency(currencyCode);
|
||||
const next = buildPlayDimensionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel, currencyCode);
|
||||
setResult({
|
||||
key: "play_dimension",
|
||||
raw: payload.items,
|
||||
@@ -1052,7 +1128,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const payload = await getAdminReportRebateCommission(
|
||||
reportListParams(filters, page, perPage),
|
||||
);
|
||||
const next = buildRebateCommissionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
|
||||
const currencyCode = resolveDisplayCurrency(payload.currency_code);
|
||||
setDisplayCurrency(currencyCode);
|
||||
const next = buildRebateCommissionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel, currencyCode);
|
||||
setResult({
|
||||
key: "rebate_commission",
|
||||
raw: payload.items,
|
||||
@@ -1149,7 +1227,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
}
|
||||
|
||||
function resetFilters(): void {
|
||||
setFilters(emptyFilters);
|
||||
setFilters(reportHasPeriodField(selectedReport) ? createDefaultFilters() : { ...emptyFilters });
|
||||
setResult(null);
|
||||
setError(null);
|
||||
setPage(1);
|
||||
@@ -1521,9 +1599,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
<TableRow key={item.normalized_number}>
|
||||
<TableCell className="font-medium">{item.normalized_number}</TableCell>
|
||||
<TableCell>{filters.drawNo}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_cap_amount, null)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.locked_amount, null)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.remaining_amount, null)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_cap_amount, displayCurrency)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.locked_amount, displayCurrency)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.remaining_amount, displayCurrency)}</TableCell>
|
||||
<TableCell>{item.is_sold_out ? t("yes") : t("no")}</TableCell>
|
||||
<TableCell>{formatUsagePercent(item.usage_ratio)}</TableCell>
|
||||
<TableCell>v{item.version}</TableCell>
|
||||
@@ -1536,10 +1614,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
<TableRow key={item.business_date}>
|
||||
<TableCell className="font-medium">{item.business_date}</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
||||
<TableCell className={signedProfitCell(item.approx_house_gross_minor, "NPR")}>
|
||||
{formatPlainMoney(item.approx_house_gross_minor, "NPR")}
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, displayCurrency)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, displayCurrency)}</TableCell>
|
||||
<TableCell className={signedProfitCell(item.approx_house_gross_minor, displayCurrency)}>
|
||||
{formatPlainMoney(item.approx_house_gross_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
@@ -1556,10 +1634,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
{adminAgentDisplayLabel(item)}
|
||||
<span className="mt-0.5 block text-muted-foreground">ID {item.player_id}</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
||||
<TableCell className={signedProfitCell(item.net_win_loss_minor, "NPR")}>
|
||||
{formatPlainMoney(item.net_win_loss_minor, "NPR")}
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, displayCurrency)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, displayCurrency)}</TableCell>
|
||||
<TableCell className={signedProfitCell(item.net_win_loss_minor, displayCurrency)}>
|
||||
{formatPlainMoney(item.net_win_loss_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
@@ -1573,10 +1651,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
<TableRow key={`${item.play_code}-${item.dimension}`}>
|
||||
<TableCell className="font-medium">{playCodeLabel(item.play_code)}</TableCell>
|
||||
<TableCell>{item.dimension}D</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, "NPR")}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, "NPR")}</TableCell>
|
||||
<TableCell className={signedProfitCell(item.approx_house_gross_minor, "NPR")}>
|
||||
{formatPlainMoney(item.approx_house_gross_minor, "NPR")}
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_bet_minor, displayCurrency)}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_payout_minor, displayCurrency)}</TableCell>
|
||||
<TableCell className={signedProfitCell(item.approx_house_gross_minor, displayCurrency)}>
|
||||
{formatPlainMoney(item.approx_house_gross_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
@@ -1590,7 +1668,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
<TableRow key={item.play_code}>
|
||||
<TableCell className="font-medium">{playCodeLabel(item.play_code)}</TableCell>
|
||||
<TableCell>{item.order_count}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_rebate_minor, "NPR")}</TableCell>
|
||||
<TableCell className="text-center">{formatPlainMoney(item.total_rebate_minor, displayCurrency)}</TableCell>
|
||||
<TableCell className="text-center">{item.ticket_item_count}</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
@@ -1648,6 +1726,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
})}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{t(`items.${selectedReport.key}.summary`)}</div>
|
||||
{reportTimeAxisKey(selectedReport.key) ? (
|
||||
<p className="text-xs text-muted-foreground">{t(`timeAxis.${reportTimeAxisKey(selectedReport.key)}`)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-0">
|
||||
@@ -1655,7 +1736,10 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
{selectedReport.fields.map(renderField)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 border-t border-border/60 pt-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-xs text-muted-foreground">{t("filterPanel")}</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<div>{t("filterPanel")}</div>
|
||||
<div>{t("queryHint")}</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={resetFilters}>
|
||||
{t("reset")}
|
||||
@@ -1686,12 +1770,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedReport.key === "rebate_commission" ? (
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-950">
|
||||
{t("items.rebate_commission.disclaimer", {
|
||||
defaultValue:
|
||||
"本报表为钱包盘「下注立减回水/佣金」口径,不属于信用占成盘账期结算。占成盘请使用「代理 → 代理账单」中的账期报表。",
|
||||
})}
|
||||
{reportDisclaimerKey(selectedReport.key) ? (
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-950 dark:border-amber-700 dark:bg-amber-950/30 dark:text-amber-100">
|
||||
{t(reportDisclaimerKey(selectedReport.key)!)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user