mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 10:03:46 +00:00
feat(sales-stats): карточки возвратов в сводке
Две карточки — количество и сумма возвратов за период (красный тон). Сетку сводки перевёл на 2/5 (10 карточек делятся ровно, без пустых ячеек). StatCard получил invertDelta: для «меньше — лучше» метрик рост красится в красный, падение в зелёный (стрелка по факту). 4 локали.
This commit is contained in:
@@ -19,6 +19,8 @@ export interface SalesSummary {
|
|||||||
trial_to_paid_conversion: number;
|
trial_to_paid_conversion: number;
|
||||||
renewals_count: number;
|
renewals_count: number;
|
||||||
addon_revenue_kopeks: number;
|
addon_revenue_kopeks: number;
|
||||||
|
refunds_count: number;
|
||||||
|
refunds_kopeks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Trials ============
|
// ============ Trials ============
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ interface StatCardProps {
|
|||||||
valueClassName?: string;
|
valueClassName?: string;
|
||||||
/** Optional period-over-period change shown under the value. */
|
/** Optional period-over-period change shown under the value. */
|
||||||
delta?: StatCardDelta | null;
|
delta?: StatCardDelta | null;
|
||||||
|
/** For "lower is better" metrics (e.g. refunds): keep the real arrow but flip
|
||||||
|
* the colour, so a rise reads as bad (red) and a drop as good (green). */
|
||||||
|
invertDelta?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatCard({
|
export function StatCard({
|
||||||
@@ -35,10 +38,18 @@ export function StatCard({
|
|||||||
tone = 'neutral',
|
tone = 'neutral',
|
||||||
valueClassName,
|
valueClassName,
|
||||||
delta,
|
delta,
|
||||||
|
invertDelta = false,
|
||||||
}: StatCardProps) {
|
}: StatCardProps) {
|
||||||
const toneStyle = TONE[tone];
|
const toneStyle = TONE[tone];
|
||||||
const valueClass = valueClassName ?? toneStyle.value;
|
const valueClass = valueClassName ?? toneStyle.value;
|
||||||
const trendStyle = delta ? (TREND_STYLES[delta.trend] ?? TREND_STYLES.stable) : null;
|
const baseTrend = delta ? (TREND_STYLES[delta.trend] ?? TREND_STYLES.stable) : null;
|
||||||
|
const trendStyle =
|
||||||
|
baseTrend && invertDelta && delta && delta.trend !== 'stable'
|
||||||
|
? {
|
||||||
|
arrow: baseTrend.arrow,
|
||||||
|
className: TREND_STYLES[delta.trend === 'up' ? 'down' : 'up'].className,
|
||||||
|
}
|
||||||
|
: baseTrend;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-dark-800/30 p-3 transition-colors hover:bg-dark-800/50">
|
<div className="rounded-xl bg-dark-800/30 p-3 transition-colors hover:bg-dark-800/50">
|
||||||
|
|||||||
@@ -1306,7 +1306,9 @@
|
|||||||
"conversion": "Conversion",
|
"conversion": "Conversion",
|
||||||
"renewals": "Renewals",
|
"renewals": "Renewals",
|
||||||
"addonRevenue": "Add-on Revenue",
|
"addonRevenue": "Add-on Revenue",
|
||||||
"manualTopup": "Manual Top-ups"
|
"manualTopup": "Manual Top-ups",
|
||||||
|
"refundsCount": "Refunds",
|
||||||
|
"refundsAmount": "Refunded amount"
|
||||||
},
|
},
|
||||||
"trials": {
|
"trials": {
|
||||||
"total": "Total Trials",
|
"total": "Total Trials",
|
||||||
|
|||||||
@@ -1179,7 +1179,9 @@
|
|||||||
"conversion": "تبدیل",
|
"conversion": "تبدیل",
|
||||||
"renewals": "تمدید",
|
"renewals": "تمدید",
|
||||||
"addonRevenue": "درآمد خدمات اضافی",
|
"addonRevenue": "درآمد خدمات اضافی",
|
||||||
"manualTopup": "شارژ دستی"
|
"manualTopup": "شارژ دستی",
|
||||||
|
"refundsCount": "بازپرداختها",
|
||||||
|
"refundsAmount": "مبلغ بازپرداخت"
|
||||||
},
|
},
|
||||||
"trials": {
|
"trials": {
|
||||||
"total": "کل آزمایشی",
|
"total": "کل آزمایشی",
|
||||||
|
|||||||
@@ -1328,7 +1328,9 @@
|
|||||||
"conversion": "Конверсия",
|
"conversion": "Конверсия",
|
||||||
"renewals": "Продления",
|
"renewals": "Продления",
|
||||||
"addonRevenue": "Доп. услуги",
|
"addonRevenue": "Доп. услуги",
|
||||||
"manualTopup": "Ручные пополнения"
|
"manualTopup": "Ручные пополнения",
|
||||||
|
"refundsCount": "Возвраты",
|
||||||
|
"refundsAmount": "Сумма возвратов"
|
||||||
},
|
},
|
||||||
"trials": {
|
"trials": {
|
||||||
"total": "Всего триалов",
|
"total": "Всего триалов",
|
||||||
|
|||||||
@@ -1179,7 +1179,9 @@
|
|||||||
"conversion": "转化率",
|
"conversion": "转化率",
|
||||||
"renewals": "续订",
|
"renewals": "续订",
|
||||||
"addonRevenue": "附加服务收入",
|
"addonRevenue": "附加服务收入",
|
||||||
"manualTopup": "手动充值"
|
"manualTopup": "手动充值",
|
||||||
|
"refundsCount": "退款",
|
||||||
|
"refundsAmount": "退款金额"
|
||||||
},
|
},
|
||||||
"trials": {
|
"trials": {
|
||||||
"total": "试用总数",
|
"total": "试用总数",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
GiftIcon,
|
GiftIcon,
|
||||||
PercentIcon,
|
PercentIcon,
|
||||||
RepeatIcon,
|
RepeatIcon,
|
||||||
|
ResetIcon,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
TicketIcon,
|
TicketIcon,
|
||||||
@@ -115,6 +116,7 @@ export default function AdminSalesStats() {
|
|||||||
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),
|
||||||
|
refunds: computeDelta(summary.refunds_kopeks, prevSummary.refunds_kopeks),
|
||||||
};
|
};
|
||||||
}, [summary, prevSummary]);
|
}, [summary, prevSummary]);
|
||||||
|
|
||||||
@@ -148,7 +150,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-5">
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.salesStats.summary.revenue')}
|
label={t('admin.salesStats.summary.revenue')}
|
||||||
value={
|
value={
|
||||||
@@ -223,6 +225,24 @@ export default function AdminSalesStats() {
|
|||||||
tone="warning"
|
tone="warning"
|
||||||
delta={deltas?.manualTopup}
|
delta={deltas?.manualTopup}
|
||||||
/>
|
/>
|
||||||
|
<StatCard
|
||||||
|
label={t('admin.salesStats.summary.refundsCount')}
|
||||||
|
value={summaryLoading ? '...' : (summary?.refunds_count ?? 0)}
|
||||||
|
icon={<ResetIcon className="h-5 w-5" />}
|
||||||
|
tone="error"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label={t('admin.salesStats.summary.refundsAmount')}
|
||||||
|
value={
|
||||||
|
summaryLoading
|
||||||
|
? '...'
|
||||||
|
: formatWithCurrency((summary?.refunds_kopeks ?? 0) / SALES_STATS.KOPEKS_DIVISOR, 0)
|
||||||
|
}
|
||||||
|
icon={<BanknotesIcon className="h-5 w-5" />}
|
||||||
|
tone="error"
|
||||||
|
delta={deltas?.refunds}
|
||||||
|
invertDelta
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
|
|||||||
Reference in New Issue
Block a user