auth + companies + invoice
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
VITE_TOKEN_NAME = 'dsc_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token'
|
||||
VITE_BASE_URL = 'http://192.168.1.113:4001'
|
||||
# VITE_BASE_URL = 'https://danak-zone-api.dev.danakcorp.com'
|
||||
VITE_DANAK_BASE_URL ='http://192.168.1.113:4000'
|
||||
VITE_SERVICE_ID = '7e3c2f08-b7e7-4402-ae5f-fea99cff56bd'
|
||||
VITE_WORKSPACE_ID = 'workspace_id'
|
||||
# VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
|
||||
VITE_LOGIN_URL = 'http://localhost:5174/auth/login'
|
||||
@@ -28,8 +28,6 @@ yarn-error.log*
|
||||
*.sublime-workspace
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
|
||||
# cache
|
||||
.cache/
|
||||
.eslintcache
|
||||
|
||||
+8
-8
@@ -36,7 +36,6 @@ const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: async (error: IApiErrorRepsonse) => {
|
||||
if (error?.response?.status === 401) {
|
||||
return
|
||||
try {
|
||||
// Use a flag to track if refresh is in progress
|
||||
if (window.isRefreshingToken) {
|
||||
@@ -60,15 +59,12 @@ const queryClient = new QueryClient({
|
||||
const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' });
|
||||
|
||||
if (data?.accessToken?.token) {
|
||||
// Save the new token
|
||||
await setToken(data?.accessToken?.token);
|
||||
|
||||
// If refresh token also returned, update it
|
||||
if (data.refreshToken?.token) {
|
||||
await setRefreshToken(data.refreshToken?.token);
|
||||
}
|
||||
|
||||
// Retry the failed request
|
||||
queryClient.invalidateQueries();
|
||||
return;
|
||||
} else {
|
||||
@@ -82,7 +78,7 @@ const queryClient = new QueryClient({
|
||||
// Clear tokens and redirect to login
|
||||
await removeToken();
|
||||
await removeRefreshToken();
|
||||
window.location.href = '/auth/login';
|
||||
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?redirect=${window.location.href}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,11 +106,15 @@ const App: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const workspaceId = urlParams.get('workspaceId');
|
||||
if (workspaceId) {
|
||||
localStorage.setItem(import.meta.env.VITE_WORKSPACE_ID, workspaceId);
|
||||
window.location.href = window.location.pathname
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -4,6 +4,9 @@ export const Pages = {
|
||||
register: "/auth/register",
|
||||
forgotPassword: "/auth/forgot",
|
||||
},
|
||||
requests: {
|
||||
list: "/requests/list",
|
||||
},
|
||||
dashboard: "/dashboard",
|
||||
services: {
|
||||
mine: "/services",
|
||||
|
||||
@@ -16,6 +16,9 @@ axiosInstance.interceptors.request.use(async function (config) {
|
||||
const token = getToken();
|
||||
|
||||
config.headers.Authorization = "Bearer " + token;
|
||||
config.headers["x-business-id"] = localStorage.getItem(
|
||||
import.meta.env.VITE_WORKSPACE_ID
|
||||
);
|
||||
return config;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import axios from "axios";
|
||||
import { getToken } from "./func";
|
||||
|
||||
const axiosDanak = axios.create({
|
||||
baseURL: import.meta.env.VITE_DANAK_BASE_URL,
|
||||
});
|
||||
|
||||
axiosDanak.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axiosDanak.interceptors.request.use(async function (config) {
|
||||
const token = getToken();
|
||||
|
||||
config.headers.Authorization = "Bearer " + token;
|
||||
return config;
|
||||
});
|
||||
|
||||
export default axiosDanak;
|
||||
+7
-5
@@ -20,19 +20,21 @@ export const getRefreshToken = () => {
|
||||
const isProduction = import.meta.env.MODE === "production";
|
||||
const domain = isProduction ? ".danakcorp.com" : undefined;
|
||||
|
||||
export const setToken = (token: string) => {
|
||||
export const setToken = async (token: string) => {
|
||||
Cookie.set(import.meta.env.VITE_TOKEN_NAME, token, { domain });
|
||||
};
|
||||
|
||||
export const setRefreshToken = (refreshToken: string) => {
|
||||
Cookie.set(import.meta.env.VITE_REFRESH_TOKEN_NAME, refreshToken, { domain });
|
||||
export const setRefreshToken = async (refreshToken: string) => {
|
||||
Cookie.set(import.meta.env.VITE_REFRESH_TOKEN_NAME, refreshToken, {
|
||||
domain,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeToken = () => {
|
||||
export const removeToken = async () => {
|
||||
Cookie.remove(import.meta.env.VITE_TOKEN_NAME, { domain, path: "/" });
|
||||
};
|
||||
|
||||
export const removeRefreshToken = () => {
|
||||
export const removeRefreshToken = async () => {
|
||||
Cookie.remove(import.meta.env.VITE_REFRESH_TOKEN_NAME, { domain, path: "/" });
|
||||
};
|
||||
|
||||
|
||||
@@ -181,3 +181,12 @@ tbody tr {
|
||||
background-color: #c9c9c9 !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+5
-2
@@ -55,7 +55,7 @@
|
||||
"ads": "تبلیغات",
|
||||
"blog": "بلاگ",
|
||||
"messages": "پیام ها",
|
||||
|
||||
"requests": "درخواست ها",
|
||||
"mainpage": "صفحه اصلی",
|
||||
"myservice": "سرویس های من",
|
||||
"other_service": "سایر سرویس ها",
|
||||
@@ -836,6 +836,9 @@
|
||||
"company_developed": "شرکت توسعه دهنده",
|
||||
"slug": "اسلاگ",
|
||||
"industries": "صنعت ها",
|
||||
"invoice_count": "تعداد صورتحساب"
|
||||
"invoice_count": "تعداد صورتحساب",
|
||||
"select_company": "انتخاب شرکت",
|
||||
"register_date": "تاریخ ثبت نام",
|
||||
"company": "شرکت"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Eye, Trash } from 'iconsax-react'
|
||||
import Td from '../../components/Td'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import Select from '../../components/Select'
|
||||
import Input from '../../components/Input'
|
||||
import { useDeleteAds, useGetAdsList } from './hooks/useAdsData'
|
||||
import { AdsItemType } from './types/AdsTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
import ToggleStatusAds from './components/ToggleStatus'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Pagination from '../../components/Pagination'
|
||||
const AddList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const [isActive, setIsActive] = useState<boolean>(true)
|
||||
const [since, setSince] = useState<string>('')
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const getAds = useGetAdsList(page, search, isActive, since)
|
||||
const deleteAds = useDeleteAds()
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteAds.mutate(id, {
|
||||
onSuccess: () => {
|
||||
getAds.refetch()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-4 min-h-[500px]'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('ads.ads')}
|
||||
</div>
|
||||
|
||||
<Link to={Pages.ads.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('ads.new_ads')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='mt-4'>
|
||||
<div className='flex flex-col xl:flex-row justify-between items-center xl:items-end'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='w-[200px]'>
|
||||
<DatePickerComponent
|
||||
label={t('ads.date')}
|
||||
onChange={(date) => setSince(moment(date, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
|
||||
placeholder=''
|
||||
/>
|
||||
</div>
|
||||
<div className='w-[200px]'>
|
||||
<Select
|
||||
label={t('ads.status')}
|
||||
items={[
|
||||
{
|
||||
label: 'فعال',
|
||||
value: 'true'
|
||||
},
|
||||
{
|
||||
label: 'غیر فعال',
|
||||
value: 'false'
|
||||
},
|
||||
]}
|
||||
className='bg-white border'
|
||||
onChange={(e) => setIsActive(e.target.value === 'true' ? true : false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 xl:mt-0 max-w-[420px] xl:w-[300px] w-full'>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('ads.search')}
|
||||
className='bg-white w-full'
|
||||
onChangeSearchFinal={(value) => setSearch(value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
getAds.isPending ?
|
||||
<div className='mt-5'>
|
||||
<PageLoading />
|
||||
</div>
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('ads.number')} />
|
||||
<Td text={t('ads.title')} />
|
||||
<Td text={t('ads.location')} />
|
||||
<Td text={t('ads.startDate')} />
|
||||
<Td text={t('ads.endDate')} />
|
||||
<Td text={t('ads.status')} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getAds.data?.data?.ads?.map((item: AdsItemType, index: number) => {
|
||||
return (
|
||||
<tr key={item.id} className='tr'>
|
||||
<Td text={String(index + 1)} />
|
||||
<Td text={item.title} />
|
||||
<Td text={t(`ads.${item.displayLocation}`)} />
|
||||
<Td text={moment(item.startDate).format('jYYYY-jMM-jDD')} />
|
||||
<Td text={moment(item.endDate).format('jYYYY-jMM-jDD')} />
|
||||
<Td text={t('')}>
|
||||
<ToggleStatusAds
|
||||
defaultActive={item.isActive}
|
||||
id={item.id}
|
||||
/>
|
||||
</Td>
|
||||
<Td text={''}>
|
||||
<div className='flex gap-3'>
|
||||
<Link to={Pages.ads.update + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' />
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
onPageChange={(page) => setPage(page)}
|
||||
totalPages={getAds.data?.data?.pager?.totalPages}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddList
|
||||
@@ -1,180 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../components/Input'
|
||||
import Button from '../../components/Button'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { TickCircle, Link2 } from 'iconsax-react'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import { useFormik } from 'formik'
|
||||
import { AdsDisplayLocation, CreateAdsType } from './types/AdsTypes'
|
||||
import * as Yup from 'yup'
|
||||
import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { useCreateAds } from './hooks/useAdsData'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { clx } from '../../helpers/utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
const CreateAd: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const singleUpload = useSingleUpload()
|
||||
const createAds = useCreateAds()
|
||||
|
||||
const formik = useFormik<CreateAdsType>({
|
||||
initialValues: {
|
||||
displayLocation: '',
|
||||
endDate: '',
|
||||
imageUrl: '',
|
||||
isActive: true,
|
||||
link: '',
|
||||
serviceId: undefined,
|
||||
startDate: '',
|
||||
title: ''
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
displayLocation: Yup.string().required(t('errors.required')),
|
||||
endDate: Yup.string().required(t('errors.required')),
|
||||
link: Yup.string().required(t('errors.required')),
|
||||
startDate: Yup.string().required(t('errors.required'))
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.imageUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
|
||||
createAds.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
navigate(Pages.ads.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error(t('ads.file_error'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('ads.new_ads')}
|
||||
</div>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createAds.isPending || singleUpload.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
{t('ads.submit')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-col-reverse xl:flex-row gap-6 xl:mt-8 mt-4'>
|
||||
<div className='flex-1 min-h-[calc(100vh-201px)] bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label={t('ads.title')}
|
||||
placeholder={t('ads.enter_your_title')}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<DatePickerComponent
|
||||
label={t('ads.startDate')}
|
||||
onChange={(value) => formik.setFieldValue('startDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
|
||||
placeholder=''
|
||||
error_text={formik.touched.startDate && formik.errors.startDate ? formik.errors.startDate : ''}
|
||||
/>
|
||||
<DatePickerComponent
|
||||
label={t('ads.endDate')}
|
||||
onChange={(value) => formik.setFieldValue('endDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
|
||||
placeholder=''
|
||||
error_text={formik.touched.endDate && formik.errors.endDate ? formik.errors.endDate : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 relative'>
|
||||
<Input
|
||||
label={t('ads.link')}
|
||||
placeholder={t('ads.enter_your_link')}
|
||||
{...formik.getFieldProps('link')}
|
||||
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ''}
|
||||
/>
|
||||
<Link2 size={20} color='blue' className={clx(
|
||||
'absolute left-4 bottom-[10px] cursor-pointer',
|
||||
formik.touched.link && formik.errors.link ? 'bottom-[31px]' : ''
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-[calc(100vh-201px)] bg-white w-full xl:w-sidebar py-10 px-5 h-fit rounded-3xl'>
|
||||
<div className='text-sm flex items-center justify-between'>
|
||||
<p>
|
||||
وضعیت تبلیغ
|
||||
</p>
|
||||
<div className='flex gap-1 text-xs items-center text-description'>
|
||||
<div>
|
||||
{t('ads.deactive')}
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className='mt-6 text-sm'>{t('ads.location')}</p>
|
||||
|
||||
<div className='p-2 border border-[##D0D0D0] rounded-lg mt-1'>
|
||||
|
||||
{Object.values(AdsDisplayLocation).map((location) => (
|
||||
<div className='flex items-center py-2' key={location}>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={formik.values.displayLocation === location}
|
||||
onChange={() => formik.setFieldValue('displayLocation', location)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
{t(`ads.${location}`)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
<div className='mt-8'>
|
||||
<UploadBoxDraggble
|
||||
label={t('ads.upload_picture')}
|
||||
onChange={(value) => setFile(value[0])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAd
|
||||
@@ -1,258 +0,0 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../components/Input'
|
||||
import Button from '../../components/Button'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { TickCircle, Link2 } from 'iconsax-react'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import { useFormik } from 'formik'
|
||||
import { AdsDisplayLocation, CreateAdsType } from './types/AdsTypes'
|
||||
import * as Yup from 'yup'
|
||||
import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { useGetDetailAds, useUpdateAds } from './hooks/useAdsData'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { clx } from '../../helpers/utils'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
const UpdateAds: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const singleUpload = useSingleUpload()
|
||||
const updateAds = useUpdateAds(id ? id : '')
|
||||
const getDetail = useGetDetailAds(id)
|
||||
|
||||
const formik = useFormik<CreateAdsType>({
|
||||
initialValues: {
|
||||
displayLocation: '',
|
||||
endDate: '',
|
||||
imageUrl: '',
|
||||
isActive: true,
|
||||
link: '',
|
||||
serviceId: undefined,
|
||||
startDate: '',
|
||||
title: ''
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
displayLocation: Yup.string().required(t('errors.required')),
|
||||
endDate: Yup.string().required(t('errors.required')),
|
||||
link: Yup.string().required(t('errors.required')),
|
||||
startDate: Yup.string().required(t('errors.required'))
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.imageUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateAds.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
navigate(Pages.ads.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (getDetail.data) {
|
||||
console.log(getDetail.data);
|
||||
|
||||
formik.setValues({
|
||||
displayLocation: getDetail.data?.data?.ads?.displayLocation,
|
||||
endDate: moment(getDetail.data?.data?.ads?.endDate).format('jYYYY-jMM-jDD'),
|
||||
imageUrl: getDetail.data?.data?.ads?.imageUrl,
|
||||
isActive: getDetail.data?.data?.ads?.isActive,
|
||||
link: getDetail.data?.data?.ads?.link,
|
||||
serviceId: getDetail.data?.data?.ads?.serviceId,
|
||||
startDate: moment(getDetail.data?.data?.ads?.startDate).format('jYYYY-jMM-jDD'),
|
||||
title: getDetail.data?.data?.ads?.title
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getDetail.data])
|
||||
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('ads.new_ads')}
|
||||
</div>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateAds.isPending || singleUpload.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
{t('ads.submit')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-col-reverse xl:flex-row gap-6 xl:mt-8 mt-4'>
|
||||
<div className='flex-1 min-h-[calc(100vh-201px)] bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label={t('ads.title')}
|
||||
placeholder={t('ads.enter_your_title')}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<DatePickerComponent
|
||||
label={t('ads.startDate')}
|
||||
onChange={(value) => formik.setFieldValue('startDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
|
||||
placeholder=''
|
||||
error_text={formik.touched.startDate && formik.errors.startDate ? formik.errors.startDate : ''}
|
||||
defaulValue={formik.values.startDate}
|
||||
/>
|
||||
<DatePickerComponent
|
||||
label={t('ads.endDate')}
|
||||
onChange={(value) => formik.setFieldValue('endDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
|
||||
placeholder=''
|
||||
error_text={formik.touched.endDate && formik.errors.endDate ? formik.errors.endDate : ''}
|
||||
defaulValue={formik.values.endDate}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 relative'>
|
||||
<Input
|
||||
label={t('ads.link')}
|
||||
placeholder={t('ads.enter_your_link')}
|
||||
{...formik.getFieldProps('link')}
|
||||
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ''}
|
||||
/>
|
||||
<Link2 size={20} color='blue' className={clx(
|
||||
'absolute left-4 bottom-[10px] cursor-pointer',
|
||||
formik.touched.link && formik.errors.link ? 'bottom-[31px]' : ''
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-[calc(100vh-201px)] bg-white w-full xl:w-sidebar py-10 px-5 h-fit rounded-3xl'>
|
||||
<div className='text-sm flex items-center justify-between'>
|
||||
<p>
|
||||
وضعیت تبلیغ
|
||||
</p>
|
||||
<div className='flex gap-1 text-xs items-center text-description'>
|
||||
<div>
|
||||
{t('ads.deactive')}
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className='mt-6 text-sm'>{t('ads.location')}</p>
|
||||
|
||||
<div className='p-2 border border-[##D0D0D0] rounded-lg mt-1'>
|
||||
<div className='flex items-center py-2'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={formik.values.displayLocation === AdsDisplayLocation.HOMEPAGE_TOP}
|
||||
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.HOMEPAGE_TOP)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
{t(`ads.${AdsDisplayLocation.HOMEPAGE_TOP}`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center py-2'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={formik.values.displayLocation === AdsDisplayLocation.SINGLE_SERVICE_PAGE}
|
||||
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.SINGLE_SERVICE_PAGE)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
{t(`ads.${AdsDisplayLocation.SINGLE_SERVICE_PAGE}`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center py-2'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={formik.values.displayLocation === AdsDisplayLocation.MY_SERVICES_PAGE}
|
||||
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.MY_SERVICES_PAGE)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
{t(`ads.${AdsDisplayLocation.MY_SERVICES_PAGE}`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center py-2'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={formik.values.displayLocation === AdsDisplayLocation.OTHER_SERVICES_TOP}
|
||||
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.OTHER_SERVICES_TOP)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
{t(`ads.${AdsDisplayLocation.OTHER_SERVICES_TOP}`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center py-2'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={formik.values.displayLocation === AdsDisplayLocation.OTHER_SERVICES_LEFT}
|
||||
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.OTHER_SERVICES_LEFT)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
{t(`ads.${AdsDisplayLocation.OTHER_SERVICES_LEFT}`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center py-2'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={formik.values.displayLocation === AdsDisplayLocation.SERVICE}
|
||||
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.SERVICE)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
{t(`ads.${AdsDisplayLocation.SERVICE}`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className='mt-8'>
|
||||
<UploadBoxDraggble
|
||||
label={t('ads.upload_picture')}
|
||||
onChange={(value) => setFile(value[0])}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-4'>
|
||||
<img src={formik.values.imageUrl} alt='ads' className='size-8 object-cover rounded-lg' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateAds
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Image, DocumentUpload } from 'iconsax-react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
|
||||
|
||||
|
||||
const ImageUploader: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFile(acceptedFiles[0])
|
||||
}, [])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='mt-6 text-sm'>{t('ads.upload_picture')}</p>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className='py-12 border border-dashed border-[#8C90A3] rounded-lg mt-2 flex items-center justify-center'>
|
||||
<div className='flex flex-col items-center space-y-4'>
|
||||
<Image size={45} color='#8C90A3' variant="Bold" className='border-[3px] border-[#8C90A3] rounded-md'
|
||||
/>
|
||||
<p className='text-sm text-[#8C90A3]'>
|
||||
{
|
||||
file ?
|
||||
file.name
|
||||
:
|
||||
t('ads.to_upload_message')
|
||||
|
||||
}
|
||||
</p>
|
||||
<div className='flex gap-1'>
|
||||
<DocumentUpload
|
||||
size={20}
|
||||
color='black'
|
||||
/>
|
||||
<button className='text-sm'>
|
||||
<input {...getInputProps()} />
|
||||
{t('ads.upload')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ImageUploader
|
||||
@@ -1,28 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import SwitchComponent from '../../../components/Switch'
|
||||
import { useToggleStatusAds } from '../hooks/useAdsData'
|
||||
|
||||
type Props = {
|
||||
defaultActive: boolean,
|
||||
id: string
|
||||
}
|
||||
|
||||
const ToggleStatusAds: FC<Props> = (props: Props) => {
|
||||
|
||||
const [isActive, setIsActive] = useState<boolean>(props.defaultActive)
|
||||
const toggleStatus = useToggleStatusAds()
|
||||
|
||||
const handleChange = (value: boolean) => {
|
||||
setIsActive(value)
|
||||
toggleStatus.mutate(props.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<SwitchComponent
|
||||
active={isActive}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToggleStatusAds
|
||||
@@ -1,47 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/AdsService";
|
||||
import { CreateAdsType } from "../types/AdsTypes";
|
||||
|
||||
export const useGetAdsList = (
|
||||
page: number,
|
||||
search: string,
|
||||
isActive: boolean,
|
||||
since: string
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["ads", page, search, isActive, since],
|
||||
queryFn: () => api.getAdsList({ page, search, isActive, since }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateAds = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateAdsType) => api.createAds(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useToggleStatusAds = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: string) => api.toggleStatusAds(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetDetailAds = (id?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["ads-detail", id],
|
||||
queryFn: () => api.getDetailAds(id ? id : ""),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateAds = (id: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateAdsType) => api.updateAds(id, variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteAds = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: string) => api.deleteAds(variables),
|
||||
});
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateAdsType } from "../types/AdsTypes";
|
||||
|
||||
interface GetAdsListParams {
|
||||
page: number;
|
||||
search?: string;
|
||||
isActive?: boolean;
|
||||
since?: string;
|
||||
}
|
||||
|
||||
export const getAdsList = async ({
|
||||
page,
|
||||
search,
|
||||
isActive,
|
||||
since,
|
||||
}: GetAdsListParams) => {
|
||||
let query = `?page=${page}&q=${search}&isActive=${isActive ? "1" : "0"}`;
|
||||
if (since) {
|
||||
query += `&since=${since}`;
|
||||
}
|
||||
const { data } = await axios.get(`/advertise/list${query}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createAds = async (params: CreateAdsType) => {
|
||||
const { data } = await axios.post(`/advertise`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const toggleStatusAds = async (id: string) => {
|
||||
const { data } = await axios.patch(`/advertise/toggle-status/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getDetailAds = async (id: string) => {
|
||||
const { data } = await axios.get(`/advertise/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateAds = async (id: string, params: CreateAdsType) => {
|
||||
const { data } = await axios.patch(`/advertise/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteAds = async (id: string) => {
|
||||
const { data } = await axios.delete(`/advertise/${id}`);
|
||||
return data;
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
export type CreateAdsType = {
|
||||
title: string;
|
||||
displayLocation: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isActive: boolean;
|
||||
imageUrl: string;
|
||||
link: string;
|
||||
serviceId?: string;
|
||||
};
|
||||
|
||||
export enum AdsDisplayLocation {
|
||||
HOMEPAGE_TOP = "HOMEPAGE_TOP",
|
||||
SINGLE_SERVICE_PAGE = "SINGLE_SERVICE_PAGE",
|
||||
MY_SERVICES_PAGE = "MY_SERVICES_PAGE",
|
||||
OTHER_SERVICES_TOP = "OTHER_SERVICES_TOP",
|
||||
OTHER_SERVICES_LEFT = "OTHER_SERVICES_LEFT",
|
||||
SERVICE = "SERVICE",
|
||||
BLOG_PAGE_TOP_RIGHT = "BLOG_PAGE_TOP_RIGHT",
|
||||
BLOG_PAGE_BOTTOM_RIGHT = "BLOG_PAGE_BOTTOM_RIGHT",
|
||||
BLOG_PAGE_TOP_LEFT = "BLOG_PAGE_TOP_LEFT",
|
||||
}
|
||||
|
||||
export type AdsItemType = {
|
||||
id: string;
|
||||
title: string;
|
||||
displayLocation: AdsDisplayLocation;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isActive: boolean;
|
||||
imageUrl: string;
|
||||
link: string;
|
||||
service?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from "../../../config/axios";
|
||||
import axios from "../../../config/axiosDanak";
|
||||
import {
|
||||
CheckHasAccountPhoneType,
|
||||
CheckHasAccountType,
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../components/Button'
|
||||
import { TickCircle, TickSquare } from 'iconsax-react'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import Input from '../../components/Input'
|
||||
import { useFormik } from 'formik'
|
||||
import { CreateBankType } from './types/CardBankTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { useCreateBank } from './hooks/useCardBankData'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
const CreateBankCard: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const createBank = useCreateBank()
|
||||
|
||||
const formik = useFormik<CreateBankType>({
|
||||
initialValues: {
|
||||
accountOwnerName: '',
|
||||
bankName: '',
|
||||
cardNumber: '',
|
||||
IBan: '',
|
||||
isActive: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
accountOwnerName: Yup.string().required(t('errors.required')),
|
||||
bankName: Yup.string().required(t('errors.required')),
|
||||
cardNumber: Yup.string().required(t('errors.required')),
|
||||
IBan: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
createBank.mutate(values, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
navigate(Pages.cardBank.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('cardBank.new_account')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-4'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createBank.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle
|
||||
size={15}
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('cardBank.submit_account')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<div>
|
||||
{t('cardBank.account_information')}
|
||||
</div>
|
||||
|
||||
<div className='mt-10 flex items-center gap-5'>
|
||||
<div>
|
||||
{t('cardBank.status_account')}
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 rowTwoInput'>
|
||||
<Input
|
||||
label={t('cardBank.account_owner_name')}
|
||||
name='accountOwnerName'
|
||||
value={formik.values.accountOwnerName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.accountOwnerName && formik.errors.accountOwnerName ? formik.errors.accountOwnerName : ''}
|
||||
/>
|
||||
<Input
|
||||
label={t('cardBank.bank_name')}
|
||||
name='bankName'
|
||||
value={formik.values.bankName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.bankName && formik.errors.bankName ? formik.errors.bankName : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-8 rowTwoInput'>
|
||||
<Input
|
||||
label={t('cardBank.card_number')}
|
||||
name='cardNumber'
|
||||
value={formik.values.cardNumber}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.cardNumber && formik.errors.cardNumber ? formik.errors.cardNumber : ''}
|
||||
/>
|
||||
<Input
|
||||
label={t('cardBank.sheba')}
|
||||
name='IBan'
|
||||
value={formik.values.IBan}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.IBan && formik.errors.IBan ? formik.errors.IBan : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||
<div className='text-sm'>
|
||||
{t('ticket.title_hint')}
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className='flex items-start gap-2 border-b pb-5'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-2 mt-6'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateBankCard
|
||||
@@ -1,179 +0,0 @@
|
||||
import { FC, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../components/Button'
|
||||
import { TickCircle, TickSquare } from 'iconsax-react'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import Input from '../../components/Input'
|
||||
import { useFormik } from 'formik'
|
||||
import { CreateBankType } from './types/CardBankTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { useGetCardBankDetail, useUpdateBank } from './hooks/useCardBankData'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
const EditBankCard: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const updateBank = useUpdateBank(id ? id : '')
|
||||
const getCard = useGetCardBankDetail(id ? id : '')
|
||||
|
||||
const formik = useFormik<CreateBankType>({
|
||||
initialValues: {
|
||||
accountOwnerName: '',
|
||||
bankName: '',
|
||||
cardNumber: '',
|
||||
IBan: '',
|
||||
isActive: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
accountOwnerName: Yup.string().required(t('errors.required')),
|
||||
bankName: Yup.string().required(t('errors.required')),
|
||||
cardNumber: Yup.string().required(t('errors.required')),
|
||||
IBan: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
updateBank.mutate(values, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
navigate(Pages.cardBank.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (getCard.data?.data?.bankAccount) {
|
||||
const data = getCard.data?.data?.bankAccount
|
||||
|
||||
formik.setValues({
|
||||
accountOwnerName: data.accountOwnerName,
|
||||
bankName: data.bankName,
|
||||
cardNumber: data.cardNumber,
|
||||
IBan: data.IBan,
|
||||
isActive: data.isActive,
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getCard.data])
|
||||
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('cardBank.new_account')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-4'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateBank.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle
|
||||
size={15}
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('edit')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<div>
|
||||
{t('cardBank.account_information')}
|
||||
</div>
|
||||
|
||||
<div className='mt-10 flex items-center gap-5'>
|
||||
<div>
|
||||
{t('cardBank.status_account')}
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 rowTwoInput'>
|
||||
<Input
|
||||
label={t('cardBank.account_owner_name')}
|
||||
name='accountOwnerName'
|
||||
value={formik.values.accountOwnerName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.accountOwnerName && formik.errors.accountOwnerName ? formik.errors.accountOwnerName : ''}
|
||||
/>
|
||||
<Input
|
||||
label={t('cardBank.bank_name')}
|
||||
name='bankName'
|
||||
value={formik.values.bankName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.bankName && formik.errors.bankName ? formik.errors.bankName : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-8 rowTwoInput'>
|
||||
<Input
|
||||
label={t('cardBank.card_number')}
|
||||
name='cardNumber'
|
||||
value={formik.values.cardNumber}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.cardNumber && formik.errors.cardNumber ? formik.errors.cardNumber : ''}
|
||||
/>
|
||||
<Input
|
||||
label={t('cardBank.sheba')}
|
||||
name='IBan'
|
||||
value={formik.values.IBan}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.IBan && formik.errors.IBan ? formik.errors.IBan : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||
<div className='text-sm'>
|
||||
{t('ticket.title_hint')}
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className='flex items-start gap-2 border-b pb-5'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-2 mt-6'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditBankCard
|
||||
@@ -1,84 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { useGetCardBanks } from './hooks/useCardBankData'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Td from '../../components/Td'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreateBankItemType } from './types/CardBankTypes'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Eye } from 'iconsax-react'
|
||||
|
||||
const CardBankList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const getCardBanks = useGetCardBanks()
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
|
||||
{t('cardBank.manage_banks')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.cardBank.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('cardBank.add_account')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
getCardBanks.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('cardBank.account_owner_name')} />
|
||||
<Td text={t('cardBank.bank_name')} />
|
||||
<Td text={t('cardBank.card_number')} />
|
||||
<Td text={t('cardBank.sheba')} />
|
||||
<Td text={t('ticket.detail')} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getCardBanks.data?.data?.bankAccounts?.map((item: CreateBankItemType) => {
|
||||
return (
|
||||
<tr key={item.id} className='tr'>
|
||||
<Td text={item.accountOwnerName} />
|
||||
<Td text={item.bankName} />
|
||||
<Td text={item.cardNumber} />
|
||||
<Td text={item.IBan} />
|
||||
<Td text={''}>
|
||||
<Link to={Pages.cardBank.detail + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardBankList
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CreateBankType } from "../types/CardBankTypes";
|
||||
import * as api from "../service/CardBankService";
|
||||
|
||||
export const useCreateBank = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateBankType) => api.createCardBank(variables),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["cardBanks"],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCardBanks = () => {
|
||||
return useQuery({
|
||||
queryKey: ["cardBanks"],
|
||||
queryFn: api.getCardBanks,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCardBankDetail = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["cardBank", id],
|
||||
queryFn: () => api.getCardBankDetail(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateBank = (id: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateBankType) =>
|
||||
api.updateCardBank(id, variables),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["cardBanks"],
|
||||
});
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["cardBank", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateBankType } from "../types/CardBankTypes";
|
||||
|
||||
export const createCardBank = async (params: CreateBankType) => {
|
||||
const { data } = await axios.post(`/payments/bank-account`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCardBanks = async () => {
|
||||
const { data } = await axios.get(`/payments/bank-account`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCardBankDetail = async (id: string) => {
|
||||
const { data } = await axios.get(`/payments/bank-account/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateCardBank = async (id: string, params: CreateBankType) => {
|
||||
const { data } = await axios.patch(`/payments/bank-account/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
export type CreateBankType = {
|
||||
cardNumber: string;
|
||||
IBan: string;
|
||||
bankName: string;
|
||||
accountOwnerName: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
export type CreateBankItemType = {
|
||||
id: string;
|
||||
cardNumber: string;
|
||||
IBan: string;
|
||||
bankName: string;
|
||||
accountOwnerName: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { TickCircle } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useSingleUpload } from '../../service/hooks/useServiceData'
|
||||
import { useCreateIndustry } from '../hooks/useCompanyData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
refetch: () => void
|
||||
@@ -45,6 +46,9 @@ const CreateIndustry: FC<Props> = ({ refetch }) => {
|
||||
onSuccess: () => {
|
||||
toast.success('صنعت با موفقیت ثبت شد')
|
||||
refetch()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error?.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Eye } from 'iconsax-react'
|
||||
@@ -10,11 +10,13 @@ import { Pages } from '../../config/Pages'
|
||||
import { useGetCustomers } from './hooks/useCustomerData'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { CustomerItemType } from './types/CustomerTypes'
|
||||
import Pagination from '../../components/Pagination'
|
||||
|
||||
const CustomerList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const getCustomers = useGetCustomers()
|
||||
const [page, setPage] = useState(1)
|
||||
const getCustomers = useGetCustomers(page)
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
@@ -117,6 +119,12 @@ const CustomerList: FC = () => {
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination
|
||||
totalPages={getCustomers.data?.data?.pager?.totalPages}
|
||||
currentPage={page}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@ import * as api from "../service/CustomerService";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CreateCustomerType } from "../types/CustomerTypes";
|
||||
|
||||
export const useGetCustomers = () => {
|
||||
export const useGetCustomers = (page: number, withoutPaginate?: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: ["customers"],
|
||||
queryFn: () => api.getCustomers(),
|
||||
queryKey: ["customers", page],
|
||||
queryFn: () => api.getCustomers(page, withoutPaginate),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateCustomerType } from "../types/CustomerTypes";
|
||||
|
||||
export const getCustomers = async () => {
|
||||
const { data } = await axios.get(`/users/customers`);
|
||||
export const getCustomers = async (page: number, withoutPaginate?: boolean) => {
|
||||
const { data } = await axios.get(
|
||||
`/users/customers?page=${page}&paginate=${withoutPaginate ? "0" : "1"}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import { FC, Fragment, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../components/Input'
|
||||
import Button from '../../components/Button'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import RadioGroup from '../../components/RadioGroup'
|
||||
import { useFormik } from 'formik'
|
||||
import { CreateDiscountType } from './types/DiscountTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { useGetAllServices } from '../service/hooks/useServiceData'
|
||||
import { ServiceItemType } from '../service/types/ServiceTypes'
|
||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useCreateDiscount } from './hooks/useDiscountData'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
const CreateDiscount: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const [serviceId, setServiceId] = useState<string[]>([])
|
||||
// const [planId, setPlanId] = useState<string>('')
|
||||
// const [customerId, setCustomerId] = useState('')
|
||||
const getServices = useGetAllServices('', 1, '', '', undefined, 50)
|
||||
// const getPlans = useGetPlans(serviceId)
|
||||
// const getCustomers = useGetCustomers()
|
||||
const createDiscount = useCreateDiscount()
|
||||
|
||||
const formik = useFormik<CreateDiscountType>({
|
||||
initialValues: {
|
||||
endDate: '',
|
||||
startDate: '',
|
||||
title: '',
|
||||
type: 1,
|
||||
targetServices: [],
|
||||
direct: true,
|
||||
value: 0,
|
||||
isActive: true,
|
||||
userPhone: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
value: Yup.number().required(t('errors.required')),
|
||||
endDate: Yup.string().required(t('errors.required')),
|
||||
startDate: Yup.string().required(t('errors.required')),
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
type: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit(values) {
|
||||
values.targetServices = serviceId
|
||||
values.type = +values.type
|
||||
if (values.userPhone === '') {
|
||||
values.userPhone = undefined
|
||||
}
|
||||
const params: CreateDiscountType = {
|
||||
...values,
|
||||
startDate: moment(values.startDate, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'),
|
||||
endDate: moment(values.endDate, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'),
|
||||
value: +values.value
|
||||
}
|
||||
createDiscount.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
navigate(Pages.discount.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('discount.new_discount')}
|
||||
</div>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
{t('discount.submit')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||
<div className='flex-1 min-h-[calc(100vh-201px)] bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<div className='mt-6'>
|
||||
{t('discount.type')}
|
||||
<div className='rowTwoInput mt-2'>
|
||||
<RadioGroup
|
||||
items={[
|
||||
{
|
||||
label: t('discount.direct_discount'),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: t('discount.discount_code'),
|
||||
value: false
|
||||
}
|
||||
]}
|
||||
onChange={(value) => formik.setFieldValue('direct', value)}
|
||||
selected={formik.values.direct}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<Input
|
||||
label={t('discount.title')}
|
||||
placeholder={t('discount.enter_discount_description')}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
{/* <div className='w-full sm:mt-7 flex flex-col xl:flex-row items-center'>
|
||||
<button className='text-xs border h-[39px] xl:h-full w-full xl:w-[142px] border-black rounded-xl flex justify-center items-center gap-2'>
|
||||
<Refresh
|
||||
size={18} color='black'
|
||||
/>
|
||||
<p>
|
||||
{t('discount.auto_generate_code')}
|
||||
</p>
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<div className='w-full'>
|
||||
{t('discount.price')}
|
||||
<div className='rowTwoInput mt-2'>
|
||||
<RadioGroup
|
||||
items={[
|
||||
{
|
||||
label: t('discount.fixed_amount'),
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
label: t('discount.percent'),
|
||||
value: '0'
|
||||
}
|
||||
]}
|
||||
onChange={(value) => formik.setFieldValue('type', value)}
|
||||
selected={formik.values.type.toString()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
label={t('discount.price_or_percent')}
|
||||
placeholder={t('discount.enter_price_or_percent')}
|
||||
{...formik.getFieldProps('value')}
|
||||
error_text={formik.touched.value && formik.errors.value ? formik.errors.value : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<DatePickerComponent
|
||||
label={t('discount.startDate')}
|
||||
onChange={(value) => formik.setFieldValue('startDate', value)}
|
||||
placeholder=''
|
||||
error_text={formik.touched.startDate && formik.errors.startDate ? formik.errors.startDate : ''}
|
||||
/>
|
||||
<DatePickerComponent
|
||||
label={t('discount.endDate')}
|
||||
onChange={(value) => formik.setFieldValue('endDate', value)}
|
||||
placeholder=''
|
||||
error_text={formik.touched.endDate && formik.errors.endDate ? formik.errors.endDate : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
!formik.values.direct &&
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<Input
|
||||
label='شماره مشتری (اختیاری)'
|
||||
placeholder='شماره مشتری را وارد کنید'
|
||||
{...formik.getFieldProps('userPhone')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className='min-h-[calc(100vh-201px)] bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||
<div className='text-sm flex items-center justify-between'>
|
||||
<p>
|
||||
{'وضعیت تخفیف'}
|
||||
</p>
|
||||
<div className='flex gap-1 text-xs items-center text-description'>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
formik.values.direct &&
|
||||
<Fragment>
|
||||
<p className='mt-10 text-sm'>{t('discount.availabe_service')}</p>
|
||||
<div className='p-2 text-sm border border-[##D0D0D0] rounded-lg mt-2'>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('header.search')}
|
||||
/>
|
||||
{
|
||||
getServices.data?.data?.services?.map((item: ServiceItemType) => (
|
||||
<div key={item.id} className='flex gap-2 mt-3 py-2 items-center'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={serviceId.includes(item.id)}
|
||||
onChange={(e) => setServiceId(e.target.checked ? [...serviceId, item.id] : serviceId.filter(id => id !== item.id))}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs leading-5'>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
{/* {
|
||||
getPlans.data &&
|
||||
<div className='p-2 text-sm border border-[##D0D0D0] rounded-lg mt-2'>
|
||||
{
|
||||
getPlans.data?.data?.subscriptions?.map((item: PlanItemType) => (
|
||||
<div key={item.id} className='flex gap-2 mt-3 py-2 items-center'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={planId === item.id}
|
||||
onChange={(e) => setPlanId(e.target.checked ? item.id : '')}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs leading-5'>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
planId && getCustomers.data &&
|
||||
<div className='p-2 text-sm border border-[##D0D0D0] rounded-lg mt-2'>
|
||||
{
|
||||
getCustomers.data?.data?.customers?.map((item: CustomerItemType) => (
|
||||
<div key={item.id} className='flex gap-2 mt-3 py-2 items-center'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={customerId === item.id}
|
||||
onChange={(e) => setCustomerId(e.target.checked ? item.id : '')}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs leading-5'>
|
||||
{item.firstName + ' ' + item.lastName}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default CreateDiscount
|
||||
@@ -1,142 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Edit, Trash } from 'iconsax-react'
|
||||
import Td from '../../components/Td'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import Input from '../../components/Input'
|
||||
import { useGetDiscounts, useDeleteDiscount } from './hooks/useDiscountData'
|
||||
import { DiscountItemType } from './types/DiscountTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
import ToggleStatusDiscount from './components/ToggleStatusDiscount'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { NumberFormat } from '../../config/func'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
|
||||
const DiscountList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const getDiscounts = useGetDiscounts(search)
|
||||
const deleteDiscount = useDeleteDiscount()
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteDiscount.mutate(id, {
|
||||
onSuccess: () => {
|
||||
getDiscounts.refetch()
|
||||
toast.success(t('success'))
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className='mt-4 min-h-[500px]'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('discount.list')}
|
||||
</div>
|
||||
|
||||
<Link to={Pages.discount.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('discount.add_new_discount')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-end items-center'>
|
||||
|
||||
<div className='xl:w-[300px] w-full'>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('ads.search')}
|
||||
className='bg-white w-full'
|
||||
onChangeSearchFinal={(value) => setSearch(value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
getDiscounts.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('discount.title')} />
|
||||
<Td text={t('discount.type')} />
|
||||
<Td text={t('discount.customer')} />
|
||||
<Td text={t('discount.code')} />
|
||||
<Td text={t('discount.startDate')} />
|
||||
<Td text={t('discount.endDate')} />
|
||||
<Td text={t('discount.price_or_percent')} />
|
||||
<Td text={t('discount.status')} />
|
||||
<Td text={t('')} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getDiscounts.data?.data?.discounts?.map((item: DiscountItemType) => {
|
||||
return (
|
||||
<tr key={item.id} className='tr'>
|
||||
<Td text={item.title} />
|
||||
<Td text={t(`discount.${item.type}`)} />
|
||||
<Td text={item.user ? item.user.firstName + ' ' + item.user.lastName : '-'} />
|
||||
<Td text={item.code} />
|
||||
<Td text={moment(item.startDate, 'jYYYY-jMM-jDD').format('jYYYY-jMM-jDD')} />
|
||||
<Td text={moment(item.endDate, 'jYYYY-jMM-jDD').format('jYYYY-jMM-jDD')} />
|
||||
<Td text={NumberFormat(+item.value)} />
|
||||
<Td text={t('')}>
|
||||
<ToggleStatusDiscount
|
||||
defaultActive={item.isActive}
|
||||
id={item.id}
|
||||
/>
|
||||
</Td>
|
||||
<Td text={t('')}>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Link
|
||||
to={Pages.discount.detail + item.id}
|
||||
>
|
||||
<Edit size={20} color='#888888' />
|
||||
</Link>
|
||||
|
||||
<Trash
|
||||
size={20}
|
||||
color='#888888'
|
||||
onClick={() => handleDelete(item.id)}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DiscountList
|
||||
@@ -1,301 +0,0 @@
|
||||
import { FC, Fragment, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../components/Input'
|
||||
import Button from '../../components/Button'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import RadioGroup from '../../components/RadioGroup'
|
||||
import { useFormik } from 'formik'
|
||||
import { CreateDiscountType, SubscriptionPlanType } from './types/DiscountTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { useGetAllServices } from '../service/hooks/useServiceData'
|
||||
import { ServiceItemType } from '../service/types/ServiceTypes'
|
||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useGetDiscountDetail, useUpdateDiscount } from './hooks/useDiscountData'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
const UpdateDiscount: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const [serviceId, setServiceId] = useState<string[]>([])
|
||||
// const [planId, setPlanId] = useState<string>('')
|
||||
// const [customerId, setCustomerId] = useState('')
|
||||
const getServices = useGetAllServices('', 1, '', '', undefined, 50)
|
||||
// const getPlans = useGetPlans(serviceId)
|
||||
// const getCustomers = useGetCustomers()
|
||||
const getDiscountDetail = useGetDiscountDetail(id || '')
|
||||
const updateDiscount = useUpdateDiscount()
|
||||
|
||||
const formik = useFormik<CreateDiscountType>({
|
||||
initialValues: {
|
||||
endDate: '',
|
||||
startDate: '',
|
||||
title: '',
|
||||
type: 1,
|
||||
targetServices: [],
|
||||
direct: true,
|
||||
value: 0,
|
||||
isActive: true,
|
||||
userPhone: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
value: Yup.number().required(t('errors.required')),
|
||||
endDate: Yup.string().required(t('errors.required')),
|
||||
startDate: Yup.string().required(t('errors.required')),
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
type: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit(values) {
|
||||
if (values.userPhone === '') {
|
||||
values.userPhone = undefined
|
||||
}
|
||||
values.targetServices = serviceId
|
||||
const params: CreateDiscountType = {
|
||||
...values,
|
||||
startDate: moment(values.startDate, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'),
|
||||
endDate: moment(values.endDate, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'),
|
||||
value: +values.value,
|
||||
type: +values.type
|
||||
}
|
||||
updateDiscount.mutate({ id: id || '', params }, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
navigate(Pages.discount.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (getDiscountDetail.data?.data?.discount) {
|
||||
formik.setValues({
|
||||
...getDiscountDetail.data?.data?.discount,
|
||||
startDate: moment(getDiscountDetail.data?.data?.discount.startDate, 'YYYY-MM-DD').format('jYYYY-jMM-jDD'),
|
||||
endDate: moment(getDiscountDetail.data?.data?.discount.endDate, 'YYYY-MM-DD').format('jYYYY-jMM-jDD'),
|
||||
direct: getDiscountDetail.data?.data?.discount.applicationType === 'DIRECT',
|
||||
})
|
||||
|
||||
if (getDiscountDetail.data?.data?.discount.user) {
|
||||
formik.setFieldValue('userPhone', getDiscountDetail.data?.data?.discount.user?.phone)
|
||||
}
|
||||
|
||||
// delete getDiscountDetail.data?.data?.discount.subscriptionPlans
|
||||
|
||||
const ids = getDiscountDetail.data?.data?.discount.subscriptionPlans.map((item: SubscriptionPlanType) => item?.service?.id)
|
||||
// Create an array of unique service IDs by removing duplicates
|
||||
const uniqueIds = [...new Set(ids)];
|
||||
setServiceId(uniqueIds as string[]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getDiscountDetail.data])
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('discount.update_discount')}
|
||||
</div>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateDiscount.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
{t('discount.submit')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||
<div className='flex-1 min-h-[calc(100vh-201px)] bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<div className='mt-6'>
|
||||
{t('discount.type')}
|
||||
<div className='rowTwoInput mt-2'>
|
||||
<RadioGroup
|
||||
items={[
|
||||
{
|
||||
label: t('discount.direct_discount'),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: t('discount.discount_code'),
|
||||
value: false
|
||||
}
|
||||
]}
|
||||
onChange={(value) => formik.setFieldValue('direct', value)}
|
||||
selected={formik.values.direct}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<Input
|
||||
label={t('discount.title')}
|
||||
placeholder={t('discount.enter_discount_description')}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
{/* <div className='w-full sm:mt-7 flex flex-col xl:flex-row items-center'>
|
||||
<button className='text-xs border h-[39px] xl:h-full w-full xl:w-[142px] border-black rounded-xl flex justify-center items-center gap-2'>
|
||||
<Refresh
|
||||
size={18} color='black'
|
||||
/>
|
||||
<p>
|
||||
{t('discount.auto_generate_code')}
|
||||
</p>
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<div className='w-full'>
|
||||
{t('discount.price')}
|
||||
<div className='rowTwoInput mt-2'>
|
||||
<RadioGroup
|
||||
items={[
|
||||
{
|
||||
label: t('discount.fixed_amount'),
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
label: t('discount.percent'),
|
||||
value: '0'
|
||||
}
|
||||
]}
|
||||
onChange={(value) => formik.setFieldValue('type', value)}
|
||||
selected={formik.values.type.toString()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
label={t('discount.price_or_percent')}
|
||||
placeholder={t('discount.enter_price_or_percent')}
|
||||
{...formik.getFieldProps('value')}
|
||||
error_text={formik.touched.value && formik.errors.value ? formik.errors.value : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<DatePickerComponent
|
||||
label={t('discount.startDate')}
|
||||
onChange={(value) => formik.setFieldValue('startDate', value)}
|
||||
placeholder=''
|
||||
error_text={formik.touched.startDate && formik.errors.startDate ? formik.errors.startDate : ''}
|
||||
defaulValue={formik.values.startDate}
|
||||
/>
|
||||
<DatePickerComponent
|
||||
label={t('discount.endDate')}
|
||||
onChange={(value) => formik.setFieldValue('endDate', value)}
|
||||
placeholder=''
|
||||
error_text={formik.touched.endDate && formik.errors.endDate ? formik.errors.endDate : ''}
|
||||
defaulValue={formik.values.endDate}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
!formik.values.direct &&
|
||||
<div className='mt-6 rowTwoInput'>
|
||||
<Input
|
||||
label='شماره مشتری (اختیاری)'
|
||||
placeholder='شماره مشتری را وارد کنید'
|
||||
{...formik.getFieldProps('userPhone')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className='min-h-[calc(100vh-201px)] bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||
<div className='text-sm flex items-center justify-between'>
|
||||
<p>
|
||||
{'وضعیت تخفیف'}
|
||||
</p>
|
||||
<div className='flex gap-1 text-xs items-center text-description'>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
formik.values.direct &&
|
||||
<Fragment>
|
||||
<p className='mt-10 text-sm'>{t('discount.availabe_service')}</p>
|
||||
<div className='p-2 text-sm border border-[##D0D0D0] rounded-lg mt-2'>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('header.search')}
|
||||
/>
|
||||
{
|
||||
getServices.data?.data?.services?.map((item: ServiceItemType) => (
|
||||
<div key={item.id} className='flex gap-2 mt-3 py-2 items-center'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={serviceId.includes(item.id)}
|
||||
onChange={(e) => setServiceId(e.target.checked ? [...serviceId, item.id] : serviceId.filter(id => id !== item.id))}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs leading-5'>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
{/* {
|
||||
getPlans.data &&
|
||||
<div className='p-2 text-sm border border-[##D0D0D0] rounded-lg mt-2'>
|
||||
{
|
||||
getPlans.data?.data?.subscriptions?.map((item: PlanItemType) => (
|
||||
<div key={item.id} className='flex gap-2 mt-3 py-2 items-center'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={planId === item.id}
|
||||
onChange={(e) => setPlanId(e.target.checked ? item.id : '')}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs leading-5'>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
planId && getCustomers.data &&
|
||||
<div className='p-2 text-sm border border-[##D0D0D0] rounded-lg mt-2'>
|
||||
{
|
||||
getCustomers.data?.data?.customers?.map((item: CustomerItemType) => (
|
||||
<div key={item.id} className='flex gap-2 mt-3 py-2 items-center'>
|
||||
<div>
|
||||
<CheckBoxComponent
|
||||
checked={customerId === item.id}
|
||||
onChange={(e) => setCustomerId(e.target.checked ? item.id : '')}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs leading-5'>
|
||||
{item.firstName + ' ' + item.lastName}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default UpdateDiscount
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Image, DocumentUpload } from 'iconsax-react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
|
||||
|
||||
|
||||
const ImageUploader: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFile(acceptedFiles[0])
|
||||
}, [])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
return(
|
||||
<>
|
||||
<p className='mt-6 text-sm'>{t('ads.upload_picture')}</p>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className='py-12 border border-dashed border-[#8C90A3] rounded-lg mt-2 flex items-center justify-center'>
|
||||
<div className='flex flex-col items-center space-y-4'>
|
||||
<Image size={45} color='#8C90A3' variant="Bold" className='border-[3px] border-[#8C90A3] rounded-md'
|
||||
/>
|
||||
<p className='text-sm text-[#8C90A3]'>
|
||||
{
|
||||
file ?
|
||||
file.name
|
||||
:
|
||||
t('ads.to_upload_message')
|
||||
|
||||
}
|
||||
</p>
|
||||
<div className='flex gap-1'>
|
||||
<DocumentUpload
|
||||
size={20}
|
||||
color='black'
|
||||
/>
|
||||
<button className='text-sm'>
|
||||
<input {...getInputProps()} />
|
||||
{t('ads.upload')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ImageUploader
|
||||
@@ -1,28 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import SwitchComponent from '../../../components/Switch'
|
||||
import { useToggleStatusDiscount } from '../hooks/useDiscountData'
|
||||
|
||||
type Props = {
|
||||
defaultActive: boolean,
|
||||
id: string
|
||||
}
|
||||
|
||||
const ToggleStatusDiscount: FC<Props> = (props: Props) => {
|
||||
|
||||
const [isActive, setIsActive] = useState<boolean>(props.defaultActive)
|
||||
const toggleStatus = useToggleStatusDiscount()
|
||||
|
||||
const handleChange = (value: boolean) => {
|
||||
setIsActive(value)
|
||||
toggleStatus.mutate(props.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<SwitchComponent
|
||||
active={isActive}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToggleStatusDiscount
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/DiscountService";
|
||||
import { CreateDiscountType } from "../types/DiscountTypes";
|
||||
|
||||
export const useCreateDiscount = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateDiscountType) =>
|
||||
api.createDiscount(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetDiscounts = (search: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["discounts", search],
|
||||
queryFn: () => api.getDiscounts(search),
|
||||
});
|
||||
};
|
||||
|
||||
export const useToggleStatusDiscount = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: string) => api.toggleStatusDiscount(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetDiscountDetail = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["discount", id],
|
||||
queryFn: () => api.getDiscountDetail(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateDiscount = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: { id: string; params: CreateDiscountType }) =>
|
||||
api.updateDiscount(variables.id, variables.params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteDiscount = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteDiscount(id),
|
||||
});
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateDiscountType } from "../types/DiscountTypes";
|
||||
|
||||
export const createDiscount = async (params: CreateDiscountType) => {
|
||||
const { data } = await axios.post(`/discounts`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getDiscounts = async (search: string) => {
|
||||
const { data } = await axios.get(`/discounts?q=${search}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const toggleStatusDiscount = async (id: string) => {
|
||||
const { data } = await axios.post(`/discounts/${id}/toggle-status`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getDiscountDetail = async (id: string) => {
|
||||
const { data } = await axios.get(`/discounts/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateDiscount = async (
|
||||
id: string,
|
||||
params: CreateDiscountType
|
||||
) => {
|
||||
const { data } = await axios.patch(`/discounts/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteDiscount = async (id: string) => {
|
||||
const { data } = await axios.delete(`/discounts/${id}`);
|
||||
return data;
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
export type CreateDiscountType = {
|
||||
title: string;
|
||||
type: number;
|
||||
direct: boolean;
|
||||
value: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isActive: boolean;
|
||||
targetServices: string[];
|
||||
userPhone?: string;
|
||||
};
|
||||
|
||||
export enum DiscountType {
|
||||
DIRECT = "DIRECT",
|
||||
CODE = "CODE",
|
||||
}
|
||||
|
||||
export enum DiscountCalculationType {
|
||||
FIXED = "FIXED",
|
||||
PERCENTAGE = "PERCENTAGE",
|
||||
}
|
||||
|
||||
export type DiscountItemType = {
|
||||
id: string;
|
||||
title: string;
|
||||
type: DiscountType;
|
||||
calculationType: DiscountCalculationType;
|
||||
value: string | number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
subscriptionPlans: { id: string; createdAt: string; name: string }[];
|
||||
userIds: string[];
|
||||
code: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isActive: boolean;
|
||||
users: { firstName: string; lastName: string }[];
|
||||
user: { firstName: string; lastName: string; phone: string };
|
||||
};
|
||||
|
||||
export type SubscriptionPlanType = {
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
duration: number;
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
originalPrice: number;
|
||||
price: number;
|
||||
service: { id: string; createdAt: string };
|
||||
author: string;
|
||||
};
|
||||
+37
-53
@@ -1,69 +1,53 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Element3, Messages3, NotificationStatus, People, Receipt2 } from 'iconsax-react'
|
||||
import ItemDashboard from './components/ItemDashboard'
|
||||
import { useGetDashboard } from './hooks/useHomeData'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useWorkspaces } from './hooks/useHomeData'
|
||||
import { workspaceItem } from './types/HomeTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
const Home: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const getDashboard = useGetDashboard()
|
||||
const getWorkspaces = useWorkspaces()
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedWorkspace(localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID))
|
||||
}, [])
|
||||
|
||||
const handleChangeWorkspace = (id: string) => {
|
||||
localStorage.setItem(import.meta.env.VITE_WORKSPACE_ID, id)
|
||||
setSelectedWorkspace(id)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full flex gap-6'>
|
||||
<div className='flex-1'>
|
||||
|
||||
<div className='mt-8 flex-wrap text-sm flex gap-6 items-center'>
|
||||
<ItemDashboard
|
||||
title={t('home.services')}
|
||||
icon={<Element3 size={20} color='black' />}
|
||||
color='#00D16C'
|
||||
count={getDashboard.data?.data?.danakServicesCount}
|
||||
description={t('home.service')}
|
||||
to={Pages.services.list}
|
||||
/>
|
||||
{
|
||||
getWorkspaces.data?.data?.workspaces?.map((item: workspaceItem) => {
|
||||
return (
|
||||
<div className={`p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl cursor-pointer transition-all duration-300 hover:shadow-lg ${selectedWorkspace === item.id ? 'border border-black' : ''}`}
|
||||
onClick={() => handleChangeWorkspace(item.id)}
|
||||
>
|
||||
<div className='mt-5'>
|
||||
{item.businessName}
|
||||
</div>
|
||||
|
||||
<ItemDashboard
|
||||
title={t('home.customers')}
|
||||
icon={<People size={20} color='black' />}
|
||||
color='#00D16C'
|
||||
count={getDashboard.data?.data?.customersCount}
|
||||
description={t('home.customer')}
|
||||
to={Pages.customers.list}
|
||||
/>
|
||||
<div className='mt-5'>
|
||||
{item.plan?.name}
|
||||
</div>
|
||||
|
||||
<ItemDashboard
|
||||
title={t('home.ticket')}
|
||||
icon={<Messages3 size={20} color='black' />}
|
||||
color='#FF7B00'
|
||||
count={getDashboard.data?.data?.unreadTickets}
|
||||
description={t('home.unread_messages')}
|
||||
to={Pages.ticket.list}
|
||||
/>
|
||||
|
||||
<ItemDashboard
|
||||
title={t('home.ads')}
|
||||
icon={<NotificationStatus size={20} color='black' />}
|
||||
color='#FF0000'
|
||||
count={getDashboard.data?.data?.adsCount}
|
||||
description={t('home.active_ads')}
|
||||
to={Pages.ads.list}
|
||||
/>
|
||||
|
||||
<ItemDashboard
|
||||
title={t('home.invoices')}
|
||||
icon={<Receipt2 size={20} color='black' />}
|
||||
color='#0047FF'
|
||||
count={getDashboard.data?.data?.invoicesCount}
|
||||
description={t('home.new_invoice')}
|
||||
to={Pages.receipts.index}
|
||||
/>
|
||||
<div className='mt-5 flex gap-1'>
|
||||
<div>تاریخ پایان: </div>
|
||||
<div className='text-description text-sm'>{moment(item?.endDate).format('jYYYY/jMM/jDD')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
<div className='px-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px]'></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/HomeService";
|
||||
|
||||
export const useGetDashboard = () => {
|
||||
export const useWorkspaces = () => {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard"],
|
||||
queryFn: api.getDashboard,
|
||||
queryKey: ["workspaces"],
|
||||
queryFn: api.getWorkspaces,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import axios from "../../../config/axios";
|
||||
import danakAxios from "../../../config/axiosDanak";
|
||||
|
||||
export const getDashboard = async () => {
|
||||
const { data } = await axios.get(`/dashboards/reports`);
|
||||
export const getWorkspaces = async () => {
|
||||
const { data } = await danakAxios.get(
|
||||
`/subscriptions/workspaces/${import.meta.env.VITE_SERVICE_ID}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { UserItemType } from "../../users/types/UserTypes";
|
||||
|
||||
export type workspaceItem = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
plan: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
duration: number;
|
||||
price: number;
|
||||
originalPrice: number;
|
||||
isActive: boolean;
|
||||
service: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
isDanakSuggest: boolean;
|
||||
description: string;
|
||||
author: string;
|
||||
createDate: string;
|
||||
userCount: number;
|
||||
serviceLanguage: string;
|
||||
softwareLanguage: string;
|
||||
metaDescription: string;
|
||||
isActive: boolean;
|
||||
showInSlider: boolean;
|
||||
link: string;
|
||||
icon: string;
|
||||
coverUrl: string;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
deletedAt: string | null;
|
||||
};
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
businessName: string;
|
||||
businessPhone: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
status: string;
|
||||
staff: UserItemType[];
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Td from '../../components/Td'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Pagination from '../../components/Pagination'
|
||||
import { useGetCategory } from './hooks/useLearningData'
|
||||
import { CategoryLearningType } from './types/LearningTypes'
|
||||
import CreateCategory from './components/CreateCategory'
|
||||
|
||||
|
||||
const LearningCategory: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const getCategory = useGetCategory()
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div>
|
||||
{t('service.cateory_services')}
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 mt-8'>
|
||||
<div className='flex-1'>
|
||||
|
||||
<div className='flex-1 bg-white py-2 xl:px-10 px-5 rounded-3xl'>
|
||||
{
|
||||
getCategory.isPending ?
|
||||
<div className='mt-4'>
|
||||
<PageLoading />
|
||||
</div>
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('service.title')} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getCategory.data?.data?.categories?.map((item: CategoryLearningType) => {
|
||||
return (
|
||||
<tr key={item.id} className='tr'>
|
||||
<Td text={item.name} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className='flex justify-end pb-3'>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
totalPages={getCategory.data?.data?.pager?.totalPages}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateCategory />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LearningCategory
|
||||
@@ -1,167 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../components/Button'
|
||||
import { useFormik } from 'formik'
|
||||
import { CategoryLearningType, CreateLearningType } from './types/LearningTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { useCreateLearning, useGetCategory } from './hooks/useLearningData'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import Input from '../../components/Input'
|
||||
import Select from '../../components/Select'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import CreateLearningSidebar from './components/CreateLearningSidebar'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const CreateLearning: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const singleUpload = useSingleUpload()
|
||||
const createLearning = useCreateLearning()
|
||||
const getCategory = useGetCategory()
|
||||
const [videoFile, setVideoFile] = useState<File>()
|
||||
const [coverFile, setCoverFile] = useState<File>()
|
||||
|
||||
const formik = useFormik<CreateLearningType>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
coverUrl: '',
|
||||
videoUrl: '',
|
||||
videoDuration: '',
|
||||
categoryId: ''
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
description: Yup.string().required(t('errors.required')),
|
||||
videoDuration: Yup.string().required(t('errors.required')),
|
||||
categoryId: Yup.string().required(t('errors.required'))
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (videoFile && coverFile) {
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', videoFile)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.videoUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
|
||||
const formDataCover = new FormData()
|
||||
formDataCover.append('file', coverFile)
|
||||
await singleUpload.mutateAsync(formDataCover, {
|
||||
onSuccess: (data) => {
|
||||
values.coverUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
|
||||
createLearning.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
navigate(Pages.learning.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error(t('learning.error_video_cover_required'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='w-full mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
{t('learning.new_learning')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-5'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createLearning.isPending || singleUpload.isPending}
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<TickCircle
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('learning.submit_learning')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
getCategory.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='flex gap-6'>
|
||||
<div className='flex-1'>
|
||||
<div className='flex-1 mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
|
||||
<Input
|
||||
label={t('learning.title')}
|
||||
name='title'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
|
||||
<div className='rowTwoInput mt-5'>
|
||||
<Select
|
||||
items={getCategory.data?.data?.categories?.map((item: CategoryLearningType) => ({ label: item.name, value: item.id.toString() }))}
|
||||
label={t('learning.category')}
|
||||
placeholder={t('select')}
|
||||
name='categoryId'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.categoryId && formik.errors.categoryId ? formik.errors.categoryId : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name='videoDuration'
|
||||
label={t('learning.video_duration')}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.videoDuration && formik.errors.videoDuration ? formik.errors.videoDuration : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<div>{t('learning.description')}</div>
|
||||
<div>
|
||||
<Textarea
|
||||
name='description'
|
||||
placeholder={t('learning.description')}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateLearningSidebar
|
||||
onChangeCoverFile={setCoverFile}
|
||||
onChangeVideoFile={setVideoFile}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateLearning
|
||||
@@ -1,79 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import Button from '../../components/Button'
|
||||
import { Add } from 'iconsax-react'
|
||||
import { useGetLearning } from './hooks/useLearningData'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Td from '../../components/Td'
|
||||
import { LearningItemType } from './types/LearningTypes'
|
||||
|
||||
const LearningList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const getLearning = useGetLearning()
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
{t('learning.list_learning')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.learning.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('learning.submit_learning')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
getLearning.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text='' />
|
||||
<Td text={t('learning.title')} />
|
||||
<Td text={t('learning.category')} />
|
||||
<Td text={t('learning.video_duration')} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getLearning.data?.data?.learnings?.map((item: LearningItemType) => {
|
||||
return (
|
||||
<tr key={item.id} className='tr'>
|
||||
<Td text={''}>
|
||||
<img src={item.coverUrl} className='w-20' />
|
||||
</Td>
|
||||
<Td text={item.title} />
|
||||
<Td text={item.category?.name} />
|
||||
<Td text={item.videoDuration} />
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LearningList
|
||||
@@ -1,81 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../../components/Input'
|
||||
import Button from '../../../components/Button'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import * as Yup from 'yup'
|
||||
import { useFormik } from 'formik'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { CreateCategoryLearningType } from '../types/LearningTypes'
|
||||
import { useCreateCategory, useGetCategory } from '../hooks/useLearningData'
|
||||
|
||||
const CreateCategory: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const createCategory = useCreateCategory()
|
||||
const getCategory = useGetCategory()
|
||||
|
||||
const formik = useFormik<CreateCategoryLearningType>({
|
||||
initialValues: {
|
||||
name: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
|
||||
createCategory.mutate(values, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
getCategory.refetch()
|
||||
toast.success(t('success'))
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='bg-white p-8 w-sidebar hidden xl:block rounded-3xl overflow-hidden relative'>
|
||||
<div>
|
||||
{t('service.add_category')}
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-8'>
|
||||
<Input
|
||||
label={t('service.title_category')}
|
||||
value={formik.values.name}
|
||||
name='name'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className='flex flex-1 justify-end items-end mt-8'>
|
||||
<Button
|
||||
className='w-fit px-5'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createCategory.isPending}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<TickCircle
|
||||
className='size-5'
|
||||
color='white'
|
||||
/>
|
||||
<div>{t('save')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateCategory
|
||||
@@ -1,40 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import UploadBoxDraggble from '../../../components/UploadBoxDraggble'
|
||||
|
||||
type Props = {
|
||||
onChangeVideoFile: (file: File) => void,
|
||||
onChangeCoverFile: (file: File) => void
|
||||
}
|
||||
|
||||
const CreateLearningSidebar: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
return (
|
||||
<div className='bg-white mt-10 w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
|
||||
|
||||
<div className='mt-8'>
|
||||
<div>{t('learning.learning_cover')}</div>
|
||||
<div className='mt-2'>
|
||||
<UploadBoxDraggble
|
||||
label={t('learning.upload_cover')}
|
||||
onChange={(file: File[]) => props.onChangeCoverFile(file[0])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-8'>
|
||||
<div>{t('learning.video')}</div>
|
||||
<div className='mt-2'>
|
||||
<UploadBoxDraggble
|
||||
label={t('learning.upload_video')}
|
||||
onChange={(file: File[]) => props.onChangeVideoFile(file[0])}
|
||||
isFile
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateLearningSidebar
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/LearningService";
|
||||
import {
|
||||
CreateCategoryLearningType,
|
||||
CreateLearningType,
|
||||
} from "../types/LearningTypes";
|
||||
|
||||
export const useGetCategory = () => {
|
||||
return useQuery({
|
||||
queryKey: ["learning-category"],
|
||||
queryFn: api.getCategory,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCategory = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateCategoryLearningType) =>
|
||||
api.createCategory(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateLearning = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateLearningType) =>
|
||||
api.CreateLearning(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetLearning = () => {
|
||||
return useQuery({
|
||||
queryKey: ["learnings"],
|
||||
queryFn: api.getLearnings,
|
||||
});
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import axios from "../../../config/axios";
|
||||
import {
|
||||
CreateCategoryLearningType,
|
||||
CreateLearningType,
|
||||
} from "../types/LearningTypes";
|
||||
|
||||
export const getCategory = async () => {
|
||||
const { data } = await axios.get(`/learnings/category`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createCategory = async (params: CreateCategoryLearningType) => {
|
||||
const { data } = await axios.post(`/learnings/category`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const CreateLearning = async (params: CreateLearningType) => {
|
||||
const { data } = await axios.post(`/learnings`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getLearnings = async () => {
|
||||
const { data } = await axios.get(`/learnings`);
|
||||
return data;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
export type CreateCategoryLearningType = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CategoryLearningType = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CreateLearningType = {
|
||||
title: string;
|
||||
description: string;
|
||||
coverUrl: string;
|
||||
videoUrl: string;
|
||||
videoDuration: string;
|
||||
categoryId: string;
|
||||
};
|
||||
|
||||
export type LearningItemType = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
coverUrl: string;
|
||||
videoUrl: string;
|
||||
videoDuration: string;
|
||||
category: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -15,7 +15,7 @@ import { toast } from 'react-toastify';
|
||||
// import { useGetDashboardSummary } from '../home/hooks/useHomeData';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Pages } from '../../config/Pages';
|
||||
import { useGetDashboard } from '../home/hooks/useHomeData';
|
||||
// import { useGetDashboard } from '../home/hooks/useHomeData';
|
||||
|
||||
const Notifications: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
@@ -26,12 +26,12 @@ const Notifications: FC = () => {
|
||||
const { data, fetchNextPage, hasNextPage, refetch } = useGetNotification(activeTab);
|
||||
const readAll = useReadAll()
|
||||
const posts = data?.pages.flatMap((page) => page.data?.notifications) || [];
|
||||
const getDashboard = useGetDashboard()
|
||||
// const getDashboard = useGetDashboard()
|
||||
|
||||
const handleAllRead = () => {
|
||||
readAll.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
getDashboard.refetch()
|
||||
// getDashboard.refetch()
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from "../../../config/axios";
|
||||
import axios from "../../../config/axiosDanak";
|
||||
|
||||
export const getNotifications = async (
|
||||
page: number,
|
||||
|
||||
@@ -16,14 +16,12 @@ import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import Email from './components/Email'
|
||||
import Phone from './components/Phone'
|
||||
import { useGetProvines } from '../customer/hooks/useCustomerData'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
|
||||
const Profile: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const getProvines = useGetProvines()
|
||||
const getProfile = useGetProfile()
|
||||
const singleUpload = useSingleUpload()
|
||||
const updateProfile = useUpdateProfile()
|
||||
@@ -107,7 +105,7 @@ const Profile: FC = () => {
|
||||
</div>
|
||||
|
||||
{
|
||||
getProfile.isPending || getProvines.isPending ?
|
||||
getProfile.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from "../../../config/axios";
|
||||
import axiosDanak from "../../../config/axiosDanak";
|
||||
import {
|
||||
CheckUserNameType,
|
||||
UpdateEmailType,
|
||||
@@ -8,7 +9,7 @@ import {
|
||||
} from "../types/ProfileTypes";
|
||||
|
||||
export const getProfile = async () => {
|
||||
const { data } = await axios.get(`/users/me`);
|
||||
const { data } = await axiosDanak.get(`/users/me`);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ import { CreateReceiptType, ReceiptCreateItemsType } from './types/ReceiptTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useGetCustomers } from '../customer/hooks/useCustomerData'
|
||||
import { CustomerItemType } from '../customer/types/CustomerTypes'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { useCreateInvoice } from './hooks/useReceiptData'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -17,16 +15,19 @@ import { Pages } from '../../config/Pages'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { ReceiptTypeEnum } from './enum/ReceipEnum'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { useGetCompanies } from '../company/hooks/useCompanyData'
|
||||
import { CompanyItemType } from '../company/types/CompanyTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
const CreateReceipt: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const [items, setItems] = useState<ReceiptCreateItemsType[]>([])
|
||||
const [customer, setCustomer] = useState<CustomerItemType>()
|
||||
const [customer, setCustomer] = useState<CompanyItemType>()
|
||||
const [isRecurring, setIsRecurring] = useState<boolean>(false)
|
||||
const [type, setType] = useState<ReceiptTypeEnum>(ReceiptTypeEnum.DAILY)
|
||||
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0)
|
||||
const getCustomers = useGetCustomers()
|
||||
const getCustomers = useGetCompanies(1)
|
||||
const createInvoice = useCreateInvoice()
|
||||
|
||||
const formik = useFormik<ReceiptCreateItemsType>({
|
||||
@@ -54,14 +55,14 @@ const CreateReceipt: FC = () => {
|
||||
|
||||
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value
|
||||
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value)
|
||||
const customer = getCustomers.data?.data?.companies?.find((item: CompanyItemType) => item.id === value)
|
||||
setCustomer(customer)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (customer) {
|
||||
const params: CreateReceiptType = {
|
||||
userId: customer?.id,
|
||||
companyId: customer?.id,
|
||||
items: items,
|
||||
isRecurring: isRecurring,
|
||||
recurringPeriod: isRecurring ? +type : undefined,
|
||||
@@ -114,18 +115,18 @@ const CreateReceipt: FC = () => {
|
||||
|
||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<div>
|
||||
{/* <div>
|
||||
{t('receip.customer_information')}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className='mt-8'>
|
||||
<div className='w-full xl:w-1/2'>
|
||||
<Select
|
||||
label={t('receip.customer')}
|
||||
label={t('company.select_company')}
|
||||
placeholder={t('select')}
|
||||
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||
items={getCustomers.data?.data?.companies?.map((item: CompanyItemType) => {
|
||||
return {
|
||||
label: item.firstName + ' ' + item.lastName,
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}
|
||||
})}
|
||||
@@ -139,37 +140,25 @@ const CreateReceipt: FC = () => {
|
||||
<div className='mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm'>
|
||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-7'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-description'>{t('receip.type_person')}</div>
|
||||
<div>{customer.legalUser ? 'حقوقی' : 'حقیقی'}</div>
|
||||
<div className='text-description'>{t('company.company_name')}</div>
|
||||
<div>{customer.name}</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-description'>{t('receip.representativeـname')}</div>
|
||||
<div>{customer.firstName + ' ' + customer.lastName}</div>
|
||||
<div className='text-description'>{t('company.ceo_name')}</div>
|
||||
<div>{customer.user?.firstName + ' ' + customer.user?.lastName}</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-description'>{t('receip.company_name')}</div>
|
||||
<div>{customer?.legalUser?.registrationName}</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-description'>{t('receip.registration_number')}</div>
|
||||
<div>{customer?.legalUser?.registrationCode}</div>
|
||||
<div className='text-description'>{t('company.register_date')}</div>
|
||||
<div>{moment(customer.createdAt).format('jYYYY/jMM/jDD')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between items-center mt-7'>
|
||||
<div className='flex items-center gap-2 flex-1'>
|
||||
<div className='text-description'>{t('receip.tel_company')}</div>
|
||||
<div>{customer?.realUser?.phone || customer?.legalUser?.phone}</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 flex-1'>
|
||||
<div className='text-description'>{t('receip.national_code')}</div>
|
||||
<div>{customer.nationalCode}</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 flex-1'>
|
||||
<div className='text-description'>{t('receip.postal_code')}</div>
|
||||
<div>{customer?.realUser?.address?.postalCode || customer?.legalUser?.address?.postalCode}</div>
|
||||
<div className='text-description'>{t('company.industry')}</div>
|
||||
<div>{customer.industry?.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between items-center mt-7'>
|
||||
{/* <div className='flex justify-between items-center mt-7'>
|
||||
<div className='flex items-center gap-2 flex-1'>
|
||||
<div className='text-description'>{t('receip.state')}</div>
|
||||
<div>{customer?.realUser?.address?.city?.province?.name || customer?.legalUser?.address?.city?.province?.name}</div>
|
||||
@@ -182,7 +171,7 @@ const CreateReceipt: FC = () => {
|
||||
<div className='text-description'>{t('receip.address')}</div>
|
||||
<div>{customer?.realUser?.address?.fullAddress || customer?.legalUser?.address?.fullAddress}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import { CloseCircle, FolderOpen, WalletCheck, WalletRemove, WalletSearch } from
|
||||
import Td from '../../components/Td'
|
||||
import { useGetInvoices } from './hooks/useReceiptData'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { InvoiceUserItemType, ReceiptItemType } from './types/ReceiptTypes'
|
||||
import { ReceiptItemType } from './types/ReceiptTypes'
|
||||
import { NumberFormat } from '../../config/func'
|
||||
import Select from '../../components/Select'
|
||||
import Input from '../../components/Input'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import moment from 'moment-jalaali'
|
||||
import { CompanyItemType } from '../company/types/CompanyTypes'
|
||||
|
||||
type ActiveTabType = "PENDING" | "WAIT_PAYMENT" | "PAID" | "CANCELLED" | "EXPIRED" | ""
|
||||
|
||||
@@ -44,11 +45,11 @@ const ReceiptsList: FC = () => {
|
||||
<div className='flex justify-between items-center mt-12'>
|
||||
<div className='flex gap-4'>
|
||||
<Select
|
||||
label={t('receip.customer')}
|
||||
label={t('company.company')}
|
||||
placeholder={t('select')}
|
||||
items={getInvoices.data?.data?.users?.map((item: InvoiceUserItemType) => {
|
||||
items={getInvoices.data?.data?.companies?.map((item: CompanyItemType) => {
|
||||
return {
|
||||
label: item.firstname + ' ' + item.lastname,
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}
|
||||
})}
|
||||
@@ -144,7 +145,7 @@ const ReceiptsList: FC = () => {
|
||||
return (
|
||||
<tr className='tr'>
|
||||
<Td text={String(index + 1)} />
|
||||
<Td text={item.user.firstName + ' ' + item.user.lastName} />
|
||||
<Td text={item.company.name} />
|
||||
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||
<Td text={moment(item.dueDate).format('jYYYY/jMM/jDD')} />
|
||||
<Td text={NumberFormat(item.totalPrice)} />
|
||||
|
||||
@@ -10,7 +10,7 @@ export const getInvoces = async (
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
if (status) params.append("status", status);
|
||||
if (customerId) params.append("userId", customerId);
|
||||
if (customerId) params.append("companyId", customerId);
|
||||
if (search) params.append("q", search);
|
||||
if (date) params.append("since", date);
|
||||
if (endDate) params.append("to", endDate);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type CreateReceiptType = {
|
||||
userId: string;
|
||||
companyId: string;
|
||||
items: ReceiptCreateItemsType[];
|
||||
isRecurring: boolean;
|
||||
recurringPeriod: number | undefined;
|
||||
@@ -24,9 +24,8 @@ export type ReceiptItemType = {
|
||||
totalPrice: number;
|
||||
subscriptionPlan?: string | null;
|
||||
items: ReceiptItemType[];
|
||||
user: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
company: {
|
||||
name: string;
|
||||
};
|
||||
dueDate: string;
|
||||
status: "PENDING" | "PAID" | "CANCELLED" | "EXPIRED" | "APPROVED";
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import { useFormik } from 'formik'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreateSliderType } from './types/SliderTypes'
|
||||
import * as Yup from 'yup'
|
||||
import Button from '../../components/Button'
|
||||
import { useCreateSlider } from './hooks/useSliderData'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import UploadBox from '../../components/UploadBox'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
const CreateSlider: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const createSlider = useCreateSlider()
|
||||
const singleUpload = useSingleUpload()
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const formik = useFormik<CreateSliderType>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
imageUrl: '',
|
||||
link: '',
|
||||
order: 0,
|
||||
isActive: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
description: Yup.string().required(t('errors.required')),
|
||||
link: Yup.string().required(t('errors.required')),
|
||||
order: Yup.number().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (!file) {
|
||||
toast.error(t('slider.image_required'))
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.imageUrl = data?.data?.url
|
||||
}
|
||||
})
|
||||
|
||||
values.order = +values.order
|
||||
|
||||
createSlider.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('slider.slider_created'))
|
||||
navigate(Pages.sliders.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('slider.new_slider')}
|
||||
</div>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createSlider.isPending || singleUpload.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
{t('slider.submit')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex-1 mt-9 bg-white py-10 xl:px-10 px-5 rounded-3xl'>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='rowTwoInput'>
|
||||
<Input
|
||||
label={t('slider.title')}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('slider.link')}
|
||||
{...formik.getFieldProps('link')}
|
||||
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<Textarea
|
||||
label={t('slider.description')}
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<div className='w-full'>
|
||||
<UploadBox
|
||||
label={t('slider.image')}
|
||||
onChange={(file) => setFile(file[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label={t('slider.order')}
|
||||
{...formik.getFieldProps('order')}
|
||||
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<div className='w-full'>
|
||||
<SwitchComponent
|
||||
label={t('slider.isActive')}
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateSlider
|
||||
@@ -1,106 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Edit } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Td from '../../components/Td'
|
||||
import { useDeleteSlider, useGetSliders } from './hooks/useSliderData'
|
||||
import { SliderItemType } from './types/SliderTypes'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||
const SliderList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { data: sliders, isPending, refetch } = useGetSliders()
|
||||
const deleteSlider = useDeleteSlider()
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteSlider.mutate(id, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('slider.slider_deleted'))
|
||||
refetch()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
{t('slider.list_slider')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.sliders.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('slider.submit_slider')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('slider.image')} />
|
||||
<Td text={t('slider.title')} />
|
||||
<Td text={t('slider.description')} />
|
||||
<Td text={t('slider.order')} />
|
||||
<Td text={t('slider.isActive')} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sliders?.data?.sliders?.map((item: SliderItemType) => (
|
||||
<tr className='tr' key={item.id}>
|
||||
<Td text={''}>
|
||||
<img src={item.imageUrl} alt={item.title} className='w-14' />
|
||||
</Td>
|
||||
<Td text={item.title} />
|
||||
<Td text={item.description} />
|
||||
<Td text={item.order.toString()} />
|
||||
<Td text={item.isActive ? t('slider.active') : t('slider.inactive')} />
|
||||
<Td text={''}>
|
||||
<div className='flex gap-2'>
|
||||
<Link to={Pages.sliders.detail + item.id}>
|
||||
<Edit
|
||||
className='size-5'
|
||||
color='#888'
|
||||
/>
|
||||
</Link>
|
||||
<TrashWithConfrim
|
||||
onDelete={() => handleDelete(item.id)}
|
||||
isLoading={deleteSlider.isPending}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SliderList
|
||||
@@ -1,149 +0,0 @@
|
||||
import { useFormik } from 'formik'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreateSliderType } from './types/SliderTypes'
|
||||
import * as Yup from 'yup'
|
||||
import Button from '../../components/Button'
|
||||
import { useGetSliderDetail, useUpdateSlider } from './hooks/useSliderData'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import UploadBox from '../../components/UploadBox'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
const UpdateSlider: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const updateSlider = useUpdateSlider()
|
||||
const singleUpload = useSingleUpload()
|
||||
const { data: sliderDetail } = useGetSliderDetail(id || '')
|
||||
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const formik = useFormik<CreateSliderType>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
imageUrl: '',
|
||||
link: '',
|
||||
order: 0,
|
||||
isActive: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
description: Yup.string().required(t('errors.required')),
|
||||
link: Yup.string().required(t('errors.required')),
|
||||
order: Yup.number().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
|
||||
if (file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.imageUrl = data?.data?.url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
values.order = +values.order
|
||||
|
||||
updateSlider.mutate({ id: id || '', data: values }, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('slider.slider_created'))
|
||||
navigate(Pages.sliders.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (sliderDetail?.data?.slider) {
|
||||
formik.setValues(sliderDetail?.data?.slider)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sliderDetail])
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('slider.new_slider')}
|
||||
</div>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateSlider.isPending || singleUpload.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
{t('slider.submit')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex-1 mt-9 bg-white py-10 xl:px-10 px-5 rounded-3xl'>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='rowTwoInput'>
|
||||
<Input
|
||||
label={t('slider.title')}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('slider.link')}
|
||||
{...formik.getFieldProps('link')}
|
||||
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<Textarea
|
||||
label={t('slider.description')}
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<div className='w-full'>
|
||||
<UploadBox
|
||||
label={t('slider.image')}
|
||||
onChange={(file) => setFile(file[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label={t('slider.order')}
|
||||
{...formik.getFieldProps('order')}
|
||||
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<div className='w-full'>
|
||||
<SwitchComponent
|
||||
label={t('slider.isActive')}
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateSlider
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/SliderService";
|
||||
import { CreateSliderType } from "../types/SliderTypes";
|
||||
export const useCreateSlider = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.createSlider,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSliders = () => {
|
||||
return useQuery({
|
||||
queryKey: ["sliders"],
|
||||
queryFn: api.getSliders,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSliderDetail = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["slider-detail", id],
|
||||
queryFn: () => api.getSliderDetail(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSlider = () => {
|
||||
return useMutation({
|
||||
mutationFn: (params: { id: string; data: CreateSliderType }) =>
|
||||
api.updateSlider(params.id, params.data),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSlider = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteSlider(id),
|
||||
});
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateSliderType } from "../types/SliderTypes";
|
||||
|
||||
export const createSlider = async (params: CreateSliderType) => {
|
||||
const { data } = await axios.post("/sliders", params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getSliders = async () => {
|
||||
const { data } = await axios.get("/sliders");
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getSliderDetail = async (id: string) => {
|
||||
const { data } = await axios.get(`/sliders/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateSlider = async (id: string, params: CreateSliderType) => {
|
||||
const { data } = await axios.patch(`/sliders/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteSlider = async (id: string) => {
|
||||
const { data } = await axios.delete(`/sliders/${id}`);
|
||||
return data;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
export type CreateSliderType = {
|
||||
title: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
link: string;
|
||||
order: number;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export type SliderItemType = {
|
||||
createdAt: string;
|
||||
description: string;
|
||||
id: string;
|
||||
imageUrl: string;
|
||||
isActive: boolean;
|
||||
link: string;
|
||||
order: number;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -1,212 +0,0 @@
|
||||
import { FC, useState } from 'react'
|
||||
import Input from '../../components/Input'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import Button from '../../components/Button'
|
||||
import { SupportPlanFeatureKey } from './enum/SupportEnum'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PlanItem from './components/PlanItem'
|
||||
import { useFormik } from 'formik'
|
||||
import { CreatePlanType } from './types/SupportTypes'
|
||||
import * as Yup from 'yup'
|
||||
import Select from '../../components/Select'
|
||||
import { useCreateSupport } from './hooks/useSupportData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
interface PlanFeature {
|
||||
featureKey: string;
|
||||
featureValue: string | boolean;
|
||||
featureType: 'boolean' | 'text';
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
const CreatePlan: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const createSupport = useCreateSupport()
|
||||
const [features, setFeatures] = useState<PlanFeature[]>(
|
||||
Object.values(SupportPlanFeatureKey).map(key => ({
|
||||
featureKey: key,
|
||||
featureValue: true,
|
||||
featureType: 'boolean',
|
||||
isEnabled: true
|
||||
}))
|
||||
)
|
||||
|
||||
const handleFeatureChange = (id: string, value: string | boolean, isEnabled: boolean) => {
|
||||
setFeatures(prevFeatures =>
|
||||
prevFeatures.map(feature =>
|
||||
feature.featureKey === id ? { ...feature, featureValue: value, isEnabled } : feature
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const handleFeatureTypeChange = (id: string, type: 'boolean' | 'text') => {
|
||||
setFeatures(prevFeatures =>
|
||||
prevFeatures.map(feature =>
|
||||
feature.featureKey === id ? {
|
||||
...feature,
|
||||
featureType: type,
|
||||
featureValue: type === 'boolean' ? true : '',
|
||||
isEnabled: type === 'boolean' ? true : false
|
||||
} : feature
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const prepareFeaturesForSubmission = (features: PlanFeature[]) => {
|
||||
return features.map(feature => {
|
||||
if (feature.featureType === 'boolean') {
|
||||
return {
|
||||
featureKey: feature.featureKey,
|
||||
featureValue: feature.isEnabled,
|
||||
featureType: 'boolean',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
featureKey: feature.featureKey,
|
||||
featureValue: feature.featureValue as string,
|
||||
featureType: 'text',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formik = useFormik<CreatePlanType>({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
price: 0,
|
||||
duration: 0,
|
||||
isActive: true,
|
||||
features: [],
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required('نام پلن الزامی است'),
|
||||
description: Yup.string().required('توضیحات پلن الزامی است'),
|
||||
price: Yup.number().required('قیمت پلن الزامی است'),
|
||||
duration: Yup.number().required('مدت پلن الزامی است'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
values.price = Number(values.price)
|
||||
values.duration = Number(values.duration)
|
||||
const preparedFeatures = prepareFeaturesForSubmission(features)
|
||||
const planData: CreatePlanType = {
|
||||
...values,
|
||||
features: preparedFeatures.map(feature => ({
|
||||
featureKey: feature.featureKey,
|
||||
featureValue: String(feature.featureValue),
|
||||
featureType: feature.featureType as "text" | "boolean"
|
||||
}))
|
||||
}
|
||||
createSupport.mutate(planData, {
|
||||
onSuccess: () => {
|
||||
toast.success('پلن با موفقیت ساخته شد')
|
||||
navigate(Pages.support.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='text-lg font-semibold mb-6'>ساخت پلن</div>
|
||||
|
||||
<div className='bg-white rounded-lg p-6 shadow-sm mb-6'>
|
||||
<div className='text-base font-medium mb-4'>اطلاعات اصلی پلن</div>
|
||||
<div className='space-y-6'>
|
||||
<div className='rowTwoInput'>
|
||||
<Input
|
||||
label='نام پلن'
|
||||
placeholder='نام پلن'
|
||||
{...formik.getFieldProps('name')}
|
||||
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<Input
|
||||
label='قیمت پلن'
|
||||
placeholder='قیمت پلن'
|
||||
{...formik.getFieldProps('price')}
|
||||
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : undefined}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label='مدت پلن'
|
||||
placeholder='مدت پلن'
|
||||
{...formik.getFieldProps('duration')}
|
||||
error_text={formik.touched.duration && formik.errors.duration ? formik.errors.duration : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<Textarea
|
||||
label='توضیحات پلن'
|
||||
placeholder='توضیحات پلن'
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(active: boolean) => formik.setFieldValue('isActive', active)}
|
||||
label='فعال'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white rounded-lg p-6 shadow-sm'>
|
||||
<div className='text-base font-medium mb-4'>ویژگیهای پلن</div>
|
||||
<div className='space-y-8'>
|
||||
{features.map((feature) => (
|
||||
<div key={feature.featureKey} className='border-b border-gray-100 pb-6 last:border-0'>
|
||||
<div className='flex items-center gap-4 mb-4'>
|
||||
<div className='w-48'>
|
||||
<Select
|
||||
label='نوع مقدار'
|
||||
items={[
|
||||
{ label: 'بله/خیر', value: 'boolean' },
|
||||
{ label: 'مقدار', value: 'text' }
|
||||
]}
|
||||
value={feature.featureType}
|
||||
onChange={(e) => handleFeatureTypeChange(feature.featureKey, e.target.value as 'boolean' | 'text')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PlanItem
|
||||
id={feature.featureKey}
|
||||
label={t(`support.${feature.featureKey}`)}
|
||||
value={feature.featureValue}
|
||||
checked={feature.isEnabled}
|
||||
featureType={feature.featureType}
|
||||
onChange={handleFeatureChange}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-end'>
|
||||
<Button
|
||||
label='ثبت پلن'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
disabled={!formik.isValid || !formik.dirty}
|
||||
className='w-fit px-7'
|
||||
isLoading={createSupport.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatePlan
|
||||
@@ -1,85 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { useGetPlans } from './hooks/useSupportData'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Td from '../../components/Td'
|
||||
import { NumberFormat } from '../../config/func'
|
||||
import { Add, Edit } from 'iconsax-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import Delete from './components/Delete'
|
||||
import Button from '../../components/Button'
|
||||
const SupportList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { data, refetch } = useGetPlans()
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('support.plans')}
|
||||
</div>
|
||||
|
||||
<Link to={Pages.support.create}>
|
||||
<Button
|
||||
className='w-fit px-6'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('support.new_plan')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('support.plan_name')} />
|
||||
<Td text={t('support.price')} />
|
||||
<Td text={t('support.duration')} />
|
||||
<Td text={t('support.users')} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
data?.data?.supportPlans?.map((item: { id: string, name: string, price: number, duration: string, userCount: number }) => {
|
||||
return (
|
||||
<tr className='tr' key={item.id}>
|
||||
<Td text={item.name} />
|
||||
<Td text={NumberFormat(item.price)} />
|
||||
<Td text={item.duration} />
|
||||
<Td text={''}>
|
||||
<Link to={Pages.support.planUsers + item.id}>
|
||||
<div className='flex items-center gap-2 text-blue-500'>
|
||||
<div>
|
||||
{item.userCount}
|
||||
</div>
|
||||
کاربر
|
||||
</div>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text={''}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Link to={Pages.support.detail + item.id}>
|
||||
<Edit size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
<Delete id={item.id} refetch={refetch} />
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SupportList
|
||||
@@ -1,56 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGetPlanUsers } from './hooks/useSupportData'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import Td from '../../components/Td'
|
||||
import moment from 'moment-jalaali'
|
||||
import { PlanItemUserType } from './types/SupportTypes'
|
||||
const PlanUsers: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { id } = useParams()
|
||||
const { data } = useGetPlanUsers(id)
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div>
|
||||
{t('support.plan_users')} {data?.data?.users?.[0]?.supportPlan?.name}
|
||||
</div>
|
||||
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('support.full_name')} />
|
||||
<Td text={t('support.start_date')} />
|
||||
<Td text={t('support.end_date')} />
|
||||
<Td text={t('support.status')} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.data?.users?.map((item: PlanItemUserType) => (
|
||||
<tr className='tr' key={item.id}>
|
||||
<Td text={item.user.firstName + ' ' + item.user.lastName} />
|
||||
<Td text={''}>
|
||||
<div className='dltr text-right'>
|
||||
{moment(item.startDate).format('jYYYY/jMM/jDD HH:mm')}
|
||||
</div>
|
||||
</Td>
|
||||
<Td text={''}>
|
||||
<div className='dltr text-right'>
|
||||
{moment(item.endDate).format('jYYYY/jMM/jDD HH:mm')}
|
||||
</div>
|
||||
</Td>
|
||||
<Td text={t(`support.${item.status}`)} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlanUsers
|
||||
@@ -1,233 +0,0 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import Input from '../../components/Input'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import Button from '../../components/Button'
|
||||
import { SupportPlanFeatureKey } from './enum/SupportEnum'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PlanItem from './components/PlanItem'
|
||||
import { useFormik } from 'formik'
|
||||
import { CreatePlanType } from './types/SupportTypes'
|
||||
import * as Yup from 'yup'
|
||||
import Select from '../../components/Select'
|
||||
import { useGetPlanById, useUpdateSupport } from './hooks/useSupportData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
interface PlanFeature {
|
||||
featureKey: string;
|
||||
featureValue: string | boolean;
|
||||
featureType: 'boolean' | 'text';
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
const UpdatePlan: FC = () => {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const { data: planData } = useGetPlanById(id ?? '')
|
||||
const updateSupport = useUpdateSupport()
|
||||
|
||||
const [features, setFeatures] = useState<PlanFeature[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (planData?.data) {
|
||||
const plan = planData.data?.supportPlan
|
||||
const mappedFeatures = Object.values(SupportPlanFeatureKey).map(key => {
|
||||
const existingFeature = plan.features.find((f: { featureKey: string }) => f.featureKey === key)
|
||||
return {
|
||||
featureKey: key,
|
||||
featureValue: existingFeature?.featureValue ?? true,
|
||||
featureType: existingFeature?.featureType ?? 'boolean',
|
||||
isEnabled: true
|
||||
}
|
||||
})
|
||||
setFeatures(mappedFeatures)
|
||||
}
|
||||
}, [planData])
|
||||
|
||||
const handleFeatureChange = (id: string, value: string | boolean, isEnabled: boolean) => {
|
||||
console.log(id, value, isEnabled);
|
||||
setFeatures(prevFeatures =>
|
||||
prevFeatures.map(feature =>
|
||||
feature.featureKey === id ? { ...feature, featureValue: value, isEnabled } : feature
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const handleFeatureTypeChange = (id: string, type: 'boolean' | 'text') => {
|
||||
setFeatures(prevFeatures =>
|
||||
prevFeatures.map(feature =>
|
||||
feature.featureKey === id ? {
|
||||
...feature,
|
||||
featureType: type,
|
||||
featureValue: type === 'boolean' ? true : '',
|
||||
isEnabled: type === 'boolean' ? true : false
|
||||
} : feature
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const prepareFeaturesForSubmission = (features: PlanFeature[]) => {
|
||||
return features.map(feature => {
|
||||
if (feature.featureType === 'boolean') {
|
||||
return {
|
||||
featureKey: feature.featureKey,
|
||||
featureValue: feature.featureValue,
|
||||
featureType: 'boolean',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
featureKey: feature.featureKey,
|
||||
featureValue: feature.featureValue as string,
|
||||
featureType: 'text',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formik = useFormik<CreatePlanType>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: planData?.data?.supportPlan?.name ?? '',
|
||||
description: planData?.data?.supportPlan?.description ?? '',
|
||||
price: planData?.data?.supportPlan?.price ?? 0,
|
||||
duration: planData?.data?.supportPlan?.duration ?? 0,
|
||||
isActive: planData?.data?.supportPlan?.isActive ?? true,
|
||||
features: [],
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required('نام پلن الزامی است'),
|
||||
description: Yup.string().required('توضیحات پلن الزامی است'),
|
||||
price: Yup.number().required('قیمت پلن الزامی است'),
|
||||
duration: Yup.number().required('مدت پلن الزامی است'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
values.price = Number(values.price)
|
||||
values.duration = Number(values.duration)
|
||||
const preparedFeatures = prepareFeaturesForSubmission(features)
|
||||
const planData: CreatePlanType = {
|
||||
...values,
|
||||
features: preparedFeatures.map(feature => ({
|
||||
featureKey: feature.featureKey,
|
||||
featureValue: String(feature.featureValue),
|
||||
featureType: feature.featureType as "text" | "boolean"
|
||||
}))
|
||||
}
|
||||
updateSupport.mutate({ id: id ?? '', data: planData }, {
|
||||
onSuccess: () => {
|
||||
toast.success('پلن با موفقیت ویرایش شد')
|
||||
navigate(Pages.support.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
if (!planData?.data) {
|
||||
return null
|
||||
}
|
||||
|
||||
console.log(features);
|
||||
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='text-lg font-semibold mb-6'>ویرایش پلن</div>
|
||||
|
||||
<div className='bg-white rounded-lg p-6 shadow-sm mb-6'>
|
||||
<div className='text-base font-medium mb-4'>اطلاعات اصلی پلن</div>
|
||||
<div className='space-y-6'>
|
||||
<div className='rowTwoInput'>
|
||||
<Input
|
||||
label='نام پلن'
|
||||
placeholder='نام پلن'
|
||||
{...formik.getFieldProps('name')}
|
||||
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<Input
|
||||
label='قیمت پلن'
|
||||
placeholder='قیمت پلن'
|
||||
{...formik.getFieldProps('price')}
|
||||
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : undefined}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label='مدت پلن'
|
||||
placeholder='مدت پلن'
|
||||
{...formik.getFieldProps('duration')}
|
||||
error_text={formik.touched.duration && formik.errors.duration ? formik.errors.duration : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<Textarea
|
||||
label='توضیحات پلن'
|
||||
placeholder='توضیحات پلن'
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput'>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(active: boolean) => formik.setFieldValue('isActive', active)}
|
||||
label='فعال'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white rounded-lg p-6 shadow-sm'>
|
||||
<div className='text-base font-medium mb-4'>ویژگیهای پلن</div>
|
||||
<div className='space-y-8'>
|
||||
{features.map((feature) => (
|
||||
<div key={feature.featureKey} className='border-b border-gray-100 pb-6 last:border-0'>
|
||||
<div className='flex items-center gap-4 mb-4'>
|
||||
<div className='w-48'>
|
||||
<Select
|
||||
label='نوع مقدار'
|
||||
items={[
|
||||
{ label: 'بله/خیر', value: 'boolean' },
|
||||
{ label: 'مقدار', value: 'text' }
|
||||
]}
|
||||
value={feature.featureType}
|
||||
onChange={(e) => handleFeatureTypeChange(feature.featureKey, e.target.value as 'boolean' | 'text')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PlanItem
|
||||
id={feature.featureKey}
|
||||
label={t(`support.${feature.featureKey}`)}
|
||||
value={feature.featureValue}
|
||||
checked={feature.featureValue === "true" || feature.featureValue === true}
|
||||
featureType={feature.featureType}
|
||||
onChange={handleFeatureChange}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-end'>
|
||||
<Button
|
||||
label='ویرایش پلن'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
disabled={!formik.isValid}
|
||||
className='w-fit px-7'
|
||||
isLoading={updateSupport.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdatePlan
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Trash } from 'iconsax-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useDeletePlan } from '../hooks/useSupportData'
|
||||
import ModalConfrim from '../../../components/ModalConfrim'
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
const Delete: FC<Props> = ({ id, refetch }) => {
|
||||
const { mutate: deletePlan, isPending } = useDeletePlan()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const close = () => setIsOpen(false)
|
||||
return (
|
||||
<div>
|
||||
<Trash size={20} color='#8C90A3' onClick={() => setIsOpen(true)} />
|
||||
|
||||
<ModalConfrim
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
onConfrim={() => {
|
||||
deletePlan(id, {
|
||||
onSuccess: () => {
|
||||
setIsOpen(false)
|
||||
refetch()
|
||||
}
|
||||
})
|
||||
}}
|
||||
isLoading={isPending}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Delete
|
||||
@@ -1,50 +0,0 @@
|
||||
import { ChangeEvent, FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import CheckBoxComponent from '../../../components/CheckBoxComponent'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
label: string
|
||||
value: string | boolean
|
||||
checked: boolean
|
||||
featureType: 'boolean' | 'text'
|
||||
onChange: (id: string, value: string | boolean, checked: boolean) => void
|
||||
}
|
||||
|
||||
const PlanItem: FC<Props> = ({ id, label, value, checked, featureType, onChange }) => {
|
||||
const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newChecked = e.target.checked
|
||||
onChange(id, newChecked, newChecked)
|
||||
}
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.value, checked)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id} className='mt-4 text-sm flex gap-4 items-center'>
|
||||
<div className='flex-1 whitespace-nowrap'>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
{featureType === 'text' && (
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
placeholder='مقدار'
|
||||
value={value as string}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{featureType === 'boolean' && (
|
||||
<CheckBoxComponent
|
||||
checked={checked}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlanItem
|
||||
@@ -1,13 +0,0 @@
|
||||
export enum SupportPlanFeatureKey {
|
||||
LEARNING_DOCS = "1-learning_docs", // راهنما و مستندات آموزشی
|
||||
TICKET_SUPPORT = "2-ticket_support", // ارسال تیکت
|
||||
TICKET_LIMIT = "3-ticket_limit", // ظرفیت ارسال تیکت
|
||||
RESPONSE_TIME = "4-response_time", // زمان پاسخگویی به تیکت
|
||||
TECHNICAL_ISSUE_RESOLUTION = "5-technical_issue_resolution", // رفع ایراد فنی
|
||||
PHONE_SUPPORT = "6-phone_support", // پاسخگویی تلفنی
|
||||
TECHNICAL_EXPERT_ACCESS = "7-technical_expert_access", // دسترسی به متخصصین فنی
|
||||
BACKUP_VERSION = "8-backup_version", // نسخه بکاپ
|
||||
SERVICE_FUNCTIONALITY_TEST = "9-service_functionality_test", // تست عملکرد سرویس
|
||||
ON_SITE_SUPPORT = "10-on_site_support", // پشتیبانی در محل
|
||||
ON_SITE_TRAINING = "11-on_site_training", // آموزش در محل
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/SupportService";
|
||||
import { CreatePlanType } from "../types/SupportTypes";
|
||||
|
||||
export const useCreateSupport = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreatePlanType) => api.createPlan(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSupport = () => {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: CreatePlanType }) =>
|
||||
api.updatePlan(id, data),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPlans = () => {
|
||||
return useQuery({
|
||||
queryKey: ["plans"],
|
||||
queryFn: () => api.getPlans(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPlanById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["plan", id],
|
||||
queryFn: () => api.getPlanById(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeletePlan = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deletePlan(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPlanUsers = (id?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["plan-users", id],
|
||||
queryFn: () => api.getPlanUsers(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreatePlanType } from "../types/SupportTypes";
|
||||
|
||||
export const createPlan = async (plan: CreatePlanType) => {
|
||||
const { data } = await axios.post("/support-plans", plan);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updatePlan = async (id: string, plan: CreatePlanType) => {
|
||||
const { data } = await axios.patch(`/support-plans/${id}`, plan);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPlans = async () => {
|
||||
const { data } = await axios.get("/support-plans/list");
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPlanById = async (id: string) => {
|
||||
const { data } = await axios.get(`/support-plans/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deletePlan = async (id: string) => {
|
||||
const { data } = await axios.delete(`/support-plans/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPlanUsers = async (id?: string) => {
|
||||
const { data } = await axios.get(`/support-plans/${id}/users`);
|
||||
return data;
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
export type CreatePlanType = {
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
duration: number;
|
||||
isActive: boolean;
|
||||
features: {
|
||||
featureKey: string;
|
||||
featureValue: string | boolean;
|
||||
featureType: "boolean" | "text";
|
||||
}[];
|
||||
};
|
||||
|
||||
export type PlanItemUserType = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
user: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
userName: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthDate: string;
|
||||
nationalCode: string;
|
||||
profilePic: string | null;
|
||||
emailVerified: boolean;
|
||||
financialType: string;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
supportPlan: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
isFree: boolean;
|
||||
duration: number;
|
||||
isActive: boolean;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: string;
|
||||
};
|
||||
@@ -4,21 +4,20 @@ import Select from '../../components/Select'
|
||||
import Input from '../../components/Input'
|
||||
import Td from '../../components/Td'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import { useGetGroups } from '../users/hooks/useUserData'
|
||||
import { useFormik } from 'formik'
|
||||
import { CategoriesItemType, CreateCategoryType } from './types/TicketTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { GroupItemType } from '../users/types/UserTypes'
|
||||
import Button from '../../components/Button'
|
||||
import { useCreateCategory, useGetCategories } from './hooks/useTicketData'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import EditCategory from './components/EditCategory'
|
||||
import DeleteCategory from './components/DeleteCategory'
|
||||
|
||||
const TicketCategory: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const getGroups = useGetGroups()
|
||||
const createCategory = useCreateCategory()
|
||||
const getCategories = useGetCategories()
|
||||
|
||||
@@ -26,12 +25,10 @@ const TicketCategory: FC = () => {
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
userGroupId: '',
|
||||
isActive: true
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
userGroupId: Yup.string().required(t('errors.required')),
|
||||
description: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: values => {
|
||||
@@ -83,14 +80,21 @@ const TicketCategory: FC = () => {
|
||||
<tr>
|
||||
<Td text={t('title')} />
|
||||
<Td text={t('status')} />
|
||||
<Td text={t('actions')} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getCategories.data?.data?.ticketCategories?.map((item: CategoriesItemType) => (
|
||||
<tr className='tr'>
|
||||
<tr className='tr' key={item.id}>
|
||||
<Td text={item.title} />
|
||||
<Td text={item.isActive ? t('active') : t('inactive')} />
|
||||
<td className='td'>
|
||||
<div className='flex gap-4'>
|
||||
<EditCategory category={item} />
|
||||
<DeleteCategory id={item.id} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
@@ -132,20 +136,6 @@ const TicketCategory: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Select
|
||||
label={t('ticket.users')}
|
||||
items={getGroups.data?.data?.usersGroup?.map((item: GroupItemType) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))}
|
||||
{...formik.getFieldProps('userGroupId')}
|
||||
error_text={formik.touched.userGroupId && formik.errors.userGroupId ? formik.errors.userGroupId : ''}
|
||||
placeholder={t('select')}
|
||||
className='border'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Button
|
||||
label={t('submit')}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Trash } from 'iconsax-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDeleteCategory } from '../hooks/useTicketData'
|
||||
import ModalConfrim from '../../../components/ModalConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const DeleteCategory: FC<Props> = ({ id }) => {
|
||||
const { t } = useTranslation('global')
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const deleteCategory = useDeleteCategory()
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteCategory.mutate(id, {
|
||||
onSuccess: () => {
|
||||
setIsOpen(false)
|
||||
toast.success(t('success'))
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Trash onClick={() => setIsOpen(true)} size={20} color='#888' />
|
||||
|
||||
<ModalConfrim
|
||||
isOpen={isOpen}
|
||||
close={() => setIsOpen(false)}
|
||||
onConfrim={handleDelete}
|
||||
isLoading={deleteCategory.isPending}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteCategory
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DefaulModal from '../../../components/DefaulModal'
|
||||
import { useUpdateCategory } from '../hooks/useTicketData'
|
||||
import { CategoriesItemType, UpdateCategoryType } from '../types/TicketTypes'
|
||||
import Input from '../../../components/Input'
|
||||
import Textarea from '../../../components/Textarea'
|
||||
import SwitchComponent from '../../../components/Switch'
|
||||
import Button from '../../../components/Button'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
interface Props {
|
||||
category: CategoriesItemType;
|
||||
}
|
||||
|
||||
const EditCategory: FC<Props> = ({ category }) => {
|
||||
const { t } = useTranslation('global')
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const updateCategory = useUpdateCategory(category.id)
|
||||
|
||||
const formik = useFormik<UpdateCategoryType>({
|
||||
initialValues: {
|
||||
title: category.title,
|
||||
description: category.description,
|
||||
isActive: category.isActive
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
description: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: values => {
|
||||
updateCategory.mutate(values, {
|
||||
onSuccess: () => {
|
||||
setIsOpen(false)
|
||||
toast.success(t('success'))
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Edit onClick={() => setIsOpen(true)} size={20} color='#888' />
|
||||
|
||||
<DefaulModal
|
||||
open={isOpen}
|
||||
close={() => setIsOpen(false)}
|
||||
isHeader
|
||||
title_header={t('ticket.edit_category')}
|
||||
>
|
||||
<div className='mt-6'>
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label={t('ticket.title_category')}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
className='bg-white bg-opacity-60'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Textarea
|
||||
label={t('description')}
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
className='bg-white bg-opacity-60'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-between items-center'>
|
||||
<div className='text-xs'>
|
||||
{t('ticket.status_category')}
|
||||
</div>
|
||||
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex justify-end'>
|
||||
<Button
|
||||
label={t('submit')}
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateCategory.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditCategory
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as api from "../service/TicketServiec";
|
||||
import { AddMessageTicketType, CreateCategoryType } from "../types/TicketTypes";
|
||||
import {
|
||||
AddMessageTicketType,
|
||||
CreateCategoryType,
|
||||
UpdateCategoryType,
|
||||
} from "../types/TicketTypes";
|
||||
|
||||
export const useGetTickets = (status: string, customerId?: string) => {
|
||||
return useQuery({
|
||||
@@ -63,3 +67,30 @@ export const useOpenTicket = () => {
|
||||
mutationFn: (id: string) => api.openTicket(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCategory = (id: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: UpdateCategoryType) =>
|
||||
api.updateCategory(id, variables),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["categories"],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCategory = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteCategory(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["categories"],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { AddMessageTicketType, CreateCategoryType } from "../types/TicketTypes";
|
||||
import {
|
||||
AddMessageTicketType,
|
||||
CreateCategoryType,
|
||||
UpdateCategoryType,
|
||||
} from "../types/TicketTypes";
|
||||
|
||||
export const getTickets = async (status: string, customerId?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -49,3 +53,16 @@ export const openTicket = async (id: string) => {
|
||||
const { data } = await axios.post(`/tickets/${id}/open`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateCategory = async (
|
||||
id: string,
|
||||
params: UpdateCategoryType
|
||||
) => {
|
||||
const { data } = await axios.patch(`/tickets/category/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteCategory = async (id: string) => {
|
||||
const { data } = await axios.delete(`/tickets/category/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -67,7 +67,6 @@ export type TicketMessageType = {
|
||||
export type CreateCategoryType = {
|
||||
title: string;
|
||||
description: string;
|
||||
userGroupId: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
@@ -79,3 +78,9 @@ export type CategoriesItemType = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type UpdateCategoryType = {
|
||||
title: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { Cards, Money3, MoneyRecive, TickSquare } from 'iconsax-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tabs from '../../components/Tabs'
|
||||
import { useGetWalletBalance } from './hooks/useWalletData'
|
||||
import { NumberFormat } from '../../config/func'
|
||||
|
||||
const Wallet: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const getWalletBalance = useGetWalletBalance()
|
||||
const [activeTab, setActiveTab] = useState<string>('online')
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div>
|
||||
{t('wallet.increese_wallet')}
|
||||
</div>
|
||||
|
||||
<div className='mt-7 xl:hidden flex xl:px-10 px-6 w-full items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-10 h-[70px] justify-between rounded-[32px] bg-white bg-opacity-45'>
|
||||
<div className='xl:text-base text-sm'>{t('wallet.balance')}</div>
|
||||
|
||||
<div className='h-8 text-black rounded-xl text-sm gap-1 w-fit xl:px-14 px-4 bg-[#EEF0F7] flex items-center'>
|
||||
{NumberFormat(getWalletBalance.data?.data?.balance)}
|
||||
<div>تومان</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex gap-6'>
|
||||
<div className='flex-1'>
|
||||
<Tabs
|
||||
active={activeTab}
|
||||
items={[
|
||||
{
|
||||
label: t('wallet.online_pay'),
|
||||
value: 'online',
|
||||
icon: <MoneyRecive color={activeTab === 'online' ? 'black' : '#8C90A3'} size={22} />
|
||||
},
|
||||
{
|
||||
icon: <Cards color={activeTab === 'card' ? 'black' : '#8C90A3'} size={22} />,
|
||||
label: t('wallet.card_to_card'),
|
||||
value: 'card'
|
||||
},
|
||||
{
|
||||
icon: <Money3 color={activeTab === 'sheba' ? 'black' : '#8C90A3'} size={22} />,
|
||||
label: t('wallet.sheba'),
|
||||
value: 'sheba'
|
||||
},
|
||||
]}
|
||||
onChange={setActiveTab}
|
||||
/>
|
||||
|
||||
{/* {
|
||||
activeTab === 'online' ?
|
||||
<Online />
|
||||
: activeTab === 'card' ?
|
||||
<CardtoCard />
|
||||
: <Sheba />
|
||||
} */}
|
||||
|
||||
</div>
|
||||
<div className='w-sidebar xl:block hidden'>
|
||||
<div className=' xl:flex hidden px-6 w-full items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-3 h-20 justify-between rounded-[32px] bg-white bg-opacity-45'>
|
||||
<div className='text-xs'>
|
||||
{t('wallet.balance')}
|
||||
</div>
|
||||
|
||||
<div className='h-8 text-black rounded-xl text-xs gap-1 w-fit px-4 bg-[#EEF0F7] flex items-center'>
|
||||
{NumberFormat(getWalletBalance.data?.data?.balance)}
|
||||
<div>تومان</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white py-10 px-5 h-fit rounded-3xl mt-8'>
|
||||
<div className='text-sm'>
|
||||
لطفا قبل از شارژ کیف پول نکات زیر را مد نظر قرار دهید.
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className='flex items-start gap-2 border-b pb-5'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
شما می توانید به صورت آنلاین یا کارت به کارت و یا واریز از طریق شماره شبا کیف پول خود را شارژ نمایید.
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
تراکنش های کیف پول شما از طریق بخش تراکنش قابل دسترسی است.
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-2 mt-6'>
|
||||
<div>
|
||||
<TickSquare size={20} color='black' variant='Bold' />
|
||||
</div>
|
||||
<div className='text-description text-xs leading-5'>
|
||||
شرایط و مقررات کیف پول داناک در بخش قوانین مقررات درج گردیده، شما با شارژ کیف پول با این قوانین و مقررات موافقت می نمایید.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Wallet
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/WalletService";
|
||||
import { DepositTransferType, PaymentType } from "../types/WalletTypes";
|
||||
|
||||
export const useGetGetWays = () => {
|
||||
return useQuery({
|
||||
queryKey: ["getGetWays"],
|
||||
queryFn: api.getGetWays,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWalletBalance = () => {
|
||||
return useQuery({
|
||||
queryKey: ["wallet-balance"],
|
||||
queryFn: api.getWalletBalance,
|
||||
});
|
||||
};
|
||||
|
||||
export const usePayment = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: PaymentType) => api.payment(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetBankAccount = () => {
|
||||
return useQuery({
|
||||
queryKey: ["bankAccount"],
|
||||
queryFn: api.getBankAccount,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDepositTransfer = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: DepositTransferType) =>
|
||||
api.depositTransfer(variables),
|
||||
});
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { DepositTransferType, PaymentType } from "../types/WalletTypes";
|
||||
|
||||
export const getGetWays = async () => {
|
||||
const { data } = await axios.get(`/payments/gateways`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getWalletBalance = async () => {
|
||||
const { data } = await axios.get(`/wallets/balance`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const payment = async (params: PaymentType) => {
|
||||
const { data } = await axios.post(`/payments/deposit/gateway`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getBankAccount = async () => {
|
||||
const { data } = await axios.get(`/payments/bank-account`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const depositTransfer = async (params: DepositTransferType) => {
|
||||
const { data } = await axios.post(`/payments/deposit/transfer`, params);
|
||||
return data;
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
export type PaymentType = {
|
||||
amount: number;
|
||||
gatewayId: string;
|
||||
};
|
||||
|
||||
export type GatewayItemType = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
logoUrl: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
nameFa: string;
|
||||
};
|
||||
|
||||
export type MethosTransferType = "CARD_TO_CARD" | "SHEBA";
|
||||
export type DepositTransferType = {
|
||||
amount: number;
|
||||
transferReceiptUrl: string;
|
||||
bankAccountId: string;
|
||||
method: MethosTransferType;
|
||||
};
|
||||
+1
-46
@@ -5,22 +5,16 @@ import { Route, Routes } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
import Home from '../pages/home/Home'
|
||||
import MyServices from '../pages/service/MyServices'
|
||||
import DiscountsList from '../pages/discounts/DiscountsList'
|
||||
import CreateDiscount from '../pages/discounts/CreateDiscount'
|
||||
import AdsList from '../pages/ads/AdsList'
|
||||
import BlogList from '../pages/blog/BlogList'
|
||||
import CreateBlog from '../pages/blog/CreateBlog'
|
||||
import BlogCategory from '../pages/blog/BlogCategory'
|
||||
import CreateAds from '../pages/ads/CreateAds'
|
||||
import TicketList from '../pages/ticket/TicketList'
|
||||
import CreateTicket from '../pages/ticket/CreateTicket'
|
||||
import TicketDetail from '../pages/ticket/Detail'
|
||||
import TransactionList from '../pages/transaction/List'
|
||||
import ReceiptsList from '../pages/receipts/List'
|
||||
import AnnouncementtList from '../pages/annoncement/List'
|
||||
import LearningList from '../pages/learning/List'
|
||||
import Setting from '../pages/setting/Setting'
|
||||
import Wallet from '../pages/wallet/Wallet'
|
||||
import ReceiptsDetail from '../pages/receipts/Detail'
|
||||
import Profile from '../pages/profile/Profile'
|
||||
import OtherServices from '../pages/service/OtherServices'
|
||||
@@ -32,8 +26,6 @@ import CreateReceipt from '../pages/receipts/Create'
|
||||
import AddService from '../pages/service/AddService'
|
||||
import ListService from '../pages/service/List'
|
||||
import Category from '../pages/service/Category'
|
||||
import CustomerList from '../pages/customer/List'
|
||||
import CreateCustomer from '../pages/customer/Create'
|
||||
import TicketCategory from '../pages/ticket/Category'
|
||||
import MessagesList from '../pages/messages/List'
|
||||
import CriticismsList from '../pages/criticisms/List'
|
||||
@@ -44,31 +36,16 @@ import CreateUser from '../pages/users/Create'
|
||||
import RolesList from '../pages/users/RolesList'
|
||||
import RoleCreate from '../pages/users/RoleCreate'
|
||||
import MessageDetail from '../pages/messages/Detail'
|
||||
import CreateBankCard from '../pages/cardBank/Create'
|
||||
import CardBankList from '../pages/cardBank/List'
|
||||
import EditBankCard from '../pages/cardBank/Edit'
|
||||
import PaymentList from '../pages/payment/List'
|
||||
import LearningCategory from '../pages/learning/Category'
|
||||
import CreateLearning from '../pages/learning/Create'
|
||||
import Plans from '../pages/service/Plans'
|
||||
import GroupCreate from '../pages/users/GroupCreate'
|
||||
import GroupList from '../pages/users/GroupList'
|
||||
import Reviews from '../pages/service/Reviews'
|
||||
import UpdateAds from '../pages/ads/Update'
|
||||
import UpdateService from '../pages/service/UpdateService'
|
||||
import UpdateCustomer from '../pages/customer/Update'
|
||||
import UpdateBlog from '../pages/blog/Update'
|
||||
import Update from '../pages/annoncement/Update'
|
||||
import UpdateDiscount from '../pages/discounts/UpdateDiscount'
|
||||
import UpdateUser from '../pages/users/Update'
|
||||
import CreateSlider from '../pages/slider/Create'
|
||||
import SliderList from '../pages/slider/List'
|
||||
import UpdateSlider from '../pages/slider/Update'
|
||||
import Comments from '../pages/blog/Comments'
|
||||
import CreatePlan from '../pages/support/Create'
|
||||
import SupportList from '../pages/support/List'
|
||||
import UpdateSupport from '../pages/support/Update'
|
||||
import PlanUsers from '../pages/support/PlanUsers'
|
||||
import CreateCompany from '../pages/company/Create'
|
||||
import Industries from '../pages/company/components/Industries'
|
||||
import CompanyList from '../pages/company/List'
|
||||
@@ -104,12 +81,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.ticket.create} element={<CreateTicket />} />
|
||||
<Route path={Pages.ticket.category} element={<TicketCategory />} />
|
||||
<Route path={Pages.ticket.detail + ':id'} element={<TicketDetail />} />
|
||||
<Route path={Pages.ads.list} element={<AdsList />} />
|
||||
<Route path={Pages.ads.create} element={<CreateAds />} />
|
||||
<Route path={Pages.ads.update + ':id'} element={<UpdateAds />} />
|
||||
<Route path={Pages.discount.list} element={<DiscountsList />} />
|
||||
<Route path={Pages.discount.create} element={<CreateDiscount />} />
|
||||
<Route path={Pages.discount.detail + ':id'} element={<UpdateDiscount />} />
|
||||
<Route path={Pages.blog.list} element={<BlogList />} />
|
||||
<Route path={Pages.blog.list} element={<BlogList />} />
|
||||
<Route path={Pages.blog.create} element={<CreateBlog />} />
|
||||
<Route path={Pages.blog.category} element={<BlogCategory />} />
|
||||
@@ -123,15 +95,8 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.announcement.create} element={<Create />} />
|
||||
<Route path={Pages.criticisms.list} element={<CriticismsList />} />
|
||||
<Route path={Pages.criticisms.detail + ':id'} element={<CriticismsDetail />} />
|
||||
<Route path={Pages.learning.list} element={<LearningList />} />
|
||||
<Route path={Pages.learning.create} element={<CreateLearning />} />
|
||||
<Route path={Pages.learning.category} element={<LearningCategory />} />
|
||||
<Route path={Pages.setting} element={<Setting />} />
|
||||
<Route path={Pages.wallet} element={<Wallet />} />
|
||||
<Route path={Pages.profile} element={<Profile />} />
|
||||
<Route path={Pages.customer.list} element={<CustomerList />} />
|
||||
<Route path={Pages.customer.create} element={<CreateCustomer />} />
|
||||
<Route path={Pages.customer.update + ':id'} element={<UpdateCustomer />} />
|
||||
<Route path={Pages.messages.list} element={<MessagesList />} />
|
||||
<Route path={Pages.messages.detail + ':id'} element={<MessageDetail />} />
|
||||
<Route path={Pages.users.list} element={<UsersList />} />
|
||||
@@ -141,18 +106,8 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.users.roleCreate} element={<RoleCreate />} />
|
||||
<Route path={Pages.users.groupAdd} element={<GroupCreate />} />
|
||||
<Route path={Pages.users.groupList} element={<GroupList />} />
|
||||
<Route path={Pages.cardBank.create} element={<CreateBankCard />} />
|
||||
<Route path={Pages.cardBank.list} element={<CardBankList />} />
|
||||
<Route path={Pages.cardBank.detail + ':id'} element={<EditBankCard />} />
|
||||
<Route path={Pages.payment.list} element={<PaymentList />} />
|
||||
<Route path={Pages.sliders.create} element={<CreateSlider />} />
|
||||
<Route path={Pages.sliders.list} element={<SliderList />} />
|
||||
<Route path={Pages.sliders.detail + ':id'} element={<UpdateSlider />} />
|
||||
<Route path={Pages.blog.comments} element={<Comments />} />
|
||||
<Route path={Pages.support.create} element={<CreatePlan />} />
|
||||
<Route path={Pages.support.list} element={<SupportList />} />
|
||||
<Route path={Pages.support.detail + ':id'} element={<UpdateSupport />} />
|
||||
<Route path={Pages.support.planUsers + ':id'} element={<PlanUsers />} />
|
||||
<Route path={Pages.company.create} element={<CreateCompany />} />
|
||||
<Route path={Pages.company.industries} element={<Industries />} />
|
||||
<Route path={Pages.company.list} element={<CompanyList />} />
|
||||
|
||||
+81
-173
@@ -1,8 +1,8 @@
|
||||
import { FC, useEffect } from 'react'
|
||||
import { FC, Fragment, useEffect, useState } from 'react'
|
||||
import LogoImage from '../assets/images/logo.svg'
|
||||
import LogoSmall from '../assets/images/logo-small.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Building, Card, Code, CodeCircle, DocumentLike, DocumentText, Gallery, Headphone, Home2, Logout, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare } from 'iconsax-react'
|
||||
import { Building, DocumentText, Home2, Logout, Messages3, NoteAdd, NotificationStatus, Receipt21, Setting2 } from 'iconsax-react'
|
||||
import SideBarItem from './SideBarItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
@@ -24,6 +24,7 @@ const SideBar: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const { openSidebar, setOpenSidebar, hasSubMenu, setSubMenuName, setSubtMenu, subMenuName } = useSharedStore()
|
||||
const location = useLocation()
|
||||
const [isWorkspace, setIsWorkspace] = useState(false)
|
||||
const { data: profile } = useGetProfile()
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
@@ -42,6 +43,11 @@ const SideBar: FC = () => {
|
||||
setSubtMenu(false)
|
||||
}
|
||||
|
||||
const isWorkspace = localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID)
|
||||
if (isWorkspace) {
|
||||
setIsWorkspace(true)
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname])
|
||||
@@ -88,95 +94,35 @@ const SideBar: FC = () => {
|
||||
activeName=''
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<Building variant={isActive('company') ? 'Bold' : 'Outline'} color={isActive('company') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.company')}
|
||||
isActive={isActive('company')}
|
||||
link={Pages.company.list}
|
||||
activeName='company'
|
||||
/>
|
||||
{
|
||||
isWorkspace &&
|
||||
<Fragment>
|
||||
<SideBarItem
|
||||
icon={<Building variant={isActive('company') ? 'Bold' : 'Outline'} color={isActive('company') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.company')}
|
||||
isActive={isActive('company')}
|
||||
link={Pages.company.list}
|
||||
activeName='company'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<Headphone variant={isActive('support') ? 'Bold' : 'Outline'} color={isActive('support') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.support')}
|
||||
isActive={isActive('support')}
|
||||
link={Pages.support.list}
|
||||
activeName='support_plan'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<NoteAdd variant={isActive('requests') ? 'Bold' : 'Outline'} color={isActive('requests') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.requests')}
|
||||
isActive={isActive('requests')}
|
||||
link={Pages.requests.list}
|
||||
activeName='requests'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<People variant={isActive('customers') ? 'Bold' : 'Outline'} color={isActive('customers') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.customers')}
|
||||
isActive={isActive('customers')}
|
||||
link={Pages.customers.list}
|
||||
activeName='customers'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<UserSquare variant={isActive('representatives') ? 'Bold' : 'Outline'} color={isActive('representatives') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.representatives')}
|
||||
isActive={isActive('representatives')}
|
||||
link={Pages.representative.list}
|
||||
activeName='agents'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<CodeCircle variant={isActive('developers') ? 'Bold' : 'Outline'} color={isActive('developers') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.developers')}
|
||||
isActive={isActive('developers')}
|
||||
link={Pages.developers.list}
|
||||
activeName='developers'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Receipt21 variant={isActive('receipts') ? 'Bold' : 'Outline'} color={isActive('receipts') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.receipt_list')}
|
||||
isActive={isActive('receipts')}
|
||||
link={Pages.receipts.index}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
|
||||
<SideBarItem
|
||||
icon={<Receipt21 variant={isActive('receipts') ? 'Bold' : 'Outline'} color={isActive('receipts') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.receipt_list')}
|
||||
isActive={isActive('receipts')}
|
||||
link={Pages.receipts.index}
|
||||
activeName='invoices'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Card variant={isActive('transactions') ? 'Bold' : 'Outline'} color={isActive('transactions') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.transactions')}
|
||||
isActive={isActive('transactions')}
|
||||
link={Pages.transactions}
|
||||
activeName='transactions'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Money3 variant={isActive('payments') ? 'Bold' : 'Outline'} color={isActive('payments') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.payments')}
|
||||
isActive={isActive('payments')}
|
||||
link={Pages.payment.list}
|
||||
activeName='payments'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<TicketDiscount variant={isActive('discounts') ? 'Bold' : 'Outline'} color={isActive('discounts') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.discounts')}
|
||||
isActive={isActive('discounts')}
|
||||
link={Pages.discount.list}
|
||||
activeName='discounts'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Code variant={isActive('referral-code') ? 'Bold' : 'Outline'} color={isActive('referral-code') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.code_referral')}
|
||||
isActive={isActive('referral-code')}
|
||||
link={Pages.referralCode.list}
|
||||
activeName='referralCode'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Profile variant={isActive('users') ? 'Bold' : 'Outline'} color={isActive('users') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.users')}
|
||||
isActive={isActive('users')}
|
||||
link={Pages.users.list}
|
||||
activeName='admins'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Gallery variant={isActive('sliders') ? 'Bold' : 'Outline'} color={isActive('sliders') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.sliders')}
|
||||
isActive={isActive('sliders')}
|
||||
link={Pages.sliders.list}
|
||||
activeName='blogs'
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -190,27 +136,32 @@ const SideBar: FC = () => {
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
isWorkspace &&
|
||||
<Fragment>
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Messages3 variant={isActive('tickets') ? 'Bold' : 'Outline'} color={isActive('tickets') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.ticket')}
|
||||
isActive={isActive('tickets')}
|
||||
link={Pages.ticket.list}
|
||||
activeName='tickets'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<DocumentText variant={isActive('criticisms') ? 'Bold' : 'Outline'} color={isActive('criticisms') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.criticisms')}
|
||||
isActive={isActive('criticisms')}
|
||||
link={Pages.criticisms.list}
|
||||
activeName='criticisms'
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Messages3 variant={isActive('tickets') ? 'Bold' : 'Outline'} color={isActive('tickets') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.ticket')}
|
||||
isActive={isActive('tickets')}
|
||||
link={Pages.ticket.list}
|
||||
activeName='tickets'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<DocumentText variant={isActive('criticisms') ? 'Bold' : 'Outline'} color={isActive('criticisms') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.criticisms')}
|
||||
isActive={isActive('criticisms')}
|
||||
link={Pages.criticisms.list}
|
||||
activeName='criticisms'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
profile?.data?.user?.roles?.some((role: { name: string }) => role.name === 'super_admin') &&
|
||||
@@ -223,77 +174,34 @@ const SideBar: FC = () => {
|
||||
}
|
||||
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<DocumentLike variant={isActive('ads') ? 'Bold' : 'Outline'} color={isActive('ads') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.ads')}
|
||||
isActive={isActive('ads')}
|
||||
link={Pages.ads.list}
|
||||
activeName='advertisements'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<NotificationStatus variant={isActive('announcement') ? 'Bold' : 'Outline'} color={isActive('announcement') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.announcement')}
|
||||
isActive={isActive('announcement')}
|
||||
link={Pages.announcement.list}
|
||||
activeName='announcements'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<DocumentText variant={isActive('blog') ? 'Bold' : 'Outline'} color={isActive('blog') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.blog')}
|
||||
isActive={isActive('blog')}
|
||||
link={Pages.blog.list}
|
||||
activeName='blogs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Teacher variant={isActive('learning') ? 'Bold' : 'Outline'} color={isActive('learning') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.learning')}
|
||||
isActive={isActive('learning')}
|
||||
link={Pages.learning.list}
|
||||
activeName='learnings'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<SmsTracking variant={isActive('messages') ? 'Bold' : 'Outline'} color={isActive('messages') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.messages')}
|
||||
isActive={isActive('messages')}
|
||||
link={Pages.messages.list}
|
||||
activeName='contacts_us'
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{
|
||||
isWorkspace &&
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<NotificationStatus variant={isActive('announcement') ? 'Bold' : 'Outline'} color={isActive('announcement') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.announcement')}
|
||||
isActive={isActive('announcement')}
|
||||
link={Pages.announcement.list}
|
||||
activeName='announcements'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className='flex-1 flex flex-col justify-end mt-14'>
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Card variant={isActive('card') ? 'Bold' : 'Outline'} color={isActive('card') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.card')}
|
||||
isActive={isActive('card')}
|
||||
link={Pages.cardBank.list}
|
||||
activeName='bank_accounts'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Setting2 variant={isActive('setting') ? 'Bold' : 'Outline'} color={isActive('setting') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.setting')}
|
||||
isActive={isActive('setting')}
|
||||
link={Pages.setting}
|
||||
activeName='settings'
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isWorkspace &&
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Setting2 variant={isActive('setting') ? 'Bold' : 'Outline'} color={isActive('setting') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.setting')}
|
||||
isActive={isActive('setting')}
|
||||
link={Pages.setting}
|
||||
activeName='settings'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Logout variant={isActive('logout') ? 'Bold' : 'Outline'} color={isActive('logout') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
|
||||
@@ -34,11 +34,6 @@ const TicketSubMenu: FC = () => {
|
||||
isActive={isActive('list')}
|
||||
link={Pages.ticket.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.send_ticket')}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.ticket.create}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.category')}
|
||||
isActive={isActive('category')}
|
||||
|
||||
Reference in New Issue
Block a user