industries

This commit is contained in:
hamid zarghami
2025-05-11 12:38:04 +03:30
parent 6cb45848ce
commit 35591cde47
12 changed files with 524 additions and 2 deletions
+1
View File
@@ -36,6 +36,7 @@ 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) {
+1
View File
@@ -114,5 +114,6 @@ export const Pages = {
},
company: {
create: "/company/create",
industries: "/company/industries",
},
};
+3
View File
@@ -804,5 +804,8 @@
"status": "وضعیت",
"full_name": "نام و نام خانوادگی",
"users": "کاربران"
},
"company": {
"count_company": "تعداد شرکت ها"
}
}
@@ -0,0 +1,106 @@
import { FC, useState } from 'react'
import SwitchComponent from '../../../components/Switch'
import { useFormik } from 'formik'
import { CreateIndustryType } from '../types/CompanyTypes'
import * as yup from 'yup'
import Input from '../../../components/Input'
import UploadBoxDraggble from '../../../components/UploadBoxDraggble'
import Button from '../../../components/Button'
import { TickCircle } from 'iconsax-react'
import { toast } from 'react-toastify'
import { useSingleUpload } from '../../service/hooks/useServiceData'
import { useCreateIndustry } from '../hooks/useCompanyData'
type Props = {
refetch: () => void
}
const CreateIndustry: FC<Props> = ({ refetch }) => {
const [file, setFile] = useState<File>()
const singleUpload = useSingleUpload()
const createIndustry = useCreateIndustry()
const formik = useFormik<CreateIndustryType>({
initialValues: {
title: '',
iconUrl: '',
isActive: true,
},
validationSchema: yup.object({
title: yup.string().required('عنوان صنعت الزامی است'),
}),
onSubmit: (values) => {
if (!file) {
toast.error('لطفا آیکن صنعت را انتخاب کنید')
return
}
const formData = new FormData()
formData.append('file', file)
singleUpload.mutate(formData, {
onSuccess: (data) => {
values.iconUrl = data?.data?.url
createIndustry.mutate(values, {
onSuccess: () => {
toast.success('صنعت با موفقیت ثبت شد')
refetch()
}
})
}
})
},
})
return (
<div className='bg-white mt-10 w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
<div className='flex justify-between items-center'>
<div className='text-sm'>
وضعیت صنعت
</div>
<SwitchComponent
active={formik.values.isActive}
onChange={(value) => {
formik.setFieldValue('isActive', value)
}}
/>
</div>
<div className='mt-6'>
<Input
label='عنوان صنعت'
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
</div>
<div className='mt-6'>
<UploadBoxDraggble
label='آیکن صنعت'
onChange={(files) => {
setFile(files[0])
}}
/>
</div>
<div className='mt-6 flex justify-end'>
<Button
onClick={() => formik.handleSubmit()}
className='w-fit px-4'
isLoading={createIndustry.isPending || singleUpload.isPending}
>
<div className='flex items-center gap-2'>
<TickCircle
size={20}
color='#fff'
/>
<span>ثبت صنعت</span>
</div>
</Button>
</div>
</div>
)
}
export default CreateIndustry
+12 -2
View File
@@ -1,11 +1,12 @@
import { FC } from 'react'
import SwitchComponent from '../../../components/Switch'
import Input from '../../../components/Input'
const CreateSidebar: FC = () => {
return (
<div className='bg-white mt-10 w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
<div className='flex justify-between'>
<div>
<div className='flex justify-between items-center'>
<div className='text-sm'>
وضعیت شرکت
</div>
<SwitchComponent
@@ -13,6 +14,15 @@ const CreateSidebar: FC = () => {
onChange={() => { }}
/>
</div>
<div className='mt-6'>
<Input
label='آدرس وبسایت شرکت'
name='name'
onChange={() => { }}
error_text=''
/>
</div>
</div>
)
}
+132
View File
@@ -0,0 +1,132 @@
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Select from '../../../components/Select'
import Input from '../../../components/Input'
import Td from '../../../components/Td'
import PageLoading from '../../../components/PageLoading'
import { Trash } from 'iconsax-react'
import CreateIndustry from './CreateIndustry'
import { useDeleteIndustry, useGetIndustries } from '../hooks/useCompanyData'
import { IndustryItemType } from '../types/CompanyTypes'
import Pagination from '../../../components/Pagination'
import StatusIndustry from './StatusIndustry'
import UpdateIndustry from './UpdateIndustry'
const Industries: FC = () => {
const { t } = useTranslation('global')
const [search, setSearch] = useState<string>('')
const [page, setPage] = useState<number>(1)
const [isActive, setIsActive] = useState<'active' | 'deactive' | ''>('')
const getIndustries = useGetIndustries(page, search, isActive)
const deleteIndustry = useDeleteIndustry()
const handleDelete = (id: string) => {
deleteIndustry.mutate(id, {
onSuccess: () => {
getIndustries.refetch()
}
})
}
return (
<div className='mt-4'>
<div>
{t('service.cateory_services')}
</div>
<div className='flex gap-6 mt-9'>
<div className='flex-1'>
<div className='flex justify-between items-center'>
<div className='w-32'>
<Select
items={[
{ label: t('all'), value: '' },
{ label: t('service.active'), value: 'active' },
{ label: t('service.deactive'), value: 'deactive' },
]}
placeholder={t('status')}
onChange={(e) => setIsActive(e.target.value as 'active' | 'deactive' | '')}
/>
</div>
<div className='w-40'>
<Input
variant='search'
placeholder={t('search')}
className='bg-white'
onChangeSearchFinal={(value) => setSearch(value)}
/>
</div>
</div>
<div className='flex-1 mt-8 bg-white py-2 xl:px-10 px-5 rounded-3xl'>
{
getIndustries.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='' />
<Td text={t('service.title')} />
<Td text={t('company.count_company')} />
<Td text={t('ticket.status')} />
<Td text={''} />
</tr>
</thead>
<tbody>
{
getIndustries.data?.data?.industries?.map((item: IndustryItemType) => {
return (
<tr key={item.id} className='tr'>
<Td text=''>
<img src={item.iconUrl} className='size-10 min-w-10 rounded-lg' />
</Td>
<Td text={item.title} />
<Td text={item.companiesCount + ''} />
<Td text={''}>
<StatusIndustry
defaultActive={item.isActive}
id={item.id}
/>
</Td>
<Td text={''}>
<div className='flex gap-2'>
<UpdateIndustry refetch={() => getIndustries.refetch()} id={item.id} />
<Trash
size={20}
color='#888888'
onClick={() => handleDelete(item.id)}
/>
</div>
</Td>
</tr>
)
})
}
</tbody>
</table>
<div className='flex justify-end pb-3'>
<Pagination
currentPage={page}
totalPages={getIndustries.data?.data?.pager?.totalPages}
onPageChange={setPage}
/>
</div>
</div>
}
</div>
</div>
<CreateIndustry refetch={() => getIndustries.refetch()} />
</div>
</div>
)
}
export default Industries
@@ -0,0 +1,28 @@
import { FC, useState } from 'react'
import SwitchComponent from '../../../components/Switch'
import { useChangeStatusIndustry } from '../hooks/useCompanyData'
type Props = {
defaultActive: boolean,
id: string
}
const StatusIndustry: FC<Props> = (props: Props) => {
const [isActive, setIsActive] = useState<boolean>(props.defaultActive)
const toggleStatus = useChangeStatusIndustry()
const handleChange = (value: boolean) => {
setIsActive(value)
toggleStatus.mutate(props.id)
}
return (
<SwitchComponent
active={isActive}
onChange={handleChange}
/>
)
}
export default StatusIndustry
@@ -0,0 +1,138 @@
import { Edit, TickCircle } from 'iconsax-react'
import { FC, useEffect, useState } from 'react'
import DefaulModal from '../../../components/DefaulModal'
import Input from '../../../components/Input'
import * as Yup from 'yup'
import { useFormik } from 'formik'
import { toast } from 'react-toastify'
import { useTranslation } from 'react-i18next'
import { ErrorType } from '../../../helpers/types'
import UploadBox from '../../../components/UploadBox'
import Button from '../../../components/Button'
import { useGetIndustryDetail, useUpdateIndustry } from '../hooks/useCompanyData'
import { useSingleUpload } from '../../service/hooks/useServiceData'
import { CreateIndustryType } from '../types/CompanyTypes'
type Props = {
id: string,
refetch: () => void
}
const UpdateIndustry: FC<Props> = (props) => {
const { t } = useTranslation('global')
const [showModal, setShowModal] = useState<boolean>(false)
const [file, setFile] = useState<File>()
const getIndustryDetail = useGetIndustryDetail(props.id)
const singleUpload = useSingleUpload()
const updateIndustry = useUpdateIndustry()
const formik = useFormik<CreateIndustryType>({
initialValues: {
title: '',
isActive: true,
iconUrl: '',
},
validationSchema: Yup.object({
title: 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.iconUrl = data?.data?.url
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
const params = {
...values,
}
updateIndustry.mutate({ id: props.id, params }, {
onSuccess: () => {
formik.resetForm()
getIndustryDetail.refetch()
toast.success(t('success'))
setShowModal(false)
props.refetch()
},
onError: (error: ErrorType) => {
toast.error(error?.response?.data?.error?.message[0])
}
})
}
})
useEffect(() => {
if (getIndustryDetail.data) {
const data = getIndustryDetail.data?.data?.industry
formik.setValues({
title: data?.title,
iconUrl: data?.iconUrl,
isActive: data?.isActive,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getIndustryDetail.data, showModal])
return (
<div>
<Edit onClick={() => setShowModal(true)} size={20} color='#888888' />
<DefaulModal
open={showModal}
close={() => setShowModal(false)}
isHeader
title_header='ویرایش دسته بندی'
>
<div className='mt-5'>
<Input
label={t('service.title_category')}
value={formik.values.title}
name='title'
onChange={formik.handleChange}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
className='bg-white bg-opacity-40'
/>
</div>
<div className='mt-5'>
<UploadBox
label='آیکون جدید'
onChange={(file) => setFile(file[0])}
/>
</div>
<div className='mt-10 border-t border-border pt-5 flex justify-end'>
<div className='flex gap-5 items-center'>
<Button
className='w-[150px] bg-white bg-opacity-15 text-description'
label={t('plan.cancel')}
onClick={() => setShowModal(false)}
/>
<Button
onClick={() => formik.handleSubmit()}
>
<div className='flex gap-2'>
<TickCircle
size={20}
color='white'
/>
<div>
{t('save')}
</div>
</div>
</Button>
</div>
</div>
</DefaulModal>
</div>
)
}
export default UpdateIndustry
+45
View File
@@ -0,0 +1,45 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/CompanyService";
import { CreateIndustryType } from "../types/CompanyTypes";
export const useGetIndustries = (
page: number,
search: string,
isActive: string
) => {
return useQuery({
queryKey: ["industries", page, search, isActive],
queryFn: () => api.getIndustries(page, search, isActive),
});
};
export const useCreateIndustry = () => {
return useMutation({
mutationFn: api.createIndustry,
});
};
export const useDeleteIndustry = () => {
return useMutation({
mutationFn: api.deleteIndustry,
});
};
export const useChangeStatusIndustry = () => {
return useMutation({
mutationFn: (id: string) => api.changeStatusIndustry(id),
});
};
export const useUpdateIndustry = () => {
return useMutation({
mutationFn: ({ id, params }: { id: string; params: CreateIndustryType }) =>
api.updateIndustry(id, params),
});
};
export const useGetIndustryDetail = (id: string) => {
return useQuery({
queryKey: ["industry-detail", id],
queryFn: () => api.getIndustryDetail(id),
});
};
@@ -0,0 +1,43 @@
import axios from "../../../config/axios";
import { CreateIndustryType } from "../types/CompanyTypes";
export const getIndustries = async (
page: number,
search: string,
isActive: string
) => {
let query = `?page=${page}&q=${search}`;
if (isActive) {
query += `&isActive=${isActive === "active" ? "1" : "0"}`;
}
const { data } = await axios.get(`/industries/list${query}`);
return data;
};
export const createIndustry = async (params: CreateIndustryType) => {
const { data } = await axios.post("/industries", params);
return data;
};
export const deleteIndustry = async (id: string) => {
const { data } = await axios.delete(`/industries/${id}`);
return data;
};
export const changeStatusIndustry = async (id: string) => {
const { data } = await axios.put(`/industries/${id}/status`);
return data;
};
export const updateIndustry = async (
id: string,
params: CreateIndustryType
) => {
const { data } = await axios.patch(`/industries/${id}`, params);
return data;
};
export const getIndustryDetail = async (id: string) => {
const { data } = await axios.get(`/industries/${id}`);
return data;
};
+13
View File
@@ -0,0 +1,13 @@
export type CreateIndustryType = {
title: string;
iconUrl: string;
isActive: boolean;
};
export type IndustryItemType = {
iconUrl: string;
id: string;
isActive: boolean;
title: string;
companiesCount: number;
};
+2
View File
@@ -70,6 +70,7 @@ 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'
const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore()
@@ -151,6 +152,7 @@ const MainRouter: FC = () => {
<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 />} />
</Routes>
</div>
</div>