mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(sales-stats): конкретика в сводке
- «Активные подписки» теперь с % (нетто-изменение за период: новые платные − истёкшие; обычная дельта снимка была бы всегда 0). - Новая карточка «Новые платные подписки» (с дельтой период-к-периоду). - «Доход» больше не включает ручные пополнения (бэк отвязал). - Убрал «Средняя длит.» из триалов — была вечный 0 и непонятна. - Сводка 9 карточек: сетка 2/3 с orphan-fill на мобайле, без пустот.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1177,6 +1177,7 @@
|
||||
"activeSubs": "اشتراک فعال",
|
||||
"activeTrials": "آزمایشی فعال",
|
||||
"newTrials": "آزمایشی جدید",
|
||||
"newPaid": "اشتراکهای پولی جدید",
|
||||
"conversion": "تبدیل",
|
||||
"renewals": "تمدید",
|
||||
"addonRevenue": "درآمد خدمات اضافی",
|
||||
@@ -1187,7 +1188,6 @@
|
||||
"totalRegistrations": "ثبتنامها",
|
||||
"trialsIssued": "آزمایشی صادرشده",
|
||||
"conversion": "نرخ تبدیل",
|
||||
"avgDuration": "میانگین مدت",
|
||||
"byProvider": "بر اساس منبع",
|
||||
"dailyChart": "ثبتنام و آزمایشی روزانه",
|
||||
"registrations": "ثبتنام"
|
||||
|
||||
@@ -1326,6 +1326,7 @@
|
||||
"activeSubs": "Активные подписки",
|
||||
"activeTrials": "Активные триалы",
|
||||
"newTrials": "Новые триалы",
|
||||
"newPaid": "Новые платные подписки",
|
||||
"conversion": "Конверсия",
|
||||
"renewals": "Продления",
|
||||
"addonRevenue": "Доп. услуги",
|
||||
@@ -1336,7 +1337,6 @@
|
||||
"totalRegistrations": "Регистрации",
|
||||
"trialsIssued": "Выданные триалы",
|
||||
"conversion": "Конверсия",
|
||||
"avgDuration": "Средняя длит.",
|
||||
"byProvider": "По источнику регистрации",
|
||||
"dailyChart": "Регистрации и триалы по дням",
|
||||
"registrations": "Регистрации"
|
||||
|
||||
@@ -1177,6 +1177,7 @@
|
||||
"activeSubs": "活跃订阅",
|
||||
"activeTrials": "活跃试用",
|
||||
"newTrials": "新试用",
|
||||
"newPaid": "新增付费订阅",
|
||||
"conversion": "转化率",
|
||||
"renewals": "续订",
|
||||
"addonRevenue": "附加服务收入",
|
||||
@@ -1187,7 +1188,6 @@
|
||||
"totalRegistrations": "注册数",
|
||||
"trialsIssued": "已发放试用",
|
||||
"conversion": "转化率",
|
||||
"avgDuration": "平均时长",
|
||||
"byProvider": "按注册来源",
|
||||
"dailyChart": "每日注册与试用",
|
||||
"registrations": "注册"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user