chrts + revensure chart
This commit is contained in:
@@ -81,4 +81,7 @@ export const Pages = {
|
||||
notifications: {
|
||||
list: "/notifications/list",
|
||||
},
|
||||
statistics: {
|
||||
list: "/statistics",
|
||||
},
|
||||
};
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@
|
||||
"shipment_methods": "روش های ارسال",
|
||||
"reviews": "نظرات",
|
||||
"pagers": "پیجرها",
|
||||
"notifications": "تنظیمات اعلانات"
|
||||
"notifications": "تنظیمات اعلانات",
|
||||
"statistics": "گزارشات"
|
||||
},
|
||||
"shipment": {
|
||||
"dineIn": "سرو در محل",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Category, DollarCircle, DocumentText, People } from 'iconsax-react'
|
||||
import { type FC } from 'react'
|
||||
import FoodSalesChart from './components/FoodSalesChart'
|
||||
import RevenueChart from './components/RevenueChart'
|
||||
import StatCard from './components/StatCard'
|
||||
|
||||
const Statistics: FC = () => {
|
||||
return (
|
||||
<div className='p-6 max-w-[1600px] mx-auto'>
|
||||
{/* Stats Cards */}
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6'>
|
||||
<StatCard
|
||||
icon={<Category size={24} color='#6B7FED' />}
|
||||
title='غذاها'
|
||||
value='۱۰۴ عددی'
|
||||
color='#6B7FED'
|
||||
/>
|
||||
<StatCard
|
||||
icon={<People size={24} color='#6B7FED' />}
|
||||
title='مشتریان'
|
||||
value='۲۶۰ مشتری'
|
||||
color='#6B7FED'
|
||||
/>
|
||||
<StatCard
|
||||
icon={<DocumentText size={24} color='#FF9A62' />}
|
||||
title='سفارشات'
|
||||
value='۳۰۳ سفارش'
|
||||
color='#FF9A62'
|
||||
/>
|
||||
<StatCard
|
||||
icon={<DollarCircle size={24} color='#10B981' />}
|
||||
title='درآمد'
|
||||
value='۲۰,۰۰۰,۰۰۰ تومان'
|
||||
color='#10B981'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
|
||||
<div className='lg:col-span-8'>
|
||||
<RevenueChart />
|
||||
</div>
|
||||
<div className='lg:col-span-4'>
|
||||
<FoodSalesChart />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Statistics
|
||||
@@ -0,0 +1,77 @@
|
||||
import { type FC } from 'react'
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'
|
||||
|
||||
interface FoodSalesChartProps {
|
||||
data?: Array<{ name: string; value: number; color: string }>
|
||||
}
|
||||
|
||||
const FoodSalesChart: FC<FoodSalesChartProps> = ({ data }) => {
|
||||
const defaultData = [
|
||||
{ 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 total = chartData.reduce((sum, item) => sum + item.value, 0)
|
||||
|
||||
return (
|
||||
<div className='bg-white rounded-3xl p-6 shadow-sm h-full'>
|
||||
<h3 className='text-base font-medium mb-6'>میزان فروش غذاها</h3>
|
||||
|
||||
<div className='flex items-center justify-center gap-6'>
|
||||
<ResponsiveContainer
|
||||
width='60%'
|
||||
height={280}
|
||||
>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
innerRadius={70}
|
||||
outerRadius={110}
|
||||
paddingAngle={3}
|
||||
dataKey='value'
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.color}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
{chartData.map((item, index) => {
|
||||
const percent = Math.round((item.value / total) * 100)
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center gap-3'
|
||||
>
|
||||
<div
|
||||
className='w-3 h-3 rounded-full flex-shrink-0'
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<span className='text-sm text-gray-700 min-w-[80px]'>
|
||||
{item.name}
|
||||
</span>
|
||||
<span className='text-sm font-medium text-gray-900'>
|
||||
{percent}%
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FoodSalesChart
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import { ArrowUp, DocumentDownload } from 'iconsax-react'
|
||||
import { type FC, useState } from 'react'
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from 'recharts'
|
||||
import DatePicker from '@/components/DatePicker'
|
||||
import Button from '@/components/Button'
|
||||
|
||||
interface RevenueChartProps {
|
||||
data?: Array<{ month: string; online: number; cash: number }>
|
||||
}
|
||||
|
||||
const RevenueChart: FC<RevenueChartProps> = ({ data }) => {
|
||||
const [selectedTab, setSelectedTab] = useState<
|
||||
'daily' | 'monthly' | 'yearly'
|
||||
>('monthly')
|
||||
|
||||
const defaultData = [
|
||||
{ month: 'فروردین', online: 10000000, cash: 8000000 },
|
||||
{ month: 'اردیبهشت', online: 15000000, cash: 11000000 },
|
||||
{ month: 'خرداد', online: 12000000, cash: 9000000 },
|
||||
{ month: 'تیر', online: 18000000, cash: 13000000 },
|
||||
{ month: 'مرداد', online: 16000000, cash: 12000000 },
|
||||
{ month: 'شهریور', online: 22000000, cash: 15000000 },
|
||||
{ month: 'مهر', online: 20000000, cash: 14000000 },
|
||||
{ month: 'آبان', online: 17000000, cash: 13000000 },
|
||||
{ month: 'آذر', online: 14000000, cash: 11000000 },
|
||||
{ month: 'دی', online: 12000000, cash: 10000000 },
|
||||
{ month: 'بهمن', online: 18000000, cash: 13000000 },
|
||||
{ month: 'اسفند', online: 16000000, cash: 12000000 }
|
||||
]
|
||||
|
||||
const chartData = data || defaultData
|
||||
|
||||
const formatYAxis = (value: number) => {
|
||||
if (value === 0) return '۰'
|
||||
if (value >= 1000000) {
|
||||
const millions = value / 1000000
|
||||
const formatted = millions
|
||||
.toFixed(1)
|
||||
.replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)])
|
||||
return `${formatted} میلیون`
|
||||
}
|
||||
return value.toLocaleString('fa-IR')
|
||||
}
|
||||
|
||||
const formatTooltip = (value: number | undefined) => {
|
||||
if (value === undefined) return ''
|
||||
return value.toLocaleString('fa-IR') + ' تومان'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-white rounded-3xl p-6 shadow-sm h-full overflow-visible'>
|
||||
{/* Header */}
|
||||
<div className='flex items-start justify-between mb-5'>
|
||||
<h3 className='text-base font-bold'>مجموع درآمد</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className='flex items-end gap-2'>
|
||||
<DatePicker
|
||||
label='تاریخ شروع'
|
||||
placeholder='mm/dd/yyyy'
|
||||
onChange={() => { }}
|
||||
/>
|
||||
<DatePicker
|
||||
label='تاریخ پایان'
|
||||
placeholder='mm/dd/yyyy'
|
||||
onChange={() => { }}
|
||||
/>
|
||||
<Button className=''>
|
||||
<div className='flex gap-3'>
|
||||
<DocumentDownload
|
||||
size={18}
|
||||
color='#fff'
|
||||
/>
|
||||
<span>گرفتن گزارش</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<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'
|
||||
? '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>
|
||||
|
||||
{/* مبلغ و درصد */}
|
||||
|
||||
<div>
|
||||
<span className='text-sm text-gray-500'>مجموع درآمد</span>
|
||||
|
||||
</div>
|
||||
<div className='flex items-center justify-between mb-5'>
|
||||
<div className='flex items-center gap-3'>
|
||||
|
||||
<p className='text-2xl font-bold'>۲۰,۰۰۰,۰۰۰ تومان</p>
|
||||
<div className='flex items-center gap-1 px-2 py-0.5 bg-green-50 rounded-full'>
|
||||
<ArrowUp
|
||||
size={12}
|
||||
color='#10b981'
|
||||
/>
|
||||
<span className='text-xs font-medium text-green-600'>
|
||||
2,5%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='w-2 h-2 rounded-full bg-[#6B7FED]' />
|
||||
<span className='text-xs text-gray-500'>
|
||||
پرداخت آنلاین
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='w-2 h-2 rounded-full bg-[#B0BAC9]' />
|
||||
<span className='text-xs text-gray-500'>
|
||||
پرداخت نقدی
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* نمودار */}
|
||||
<div className='w-full h-[400px] min-h-[400px]'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 10, left: 10, bottom: 40 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id='colorOnline'
|
||||
x1='0'
|
||||
y1='0'
|
||||
x2='0'
|
||||
y2='1'
|
||||
>
|
||||
<stop
|
||||
offset='5%'
|
||||
stopColor='#6B7FED'
|
||||
stopOpacity={0.3}
|
||||
/>
|
||||
<stop
|
||||
offset='95%'
|
||||
stopColor='#6B7FED'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id='colorCash'
|
||||
x1='0'
|
||||
y1='0'
|
||||
x2='0'
|
||||
y2='1'
|
||||
>
|
||||
<stop
|
||||
offset='5%'
|
||||
stopColor='#B0BAC9'
|
||||
stopOpacity={0.2}
|
||||
/>
|
||||
<stop
|
||||
offset='95%'
|
||||
stopColor='#B0BAC9'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid
|
||||
strokeDasharray='0'
|
||||
stroke='#f0f0f0'
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey='month'
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#9CA3AF', fontSize: 11 }}
|
||||
reversed
|
||||
interval={0}
|
||||
angle={0}
|
||||
textAnchor='middle'
|
||||
height={40}
|
||||
/>
|
||||
<YAxis
|
||||
orientation='right'
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#9CA3AF', fontSize: 10 }}
|
||||
tickFormatter={formatYAxis}
|
||||
domain={[0, 'dataMax + 2000000']}
|
||||
tickMargin={50}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={formatTooltip}
|
||||
contentStyle={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '12px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type='natural'
|
||||
dataKey='cash'
|
||||
stroke='#B0BAC9'
|
||||
strokeWidth={2}
|
||||
fillOpacity={1}
|
||||
fill='url(#colorCash)'
|
||||
/>
|
||||
<Area
|
||||
type='natural'
|
||||
dataKey='online'
|
||||
stroke='#6B7FED'
|
||||
strokeWidth={2}
|
||||
fillOpacity={1}
|
||||
fill='url(#colorOnline)'
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RevenueChart
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
|
||||
interface StatCardProps {
|
||||
icon: ReactNode
|
||||
title: string
|
||||
value: string
|
||||
subtitle?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
const StatCard: FC<StatCardProps> = ({
|
||||
icon,
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
color = '#6B7FED'
|
||||
}) => {
|
||||
return (
|
||||
<div className='bg-white rounded-3xl p-6 flex flex-col items-center justify-center shadow-sm min-h-[150px] transition-all hover:shadow-md'>
|
||||
<div
|
||||
className='w-12 h-12 rounded-2xl flex items-center justify-center mb-3'
|
||||
style={{ backgroundColor: `${color}15` }}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className='text-sm font-medium mb-1.5 text-gray-900'>
|
||||
{title}
|
||||
</h3>
|
||||
<div className='flex flex-col items-center gap-0.5'>
|
||||
<p className='text-sm text-gray-600'>{value}</p>
|
||||
{subtitle && (
|
||||
<span className='text-xs text-gray-400'>{subtitle}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatCard
|
||||
|
||||
@@ -42,6 +42,7 @@ import PagersList from '@/pages/pager/List'
|
||||
import NotificationsList from '@/pages/notifications/List'
|
||||
import Notification from '@/components/Notification'
|
||||
import ReportsList from '@/pages/report/List'
|
||||
import Statistics from '@/pages/statistics/Statistics'
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
const { hasSubMenu } = useSharedStore()
|
||||
@@ -102,6 +103,8 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.notifications.list} element={<NotificationsList />} />
|
||||
|
||||
<Route path={Pages.reports.list} element={<ReportsList />} />
|
||||
|
||||
<Route path={Pages.statistics.list} element={<Statistics />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type FC, useEffect } from 'react'
|
||||
import LogoImage from '../assets/images/logo.svg'
|
||||
import LogoSmall from '../assets/images/logo-small.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1, NotificationBing, SmsEdit } from 'iconsax-react'
|
||||
import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1, NotificationBing, SmsEdit, Chart1 } from 'iconsax-react'
|
||||
import SideBarItem from './SideBarItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
@@ -199,13 +199,13 @@ const SideBar: FC = () => {
|
||||
link={Pages.announcements.list}
|
||||
activeName='announcements'
|
||||
/>
|
||||
{/* <SideBarItem
|
||||
icon={<Chart variant={isActive('reports') ? 'Bold' : 'Outline'} color={isActive('reports') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.reports')}
|
||||
isActive={isActive('reports')}
|
||||
link={Pages.reports.list}
|
||||
activeName='reports'
|
||||
/> */}
|
||||
<SideBarItem
|
||||
icon={<Chart1 variant={isActive('statistics') ? 'Bold' : 'Outline'} color={isActive('statistics') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.statistics')}
|
||||
isActive={isActive('statistics')}
|
||||
link={Pages.statistics.list}
|
||||
activeName='statistics'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Message variant={isActive('comments') ? 'Bold' : 'Outline'} color={isActive('comments') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.comments')}
|
||||
|
||||
Reference in New Issue
Block a user