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_trials: number;
new_trials: number;
new_paid_subscriptions: number;
expired_subscriptions: number;
trial_to_paid_conversion: number;
renewals_count: number;
addon_revenue_kopeks: number;

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import type { SalesStatsParams } from '../../api/adminSalesStats';
import { salesStatsApi } from '../../api/adminSalesStats';
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 { DonutChart } from './DonutChart';
@@ -61,7 +61,7 @@ export function TrialsTab({ params }: TrialsTabProps) {
return (
<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
label={t('admin.salesStats.trials.totalRegistrations')}
value={data.total_registrations}
@@ -80,12 +80,6 @@ export function TrialsTab({ params }: TrialsTabProps) {
icon={<PercentIcon className="h-5 w-5" />}
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 className="grid grid-cols-1 gap-4 lg:grid-cols-2">

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import {
BanknotesIcon,
GiftIcon,
PercentIcon,
PlusIcon,
RepeatIcon,
RocketIcon,
SparklesIcon,
@@ -113,12 +114,26 @@ export default function AdminSalesStats() {
return {
revenue: computeDelta(summary.total_revenue_kopeks, prevSummary.total_revenue_kopeks),
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),
addonRevenue: computeDelta(summary.addon_revenue_kopeks, prevSummary.addon_revenue_kopeks),
manualTopup: computeDelta(summary.manual_topup_kopeks, prevSummary.manual_topup_kopeks),
};
}, [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 }[] = [
{ id: 'trials', label: t('admin.salesStats.tabs.trials') },
{ id: 'sales', label: t('admin.salesStats.tabs.sales') },
@@ -150,7 +165,7 @@ export default function AdminSalesStats() {
{t('admin.salesStats.loadError')}
</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
label={t('admin.salesStats.summary.revenue')}
value={
@@ -170,6 +185,14 @@ export default function AdminSalesStats() {
value={summaryLoading ? '...' : (summary?.active_subscriptions ?? 0)}
icon={<TicketIcon className="h-5 w-5" />}
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
label={t('admin.salesStats.summary.activeTrials')}
@@ -207,7 +230,7 @@ export default function AdminSalesStats() {
0,
)
}
icon={<RocketIcon className="h-5 w-5" />}
icon={<PlusIcon className="h-5 w-5" />}
tone="accent"
delta={deltas?.addonRevenue}
/>