feat(sales-stats): конкретика в сводке

- «Активные подписки» теперь с % (нетто-изменение за период: новые
  платные − истёкшие; обычная дельта снимка была бы всегда 0).
- Новая карточка «Новые платные подписки» (с дельтой период-к-периоду).
- «Доход» больше не включает ручные пополнения (бэк отвязал).
- Убрал «Средняя длит.» из триалов — была вечный 0 и непонятна.
- Сводка 9 карточек: сетка 2/3 с orphan-fill на мобайле, без пустот.
This commit is contained in:
c0mrade
2026-06-02 19:38:38 +03:00
parent 436d9f860c
commit 3adda6fa03
7 changed files with 33 additions and 14 deletions

View File

@@ -16,6 +16,8 @@ export interface SalesSummary {
active_subscriptions: number; active_subscriptions: number;
active_trials: number; active_trials: number;
new_trials: number; new_trials: number;
new_paid_subscriptions: number;
expired_subscriptions: number;
trial_to_paid_conversion: number; trial_to_paid_conversion: number;
renewals_count: number; renewals_count: number;
addon_revenue_kopeks: number; addon_revenue_kopeks: number;

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import type { SalesStatsParams } from '../../api/adminSalesStats'; import type { SalesStatsParams } from '../../api/adminSalesStats';
import { salesStatsApi } from '../../api/adminSalesStats'; import { salesStatsApi } from '../../api/adminSalesStats';
import { SALES_STATS } from '../../constants/salesStats'; import { SALES_STATS } from '../../constants/salesStats';
import { ClockIcon, GiftIcon, PercentIcon, UserPlusIcon } from '../../components/icons'; import { GiftIcon, PercentIcon, UserPlusIcon } from '../../components/icons';
import { StatCard } from '../stats'; import { StatCard } from '../stats';
import { DonutChart } from './DonutChart'; import { DonutChart } from './DonutChart';
@@ -61,7 +61,7 @@ export function TrialsTab({ params }: TrialsTabProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<StatCard <StatCard
label={t('admin.salesStats.trials.totalRegistrations')} label={t('admin.salesStats.trials.totalRegistrations')}
value={data.total_registrations} value={data.total_registrations}
@@ -80,12 +80,6 @@ export function TrialsTab({ params }: TrialsTabProps) {
icon={<PercentIcon className="h-5 w-5" />} icon={<PercentIcon className="h-5 w-5" />}
tone="success" tone="success"
/> />
<StatCard
label={t('admin.salesStats.trials.avgDuration')}
value={`${data.avg_trial_duration_days} ${t('admin.trafficUsage.days')}`}
icon={<ClockIcon className="h-5 w-5" />}
tone="neutral"
/>
</div> </div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">

View File

@@ -1304,6 +1304,7 @@
"activeSubs": "Active Subs", "activeSubs": "Active Subs",
"activeTrials": "Active Trials", "activeTrials": "Active Trials",
"newTrials": "New Trials", "newTrials": "New Trials",
"newPaid": "New Paid Subscriptions",
"conversion": "Conversion", "conversion": "Conversion",
"renewals": "Renewals", "renewals": "Renewals",
"addonRevenue": "Add-on Revenue", "addonRevenue": "Add-on Revenue",
@@ -1314,7 +1315,6 @@
"totalRegistrations": "Registrations", "totalRegistrations": "Registrations",
"trialsIssued": "Trials Issued", "trialsIssued": "Trials Issued",
"conversion": "Conversion Rate", "conversion": "Conversion Rate",
"avgDuration": "Avg Duration",
"byProvider": "By Registration Source", "byProvider": "By Registration Source",
"dailyChart": "Daily Registrations & Trials", "dailyChart": "Daily Registrations & Trials",
"registrations": "Registrations" "registrations": "Registrations"

View File

@@ -1177,6 +1177,7 @@
"activeSubs": "اشتراک فعال", "activeSubs": "اشتراک فعال",
"activeTrials": "آزمایشی فعال", "activeTrials": "آزمایشی فعال",
"newTrials": "آزمایشی جدید", "newTrials": "آزمایشی جدید",
"newPaid": "اشتراک‌های پولی جدید",
"conversion": "تبدیل", "conversion": "تبدیل",
"renewals": "تمدید", "renewals": "تمدید",
"addonRevenue": "درآمد خدمات اضافی", "addonRevenue": "درآمد خدمات اضافی",
@@ -1187,7 +1188,6 @@
"totalRegistrations": "ثبت‌نام‌ها", "totalRegistrations": "ثبت‌نام‌ها",
"trialsIssued": "آزمایشی صادرشده", "trialsIssued": "آزمایشی صادرشده",
"conversion": "نرخ تبدیل", "conversion": "نرخ تبدیل",
"avgDuration": "میانگین مدت",
"byProvider": "بر اساس منبع", "byProvider": "بر اساس منبع",
"dailyChart": "ثبت‌نام و آزمایشی روزانه", "dailyChart": "ثبت‌نام و آزمایشی روزانه",
"registrations": "ثبت‌نام" "registrations": "ثبت‌نام"

View File

@@ -1326,6 +1326,7 @@
"activeSubs": "Активные подписки", "activeSubs": "Активные подписки",
"activeTrials": "Активные триалы", "activeTrials": "Активные триалы",
"newTrials": "Новые триалы", "newTrials": "Новые триалы",
"newPaid": "Новые платные подписки",
"conversion": "Конверсия", "conversion": "Конверсия",
"renewals": "Продления", "renewals": "Продления",
"addonRevenue": "Доп. услуги", "addonRevenue": "Доп. услуги",
@@ -1336,7 +1337,6 @@
"totalRegistrations": "Регистрации", "totalRegistrations": "Регистрации",
"trialsIssued": "Выданные триалы", "trialsIssued": "Выданные триалы",
"conversion": "Конверсия", "conversion": "Конверсия",
"avgDuration": "Средняя длит.",
"byProvider": "По источнику регистрации", "byProvider": "По источнику регистрации",
"dailyChart": "Регистрации и триалы по дням", "dailyChart": "Регистрации и триалы по дням",
"registrations": "Регистрации" "registrations": "Регистрации"

View File

@@ -1177,6 +1177,7 @@
"activeSubs": "活跃订阅", "activeSubs": "活跃订阅",
"activeTrials": "活跃试用", "activeTrials": "活跃试用",
"newTrials": "新试用", "newTrials": "新试用",
"newPaid": "新增付费订阅",
"conversion": "转化率", "conversion": "转化率",
"renewals": "续订", "renewals": "续订",
"addonRevenue": "附加服务收入", "addonRevenue": "附加服务收入",
@@ -1187,7 +1188,6 @@
"totalRegistrations": "注册数", "totalRegistrations": "注册数",
"trialsIssued": "已发放试用", "trialsIssued": "已发放试用",
"conversion": "转化率", "conversion": "转化率",
"avgDuration": "平均时长",
"byProvider": "按注册来源", "byProvider": "按注册来源",
"dailyChart": "每日注册与试用", "dailyChart": "每日注册与试用",
"registrations": "注册" "registrations": "注册"

View File

@@ -12,6 +12,7 @@ import {
BanknotesIcon, BanknotesIcon,
GiftIcon, GiftIcon,
PercentIcon, PercentIcon,
PlusIcon,
RepeatIcon, RepeatIcon,
RocketIcon, RocketIcon,
SparklesIcon, SparklesIcon,
@@ -113,12 +114,26 @@ export default function AdminSalesStats() {
return { return {
revenue: computeDelta(summary.total_revenue_kopeks, prevSummary.total_revenue_kopeks), revenue: computeDelta(summary.total_revenue_kopeks, prevSummary.total_revenue_kopeks),
newTrials: computeDelta(summary.new_trials, prevSummary.new_trials), newTrials: computeDelta(summary.new_trials, prevSummary.new_trials),
newPaid: computeDelta(summary.new_paid_subscriptions, prevSummary.new_paid_subscriptions),
renewals: computeDelta(summary.renewals_count, prevSummary.renewals_count), renewals: computeDelta(summary.renewals_count, prevSummary.renewals_count),
addonRevenue: computeDelta(summary.addon_revenue_kopeks, prevSummary.addon_revenue_kopeks), addonRevenue: computeDelta(summary.addon_revenue_kopeks, prevSummary.addon_revenue_kopeks),
manualTopup: computeDelta(summary.manual_topup_kopeks, prevSummary.manual_topup_kopeks), manualTopup: computeDelta(summary.manual_topup_kopeks, prevSummary.manual_topup_kopeks),
}; };
}, [summary, prevSummary]); }, [summary, prevSummary]);
// Active-subscriptions trend = net change over the period (new paid expired),
// shown relative to the count at the start of the period. The active count
// itself is a "right now" snapshot, so a plain period-over-period delta would
// always be zero — this shows real growth/shrinkage instead.
const activeDelta = useMemo<Delta | null>(() => {
if (!summary) return null;
const net = summary.new_paid_subscriptions - summary.expired_subscriptions;
if (net === 0) return null;
const base = summary.active_subscriptions - net;
const percent = base > 0 ? Math.round((Math.abs(net) / base) * 1000) / 10 : 100;
return { percent, trend: net > 0 ? 'up' : 'down' };
}, [summary]);
const tabs: { id: TabId; label: string }[] = [ const tabs: { id: TabId; label: string }[] = [
{ id: 'trials', label: t('admin.salesStats.tabs.trials') }, { id: 'trials', label: t('admin.salesStats.tabs.trials') },
{ id: 'sales', label: t('admin.salesStats.tabs.sales') }, { id: 'sales', label: t('admin.salesStats.tabs.sales') },
@@ -150,7 +165,7 @@ export default function AdminSalesStats() {
{t('admin.salesStats.loadError')} {t('admin.salesStats.loadError')}
</div> </div>
)} )}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 max-sm:[&>*:last-child:nth-child(odd)]:col-span-2">
<StatCard <StatCard
label={t('admin.salesStats.summary.revenue')} label={t('admin.salesStats.summary.revenue')}
value={ value={
@@ -170,6 +185,14 @@ export default function AdminSalesStats() {
value={summaryLoading ? '...' : (summary?.active_subscriptions ?? 0)} value={summaryLoading ? '...' : (summary?.active_subscriptions ?? 0)}
icon={<TicketIcon className="h-5 w-5" />} icon={<TicketIcon className="h-5 w-5" />}
tone="accent" tone="accent"
delta={activeDelta}
/>
<StatCard
label={t('admin.salesStats.summary.newPaid')}
value={summaryLoading ? '...' : (summary?.new_paid_subscriptions ?? 0)}
icon={<RocketIcon className="h-5 w-5" />}
tone="success"
delta={deltas?.newPaid}
/> />
<StatCard <StatCard
label={t('admin.salesStats.summary.activeTrials')} label={t('admin.salesStats.summary.activeTrials')}
@@ -207,7 +230,7 @@ export default function AdminSalesStats() {
0, 0,
) )
} }
icon={<RocketIcon className="h-5 w-5" />} icon={<PlusIcon className="h-5 w-5" />}
tone="accent" tone="accent"
delta={deltas?.addonRevenue} delta={deltas?.addonRevenue}
/> />