import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Stack } from "@mui/material"; import { styled, useTheme } from "@mui/material/styles"; import { ChartContainer, ChartsLegend } from "@mui/x-charts"; import { HighlightItemData } from "@mui/x-charts/context/HighlightedProvider"; import { useDrawingArea } from "@mui/x-charts/hooks"; import { PieChart } from "@mui/x-charts/PieChart"; import WidgetCard from "../../../components/WidgetCard"; import { useI18n } from "../../../contexts/I18nContext"; import { ContribComponent } from "../../../types"; import { PaymentMethodItem } from "../../../types/dashboard"; import { getHumanReadablePaymentMethod, HumanReadablePaymentMethod, } from "../utils/getHumanReadablePaymentMethod"; /** * Calculate the percentage of a payment method. */ function calculatePercentage(count: number, total: number): number { return Math.round((count / total) * 100); } interface PaymentMethod extends HumanReadablePaymentMethod, PaymentMethodItem { label: string; value: number; } const StyledTitle = styled("text")(({ theme }) => ({ fill: theme.palette.text.primary, textAnchor: "middle", dominantBaseline: "central", fontWeight: 700, fontSize: theme.typography.h4.fontSize, })); const StyledSubtitle = styled("text")(({ theme }) => ({ fill: theme.palette.text.secondary, textAnchor: "middle", dominantBaseline: "central", fontSize: theme.typography.htmlFontSize, })); function PieCenterLabel({ title, subtitle }: { title: string; subtitle: string }) { const { width, height, left, top } = useDrawingArea(); return ( <> {title} {subtitle} ); } const PaymentMethodWidget: ContribComponent = ({ data, sx }) => { const { t } = useI18n(); const theme = useTheme(); const total = useMemo(() => data.reduce((acc, curr) => acc + curr.count, 0), [data]); const dataSeries = useMemo(() => { // Accumulate counts per payment method name using a Map for efficient lookups const paymentMethodMap = data.reduce((map, item) => { const { name, color } = getHumanReadablePaymentMethod(item.method, { disambiguateCards: false, theme, }); if (map.has(name)) { const existingItem = map.get(name)!; existingItem.count += item.count; existingItem.value += item.count; } else { map.set(name, { ...item, color, name, value: item.count, label: name }); } return map; }, new Map()); // Convert Map values to an array and sort by count in descending order const sortedItems = Array.from(paymentMethodMap.values()).sort((a, b) => b.count - a.count); // Take the top 5 items and accumulate the rest into an 'Other' category const topItems = sortedItems.slice(0, 5); const otherItems = sortedItems.slice(5); if (otherItems.length > 0) { const otherTotals = otherItems.reduce( (totals, item) => { totals.count += item.count; totals.value += item.value; return totals; }, { count: 0, value: 0 }, ); topItems.push({ method: "other", name: "Other", count: otherTotals.count, value: otherTotals.value, color: theme.palette.grey[300], label: "Other", }); } // Map the items to include percentage labels return topItems.map((item) => { const percentage = calculatePercentage(item.count, total); return { ...item, label: `${item.name} ${percentage}%` }; }); }, [data, total, theme]); const [highlighted, setHighlighted] = useState(dataSeries?.[0]); // Update highlight when data changes. useEffect(() => { setHighlighted(dataSeries?.[0]); }, [data, total, highlighted]); const handleHighlightChange = useCallback( (highlight: HighlightItemData | null) => { if (typeof highlight?.dataIndex === "number") { setHighlighted(dataSeries[highlight.dataIndex]); } else { setHighlighted(dataSeries?.[0]); } }, [data, total, highlighted], ); const highlightedPercentage = useMemo(() => { if (highlighted == null) return null; const percentage: number = calculatePercentage(highlighted.count, total); if (Number.isNaN(percentage) || !Number.isFinite(percentage)) return null; return percentage; }, [data, total, highlighted]); return ( {highlighted != null && ( )} ({ type: "bar", label, color }))} width={200} > ); }; export default PaymentMethodWidget;