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