chart edited

This commit is contained in:
hamid zarghami
2025-12-24 15:18:21 +03:30
parent 7797f47a7e
commit 2fd013d282
8 changed files with 275 additions and 129 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>پنل مدیریت دی منو</title>
</head>
<body>
+3
View File
@@ -0,0 +1,3 @@
User-agent: *
Disallow: /
+8 -1
View File
@@ -28,8 +28,15 @@ const DatePickerComponent: FC<Props> = (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]);
+169 -101
View File
@@ -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<FoodSalesChartProps> = ({ 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 (
<div className='bg-white h-fit rounded-3xl p-6 shadow-sm flex items-center justify-center min-h-[500px]'>
<PageLoading />
</div>
)
}
return (
<div className='bg-white h-fit rounded-3xl p-6 shadow-sm'>
<h3 className='text-base font-medium mb-6'>میزان فروش غذاها</h3>
<div className='flex items-center justify-center mb-6'>
<ResponsiveContainer
width='100%'
height={280}
>
<PieChart>
<Pie
data={dataWithPercent}
cx='50%'
cy='50%'
innerRadius={70}
outerRadius={110}
paddingAngle={3}
dataKey='value'
>
{dataWithPercent.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
/>
))}
<LabelList
dataKey='percent'
position='outside'
content={(props: unknown) => {
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 (
<text
x={x}
y={y}
textAnchor='middle'
fontSize='14'
fontWeight={500}
fill='#1F2937'
>
{labelProps.value}%
</text>
)
}}
/>
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Filters */}
{/* <div className='flex items-end gap-2 mb-4'>
<DatePicker
label='تاریخ شروع'
placeholder='انتخاب تاریخ شروع'
onChange={setStartDate}
/>
<DatePicker
label='تاریخ پایان'
placeholder='انتخاب تاریخ پایان'
onChange={setEndDate}
/>
</div> */}
<div className='flex items-center justify-center gap-6 flex-wrap'>
{dataWithPercent.map((item, index) => (
<div
key={index}
className='flex items-center gap-2'
>
<div
className='w-3 h-3 rounded-full flex-shrink-0'
style={{ backgroundColor: item.color }}
/>
<span className='text-sm text-gray-700'>
{item.name}
</span>
{/* Tabs */}
{/* <div className='flex items-center gap-2 mb-6'>
<button
onClick={() => setSelectedTab('yearly')}
className={`px-4 py-1.5 rounded-full text-xs transition-all border ${selectedTab === 'yearly'
? 'bg-black text-white border-black'
: 'bg-white text-gray-600 border-gray-200'
}`}
>
سالانه
</button>
<button
onClick={() => setSelectedTab('monthly')}
className={`px-4 py-1.5 rounded-full text-xs transition-all border ${selectedTab === 'monthly'
? 'bg-black text-white border-black'
: 'bg-white text-gray-600 border-gray-200'
}`}
>
ماهانه
</button>
<button
onClick={() => setSelectedTab('daily')}
className={`px-4 py-1.5 rounded-full text-xs transition-all border ${selectedTab === 'daily'
? 'bg-black text-white border-black'
: 'bg-white text-gray-600 border-gray-200'
}`}
>
روزانه
</button>
</div> */}
{dataWithPercent.length === 0 ? (
<div className='flex items-center justify-center min-h-[400px] text-gray-500'>
دادهای برای نمایش وجود ندارد
</div>
) : (
<>
<div className='flex items-center justify-center mb-6'>
<ResponsiveContainer
width='100%'
height={280}
>
<PieChart>
<Pie
data={dataWithPercent}
cx='50%'
cy='50%'
innerRadius={70}
outerRadius={110}
paddingAngle={3}
dataKey='value'
>
{dataWithPercent.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
/>
))}
<LabelList
dataKey='percent'
position='outside'
content={(props: unknown) => {
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 (
<text
x={x}
y={y}
textAnchor='middle'
fontSize='14'
fontWeight={500}
fill='#1F2937'
>
{labelProps.value}%
</text>
)
}}
/>
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
))}
</div>
<div className='flex items-center gap-6 flex-wrap'>
{dataWithPercent.map((item, index) => (
<div
key={index}
className='flex items-center gap-2'
>
<div
className='w-3 h-3 rounded-full flex-shrink-0'
style={{ backgroundColor: item.color }}
/>
<span className='text-sm text-gray-700'>
{item.name}
</span>
</div>
))}
</div>
</>
)}
</div>
)
}
@@ -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<string>('')
@@ -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 (
<div className='bg-white p-3 rounded-xl shadow-lg border border-gray-200'>
<p className='text-sm font-medium text-gray-900 mb-2'>{label}</p>
<div className='space-y-1'>
{cashValue !== undefined && (
<div className='flex items-center gap-2'>
<div className='w-2 h-2 rounded-full bg-[#B0BAC9]' />
<span className='text-xs text-gray-600'>پرداخت نقدی:</span>
<span className='text-xs font-medium text-gray-900'>
{cashValue.toLocaleString('fa-IR')} تومان
</span>
</div>
)}
{onlineValue !== undefined && (
<div className='flex items-center gap-2'>
<div className='w-2 h-2 rounded-full bg-[#6B7FED]' />
<span className='text-xs text-gray-600'>پرداخت آنلاین:</span>
<span className='text-xs font-medium text-gray-900'>
{onlineValue.toLocaleString('fa-IR')} تومان
</span>
</div>
)}
{cashValue !== undefined && onlineValue !== undefined && (
<div className='pt-1 mt-1 border-t border-gray-100'>
<span className='text-xs text-gray-600'>جمع:</span>
<span className='text-xs font-medium text-gray-900 mr-2'>
{(cashValue + onlineValue).toLocaleString('fa-IR')} تومان
</span>
</div>
)}
</div>
</div>
)
}
if (isLoading) {
@@ -142,7 +188,7 @@ const RevenueChart: FC = () => {
</div>
{/* Tabs */}
<div className='flex items-center gap-2 mt-5 mb-5'>
{/* <div className='flex items-center gap-2 mt-5 mb-5'>
<button
onClick={() => setSelectedTab('yearly')}
className={`px-4 py-1.5 rounded-full text-xs transition-all border ${selectedTab === 'yearly'
@@ -170,7 +216,7 @@ const RevenueChart: FC = () => {
>
روزانه
</button>
</div>
</div> */}
{/* مبلغ و درصد */}
@@ -304,15 +350,7 @@ const RevenueChart: FC = () => {
domain={[0, 'dataMax + 2000000']}
tickMargin={50}
/>
<Tooltip
formatter={formatTooltip}
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '12px',
fontSize: '12px'
}}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type='natural'
dataKey='cash'
+11 -3
View File
@@ -8,10 +8,18 @@ export const useGetStatistics = () => {
});
};
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),
});
};
@@ -2,6 +2,7 @@ import axios from "@/config/axios";
import type {
IStatisticsResponse,
IPaymentChartResponse,
IFoodSalesPieChartResponse,
} from "../types/Types";
export const getStatistics = async (): Promise<IStatisticsResponse> => {
@@ -9,8 +10,19 @@ export const getStatistics = async (): Promise<IStatisticsResponse> => {
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<IFoodSalesPieChartResponse> => {
const { data } = await axios.get<IFoodSalesPieChartResponse>(
"/admin/orders/food-sales-pie-chart",
{ params }
);
return data;
};
+10
View File
@@ -16,3 +16,13 @@ export interface IPaymentChartItem {
}
export type IPaymentChartResponse = IResponse<IPaymentChartItem[]>;
export interface IFoodSalesPieChartItem {
foodId: string;
foodTitle: string;
quantity: number;
revenue: number;
percentage: number;
}
export type IFoodSalesPieChartResponse = IResponse<IFoodSalesPieChartItem[]>;