fix chart
This commit is contained in:
@@ -5,11 +5,45 @@ import { formatPrice } from "@/helpers/func";
|
|||||||
import { ArrowDown, ArrowUp, DocumentDownload } from "iconsax-react";
|
import { ArrowDown, ArrowUp, DocumentDownload } from "iconsax-react";
|
||||||
import moment from "moment-jalaali";
|
import moment from "moment-jalaali";
|
||||||
import { type FC, useMemo, useState } from "react";
|
import { type FC, useMemo, useState } from "react";
|
||||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
import { CartesianGrid, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
|
||||||
import { useGetPaymentsChart } from "../hooks/useStatsData";
|
import { useGetPaymentsChart } from "../hooks/useStatsData";
|
||||||
|
|
||||||
|
const PERSIAN_MONTHS = [
|
||||||
|
"فروردین",
|
||||||
|
"اردیبهشت",
|
||||||
|
"خرداد",
|
||||||
|
"تیر",
|
||||||
|
"مرداد",
|
||||||
|
"شهریور",
|
||||||
|
"مهر",
|
||||||
|
"آبان",
|
||||||
|
"آذر",
|
||||||
|
"دی",
|
||||||
|
"بهمن",
|
||||||
|
"اسفند",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const toPersianDigits = (value: string | number): string =>
|
||||||
|
String(value).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]);
|
||||||
|
|
||||||
|
const formatJalaliAxisLabel = (dateString: string, period: "daily" | "monthly" | "yearly"): string => {
|
||||||
|
const m = moment(dateString, "YYYY-MM-DD");
|
||||||
|
const jYear = m.jYear();
|
||||||
|
const jMonth = m.jMonth();
|
||||||
|
const jDate = m.jDate();
|
||||||
|
|
||||||
|
if (period === "yearly") return toPersianDigits(jYear);
|
||||||
|
if (period === "monthly") return `${PERSIAN_MONTHS[jMonth]} ${toPersianDigits(jYear)}`;
|
||||||
|
return toPersianDigits(`${String(jMonth + 1).padStart(2, "0")}/${String(jDate).padStart(2, "0")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatJalaliFullDate = (dateString: string): string => {
|
||||||
|
const m = moment(dateString, "YYYY-MM-DD");
|
||||||
|
return `${toPersianDigits(m.jDate())} ${PERSIAN_MONTHS[m.jMonth()]} ${toPersianDigits(m.jYear())}`;
|
||||||
|
};
|
||||||
|
|
||||||
const RevenueChart: FC = () => {
|
const RevenueChart: FC = () => {
|
||||||
const [selectedTab] = useState<"daily" | "monthly" | "yearly">("monthly");
|
const [selectedTab] = useState<"daily" | "monthly" | "yearly">("daily");
|
||||||
const [startDate, setStartDate] = useState<string>("");
|
const [startDate, setStartDate] = useState<string>("");
|
||||||
const [endDate, setEndDate] = useState<string>("");
|
const [endDate, setEndDate] = useState<string>("");
|
||||||
|
|
||||||
@@ -33,38 +67,20 @@ const RevenueChart: FC = () => {
|
|||||||
|
|
||||||
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 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(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!paymentsChartData?.data) return [];
|
if (!paymentsChartData?.data) return [];
|
||||||
return paymentsChartData.data.map((item) => ({
|
return [...paymentsChartData.data]
|
||||||
month: formatDateLabel(item.date, selectedTab),
|
.sort((a, b) => moment(a.date, "YYYY-MM-DD").valueOf() - moment(b.date, "YYYY-MM-DD").valueOf())
|
||||||
online: item.online,
|
.map((item) => ({
|
||||||
cash: item.cash,
|
date: item.date,
|
||||||
}));
|
label: formatJalaliAxisLabel(item.date, selectedTab),
|
||||||
|
tooltipLabel: formatJalaliFullDate(item.date),
|
||||||
|
online: item.online,
|
||||||
|
cash: item.cash,
|
||||||
|
}));
|
||||||
}, [paymentsChartData?.data, selectedTab]);
|
}, [paymentsChartData?.data, selectedTab]);
|
||||||
|
|
||||||
const xAxisInterval = useMemo(() => {
|
const chartWidth = useMemo(() => Math.max(chartData.length * 48, 640), [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(() => {
|
const totalRevenue = useMemo(() => {
|
||||||
return chartData.reduce((sum, item) => sum + item.online + item.cash, 0);
|
return chartData.reduce((sum, item) => sum + item.online + item.cash, 0);
|
||||||
@@ -94,49 +110,16 @@ const RevenueChart: FC = () => {
|
|||||||
const CustomTooltip = (props: {
|
const CustomTooltip = (props: {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
payload?: Array<{
|
payload?: Array<{
|
||||||
dataKey?: string;
|
|
||||||
value?: number;
|
|
||||||
color?: string;
|
|
||||||
payload?: {
|
payload?: {
|
||||||
cash?: number;
|
cash: number;
|
||||||
online?: number;
|
online: number;
|
||||||
month?: string;
|
tooltipLabel: string;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
label?: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { active, payload, label } = props;
|
const { active, payload } = props;
|
||||||
if (!active || !payload || payload.length === 0 || !label) {
|
const dataPoint = payload?.[0]?.payload;
|
||||||
return null;
|
if (!active || !dataPoint) {
|
||||||
}
|
|
||||||
|
|
||||||
// استفاده از 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +128,7 @@ const RevenueChart: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-3 rounded-xl shadow-lg border border-gray-200">
|
<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>
|
<p className="text-sm font-medium text-gray-900 mb-2">{dataPoint.tooltipLabel}</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full bg-[#B0BAC9]" />
|
<div className="w-2 h-2 rounded-full bg-[#B0BAC9]" />
|
||||||
@@ -248,38 +231,26 @@ const RevenueChart: FC = () => {
|
|||||||
) : chartData.length === 0 ? (
|
) : 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]">
|
<div className="w-full overflow-x-auto pb-2">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<div style={{ width: chartWidth, height: 420 }}>
|
||||||
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 10, bottom: 40 }}>
|
<LineChart data={chartData} width={chartWidth} height={420} margin={{ top: 10, right: 10, left: 10, bottom: 72 }}>
|
||||||
<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} />
|
<CartesianGrid strokeDasharray="0" stroke="#f0f0f0" vertical={false} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="month"
|
dataKey="label"
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
tick={{ fill: "#9CA3AF", fontSize: 11 }}
|
tick={{ fill: "#9CA3AF", fontSize: 10 }}
|
||||||
reversed
|
interval={0}
|
||||||
interval={xAxisInterval}
|
angle={-45}
|
||||||
angle={chartData.length > 15 ? -45 : 0}
|
textAnchor="end"
|
||||||
textAnchor={chartData.length > 15 ? "end" : "middle"}
|
height={72}
|
||||||
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 />} />
|
<Tooltip content={<CustomTooltip />} cursor={{ stroke: "#E5E7EB", strokeWidth: 1 }} />
|
||||||
<Area type="natural" dataKey="cash" stroke="#B0BAC9" strokeWidth={2} fillOpacity={1} fill="url(#colorCash)" />
|
<Line type="monotone" dataKey="cash" name="پرداخت نقدی" stroke="#B0BAC9" strokeWidth={2} dot={{ r: 3, fill: "#B0BAC9", strokeWidth: 0 }} activeDot={{ r: 5 }} />
|
||||||
<Area type="natural" dataKey="online" stroke="#6B7FED" strokeWidth={2} fillOpacity={1} fill="url(#colorOnline)" />
|
<Line type="monotone" dataKey="online" name="پرداخت آنلاین" stroke="#6B7FED" strokeWidth={2} dot={{ r: 3, fill: "#6B7FED", strokeWidth: 0 }} activeDot={{ r: 5 }} />
|
||||||
</AreaChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user