mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
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:
@@ -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 ============
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -975,6 +975,7 @@
|
|||||||
"avgDeposit": "میانگین شارژ",
|
"avgDeposit": "میانگین شارژ",
|
||||||
"byMethod": "بر اساس روش پرداخت",
|
"byMethod": "بر اساس روش پرداخت",
|
||||||
"dailyChart": "شارژ روزانه",
|
"dailyChart": "شارژ روزانه",
|
||||||
|
"dailyByMethod": "شارژ روزانه بر اساس روش پرداخت",
|
||||||
"revenue": "درآمد"
|
"revenue": "درآمد"
|
||||||
},
|
},
|
||||||
"loadError": "بارگذاری آمار ناموفق بود"
|
"loadError": "بارگذاری آمار ناموفق بود"
|
||||||
|
|||||||
@@ -1173,6 +1173,7 @@
|
|||||||
"avgDeposit": "Средний чек",
|
"avgDeposit": "Средний чек",
|
||||||
"byMethod": "По способу оплаты",
|
"byMethod": "По способу оплаты",
|
||||||
"dailyChart": "Пополнения по дням",
|
"dailyChart": "Пополнения по дням",
|
||||||
|
"dailyByMethod": "Пополнения по дням по платёжкам",
|
||||||
"revenue": "Доход"
|
"revenue": "Доход"
|
||||||
},
|
},
|
||||||
"loadError": "Не удалось загрузить статистику"
|
"loadError": "Не удалось загрузить статистику"
|
||||||
|
|||||||
@@ -975,6 +975,7 @@
|
|||||||
"avgDeposit": "平均充值",
|
"avgDeposit": "平均充值",
|
||||||
"byMethod": "按支付方式",
|
"byMethod": "按支付方式",
|
||||||
"dailyChart": "每日充值",
|
"dailyChart": "每日充值",
|
||||||
|
"dailyByMethod": "每日按支付方式充值",
|
||||||
"revenue": "收入"
|
"revenue": "收入"
|
||||||
},
|
},
|
||||||
"loadError": "加载统计失败"
|
"loadError": "加载统计失败"
|
||||||
|
|||||||
Reference in New Issue
Block a user