"use client"; import type { ReactElement } from "react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Bar, BarChart, CartesianGrid, Cell, XAxis, YAxis } from "recharts"; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; import { signedMoneyClass } from "@/lib/admin-signed-money"; import { cn } from "@/lib/utils"; import { buildTrendChartConfig, DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config"; import { DashboardChartEmpty } from "@/modules/dashboard/dashboard-chart-empty"; import type { AdminDashboardAnalyticsPlayRow } from "@/types/api/admin-dashboard-analytics"; import type { AdminReportDailyProfitRow } from "@/types/api/admin-reports"; import type { DashboardAnalyticsMetric } from "@/types/api/admin-dashboard-analytics"; type MoneyFormatter = (minor: number, currency: string | null) => string; function playMetricValue(row: AdminDashboardAnalyticsPlayRow, metric: DashboardAnalyticsMetric): number { switch (metric) { case "bet": return row.total_bet_minor; case "payout": return row.total_payout_minor; case "profit": return row.approx_house_gross_minor; default: return row.total_bet_minor; } } function metricBarFill(metric: DashboardAnalyticsMetric, value: number): string { if (metric === "payout") { return DASHBOARD_CHART_COLORS.rose; } if (metric === "profit") { return value >= 0 ? DASHBOARD_CHART_COLORS.success : DASHBOARD_CHART_COLORS.warning; } return DASHBOARD_CHART_COLORS.primary; } export function DailyTrendChart({ series, metric, formatMoney, currency, }: { series: AdminReportDailyProfitRow[]; metric: DashboardAnalyticsMetric; formatMoney: MoneyFormatter; currency: string | null; }): ReactElement { const { t } = useTranslation("dashboard"); const chartConfig = useMemo( () => buildTrendChartConfig({ bet: t("chartLegend.bet"), payout: t("chartLegend.payout"), profit: t("chartLegend.profit"), }), [t], ); const chartData = useMemo( () => series.map((day) => ({ date: day.business_date.slice(5), fullDate: day.business_date, bet: day.total_bet_minor, payout: day.total_payout_minor, profit: day.approx_house_gross_minor, profitAbs: Math.abs(day.approx_house_gross_minor), })), [series], ); if (series.length === 0) { return ; } const plotHeight = series.length <= 7 ? 240 : series.length <= 14 ? 260 : 280; if (metric === "overview") { return ( 14 ? Math.ceil(series.length / 7) - 1 : 0} /> { const row = payload?.[0]?.payload as { fullDate?: string } | undefined; return row?.fullDate ?? ""; }} formatter={(value, name, item) => { if (name === "profit") { const row = item?.payload as { profit?: number } | undefined; return formatMoney(row?.profit ?? Number(value), currency); } return formatMoney(Number(value), currency); }} /> } /> } /> ); } const activeKey = metric === "bet" ? "bet" : metric === "payout" ? "payout" : "profitAbs"; const dataKey = metric === "profit" ? "profitAbs" : activeKey; return ( 14 ? Math.ceil(series.length / 7) - 1 : 0} /> { const row = payload?.[0]?.payload as { fullDate?: string } | undefined; return row?.fullDate ?? ""; }} formatter={(value) => formatMoney(Number(value), currency)} /> } /> {chartData.map((row) => ( ))} ); } export function PlayBreakdownChart({ rows, metric, formatMoney, currency, playLabel, compact = false, }: { rows: AdminDashboardAnalyticsPlayRow[]; metric: DashboardAnalyticsMetric; formatMoney: MoneyFormatter; currency: string | null; playLabel: (code: string, dimension: number) => string; compact?: boolean; }): ReactElement { const { t } = useTranslation("dashboard"); const activeMetric = metric === "overview" ? "bet" : metric; const chartConfig = useMemo( () => buildTrendChartConfig({ bet: t("chartLegend.bet"), payout: t("chartLegend.payout"), profit: t("chartLegend.profit"), }), [t], ); const chartData = useMemo( () => rows.map((row) => { const value = playMetricValue(row, activeMetric); return { id: `${row.play_code}-${row.dimension}`, label: playLabel(row.play_code, row.dimension), value: Math.abs(value), signed: value, payout: row.total_payout_minor, profit: row.approx_house_gross_minor, fill: metricBarFill(activeMetric, value), }; }), [rows, activeMetric, playLabel], ); if (rows.length === 0) { return ; } const chartHeight = compact ? Math.max(160, rows.length * 32 + 24) : Math.min(480, Math.max(180, rows.length * 36 + 48)); return ( { const row = payload?.[0]?.payload as { label?: string } | undefined; return row?.label ?? ""; }} formatter={(value, _name, item) => { const row = item.payload as { signed: number; payout: number; profit: number }; if (metric === "overview") { return ( {formatMoney(row.signed, currency)} · {t("playBreakdownHint", { payout: formatMoney(row.payout, currency), profit: formatMoney(row.profit, currency), })} ); } return formatMoney(row.signed, currency); }} /> } /> {chartData.map((entry) => ( ))} typeof value === "string" && value.length > 10 ? `${value.slice(0, 10)}…` : String(value) } /> ); } export function PeriodCompareStrip({ series, formatMoney, currency, }: { series: AdminReportDailyProfitRow[]; formatMoney: MoneyFormatter; currency: string | null; }): ReactElement { const { t } = useTranslation("dashboard"); const totalBet = series.reduce((s, d) => s + d.total_bet_minor, 0); const totalPayout = series.reduce((s, d) => s + d.total_payout_minor, 0); const totalProfit = series.reduce((s, d) => s + d.approx_house_gross_minor, 0); const maxAbs = Math.max(Math.abs(totalBet), Math.abs(totalPayout), Math.abs(totalProfit), 1); const payoutRate = totalBet > 0 ? (totalPayout / totalBet) * 100 : 0; const profitRate = totalBet > 0 ? (totalProfit / totalBet) * 100 : 0; const rows = [ { key: "bet", label: t("chartLegend.bet"), value: totalBet, pctText: "100%", width: (Math.abs(totalBet) / maxAbs) * 100, fill: "var(--chart-1)", }, { key: "payout", label: t("chartLegend.payout"), value: totalPayout, pctText: `${payoutRate.toFixed(1)}%`, width: (Math.abs(totalPayout) / maxAbs) * 100, fill: "var(--chart-5)", }, { key: "profit", label: t("chartLegend.profit"), value: totalProfit, pctText: `${profitRate >= 0 ? "+" : ""}${profitRate.toFixed(1)}%`, width: (Math.abs(totalProfit) / maxAbs) * 100, fill: totalProfit >= 0 ? "var(--chart-2)" : DASHBOARD_CHART_COLORS.warning, }, ] as const; return (
{rows.map((row) => (
{row.label} {row.pctText}
{formatMoney(row.value, currency)}
))}
); }