From 2fd013d282c5179ac1cd648f91ad194a8837acb4 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 24 Dec 2025 15:18:21 +0330 Subject: [PATCH] chart edited --- index.html | 2 +- public/robots.txt | 3 + src/components/DatePicker.tsx | 9 +- .../statistics/components/FoodSalesChart.tsx | 270 +++++++++++------- .../statistics/components/RevenueChart.tsx | 80 ++++-- src/pages/statistics/hooks/useStatsData.ts | 14 +- .../statistics/service/StatisticsService.ts | 16 +- src/pages/statistics/types/Types.ts | 10 + 8 files changed, 275 insertions(+), 129 deletions(-) create mode 100644 public/robots.txt diff --git a/index.html b/index.html index 7305ced..adaf877 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - Vite + React + TS + پنل مدیریت دی منو diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..6ffbc30 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: / + diff --git a/src/components/DatePicker.tsx b/src/components/DatePicker.tsx index 1725c9b..6e2e365 100644 --- a/src/components/DatePicker.tsx +++ b/src/components/DatePicker.tsx @@ -28,8 +28,15 @@ const DatePickerComponent: FC = (props: Props) => { useEffect(() => { if (value) { - const formattedDate = `${value.year}-${value.month.number}-${value.day}`; + // تبدیل تاریخ شمسی به میلادی + const gregorianDate = value.toDate(); + const year = gregorianDate.getFullYear(); + const month = String(gregorianDate.getMonth() + 1).padStart(2, '0'); + const day = String(gregorianDate.getDate()).padStart(2, '0'); + const formattedDate = `${year}-${month}-${day}`; props.onChange(formattedDate); + } else { + props.onChange(''); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); diff --git a/src/pages/statistics/components/FoodSalesChart.tsx b/src/pages/statistics/components/FoodSalesChart.tsx index 3e5b9dd..c3e3c1a 100644 --- a/src/pages/statistics/components/FoodSalesChart.tsx +++ b/src/pages/statistics/components/FoodSalesChart.tsx @@ -1,119 +1,187 @@ -import { type FC } from 'react' +import { type FC, useMemo } from 'react' import { Cell, LabelList, Pie, PieChart, ResponsiveContainer } from 'recharts' -import { useGetFoodSalesPieChart } from '../hooks/useStatsData'; +import { useGetFoodSalesPieChart } from '../hooks/useStatsData' +import PageLoading from '@/components/PageLoading' -interface FoodSalesChartProps { - data?: Array<{ name: string; value: number; color: string }> -} +const COLORS = ['#6B7FED', '#A78BFA', '#FF6B9D', '#FF9A62', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'] -const FoodSalesChart: FC = ({ data }) => { +const FoodSalesChart: FC = () => { - useGetFoodSalesPieChart() - const defaultData = [ - { name: 'پیتزا', value: 50, color: '#6B7FED' }, - { name: 'برگر', value: 25, color: '#A78BFA' }, - { name: 'پاستا', value: 15, color: '#FF6B9D' }, - { name: 'نوشیدنی ها', value: 10, color: '#FF9A62' } - ] + const { data: apiData, isLoading } = useGetFoodSalesPieChart() - const chartData = data || defaultData + const chartData = useMemo(() => { + if (!apiData?.data || apiData.data.length === 0) return [] - const total = chartData.reduce((sum, item) => sum + item.value, 0) + return apiData.data.map((item, index) => ({ + name: item.foodTitle, + value: item.quantity, + color: COLORS[index % COLORS.length], + revenue: item.revenue, + percentage: item.percentage + })) + }, [apiData?.data]) - const dataWithPercent = chartData.map((item) => ({ - ...item, - percent: Math.round((item.value / total) * 100) - })) + const total = useMemo(() => { + return chartData.reduce((sum, item) => sum + item.value, 0) + }, [chartData]) + + const dataWithPercent = useMemo(() => { + return chartData.map((item) => ({ + ...item, + percent: total > 0 ? Math.round((item.value / total) * 100) : 0 + })) + }, [chartData, total]) + + if (isLoading) { + return ( +
+ +
+ ) + } return (

میزان فروش غذاها

-
- - - - {dataWithPercent.map((entry, index) => ( - - ))} - { - const labelProps = props as { - x?: number | string - y?: number | string - value?: number | string - } - if ( - labelProps.x === undefined || - labelProps.y === undefined || - labelProps.value === undefined - ) { - return null - } - const x = - typeof labelProps.x === 'number' - ? labelProps.x - : parseFloat( - String(labelProps.x) - ) || 0 - const y = - typeof labelProps.y === 'number' - ? labelProps.y - : parseFloat( - String(labelProps.y) - ) || 0 - return ( - - {labelProps.value}% - - ) - }} - /> - - - -
+ {/* Filters */} + {/*
+ + +
*/} -
- {dataWithPercent.map((item, index) => ( -
-
- - {item.name} - + {/* Tabs */} + {/*
+ + + +
*/} + + {dataWithPercent.length === 0 ? ( +
+ داده‌ای برای نمایش وجود ندارد +
+ ) : ( + <> +
+ + + + {dataWithPercent.map((entry, index) => ( + + ))} + { + const labelProps = props as { + x?: number | string + y?: number | string + value?: number | string + } + if ( + labelProps.x === undefined || + labelProps.y === undefined || + labelProps.value === undefined + ) { + return null + } + const x = + typeof labelProps.x === 'number' + ? labelProps.x + : parseFloat( + String(labelProps.x) + ) || 0 + const y = + typeof labelProps.y === 'number' + ? labelProps.y + : parseFloat( + String(labelProps.y) + ) || 0 + return ( + + {labelProps.value}% + + ) + }} + /> + + +
- ))} -
+ +
+ {dataWithPercent.map((item, index) => ( +
+
+ + {item.name} + +
+ ))} +
+ + )}
) } diff --git a/src/pages/statistics/components/RevenueChart.tsx b/src/pages/statistics/components/RevenueChart.tsx index 12ca6fc..55d2ba3 100644 --- a/src/pages/statistics/components/RevenueChart.tsx +++ b/src/pages/statistics/components/RevenueChart.tsx @@ -16,7 +16,7 @@ import { formatPrice } from '@/helpers/func' import PageLoading from '@/components/PageLoading' const RevenueChart: FC = () => { - const [selectedTab, setSelectedTab] = useState< + const [selectedTab] = useState< 'daily' | 'monthly' | 'yearly' >('monthly') const [startDate, setStartDate] = useState('') @@ -27,13 +27,12 @@ const RevenueChart: FC = () => { period?: 'daily' | 'monthly' | 'yearly'; startDate?: string; endDate?: string; - } = { - period: selectedTab, - } - if (startDate) params.startDate = startDate - if (endDate) params.endDate = endDate + } = {} + // فقط اگر تاریخ انتخاب شده باشد ارسال شود + if (startDate && startDate.trim() !== '') params.startDate = startDate + if (endDate && endDate.trim() !== '') params.endDate = endDate return params - }, [selectedTab, startDate, endDate]) + }, [startDate, endDate]) const { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams) @@ -97,9 +96,56 @@ const RevenueChart: FC = () => { return value.toLocaleString('fa-IR') } - const formatTooltip = (value: number | undefined) => { - if (value === undefined) return '' - return value.toLocaleString('fa-IR') + ' تومان' + const CustomTooltip = (props: { + active?: boolean + payload?: Array<{ + dataKey?: string + value?: number + color?: string + }> + label?: string + }) => { + const { active, payload, label } = props + if (!active || !payload || payload.length === 0) { + return null + } + + const cashValue = payload.find((item) => item.dataKey === 'cash')?.value as number | undefined + const onlineValue = payload.find((item) => item.dataKey === 'online')?.value as number | undefined + + return ( +
+

{label}

+
+ {cashValue !== undefined && ( +
+
+ پرداخت نقدی: + + {cashValue.toLocaleString('fa-IR')} تومان + +
+ )} + {onlineValue !== undefined && ( +
+
+ پرداخت آنلاین: + + {onlineValue.toLocaleString('fa-IR')} تومان + +
+ )} + {cashValue !== undefined && onlineValue !== undefined && ( +
+ جمع: + + {(cashValue + onlineValue).toLocaleString('fa-IR')} تومان + +
+ )} +
+
+ ) } if (isLoading) { @@ -142,7 +188,7 @@ const RevenueChart: FC = () => {
{/* Tabs */} -
+ {/*
-
+
*/} {/* مبلغ و درصد */} @@ -304,15 +350,7 @@ const RevenueChart: FC = () => { domain={[0, 'dataMax + 2000000']} tickMargin={50} /> - + } /> { }); }; -export const useGetFoodSalesPieChart = () => { +interface GetFoodSalesPieChartParams { + period?: "daily" | "monthly" | "yearly"; + startDate?: string; + endDate?: string; +} + +export const useGetFoodSalesPieChart = ( + params?: GetFoodSalesPieChartParams +) => { return useQuery({ - queryKey: ["food-sales-pie-chart"], - queryFn: api.getFoodSalesPieChart, + queryKey: ["food-sales-pie-chart", params], + queryFn: () => api.getFoodSalesPieChart(params), }); }; diff --git a/src/pages/statistics/service/StatisticsService.ts b/src/pages/statistics/service/StatisticsService.ts index 4a989f7..e37df40 100644 --- a/src/pages/statistics/service/StatisticsService.ts +++ b/src/pages/statistics/service/StatisticsService.ts @@ -2,6 +2,7 @@ import axios from "@/config/axios"; import type { IStatisticsResponse, IPaymentChartResponse, + IFoodSalesPieChartResponse, } from "../types/Types"; export const getStatistics = async (): Promise => { @@ -9,8 +10,19 @@ export const getStatistics = async (): Promise => { return data; }; -export const getFoodSalesPieChart = async () => { - const { data } = await axios.get("/admin/orders/food-sales-pie-chart"); +interface GetFoodSalesPieChartParams { + period?: "daily" | "monthly" | "yearly"; + startDate?: string; + endDate?: string; +} + +export const getFoodSalesPieChart = async ( + params?: GetFoodSalesPieChartParams +): Promise => { + const { data } = await axios.get( + "/admin/orders/food-sales-pie-chart", + { params } + ); return data; }; diff --git a/src/pages/statistics/types/Types.ts b/src/pages/statistics/types/Types.ts index 2442200..7959649 100644 --- a/src/pages/statistics/types/Types.ts +++ b/src/pages/statistics/types/Types.ts @@ -16,3 +16,13 @@ export interface IPaymentChartItem { } export type IPaymentChartResponse = IResponse; + +export interface IFoodSalesPieChartItem { + foodId: string; + foodTitle: string; + quantity: number; + revenue: number; + percentage: number; +} + +export type IFoodSalesPieChartResponse = IResponse;