feat(sales-stats): пресет «Этот месяц» (с 1-го числа) и он же по умолчанию

Статистика по умолчанию открывается за текущий месяц с 1-го числа до сегодня
(month-to-date), а не за фиксированные 30 дней. Добавлена кнопка «Этот месяц»
в селектор периода; util getMonthToDateRange/isMonthToDate.
This commit is contained in:
c0mrade
2026-06-02 15:55:10 +03:00
parent 80ac63102b
commit 9a05f23a04
7 changed files with 50 additions and 24 deletions

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SALES_STATS } from '../../constants/salesStats'; import { SALES_STATS } from '../../constants/salesStats';
import { getMonthToDateRange, isMonthToDate } from '../../utils/period';
interface PeriodSelectorProps { interface PeriodSelectorProps {
value: { days?: number; startDate?: string; endDate?: string }; value: { days?: number; startDate?: string; endDate?: string };
@@ -24,46 +25,44 @@ export function PeriodSelector({ value, onChange }: PeriodSelectorProps) {
onChange({ days }); onChange({ days });
}; };
const handleCustomToggle = () => { const handleThisMonth = () => {
setShowCustom((prev) => !prev); setShowCustom(false);
onChange({ days: undefined, ...getMonthToDateRange() });
}; };
const handleCustomToggle = () => setShowCustom((prev) => !prev);
const handleDateChange = (field: 'startDate' | 'endDate', dateStr: string) => { const handleDateChange = (field: 'startDate' | 'endDate', dateStr: string) => {
onChange({ onChange({ ...value, days: undefined, [field]: dateStr });
...value,
days: undefined,
[field]: dateStr,
});
}; };
const isPresetActive = (days: number) => !showCustom && value.days === days; const buttonClass = (active: boolean) =>
`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
active
? 'bg-accent-500/20 text-accent-400'
: 'bg-dark-800/50 text-dark-400 hover:bg-dark-700/50 hover:text-dark-300'
}`;
const mtdActive = !showCustom && isMonthToDate(value);
return ( return (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={handleThisMonth} className={buttonClass(mtdActive)}>
{t('admin.salesStats.period.thisMonth')}
</button>
{SALES_STATS.PERIOD_PRESETS.map((days) => ( {SALES_STATS.PERIOD_PRESETS.map((days) => (
<button <button
key={days} key={days}
type="button" type="button"
onClick={() => handlePreset(days)} onClick={() => handlePreset(days)}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${ className={buttonClass(!showCustom && value.days === days)}
isPresetActive(days)
? 'bg-accent-500/20 text-accent-400'
: 'bg-dark-800/50 text-dark-400 hover:bg-dark-700/50 hover:text-dark-300'
}`}
> >
{presetLabels[days]} {presetLabels[days]}
</button> </button>
))} ))}
<button <button type="button" onClick={handleCustomToggle} className={buttonClass(showCustom)}>
type="button"
onClick={handleCustomToggle}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
showCustom
? 'bg-accent-500/20 text-accent-400'
: 'bg-dark-800/50 text-dark-400 hover:bg-dark-700/50 hover:text-dark-300'
}`}
>
{t('admin.salesStats.period.custom')} {t('admin.salesStats.period.custom')}
</button> </button>
@@ -75,7 +74,7 @@ export function PeriodSelector({ value, onChange }: PeriodSelectorProps) {
onChange={(e) => handleDateChange('startDate', e.target.value)} onChange={(e) => handleDateChange('startDate', e.target.value)}
className="rounded-lg border border-dark-600 bg-dark-800 px-2 py-1 text-sm text-dark-200" className="rounded-lg border border-dark-600 bg-dark-800 px-2 py-1 text-sm text-dark-200"
/> />
<span className="text-dark-500">{'\u2014'}</span> <span className="text-dark-500">{''}</span>
<input <input
type="date" type="date"
value={value.endDate || ''} value={value.endDate || ''}

View File

@@ -1282,6 +1282,7 @@
"title": "Sales Statistics", "title": "Sales Statistics",
"subtitle": "Revenue, trials, subscriptions, and renewals", "subtitle": "Revenue, trials, subscriptions, and renewals",
"period": { "period": {
"thisMonth": "This month",
"week": "7 days", "week": "7 days",
"month": "30 days", "month": "30 days",
"quarter": "90 days", "quarter": "90 days",

View File

@@ -1155,6 +1155,7 @@
"title": "آمار فروش", "title": "آمار فروش",
"subtitle": "درآمد، آزمایشی، اشتراک و تمدید", "subtitle": "درآمد، آزمایشی، اشتراک و تمدید",
"period": { "period": {
"thisMonth": "این ماه",
"week": "۷ روز", "week": "۷ روز",
"month": "۳۰ روز", "month": "۳۰ روز",
"quarter": "۹۰ روز", "quarter": "۹۰ روز",

View File

@@ -1304,6 +1304,7 @@
"title": "Статистика продаж", "title": "Статистика продаж",
"subtitle": "Доход, триалы, подписки и продления", "subtitle": "Доход, триалы, подписки и продления",
"period": { "period": {
"thisMonth": "Этот месяц",
"week": "7 дней", "week": "7 дней",
"month": "30 дней", "month": "30 дней",
"quarter": "90 дней", "quarter": "90 дней",

View File

@@ -1155,6 +1155,7 @@
"title": "销售统计", "title": "销售统计",
"subtitle": "收入、试用、订阅和续订", "subtitle": "收入、试用、订阅和续订",
"period": { "period": {
"thisMonth": "本月",
"week": "7天", "week": "7天",
"month": "30天", "month": "30天",
"quarter": "90天", "quarter": "90天",

View File

@@ -6,6 +6,7 @@ import type { SalesStatsParams } from '../api/adminSalesStats';
import { salesStatsApi } from '../api/adminSalesStats'; import { salesStatsApi } from '../api/adminSalesStats';
import { SALES_STATS } from '../constants/salesStats'; import { SALES_STATS } from '../constants/salesStats';
import { useCurrency } from '../hooks/useCurrency'; import { useCurrency } from '../hooks/useCurrency';
import { getMonthToDateRange } from '../utils/period';
import { AdminBackButton } from '../components/admin/AdminBackButton'; import { AdminBackButton } from '../components/admin/AdminBackButton';
import { import {
BanknotesIcon, BanknotesIcon,
@@ -72,7 +73,7 @@ export default function AdminSalesStats() {
days?: number; days?: number;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
}>({ days: SALES_STATS.DEFAULT_PERIOD }); }>(() => getMonthToDateRange());
const params: SalesStatsParams = useMemo( const params: SalesStatsParams = useMemo(
() => ({ () => ({

22
src/utils/period.ts Normal file
View File

@@ -0,0 +1,22 @@
/** First day of the current month → today, as local YYYY-MM-DD strings.
* Used as the default sales-stats window ("this month so far"). */
export function getMonthToDateRange(): { startDate: string; endDate: string } {
const now = new Date();
const toLocalISODate = (d: Date) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
return {
startDate: toLocalISODate(new Date(now.getFullYear(), now.getMonth(), 1)),
endDate: toLocalISODate(now),
};
}
/** True when the period is exactly "from the 1st of the current month to today". */
export function isMonthToDate(period: {
days?: number;
startDate?: string;
endDate?: string;
}): boolean {
if (period.days !== undefined) return false;
const mtd = getMonthToDateRange();
return period.startDate === mtd.startDate && period.endDate === mtd.endDate;
}