Compare commits
3 Commits
6bff75109a
...
4fc7d19766
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc7d19766 | |||
| b155c0b959 | |||
| 53ebbc8d07 |
+36
-12
@@ -17,30 +17,54 @@ type Props = {
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||
|
||||
const Select: FC<Props> = (props: Props) => {
|
||||
const {
|
||||
className,
|
||||
items,
|
||||
error_text,
|
||||
placeholder,
|
||||
label,
|
||||
isNotRequired,
|
||||
value,
|
||||
...selectProps
|
||||
} = props
|
||||
|
||||
const hasMatchingItem =
|
||||
value !== undefined &&
|
||||
value !== '' &&
|
||||
items?.some((item) => String(item.value) === String(value))
|
||||
|
||||
const selectValue =
|
||||
!value || hasMatchingItem || !items?.length
|
||||
? (value ?? '')
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div className='w-full relative'>
|
||||
{
|
||||
props.label &&
|
||||
label &&
|
||||
<div className='flex items-center gap-1'>
|
||||
<label className='text-sm'>
|
||||
{props.label}
|
||||
{label}
|
||||
</label>
|
||||
{props.isNotRequired && (
|
||||
{isNotRequired && (
|
||||
<span className='text-xs text-description'>(اختیاری)</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
<select {...props} className={clx(
|
||||
<select
|
||||
{...selectProps}
|
||||
value={selectValue}
|
||||
className={clx(
|
||||
'w-full text-black block border appearance-none border-border !bg-white px-2.5 h-10 text-sm rounded-[10px] bg-gray',
|
||||
props.className,
|
||||
props.label && 'mt-1'
|
||||
className,
|
||||
label && 'mt-1'
|
||||
)}>
|
||||
{
|
||||
props.placeholder &&
|
||||
<option value="" disabled selected>{props.placeholder}</option>
|
||||
placeholder &&
|
||||
<option value="" disabled>{placeholder}</option>
|
||||
}
|
||||
{
|
||||
props.items?.map((item) => {
|
||||
items?.map((item) => {
|
||||
return (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
@@ -51,12 +75,12 @@ const Select: FC<Props> = (props: Props) => {
|
||||
</select>
|
||||
<ArrowDown2 size={16} color='black' className={clx(
|
||||
'absolute z-0 top-3 left-2',
|
||||
props.label && 'top-10'
|
||||
label && 'top-10'
|
||||
)} />
|
||||
{
|
||||
props.error_text && props.error_text !== '' ?
|
||||
error_text && error_text !== '' ?
|
||||
<Error
|
||||
errorText={props.error_text}
|
||||
errorText={error_text}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
||||
+134
-142
@@ -1,156 +1,148 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import Button from '@/components/Button'
|
||||
import Input from '@/components/Input'
|
||||
import Select from '@/components/Select'
|
||||
import { useGetAdminById, useUpdateAdmin } from './hooks/useAdminData'
|
||||
import { useGetRoles } from '@/pages/roles/hooks/useRolesData'
|
||||
import type { CreateAdminType } from './types/Types'
|
||||
import { Pages } from '@/config/Pages'
|
||||
import type { ErrorType } from '@/helpers/types'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
import Select from "@/components/Select";
|
||||
import { extractErrorMessage } from "@/config/func";
|
||||
import { Pages } from "@/config/Pages";
|
||||
import type { ErrorType } from "@/helpers/types";
|
||||
import { useGetRoles } from "@/pages/roles/hooks/useRolesData";
|
||||
import { useFormik } from "formik";
|
||||
import { TickCircle } from "iconsax-react";
|
||||
import { type FC, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import * as Yup from "yup";
|
||||
import { useGetAdminById, useUpdateAdmin } from "./hooks/useAdminData";
|
||||
import type { CreateAdminType } from "./types/Types";
|
||||
|
||||
const UpdateAdmin: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams()
|
||||
const { data: adminData, isLoading } = useGetAdminById(id!)
|
||||
const { mutate: updateAdmin, isPending } = useUpdateAdmin()
|
||||
const { data: rolesData } = useGetRoles()
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const { data: adminData, isLoading } = useGetAdminById(id!);
|
||||
const { mutate: updateAdmin, isPending } = useUpdateAdmin();
|
||||
const { data: rolesData } = useGetRoles();
|
||||
|
||||
const roles = rolesData?.data?.map(role => ({
|
||||
label: role.name,
|
||||
value: role.id
|
||||
})) || []
|
||||
const roles =
|
||||
rolesData?.data?.map((role) => ({
|
||||
label: role.name,
|
||||
value: role.id,
|
||||
})) || [];
|
||||
|
||||
const admin = adminData?.data
|
||||
const admin = adminData?.data;
|
||||
|
||||
const formik = useFormik<CreateAdminType>({
|
||||
initialValues: {
|
||||
phone: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
roleId: '',
|
||||
const formik = useFormik<CreateAdminType>({
|
||||
initialValues: {
|
||||
phone: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
roleId: "",
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
phone: Yup.string().required("شماره تلفن الزامی است"),
|
||||
firstName: Yup.string().required("نام الزامی است"),
|
||||
lastName: Yup.string().required("نام خانوادگی الزامی است"),
|
||||
roleId: Yup.string().required("نقش الزامی است"),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (!id) {
|
||||
toast.error("شناسه مدیر یافت نشد");
|
||||
return;
|
||||
}
|
||||
|
||||
updateAdmin(
|
||||
{ id, params: values },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("مدیر با موفقیت بهروزرسانی شد");
|
||||
navigate(Pages.admins.list);
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(extractErrorMessage(error));
|
||||
},
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
phone: Yup.string().required('شماره تلفن الزامی است'),
|
||||
firstName: Yup.string().required('نام الزامی است'),
|
||||
lastName: Yup.string().required('نام خانوادگی الزامی است'),
|
||||
roleId: Yup.string().required('نقش الزامی است'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (!id) {
|
||||
toast.error('شناسه مدیر یافت نشد')
|
||||
return
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
updateAdmin({ id, params: values }, {
|
||||
onSuccess: () => {
|
||||
toast.success('مدیر با موفقیت بهروزرسانی شد')
|
||||
navigate(Pages.admins.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (admin) {
|
||||
formik.setValues({
|
||||
phone: admin.phone || '',
|
||||
firstName: admin.firstName || '',
|
||||
lastName: admin.lastName || '',
|
||||
roleId: admin.roles?.[0]?.role?.id || '',
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [admin])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='w-full mt-4 flex justify-center items-center'>
|
||||
<div>در حال بارگذاری...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!admin) {
|
||||
return (
|
||||
<div className='w-full mt-4 flex justify-center items-center'>
|
||||
<div>مدیر یافت نشد</div>
|
||||
</div>
|
||||
)
|
||||
useEffect(() => {
|
||||
if (admin) {
|
||||
formik.setValues({
|
||||
phone: admin.phone || "",
|
||||
firstName: admin.firstName || "",
|
||||
lastName: admin.lastName || "",
|
||||
roleId: admin.role?.id || "",
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [admin]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div className='text-lg font-light'>
|
||||
ویرایش مدیر
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-5'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isloading={isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle className='size-5' color='white' />
|
||||
<div>ذخیره تغییرات</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full mt-4 flex justify-center items-center">
|
||||
<div>در حال بارگذاری...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className='bg-white rounded-3xl p-6'>
|
||||
<div className='mb-4'>
|
||||
<Input
|
||||
label='نام'
|
||||
name='firstName'
|
||||
value={formik.values.firstName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label='نام خانوادگی'
|
||||
name='lastName'
|
||||
value={formik.values.lastName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label='شماره تلفن'
|
||||
name='phone'
|
||||
value={formik.values.phone}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<Select
|
||||
label='نقش'
|
||||
name='roleId'
|
||||
value={formik.values.roleId}
|
||||
onChange={formik.handleChange}
|
||||
items={roles}
|
||||
placeholder='نقش را انتخاب کنید'
|
||||
error_text={formik.touched.roleId && formik.errors.roleId ? formik.errors.roleId : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
if (!admin) {
|
||||
return (
|
||||
<div className="w-full mt-4 flex justify-center items-center">
|
||||
<div>مدیر یافت نشد</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<div className="text-lg font-light">ویرایش مدیر</div>
|
||||
<div>
|
||||
<Button className="px-5" onClick={() => formik.handleSubmit()} isloading={isPending}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<TickCircle className="size-5" color="white" />
|
||||
<div>ذخیره تغییرات</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
export default UpdateAdmin
|
||||
<div className="mt-6">
|
||||
<div className="bg-white rounded-3xl p-6">
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="نام"
|
||||
name="firstName"
|
||||
value={formik.values.firstName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Input
|
||||
label="نام خانوادگی"
|
||||
name="lastName"
|
||||
value={formik.values.lastName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Input label="شماره تلفن" name="phone" value={formik.values.phone} onChange={formik.handleChange} error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ""} />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Select
|
||||
label="نقش"
|
||||
name="roleId"
|
||||
value={formik.values.roleId}
|
||||
onChange={formik.handleChange}
|
||||
items={roles}
|
||||
placeholder="نقش را انتخاب کنید"
|
||||
error_text={formik.touched.roleId && formik.errors.roleId ? formik.errors.roleId : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateAdmin;
|
||||
|
||||
@@ -28,6 +28,7 @@ export type Admin = {
|
||||
lastName: string;
|
||||
phone: string;
|
||||
roles: AdminRole[];
|
||||
role: RoleDetail;
|
||||
};
|
||||
|
||||
export type GetAdminsParams = {
|
||||
|
||||
+24
-36
@@ -1,42 +1,30 @@
|
||||
import { type FC } from 'react'
|
||||
import LogoImage from '../../assets/images/logo.png'
|
||||
import LogoSmallImage from '../../assets/images/logo-small.png'
|
||||
import { useAuthStore } from './store/AuthStore'
|
||||
import LoginStep1 from './components/LoginStep1'
|
||||
import LoginStep2 from './components/LoginStep2'
|
||||
import { type FC } from "react";
|
||||
import LogoSmallImage from "../../assets/images/logo-small.png";
|
||||
import LogoImage from "../../assets/images/logo.png";
|
||||
import LoginStep1 from "./components/LoginStep1";
|
||||
import LoginStep2 from "./components/LoginStep2";
|
||||
import { useAuthStore } from "./store/AuthStore";
|
||||
|
||||
const Login: FC = () => {
|
||||
const { stepLogin, phone } = useAuthStore();
|
||||
|
||||
const { stepLogin, phone } = useAuthStore()
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4">
|
||||
<div className="flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden">
|
||||
<div className="flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7">
|
||||
<div className="flex-1 h-full flex flex-col">
|
||||
<img src={LogoSmallImage} className="w-8" />
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||
<div className='flex-1 h-full flex flex-col'>
|
||||
<img src={LogoSmallImage} className='w-8' />
|
||||
|
||||
<div className='flex flex-1 flex-col h-full justify-center'>
|
||||
{
|
||||
stepLogin === 1 ?
|
||||
<LoginStep1 />
|
||||
:
|
||||
stepLogin === 2 && !!phone ?
|
||||
<LoginStep2 />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||
<img src={LogoImage} className='h-20' />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col h-full justify-center">{stepLogin === 1 ? <LoginStep1 /> : stepLogin === 2 && !!phone ? <LoginStep2 /> : null}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
<div className="flex-1 min-w-[50%] lg:flex hidden justify-center items-center">
|
||||
<img src={LogoImage} className="h-14" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
||||
@@ -1,224 +1,192 @@
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
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])
|
||||
// استفاده از paymentsChartData.data مستقیم برای دریافت دادههای واقعی
|
||||
// پیدا کردن آخرین آیتمی که label آن با formatDateLabel match میکند
|
||||
let dataPoint: { cash: number; online: number } | null = null;
|
||||
|
||||
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 (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 (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
|
||||
}
|
||||
if (matchingItems.length > 0) {
|
||||
dataPoint = matchingItems[matchingItems.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
// اگر پیدا نکردیم، از chartData استفاده میکنیم
|
||||
if (!dataPoint) {
|
||||
const matchingItems = chartData.filter((item) => item.month === label);
|
||||
if (matchingItems.length > 0) {
|
||||
dataPoint = matchingItems[matchingItems.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
if (!dataPoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cashValue = dataPoint.cash;
|
||||
const onlineValue = dataPoint.online;
|
||||
|
||||
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 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>
|
||||
{formatPrice(totalRevenue)}
|
||||
</div>
|
||||
|
||||
</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>
|
||||
|
||||
<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'>
|
||||
{/* 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'
|
||||
@@ -248,165 +216,74 @@ const RevenueChart: FC = () => {
|
||||
</button>
|
||||
</div> */}
|
||||
|
||||
{/* مبلغ و درصد */}
|
||||
{/* مبلغ و درصد */}
|
||||
|
||||
<div>
|
||||
<span className='text-sm text-gray-500'>مجموع درآمد</span>
|
||||
<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>
|
||||
<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
|
||||
<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;
|
||||
|
||||
Reference in New Issue
Block a user