Compare commits

...

3 Commits

Author SHA1 Message Date
hamid zarghami 4fc7d19766 fix role not found
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-29 15:05:30 +03:30
hamid zarghami b155c0b959 revenuechart 2026-06-29 14:54:56 +03:30
hamid zarghami 53ebbc8d07 login image change size 2026-06-29 14:51:32 +03:30
5 changed files with 434 additions and 552 deletions
+36 -12
View File
@@ -17,30 +17,54 @@ type Props = {
} & SelectHTMLAttributes<HTMLSelectElement> } & SelectHTMLAttributes<HTMLSelectElement>
const Select: FC<Props> = (props: Props) => { 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 ( return (
<div className='w-full relative'> <div className='w-full relative'>
{ {
props.label && label &&
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
<label className='text-sm'> <label className='text-sm'>
{props.label} {label}
</label> </label>
{props.isNotRequired && ( {isNotRequired && (
<span className='text-xs text-description'>(اختیاری)</span> <span className='text-xs text-description'>(اختیاری)</span>
)} )}
</div> </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', '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, className,
props.label && 'mt-1' label && 'mt-1'
)}> )}>
{ {
props.placeholder && placeholder &&
<option value="" disabled selected>{props.placeholder}</option> <option value="" disabled>{placeholder}</option>
} }
{ {
props.items?.map((item) => { items?.map((item) => {
return ( return (
<option key={item.value} value={item.value}> <option key={item.value} value={item.value}>
{item.label} {item.label}
@@ -51,12 +75,12 @@ const Select: FC<Props> = (props: Props) => {
</select> </select>
<ArrowDown2 size={16} color='black' className={clx( <ArrowDown2 size={16} color='black' className={clx(
'absolute z-0 top-3 left-2', '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 <Error
errorText={props.error_text} errorText={error_text}
/> />
: null : null
} }
+80 -88
View File
@@ -1,156 +1,148 @@
import { type FC, useEffect } from 'react' import Button from "@/components/Button";
import { useFormik } from 'formik' import Input from "@/components/Input";
import * as Yup from 'yup' import Select from "@/components/Select";
import { TickCircle } from 'iconsax-react' import { extractErrorMessage } from "@/config/func";
import { toast } from 'react-toastify' import { Pages } from "@/config/Pages";
import { useNavigate, useParams } from 'react-router-dom' import type { ErrorType } from "@/helpers/types";
import Button from '@/components/Button' import { useGetRoles } from "@/pages/roles/hooks/useRolesData";
import Input from '@/components/Input' import { useFormik } from "formik";
import Select from '@/components/Select' import { TickCircle } from "iconsax-react";
import { useGetAdminById, useUpdateAdmin } from './hooks/useAdminData' import { type FC, useEffect } from "react";
import { useGetRoles } from '@/pages/roles/hooks/useRolesData' import { useNavigate, useParams } from "react-router-dom";
import type { CreateAdminType } from './types/Types' import { toast } from "react-toastify";
import { Pages } from '@/config/Pages' import * as Yup from "yup";
import type { ErrorType } from '@/helpers/types' import { useGetAdminById, useUpdateAdmin } from "./hooks/useAdminData";
import { extractErrorMessage } from '@/config/func' import type { CreateAdminType } from "./types/Types";
const UpdateAdmin: FC = () => { const UpdateAdmin: FC = () => {
const navigate = useNavigate() const navigate = useNavigate();
const { id } = useParams() const { id } = useParams();
const { data: adminData, isLoading } = useGetAdminById(id!) const { data: adminData, isLoading } = useGetAdminById(id!);
const { mutate: updateAdmin, isPending } = useUpdateAdmin() const { mutate: updateAdmin, isPending } = useUpdateAdmin();
const { data: rolesData } = useGetRoles() const { data: rolesData } = useGetRoles();
const roles = rolesData?.data?.map(role => ({ const roles =
rolesData?.data?.map((role) => ({
label: role.name, label: role.name,
value: role.id value: role.id,
})) || [] })) || [];
const admin = adminData?.data const admin = adminData?.data;
const formik = useFormik<CreateAdminType>({ const formik = useFormik<CreateAdminType>({
initialValues: { initialValues: {
phone: '', phone: "",
firstName: '', firstName: "",
lastName: '', lastName: "",
roleId: '', roleId: "",
}, },
validationSchema: Yup.object().shape({ validationSchema: Yup.object().shape({
phone: Yup.string().required('شماره تلفن الزامی است'), phone: Yup.string().required("شماره تلفن الزامی است"),
firstName: Yup.string().required('نام الزامی است'), firstName: Yup.string().required("نام الزامی است"),
lastName: Yup.string().required('نام خانوادگی الزامی است'), lastName: Yup.string().required("نام خانوادگی الزامی است"),
roleId: Yup.string().required('نقش الزامی است'), roleId: Yup.string().required("نقش الزامی است"),
}), }),
onSubmit: (values) => { onSubmit: (values) => {
if (!id) { if (!id) {
toast.error('شناسه مدیر یافت نشد') toast.error("شناسه مدیر یافت نشد");
return return;
} }
updateAdmin({ id, params: values }, { updateAdmin(
{ id, params: values },
{
onSuccess: () => { onSuccess: () => {
toast.success('مدیر با موفقیت به‌روزرسانی شد') toast.success("مدیر با موفقیت به‌روزرسانی شد");
navigate(Pages.admins.list) navigate(Pages.admins.list);
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast.error(extractErrorMessage(error)) toast.error(extractErrorMessage(error));
}, },
})
}, },
}) );
},
});
useEffect(() => { useEffect(() => {
if (admin) { if (admin) {
formik.setValues({ formik.setValues({
phone: admin.phone || '', phone: admin.phone || "",
firstName: admin.firstName || '', firstName: admin.firstName || "",
lastName: admin.lastName || '', lastName: admin.lastName || "",
roleId: admin.roles?.[0]?.role?.id || '', roleId: admin.role?.id || "",
}) });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [admin]) }, [admin]);
if (isLoading) { if (isLoading) {
return ( return (
<div className='w-full mt-4 flex justify-center items-center'> <div className="w-full mt-4 flex justify-center items-center">
<div>در حال بارگذاری...</div> <div>در حال بارگذاری...</div>
</div> </div>
) );
} }
if (!admin) { if (!admin) {
return ( return (
<div className='w-full mt-4 flex justify-center items-center'> <div className="w-full mt-4 flex justify-center items-center">
<div>مدیر یافت نشد</div> <div>مدیر یافت نشد</div>
</div> </div>
) );
} }
return ( return (
<div className='mt-5'> <div className="mt-5">
<div className='flex w-full justify-between items-center'> <div className="flex w-full justify-between items-center">
<div className='text-lg font-light'> <div className="text-lg font-light">ویرایش مدیر</div>
ویرایش مدیر
</div>
<div> <div>
<Button <Button className="px-5" onClick={() => formik.handleSubmit()} isloading={isPending}>
className='px-5' <div className="flex gap-2 items-center">
onClick={() => formik.handleSubmit()} <TickCircle className="size-5" color="white" />
isloading={isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
<div>ذخیره تغییرات</div> <div>ذخیره تغییرات</div>
</div> </div>
</Button> </Button>
</div> </div>
</div> </div>
<div className='mt-6'> <div className="mt-6">
<div className='bg-white rounded-3xl p-6'> <div className="bg-white rounded-3xl p-6">
<div className='mb-4'> <div className="mb-4">
<Input <Input
label='نام' label="نام"
name='firstName' name="firstName"
value={formik.values.firstName} value={formik.values.firstName}
onChange={formik.handleChange} onChange={formik.handleChange}
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''} error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ""}
/> />
</div> </div>
<div className='mt-6'> <div className="mt-6">
<Input <Input
label='نام خانوادگی' label="نام خانوادگی"
name='lastName' name="lastName"
value={formik.values.lastName} value={formik.values.lastName}
onChange={formik.handleChange} onChange={formik.handleChange}
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''} error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ""}
/> />
</div> </div>
<div className='mt-6'> <div className="mt-6">
<Input <Input label="شماره تلفن" name="phone" value={formik.values.phone} onChange={formik.handleChange} error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ""} />
label='شماره تلفن'
name='phone'
value={formik.values.phone}
onChange={formik.handleChange}
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ''}
/>
</div> </div>
<div className='mt-6'> <div className="mt-6">
<Select <Select
label='نقش' label="نقش"
name='roleId' name="roleId"
value={formik.values.roleId} value={formik.values.roleId}
onChange={formik.handleChange} onChange={formik.handleChange}
items={roles} items={roles}
placeholder='نقش را انتخاب کنید' placeholder="نقش را انتخاب کنید"
error_text={formik.touched.roleId && formik.errors.roleId ? formik.errors.roleId : ''} error_text={formik.touched.roleId && formik.errors.roleId ? formik.errors.roleId : ""}
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default UpdateAdmin export default UpdateAdmin;
+1
View File
@@ -28,6 +28,7 @@ export type Admin = {
lastName: string; lastName: string;
phone: string; phone: string;
roles: AdminRole[]; roles: AdminRole[];
role: RoleDetail;
}; };
export type GetAdminsParams = { export type GetAdminsParams = {
+18 -30
View File
@@ -1,42 +1,30 @@
import { type FC } from 'react' import { type FC } from "react";
import LogoImage from '../../assets/images/logo.png' import LogoSmallImage from "../../assets/images/logo-small.png";
import LogoSmallImage from '../../assets/images/logo-small.png' import LogoImage from "../../assets/images/logo.png";
import { useAuthStore } from './store/AuthStore' import LoginStep1 from "./components/LoginStep1";
import LoginStep1 from './components/LoginStep1' import LoginStep2 from "./components/LoginStep2";
import LoginStep2 from './components/LoginStep2' import { useAuthStore } from "./store/AuthStore";
const Login: FC = () => { const Login: FC = () => {
const { stepLogin, phone } = useAuthStore();
const { stepLogin, phone } = useAuthStore()
return ( 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="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 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 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7">
<div className='flex-1 h-full flex flex-col'> <div className="flex-1 h-full flex flex-col">
<img src={LogoSmallImage} className='w-8' /> <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 className="flex flex-1 flex-col h-full justify-center">{stepLogin === 1 ? <LoginStep1 /> : stepLogin === 2 && !!phone ? <LoginStep2 /> : null}</div>
</div> </div>
</div> </div>
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'> <div className="flex-1 min-w-[50%] lg:flex hidden justify-center items-center">
<img src={LogoImage} className='h-20' /> <img src={LogoImage} className="h-14" />
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default Login export default Login;
+143 -266
View File
@@ -1,217 +1,185 @@
import { ArrowUp, DocumentDownload, ArrowDown } from 'iconsax-react' import Button from "@/components/Button";
import { type FC, useState, useMemo } from 'react' import DatePicker from "@/components/DatePicker";
import { import PageLoading from "@/components/PageLoading";
Area, import { formatPrice } from "@/helpers/func";
AreaChart, import { ArrowDown, ArrowUp, DocumentDownload } from "iconsax-react";
CartesianGrid, import moment from "moment-jalaali";
ResponsiveContainer, import { type FC, useMemo, useState } from "react";
Tooltip, import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
XAxis, import { useGetPaymentsChart } from "../hooks/useStatsData";
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 RevenueChart: FC = () => {
const [selectedTab] = useState< const [selectedTab] = useState<"daily" | "monthly" | "yearly">("monthly");
'daily' | 'monthly' | 'yearly' const [startDate, setStartDate] = useState<string>("");
>('monthly') const [endDate, setEndDate] = useState<string>("");
const [startDate, setStartDate] = useState<string>('')
const [endDate, setEndDate] = useState<string>('')
const convertToPersian = (gregorianDate: string | null | undefined): string | undefined => { const convertToPersian = (gregorianDate: string | null | undefined): string | undefined => {
if (!gregorianDate) return undefined if (!gregorianDate) return undefined;
const persianDate = moment(gregorianDate, 'YYYY-MM-DD').format('jYYYY-jMM-jDD') const persianDate = moment(gregorianDate, "YYYY-MM-DD").format("jYYYY-jMM-jDD");
return persianDate return persianDate;
} };
const apiParams = useMemo(() => { const apiParams = useMemo(() => {
const params: { const params: {
period?: 'daily' | 'monthly' | 'yearly'; period?: "daily" | "monthly" | "yearly";
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
} = {} } = {};
// فقط اگر تاریخ انتخاب شده باشد ارسال شود // فقط اگر تاریخ انتخاب شده باشد ارسال شود
if (startDate && startDate.trim() !== '') params.startDate = startDate if (startDate && startDate.trim() !== "") params.startDate = startDate;
if (endDate && endDate.trim() !== '') params.endDate = endDate if (endDate && endDate.trim() !== "") params.endDate = endDate;
return params return params;
}, [startDate, endDate]) }, [startDate, endDate]);
const { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams) const { data: paymentsChartData, isLoading } = useGetPaymentsChart(apiParams);
const formatDateLabel = (dateString: string, period: 'daily' | 'monthly' | 'yearly'): string => { const formatDateLabel = (dateString: string, period: "daily" | "monthly" | "yearly"): string => {
const date = new Date(dateString) const date = new Date(dateString);
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: 'numeric', year: "numeric",
} };
if (period === 'monthly') { if (period === "monthly") {
options.month = 'long' options.month = "long";
} else if (period === 'daily') { } else if (period === "daily") {
options.month = 'short' options.month = "short";
options.day = 'numeric' options.day = "numeric";
}
const persianDate = new Intl.DateTimeFormat('fa-IR', options).format(date)
return persianDate
} }
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.map((item) => ({
month: formatDateLabel(item.date, selectedTab), month: formatDateLabel(item.date, selectedTab),
online: item.online, online: item.online,
cash: item.cash, cash: item.cash,
})) }));
}, [paymentsChartData?.data, selectedTab]) }, [paymentsChartData?.data, selectedTab]);
const xAxisInterval = useMemo(() => { const xAxisInterval = useMemo(() => {
const dataLength = chartData.length const dataLength = chartData.length;
if (dataLength <= 7) return 0 if (dataLength <= 7) return 0;
if (dataLength <= 15) return 1 if (dataLength <= 15) return 1;
if (dataLength <= 30) return 2 if (dataLength <= 30) return 2;
if (dataLength <= 60) return 4 if (dataLength <= 60) return 4;
return Math.floor(dataLength / 12) return Math.floor(dataLength / 12);
}, [chartData.length]) }, [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);
}, [chartData]) }, [chartData]);
const revenueChange = useMemo(() => { const revenueChange = useMemo(() => {
if (chartData.length < 2) return null if (chartData.length < 2) return null;
const currentPeriod = chartData[chartData.length - 1] const currentPeriod = chartData[chartData.length - 1];
const previousPeriod = chartData[chartData.length - 2] const previousPeriod = chartData[chartData.length - 2];
const currentTotal = currentPeriod.online + currentPeriod.cash const currentTotal = currentPeriod.online + currentPeriod.cash;
const previousTotal = previousPeriod.online + previousPeriod.cash const previousTotal = previousPeriod.online + previousPeriod.cash;
if (previousTotal === 0) return null if (previousTotal === 0) return null;
const change = ((currentTotal - previousTotal) / previousTotal) * 100 const change = ((currentTotal - previousTotal) / previousTotal) * 100;
return change return change;
}, [chartData]) }, [chartData]);
const formatYAxis = (value: number) => { const formatYAxis = (value: number) => {
if (value === 0) return '۰' if (value === 0) return "۰";
if (value >= 1000000) { if (value >= 1000000) {
const millions = value / 1000000 const millions = value / 1000000;
const formatted = millions const formatted = millions.toFixed(1).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[parseInt(d)]);
.toFixed(1) return `${formatted} میلیون`;
.replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)])
return `${formatted} میلیون`
}
return value.toLocaleString('fa-IR')
} }
return value.toLocaleString("fa-IR");
};
const CustomTooltip = (props: { const CustomTooltip = (props: {
active?: boolean active?: boolean;
payload?: Array<{ payload?: Array<{
dataKey?: string dataKey?: string;
value?: number value?: number;
color?: string color?: string;
payload?: { payload?: {
cash?: number cash?: number;
online?: number online?: number;
month?: string month?: string;
} };
}> }>;
label?: string label?: string;
}) => { }) => {
const { active, payload, label } = props const { active, payload, label } = props;
if (!active || !payload || payload.length === 0 || !label) { if (!active || !payload || payload.length === 0 || !label) {
return null return null;
} }
// استفاده از paymentsChartData.data مستقیم برای دریافت داده‌های واقعی // استفاده از paymentsChartData.data مستقیم برای دریافت داده‌های واقعی
// پیدا کردن آخرین آیتمی که label آن با formatDateLabel match می‌کند // پیدا کردن آخرین آیتمی که label آن با formatDateLabel match می‌کند
let dataPoint: { cash: number; online: number } | null = null let dataPoint: { cash: number; online: number } | null = null;
if (paymentsChartData?.data) { if (paymentsChartData?.data) {
// پیدا کردن همه آیتم‌های match شده و استفاده از آخرین // پیدا کردن همه آیتم‌های match شده و استفاده از آخرین
const matchingItems: Array<{ cash: number; online: number }> = [] const matchingItems: Array<{ cash: number; online: number }> = [];
for (const item of paymentsChartData.data) { for (const item of paymentsChartData.data) {
const formattedLabel = formatDateLabel(item.date, selectedTab) const formattedLabel = formatDateLabel(item.date, selectedTab);
if (formattedLabel === label) { if (formattedLabel === label) {
matchingItems.push({ cash: item.cash, online: item.online }) matchingItems.push({ cash: item.cash, online: item.online });
} }
} }
if (matchingItems.length > 0) { if (matchingItems.length > 0) {
dataPoint = matchingItems[matchingItems.length - 1] dataPoint = matchingItems[matchingItems.length - 1];
} }
} }
// اگر پیدا نکردیم، از chartData استفاده می‌کنیم // اگر پیدا نکردیم، از chartData استفاده می‌کنیم
if (!dataPoint) { if (!dataPoint) {
const matchingItems = chartData.filter((item) => item.month === label) const matchingItems = chartData.filter((item) => item.month === label);
if (matchingItems.length > 0) { if (matchingItems.length > 0) {
dataPoint = matchingItems[matchingItems.length - 1] dataPoint = matchingItems[matchingItems.length - 1];
} }
} }
if (!dataPoint) { if (!dataPoint) {
return null return null;
} }
const cashValue = dataPoint.cash const cashValue = dataPoint.cash;
const onlineValue = dataPoint.online const onlineValue = dataPoint.online;
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">{label}</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]" />
<span className='text-xs text-gray-600'>پرداخت نقدی:</span> <span className="text-xs text-gray-600">پرداخت نقدی:</span>
<span className='text-xs font-medium text-gray-900'> <span className="text-xs font-medium text-gray-900">{cashValue.toLocaleString("fa-IR")} تومان</span>
{cashValue.toLocaleString('fa-IR')} تومان
</span>
</div> </div>
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<div className='w-2 h-2 rounded-full bg-[#6B7FED]' /> <div className="w-2 h-2 rounded-full bg-[#6B7FED]" />
<span className='text-xs text-gray-600'>پرداخت آنلاین:</span> <span className="text-xs text-gray-600">پرداخت آنلاین:</span>
<span className='text-xs font-medium text-gray-900'> <span className="text-xs font-medium text-gray-900">{onlineValue.toLocaleString("fa-IR")} تومان</span>
{onlineValue.toLocaleString('fa-IR')} تومان
</span>
</div> </div>
<div className='pt-1 mt-1 border-t border-gray-100'> <div className="pt-1 mt-1 border-t border-gray-100">
<span className='text-xs text-gray-600'>جمع:</span> <span className="text-xs text-gray-600">جمع:</span>
<span className='text-xs font-medium text-gray-900 mr-2'> <span className="text-xs font-medium text-gray-900 mr-2">{(cashValue + onlineValue).toLocaleString("fa-IR")} تومان</span>
{(cashValue + onlineValue).toLocaleString('fa-IR')} تومان
</span>
</div> </div>
</div> </div>
</div> </div>
) );
} };
return ( 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 */} {/* Header */}
<div className='flex items-start justify-between my-5'> <div className="flex items-start justify-between my-5">
<h3 className='text-base font-bold'>مجموع درآمد</h3> <h3 className="text-base font-bold">مجموع درآمد</h3>
{formatPrice(totalRevenue)}
</div> </div>
<div className='flex items-end gap-2'> <div className="flex items-end gap-2">
<DatePicker <DatePicker label="تاریخ شروع" placeholder="انتخاب تاریخ شروع" defaulValue={convertToPersian(startDate)} onChange={setStartDate} />
label='تاریخ شروع' <DatePicker label="تاریخ پایان" placeholder="انتخاب تاریخ پایان" defaulValue={convertToPersian(endDate)} onChange={setEndDate} />
placeholder='انتخاب تاریخ شروع' <Button className="">
defaulValue={convertToPersian(startDate)} <div className="flex gap-3">
onChange={setStartDate} <DocumentDownload size={18} color="#fff" />
/>
<DatePicker
label='تاریخ پایان'
placeholder='انتخاب تاریخ پایان'
defaulValue={convertToPersian(endDate)}
onChange={setEndDate}
/>
<Button className=''>
<div className='flex gap-3'>
<DocumentDownload
size={18}
color='#fff'
/>
<span>گرفتن گزارش</span> <span>گرفتن گزارش</span>
</div> </div>
</Button> </Button>
@@ -250,163 +218,72 @@ const RevenueChart: FC = () => {
{/* مبلغ و درصد */} {/* مبلغ و درصد */}
<div> <div className="flex items-center justify-between mb-5 mt-5">
<span className='text-sm text-gray-500'>مجموع درآمد</span> <div className="flex items-center gap-3">
</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 && ( {revenueChange !== null && (
<div <div className={`flex items-center gap-1 px-2 py-0.5 rounded-full ${revenueChange >= 0 ? "bg-green-50" : "bg-red-50"}`}>
className={`flex items-center gap-1 px-2 py-0.5 rounded-full ${revenueChange >= 0 {revenueChange >= 0 ? <ArrowUp size={12} color="#10b981" /> : <ArrowDown size={12} color="#ef4444" />}
? 'bg-green-50' <span className={`text-xs font-medium ${revenueChange >= 0 ? "text-green-600" : "text-red-600"}`}>{Math.abs(revenueChange).toFixed(1)}%</span>
: '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> </div>
<div className='flex items-center gap-3'> <div className="flex items-center gap-3">
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<div className='w-2 h-2 rounded-full bg-[#6B7FED]' /> <div className="w-2 h-2 rounded-full bg-[#6B7FED]" />
<span className='text-xs text-gray-500'> <span className="text-xs text-gray-500">پرداخت آنلاین</span>
پرداخت آنلاین
</span>
</div> </div>
<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]" />
<span className='text-xs text-gray-500'> <span className="text-xs text-gray-500">پرداخت نقدی</span>
پرداخت نقدی
</span>
</div> </div>
</div> </div>
</div> </div>
{/* نمودار */} {/* نمودار */}
{isLoading ? ( {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 /> <PageLoading />
</div> </div>
) : chartData.length === 0 ? ( ) : chartData.length === 0 ? (
<div className='w-full h-[400px] min-h-[400px] flex items-center justify-center text-gray-500'> <div className="w-full h-[400px] min-h-[400px] flex items-center justify-center text-gray-500">دادهای برای نمایش وجود ندارد</div>
دادهای برای نمایش وجود ندارد
</div>
) : ( ) : (
<div className='w-full h-[400px] min-h-[400px]'> <div className="w-full h-[400px] min-h-[400px]">
<ResponsiveContainer width='100%' height='100%'> <ResponsiveContainer width="100%" height="100%">
<AreaChart <AreaChart data={chartData} margin={{ top: 10, right: 10, left: 10, bottom: 40 }}>
data={chartData}
margin={{ top: 10, right: 10, left: 10, bottom: 40 }}
>
<defs> <defs>
<linearGradient <linearGradient id="colorOnline" x1="0" y1="0" x2="0" y2="1">
id='colorOnline' <stop offset="5%" stopColor="#6B7FED" stopOpacity={0.3} />
x1='0' <stop offset="95%" stopColor="#6B7FED" stopOpacity={0} />
y1='0'
x2='0'
y2='1'
>
<stop
offset='5%'
stopColor='#6B7FED'
stopOpacity={0.3}
/>
<stop
offset='95%'
stopColor='#6B7FED'
stopOpacity={0}
/>
</linearGradient> </linearGradient>
<linearGradient <linearGradient id="colorCash" x1="0" y1="0" x2="0" y2="1">
id='colorCash' <stop offset="5%" stopColor="#B0BAC9" stopOpacity={0.2} />
x1='0' <stop offset="95%" stopColor="#B0BAC9" stopOpacity={0} />
y1='0'
x2='0'
y2='1'
>
<stop
offset='5%'
stopColor='#B0BAC9'
stopOpacity={0.2}
/>
<stop
offset='95%'
stopColor='#B0BAC9'
stopOpacity={0}
/>
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid <CartesianGrid strokeDasharray="0" stroke="#f0f0f0" vertical={false} />
strokeDasharray='0'
stroke='#f0f0f0'
vertical={false}
/>
<XAxis <XAxis
dataKey='month' dataKey="month"
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
tick={{ fill: '#9CA3AF', fontSize: 11 }} tick={{ fill: "#9CA3AF", fontSize: 11 }}
reversed reversed
interval={xAxisInterval} interval={xAxisInterval}
angle={chartData.length > 15 ? -45 : 0} angle={chartData.length > 15 ? -45 : 0}
textAnchor={chartData.length > 15 ? 'end' : 'middle'} textAnchor={chartData.length > 15 ? "end" : "middle"}
height={chartData.length > 15 ? 60 : 40} height={chartData.length > 15 ? 60 : 40}
minTickGap={10} minTickGap={10}
/> />
<YAxis <YAxis orientation="right" axisLine={false} tickLine={false} tick={{ fill: "#9CA3AF", fontSize: 10 }} tickFormatter={formatYAxis} domain={[0, "dataMax + 2000000"]} tickMargin={50} />
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 />} />
<Area <Area type="natural" dataKey="cash" stroke="#B0BAC9" strokeWidth={2} fillOpacity={1} fill="url(#colorCash)" />
type='natural' <Area type="natural" dataKey="online" stroke="#6B7FED" strokeWidth={2} fillOpacity={1} fill="url(#colorOnline)" />
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> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
)} )}
</div> </div>
) );
} };
export default RevenueChart export default RevenueChart;