create request company
This commit is contained in:
+7
-3
@@ -30,7 +30,7 @@ declare global {
|
|||||||
function App() {
|
function App() {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
queryCache: new QueryCache({
|
queryCache: new QueryCache({
|
||||||
onError: async (error: IApiErrorRepsonse) => {
|
onError: async (error: IApiErrorRepsonse, query) => {
|
||||||
if (error?.response?.status === 401) {
|
if (error?.response?.status === 401) {
|
||||||
try {
|
try {
|
||||||
// Use a flag to track if refresh is in progress
|
// Use a flag to track if refresh is in progress
|
||||||
@@ -63,8 +63,12 @@ function App() {
|
|||||||
await setRefreshToken(data.refreshToken?.token);
|
await setRefreshToken(data.refreshToken?.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry the failed request
|
// Retry the failed request, even if it was muted
|
||||||
queryClient.invalidateQueries();
|
if (query) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: query.queryKey });
|
||||||
|
} else {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid token response');
|
throw new Error('Invalid token response');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
isLoading?: boolean,
|
isLoading?: boolean,
|
||||||
} & ButtonHTMLAttributes<HTMLButtonElement> &
|
} & 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 Button: FC<Props> = memo((props: Props) => {
|
||||||
const buttonClass = clx(
|
const buttonClass = clx(
|
||||||
'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary text-white w-full',
|
'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 (
|
return (
|
||||||
<button {...props} className={`${buttonClass} ${props.className}`} >
|
<button {...props} className={`${buttonClass} ${props.className}`} >
|
||||||
<img src={props.icon} alt="" />
|
|
||||||
{
|
{
|
||||||
props.isLoading ?
|
props.isLoading ?
|
||||||
<ReactLoading type='spin' color={'white'} width={20} height={20} />
|
<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 { useTranslation } from 'react-i18next'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string,
|
label: string,
|
||||||
|
isMultiple?: boolean,
|
||||||
|
onChange?: (file: File[]) => void;
|
||||||
|
isReset?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadBox: FC<Props> = (props: Props) => {
|
const UploadBox: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [file, setFile] = useState<File>()
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: 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 { 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -28,15 +63,27 @@ const UploadBox: FC<Props> = (props: Props) => {
|
|||||||
{t('select_file')}
|
{t('select_file')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className='text-description text-xs'>
|
<div className='lg:flex gap-2 hidden'>
|
||||||
{
|
{
|
||||||
file ?
|
files?.map((item, index) => (
|
||||||
file.name
|
<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' />
|
||||||
t('no_select_file')
|
<div className='text-xs'>{item.name}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ export const Pages = {
|
|||||||
reportCard: {
|
reportCard: {
|
||||||
list: "report-card/list",
|
list: "report-card/list",
|
||||||
},
|
},
|
||||||
|
request: "request",
|
||||||
};
|
};
|
||||||
|
|||||||
+54
-2
@@ -1,5 +1,13 @@
|
|||||||
import axios from "axios";
|
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";
|
// import { Pages } from "./Pages";
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
@@ -8,7 +16,51 @@ const axiosInstance = axios.create({
|
|||||||
|
|
||||||
axiosInstance.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
(response) => response,
|
(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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ import { useNavigate, useParams } from 'react-router-dom'
|
|||||||
import { getToken } from '../config/func'
|
import { getToken } from '../config/func'
|
||||||
import { buildPath } from '../helpers/utils'
|
import { buildPath } from '../helpers/utils'
|
||||||
|
|
||||||
// تایپ برای کامپوننتهای React
|
|
||||||
type ComponentType = FC<any>
|
type ComponentType = FC<any>
|
||||||
|
|
||||||
// HOC برای محافظت از کامپوننتهای نیازمند لاگین
|
|
||||||
const withAuth = (WrappedComponent: ComponentType): ComponentType => {
|
const withAuth = (WrappedComponent: ComponentType): ComponentType => {
|
||||||
// کامپوننت جدید که لاگین را چک میکند
|
|
||||||
const WithAuthComponent: FC<any> = (props) => {
|
const WithAuthComponent: FC<any> = (props) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { slug } = useParams()
|
const { slug } = useParams()
|
||||||
@@ -25,11 +22,9 @@ const withAuth = (WrappedComponent: ComponentType): ComponentType => {
|
|||||||
checkAuth()
|
checkAuth()
|
||||||
}, [navigate, slug])
|
}, [navigate, slug])
|
||||||
|
|
||||||
// کامپوننت اصلی را با props خودش رندر کن
|
|
||||||
return <WrappedComponent {...props} />
|
return <WrappedComponent {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// نام نمایشی کامپوننت را برای دیباگ بهتر تنظیم کن
|
|
||||||
WithAuthComponent.displayName = `WithAuth(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
|
WithAuthComponent.displayName = `WithAuth(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
|
||||||
|
|
||||||
return WithAuthComponent
|
return WithAuthComponent
|
||||||
|
|||||||
+36
-1
@@ -320,5 +320,40 @@
|
|||||||
"title_course": "عنوان دوره"
|
"title_course": "عنوان دوره"
|
||||||
},
|
},
|
||||||
"status": "وضعیت",
|
"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 { useGetSlugId } from "../pages/home/hooks/useHomeData"
|
||||||
import { ErrorType } from "../helpers/types"
|
import { ErrorType } from "../helpers/types"
|
||||||
import { toast } from "react-toastify"
|
import { toast } from "react-toastify"
|
||||||
|
import Request from "../pages/request/Request"
|
||||||
const MainRouter = () => {
|
const MainRouter = () => {
|
||||||
const { slug } = useParams()
|
const { slug } = useParams()
|
||||||
const getSlugId = useGetSlugId()
|
const getSlugId = useGetSlugId()
|
||||||
@@ -47,12 +48,12 @@ const MainRouter = () => {
|
|||||||
}, [slug])
|
}, [slug])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-4 overflow-hidden'>
|
<div className='p-4 overflow-hidden '>
|
||||||
<SideBar />
|
<SideBar />
|
||||||
<Header />
|
<Header />
|
||||||
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
|
<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 className={`hide-scroll-bar overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}>
|
||||||
<div>
|
<div className="pb-20">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="dashboard" element={<Home />} />
|
<Route path="dashboard" element={<Home />} />
|
||||||
@@ -69,6 +70,7 @@ const MainRouter = () => {
|
|||||||
<Route path="wallet" element={<Wallet />} />
|
<Route path="wallet" element={<Wallet />} />
|
||||||
<Route path="reportCard/list" element={<ReportCardList />} />
|
<Route path="reportCard/list" element={<ReportCardList />} />
|
||||||
<Route path="profile" element={<Profile />} />
|
<Route path="profile" element={<Profile />} />
|
||||||
|
<Route path="request" element={<Request />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user