import React, { useMemo, useState } from "react"; import { Chart as ChartJS, CategoryScale, LinearScale, LineController, LineElement, PointElement, BarController, BarElement, Tooltip, type ChartOptions, } from "chart.js"; import { Chart } from "react-chartjs-2"; import { BarChart2, LineChart } from "lucide-react"; import { useThemeVars } from "@/lib/theme-provider"; import { Card, CardContent, CardHeader, CardTitle, CardAction } from "./card"; import { Empty, EmptyDescription } from "./empty"; import { Skeleton } from "./skeleton"; import { cn } from "@/lib/utils"; import { FALLBACK_TICK, hexToRgba, formatTooltipDate, formatMonthLabel, formatCount, ChartLegendItem, ChartPeriodButton, } from "./chart-shared"; import { ToggleGroup, ToggleGroupItem } from "./toggle-group"; ChartJS.register( CategoryScale, LinearScale, LineController, LineElement, PointElement, BarController, BarElement, Tooltip, ); // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface ConnectionSummaryDataPoint { /** ISO date string e.g. "2024-01-01" */ date: string; /** Total client count */ all: number; /** Clients with bank connected */ bankConnected: number; /** Clients with property connected */ propertyConnected: number; /** Clients with no connection */ notConnected: number; } export type ConnectionSummaryPeriod = 3 | 6 | 12; type ConnectionChartType = "line" | "bar"; export interface BackofficeConnectionsChartProps { chartData?: ConnectionSummaryDataPoint[] | null; title?: string; showLegend?: boolean; legendPosition?: "top" | "bottom"; showXAxis?: boolean; showYAxis?: boolean; defaultPeriod?: ConnectionSummaryPeriod; height?: number; width?: number | string; className?: string; isLoading?: boolean; } // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const PERIODS: ConnectionSummaryPeriod[] = [3, 6, 12]; const SLICE_COUNT: Record = { 3: 3, 6: 6, 12: 12, }; const ALL_COLOR = "#040D13"; const BANK_COLOR = "#33FF99"; const PROPERTY_COLOR = "#3B82F6"; const NONE_COLOR = "#9EAAB5"; // --------------------------------------------------------------------------- // Legend // --------------------------------------------------------------------------- function ConnectionsLegend() { return (
); } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function BackofficeConnectionsChart({ chartData, title = "Clients and Connections", showLegend = true, legendPosition = "top", showXAxis = true, showYAxis = false, defaultPeriod = 6, height = 200, width = "100%", className, isLoading = false, }: BackofficeConnectionsChartProps) { const [period, setPeriod] = useState(defaultPeriod); const [chartType, setChartType] = useState("line"); const themeVars = useThemeVars(); const fontFamily: string = (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif"; const sliced = useMemo(() => { if (!chartData?.length) return null; const count = Math.min(SLICE_COUNT[period], chartData.length); return chartData.slice(chartData.length - count); }, [chartData, period]); const labels = useMemo( () => sliced?.map((p) => formatMonthLabel(p.date)) ?? [], [sliced], ); const isBar = chartType === "bar"; const datasets = useMemo(() => { if (!sliced) return []; return [ { type: chartType, label: "All Clients", data: sliced.map((p) => p.all), borderColor: ALL_COLOR, backgroundColor: isBar ? ALL_COLOR : hexToRgba(ALL_COLOR, 0.08), fill: !isBar, tension: 0.3, pointRadius: 0, borderWidth: 2, }, { type: chartType, label: "Bank Connected", data: sliced.map((p) => p.bankConnected), borderColor: BANK_COLOR, backgroundColor: isBar ? BANK_COLOR : hexToRgba(BANK_COLOR, 0.08), fill: !isBar, tension: 0.3, pointRadius: 0, borderWidth: 2, }, { type: chartType, label: "Property Connected", data: sliced.map((p) => p.propertyConnected), borderColor: PROPERTY_COLOR, backgroundColor: isBar ? PROPERTY_COLOR : hexToRgba(PROPERTY_COLOR, 0.08), fill: !isBar, tension: 0.3, pointRadius: 0, borderWidth: 2, }, { type: chartType, label: "Not Connected", data: sliced.map((p) => p.notConnected), borderColor: NONE_COLOR, backgroundColor: isBar ? NONE_COLOR : hexToRgba(NONE_COLOR, 0.08), fill: !isBar, tension: 0.3, pointRadius: 0, borderWidth: 2, }, ]; }, [sliced, chartType, isBar]); const options = useMemo>( () => ({ responsive: true, maintainAspectRatio: false, animation: { duration: 300 }, layout: { padding: 0 }, plugins: { legend: { display: false }, tooltip: { mode: "index", intersect: false, padding: 12, cornerRadius: 0, titleFont: { size: 11, weight: 600 }, bodyFont: { size: 12, weight: 500 }, callbacks: { title: (tooltipItems) => { const idx = tooltipItems[0]?.dataIndex; if (idx != null && sliced?.[idx]?.date) { return formatTooltipDate(sliced[idx].date, "monthly"); } return tooltipItems[0]?.label ?? ""; }, label: (ctx) => { const val = ctx.raw as number; if (val === 0) return; return ` ${ctx.dataset.label}: ${formatCount(val)}`; }, }, }, }, scales: { x: { display: showXAxis, grid: { display: false }, border: { display: false }, ticks: { maxRotation: 0, minRotation: 0, color: FALLBACK_TICK, font: { size: 10 }, }, }, y: { display: showYAxis, grid: { display: false }, border: { display: false }, beginAtZero: true, ticks: { padding: 8, maxTicksLimit: 5, color: FALLBACK_TICK, font: { size: 10 }, callback: (v) => formatCount(Number(v)), }, }, }, }), [showXAxis, showYAxis, sliced], ); return ( {title}
v[0] && setChartType(v[0] as ConnectionChartType) } >
{PERIODS.map((p) => ( setPeriod(p)} /> ))}
{isLoading ? ( ) : !sliced ? ( No data available ) : (
{showLegend && legendPosition === "top" && }
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{showLegend && legendPosition === "bottom" && }
)}
); }