mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
feat(sales-stats): пресет «Этот месяц» (с 1-го числа) и он же по умолчанию
Статистика по умолчанию открывается за текущий месяц с 1-го числа до сегодня (month-to-date), а не за фиксированные 30 дней. Добавлена кнопка «Этот месяц» в селектор периода; util getMonthToDateRange/isMonthToDate.
This commit is contained in:
@@ -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 || ''}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1155,6 +1155,7 @@
|
|||||||
"title": "آمار فروش",
|
"title": "آمار فروش",
|
||||||
"subtitle": "درآمد، آزمایشی، اشتراک و تمدید",
|
"subtitle": "درآمد، آزمایشی، اشتراک و تمدید",
|
||||||
"period": {
|
"period": {
|
||||||
|
"thisMonth": "این ماه",
|
||||||
"week": "۷ روز",
|
"week": "۷ روز",
|
||||||
"month": "۳۰ روز",
|
"month": "۳۰ روز",
|
||||||
"quarter": "۹۰ روز",
|
"quarter": "۹۰ روز",
|
||||||
|
|||||||
@@ -1304,6 +1304,7 @@
|
|||||||
"title": "Статистика продаж",
|
"title": "Статистика продаж",
|
||||||
"subtitle": "Доход, триалы, подписки и продления",
|
"subtitle": "Доход, триалы, подписки и продления",
|
||||||
"period": {
|
"period": {
|
||||||
|
"thisMonth": "Этот месяц",
|
||||||
"week": "7 дней",
|
"week": "7 дней",
|
||||||
"month": "30 дней",
|
"month": "30 дней",
|
||||||
"quarter": "90 дней",
|
"quarter": "90 дней",
|
||||||
|
|||||||
@@ -1155,6 +1155,7 @@
|
|||||||
"title": "销售统计",
|
"title": "销售统计",
|
||||||
"subtitle": "收入、试用、订阅和续订",
|
"subtitle": "收入、试用、订阅和续订",
|
||||||
"period": {
|
"period": {
|
||||||
|
"thisMonth": "本月",
|
||||||
"week": "7天",
|
"week": "7天",
|
||||||
"month": "30天",
|
"month": "30天",
|
||||||
"quarter": "90天",
|
"quarter": "90天",
|
||||||
|
|||||||
@@ -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
22
src/utils/period.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user