Files
dmenu-admin/src/pages/statistics/components/RevenueChart.tsx
T
hamid zarghami 92ba2b9f75 date picker
2025-12-29 17:02:46 +03:30

413 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ArrowUp, DocumentDownload, ArrowDown } from 'iconsax-react'
import { type FC, useState, useMemo } from 'react'
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts'
import DatePicker from '@/components/DatePicker'
import Button from '@/components/Button'
import { useGetPaymentsChart } from '../hooks/useStatsData'
import { formatPrice } from '@/helpers/func'
import PageLoading from '@/components/PageLoading'
import moment from 'moment-jalaali'
const RevenueChart: FC = () => {
const [selectedTab] = useState<
'daily' | 'monthly' | 'yearly'
>('monthly')
const [startDate, setStartDate] = useState<string>('')
const [endDate, setEndDate] = useState<string>('')
const convertToPersian = (gregorianDate: string | null | undefined): string | undefined => {
if (!gregorianDate) return undefined
const persianDate = moment(gregorianDate, 'YYYY-MM-DD').format('jYYYY-jMM-jDD')
return persianDate
}
const apiParams = useMemo(() => {
const params: {
period?: 'daily' | 'monthly' | 'yearly';
startDate?: string;
endDate?: string;
} = {}
// فقط اگر تاریخ انتخاب شده باشد ارسال شود
if (startDate && startDate.trim() !== '') params.startDate = startDate
if (endDate && endDate.trim() !== '') params.endDate = endDate
return params
}, [startDate, endDate])
const { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams)
const formatDateLabel = (dateString: string, period: 'daily' | 'monthly' | 'yearly'): string => {
const date = new Date(dateString)
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
}
if (period === 'monthly') {
options.month = 'long'
} else if (period === 'daily') {
options.month = 'short'
options.day = 'numeric'
}
const persianDate = new Intl.DateTimeFormat('fa-IR', options).format(date)
return persianDate
}
const chartData = useMemo(() => {
if (!paymentsChartData?.data) return []
return paymentsChartData.data.map((item) => ({
month: formatDateLabel(item.date, selectedTab),
online: item.online,
cash: item.cash,
}))
}, [paymentsChartData?.data, selectedTab])
const xAxisInterval = useMemo(() => {
const dataLength = chartData.length
if (dataLength <= 7) return 0
if (dataLength <= 15) return 1
if (dataLength <= 30) return 2
if (dataLength <= 60) return 4
return Math.floor(dataLength / 12)
}, [chartData.length])
const totalRevenue = useMemo(() => {
return chartData.reduce((sum, item) => sum + item.online + item.cash, 0)
}, [chartData])
const revenueChange = useMemo(() => {
if (chartData.length < 2) return null
const currentPeriod = chartData[chartData.length - 1]
const previousPeriod = chartData[chartData.length - 2]
const currentTotal = currentPeriod.online + currentPeriod.cash
const previousTotal = previousPeriod.online + previousPeriod.cash
if (previousTotal === 0) return null
const change = ((currentTotal - previousTotal) / previousTotal) * 100
return change
}, [chartData])
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 CustomTooltip = (props: {
active?: boolean
payload?: Array<{
dataKey?: string
value?: number
color?: string
payload?: {
cash?: number
online?: number
month?: string
}
}>
label?: string
}) => {
const { active, payload, label } = props
if (!active || !payload || payload.length === 0 || !label) {
return null
}
// استفاده از paymentsChartData.data مستقیم برای دریافت داده‌های واقعی
// پیدا کردن آخرین آیتمی که label آن با formatDateLabel match می‌کند
let dataPoint: { cash: number; online: number } | null = null
if (paymentsChartData?.data) {
// پیدا کردن همه آیتم‌های match شده و استفاده از آخرین
const matchingItems: Array<{ cash: number; online: number }> = []
for (const item of paymentsChartData.data) {
const formattedLabel = formatDateLabel(item.date, selectedTab)
if (formattedLabel === label) {
matchingItems.push({ cash: item.cash, online: item.online })
}
}
if (matchingItems.length > 0) {
dataPoint = matchingItems[matchingItems.length - 1]
}
}
// اگر پیدا نکردیم، از chartData استفاده می‌کنیم
if (!dataPoint) {
const matchingItems = chartData.filter((item) => item.month === label)
if (matchingItems.length > 0) {
dataPoint = matchingItems[matchingItems.length - 1]
}
}
if (!dataPoint) {
return null
}
const cashValue = dataPoint.cash
const onlineValue = dataPoint.online
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'>
<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>
<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>
<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>
)
}
return (
<div className='bg-white rounded-3xl p-6 shadow-sm h-full overflow-visible'>
{/* Header */}
<div className='flex items-start justify-between my-5'>
<h3 className='text-base font-bold'>مجموع درآمد</h3>
</div>
<div className='flex items-end gap-2'>
<DatePicker
label='تاریخ شروع'
placeholder='انتخاب تاریخ شروع'
defaulValue={convertToPersian(startDate)}
onChange={setStartDate}
/>
<DatePicker
label='تاریخ پایان'
placeholder='انتخاب تاریخ پایان'
defaulValue={convertToPersian(endDate)}
onChange={setEndDate}
/>
<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'>
{formatPrice(totalRevenue)}
</p>
{revenueChange !== null && (
<div
className={`flex items-center gap-1 px-2 py-0.5 rounded-full ${revenueChange >= 0
? 'bg-green-50'
: 'bg-red-50'
}`}
>
{revenueChange >= 0 ? (
<ArrowUp
size={12}
color='#10b981'
/>
) : (
<ArrowDown
size={12}
color='#ef4444'
/>
)}
<span
className={`text-xs font-medium ${revenueChange >= 0
? 'text-green-600'
: 'text-red-600'
}`}
>
{Math.abs(revenueChange).toFixed(1)}%
</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>
{/* نمودار */}
{isLoading ? (
<div className='w-full h-[400px] min-h-[400px] flex items-center justify-center'>
<PageLoading />
</div>
) : chartData.length === 0 ? (
<div className='w-full h-[400px] min-h-[400px] flex items-center justify-center text-gray-500'>
دادهای برای نمایش وجود ندارد
</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={xAxisInterval}
angle={chartData.length > 15 ? -45 : 0}
textAnchor={chartData.length > 15 ? 'end' : 'middle'}
height={chartData.length > 15 ? 60 : 40}
minTickGap={10}
/>
<YAxis
orientation='right'
axisLine={false}
tickLine={false}
tick={{ fill: '#9CA3AF', fontSize: 10 }}
tickFormatter={formatYAxis}
domain={[0, 'dataMax + 2000000']}
tickMargin={50}
/>
<Tooltip content={<CustomTooltip />} />
<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