revenuechart dynamic

This commit is contained in:
hamid zarghami
2025-12-23 14:09:28 +03:30
parent 860502f160
commit 14bc394801
4 changed files with 257 additions and 136 deletions
+216 -135
View File
@@ -1,5 +1,5 @@
import { ArrowUp, DocumentDownload } from 'iconsax-react' import { ArrowUp, DocumentDownload, ArrowDown } from 'iconsax-react'
import { type FC, useState } from 'react' import { type FC, useState, useMemo } from 'react'
import { import {
Area, Area,
AreaChart, AreaChart,
@@ -11,32 +11,79 @@ import {
} from 'recharts' } from 'recharts'
import DatePicker from '@/components/DatePicker' import DatePicker from '@/components/DatePicker'
import Button from '@/components/Button' import Button from '@/components/Button'
import { useGetPaymentsChart } from '../hooks/useStatsData'
import { formatPrice } from '@/helpers/func'
import PageLoading from '@/components/PageLoading'
interface RevenueChartProps { const RevenueChart: FC = () => {
data?: Array<{ month: string; online: number; cash: number }>
}
const RevenueChart: FC<RevenueChartProps> = ({ data }) => {
const [selectedTab, setSelectedTab] = useState< const [selectedTab, setSelectedTab] = useState<
'daily' | 'monthly' | 'yearly' 'daily' | 'monthly' | 'yearly'
>('monthly') >('monthly')
const [startDate, setStartDate] = useState<string>('')
const [endDate, setEndDate] = useState<string>('')
const defaultData = [ const apiParams = useMemo(() => {
{ month: 'فروردین', online: 10000000, cash: 8000000 }, const params: {
{ month: 'اردیبهشت', online: 15000000, cash: 11000000 }, period?: 'daily' | 'monthly' | 'yearly';
{ month: 'خرداد', online: 12000000, cash: 9000000 }, startDate?: string;
{ month: 'تیر', online: 18000000, cash: 13000000 }, endDate?: string;
{ month: 'مرداد', online: 16000000, cash: 12000000 }, } = {
{ month: 'شهریور', online: 22000000, cash: 15000000 }, period: selectedTab,
{ month: 'مهر', online: 20000000, cash: 14000000 }, }
{ month: 'آبان', online: 17000000, cash: 13000000 }, if (startDate) params.startDate = startDate
{ month: 'آذر', online: 14000000, cash: 11000000 }, if (endDate) params.endDate = endDate
{ month: 'دی', online: 12000000, cash: 10000000 }, return params
{ month: 'بهمن', online: 18000000, cash: 13000000 }, }, [selectedTab, startDate, endDate])
{ month: 'اسفند', online: 16000000, cash: 12000000 }
]
const chartData = data || defaultData const { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams)
const formatDateLabel = (dateString: string, period: 'daily' | 'monthly' | 'yearly'): string => {
const date = new Date(dateString)
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
}
if (period === 'monthly') {
options.month = 'long'
} else if (period === 'daily') {
options.month = 'short'
options.day = 'numeric'
}
const persianDate = new Intl.DateTimeFormat('fa-IR', options).format(date)
return persianDate
}
const chartData = useMemo(() => {
if (!paymentsChartData?.data) return []
return paymentsChartData.data.map((item) => ({
month: formatDateLabel(item.date, selectedTab),
online: item.online,
cash: item.cash,
}))
}, [paymentsChartData?.data, selectedTab])
const xAxisInterval = useMemo(() => {
const dataLength = chartData.length
if (dataLength <= 7) return 0
if (dataLength <= 15) return 1
if (dataLength <= 30) return 2
if (dataLength <= 60) return 4
return Math.floor(dataLength / 12)
}, [chartData.length])
const totalRevenue = useMemo(() => {
return chartData.reduce((sum, item) => sum + item.online + item.cash, 0)
}, [chartData])
const revenueChange = useMemo(() => {
if (chartData.length < 2) return null
const currentPeriod = chartData[chartData.length - 1]
const previousPeriod = chartData[chartData.length - 2]
const currentTotal = currentPeriod.online + currentPeriod.cash
const previousTotal = previousPeriod.online + previousPeriod.cash
if (previousTotal === 0) return null
const change = ((currentTotal - previousTotal) / previousTotal) * 100
return change
}, [chartData])
const formatYAxis = (value: number) => { const formatYAxis = (value: number) => {
if (value === 0) return '۰' if (value === 0) return '۰'
@@ -55,6 +102,14 @@ const RevenueChart: FC<RevenueChartProps> = ({ data }) => {
return value.toLocaleString('fa-IR') + ' تومان' return value.toLocaleString('fa-IR') + ' تومان'
} }
if (isLoading) {
return (
<div className='bg-white rounded-3xl p-6 shadow-sm h-full overflow-visible flex items-center justify-center min-h-[600px]'>
<PageLoading />
</div>
)
}
return ( return (
<div className='bg-white rounded-3xl p-6 shadow-sm h-full overflow-visible'> <div className='bg-white rounded-3xl p-6 shadow-sm h-full overflow-visible'>
{/* Header */} {/* Header */}
@@ -67,13 +122,13 @@ const RevenueChart: FC<RevenueChartProps> = ({ data }) => {
<div className='flex items-end gap-2'> <div className='flex items-end gap-2'>
<DatePicker <DatePicker
label='تاریخ شروع' label='تاریخ شروع'
placeholder='mm/dd/yyyy' placeholder='انتخاب تاریخ شروع'
onChange={() => { }} onChange={setStartDate}
/> />
<DatePicker <DatePicker
label='تاریخ پایان' label='تاریخ پایان'
placeholder='mm/dd/yyyy' placeholder='انتخاب تاریخ پایان'
onChange={() => { }} onChange={setEndDate}
/> />
<Button className=''> <Button className=''>
<div className='flex gap-3'> <div className='flex gap-3'>
@@ -121,21 +176,40 @@ const RevenueChart: FC<RevenueChartProps> = ({ data }) => {
<div> <div>
<span className='text-sm text-gray-500'>مجموع درآمد</span> <span className='text-sm text-gray-500'>مجموع درآمد</span>
</div> </div>
<div className='flex items-center justify-between mb-5'> <div className='flex items-center justify-between mb-5'>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<p className='text-2xl font-bold'>
<p className='text-2xl font-bold'>۲۰,۰۰۰,۰۰۰ تومان</p> {formatPrice(totalRevenue)}
<div className='flex items-center gap-1 px-2 py-0.5 bg-green-50 rounded-full'> </p>
<ArrowUp {revenueChange !== null && (
size={12} <div
color='#10b981' className={`flex items-center gap-1 px-2 py-0.5 rounded-full ${revenueChange >= 0
/> ? 'bg-green-50'
<span className='text-xs font-medium text-green-600'> : 'bg-red-50'
2,5% }`}
</span> >
</div> {revenueChange >= 0 ? (
<ArrowUp
size={12}
color='#10b981'
/>
) : (
<ArrowDown
size={12}
color='#ef4444'
/>
)}
<span
className={`text-xs font-medium ${revenueChange >= 0
? 'text-green-600'
: 'text-red-600'
}`}
>
{Math.abs(revenueChange).toFixed(1)}%
</span>
</div>
)}
</div> </div>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
@@ -155,103 +229,110 @@ const RevenueChart: FC<RevenueChartProps> = ({ data }) => {
</div> </div>
{/* نمودار */} {/* نمودار */}
<div className='w-full h-[400px] min-h-[400px]'> {chartData.length === 0 ? (
<ResponsiveContainer width='100%' height='100%'> <div className='w-full h-[400px] min-h-[400px] flex items-center justify-center text-gray-500'>
<AreaChart دادهای برای نمایش وجود ندارد
data={chartData} </div>
margin={{ top: 10, right: 10, left: 10, bottom: 40 }} ) : (
> <div className='w-full h-[400px] min-h-[400px]'>
<defs> <ResponsiveContainer width='100%' height='100%'>
<linearGradient <AreaChart
id='colorOnline' data={chartData}
x1='0' margin={{ top: 10, right: 10, left: 10, bottom: 40 }}
y1='0' >
x2='0' <defs>
y2='1' <linearGradient
> id='colorOnline'
<stop x1='0'
offset='5%' y1='0'
stopColor='#6B7FED' x2='0'
stopOpacity={0.3} y2='1'
/> >
<stop <stop
offset='95%' offset='5%'
stopColor='#6B7FED' stopColor='#6B7FED'
stopOpacity={0} stopOpacity={0.3}
/> />
</linearGradient> <stop
<linearGradient offset='95%'
id='colorCash' stopColor='#6B7FED'
x1='0' stopOpacity={0}
y1='0' />
x2='0' </linearGradient>
y2='1' <linearGradient
> id='colorCash'
<stop x1='0'
offset='5%' y1='0'
stopColor='#B0BAC9' x2='0'
stopOpacity={0.2} y2='1'
/> >
<stop <stop
offset='95%' offset='5%'
stopColor='#B0BAC9' stopColor='#B0BAC9'
stopOpacity={0} stopOpacity={0.2}
/> />
</linearGradient> <stop
</defs> offset='95%'
<CartesianGrid stopColor='#B0BAC9'
strokeDasharray='0' stopOpacity={0}
stroke='#f0f0f0' />
vertical={false} </linearGradient>
/> </defs>
<XAxis <CartesianGrid
dataKey='month' strokeDasharray='0'
axisLine={false} stroke='#f0f0f0'
tickLine={false} vertical={false}
tick={{ fill: '#9CA3AF', fontSize: 11 }} />
reversed <XAxis
interval={0} dataKey='month'
angle={0} axisLine={false}
textAnchor='middle' tickLine={false}
height={40} tick={{ fill: '#9CA3AF', fontSize: 11 }}
/> reversed
<YAxis interval={xAxisInterval}
orientation='right' angle={chartData.length > 15 ? -45 : 0}
axisLine={false} textAnchor={chartData.length > 15 ? 'end' : 'middle'}
tickLine={false} height={chartData.length > 15 ? 60 : 40}
tick={{ fill: '#9CA3AF', fontSize: 10 }} minTickGap={10}
tickFormatter={formatYAxis} />
domain={[0, 'dataMax + 2000000']} <YAxis
tickMargin={50} orientation='right'
/> axisLine={false}
<Tooltip tickLine={false}
formatter={formatTooltip} tick={{ fill: '#9CA3AF', fontSize: 10 }}
contentStyle={{ tickFormatter={formatYAxis}
backgroundColor: 'white', domain={[0, 'dataMax + 2000000']}
border: '1px solid #e5e7eb', tickMargin={50}
borderRadius: '12px', />
fontSize: '12px' <Tooltip
}} formatter={formatTooltip}
/> contentStyle={{
<Area backgroundColor: 'white',
type='natural' border: '1px solid #e5e7eb',
dataKey='cash' borderRadius: '12px',
stroke='#B0BAC9' fontSize: '12px'
strokeWidth={2} }}
fillOpacity={1} />
fill='url(#colorCash)' <Area
/> type='natural'
<Area dataKey='cash'
type='natural' stroke='#B0BAC9'
dataKey='online' strokeWidth={2}
stroke='#6B7FED' fillOpacity={1}
strokeWidth={2} fill='url(#colorCash)'
fillOpacity={1} />
fill='url(#colorOnline)' <Area
/> type='natural'
</AreaChart> dataKey='online'
</ResponsiveContainer> stroke='#6B7FED'
</div> strokeWidth={2}
fillOpacity={1}
fill='url(#colorOnline)'
/>
</AreaChart>
</ResponsiveContainer>
</div>
)}
</div> </div>
) )
} }
@@ -14,3 +14,16 @@ export const useGetFoodSalesPieChart = () => {
queryFn: api.getFoodSalesPieChart, queryFn: api.getFoodSalesPieChart,
}); });
}; };
interface GetPaymentsChartParams {
period?: "daily" | "monthly" | "yearly";
startDate?: string;
endDate?: string;
}
export const useGetPaymentsChart = (params?: GetPaymentsChartParams) => {
return useQuery({
queryKey: ["payments-chart", params],
queryFn: () => api.getPaymentsChart(params),
});
};
@@ -1,5 +1,8 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import type { IStatisticsResponse } from "../types/Types"; import type {
IStatisticsResponse,
IPaymentChartResponse,
} from "../types/Types";
export const getStatistics = async (): Promise<IStatisticsResponse> => { export const getStatistics = async (): Promise<IStatisticsResponse> => {
const { data } = await axios.get<IStatisticsResponse>("/admin/orders/stats"); const { data } = await axios.get<IStatisticsResponse>("/admin/orders/stats");
@@ -10,3 +13,19 @@ export const getFoodSalesPieChart = async () => {
const { data } = await axios.get("/admin/orders/food-sales-pie-chart"); const { data } = await axios.get("/admin/orders/food-sales-pie-chart");
return data; return data;
}; };
interface GetPaymentsChartParams {
period?: "daily" | "monthly" | "yearly";
startDate?: string;
endDate?: string;
}
export const getPaymentsChart = async (
params?: GetPaymentsChartParams
): Promise<IPaymentChartResponse> => {
const { data } = await axios.get<IPaymentChartResponse>(
"/admin/payments/chart",
{ params }
);
return data;
};
+8
View File
@@ -8,3 +8,11 @@ export interface IStatisticsData {
} }
export type IStatisticsResponse = IResponse<IStatisticsData>; export type IStatisticsResponse = IResponse<IStatisticsData>;
export interface IPaymentChartItem {
date: string;
cash: number;
online: number;
}
export type IPaymentChartResponse = IResponse<IPaymentChartItem[]>;