Android_Retail/src/Pages/DashBoard/TopSellingProductsChart.jsx

525 lines
26 KiB
React
Raw Normal View History

2026-01-27 18:27:29 +05:30
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;