crud company

This commit is contained in:
hamid zarghami
2025-05-17 10:53:08 +03:30
parent f2bef6c9d4
commit 8af0b9f9d9
21 changed files with 1222 additions and 91 deletions
+2
View File
@@ -115,5 +115,7 @@ export const Pages = {
company: {
create: "/company/create",
industries: "/company/industries",
list: "/company/list",
detail: "/company/detail/",
},
};
+1
View File
@@ -7,4 +7,5 @@ export const SideBarItemHasSubMenu = [
"learning",
"blog",
"support",
"company",
];
+25 -14
View File
@@ -25,6 +25,17 @@ body,
}
* {
outline: none;
scrollbar-width: auto;
-ms-overflow-style: auto;
}
*::-webkit-scrollbar {
display: block;
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-thumb {
background-color: #c9c9c9;
border-radius: 5px;
}
input::placeholder,
textarea::placeholder {
@@ -135,9 +146,14 @@ tbody tr {
@apply rounded-b-xl text-right;
font-family: irancell !important;
direction: rtl;
min-height: 200px !important;
max-height: 500px;
overflow-y: auto;
}
.ql-toolbar {
@apply rounded-t-xl;
position: relative !important;
z-index: 10;
}
.ql-editor {
@apply !text-right;
@@ -151,22 +167,17 @@ tbody tr {
font-family: "irancell" !important;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
.main-content-scroll {
scrollbar-width: auto !important;
-ms-overflow-style: auto !important;
}
*::-webkit-scrollbar {
display: none;
.main-content-scroll::-webkit-scrollbar {
display: block !important;
width: 5px !important;
}
.ql-container {
min-height: 200px !important;
max-height: 500px;
overflow-y: auto;
}
.ql-toolbar {
position: relative !important;
z-index: 10;
.main-content-scroll::-webkit-scrollbar-thumb {
background-color: #c9c9c9 !important;
border-radius: 5px !important;
}
+22 -2
View File
@@ -71,7 +71,8 @@
"card": "حساب های بانکی",
"payments": "پرداخت ها",
"sliders": "اسلایدر",
"support": "پلن پشتیبانی"
"support": "پلن پشتیبانی",
"company": "شرکت ها"
},
"slider": {
"new_slider": "اسلایدر جدید",
@@ -808,6 +809,7 @@
"company": {
"count_company": "تعداد شرکت ها",
"company_name": "نام شرکت",
"ceo_name": "نام مدیر عامل",
"ceo_first_name": "نام مدیر عامل",
"ceo_last_name": "نام خانوادگی مدیر عامل",
"phone_number": "شماره تماس",
@@ -816,6 +818,24 @@
"establishment_date": "تاریخ تاسیس",
"address": "آدرس",
"map_link": "لینک نقشه شرکت",
"short_description": "توضیح کوتاه شرکت"
"short_description": "توضیح کوتاه شرکت",
"website_url": "آدرس وبسایت",
"profile_image_url": "تصویر پروفایل",
"cover_image_url": "تصویر پوشه",
"is_active": "وضعیت فعالیت",
"industry_id": "صنعت",
"industry": "صنعت",
"profile_image": "تصویر پروفایل",
"cover_image": "تصویر کاور",
"created_at": "تاریخ ثبت",
"status": "وضعیت",
"plans": "پلن ها",
"detail": "جزییات",
"slider": "اسلایدر",
"category": "دسته بندی",
"company_developed": "شرکت توسعه دهنده",
"slug": "اسلاگ",
"industries": "صنعت ها",
"invoice_count": "تعداد صورتحساب"
}
}
+167 -53
View File
@@ -6,40 +6,129 @@ import CreateSidebar from './components/CreateSidebar'
import { useTranslation } from 'react-i18next'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { CreateCompanyType } from './types/CompanyTypes'
import { useState } from 'react'
import { useMultiUpload, useSingleUpload } from '../service/hooks/useServiceData'
import { useCreateCompany } from './hooks/useCompanyData'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { Pages } from '../../config/Pages'
import { useNavigate } from 'react-router-dom'
import CompanyServices from './components/CompanyServices'
import CompanyProducts from './components/CompanyProducts'
import DatePickerComponent from '../../components/DatePicker'
import moment from 'moment-jalaali'
const CreateCompany: FC = () => {
const navigate = useNavigate()
const { t } = useTranslation('global')
const [profileImage, setProfileImage] = useState<File | null>(null)
const [coverImage, setCoverImage] = useState<File | null>(null)
const multiUpload = useMultiUpload()
const singleUpload = useSingleUpload()
const createCompany = useCreateCompany()
const formik = useFormik({
const formik = useFormik<CreateCompanyType>({
initialValues: {
phone: '',
firstName: '',
lastName: '',
nationalCode: '',
password: '',
name: '',
ceoFirstName: '',
ceoLastName: '',
phoneNumber: '',
email: '',
nationalId: '',
establishmentDate: '',
identificationNumber: '',
dateOfEstablishment: '',
address: '',
mapLink: '',
shortDescription: ''
mapAddressLink: '',
description: '',
websiteUrl: '',
profileImageUrl: '',
coverImageUrl: '',
isActive: true,
industryId: '',
products: [],
services: [],
},
validationSchema: Yup.object({
phone: Yup.string().required(t('errors.required')),
firstName: Yup.string().required(t('errors.required')),
lastName: Yup.string().required(t('errors.required')),
nationalCode: Yup.string().required(t('errors.required')),
password: Yup.string().required(t('errors.required')),
name: Yup.string().required(t('errors.required')),
ceoFirstName: Yup.string().required(t('errors.required')),
ceoLastName: Yup.string().required(t('errors.required')),
phoneNumber: Yup.string().required(t('errors.required')),
email: Yup.string().email(t('errors.invalid_email')).required(t('errors.required')),
nationalId: Yup.string().required(t('errors.required')),
establishmentDate: Yup.string().required(t('errors.required')),
identificationNumber: Yup.string().required(t('errors.required')),
dateOfEstablishment: Yup.string().required(t('errors.required')),
address: Yup.string().required(t('errors.required')),
mapLink: Yup.string().url(t('errors.invalid_url')),
shortDescription: Yup.string().required(t('errors.required'))
mapAddressLink: Yup.string().url(t('errors.invalid_url')),
description: Yup.string().required(t('errors.required')),
websiteUrl: Yup.string().url(t('errors.invalid_url')).required(t('errors.required')),
isActive: Yup.boolean().required(t('errors.required')),
industryId: Yup.string().required(t('errors.required')),
// products and services validation can be added if needed
}),
onSubmit: (values) => {
console.log(values)
onSubmit: async (values) => {
if (!profileImage || !coverImage) {
toast.error('لطفا تصاویر را انتخاب کنید')
return
}
// آپلود تصاویر خدمات
let serviceImages: string[] = []
if (services.length > 0) {
const formDataServices = new FormData()
services.forEach(s => s.image && formDataServices.append('files', s.image))
const serviceUploadRes = await multiUpload.mutateAsync(formDataServices)
serviceImages = serviceUploadRes.data.map((item: { url: string }) => item?.url)
}
// آپلود تصاویر محصولات
let productImages: string[] = []
if (products.length > 0) {
const formDataProducts = new FormData()
products.forEach(p => p.image && formDataProducts.append('files', p.image))
const productUploadRes = await multiUpload.mutateAsync(formDataProducts)
productImages = productUploadRes.data.map((item: { url: string }) => item?.url)
}
// مقداردهی به values
values.services = services.map((s, idx) => ({
title: s.title,
imageUrl: serviceImages[idx] ? serviceImages[idx] : ''
}))
values.products = products.map((p, idx) => ({
title: p.title,
imageUrl: productImages[idx] ? productImages[idx] : ''
}))
const formDataProfile = new FormData()
formDataProfile.append('file', profileImage)
const profileUploadRes = await singleUpload.mutateAsync(formDataProfile)
values.profileImageUrl = profileUploadRes.data.url
const formDataCover = new FormData()
formDataCover.append('file', coverImage)
const coverUploadRes = await singleUpload.mutateAsync(formDataCover)
values.coverImageUrl = coverUploadRes.data.url
await createCompany.mutateAsync(values, {
onSuccess: () => {
toast.success('شرکت با موفقیت ثبت شد')
navigate(Pages.company.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
})
const [services, setServices] = useState<{ title: string; image?: File }[]>([])
const [products, setProducts] = useState<{ title: string; image?: File }[]>([])
return (
<div className='mt-4'>
<div className='flex justify-between items-center'>
@@ -50,6 +139,7 @@ const CreateCompany: FC = () => {
<Button
className='w-fit px-4'
onClick={() => formik.handleSubmit()}
isLoading={createCompany.isPending || singleUpload.isPending || multiUpload.isPending}
>
<div className='flex items-center gap-2'>
<TickCircle size={20} color='white' />
@@ -63,41 +153,57 @@ const CreateCompany: FC = () => {
<div className='flex-1 mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div>اطلاعات شرکت</div>
<div className='rowTwoInput mt-6'>
<div className='mt-6'>
<Input
label={t('company.company_name')}
label='نام شرکت'
name='name'
value={formik.values.name}
onChange={formik.handleChange}
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
/>
<Input
label={t('company.ceo_first_name')}
name='ceoFirstName'
value={formik.values.ceoFirstName}
onChange={formik.handleChange}
error_text={formik.touched.ceoFirstName && formik.errors.ceoFirstName ? formik.errors.ceoFirstName : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.ceo_last_name')}
name='ceoLastName'
value={formik.values.ceoLastName}
label='نام'
name='firstName'
value={formik.values.firstName}
onChange={formik.handleChange}
error_text={formik.touched.ceoLastName && formik.errors.ceoLastName ? formik.errors.ceoLastName : ''}
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''}
/>
<Input
label='نام خانوادگی'
name='lastName'
value={formik.values.lastName}
onChange={formik.handleChange}
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label='کد ملی'
name='nationalCode'
value={formik.values.nationalCode}
onChange={formik.handleChange}
error_text={formik.touched.nationalCode && formik.errors.nationalCode ? formik.errors.nationalCode : ''}
/>
<Input
label='رمز عبور'
name='password'
type='password'
value={formik.values.password}
onChange={formik.handleChange}
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.phone_number')}
name='phoneNumber'
value={formik.values.phoneNumber}
name='phone'
value={formik.values.phone}
onChange={formik.handleChange}
error_text={formik.touched.phoneNumber && formik.errors.phoneNumber ? formik.errors.phoneNumber : ''}
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.email')}
name='email'
@@ -105,29 +211,32 @@ const CreateCompany: FC = () => {
onChange={formik.handleChange}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.national_id')}
name='nationalId'
value={formik.values.nationalId}
name='identificationNumber'
value={formik.values.identificationNumber}
onChange={formik.handleChange}
error_text={formik.touched.nationalId && formik.errors.nationalId ? formik.errors.nationalId : ''}
error_text={formik.touched.identificationNumber && formik.errors.identificationNumber ? formik.errors.identificationNumber : ''}
/>
<DatePickerComponent
label={t('company.establishment_date')}
placeholder='تاریخ تاسیس'
onChange={(date) => formik.setFieldValue('dateOfEstablishment', moment(date).format('YYYY-MM-DD'))}
error_text={formik.touched.dateOfEstablishment && formik.errors.dateOfEstablishment ? formik.errors.dateOfEstablishment : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.establishment_date')}
name='establishmentDate'
value={formik.values.establishmentDate}
onChange={formik.handleChange}
error_text={formik.touched.establishmentDate && formik.errors.establishmentDate ? formik.errors.establishmentDate : ''}
/>
<Input
label={t('company.map_link')}
name='mapLink'
value={formik.values.mapLink}
name='mapAddressLink'
value={formik.values.mapAddressLink}
onChange={formik.handleChange}
error_text={formik.touched.mapLink && formik.errors.mapLink ? formik.errors.mapLink : ''}
error_text={formik.touched.mapAddressLink && formik.errors.mapAddressLink ? formik.errors.mapAddressLink : ''}
/>
</div>
@@ -144,16 +253,21 @@ const CreateCompany: FC = () => {
<div className='mt-6'>
<Input
label={t('company.short_description')}
name='shortDescription'
value={formik.values.shortDescription}
name='description'
value={formik.values.description}
onChange={formik.handleChange}
error_text={formik.touched.shortDescription && formik.errors.shortDescription ? formik.errors.shortDescription : ''}
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
/>
</div>
<CompanyServices value={services} onChange={setServices} />
<CompanyProducts value={products} onChange={setProducts} />
</div>
</div>
<CreateSidebar />
<CreateSidebar formik={formik} setProfileImage={setProfileImage} setCoverImage={setCoverImage} />
</div>
</div>
)
+72
View File
@@ -0,0 +1,72 @@
import { FC } from 'react'
import { useGetCompanies } from './hooks/useCompanyData'
import Td from '../../components/Td'
import PageLoading from '../../components/PageLoading'
import { useTranslation } from 'react-i18next'
import { CompanyItemType } from './types/CompanyTypes'
import moment from 'moment-jalaali'
import { Edit } from 'iconsax-react'
import DeleteCompany from './components/DeleteCompany'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import ToggleStatusCompany from './components/ToggleStatus'
const CompanyList: FC = () => {
const { t } = useTranslation('global')
const { data, isLoading, refetch } = useGetCompanies(1)
return (
<div>
{
isLoading ?
<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('company.company_name')} />
<Td text={t('company.ceo_name')} />
<Td text={t('company.industry')} />
<Td text={t('company.invoice_count')} />
<Td text={t('company.status')} />
<Td text={t('company.created_at')} />
<Td text={''} />
</tr>
</thead>
<tbody>
{
data?.data?.companies?.map((item: CompanyItemType) => {
return <tr className='tr' key={item.id}>
<Td text={item.name} />
<Td text={item.user?.firstName + ' ' + item.user?.lastName} />
<Td text={item.industry?.title} />
<Td text={item.invoiceCount.toString()} />
<Td text=''>
<ToggleStatusCompany
defaultActive={item.isActive}
id={item.id}
/>
</Td>
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
<Td text={''}>
<div className='flex gap-2'>
<Link to={Pages.company.detail + item.id}>
<Edit size={20} color='#888' />
</Link>
<DeleteCompany refetch={() => refetch()} id={item.id} />
</div>
</Td>
</tr>
})
}
</tbody>
</table>
</div>
}
</div>
)
}
export default CompanyList
+378
View File
@@ -0,0 +1,378 @@
import { FC, useEffect } from 'react'
import Button from '../../components/Button'
import { TickCircle } from 'iconsax-react'
import Input from '../../components/Input'
import { useTranslation } from 'react-i18next'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { CreateCompanyType } from './types/CompanyTypes'
import { useState } from 'react'
import { useMultiUpload, useSingleUpload } from '../service/hooks/useServiceData'
import { useGetCompanyDetail, useUpdateCompany } from './hooks/useCompanyData'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { Pages } from '../../config/Pages'
import { useNavigate, useParams } from 'react-router-dom'
import CompanyServices from './components/CompanyServices'
import CompanyProducts from './components/CompanyProducts'
import DatePickerComponent from '../../components/DatePicker'
import moment from 'moment-jalaali'
import UpdateSidebar from './components/UpdateSideBar'
type ServiceType = {
id: string;
title: string;
imageUrl: string;
company: string;
}
type ProductType = {
id: string;
title: string;
imageUrl: string;
company: string;
}
const UpdateCompany: FC = () => {
const navigate = useNavigate()
const { id } = useParams()
const { t } = useTranslation('global')
const [profileImage, setProfileImage] = useState<File | null>(null)
const [coverImage, setCoverImage] = useState<File | null>(null)
const multiUpload = useMultiUpload()
const singleUpload = useSingleUpload()
const updateCompany = useUpdateCompany()
const { data: companyDetail } = useGetCompanyDetail(id)
const formik = useFormik<CreateCompanyType>({
initialValues: {
phone: '',
firstName: '',
lastName: '',
nationalCode: '',
password: '',
name: '',
email: '',
identificationNumber: '',
dateOfEstablishment: '',
address: '',
mapAddressLink: '',
description: '',
websiteUrl: '',
profileImageUrl: '',
coverImageUrl: '',
isActive: true,
industryId: '',
products: [],
services: [],
},
validationSchema: Yup.object({
phone: Yup.string().required(t('errors.required')),
firstName: Yup.string().required(t('errors.required')),
lastName: Yup.string().required(t('errors.required')),
nationalCode: Yup.string().required(t('errors.required')),
name: Yup.string().required(t('errors.required')),
email: Yup.string().email(t('errors.invalid_email')).required(t('errors.required')),
identificationNumber: Yup.string().required(t('errors.required')),
dateOfEstablishment: Yup.string().required(t('errors.required')),
address: Yup.string().required(t('errors.required')),
mapAddressLink: Yup.string().url(t('errors.invalid_url')),
description: Yup.string().required(t('errors.required')),
websiteUrl: Yup.string().url(t('errors.invalid_url')).required(t('errors.required')),
isActive: Yup.boolean().required(t('errors.required')),
industryId: Yup.string().required(t('errors.required')),
// products and services validation can be added if needed
}),
onSubmit: async (values) => {
// آپلود تصاویر خدمات
let serviceImages: string[] = []
const serviceImageIndices: number[] = []
if (services.length > 0) {
const formDataServices = new FormData()
services.forEach((s, index) => {
if (s.image) {
formDataServices.append('files', s.image)
serviceImageIndices.push(index)
}
})
// فقط تصاویر جدید را آپلود کن
if (formDataServices.getAll('files').length > 0) {
const serviceUploadRes = await multiUpload.mutateAsync(formDataServices)
serviceImages = serviceUploadRes.data.map((item: { url: string }) => item?.url)
}
}
// آپلود تصاویر محصولات
let productImages: string[] = []
const productImageIndices: number[] = []
if (products.length > 0) {
const formDataProducts = new FormData()
products.forEach((p, index) => {
if (p.image) {
formDataProducts.append('files', p.image)
productImageIndices.push(index)
}
})
// فقط تصاویر جدید را آپلود کن
if (formDataProducts.getAll('files').length > 0) {
const productUploadRes = await multiUpload.mutateAsync(formDataProducts)
productImages = productUploadRes.data.map((item: { url: string }) => item?.url)
}
}
// مقداردهی به values
values.services = services.map((s, idx) => {
if (s.existingImageUrl) {
return {
title: s.title,
imageUrl: s.existingImageUrl
}
}
if (s.image) {
const imageIndex = serviceImageIndices.indexOf(idx)
if (imageIndex !== -1 && serviceImages[imageIndex]) {
return {
title: s.title,
imageUrl: serviceImages[imageIndex]
}
}
}
return {
title: s.title,
imageUrl: ''
}
})
values.products = products.map((p, idx) => {
if (p.existingImageUrl) {
return {
title: p.title,
imageUrl: p.existingImageUrl
}
}
if (p.image) {
const imageIndex = productImageIndices.indexOf(idx)
if (imageIndex !== -1 && productImages[imageIndex]) {
return {
title: p.title,
imageUrl: productImages[imageIndex]
}
}
}
return {
title: p.title,
imageUrl: ''
}
})
if (profileImage) {
const formDataProfile = new FormData()
formDataProfile.append('file', profileImage)
const profileUploadRes = await singleUpload.mutateAsync(formDataProfile)
values.profileImageUrl = profileUploadRes.data.url
}
if (coverImage) {
const formDataCover = new FormData()
formDataCover.append('file', coverImage)
const coverUploadRes = await singleUpload.mutateAsync(formDataCover)
values.coverImageUrl = coverUploadRes.data.url
}
await updateCompany.mutateAsync({ id: id || '', params: values }, {
onSuccess: () => {
toast.success('شرکت با موفقیت ثبت شد')
navigate(Pages.company.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
})
useEffect(() => {
if (companyDetail) {
formik.setValues(companyDetail?.data?.company)
formik.setFieldValue('dateOfEstablishment', moment(companyDetail?.data?.company?.dateOfEstablishment).format('YYYY-MM-DD'))
formik.setFieldValue('firstName', companyDetail?.data?.company?.user?.firstName)
formik.setFieldValue('lastName', companyDetail?.data?.company?.user?.lastName)
formik.setFieldValue('nationalCode', companyDetail?.data?.company?.user?.nationalCode)
formik.setFieldValue('phone', companyDetail?.data?.company?.user?.phone)
formik.setFieldValue('email', companyDetail?.data?.company?.user?.email)
formik.setFieldValue('address', companyDetail?.data?.company?.address)
formik.setFieldValue("industryId", companyDetail?.data?.company?.industry?.id)
// ذخیره سرویس‌ها با URL تصاویر موجود
setServices(companyDetail?.data?.company?.services?.map((s: ServiceType) => ({
title: s.title,
existingImageUrl: s.imageUrl
})) || [])
// ذخیره محصولات با URL تصاویر موجود
setProducts(companyDetail?.data?.company?.products?.map((p: ProductType) => ({
title: p.title,
existingImageUrl: p.imageUrl
})) || [])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [companyDetail])
const [services, setServices] = useState<{ title: string; image?: File; existingImageUrl?: string }[]>([])
const [products, setProducts] = useState<{ title: string; image?: File; existingImageUrl?: string }[]>([])
console.log(formik.errors);
return (
<div className='mt-4'>
<div className='flex justify-between items-center'>
<div>
ویرایش شرکت
</div>
<Button
className='w-fit px-4'
onClick={() => formik.handleSubmit()}
isLoading={updateCompany.isPending || singleUpload.isPending || multiUpload.isPending}
>
<div className='flex items-center gap-2'>
<TickCircle size={20} color='white' />
<div>ثبت شرکت</div>
</div>
</Button>
</div>
<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'>
<div>اطلاعات شرکت</div>
<div className='mt-6'>
<Input
label='نام شرکت'
name='name'
value={formik.values.name}
onChange={formik.handleChange}
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label='نام'
name='firstName'
value={formik.values.firstName}
onChange={formik.handleChange}
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''}
/>
<Input
label='نام خانوادگی'
name='lastName'
value={formik.values.lastName}
onChange={formik.handleChange}
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label='کد ملی'
name='nationalCode'
value={formik.values.nationalCode}
onChange={formik.handleChange}
error_text={formik.touched.nationalCode && formik.errors.nationalCode ? formik.errors.nationalCode : ''}
/>
<Input
label='رمز عبور'
name='password'
type='password'
value={formik.values.password}
onChange={formik.handleChange}
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.phone_number')}
name='phone'
value={formik.values.phone}
onChange={formik.handleChange}
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ''}
/>
<Input
label={t('company.email')}
name='email'
value={formik.values.email}
onChange={formik.handleChange}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : ''}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.national_id')}
name='identificationNumber'
value={formik.values.identificationNumber}
onChange={formik.handleChange}
error_text={formik.touched.identificationNumber && formik.errors.identificationNumber ? formik.errors.identificationNumber : ''}
/>
<DatePickerComponent
label={t('company.establishment_date')}
placeholder='تاریخ تاسیس'
onChange={(date) => formik.setFieldValue('dateOfEstablishment', moment(date).format('YYYY-MM-DD'))}
error_text={formik.touched.dateOfEstablishment && formik.errors.dateOfEstablishment ? formik.errors.dateOfEstablishment : ''}
defaulValue={formik.values.dateOfEstablishment}
/>
</div>
<div className='rowTwoInput mt-6'>
<Input
label={t('company.map_link')}
name='mapAddressLink'
value={formik.values.mapAddressLink}
onChange={formik.handleChange}
error_text={formik.touched.mapAddressLink && formik.errors.mapAddressLink ? formik.errors.mapAddressLink : ''}
/>
</div>
<div className='mt-6'>
<Input
label={t('company.address')}
name='address'
value={formik.values.address}
onChange={formik.handleChange}
error_text={formik.touched.address && formik.errors.address ? formik.errors.address : ''}
/>
</div>
<div className='mt-6'>
<Input
label={t('company.short_description')}
name='description'
value={formik.values.description}
onChange={formik.handleChange}
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
/>
</div>
<CompanyServices value={services} onChange={setServices} />
<CompanyProducts value={products} onChange={setProducts} />
</div>
</div>
<UpdateSidebar formik={formik} setProfileImage={setProfileImage} setCoverImage={setCoverImage} />
</div>
</div>
)
}
export default UpdateCompany
@@ -0,0 +1,85 @@
import { FC, useState } from 'react'
import Input from '../../../components/Input'
import UploadBox from '../../../components/UploadBox'
import { Add, CloseCircle } from 'iconsax-react'
type Product = { title: string; image?: File; existingImageUrl?: string }
type Props = {
value: Product[]
onChange: (val: Product[]) => void
}
const CompanyProducts: FC<Props> = ({ value, onChange }) => {
const [productTitle, setProductTitle] = useState('')
const [productImage, setProductImage] = useState<File | undefined>(undefined)
const [productError, setProductError] = useState('')
const handleAddProduct = () => {
if (!productTitle.trim()) {
setProductError('عنوان محصول الزامی است')
return
}
if (!productImage) {
setProductError('تصویر محصول الزامی است')
return
}
if (value.some(s => s.title.trim() === productTitle.trim())) {
setProductError('عنوان محصول تکراری است')
return
}
onChange([...value, { title: productTitle.trim(), image: productImage }])
setProductTitle('')
setProductImage(undefined)
setProductError('')
}
const handleRemoveProduct = (idx: number) => {
onChange(value.filter((_, i) => i !== idx))
}
return (
<div className='mt-8'>
<div className='mb-2'>محصولات شرکت</div>
<div className='rowTwoInput items-end gap-4'>
<div className='w-full'>
<Input
label='عنوان محصول'
name='productTitle'
value={productTitle}
onChange={e => setProductTitle(e.target.value)}
/>
</div>
<div className='w-full'>
<UploadBox
label='تصویر'
onChange={files => setProductImage(files[0])}
isMultiple={false}
isReset={!productImage && !productTitle}
/>
</div>
<div onClick={handleAddProduct} className='size-10 min-w-10 flex items-center justify-center border-black rounded-xl border'>
<Add size={22} color='black' />
</div>
</div>
{productError && <div className='text-xs text-red-600 mt-2'>{productError}</div>}
{/* نمایش لیست محصولات */}
<div className='mt-4 flex gap-2'>
{value.map((prd, idx) => (
<div key={idx} className='rowTwoInput border p-2 rounded-xl items-center gap-2'>
<div className='flex items-center gap-2'>
{prd.image && <img src={URL.createObjectURL(prd.image)} alt='' className='size-6 min-w-6 rounded-full object-cover' />}
{prd.existingImageUrl && !prd.image && <img src={prd.existingImageUrl} alt='' className='size-6 min-w-6 rounded-full object-cover' />}
</div>
<div className='w-full text-xs'>{prd.title}</div>
<button type='button' onClick={() => handleRemoveProduct(idx)} className='ml-auto'>
<CloseCircle size={16} color='red' />
</button>
</div>
))}
</div>
</div>
)
}
export default CompanyProducts
@@ -0,0 +1,85 @@
import { FC, useState } from 'react'
import Input from '../../../components/Input'
import UploadBox from '../../../components/UploadBox'
import { Add, CloseCircle } from 'iconsax-react'
type Service = { title: string; image?: File; existingImageUrl?: string }
type Props = {
value: Service[]
onChange: (val: Service[]) => void
}
const CompanyServices: FC<Props> = ({ value, onChange }) => {
const [serviceTitle, setServiceTitle] = useState('')
const [serviceImage, setServiceImage] = useState<File | undefined>(undefined)
const [serviceError, setServiceError] = useState('')
const handleAddService = () => {
if (!serviceTitle.trim()) {
setServiceError('عنوان خدمت الزامی است')
return
}
if (!serviceImage) {
setServiceError('تصویر خدمت الزامی است')
return
}
if (value.some(s => s.title.trim() === serviceTitle.trim())) {
setServiceError('عنوان خدمت تکراری است')
return
}
onChange([...value, { title: serviceTitle.trim(), image: serviceImage }])
setServiceTitle('')
setServiceImage(undefined)
setServiceError('')
}
const handleRemoveService = (idx: number) => {
onChange(value.filter((_, i) => i !== idx))
}
return (
<div className='mt-8'>
<div className='mb-2'>خدمات شرکت</div>
<div className='rowTwoInput items-end gap-4'>
<div className='w-full'>
<Input
label='عنوان خدمت'
name='serviceTitle'
value={serviceTitle}
onChange={e => setServiceTitle(e.target.value)}
/>
</div>
<div className='w-full'>
<UploadBox
label='تصویر'
onChange={files => setServiceImage(files[0])}
isMultiple={false}
isReset={!serviceImage && !serviceTitle}
/>
</div>
<div onClick={handleAddService} className='size-10 min-w-10 flex items-center justify-center border-black rounded-xl border'>
<Add size={22} color='black' />
</div>
</div>
{serviceError && <div className='text-xs text-red-600 mt-2'>{serviceError}</div>}
{/* نمایش لیست خدمات */}
<div className='mt-4 flex gap-2'>
{value.map((srv, idx) => (
<div key={idx} className='rowTwoInput border p-2 rounded-xl items-center gap-2'>
<div className='flex items-center gap-2'>
{srv.image && <img src={URL.createObjectURL(srv.image)} alt='' className='size-6 min-w-6 rounded-full object-cover' />}
{srv.existingImageUrl && !srv.image && <img src={srv.existingImageUrl} alt='' className='size-6 min-w-6 rounded-full object-cover' />}
</div>
<div className='w-full text-xs'>{srv.title}</div>
<button type='button' onClick={() => handleRemoveService(idx)} className='ml-auto'>
<CloseCircle size={16} color='red' />
</button>
</div>
))}
</div>
</div>
)
}
export default CompanyServices
@@ -53,7 +53,7 @@ const CreateIndustry: FC<Props> = ({ 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='bg-white 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'>
وضعیت صنعت
+59 -6
View File
@@ -1,8 +1,24 @@
import { FC } from 'react'
import SwitchComponent from '../../../components/Switch'
import Input from '../../../components/Input'
import { useGetIndustries } from '../hooks/useCompanyData'
import { useTranslation } from 'react-i18next'
import CheckBoxComponent from '../../../components/CheckBoxComponent'
import { CreateCompanyType, IndustryItemType } from '../types/CompanyTypes'
import { FormikProps } from 'formik'
import UploadBoxDraggble from '../../../components/UploadBoxDraggble'
type Props = {
formik: FormikProps<CreateCompanyType>
setProfileImage: (image: File) => void
setCoverImage: (image: File) => void
}
const CreateSidebar: FC<Props> = ({ formik, setProfileImage, setCoverImage }) => {
const { t } = useTranslation('global')
const { data: industries } = useGetIndustries(1, '', '')
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 items-center'>
@@ -10,17 +26,54 @@ const CreateSidebar: FC = () => {
وضعیت شرکت
</div>
<SwitchComponent
active={true}
onChange={() => { }}
active={formik.values.isActive}
onChange={(value) => formik.setFieldValue('isActive', value)}
/>
</div>
<div className='mt-6'>
<Input
label='آدرس وبسایت شرکت'
name='name'
onChange={() => { }}
error_text=''
name='websiteUrl'
value={formik.values.websiteUrl}
onChange={formik.handleChange}
error_text={formik.touched.websiteUrl && formik.errors.websiteUrl ? formik.errors.websiteUrl : ''}
/>
</div>
<div className='mt-8'>
<div className='text-sm'>{t('company.industry')}</div>
<div className='mt-2 border border-border rounded-xl p-4'>
{
industries?.data?.industries?.map((item: IndustryItemType) => {
return (
<div key={item.id} className='flex items-center'>
<CheckBoxComponent
checked={formik.values.industryId === item.id}
onChange={(e) => formik.setFieldValue('industryId', e.target.checked ? item.id : '')}
/>
<div>{item.title}</div>
</div>
)
})
}
</div>
</div>
<div className='mt-8'>
<div className='text-sm mb-2'>{t('company.profile_image')}</div>
<UploadBoxDraggble
label={t('company.profile_image')}
onChange={(files) => setProfileImage(files[0])}
/>
</div>
<div className='mt-8'>
<div className='text-sm mb-2'>{t('company.cover_image')}</div>
<UploadBoxDraggble
label={t('company.cover_image')}
onChange={(files) => setCoverImage(files[0])}
/>
</div>
</div>
@@ -0,0 +1,43 @@
import { Trash } from 'iconsax-react'
import { FC, useState } from 'react'
import { useDeleteCompany } from '../hooks/useCompanyData'
import ModalConfrim from '../../../components/ModalConfrim'
import { ErrorType } from '../../../helpers/types'
import { toast } from 'react-toastify'
type Props = {
id: string,
refetch: () => void,
}
const DeleteCompany: FC<Props> = ({ id, refetch }) => {
const [isOpen, setIsOpen] = useState<boolean>(false)
const deleteCompany = useDeleteCompany()
const handleConfrim = () => {
deleteCompany.mutate(id, {
onSuccess: () => {
refetch()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
return (
<div>
<Trash onClick={() => setIsOpen(true)} size={20} color='#888' />
<ModalConfrim
close={() => setIsOpen(false)}
isOpen={isOpen}
onConfrim={handleConfrim}
isLoading={deleteCompany.isPending}
/>
</div>
)
}
export default DeleteCompany
+1 -1
View File
@@ -32,7 +32,7 @@ const Industries: FC = () => {
return (
<div className='mt-4'>
<div>
{t('service.cateory_services')}
{t('company.industries')}
</div>
<div className='flex gap-6 mt-9'>
@@ -0,0 +1,28 @@
import { FC, useState } from 'react'
import SwitchComponent from '../../../components/Switch'
import { useToggleStatusCompany } from '../hooks/useCompanyData'
type Props = {
defaultActive: boolean,
id: string
}
const ToggleStatusCompany: FC<Props> = (props: Props) => {
const toggleStatus = useToggleStatusCompany()
const [isActive, setIsActive] = useState<boolean>(props.defaultActive)
const handleChange = (value: boolean) => {
setIsActive(value)
toggleStatus.mutate(props.id)
}
return (
<SwitchComponent
active={isActive}
onChange={handleChange}
/>
)
}
export default ToggleStatusCompany
@@ -0,0 +1,83 @@
import { FC } from 'react'
import SwitchComponent from '../../../components/Switch'
import Input from '../../../components/Input'
import { useGetIndustries } from '../hooks/useCompanyData'
import { useTranslation } from 'react-i18next'
import CheckBoxComponent from '../../../components/CheckBoxComponent'
import { CreateCompanyType, IndustryItemType } from '../types/CompanyTypes'
import { FormikProps } from 'formik'
import UploadBoxDraggble from '../../../components/UploadBoxDraggble'
type Props = {
formik: FormikProps<CreateCompanyType>
setProfileImage: (image: File) => void
setCoverImage: (image: File) => void
}
const UpdateSidebar: FC<Props> = ({ formik, setProfileImage, setCoverImage }) => {
const { t } = useTranslation('global')
const { data: industries } = useGetIndustries(1, '', '')
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='آدرس وبسایت شرکت'
name='websiteUrl'
value={formik.values.websiteUrl}
onChange={formik.handleChange}
error_text={formik.touched.websiteUrl && formik.errors.websiteUrl ? formik.errors.websiteUrl : ''}
/>
</div>
<div className='mt-8'>
<div className='text-sm'>{t('company.industry')}</div>
<div className='mt-2 border border-border rounded-xl p-4'>
{
industries?.data?.industries?.map((item: IndustryItemType) => {
return (
<div key={item.id} className='flex items-center'>
<CheckBoxComponent
checked={formik.values.industryId === item.id}
onChange={(e) => formik.setFieldValue('industryId', e.target.checked ? item.id : '')}
/>
<div>{item.title}</div>
</div>
)
})
}
</div>
</div>
<div className='mt-8'>
<div className='text-sm mb-2'>{t('company.profile_image')}</div>
<UploadBoxDraggble
label={t('company.profile_image')}
onChange={(files) => setProfileImage(files[0])}
/>
</div>
<div className='mt-8'>
<div className='text-sm mb-2'>{t('company.cover_image')}</div>
<UploadBoxDraggble
label={t('company.cover_image')}
onChange={(files) => setCoverImage(files[0])}
/>
</div>
</div>
)
}
export default UpdateSidebar
+41 -1
View File
@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/CompanyService";
import { CreateIndustryType } from "../types/CompanyTypes";
import { CreateIndustryType, CreateCompanyType } from "../types/CompanyTypes";
export const useGetIndustries = (
page: number,
search: string,
@@ -43,3 +43,43 @@ export const useGetIndustryDetail = (id: string) => {
queryFn: () => api.getIndustryDetail(id),
});
};
export const useCreateCompany = () => {
return useMutation({
mutationFn: (params: CreateCompanyType) => api.createCompany(params),
});
};
export const useGetCompanies = (page: number) => {
return useQuery({
queryKey: ["companies", page],
queryFn: () => api.getCompanies(page),
});
};
export const useDeleteCompany = () => {
return useMutation({
mutationFn: (id: string) => api.deleteCompany(id),
});
};
export const useGetCompanyDetail = (id?: string) => {
return useQuery({
queryKey: ["company-detail", id],
queryFn: () => api.getCompanyDetail(id || ""),
enabled: !!id,
});
};
export const useUpdateCompany = () => {
return useMutation({
mutationFn: ({ id, params }: { id: string; params: CreateCompanyType }) =>
api.updateCompany(id, params),
});
};
export const useToggleStatusCompany = () => {
return useMutation({
mutationFn: (id: string) => api.toggleStatusCompany(id),
});
};
+31 -1
View File
@@ -1,5 +1,5 @@
import axios from "../../../config/axios";
import { CreateIndustryType } from "../types/CompanyTypes";
import { CreateCompanyType, CreateIndustryType } from "../types/CompanyTypes";
export const getIndustries = async (
page: number,
@@ -41,3 +41,33 @@ export const getIndustryDetail = async (id: string) => {
const { data } = await axios.get(`/industries/${id}`);
return data;
};
export const createCompany = async (params: CreateCompanyType) => {
const { data } = await axios.post("/companies", params);
return data;
};
export const getCompanies = async (page: number) => {
const { data } = await axios.get(`/companies/list?page=${page}`);
return data;
};
export const deleteCompany = async (id: string) => {
const { data } = await axios.delete(`/companies/${id}`);
return data;
};
export const getCompanyDetail = async (id: string) => {
const { data } = await axios.get(`/companies/${id}`);
return data;
};
export const updateCompany = async (id: string, params: CreateCompanyType) => {
const { data } = await axios.patch(`/companies/${id}`, params);
return data;
};
export const toggleStatusCompany = async (id: string) => {
const { data } = await axios.patch(`/companies/${id}/toggle-status`);
return data;
};
+31 -2
View File
@@ -13,9 +13,12 @@ export type IndustryItemType = {
};
export type CreateCompanyType = {
name: string;
chiefExecutive: string;
phone: string;
firstName: string;
lastName: string;
nationalCode: string;
password: string;
name: string;
email: string;
identificationNumber: string;
dateOfEstablishment: string;
@@ -27,4 +30,30 @@ export type CreateCompanyType = {
coverImageUrl: string;
isActive: boolean;
industryId: string;
products: {
title: string;
imageUrl: string;
}[];
services: {
title: string;
imageUrl: string;
}[];
};
export type CompanyItemType = {
id: string;
createdAt: string;
name: string;
isActive: boolean;
status: string;
industry: {
id: string;
title: string;
};
user: {
id: string;
firstName: string;
lastName: string;
};
invoiceCount: number;
};
+6 -2
View File
@@ -71,12 +71,14 @@ 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'
import UpdateCompany from '../pages/company/Update'
const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore()
return (
<div className='p-4 overflow-hidden'>
<div className='p-4 overflow-hidden h-screen'>
<SideBar />
<Header />
<div className={clx(
@@ -84,7 +86,7 @@ const MainRouter: FC = () => {
hasSubMenu && 'xl:ms-[305px]',
)}>
<div className={clx(
`overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`,
`overflow-auto h-[calc(100vh-113px)] main-content-scroll`,
)}>
<div>
<Routes>
@@ -153,6 +155,8 @@ const MainRouter: FC = () => {
<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 />} />
<Route path={Pages.company.detail + ':id'} element={<UpdateCompany />} />
</Routes>
</div>
</div>
+11 -8
View File
@@ -2,7 +2,7 @@ import { FC, useEffect } from 'react'
import LogoImage from '../assets/images/logo.svg'
import LogoSmall from '../assets/images/logo-small.svg'
import { useTranslation } from 'react-i18next'
import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Headphone, Home2, Logout, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare } from 'iconsax-react'
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 SideBarItem from './SideBarItem'
import { useLocation } from 'react-router-dom'
import { Pages } from '../config/Pages'
@@ -18,6 +18,7 @@ import LearningSubMenu from './components/LearningSubMenu'
import BlogSubMenu from './components/BlogSubMenu'
import { useGetProfile } from '../pages/profile/hooks/useProfileData'
import SupportSubMenu from './components/SupportSubMenu'
import CompanySubMenu from './components/CompanySubMenu'
const SideBar: FC = () => {
const { t } = useTranslation('global')
@@ -86,13 +87,13 @@ const SideBar: FC = () => {
link={Pages.dashboard}
activeName=''
/>
<SideBarItem
icon={<Element3 variant={isActive('services') ? 'Bold' : 'Outline'} color={isActive('services') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('sidebar.services')}
isActive={isActive('services')}
link={Pages.services.list}
name='services'
activeName='services'
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
@@ -327,7 +328,9 @@ const SideBar: FC = () => {
<BlogSubMenu />
: subMenuName === 'support' ?
<SupportSubMenu />
: null
: subMenuName === 'company' ?
<CompanySubMenu />
: null
}
</div>
}
+50
View File
@@ -0,0 +1,50 @@
import { Building } from 'iconsax-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { Pages } from '../../config/Pages'
const CompanySubMenu: FC = () => {
const { t } = useTranslation('global')
const isActive = (name: string) => {
return location.pathname.includes(name)
}
return (
<div className='py-12'>
<div className='flex gap-3 items-center border-b pb-10 px-6'>
<Building
size={24}
color='#000'
variant='Bold'
/>
<div className='text-xs'>
{t('sidebar.company')}
</div>
</div>
<div className='flex flex-col mt-10 gap-4'>
<SubMenuItem
title={'لیست'}
isActive={isActive('list')}
link={Pages.company.list}
/>
<SubMenuItem
title={'ایجاد'}
isActive={isActive('create')}
link={Pages.company.create}
/>
<SubMenuItem
title={'صنعت'}
isActive={isActive('industries')}
link={Pages.company.industries}
/>
</div>
</div>
)
}
export default CompanySubMenu