create request company
This commit is contained in:
+6
-2
@@ -30,7 +30,7 @@ declare global {
|
||||
function App() {
|
||||
const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: async (error: IApiErrorRepsonse) => {
|
||||
onError: async (error: IApiErrorRepsonse, query) => {
|
||||
if (error?.response?.status === 401) {
|
||||
try {
|
||||
// Use a flag to track if refresh is in progress
|
||||
@@ -63,8 +63,12 @@ function App() {
|
||||
await setRefreshToken(data.refreshToken?.token);
|
||||
}
|
||||
|
||||
// Retry the failed request
|
||||
// Retry the failed request, even if it was muted
|
||||
if (query) {
|
||||
queryClient.invalidateQueries({ queryKey: query.queryKey });
|
||||
} else {
|
||||
queryClient.invalidateQueries();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
throw new Error('Invalid token response');
|
||||
|
||||
@@ -6,7 +6,7 @@ type Props = {
|
||||
className?: string;
|
||||
isLoading?: boolean,
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
XOR<{ children: ReactNode }, { label: string, icon?: string, isLoading?: boolean }>;
|
||||
XOR<{ children: ReactNode }, { label: string, icon?: string }>;
|
||||
const Button: FC<Props> = memo((props: Props) => {
|
||||
const buttonClass = clx(
|
||||
'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary text-white w-full',
|
||||
@@ -15,7 +15,6 @@ const Button: FC<Props> = memo((props: Props) => {
|
||||
);
|
||||
return (
|
||||
<button {...props} className={`${buttonClass} ${props.className}`} >
|
||||
<img src={props.icon} alt="" />
|
||||
{
|
||||
props.isLoading ?
|
||||
<ReactLoading type='spin' color={'white'} width={20} height={20} />
|
||||
|
||||
@@ -1,22 +1,57 @@
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { CloseCircle } from 'iconsax-react'
|
||||
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
isMultiple?: boolean,
|
||||
onChange?: (file: File[]) => void;
|
||||
isReset?: boolean;
|
||||
}
|
||||
|
||||
const UploadBox: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFile(acceptedFiles[0])
|
||||
}, [])
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array);
|
||||
}
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
if (props.onChange) {
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
const array = [...files]
|
||||
array.splice(index, 1)
|
||||
setFiles(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (props.isReset) {
|
||||
setFiles([])
|
||||
}
|
||||
|
||||
}, [props.isReset])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -28,15 +63,27 @@ const UploadBox: FC<Props> = (props: Props) => {
|
||||
{t('select_file')}
|
||||
</button>
|
||||
|
||||
<div className='text-description text-xs'>
|
||||
<div className='lg:flex gap-2 hidden'>
|
||||
{
|
||||
file ?
|
||||
file.name
|
||||
:
|
||||
t('no_select_file')
|
||||
files?.map((item, index) => (
|
||||
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||
<div className='text-xs'>{item.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='lg:hidden gap-2 flex flex-wrap mt-4'>
|
||||
{
|
||||
files?.map((item, index) => (
|
||||
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||
<div className='text-xs'>{item.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,4 +41,5 @@ export const Pages = {
|
||||
reportCard: {
|
||||
list: "report-card/list",
|
||||
},
|
||||
request: "request",
|
||||
};
|
||||
|
||||
+54
-2
@@ -1,5 +1,13 @@
|
||||
import axios from "axios";
|
||||
import { getToken } from "./func";
|
||||
import {
|
||||
getToken,
|
||||
getRefreshToken,
|
||||
setToken,
|
||||
setRefreshToken,
|
||||
removeToken,
|
||||
removeRefreshToken,
|
||||
} from "./func";
|
||||
import { refreshToken } from "../pages/auth/service/AuthService";
|
||||
// import { Pages } from "./Pages";
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
@@ -8,7 +16,51 @@ const axiosInstance = axios.create({
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error?.response?.status === 401 && !originalRequest._retry) {
|
||||
if (window.isRefreshingToken) {
|
||||
// Wait for the refresh to complete
|
||||
await new Promise((resolve) => {
|
||||
const checkComplete = setInterval(() => {
|
||||
if (!window.isRefreshingToken) {
|
||||
clearInterval(checkComplete);
|
||||
resolve(true);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
return axiosInstance(originalRequest);
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
window.isRefreshingToken = true;
|
||||
|
||||
try {
|
||||
const refreshTokenValue = await getRefreshToken();
|
||||
const { data } = await refreshToken({
|
||||
refreshToken: refreshTokenValue || "",
|
||||
});
|
||||
|
||||
if (data?.accessToken?.token) {
|
||||
await setToken(data?.accessToken?.token);
|
||||
if (data.refreshToken?.token) {
|
||||
await setRefreshToken(data.refreshToken?.token);
|
||||
}
|
||||
|
||||
originalRequest.headers.Authorization = `Bearer ${data.accessToken.token}`;
|
||||
return axiosInstance(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
await removeToken();
|
||||
await removeRefreshToken();
|
||||
window.location.href = "/auth/login";
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
window.isRefreshingToken = false;
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,12 +3,9 @@ import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { getToken } from '../config/func'
|
||||
import { buildPath } from '../helpers/utils'
|
||||
|
||||
// تایپ برای کامپوننتهای React
|
||||
type ComponentType = FC<any>
|
||||
|
||||
// HOC برای محافظت از کامپوننتهای نیازمند لاگین
|
||||
const withAuth = (WrappedComponent: ComponentType): ComponentType => {
|
||||
// کامپوننت جدید که لاگین را چک میکند
|
||||
const WithAuthComponent: FC<any> = (props) => {
|
||||
const navigate = useNavigate()
|
||||
const { slug } = useParams()
|
||||
@@ -25,11 +22,9 @@ const withAuth = (WrappedComponent: ComponentType): ComponentType => {
|
||||
checkAuth()
|
||||
}, [navigate, slug])
|
||||
|
||||
// کامپوننت اصلی را با props خودش رندر کن
|
||||
return <WrappedComponent {...props} />
|
||||
}
|
||||
|
||||
// نام نمایشی کامپوننت را برای دیباگ بهتر تنظیم کن
|
||||
WithAuthComponent.displayName = `WithAuth(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
|
||||
|
||||
return WithAuthComponent
|
||||
|
||||
+36
-1
@@ -320,5 +320,40 @@
|
||||
"title_course": "عنوان دوره"
|
||||
},
|
||||
"status": "وضعیت",
|
||||
"download": "دانلود"
|
||||
"download": "دانلود",
|
||||
"company": {
|
||||
"count_company": "تعداد شرکت ها",
|
||||
"company_name": "نام شرکت",
|
||||
"ceo_name": "نام مدیر عامل",
|
||||
"ceo_first_name": "نام مدیر عامل",
|
||||
"ceo_last_name": "نام خانوادگی مدیر عامل",
|
||||
"phone_number": "شماره تماس",
|
||||
"email": "ایمیل",
|
||||
"national_id": "شناسه ملی",
|
||||
"establishment_date": "تاریخ تاسیس",
|
||||
"address": "آدرس",
|
||||
"map_link": "لینک نقشه شرکت",
|
||||
"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": "تعداد صورتحساب",
|
||||
"select_company": "انتخاب شرکت",
|
||||
"register_date": "تاریخ ثبت نام",
|
||||
"company": "شرکت"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { FC } from 'react'
|
||||
import withAuth from '../../hoc/withAuth'
|
||||
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 { useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import moment from 'moment-jalaali'
|
||||
import { useCreateCompany, useMultiUpload, useSingleUpload } from './hooks/useRequestData'
|
||||
import { CreateCompanyType } from './types/Types'
|
||||
import UploadBox from '../../components/UploadBox'
|
||||
import CompanyServices from './components/CompanyServices'
|
||||
import CompanyProducts from './components/CompanyProducts'
|
||||
import { useGetIndustry } from '../home/hooks/useHomeData'
|
||||
import Select from '../../components/Select'
|
||||
import { IndustryItemType } from '../home/types/HomeTypes'
|
||||
import { buildPath } from '../../helpers/utils'
|
||||
const Request: FC = () => {
|
||||
|
||||
const createCompany = useCreateCompany()
|
||||
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 { data: industries } = useGetIndustry()
|
||||
const { slug } = useParams()
|
||||
|
||||
const [services, setServices] = useState<{ title: string; image?: File }[]>([])
|
||||
const [products, setProducts] = useState<{ title: string; image?: File }[]>([])
|
||||
|
||||
const formik = useFormik<CreateCompanyType>({
|
||||
initialValues: {
|
||||
phone: '',
|
||||
name: '',
|
||||
email: '',
|
||||
identificationNumber: '',
|
||||
dateOfEstablishment: '',
|
||||
address: '',
|
||||
mapAddressLink: '',
|
||||
description: '',
|
||||
websiteUrl: '',
|
||||
profileImageUrl: '',
|
||||
coverImageUrl: '',
|
||||
industryId: '',
|
||||
chiefExecutiveOfficer: '',
|
||||
products: [],
|
||||
services: [],
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone: 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')),
|
||||
industryId: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
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
|
||||
|
||||
createCompany.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success('درخواست با موفقیت ثبت شد')
|
||||
navigate(buildPath(Pages.dashboard, slug))
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
toast.success('درخواست با موفقیت ثبت شد')
|
||||
// navigate(Pages.request.list)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex flex-col md:flex-row justify-between items-start md:items-center gap-4'>
|
||||
<div className='text-base'>
|
||||
ثبت درخواست
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end w-full'>
|
||||
<Button
|
||||
className='w-fit px-4'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={singleUpload.isPending || multiUpload.isPending}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>ثبت درخواست</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-10 bg-white py-8 px-4 md:px-6 lg:px-10 rounded-3xl'>
|
||||
<div className='text-lg font-medium mb-6'>اطلاعات درخواست</div>
|
||||
|
||||
<div className='space-y-6'>
|
||||
<Input
|
||||
label='نام شرکت'
|
||||
name='name'
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
|
||||
/>
|
||||
|
||||
<div className=''>
|
||||
<Input
|
||||
label='نام مدیر عامل'
|
||||
name='chiefExecutiveOfficer'
|
||||
value={formik.values.chiefExecutiveOfficer}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.chiefExecutiveOfficer && formik.errors.chiefExecutiveOfficer ? formik.errors.chiefExecutiveOfficer : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-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='grid grid-cols-1 md:grid-cols-2 gap-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 : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label={t('company.industry')}
|
||||
name='industryId'
|
||||
items={industries?.data?.industries?.map((industry: IndustryItemType) => ({ label: industry.title, value: industry.id })) || []}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.industryId && formik.errors.industryId ? formik.errors.industryId : ''}
|
||||
/>
|
||||
|
||||
<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 : ''}
|
||||
/>
|
||||
|
||||
<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 : ''}
|
||||
/>
|
||||
|
||||
<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 : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('company.website_url')}
|
||||
name='websiteUrl'
|
||||
value={formik.values.websiteUrl}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.websiteUrl && formik.errors.websiteUrl ? formik.errors.websiteUrl : ''}
|
||||
/>
|
||||
|
||||
<UploadBox
|
||||
label='تصویر پروفایل شرکت'
|
||||
onChange={(file) => setProfileImage(file[0])}
|
||||
/>
|
||||
|
||||
<UploadBox
|
||||
label='تصویر کاور'
|
||||
onChange={(file) => setCoverImage(file[0])}
|
||||
/>
|
||||
|
||||
|
||||
<CompanyServices value={services} onChange={setServices} />
|
||||
<CompanyProducts value={products} onChange={setProducts} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(Request)
|
||||
@@ -0,0 +1,92 @@
|
||||
import { FC, useState } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import UploadBox from '../../../components/UploadBox'
|
||||
import { Add, CloseCircle } from 'iconsax-react'
|
||||
import Button from '../../../components/Button'
|
||||
|
||||
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>
|
||||
<Button
|
||||
onClick={handleAddProduct}
|
||||
className='w-fit px-4 whitespace-nowrap'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add size={22} color='white' />
|
||||
<div>افزودن خدمت</div>
|
||||
</div>
|
||||
</Button>
|
||||
</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='flex 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,92 @@
|
||||
import { FC, useState } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import UploadBox from '../../../components/UploadBox'
|
||||
import { Add, CloseCircle } from 'iconsax-react'
|
||||
import Button from '../../../components/Button'
|
||||
|
||||
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>
|
||||
<Button
|
||||
onClick={handleAddService}
|
||||
className='w-fit px-4 whitespace-nowrap'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add size={22} color='white' />
|
||||
<div>افزودن خدمت</div>
|
||||
</div>
|
||||
</Button>
|
||||
</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='flex 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
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as api from "../service/Service";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { CreateCompanyType } from "../types/Types";
|
||||
|
||||
export const useCreateCompany = () => {
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateCompanyType) => api.createCompany(data),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSingleUpload = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: FormData) => api.uploadSingle(variables),
|
||||
});
|
||||
};
|
||||
export const useMultiUpload = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: FormData) => api.uploadMultiple(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateService = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateCompanyType) => api.createCompany(variables),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateCompanyType } from "../types/Types";
|
||||
|
||||
export const createCompany = async (data: CreateCompanyType) => {
|
||||
const response = await axios.post("/companies/requests", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const uploadSingle = async (params: FormData) => {
|
||||
const { data } = await axios.post(`/uploader/single-file`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const uploadMultiple = async (params: FormData) => {
|
||||
const { data } = await axios.post(`/uploader/multi-file`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createService = async (params: CreateCompanyType) => {
|
||||
const { data } = await axios.post("/companies/company-request", params);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export type CreateCompanyType = {
|
||||
phone: string;
|
||||
name: string;
|
||||
email: string;
|
||||
identificationNumber: string;
|
||||
dateOfEstablishment: string;
|
||||
address: string;
|
||||
mapAddressLink: string;
|
||||
description: string;
|
||||
websiteUrl: string;
|
||||
profileImageUrl: string;
|
||||
coverImageUrl: string;
|
||||
industryId: string;
|
||||
products: {
|
||||
title: string;
|
||||
imageUrl: string;
|
||||
}[];
|
||||
services: {
|
||||
title: string;
|
||||
imageUrl: string;
|
||||
}[];
|
||||
chiefExecutiveOfficer: string;
|
||||
};
|
||||
+4
-2
@@ -21,6 +21,7 @@ import { useEffect } from "react"
|
||||
import { useGetSlugId } from "../pages/home/hooks/useHomeData"
|
||||
import { ErrorType } from "../helpers/types"
|
||||
import { toast } from "react-toastify"
|
||||
import Request from "../pages/request/Request"
|
||||
const MainRouter = () => {
|
||||
const { slug } = useParams()
|
||||
const getSlugId = useGetSlugId()
|
||||
@@ -47,12 +48,12 @@ const MainRouter = () => {
|
||||
}, [slug])
|
||||
|
||||
return (
|
||||
<div className='p-4 overflow-hidden'>
|
||||
<div className='p-4 overflow-hidden '>
|
||||
<SideBar />
|
||||
<Header />
|
||||
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
|
||||
<div className={`hide-scroll-bar overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}>
|
||||
<div>
|
||||
<div className="pb-20">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="dashboard" element={<Home />} />
|
||||
@@ -69,6 +70,7 @@ const MainRouter = () => {
|
||||
<Route path="wallet" element={<Wallet />} />
|
||||
<Route path="reportCard/list" element={<ReportCardList />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
<Route path="request" element={<Request />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user