feat: add admin sales statistics dashboard with 5 analytics tabs

- Add sales stats API client with typed interfaces for all endpoints
- Implement AdminSalesStats page with summary cards and period selector
- Add TrialsTab with provider breakdown donut chart and daily area chart
- Add SalesTab with tariff/period breakdown and daily revenue chart
- Add RenewalsTab with period comparison and trend indicators
- Add AddonsTab with package breakdown bar chart
- Add DepositsTab with payment method breakdown bar chart
- Add SimpleBarChart, SimpleAreaChart, DonutChart components
- Add PeriodSelector with preset periods and custom date range
- Full responsive design (mobile/tablet/desktop/TG Mini App)
- ARIA accessibility (tablist, tab, tabpanel, aria-selected)
- Error handling with isError on all queries
- Empty data guards on all chart components
- i18n support across all 4 locales (en, ru, zh, fa)
This commit is contained in:
Fringg
2026-03-02 20:34:39 +03:00
parent 673de08dd4
commit a47c222310
13 changed files with 1226 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import type { SalesStatsParams } from '../../api/adminSalesStats';
import { salesStatsApi } from '../../api/adminSalesStats';
import { SALES_STATS } from '../../constants/salesStats';
import { useCurrency } from '../../hooks/useCurrency';
import { StatCard } from '../stats';
import { DonutChart } from './DonutChart';
import { SimpleAreaChart } from './SimpleAreaChart';
import { SimpleBarChart } from './SimpleBarChart';
interface SalesTabProps {
params: SalesStatsParams;
}
export function SalesTab({ params }: SalesTabProps) {
const { t } = useTranslation();
const { formatWithCurrency } = useCurrency();
const { data, isLoading, isError } = useQuery({
queryKey: ['sales-stats', 'sales', params],
queryFn: () => salesStatsApi.getSales(params),
staleTime: SALES_STATS.STALE_TIME,
});
if (isLoading) {
return (
<div className="animate-pulse space-y-4">
{Array.from({ length: 3 }, (_, i) => (
<div key={i} className="h-24 rounded-xl bg-dark-800/30" />
))}
</div>
);
}
if (isError || !data) {
return <div className="py-8 text-center text-red-400">{t('admin.salesStats.loadError')}</div>;
}
const tariffBarData = data.by_tariff.map((item) => ({
name: item.tariff_name,
value: item.count,
}));
const periodPieData = data.by_period.map((item) => ({
name: `${item.period_days} ${t('admin.trafficUsage.days')}`,
value: item.count,
}));
const dailyData = data.daily.map((item) => ({
date: item.date,
value: item.revenue_kopeks / SALES_STATS.KOPEKS_DIVISOR,
}));
return (
<div className="space-y-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<StatCard label={t('admin.salesStats.sales.totalSales')} value={data.total_sales} />
<StatCard
label={t('admin.salesStats.sales.avgOrder')}
value={formatWithCurrency(data.avg_order_kopeks / SALES_STATS.KOPEKS_DIVISOR)}
valueClassName="text-success-400"
/>
<StatCard label={t('admin.salesStats.sales.topTariff')} value={data.top_tariff_name} />
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<SimpleBarChart data={tariffBarData} title={t('admin.salesStats.sales.byTariff')} />
<DonutChart data={periodPieData} title={t('admin.salesStats.sales.byPeriod')} />
</div>
<SimpleAreaChart
data={dailyData}
title={t('admin.salesStats.sales.dailyChart')}
chartId="sales-daily"
valueLabel={t('admin.salesStats.summary.revenue')}
/>
</div>
);
}