chart edited
This commit is contained in:
+1
-1
@@ -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>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
Reference in New Issue
Block a user