auth + companies + invoice

This commit is contained in:
hamid zarghami
2025-05-18 12:41:42 +03:30
parent 8af0b9f9d9
commit 1b838e7a4d
85 changed files with 513 additions and 4635 deletions
+9
View File
@@ -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'
-2
View File
@@ -28,8 +28,6 @@ yarn-error.log*
*.sublime-workspace
# environment variables
.env
# cache
.cache/
.eslintcache
+8 -8
View File
@@ -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}>
+3
View File
@@ -4,6 +4,9 @@ export const Pages = {
register: "/auth/register",
forgotPassword: "/auth/forgot",
},
requests: {
list: "/requests/list",
},
dashboard: "/dashboard",
services: {
mine: "/services",
+3
View File
@@ -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;
});
+22
View File
@@ -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
View File
@@ -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: "/" });
};
+9
View File
@@ -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
View File
@@ -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": "شرکت"
}
}
-157
View File
@@ -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
-180
View File
@@ -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
-258
View File
@@ -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
-28
View File
@@ -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
-47
View File
@@ -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),
});
};
-48
View File
@@ -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;
};
-36
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import axios from "../../../config/axios";
import axios from "../../../config/axiosDanak";
import {
CheckHasAccountPhoneType,
CheckHasAccountType,
-159
View File
@@ -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
-179
View File
@@ -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
-84
View File
@@ -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;
};
-15
View File
@@ -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])
}
})
}
+10 -2
View File
@@ -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>
+3 -3
View File
@@ -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;
};
-273
View File
@@ -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
-142
View File
@@ -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
-301
View File
@@ -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
View File
@@ -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>
)
+3 -3
View File
@@ -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,
});
};
+5 -3
View File
@@ -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;
};
+48
View File
@@ -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[];
};
-74
View File
@@ -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
-167
View File
@@ -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
-79
View File
@@ -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;
};
-34
View File
@@ -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;
};
+3 -3
View File
@@ -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,
+1 -3
View File
@@ -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'>
+2 -1
View File
@@ -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;
};
+22 -33
View File
@@ -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>
}
+6 -5
View File
@@ -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)} />
+1 -1
View File
@@ -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);
+3 -4
View File
@@ -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";
-141
View File
@@ -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
-106
View File
@@ -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
-149
View File
@@ -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
-35
View File
@@ -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),
});
};
-27
View File
@@ -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;
};
-20
View File
@@ -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;
};
-212
View File
@@ -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
-85
View File
@@ -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
-56
View File
@@ -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
-233
View File
@@ -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
-36
View File
@@ -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
-50
View File
@@ -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
-13
View File
@@ -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", // آموزش در محل
}
-44
View File
@@ -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;
};
-49
View File
@@ -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;
};
+10 -20
View File
@@ -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
+32 -1
View File
@@ -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"],
});
},
});
};
+18 -1
View File
@@ -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;
};
+6 -1
View File
@@ -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;
};
-113
View File
@@ -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
-37
View File
@@ -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),
});
};
-27
View File
@@ -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;
};
-23
View File
@@ -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
View File
@@ -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
View File
@@ -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} />}
-5
View File
@@ -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')}