feat(dashboard, i18n): enhance agent dashboard and localization support
Updated the agent dashboard to include new metrics for today's bets and payouts, improving visibility for users. Enhanced localization files with additional hints and labels for better user experience across English, Nepali, and Chinese. Introduced new functions for formatting business dates and improved the handling of analytics permissions in the dashboard components.
This commit is contained in:
@@ -35,7 +35,6 @@ import {
|
||||
} from "@/api/admin-reports";
|
||||
import {
|
||||
buildReportJobParameters,
|
||||
REPORT_UI_SERVER_FULL_EXPORT,
|
||||
REPORT_UI_TO_JOB_TYPE,
|
||||
type ReportUiKey,
|
||||
} from "@/lib/report-export-map";
|
||||
@@ -239,37 +238,6 @@ function resolveDisplayCurrency(apiCode?: string | null): string {
|
||||
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: "",
|
||||
@@ -328,41 +296,6 @@ function formatExportInstant(iso: string | null | undefined): ExportCell {
|
||||
return formatAdminInstant(iso, { locale: getAdminRequestLocale() });
|
||||
}
|
||||
|
||||
function toCsvValue(value: ExportCell): string {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const stringValue = String(value);
|
||||
if (/[",\n]/.test(stringValue)) {
|
||||
return `"${stringValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
async function exportRows(rows: ExportRow[], filename: string, sheetName: string, format: ExportFormat): Promise<void> {
|
||||
if (rows.length === 0) {
|
||||
throw new LotteryApiBizError("no_data", -1, null);
|
||||
}
|
||||
|
||||
if (format === "csv") {
|
||||
const headers = Object.keys(rows[0]);
|
||||
const lines = [
|
||||
headers.map(toCsvValue).join(","),
|
||||
...rows.map((row) => headers.map((header) => toCsvValue(row[header] ?? "")).join(",")),
|
||||
];
|
||||
const blob = new Blob([`\uFEFF${lines.join("\n")}`], { type: "text/csv;charset=utf-8;" });
|
||||
downloadBlob(blob, `${filename}.csv`);
|
||||
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,
|
||||
@@ -742,7 +675,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
}, [filteredReports, selectedKey]);
|
||||
|
||||
const pageScopedLabel = useCallback(
|
||||
(statKey: string) => `${t(`preview.stats.${statKey}`)} · ${t("preview.scope.currentPage")}`,
|
||||
(statKey: string) => t(`preview.stats.${statKey}`),
|
||||
[t],
|
||||
);
|
||||
|
||||
@@ -1233,9 +1166,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
const usesServerExport = REPORT_UI_SERVER_FULL_EXPORT.has(selectedReport.key as ReportUiKey);
|
||||
|
||||
async function exportViaServer(format: ExportFormat): Promise<void> {
|
||||
async function exportReport(format: ExportFormat): Promise<void> {
|
||||
if (!canExportReports) {
|
||||
return;
|
||||
}
|
||||
@@ -1260,10 +1191,9 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
const ext = job.export_format === "xlsx" ? "xlsx" : "csv";
|
||||
downloadBlob(blob, filename ?? `${exportFileBase}.${ext}`);
|
||||
toast.success(
|
||||
t("exportServerSuccess", {
|
||||
t("exportSuccess", {
|
||||
report: t(`items.${selectedReport.key}.title`),
|
||||
format: t(`formats.${format}`),
|
||||
jobNo: job.job_no,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -1273,36 +1203,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
}
|
||||
}
|
||||
|
||||
function exportPreview(format: ExportFormat): void {
|
||||
if (!canExportReports) {
|
||||
return;
|
||||
}
|
||||
if (!result || result.rows.length === 0) {
|
||||
toast.info(t("empty"));
|
||||
return;
|
||||
}
|
||||
setExporting(format);
|
||||
try {
|
||||
exportRows(result.rows, exportFileBase, t(`items.${selectedReport.key}.title`), format);
|
||||
toast.success(t("exportSuccess", { report: t(`items.${selectedReport.key}.title`), format: t(`formats.${format}`) }));
|
||||
} catch (err) {
|
||||
toast.error(err instanceof LotteryApiBizError ? err.message : t("exportFailed"));
|
||||
} finally {
|
||||
setExporting(null);
|
||||
}
|
||||
}
|
||||
|
||||
function exportReport(format: ExportFormat): void {
|
||||
if (!canExportReports) {
|
||||
return;
|
||||
}
|
||||
if (usesServerExport) {
|
||||
void exportViaServer(format);
|
||||
return;
|
||||
}
|
||||
exportPreview(format);
|
||||
}
|
||||
|
||||
const renderSearchPicker = (kind: SearchKind) => {
|
||||
const value =
|
||||
kind === "draw" ? filters.drawNo : kind === "player" ? filters.player : filters.operator;
|
||||
@@ -1726,21 +1626,13 @@ 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">
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{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="space-y-1 text-xs text-muted-foreground">
|
||||
<div>{t("filterPanel")}</div>
|
||||
<div>{t("queryHint")}</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<div className="flex justify-end gap-2 border-t border-border/60 pt-3">
|
||||
<Button type="button" variant="outline" size="sm" onClick={resetFilters}>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
@@ -1756,7 +1648,6 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
<Database data-icon="inline-start" />
|
||||
{loading ? t("querying") : t("query")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1770,70 +1661,34 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
|
||||
))}
|
||||
</div>
|
||||
|
||||
{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}
|
||||
|
||||
<Card className="admin-list-card">
|
||||
<CardHeader className="admin-list-header flex flex-col gap-2 pb-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<CardTitle className="admin-list-title">{t("preview.title")}</CardTitle>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportReport("csv")}
|
||||
>
|
||||
<FileDown data-icon="inline-start" />
|
||||
{t("formats.csvServer")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportReport("excel")}
|
||||
>
|
||||
<FileSpreadsheet data-icon="inline-start" />
|
||||
{t("formats.excelServer")}
|
||||
</Button>
|
||||
</div>
|
||||
{result && result.rows.length > 0 ? (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground">{t("exportPreviewHint")}</p>
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportPreview("csv")}
|
||||
>
|
||||
{t("formats.csvPreview")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportPreview("excel")}
|
||||
>
|
||||
{t("formats.excelPreview")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-3">
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50/70 px-3 py-2 text-xs text-amber-950">
|
||||
{t("preview.summaryScopeHint")}
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => void exportReport("csv")}
|
||||
>
|
||||
<FileDown data-icon="inline-start" />
|
||||
{t("formats.csv")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => void exportReport("excel")}
|
||||
>
|
||||
<FileSpreadsheet data-icon="inline-start" />
|
||||
{t("formats.excel")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-3">
|
||||
<Table id="reports-preview-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
Reference in New Issue
Block a user