chart edited
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>پنل مدیریت دی منو</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
@@ -28,8 +28,15 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
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);
|
props.onChange(formattedDate);
|
||||||
|
} else {
|
||||||
|
props.onChange('');
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [value]);
|
}, [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 { Cell, LabelList, Pie, PieChart, ResponsiveContainer } from 'recharts'
|
||||||
import { useGetFoodSalesPieChart } from '../hooks/useStatsData';
|
import { useGetFoodSalesPieChart } from '../hooks/useStatsData'
|
||||||
|
import PageLoading from '@/components/PageLoading'
|
||||||
|
|
||||||
interface FoodSalesChartProps {
|
const COLORS = ['#6B7FED', '#A78BFA', '#FF6B9D', '#FF9A62', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
|
||||||
data?: Array<{ name: string; value: number; color: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
const FoodSalesChart: FC<FoodSalesChartProps> = ({ data }) => {
|
const FoodSalesChart: FC = () => {
|
||||||
|
|
||||||
useGetFoodSalesPieChart()
|
|
||||||
|
|
||||||
const defaultData = [
|
const { data: apiData, isLoading } = useGetFoodSalesPieChart()
|
||||||
{ name: 'پیتزا', value: 50, color: '#6B7FED' },
|
|
||||||
{ name: 'برگر', value: 25, color: '#A78BFA' },
|
|
||||||
{ name: 'پاستا', value: 15, color: '#FF6B9D' },
|
|
||||||
{ name: 'نوشیدنی ها', value: 10, color: '#FF9A62' }
|
|
||||||
]
|
|
||||||
|
|
||||||
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) => ({
|
const total = useMemo(() => {
|
||||||
...item,
|
return chartData.reduce((sum, item) => sum + item.value, 0)
|
||||||
percent: Math.round((item.value / total) * 100)
|
}, [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 (
|
return (
|
||||||
<div className='bg-white h-fit rounded-3xl p-6 shadow-sm'>
|
<div className='bg-white h-fit rounded-3xl p-6 shadow-sm'>
|
||||||
<h3 className='text-base font-medium mb-6'>میزان فروش غذاها</h3>
|
<h3 className='text-base font-medium mb-6'>میزان فروش غذاها</h3>
|
||||||
|
|
||||||
<div className='flex items-center justify-center mb-6'>
|
{/* Filters */}
|
||||||
<ResponsiveContainer
|
{/* <div className='flex items-end gap-2 mb-4'>
|
||||||
width='100%'
|
<DatePicker
|
||||||
height={280}
|
label='تاریخ شروع'
|
||||||
>
|
placeholder='انتخاب تاریخ شروع'
|
||||||
<PieChart>
|
onChange={setStartDate}
|
||||||
<Pie
|
/>
|
||||||
data={dataWithPercent}
|
<DatePicker
|
||||||
cx='50%'
|
label='تاریخ پایان'
|
||||||
cy='50%'
|
placeholder='انتخاب تاریخ پایان'
|
||||||
innerRadius={70}
|
onChange={setEndDate}
|
||||||
outerRadius={110}
|
/>
|
||||||
paddingAngle={3}
|
</div> */}
|
||||||
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 className='flex items-center justify-center gap-6 flex-wrap'>
|
{/* Tabs */}
|
||||||
{dataWithPercent.map((item, index) => (
|
{/* <div className='flex items-center gap-2 mb-6'>
|
||||||
<div
|
<button
|
||||||
key={index}
|
onClick={() => setSelectedTab('yearly')}
|
||||||
className='flex items-center gap-2'
|
className={`px-4 py-1.5 rounded-full text-xs transition-all border ${selectedTab === 'yearly'
|
||||||
>
|
? 'bg-black text-white border-black'
|
||||||
<div
|
: 'bg-white text-gray-600 border-gray-200'
|
||||||
className='w-3 h-3 rounded-full flex-shrink-0'
|
}`}
|
||||||
style={{ backgroundColor: item.color }}
|
>
|
||||||
/>
|
سالانه
|
||||||
<span className='text-sm text-gray-700'>
|
</button>
|
||||||
{item.name}
|
<button
|
||||||
</span>
|
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>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { formatPrice } from '@/helpers/func'
|
|||||||
import PageLoading from '@/components/PageLoading'
|
import PageLoading from '@/components/PageLoading'
|
||||||
|
|
||||||
const RevenueChart: FC = () => {
|
const RevenueChart: FC = () => {
|
||||||
const [selectedTab, setSelectedTab] = useState<
|
const [selectedTab] = useState<
|
||||||
'daily' | 'monthly' | 'yearly'
|
'daily' | 'monthly' | 'yearly'
|
||||||
>('monthly')
|
>('monthly')
|
||||||
const [startDate, setStartDate] = useState<string>('')
|
const [startDate, setStartDate] = useState<string>('')
|
||||||
@@ -27,13 +27,12 @@ const RevenueChart: FC = () => {
|
|||||||
period?: 'daily' | 'monthly' | 'yearly';
|
period?: 'daily' | 'monthly' | 'yearly';
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
} = {
|
} = {}
|
||||||
period: selectedTab,
|
// فقط اگر تاریخ انتخاب شده باشد ارسال شود
|
||||||
}
|
if (startDate && startDate.trim() !== '') params.startDate = startDate
|
||||||
if (startDate) params.startDate = startDate
|
if (endDate && endDate.trim() !== '') params.endDate = endDate
|
||||||
if (endDate) params.endDate = endDate
|
|
||||||
return params
|
return params
|
||||||
}, [selectedTab, startDate, endDate])
|
}, [startDate, endDate])
|
||||||
|
|
||||||
const { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams)
|
const { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams)
|
||||||
|
|
||||||
@@ -97,9 +96,56 @@ const RevenueChart: FC = () => {
|
|||||||
return value.toLocaleString('fa-IR')
|
return value.toLocaleString('fa-IR')
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTooltip = (value: number | undefined) => {
|
const CustomTooltip = (props: {
|
||||||
if (value === undefined) return ''
|
active?: boolean
|
||||||
return value.toLocaleString('fa-IR') + ' تومان'
|
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) {
|
if (isLoading) {
|
||||||
@@ -142,7 +188,7 @@ const RevenueChart: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className='flex items-center gap-2 mt-5 mb-5'>
|
{/* <div className='flex items-center gap-2 mt-5 mb-5'>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTab('yearly')}
|
onClick={() => setSelectedTab('yearly')}
|
||||||
className={`px-4 py-1.5 rounded-full text-xs transition-all border ${selectedTab === 'yearly'
|
className={`px-4 py-1.5 rounded-full text-xs transition-all border ${selectedTab === 'yearly'
|
||||||
@@ -170,7 +216,7 @@ const RevenueChart: FC = () => {
|
|||||||
>
|
>
|
||||||
روزانه
|
روزانه
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* مبلغ و درصد */}
|
{/* مبلغ و درصد */}
|
||||||
|
|
||||||
@@ -304,15 +350,7 @@ const RevenueChart: FC = () => {
|
|||||||
domain={[0, 'dataMax + 2000000']}
|
domain={[0, 'dataMax + 2000000']}
|
||||||
tickMargin={50}
|
tickMargin={50}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip content={<CustomTooltip />} />
|
||||||
formatter={formatTooltip}
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: '1px solid #e5e7eb',
|
|
||||||
borderRadius: '12px',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
<Area
|
||||||
type='natural'
|
type='natural'
|
||||||
dataKey='cash'
|
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({
|
return useQuery({
|
||||||
queryKey: ["food-sales-pie-chart"],
|
queryKey: ["food-sales-pie-chart", params],
|
||||||
queryFn: api.getFoodSalesPieChart,
|
queryFn: () => api.getFoodSalesPieChart(params),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import axios from "@/config/axios";
|
|||||||
import type {
|
import type {
|
||||||
IStatisticsResponse,
|
IStatisticsResponse,
|
||||||
IPaymentChartResponse,
|
IPaymentChartResponse,
|
||||||
|
IFoodSalesPieChartResponse,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const getStatistics = async (): Promise<IStatisticsResponse> => {
|
export const getStatistics = async (): Promise<IStatisticsResponse> => {
|
||||||
@@ -9,8 +10,19 @@ export const getStatistics = async (): Promise<IStatisticsResponse> => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFoodSalesPieChart = async () => {
|
interface GetFoodSalesPieChartParams {
|
||||||
const { data } = await axios.get("/admin/orders/food-sales-pie-chart");
|
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;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,13 @@ export interface IPaymentChartItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type IPaymentChartResponse = IResponse<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