revenuechart dynamic
This commit is contained in:
@@ -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,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[]>;
|
||||||
|
|||||||
Reference in New Issue
Block a user