revenuechart

This commit is contained in:
hamid zarghami
2026-06-29 14:54:56 +03:30
parent 53ebbc8d07
commit b155c0b959
+143 -266
View File
@@ -1,217 +1,185 @@
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'
import Button from "@/components/Button";
import DatePicker from "@/components/DatePicker";
import PageLoading from "@/components/PageLoading";
import { formatPrice } from "@/helpers/func";
import { ArrowDown, ArrowUp, DocumentDownload } from "iconsax-react";
import moment from "moment-jalaali";
import { type FC, useMemo, useState } from "react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { useGetPaymentsChart } from "../hooks/useStatsData";
const RevenueChart: FC = () => {
const [selectedTab] = useState<
'daily' | 'monthly' | 'yearly'
>('monthly')
const [startDate, setStartDate] = useState<string>('')
const [endDate, setEndDate] = useState<string>('')
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
}
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';
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])
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 { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams);
const formatDateLabel = (dateString: string, period: 'daily' | 'monthly' | 'yearly'): string => {
const date = new Date(dateString)
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
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 []
if (!paymentsChartData?.data) return [];
return paymentsChartData.data.map((item) => ({
month: formatDateLabel(item.date, selectedTab),
online: item.online,
cash: item.cash,
}))
}, [paymentsChartData?.data, selectedTab])
}));
}, [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 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])
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])
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 === 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 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
active?: boolean;
payload?: Array<{
dataKey?: string
value?: number
color?: string
dataKey?: string;
value?: number;
color?: string;
payload?: {
cash?: number
online?: number
month?: string
}
}>
label?: string
cash?: number;
online?: number;
month?: string;
};
}>;
label?: string;
}) => {
const { active, payload, label } = props
const { active, payload, label } = props;
if (!active || !payload || payload.length === 0 || !label) {
return null
return null;
}
// استفاده از paymentsChartData.data مستقیم برای دریافت داده‌های واقعی
// پیدا کردن آخرین آیتمی که label آن با formatDateLabel match می‌کند
let dataPoint: { cash: number; online: number } | null = null
let dataPoint: { cash: number; online: number } | null = null;
if (paymentsChartData?.data) {
// پیدا کردن همه آیتم‌های match شده و استفاده از آخرین
const matchingItems: Array<{ cash: number; online: number }> = []
const matchingItems: Array<{ cash: number; online: number }> = [];
for (const item of paymentsChartData.data) {
const formattedLabel = formatDateLabel(item.date, selectedTab)
const formattedLabel = formatDateLabel(item.date, selectedTab);
if (formattedLabel === label) {
matchingItems.push({ cash: item.cash, online: item.online })
matchingItems.push({ cash: item.cash, online: item.online });
}
}
if (matchingItems.length > 0) {
dataPoint = matchingItems[matchingItems.length - 1]
dataPoint = matchingItems[matchingItems.length - 1];
}
}
// اگر پیدا نکردیم، از chartData استفاده می‌کنیم
if (!dataPoint) {
const matchingItems = chartData.filter((item) => item.month === label)
const matchingItems = chartData.filter((item) => item.month === label);
if (matchingItems.length > 0) {
dataPoint = matchingItems[matchingItems.length - 1]
dataPoint = matchingItems[matchingItems.length - 1];
}
}
if (!dataPoint) {
return null
return null;
}
const cashValue = dataPoint.cash
const onlineValue = dataPoint.online
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 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 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 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'>
<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 className="flex items-start justify-between my-5">
<h3 className="text-base font-bold">مجموع درآمد</h3>
{formatPrice(totalRevenue)}
</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'
/>
<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>
@@ -250,163 +218,72 @@ const RevenueChart: FC = () => {
{/* مبلغ و درصد */}
<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>
<div className="flex items-center justify-between mb-5 mt-5">
<div className="flex items-center gap-3">
{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 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 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 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'>
<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] 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 }}
>
<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 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 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}
/>
<CartesianGrid strokeDasharray="0" stroke="#f0f0f0" vertical={false} />
<XAxis
dataKey='month'
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fill: '#9CA3AF', fontSize: 11 }}
tick={{ fill: "#9CA3AF", fontSize: 11 }}
reversed
interval={xAxisInterval}
angle={chartData.length > 15 ? -45 : 0}
textAnchor={chartData.length > 15 ? 'end' : 'middle'}
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}
/>
<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)'
/>
<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
export default RevenueChart;