profile
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-21 16:04:57 +03:30
parent 8c89128dad
commit 71ee8f4575
10 changed files with 222 additions and 124 deletions
+1
View File
@@ -56,6 +56,7 @@ export const Paths = {
print: '/service/print', print: '/service/print',
}, },
home: '/home', home: '/home',
profile: '/profile',
myOrders: '/my-orders', myOrders: '/my-orders',
perfomaInvoice: { perfomaInvoice: {
list: '/invoice/list', list: '/invoice/list',
+26 -10
View File
@@ -1,31 +1,47 @@
import { useState, type FC } from "react"; import { useState, type FC } from "react";
import { useGetAdmins, useDeleteAdmin } from "./hooks/useAdminData"; import { useGetAdmins, useDeleteAdmin, useGetAdminMe } from "./hooks/useAdminData";
import Table from "@/components/Table"; import Table from "@/components/Table";
import PresignedImage from "@/components/PresignedImage"; import PresignedImage from "@/components/PresignedImage";
import moment from "moment-jalaali"; import moment from "moment-jalaali";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Paths } from "@/config/Paths"; import { Paths } from "@/config/Paths";
import Button from "@/components/Button"; import Button from "@/components/Button";
import { AddSquare, Eye } from "iconsax-react"; import { AddSquare, Eye, ShieldSecurity } from "iconsax-react";
import TrashWithConfrim from "@/components/TrashWithConfrim"; import TrashWithConfrim from "@/components/TrashWithConfrim";
const AdminList: FC = () => { const AdminList: FC = () => {
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const { data } = useGetAdmins(page); const { data } = useGetAdmins(page);
const deleteAdminMutation = useDeleteAdmin(); const deleteAdminMutation = useDeleteAdmin();
const { data: adminMe } = useGetAdminMe();
const canManageRoles = adminMe?.data?.role?.permissions?.some(
(p) => p.name === "manage_roles",
);
return ( return (
<div className="mt-5"> <div className="mt-5">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-lg font-light">مدیران</h1> <h1 className="text-lg font-light">مدیران</h1>
<Link to={Paths.admin.create}> <div className="flex gap-2">
<Button className="w-fit px-6"> {canManageRoles && (
<div className="flex gap-1.5"> <Link to={Paths.role.list}>
<AddSquare size={18} color="black" /> <Button className="w-fit px-6 bg-[#F5F7FC]">
<div className="text-[13px] font-light">ادمین جدید</div> <div className="flex gap-1.5">
</div> <ShieldSecurity size={18} color="black" />
</Button> <div className="text-[13px] font-light text-black">نقشها</div>
</Link> </div>
</Button>
</Link>
)}
<Link to={Paths.admin.create}>
<Button className="w-fit px-6">
<div className="flex gap-1.5">
<AddSquare size={18} color="black" />
<div className="text-[13px] font-light">ادمین جدید</div>
</div>
</Button>
</Link>
</div>
</div> </div>
<Table <Table
+12 -1
View File
@@ -1,6 +1,6 @@
import * as api from "../service/AdminService"; import * as api from "../service/AdminService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CreateAdminType } from "../types/Types"; import type { CreateAdminType, UpdateAdminMeType } from "../types/Types";
export const useGetAdmins = (page: number = 1, limit: number = 10) => { export const useGetAdmins = (page: number = 1, limit: number = 10) => {
return useQuery({ return useQuery({
@@ -59,3 +59,14 @@ export const useGetAdminMe = () => {
queryFn: api.getAdminMe, queryFn: api.getAdminMe,
}); });
}; };
export const useUpdateAdminMe = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params: UpdateAdminMeType) => api.updateAdminMe(params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["adminMe"] });
},
});
};
+6
View File
@@ -5,6 +5,7 @@ import type {
RolesResponseType, RolesResponseType,
AdminDetailResponseType, AdminDetailResponseType,
AdminMeResponseType, AdminMeResponseType,
UpdateAdminMeType,
} from "../types/Types"; } from "../types/Types";
export const getAdmins = async (page: number, limit: number = 10) => { export const getAdmins = async (page: number, limit: number = 10) => {
@@ -52,3 +53,8 @@ export const getAdminMe = async () => {
const { data } = await axios.get<AdminMeResponseType>("/admin/admins/me"); const { data } = await axios.get<AdminMeResponseType>("/admin/admins/me");
return data; return data;
}; };
export const updateAdminMe = async (params: UpdateAdminMeType) => {
const { data } = await axios.patch<AdminMeResponseType>("/admin/admins/me", params);
return data;
};
+7
View File
@@ -65,4 +65,11 @@ export interface AdminMeDataType {
avatarUrl?: string | null; avatarUrl?: string | null;
role: RoleItemType; role: RoleItemType;
} }
export type UpdateAdminMeType = {
phone: string;
firstName: string;
lastName: string;
avatarUrl?: string;
};
export type AdminMeResponseType = BaseResponse<AdminMeDataType>; export type AdminMeResponseType = BaseResponse<AdminMeDataType>;
+148
View File
@@ -0,0 +1,148 @@
import { useFormik } from 'formik';
import { type FC, useState } from 'react';
import * as Yup from 'yup';
import Button from '@/components/Button';
import { Edit } from 'iconsax-react';
import Input from '@/components/Input';
import UploadBox from '@/components/UploadBox';
import PresignedImage from '@/components/PresignedImage';
import { useGetAdminMe, useUpdateAdminMe } from '@/pages/admin/hooks/useAdminData';
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader';
import { toast } from 'react-toastify';
import { extractErrorMessage } from '@/config/func';
import type { UpdateAdminMeType } from '@/pages/admin/types/Types';
const Profile: FC = () => {
const { data: adminData, isLoading } = useGetAdminMe();
const { mutate: update, isPending: isUpdating } = useUpdateAdminMe();
const { mutate: upload, isPending: isUploading } = useSingleUpload();
const [file, setFile] = useState<File>();
const admin = adminData?.data;
const handleSave = (values: UpdateAdminMeType) => {
update(values, {
onSuccess: () => {
toast.success('پروفایل با موفقیت ویرایش شد');
},
onError: (error) => {
toast.error(extractErrorMessage(error));
},
});
};
const formik = useFormik<UpdateAdminMeType>({
enableReinitialize: true,
initialValues: {
firstName: admin?.firstName ?? '',
lastName: admin?.lastName ?? '',
phone: admin?.phone ? `0${admin.phone}` : '',
avatarUrl: admin?.avatarUrl ?? undefined,
},
validationSchema: Yup.object({
firstName: Yup.string()
.required('نام اجباری می باشد')
.min(2, 'نام باید حداقل 2 کاراکتر باشد'),
lastName: Yup.string()
.required('نام خانوادگی اجباری می باشد')
.min(2, 'نام خانوادگی باید حداقل 2 کاراکتر باشد'),
phone: Yup.string()
.required('شماره موبایل اجباری می باشد')
.matches(/^09[0-9]{9}$/, 'شماره موبایل معتبر نمی باشد'),
}),
onSubmit: (values) => {
if (file) {
upload(file, {
onSuccess: (data) => {
handleSave({ ...values, avatarUrl: data?.data?.key });
},
onError: (error) => toast.error(extractErrorMessage(error)),
});
} else {
handleSave(values);
}
},
});
if (isLoading) {
return (
<div className='mt-5 flex h-64 items-center justify-center'>
<div className='text-lg'>در حال بارگذاری...</div>
</div>
);
}
return (
<div className='mt-5'>
<div className='flex items-center justify-between'>
<h1 className='text-lg font-light'>پروفایل</h1>
<Button
className='w-fit px-6'
onClick={() => formik.handleSubmit()}
isLoading={isUpdating || isUploading}
>
<div className='flex gap-1.5'>
<Edit size={18} color='black' />
<div className='text-[13px] font-light'>ذخیره تغییرات</div>
</div>
</Button>
</div>
<div className='mt-8 flex-1 rounded-3xl bg-white p-6'>
{formik.values.avatarUrl && !file && (
<div className='mb-4'>
<PresignedImage
src={formik.values.avatarUrl}
className='h-20 w-20 rounded-full object-cover'
alt='آواتار'
/>
</div>
)}
<UploadBox
label='آواتار (اختیاری)'
onChange={(files) => setFile(files[0])}
/>
<div className='rowTwoInput mt-6'>
<Input
label='نام'
placeholder='نام را وارد کنید'
{...formik.getFieldProps('firstName')}
error_text={
formik.touched.firstName && formik.errors.firstName
? formik.errors.firstName
: ''
}
/>
<Input
label='نام خانوادگی'
placeholder='نام خانوادگی را وارد کنید'
{...formik.getFieldProps('lastName')}
error_text={
formik.touched.lastName && formik.errors.lastName
? formik.errors.lastName
: ''
}
/>
</div>
<div className='mt-6'>
<Input
label='شماره موبایل'
placeholder='شماره موبایل را وارد کنید'
{...formik.getFieldProps('phone')}
error_text={
formik.touched.phone && formik.errors.phone
? formik.errors.phone
: ''
}
/>
</div>
</div>
</div>
);
};
export default Profile;
-21
View File
@@ -1,21 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import axios from "@/config/axios";
export interface ProfileData {
user: {
firstName: string;
lastName: string;
email: string;
profilePic?: string;
};
}
export const useGetProfile = () => {
return useQuery({
queryKey: ["profile"],
queryFn: async (): Promise<{ data: ProfileData }> => {
const response = await axios.get("/profile");
return response.data;
},
});
};
+2
View File
@@ -47,6 +47,7 @@ import UpdateUser from "@/pages/user/Update";
import AdminList from "@/pages/admin/List"; import AdminList from "@/pages/admin/List";
import CreateAdmin from "@/pages/admin/Create"; import CreateAdmin from "@/pages/admin/Create";
import EditAdmin from "@/pages/admin/Update"; import EditAdmin from "@/pages/admin/Update";
import Profile from "@/pages/profile/Profile";
import RoleList from "@/pages/role/List"; import RoleList from "@/pages/role/List";
import CreateRole from "@/pages/role/Create"; import CreateRole from "@/pages/role/Create";
import UpdateRole from "@/pages/role/Update"; import UpdateRole from "@/pages/role/Update";
@@ -75,6 +76,7 @@ const MainRouter: FC = () => {
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path={Paths.home} element={<Home />} /> <Route path={Paths.home} element={<Home />} />
<Route path={Paths.profile} element={<Profile />} />
<Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} /> <Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} />
<Route path={Paths.payments.list} element={<PaymentsList />} /> <Route path={Paths.payments.list} element={<PaymentsList />} />
<Route path={Paths.perfomaInvoice.create} element={<CreateInvoice />} /> <Route path={Paths.perfomaInvoice.create} element={<CreateInvoice />} />
+19 -82
View File
@@ -1,21 +1,16 @@
import { type FC, useEffect, useState } from 'react' import { type FC } from 'react'
import Input from '@/components/Input' import Input from '@/components/Input'
import UserAvatar from '@/components/UserAvatar' import UserAvatar from '@/components/UserAvatar'
import { ArrowDown2, CloseCircle, HambergerMenu, Logout } from 'iconsax-react' import { HambergerMenu } from 'iconsax-react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import Notifications from '@/pages/notification/Notification' import Notifications from '@/pages/notification/Notification'
import { useSharedStore } from './store/useSharedStore' import { useSharedStore } from './store/useSharedStore'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import SidebarItem from './SidebarItem'
import { t } from '@/locale' import { t } from '@/locale'
import { useGetAdminMe } from '@/pages/admin/hooks/useAdminData' import { useGetAdminMe } from '@/pages/admin/hooks/useAdminData'
import { Paths } from '@/config/Paths' import { Paths } from '@/config/Paths'
const Header: FC = () => { const Header: FC = () => {
const location = useLocation();
const [popoverKey, setPopoverKey] = useState(0);
const { setOpenSidebar, openSidebar } = useSharedStore() const { setOpenSidebar, openSidebar } = useSharedStore()
const { data } = useGetAdminMe() const { data } = useGetAdminMe()
const admin = data?.data const admin = data?.data
@@ -23,10 +18,6 @@ const Header: FC = () => {
const displayName = [admin?.firstName, admin?.lastName].filter(Boolean).join(' ').trim() const displayName = [admin?.firstName, admin?.lastName].filter(Boolean).join(' ').trim()
const roleTitle = admin?.role?.title || admin?.role?.name || '' const roleTitle = admin?.role?.title || admin?.role?.name || ''
useEffect(() => {
setPopoverKey((prevKey) => prevKey + 1);
}, [location.pathname]);
return ( return (
<div className='fixed z-10 left-[var(--layout-frame)] right-[var(--layout-frame)] top-[var(--layout-frame)] flex h-[var(--layout-header-height)] items-center justify-between rounded-[32px] bg-white px-6 xl:left-auto xl:right-[calc(var(--layout-frame)+var(--layout-main-offset))] xl:h-[var(--layout-header-height-xl)] xl:w-[calc(100%-(var(--layout-frame)*2)-var(--layout-main-offset))]'> <div className='fixed z-10 left-[var(--layout-frame)] right-[var(--layout-frame)] top-[var(--layout-frame)] flex h-[var(--layout-header-height)] items-center justify-between rounded-[32px] bg-white px-6 xl:left-auto xl:right-[calc(var(--layout-frame)+var(--layout-main-offset))] xl:h-[var(--layout-header-height-xl)] xl:w-[calc(100%-(var(--layout-frame)*2)-var(--layout-main-offset))]'>
@@ -39,82 +30,28 @@ const Header: FC = () => {
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'> <div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
<HambergerMenu size={24} color='black' /> <HambergerMenu size={24} color='black' />
</div> </div>
{/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */}
<div className='flex xl:gap-6 gap-4 items-center'> <div className='flex xl:gap-6 gap-4 items-center'>
{/* <Link to={Pages.services.other}>
<Element3 color='black' className='xl:size-[18px] size-[17px]' />
</Link>
<Link className='xl:hidden' to={Pages.wallet}>
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
</Link>
<Link className='hidden xl:block' to={Pages.wallet}> */}
<Notifications /> <Notifications />
{admin && ( {admin && (
<Popover className="relative" key={popoverKey}> <Link to={Paths.profile} className='flex gap-2 items-center mt-2.5'>
<PopoverButton> <UserAvatar
<div className='flex gap-2 items-center mt-2.5'> src={admin.avatarUrl}
<UserAvatar firstName={admin.firstName}
src={admin.avatarUrl} lastName={admin.lastName}
firstName={admin.firstName} className='size-6'
lastName={admin.lastName} textClassName='text-xs'
className='size-6' />
textClassName='text-xs' <div className='xl:flex hidden flex-col items-start'>
/> <div className='text-xs'>
<div className='xl:flex hidden gap-1 items-center'> {displayName || String(admin.phone ?? '')}
<div className='flex flex-col items-start'>
<div className='text-xs'>
{displayName || String(admin.phone ?? '')}
</div>
{roleTitle && (
<div className='text-[10px] text-description'>
{roleTitle}
</div>
)}
</div>
<ArrowDown2 size={14} color='#8C90A3' />
</div>
</div> </div>
</PopoverButton> {roleTitle && (
<div className='text-[10px] text-description'>
<PopoverPanel style={{ minHeight: window.innerWidth < 1140 ? window.innerHeight : undefined }} anchor="bottom" className="flex xl:ml-6 overflow-auto flex-col gap-3 bg-white boxShadow xl:mt-7 z-30 py-4 text-xs rounded-2.5 xl:w-[300px] -mt-[50px] w-full fixed xl:h-fit h-full shadow-md"> {roleTitle}
<div className='absolute xl:hidden top-6 left-6'>
<CloseCircle onClick={() => setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' />
</div>
<Link to={`${Paths.admin.update}${admin.id}`} className='flex flex-col gap-2 items-center justify-center border-b border-[#EAEDF5] pb-4'>
<UserAvatar
src={admin.avatarUrl}
firstName={admin.firstName}
lastName={admin.lastName}
className='size-14'
textClassName='text-lg'
/>
<div>
{displayName || String(admin.phone ?? '')}
</div> </div>
{roleTitle && ( )}
<div className='text-description'> </div>
{roleTitle} </Link>
</div>
)}
<div className='text-description'>
{String(admin.phone ?? '')}
</div>
</Link>
<div className='px-6 -mt-[2px]'>
<SidebarItem
icon={<Logout color={'black'} size={20} />}
title={t('sidebar.logout')}
isActive
link={Paths.auth.login}
isWithoutLine
isLogout
activeName=''
/>
</div>
</PopoverPanel>
</Popover>
)} )}
</div> </div>
</div> </div>
+1 -10
View File
@@ -13,7 +13,6 @@ import {
Printer, Printer,
Receipt21, Receipt21,
MoneyRecive, MoneyRecive,
ShieldSecurity,
Teacher, Teacher,
User, User,
} from 'iconsax-react'; } from 'iconsax-react';
@@ -128,20 +127,12 @@ const SideBar: FC = () => {
<SideBarItem <SideBarItem
icon={<User size={iconSizeSideBar} color="#4F5260" />} icon={<User size={iconSizeSideBar} color="#4F5260" />}
title={'ادمین'} title={'مدیران'}
isActive={isActive('admin')} isActive={isActive('admin')}
link={Paths.admin.list} link={Paths.admin.list}
activeName='manage_admins' activeName='manage_admins'
/> />
<SideBarItem
icon={<ShieldSecurity size={iconSizeSideBar} color="#4F5260" />}
title={'نقش‌ها'}
isActive={isActive('role')}
link={Paths.role.list}
activeName='manage_roles'
/>
<SideBarItem <SideBarItem
icon={<ElementEqual size={iconSizeSideBar} color="#4F5260" />} icon={<ElementEqual size={iconSizeSideBar} color="#4F5260" />}
title={'محصولات'} title={'محصولات'}