This commit is contained in:
@@ -14,7 +14,8 @@ type Props = {
|
|||||||
reset?: boolean;
|
reset?: boolean;
|
||||||
isDateTime?: boolean;
|
isDateTime?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: string
|
label?: string;
|
||||||
|
isNotRequired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DatePickerComponent: FC<Props> = (props: Props) => {
|
const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||||
@@ -85,8 +86,11 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{props.label &&
|
{props.label &&
|
||||||
<div className='text-sm'>
|
<div className='flex items-center gap-1 text-sm'>
|
||||||
{props.label}
|
<div>{props.label}</div>
|
||||||
|
{props.isNotRequired && (
|
||||||
|
<span className='text-xs text-description'>(اختیاری)</span>
|
||||||
|
)}
|
||||||
</div>}
|
</div>}
|
||||||
<div className={clx(
|
<div className={clx(
|
||||||
'relative ',
|
'relative ',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Props = {
|
|||||||
error_text?: string,
|
error_text?: string,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
label?: string,
|
label?: string,
|
||||||
|
isNotRequired?: boolean,
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
const Select: FC<Props> = (props: Props) => {
|
const Select: FC<Props> = (props: Props) => {
|
||||||
@@ -20,9 +21,14 @@ const Select: FC<Props> = (props: Props) => {
|
|||||||
<div className='w-full relative'>
|
<div className='w-full relative'>
|
||||||
{
|
{
|
||||||
props.label &&
|
props.label &&
|
||||||
|
<div className='flex items-center gap-1'>
|
||||||
<label className='text-sm'>
|
<label className='text-sm'>
|
||||||
{props.label}
|
{props.label}
|
||||||
</label>
|
</label>
|
||||||
|
{props.isNotRequired && (
|
||||||
|
<span className='text-xs text-description'>(اختیاری)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<select {...props} className={clx(
|
<select {...props} className={clx(
|
||||||
'w-full text-black block border appearance-none border-border !bg-white px-2.5 h-10 text-sm rounded-[10px] bg-gray',
|
'w-full text-black block border appearance-none border-border !bg-white px-2.5 h-10 text-sm rounded-[10px] bg-gray',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type Props = {
|
|||||||
isMultiple?: boolean,
|
isMultiple?: boolean,
|
||||||
onChange?: (file: File[]) => void;
|
onChange?: (file: File[]) => void;
|
||||||
isReset?: boolean;
|
isReset?: boolean;
|
||||||
|
isNotRequired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadBox: FC<Props> = (props: Props) => {
|
const UploadBox: FC<Props> = (props: Props) => {
|
||||||
@@ -55,7 +56,12 @@ const UploadBox: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='text-sm'>{props.label}</div>
|
<div className='flex items-center gap-1 text-sm'>
|
||||||
|
<div>{props.label}</div>
|
||||||
|
{props.isNotRequired && (
|
||||||
|
<span className='text-xs text-description'>(اختیاری)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
||||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
|
|||||||
+175
-111
@@ -1,155 +1,219 @@
|
|||||||
|
import { type FC, useEffect, useState } from 'react'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
import DatePicker from '@/components/DatePicker'
|
import DatePicker from '@/components/DatePicker'
|
||||||
import Textarea from '@/components/Textarea'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import CustomerStats from '@/components/CustomerStats'
|
import { useCreateUser } from './hooks/useUsersData'
|
||||||
import { TickCircle, Wallet2, TicketDiscount } from 'iconsax-react'
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
|
||||||
import { type FC } from 'react'
|
import type { CreateUserType } from './types/Types'
|
||||||
|
import { Pages } from '@/config/Pages'
|
||||||
|
import type { ErrorType } from '@/helpers/types'
|
||||||
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
|
||||||
|
type CreateCustomerFormValues = {
|
||||||
|
phone: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
birthDate: string
|
||||||
|
marriageDate: string
|
||||||
|
gender: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildCreateUserPayload = (
|
||||||
|
values: CreateCustomerFormValues,
|
||||||
|
avatarUrl?: string,
|
||||||
|
): CreateUserType => {
|
||||||
|
const payload: CreateUserType = {
|
||||||
|
phone: values.phone.trim(),
|
||||||
|
firstName: values.firstName.trim(),
|
||||||
|
lastName: values.lastName.trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.birthDate) payload.birthDate = values.birthDate
|
||||||
|
if (values.marriageDate) payload.marriageDate = values.marriageDate
|
||||||
|
if (values.gender !== '') payload.gender = values.gender === 'true'
|
||||||
|
if (avatarUrl) payload.avatarUrl = avatarUrl
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
const CreateCustomer: FC = () => {
|
const CreateCustomer: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { mutate: createUser, isPending } = useCreateUser()
|
||||||
|
const { mutate: singleUpload, isPending: isUploading } = useSingleUpload()
|
||||||
|
const [avatarFile, setAvatarFile] = useState<File[]>([])
|
||||||
|
const [avatarPreviewUrl, setAvatarPreviewUrl] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (avatarFile.length > 0) {
|
||||||
|
const url = URL.createObjectURL(avatarFile[0])
|
||||||
|
setAvatarPreviewUrl(url)
|
||||||
|
return () => URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
setAvatarPreviewUrl('')
|
||||||
|
}, [avatarFile])
|
||||||
|
|
||||||
|
const formik = useFormik<CreateCustomerFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
phone: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
birthDate: '',
|
||||||
|
marriageDate: '',
|
||||||
|
gender: '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object().shape({
|
||||||
|
phone: Yup.string().required('شماره تلفن الزامی است'),
|
||||||
|
firstName: Yup.string().required('نام الزامی است'),
|
||||||
|
lastName: Yup.string().required('نام خانوادگی الزامی است'),
|
||||||
|
birthDate: Yup.string(),
|
||||||
|
marriageDate: Yup.string(),
|
||||||
|
gender: Yup.string(),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
const submitCustomer = (avatarUrl?: string) => {
|
||||||
|
createUser(buildCreateUserPayload(values, avatarUrl), {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
toast.success('مشتری با موفقیت ایجاد شد')
|
||||||
|
const userId = response.data.user.id
|
||||||
|
navigate(Pages.customers.detail + userId)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarFile.length > 0) {
|
||||||
|
singleUpload(avatarFile[0], {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const avatarUrl = response?.data?.url || ''
|
||||||
|
submitCustomer(avatarUrl)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(extractErrorMessage(error, 'خطا در آپلود تصویر'))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
submitCustomer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<h1 className='text-lg font-light'>افزودن مشتری</h1>
|
<h1 className='text-lg font-light'>افزودن مشتری</h1>
|
||||||
<Button className='w-fit px-6'>
|
<Button
|
||||||
|
className='w-fit px-6'
|
||||||
|
isloading={isPending || isUploading}
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<TickCircle
|
<TickCircle size={20} color='#fff' />
|
||||||
size={20}
|
|
||||||
color='#fff'
|
|
||||||
/>
|
|
||||||
<span>ثبت مشتری</span>
|
<span>ثبت مشتری</span>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-7 mt-9'>
|
<div className='mt-9'>
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='w-full bg-white rounded-4xl p-8'>
|
<div className='w-full bg-white rounded-4xl p-8'>
|
||||||
<div className='text-lg font-light'>
|
<div className='text-lg font-light'>اطلاعات مشتری</div>
|
||||||
اطلاعات مشتری
|
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
||||||
|
<Input
|
||||||
|
label='شماره تماس'
|
||||||
|
placeholder='۰۹۱۲۱۲۳۴۵۶۷'
|
||||||
|
name='phone'
|
||||||
|
value={formik.values.phone}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={
|
||||||
|
formik.touched.phone && formik.errors.phone
|
||||||
|
? formik.errors.phone
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label='جنسیت'
|
||||||
|
placeholder='انتخاب'
|
||||||
|
name='gender'
|
||||||
|
value={formik.values.gender}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isNotRequired
|
||||||
|
items={[
|
||||||
|
{ value: 'true', label: 'مرد' },
|
||||||
|
{ value: 'false', label: 'زن' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4 mt-6'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-5'>
|
||||||
<Input
|
<Input
|
||||||
label='نام'
|
label='نام'
|
||||||
placeholder=''
|
placeholder=''
|
||||||
name='name'
|
name='firstName'
|
||||||
|
value={formik.values.firstName}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={
|
||||||
|
formik.touched.firstName && formik.errors.firstName
|
||||||
|
? formik.errors.firstName
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='کد ملی'
|
label='نام خانوادگی'
|
||||||
placeholder=''
|
placeholder=''
|
||||||
name='nationalCode'
|
name='lastName'
|
||||||
|
value={formik.values.lastName}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={
|
||||||
|
formik.touched.lastName && formik.errors.lastName
|
||||||
|
? formik.errors.lastName
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4 mt-5'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-5'>
|
||||||
<Input
|
|
||||||
label='شماره تماس'
|
|
||||||
placeholder=''
|
|
||||||
name='phone'
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label='ایمیل'
|
|
||||||
placeholder=''
|
|
||||||
name='email'
|
|
||||||
type='email'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4 mt-5'>
|
|
||||||
<div>
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label='تاریخ تولد'
|
label='تاریخ تولد'
|
||||||
placeholder='انتخاب'
|
placeholder='انتخاب'
|
||||||
onChange={() => { }}
|
isNotRequired
|
||||||
|
defaulValue={formik.values.birthDate}
|
||||||
|
onChange={(date) => formik.setFieldValue('birthDate', date)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label='تاریخ ازدواج'
|
label='تاریخ ازدواج'
|
||||||
placeholder='انتخاب'
|
placeholder='انتخاب'
|
||||||
onChange={() => { }}
|
isNotRequired
|
||||||
/>
|
defaulValue={formik.values.marriageDate}
|
||||||
</div>
|
onChange={(date) => formik.setFieldValue('marriageDate', date)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='w-full bg-white rounded-4xl p-8 mt-8'>
|
|
||||||
<div className='text-lg font-light'>
|
|
||||||
آدرس
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4 mt-6'>
|
|
||||||
<Select
|
|
||||||
label='استان'
|
|
||||||
placeholder='انتخاب'
|
|
||||||
items={[]}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label='شهر'
|
|
||||||
placeholder='انتخاب'
|
|
||||||
items={[]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<Input
|
<UploadBox
|
||||||
label='عنوان آدرس'
|
label='تصویر پروفایل'
|
||||||
placeholder=''
|
isNotRequired
|
||||||
name='addressTitle'
|
onChange={(files) => setAvatarFile(files)}
|
||||||
|
isReset={avatarFile.length === 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{avatarPreviewUrl && (
|
||||||
|
<div className='mt-3'>
|
||||||
|
<img
|
||||||
|
src={avatarPreviewUrl}
|
||||||
|
alt='avatar preview'
|
||||||
|
className='w-20 h-20 rounded-full object-cover'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className='mt-5'>
|
|
||||||
<Textarea
|
|
||||||
label='آدرس'
|
|
||||||
placeholder=''
|
|
||||||
name='address'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='w-[330px]'>
|
|
||||||
<div className='bg-white rounded-4xl p-8'>
|
|
||||||
<CustomerStats points={0} walletBalance='۰ تومان' />
|
|
||||||
<Button className='w-full mt-5'>
|
|
||||||
<div className='flex gap-2 items-center'>
|
|
||||||
<Wallet2 size={16} color='#fff' />
|
|
||||||
<span>افزایش موجودی کیف پول</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6 bg-white rounded-4xl p-8'>
|
|
||||||
<div className='font-light'>
|
|
||||||
کدهای تخفیف
|
|
||||||
</div>
|
|
||||||
<div className='mt-4'>
|
|
||||||
<table className='w-full'>
|
|
||||||
<thead>
|
|
||||||
<tr className='border-b border-border'>
|
|
||||||
<th className='text-right pb-2 font-light text-xs text-description'>
|
|
||||||
کد
|
|
||||||
</th>
|
|
||||||
<th className='text-center pb-2 font-light text-xs text-description'>
|
|
||||||
انقضا
|
|
||||||
</th>
|
|
||||||
<th className='text-center pb-2 font-light text-xs text-description'>
|
|
||||||
وضعیت
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
<div className='text-center py-8 text-xs text-description'>
|
|
||||||
کد تخفیفی موجود نیست
|
|
||||||
</div>
|
|
||||||
<Button className='w-full'>
|
|
||||||
<div className='flex gap-2 items-center'>
|
|
||||||
<TicketDiscount size={16} color='#fff' />
|
|
||||||
<span>اضافه کردن کد تخفیف</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ export type CreateUserType = {
|
|||||||
phone: string;
|
phone: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
birthDate?: string;
|
||||||
|
marriageDate?: string;
|
||||||
|
gender?: boolean;
|
||||||
|
avatarUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateUserResult = {
|
export type CreateUserResult = {
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ const CustomersSubMenu: FC = () => {
|
|||||||
isActive={isActive('list')}
|
isActive={isActive('list')}
|
||||||
link={Pages.customers.list}
|
link={Pages.customers.list}
|
||||||
/>
|
/>
|
||||||
{/* <SubMenuItem
|
<SubMenuItem
|
||||||
title={t('submenu.add_customer')}
|
title={t('submenu.add_customer')}
|
||||||
isActive={isActive('add')}
|
isActive={isActive('add')}
|
||||||
link={Pages.customers.add}
|
link={Pages.customers.add}
|
||||||
/> */}
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user