mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
Sweep 30 files: text/bg/border/ring/from/to/via/fill/stroke/shadow/divide/decoration/ outline/placeholder-red-N → -error-N. All red usages were semantically error/danger (no brand red), so tokens now flow through the design-system CSS variables and respond to palette overrides.
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
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 { StatCard } from '../stats';
|
|
|
|
import { DonutChart } from './DonutChart';
|
|
import { DualAreaChart } from './DualAreaChart';
|
|
|
|
interface TrialsTabProps {
|
|
params: SalesStatsParams;
|
|
}
|
|
|
|
const PROVIDER_LABELS: Record<string, string> = {
|
|
telegram: 'Telegram',
|
|
email: 'Email',
|
|
vk: 'VK',
|
|
yandex: 'Yandex',
|
|
google: 'Google',
|
|
discord: 'Discord',
|
|
};
|
|
|
|
export function TrialsTab({ params }: TrialsTabProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const { data, isLoading, isError } = useQuery({
|
|
queryKey: ['sales-stats', 'trials', params],
|
|
queryFn: () => salesStatsApi.getTrials(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-error-400">{t('admin.salesStats.loadError')}</div>;
|
|
}
|
|
|
|
const pieData = data.by_provider.map((item) => ({
|
|
name: PROVIDER_LABELS[item.provider] || item.provider,
|
|
value: item.count,
|
|
color: SALES_STATS.PROVIDER_COLORS[item.provider as keyof typeof SALES_STATS.PROVIDER_COLORS],
|
|
}));
|
|
|
|
const dualData = data.daily.map((item) => ({
|
|
date: item.date,
|
|
series1: item.registrations,
|
|
series2: item.trials,
|
|
}));
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
<StatCard
|
|
label={t('admin.salesStats.trials.totalRegistrations')}
|
|
value={data.total_registrations}
|
|
/>
|
|
<StatCard label={t('admin.salesStats.trials.total')} value={data.total_trials} />
|
|
<StatCard
|
|
label={t('admin.salesStats.trials.conversion')}
|
|
value={`${data.conversion_rate}%`}
|
|
valueClassName="text-success-400"
|
|
/>
|
|
<StatCard
|
|
label={t('admin.salesStats.trials.avgDuration')}
|
|
value={`${data.avg_trial_duration_days} ${t('admin.trafficUsage.days')}`}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
<DonutChart data={pieData} title={t('admin.salesStats.trials.byProvider')} />
|
|
<DualAreaChart
|
|
data={dualData}
|
|
title={t('admin.salesStats.trials.dailyChart')}
|
|
chartId="trials-daily"
|
|
series1Label={t('admin.salesStats.trials.totalRegistrations')}
|
|
series2Label={t('admin.salesStats.trials.trialsIssued')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|