diff --git a/src/components/sales-stats/BreakdownList.tsx b/src/components/sales-stats/BreakdownList.tsx
new file mode 100644
index 0000000..a3c7065
--- /dev/null
+++ b/src/components/sales-stats/BreakdownList.tsx
@@ -0,0 +1,80 @@
+import { type ReactNode, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { SALES_STATS } from '../../constants/salesStats';
+
+export interface BreakdownItem {
+ key: string;
+ label: string;
+ value: number;
+ color?: string;
+ icon?: ReactNode;
+}
+
+interface BreakdownListProps {
+ title: string;
+ items: BreakdownItem[];
+ /** Formats the value shown on the right (e.g. money). Defaults to the raw number. */
+ valueFormatter?: (value: number) => string;
+}
+
+/**
+ * Categorical breakdown rendered as a ranked list instead of a bar chart:
+ * icon/colour dot + label + value + share-% + a proportional mini bar. Far more
+ * readable than a recharts bar for "by method / by tariff / by package" data.
+ */
+export function BreakdownList({ title, items, valueFormatter }: BreakdownListProps) {
+ const { t } = useTranslation();
+
+ const { sorted, total, max } = useMemo(() => {
+ const sortedItems = [...items].sort((a, b) => b.value - a.value);
+ const totalValue = sortedItems.reduce((sum, item) => sum + item.value, 0);
+ const maxValue = sortedItems.length > 0 ? sortedItems[0].value : 0;
+ return { sorted: sortedItems, total: totalValue, max: maxValue };
+ }, [items]);
+
+ return (
+
+
{title}
+ {sorted.length === 0 ? (
+
{t('common.noData')}
+ ) : (
+
+ {sorted.map((item, index) => {
+ const color =
+ item.color || SALES_STATS.BAR_COLORS[index % SALES_STATS.BAR_COLORS.length];
+ const share = total > 0 ? (item.value / total) * 100 : 0;
+ const barWidth = max > 0 ? (item.value / max) * 100 : 0;
+ return (
+
+
+ {item.icon ?? (
+
+ )}
+
+ {item.label}
+
+
+ {valueFormatter ? valueFormatter(item.value) : item.value}
+
+
+ {share.toFixed(1)}%
+
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/src/components/sales-stats/DepositsTab.tsx b/src/components/sales-stats/DepositsTab.tsx
index e62599c..8aa3003 100644
--- a/src/components/sales-stats/DepositsTab.tsx
+++ b/src/components/sales-stats/DepositsTab.tsx
@@ -9,9 +9,11 @@ import { SALES_STATS } from '../../constants/salesStats';
import { useCurrency } from '../../hooks/useCurrency';
import { StatCard } from '../stats';
-import { MultiSeriesAreaChart } from './MultiSeriesAreaChart';
+import PaymentMethodIcon from '../PaymentMethodIcon';
+
+import { BreakdownList } from './BreakdownList';
import { SimpleAreaChart } from './SimpleAreaChart';
-import { SimpleBarChart } from './SimpleBarChart';
+import { StackedBarChart } from './StackedBarChart';
interface DepositsTabProps {
params: SalesStatsParams;
@@ -29,11 +31,13 @@ export function DepositsTab({ params }: DepositsTabProps) {
const formatValue = useCallback((v: number) => formatWithCurrency(v), [formatWithCurrency]);
- const methodBarData = useMemo(
+ const methodBreakdown = useMemo(
() =>
data?.by_method.map((item) => ({
- name: METHOD_LABELS[item.method] || item.method,
+ key: item.method,
+ label: METHOD_LABELS[item.method] || item.method,
value: item.amount_kopeks / SALES_STATS.KOPEKS_DIVISOR,
+ icon: ,
})) ?? [],
[data?.by_method],
);
@@ -89,9 +93,9 @@ export function DepositsTab({ params }: DepositsTabProps) {
/>
-
@@ -102,11 +106,9 @@ export function DepositsTab({ params }: DepositsTabProps) {
valueLabel={t('admin.salesStats.deposits.revenue')}
/>
-
diff --git a/src/components/sales-stats/SalesTab.tsx b/src/components/sales-stats/SalesTab.tsx
index 5b7bc44..58dc043 100644
--- a/src/components/sales-stats/SalesTab.tsx
+++ b/src/components/sales-stats/SalesTab.tsx
@@ -8,10 +8,10 @@ import { SALES_STATS } from '../../constants/salesStats';
import { useCurrency } from '../../hooks/useCurrency';
import { StatCard } from '../stats';
+import { BreakdownList } from './BreakdownList';
import { DonutChart } from './DonutChart';
-import { MultiSeriesAreaChart } from './MultiSeriesAreaChart';
import { SimpleAreaChart } from './SimpleAreaChart';
-import { SimpleBarChart } from './SimpleBarChart';
+import { StackedBarChart } from './StackedBarChart';
interface SalesTabProps {
params: SalesStatsParams;
@@ -48,8 +48,9 @@ export function SalesTab({ params }: SalesTabProps) {
return {t('admin.salesStats.loadError')}
;
}
- const tariffBarData = data.by_tariff.map((item) => ({
- name: item.tariff_name,
+ const tariffBreakdown = data.by_tariff.map((item) => ({
+ key: String(item.tariff_id),
+ label: item.tariff_name,
value: item.count,
}));
@@ -65,8 +66,13 @@ export function SalesTab({ params }: SalesTabProps) {
return (
-
+
+
-
+
@@ -87,11 +93,10 @@ export function SalesTab({ params }: SalesTabProps) {
valueLabel={t('admin.salesStats.summary.revenue')}
/>
- String(v)}
/>
);
diff --git a/src/components/sales-stats/StackedBarChart.tsx b/src/components/sales-stats/StackedBarChart.tsx
new file mode 100644
index 0000000..8a13edb
--- /dev/null
+++ b/src/components/sales-stats/StackedBarChart.tsx
@@ -0,0 +1,135 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Legend,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+
+import { SALES_STATS } from '../../constants/salesStats';
+import { useChartColors } from '../../hooks/useChartColors';
+
+interface StackedBarChartProps {
+ data: { date: string; key: string; value: number }[];
+ title: string;
+ valueFormatter?: (value: number) => string;
+ height?: number;
+}
+
+/**
+ * Composition-over-time as a STACKED bar chart. Replaces the overlapping
+ * multi-series area chart for "daily by method / by tariff" — stacked bars read
+ * far cleaner (no layered translucent areas hiding each other) and the tooltip
+ * shows every series for the hovered day plus the day total.
+ */
+export function StackedBarChart({
+ data,
+ title,
+ valueFormatter,
+ height = SALES_STATS.CHART.HEIGHT,
+}: StackedBarChartProps) {
+ const { t, i18n } = useTranslation();
+ const colors = useChartColors();
+
+ const { chartData, seriesKeys } = useMemo(() => {
+ const dateMap = new Map
>();
+ const keySet = new Set();
+
+ for (const item of data) {
+ keySet.add(item.key);
+ const existing = dateMap.get(item.date) || {};
+ existing[item.key] = (existing[item.key] || 0) + item.value;
+ dateMap.set(item.date, existing);
+ }
+
+ const keys = Array.from(keySet).sort();
+ const sortedDates = Array.from(dateMap.keys()).sort();
+ const pivoted = sortedDates.map((date) => {
+ const row: Record = {
+ date,
+ label: new Date(date + 'T00:00:00').toLocaleDateString(i18n.language, {
+ month: 'short',
+ day: 'numeric',
+ }),
+ };
+ const values = dateMap.get(date) || {};
+ for (const key of keys) {
+ row[key] = values[key] || 0;
+ }
+ return row;
+ });
+
+ return { chartData: pivoted, seriesKeys: keys };
+ }, [data, i18n.language]);
+
+ if (data.length === 0) {
+ return (
+
+
{title}
+
+ {t('common.noData')}
+
+
+ );
+ }
+
+ return (
+
+
{title}
+
+
+
+
+
+ [
+ valueFormatter ? valueFormatter(value ?? 0) : (value ?? 0),
+ name || '',
+ ]}
+ />
+
+ {seriesKeys.map((key, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/locales/en.json b/src/locales/en.json
index 0976dba..d29d7fa 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1317,6 +1317,7 @@
},
"sales": {
"totalSales": "Total Sales",
+ "totalRevenue": "Revenue",
"avgOrder": "Avg Order",
"topTariff": "Top Tariff",
"byTariff": "Sales by Tariff",
diff --git a/src/locales/fa.json b/src/locales/fa.json
index 7c4e281..ac32a24 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -1190,6 +1190,7 @@
},
"sales": {
"totalSales": "کل فروش",
+ "totalRevenue": "درآمد",
"avgOrder": "میانگین سفارش",
"topTariff": "تعرفه برتر",
"byTariff": "بر اساس تعرفه",
diff --git a/src/locales/ru.json b/src/locales/ru.json
index bf8081d..5e95ba5 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -1339,6 +1339,7 @@
},
"sales": {
"totalSales": "Всего продаж",
+ "totalRevenue": "Выручка",
"avgOrder": "Средний чек",
"topTariff": "Топ тариф",
"byTariff": "Продажи по тарифам",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index bcc6376..69ffe95 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -1190,6 +1190,7 @@
},
"sales": {
"totalSales": "总销售",
+ "totalRevenue": "营收",
"avgOrder": "平均订单",
"topTariff": "热门套餐",
"byTariff": "按套餐",