feat: add daily deposits by payment method chart

Add MultiSeriesAreaChart showing daily deposit amounts per payment
method with human-readable labels and currency formatting. Memoize
all derived data before early returns for React Compiler compat.
This commit is contained in:
Fringg
2026-03-02 22:08:16 +03:00
parent 2235b3cb77
commit f012710df0
7 changed files with 55 additions and 12 deletions

View File

@@ -149,12 +149,19 @@ export interface DailyDepositItem {
amount_kopeks: number; amount_kopeks: number;
} }
export interface DailyDepositByMethodItem {
date: string;
method: string;
amount_kopeks: number;
}
export interface DepositsStats { export interface DepositsStats {
total_deposits: number; total_deposits: number;
total_amount_kopeks: number; total_amount_kopeks: number;
avg_deposit_kopeks: number; avg_deposit_kopeks: number;
by_method: DepositByMethodItem[]; by_method: DepositByMethodItem[];
daily: DailyDepositItem[]; daily: DailyDepositItem[];
daily_by_method: DailyDepositByMethodItem[];
} }
// ============ API ============ // ============ API ============

View File

@@ -1,3 +1,4 @@
import { useCallback, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -7,6 +8,7 @@ import { SALES_STATS } from '../../constants/salesStats';
import { useCurrency } from '../../hooks/useCurrency'; import { useCurrency } from '../../hooks/useCurrency';
import { StatCard } from '../stats'; import { StatCard } from '../stats';
import { MultiSeriesAreaChart } from './MultiSeriesAreaChart';
import { SimpleAreaChart } from './SimpleAreaChart'; import { SimpleAreaChart } from './SimpleAreaChart';
import { SimpleBarChart } from './SimpleBarChart'; import { SimpleBarChart } from './SimpleBarChart';
@@ -39,6 +41,36 @@ export function DepositsTab({ params }: DepositsTabProps) {
staleTime: SALES_STATS.STALE_TIME, staleTime: SALES_STATS.STALE_TIME,
}); });
const formatValue = useCallback((v: number) => formatWithCurrency(v), [formatWithCurrency]);
const methodBarData = useMemo(
() =>
data?.by_method.map((item) => ({
name: METHOD_LABELS[item.method] || item.method,
value: item.amount_kopeks / SALES_STATS.KOPEKS_DIVISOR,
})) ?? [],
[data?.by_method],
);
const dailyData = useMemo(
() =>
data?.daily.map((item) => ({
date: item.date,
value: item.amount_kopeks / SALES_STATS.KOPEKS_DIVISOR,
})) ?? [],
[data?.daily],
);
const dailyByMethodData = useMemo(
() =>
data?.daily_by_method.map((i) => ({
date: i.date,
key: METHOD_LABELS[i.method] || i.method,
value: i.amount_kopeks / SALES_STATS.KOPEKS_DIVISOR,
})) ?? [],
[data?.daily_by_method],
);
if (isLoading) { if (isLoading) {
return ( return (
<div className="animate-pulse space-y-4"> <div className="animate-pulse space-y-4">
@@ -53,16 +85,6 @@ export function DepositsTab({ params }: DepositsTabProps) {
return <div className="py-8 text-center text-red-400">{t('admin.salesStats.loadError')}</div>; return <div className="py-8 text-center text-red-400">{t('admin.salesStats.loadError')}</div>;
} }
const methodBarData = data.by_method.map((item) => ({
name: METHOD_LABELS[item.method] || item.method,
value: item.amount_kopeks / SALES_STATS.KOPEKS_DIVISOR,
}));
const dailyData = data.daily.map((item) => ({
date: item.date,
value: item.amount_kopeks / SALES_STATS.KOPEKS_DIVISOR,
}));
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
@@ -84,7 +106,7 @@ export function DepositsTab({ params }: DepositsTabProps) {
<SimpleBarChart <SimpleBarChart
data={methodBarData} data={methodBarData}
title={t('admin.salesStats.deposits.byMethod')} title={t('admin.salesStats.deposits.byMethod')}
valueFormatter={(v) => formatWithCurrency(v)} valueFormatter={formatValue}
/> />
<SimpleAreaChart <SimpleAreaChart
@@ -93,6 +115,14 @@ export function DepositsTab({ params }: DepositsTabProps) {
chartId="deposits-daily" chartId="deposits-daily"
valueLabel={t('admin.salesStats.deposits.revenue')} valueLabel={t('admin.salesStats.deposits.revenue')}
/> />
<MultiSeriesAreaChart
data={dailyByMethodData}
title={t('admin.salesStats.deposits.dailyByMethod')}
chartId="deposits-daily-by-method"
valueLabel={t('admin.salesStats.deposits.revenue')}
valueFormatter={formatValue}
/>
</div> </div>
); );
} }

View File

@@ -19,6 +19,7 @@ interface MultiSeriesAreaChartProps {
title: string; title: string;
chartId: string; chartId: string;
valueLabel?: string; valueLabel?: string;
valueFormatter?: (value: number) => string;
height?: number; height?: number;
} }
@@ -27,6 +28,7 @@ export function MultiSeriesAreaChart({
title, title,
chartId, chartId,
valueLabel, valueLabel,
valueFormatter,
height = SALES_STATS.CHART.HEIGHT, height = SALES_STATS.CHART.HEIGHT,
}: MultiSeriesAreaChartProps) { }: MultiSeriesAreaChartProps) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@@ -129,7 +131,7 @@ export function MultiSeriesAreaChart({
}} }}
labelStyle={{ color: colors.label }} labelStyle={{ color: colors.label }}
formatter={(value: number | undefined, name: string | undefined) => [ formatter={(value: number | undefined, name: string | undefined) => [
value ?? 0, valueFormatter ? valueFormatter(value ?? 0) : (value ?? 0),
name || valueLabel || '', name || valueLabel || '',
]} ]}
/> />

View File

@@ -1152,6 +1152,7 @@
"avgDeposit": "Avg Deposit", "avgDeposit": "Avg Deposit",
"byMethod": "By Payment Method", "byMethod": "By Payment Method",
"dailyChart": "Daily Deposits", "dailyChart": "Daily Deposits",
"dailyByMethod": "Daily Deposits by Payment Method",
"revenue": "Revenue" "revenue": "Revenue"
}, },
"loadError": "Failed to load statistics" "loadError": "Failed to load statistics"

View File

@@ -975,6 +975,7 @@
"avgDeposit": "میانگین شارژ", "avgDeposit": "میانگین شارژ",
"byMethod": "بر اساس روش پرداخت", "byMethod": "بر اساس روش پرداخت",
"dailyChart": "شارژ روزانه", "dailyChart": "شارژ روزانه",
"dailyByMethod": "شارژ روزانه بر اساس روش پرداخت",
"revenue": "درآمد" "revenue": "درآمد"
}, },
"loadError": "بارگذاری آمار ناموفق بود" "loadError": "بارگذاری آمار ناموفق بود"

View File

@@ -1173,6 +1173,7 @@
"avgDeposit": "Средний чек", "avgDeposit": "Средний чек",
"byMethod": "По способу оплаты", "byMethod": "По способу оплаты",
"dailyChart": "Пополнения по дням", "dailyChart": "Пополнения по дням",
"dailyByMethod": "Пополнения по дням по платёжкам",
"revenue": "Доход" "revenue": "Доход"
}, },
"loadError": "Не удалось загрузить статистику" "loadError": "Не удалось загрузить статистику"

View File

@@ -975,6 +975,7 @@
"avgDeposit": "平均充值", "avgDeposit": "平均充值",
"byMethod": "按支付方式", "byMethod": "按支付方式",
"dailyChart": "每日充值", "dailyChart": "每日充值",
"dailyByMethod": "每日按支付方式充值",
"revenue": "收入" "revenue": "收入"
}, },
"loadError": "加载统计失败" "loadError": "加载统计失败"