chrts + revensure chart

This commit is contained in:
hamid zarghami
2025-12-20 11:07:18 +03:30
parent 24cce65444
commit 0b9d8e3421
10 changed files with 826 additions and 9 deletions
+3
View File
@@ -81,4 +81,7 @@ export const Pages = {
notifications: {
list: "/notifications/list",
},
statistics: {
list: "/statistics",
},
};
+2 -1
View File
@@ -67,7 +67,8 @@
"shipment_methods": "روش های ارسال",
"reviews": "نظرات",
"pagers": "پیجرها",
"notifications": "تنظیمات اعلانات"
"notifications": "تنظیمات اعلانات",
"statistics": "گزارشات"
},
"shipment": {
"dineIn": "سرو در محل",
+51
View File
@@ -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
+3
View File
@@ -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>
+8 -8
View File
@@ -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')}