525 lines
26 KiB
React
525 lines
26 KiB
React
|
|
import { useState, useEffect, useRef, useLayoutEffect, useMemo } from 'react';
|
||
|
|
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Legend, Tooltip, ResponsiveContainer } from 'recharts';
|
||
|
|
import { TrendingUp, Package, IndianRupee, ChevronLeft, ChevronRight } from 'lucide-react';
|
||
|
|
import './TopSellingProductsChart.scss';
|
||
|
|
import { useDispatch } from 'react-redux';
|
||
|
|
import { getTopSellingProduct } from '../../Features/ProductPage/ProductPage';
|
||
|
|
import { getSession } from '../../Services/Others';
|
||
|
|
import { changeBreadCrumb } from '../../Features/AppPage/CenterPage';
|
||
|
|
import { Switch, DatePicker } from 'antd';
|
||
|
|
import moment from 'moment';
|
||
|
|
import dayjs from 'dayjs';
|
||
|
|
import { DropDowns } from '../../Components/Forms/DropDown';
|
||
|
|
|
||
|
|
const { RangePicker } = DatePicker;
|
||
|
|
|
||
|
|
const subDirectory = import.meta.env.BASE_URL;
|
||
|
|
|
||
|
|
const items = [
|
||
|
|
{
|
||
|
|
name: 'Home',
|
||
|
|
link: `${subDirectory}app-page/home`,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const TopSellingProducts = ({ dates = [null, null], products = [], loading = false, totalPages, page = 1, setPage = () => { }, fetchProducts = () => { }, initialDate = true }) => {
|
||
|
|
|
||
|
|
const today = new Date();
|
||
|
|
const year = today.getFullYear();
|
||
|
|
const month = today.getMonth();
|
||
|
|
|
||
|
|
const formatDate = (date) =>
|
||
|
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||
|
|
|
||
|
|
const intialFromDate = `${year}-${String(month + 1).padStart(2, '0')}-01`;
|
||
|
|
const intialToDate = formatDate(today);
|
||
|
|
|
||
|
|
const dispatch = useDispatch();
|
||
|
|
|
||
|
|
const AppId = getSession('AppId');
|
||
|
|
const CompId = getSession('CompId');
|
||
|
|
const BranchId = getSession('BranchId');
|
||
|
|
|
||
|
|
const [chartType, setChartType] = useState('bar');
|
||
|
|
const [metric, setMetric] = useState('sales');
|
||
|
|
const chartWrapperRef = useRef(null);
|
||
|
|
const [containerWidth, setContainerWidth] = useState(800); // fallback
|
||
|
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||
|
|
const [hoveredItem, setHoveredItem] = useState(null);
|
||
|
|
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||
|
|
const startIndex = (page - 1) * 10;
|
||
|
|
|
||
|
|
console.log(products?.[0]?.TotalCount, "products")
|
||
|
|
|
||
|
|
const options = useMemo(() => {
|
||
|
|
const total = products?.[0]?.TotalCount || 0;
|
||
|
|
const perPage = 10;
|
||
|
|
|
||
|
|
const totalPages = Math.ceil(total / perPage);
|
||
|
|
|
||
|
|
const arr = [];
|
||
|
|
|
||
|
|
for (let page = 1; page <= totalPages; page++) {
|
||
|
|
const start = (page - 1) * perPage + 1;
|
||
|
|
const end = Math.min(page * perPage, total);
|
||
|
|
|
||
|
|
arr.push({
|
||
|
|
label: `${start}-${end}`,
|
||
|
|
value: page
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return arr;
|
||
|
|
}, [products?.[0]?.TotalCount]);
|
||
|
|
|
||
|
|
useLayoutEffect(() => {
|
||
|
|
if (!chartWrapperRef.current) return;
|
||
|
|
const el = chartWrapperRef.current;
|
||
|
|
|
||
|
|
// set initial width
|
||
|
|
setContainerWidth(el.clientWidth || 800);
|
||
|
|
|
||
|
|
// ResizeObserver to update container width precisely
|
||
|
|
const ro = new ResizeObserver(entries => {
|
||
|
|
for (const entry of entries) {
|
||
|
|
const w = Math.floor(entry.contentRect.width);
|
||
|
|
if (w && w !== containerWidth) setContainerWidth(w);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
ro.observe(el);
|
||
|
|
|
||
|
|
return () => ro.disconnect();
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
}, [chartWrapperRef.current]);
|
||
|
|
// breakpoints
|
||
|
|
const mobileBreakpoint = 430; // you said < 430px is problematic
|
||
|
|
const isNarrow = containerWidth <= mobileBreakpoint;
|
||
|
|
|
||
|
|
// compute sizes based on actual container width
|
||
|
|
// make pie fill a good portion of container (leave some padding)
|
||
|
|
const pieOuterRadius = Math.max(48, Math.floor(containerWidth * (isNarrow ? 0.36 : 0.28)));
|
||
|
|
const pieInnerRadius = Math.floor(pieOuterRadius * (isNarrow ? 0.5 : 0.55));
|
||
|
|
|
||
|
|
// height for ResponsiveContainer: tether to containerWidth so chart stays proportional
|
||
|
|
const responsiveHeight = Math.max(220, Math.floor(containerWidth * (isNarrow ? 0.62 : 0.5)));
|
||
|
|
|
||
|
|
// tweak minAngle: small screens should allow smaller minAngle so slices remain visible
|
||
|
|
const minAngle = isNarrow ? 4 : 8;
|
||
|
|
|
||
|
|
|
||
|
|
console.log(pieOuterRadius, "pieOuterRadius")
|
||
|
|
|
||
|
|
// const COLORS = ['#2563eb', '#7c3aed', '#db2777', '#ea580c', '#16a34a', '#0891b2', '#d97706', '#65a30d', '#4f46e5', '#0d9488'];
|
||
|
|
const COLORS = [
|
||
|
|
'#4f8df5', // lighter blue (was #2563eb)
|
||
|
|
'#9a6df3', // lighter violet (was #7c3aed)
|
||
|
|
'#e95b96', // lighter pink (was #db2777)
|
||
|
|
'#f48b3c', // lighter orange (was #ea580c)
|
||
|
|
'#34c86a', // lighter green (was #16a34a)
|
||
|
|
'#33bcd4', // lighter cyan (was #0891b2)
|
||
|
|
'#e39a1a', // lighter amber (was #d97706)
|
||
|
|
'#8bc329', // lighter lime (was #65a30d)
|
||
|
|
'#6d63ef', // lighter indigo (was #4f46e5)
|
||
|
|
'#35bfa9' // lighter teal (was #0d9488)
|
||
|
|
];
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
try {
|
||
|
|
dispatch(changeBreadCrumb({ items: items }));
|
||
|
|
} catch (error) {
|
||
|
|
console.log(error?.message, 'error displaying breadcrumbs');
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handlePageChange = (data) => {
|
||
|
|
setPage(prev => prev + data)
|
||
|
|
}
|
||
|
|
|
||
|
|
const CustomTooltip = ({ active, payload }) => {
|
||
|
|
if (active && payload && payload?.[0]) {
|
||
|
|
const product = payload?.[0]?.payload;
|
||
|
|
return (
|
||
|
|
<div className="custom-tooltip">
|
||
|
|
<p className="tooltip-title">{product?.ProdName} - {product?.UOMName}</p>
|
||
|
|
{(product?.BrandName) ? <p className="tooltip-brand">Brand: {product?.BrandName}</p> : null}
|
||
|
|
<div className="tooltip-divider">
|
||
|
|
<p className="tooltip-subtitle">Variants:</p>
|
||
|
|
{product?.variantDetails?.map((variant, idx) => (
|
||
|
|
<div key={idx} className="tooltip-variant">
|
||
|
|
<span className="variant-name">{variant?.ProdVariantName}:</span>
|
||
|
|
<span className="variant-data">
|
||
|
|
{variant?.SalesCount} sold | ₹{variant?.Revenue}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="tooltip-total">
|
||
|
|
<div className="total-row">
|
||
|
|
<span>Total Sales:</span>
|
||
|
|
<span>{product?.TotalSoldQty}</span>
|
||
|
|
</div>
|
||
|
|
<div className="total-row">
|
||
|
|
<span>Total Revenue:</span>
|
||
|
|
<span>₹{product?.TotalRevenue}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
};
|
||
|
|
|
||
|
|
const renderPercentInside = ({
|
||
|
|
cx, cy, midAngle, innerRadius, outerRadius, percent, value
|
||
|
|
}) => {
|
||
|
|
const percentValue = Math.round(percent * 100);
|
||
|
|
// hide labels for very small percentages on narrow screens
|
||
|
|
if (percentValue < (isNarrow ? 3 : 2)) return null;
|
||
|
|
|
||
|
|
const RADIAN = Math.PI / 180;
|
||
|
|
// place label inside slice (closer to inner radius on narrow screens)
|
||
|
|
const radius = innerRadius + (outerRadius - innerRadius) * (isNarrow ? 0.55 : 0.6);
|
||
|
|
|
||
|
|
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||
|
|
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||
|
|
|
||
|
|
const fontSize = isNarrow ? 10 : 12;
|
||
|
|
|
||
|
|
// choose contrasting color: white usually works on colored slices
|
||
|
|
return (
|
||
|
|
<text
|
||
|
|
x={x}
|
||
|
|
y={y}
|
||
|
|
fill="#fff"
|
||
|
|
fontSize={fontSize}
|
||
|
|
fontWeight="700"
|
||
|
|
textAnchor="middle"
|
||
|
|
dominantBaseline="central"
|
||
|
|
pointerEvents="none"
|
||
|
|
style={{ textShadow: "0px 1px 2px rgba(0,0,0,0.35)" }}
|
||
|
|
>
|
||
|
|
{/* {`${percentValue}%`} */}
|
||
|
|
{value}
|
||
|
|
</text>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
const getBenchmarkValue = (maxValue) => {
|
||
|
|
if (maxValue <= 0) return 100;
|
||
|
|
|
||
|
|
const benchmarks = [
|
||
|
|
// Fine granularity for small numbers, coarser for large numbers
|
||
|
|
1, 2, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50,
|
||
|
|
60, 70, 80, 90, 100, 125, 150, 175, 200, 225, 250, 275, 300,
|
||
|
|
350, 400, 450, 500, 600, 700, 800, 900, 1000,
|
||
|
|
1250, 1500, 1750, 2000, 2500, 3000, 3500, 4000, 4500, 5000,
|
||
|
|
6000, 7000, 8000, 9000, 10000, 12500, 15000, 17500, 20000, 25000,
|
||
|
|
30000, 35000, 40000, 45000, 50000, 60000, 70000, 80000, 90000, 100000,
|
||
|
|
125000, 150000, 175000, 200000, 250000, 300000, 350000, 400000, 450000, 500000,
|
||
|
|
600000, 700000, 800000, 900000, 1000000
|
||
|
|
];
|
||
|
|
|
||
|
|
const benchmark = benchmarks.find(b => b >= maxValue);
|
||
|
|
return benchmark || Math.ceil(maxValue / 10000) * 10000;
|
||
|
|
};
|
||
|
|
|
||
|
|
const chartData = products?.map(p => ({
|
||
|
|
...p,
|
||
|
|
name: p?.ProdName,
|
||
|
|
value: metric === 'sales' ? p?.TotalSoldQty : p?.TotalRevenue,
|
||
|
|
benchMarkValue: metric === 'sales' ? p?.TotalRevenueCount : p?.TotalRevenueAmount
|
||
|
|
}));
|
||
|
|
|
||
|
|
const benchmarkValue = getBenchmarkValue(Math.max(...(chartData?.map(d => d?.benchMarkValue) || [0])));
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="top-selling-container">
|
||
|
|
<div className="content-wrapper">
|
||
|
|
{/* Header */}
|
||
|
|
{/* <div className="header-title-wrapper">
|
||
|
|
<div className="header-title">Top Selling Products</div>
|
||
|
|
<div className="header-date-picker">
|
||
|
|
<div className="date-picker-label">Date Range :</div>
|
||
|
|
<RangePicker
|
||
|
|
format="DD MMM YYYY"
|
||
|
|
placeholder={['Start Date', 'End Date']}
|
||
|
|
disabledDate={(current) => current && current > moment().endOf('day')}
|
||
|
|
onChange={async (dates) => {
|
||
|
|
setDates(dates);
|
||
|
|
const FromDate = dates?.[0]?.format('YYYY-MM-DD') || intialFromDate;
|
||
|
|
const ToDate = dates?.[1]?.format('YYYY-MM-DD') || intialToDate;
|
||
|
|
await fetchProducts({ AppId, CompId, BranchId, FromDate, ToDate, pageNumber: 1 });
|
||
|
|
setPage(1);
|
||
|
|
}}
|
||
|
|
value={dates}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div> */}
|
||
|
|
|
||
|
|
|
||
|
|
{/* Stats Cards */}
|
||
|
|
<div className="stats-grid">
|
||
|
|
<div className="stat-card1">
|
||
|
|
<div className="stat-content">
|
||
|
|
<div className="stat-text">
|
||
|
|
<p className="stat-label">Total Products</p>
|
||
|
|
<p className="stat-value">{products?.length}</p>
|
||
|
|
</div>
|
||
|
|
<Package className="stat-icon" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="stat-card2">
|
||
|
|
<div className="stat-content">
|
||
|
|
<div className="stat-text">
|
||
|
|
<p className="stat-label">Total Sales</p>
|
||
|
|
<p className="stat-value">
|
||
|
|
{products?.reduce((sum, p) => sum + (p?.TotalSoldQty || 0), 0)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<TrendingUp className="stat-icon" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="stat-card3">
|
||
|
|
<div className="stat-content">
|
||
|
|
<div className="stat-text">
|
||
|
|
<p className="stat-label">Total Revenue</p>
|
||
|
|
<p className="stat-value">
|
||
|
|
₹{products?.reduce((sum, p) => sum + (p?.TotalRevenue || 0), 0)?.toFixed(2)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<IndianRupee className="stat-icon" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Controls */}
|
||
|
|
<div className="controls-card">
|
||
|
|
<div className="controls-wrapper">
|
||
|
|
<div className="metric-controls">
|
||
|
|
<div
|
||
|
|
onClick={() => setMetric('sales')}
|
||
|
|
className={`btn-metric ${metric === 'sales' ? 'active' : ''}`}
|
||
|
|
>
|
||
|
|
<Package className="metric-icon" />
|
||
|
|
Sales Count
|
||
|
|
</div>
|
||
|
|
<div
|
||
|
|
onClick={() => setMetric('revenue')}
|
||
|
|
className={`btn-metric ${metric === 'revenue' ? 'active' : ''}`}
|
||
|
|
>
|
||
|
|
<IndianRupee className="metric-icon" />
|
||
|
|
Revenue
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="chart-type-controls">
|
||
|
|
{/* <Switch
|
||
|
|
checked={chartType === 'bar' ? true : false}
|
||
|
|
onChange={(checked) => setChartType(checked ? 'bar' : 'pie')}
|
||
|
|
checkedChildren="Bar Chart"
|
||
|
|
unCheckedChildren="Pie Chart"
|
||
|
|
className="bar-pie-switch"
|
||
|
|
value={chartType === 'bar' ? true : false}
|
||
|
|
/> */}
|
||
|
|
<div
|
||
|
|
onClick={() => setChartType('bar')}
|
||
|
|
className={`btn-chart-type ${chartType === 'bar' ? 'active' : ''}`}
|
||
|
|
>
|
||
|
|
Bar Chart
|
||
|
|
</div>
|
||
|
|
<div
|
||
|
|
onClick={() => setChartType('pie')}
|
||
|
|
className={`btn-chart-type ${chartType === 'pie' ? 'active' : ''}`}
|
||
|
|
>
|
||
|
|
Pie Chart
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/* {(dates?.[0] && dates?.[1]) && (
|
||
|
|
<div className='date-wrapper-class' style={{
|
||
|
|
display: "flex",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "flex-end",
|
||
|
|
gap: "1rem",
|
||
|
|
flexWrap: "wrap"
|
||
|
|
}}>
|
||
|
|
|
||
|
|
<div className='date-range'>{`${dates[0]?.format('DD/MMM/YYYY') || intialFromDate} - ${dates[1]?.format('DD/MMM/YYYY') || intialToDate}`}</div>
|
||
|
|
</div>
|
||
|
|
)} */}
|
||
|
|
|
||
|
|
<DropDowns
|
||
|
|
className={'item-range-dropdown'}
|
||
|
|
valueData={page}
|
||
|
|
options={options}
|
||
|
|
label={'Products'}
|
||
|
|
onChangeFunction={async (value) => {
|
||
|
|
setPage(value);
|
||
|
|
const FromDate = dates?.[0]?.format('YYYY-MM-DD') || intialFromDate;
|
||
|
|
const ToDate = dates?.[1]?.format('YYYY-MM-DD') || intialToDate;
|
||
|
|
await fetchProducts({ AppId, CompId, BranchId, FromDate, ToDate, pageNumber: value });
|
||
|
|
}}
|
||
|
|
isOnchanges={page ? true : false}
|
||
|
|
/>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{((initialDate ? true : (!dates && !dates?.[0] && !dates?.[1])) && intialFromDate && intialToDate) && <div className='date-range'>{`${dayjs(intialFromDate)?.format('DD/MMM/YYYY')} - ${dayjs(intialToDate)?.format('DD/MMM/YYYY')}`}</div>}
|
||
|
|
|
||
|
|
<div className='chart-dtls'>
|
||
|
|
|
||
|
|
{/* Chart */}
|
||
|
|
<div className='chart-section'>
|
||
|
|
|
||
|
|
|
||
|
|
<div className="chart-card" ref={chartWrapperRef}>
|
||
|
|
{loading && products?.length === 0 ? (
|
||
|
|
<div className="loading-container">
|
||
|
|
<div className="loading-spinner"></div>
|
||
|
|
</div>
|
||
|
|
) : chartData?.length === 0 ? (
|
||
|
|
<div className="no-data">
|
||
|
|
No data available
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
{chartType === 'bar' ? (
|
||
|
|
<div className='chart-data'>
|
||
|
|
<div className='chart-axis'>
|
||
|
|
<div></div>
|
||
|
|
<div className="axis-points">
|
||
|
|
{[
|
||
|
|
0,
|
||
|
|
Math.round(benchmarkValue * 0.25),
|
||
|
|
Math.round(benchmarkValue * 0.5),
|
||
|
|
Math.round(benchmarkValue * 0.75),
|
||
|
|
benchmarkValue
|
||
|
|
].map((num, i) => (
|
||
|
|
<div key={i} className="axis-label-wrapper">
|
||
|
|
<div className="axis-tick"></div>
|
||
|
|
<div className="axis-label">{num}</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div></div>
|
||
|
|
</div>
|
||
|
|
{chartData?.length > 0 ? (
|
||
|
|
<>
|
||
|
|
{chartData?.map((item, index) => (
|
||
|
|
<div
|
||
|
|
key={index}
|
||
|
|
className='chart-data-item'
|
||
|
|
onMouseEnter={(e) => {
|
||
|
|
setHoveredItem(item);
|
||
|
|
setMousePosition({ x: e.clientX, y: e.clientY });
|
||
|
|
}}
|
||
|
|
// style={{}}
|
||
|
|
onMouseLeave={() => setHoveredItem(null)}
|
||
|
|
onMouseMove={(e) => setMousePosition({ x: e.clientX, y: e.clientY })}
|
||
|
|
>
|
||
|
|
<div className='chart-data-item-name'><span>{`#${(index + 1) + ((page - 1) * 10)}`}</span><span>{item?.name}</span></div>
|
||
|
|
<div className='barcharLine' style={{ borderRadius: "1px" }}>
|
||
|
|
|
||
|
|
<div
|
||
|
|
className='barcharLineInside'
|
||
|
|
style={{
|
||
|
|
width: `${Math.max(5, (item?.value / benchmarkValue) * 100)}%`,
|
||
|
|
backgroundColor: COLORS[index % COLORS.length]
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<span className='bar-percentage'>
|
||
|
|
{/* {((item?.value / Math.max(...chartData.map(d => d?.value))) * 100).toFixed(1)}% */}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className='chart-data-item-value'>{item?.value}</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</>
|
||
|
|
|
||
|
|
) : (
|
||
|
|
<div className='chart-data'>
|
||
|
|
<div className='chart-data-item'>
|
||
|
|
<div className='chart-data-item-name'>No Data</div>
|
||
|
|
<div className='chart-data-item-value'>0</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
/* Replace your existing <Pie ...> with this */
|
||
|
|
/* PIE + RIGHT-LEGEND: replace the existing pie rendering */
|
||
|
|
<div className="pie-with-legend">
|
||
|
|
<div className="pie-wrapper">
|
||
|
|
<ResponsiveContainer width="100%" height={responsiveHeight}>
|
||
|
|
<PieChart>
|
||
|
|
<Pie
|
||
|
|
data={chartData}
|
||
|
|
cx="50%"
|
||
|
|
cy="50%"
|
||
|
|
labelLine={false}
|
||
|
|
label={windowWidth <= 600 ? false : renderPercentInside}
|
||
|
|
// outerRadius={pieOuterRadius}
|
||
|
|
innerRadius={pieInnerRadius - 50}
|
||
|
|
paddingAngle={1}
|
||
|
|
minAngle={minAngle}
|
||
|
|
dataKey="value"
|
||
|
|
>
|
||
|
|
{chartData.map((entry, index) => (
|
||
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||
|
|
))}
|
||
|
|
</Pie>
|
||
|
|
<Tooltip content={<CustomTooltip />} />
|
||
|
|
</PieChart>
|
||
|
|
</ResponsiveContainer>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="pie-legend">
|
||
|
|
{(() => {
|
||
|
|
const total = chartData.reduce((s, d) => s + (Number(d?.value || 0)), 0) || 1;
|
||
|
|
return chartData.map((d, i) => {
|
||
|
|
const color = COLORS[i % COLORS.length];
|
||
|
|
const value = Number(d?.value || 0);
|
||
|
|
const percent = total ? ((value / total) * 100) : 0;
|
||
|
|
return (
|
||
|
|
<div className="legend-row" key={i}>
|
||
|
|
<span className="legend-color" style={{ backgroundColor: color }} />
|
||
|
|
<div className="legend-text">
|
||
|
|
<div className="legend-title">{d?.name}</div>
|
||
|
|
<div className="legend-sub">
|
||
|
|
<span className="legend-value">{value}</span>
|
||
|
|
<span className="legend-percent" style={{ color: color }} >{percent.toFixed(2)}%</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
});
|
||
|
|
})()}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
)}
|
||
|
|
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{hoveredItem && (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
position: 'fixed',
|
||
|
|
left: mousePosition.x + 10,
|
||
|
|
top: mousePosition.y - 10,
|
||
|
|
zIndex: 1000,
|
||
|
|
pointerEvents: 'none'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<CustomTooltip active={true} payload={[{ payload: hoveredItem }]} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default TopSellingProducts;
|