This commit is contained in:
@@ -14,7 +14,8 @@ type Props = {
|
||||
reset?: boolean;
|
||||
isDateTime?: boolean;
|
||||
className?: string;
|
||||
label?: string
|
||||
label?: string;
|
||||
isNotRequired?: boolean;
|
||||
};
|
||||
|
||||
const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||
@@ -85,8 +86,11 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{props.label &&
|
||||
<div className='text-sm'>
|
||||
{props.label}
|
||||
<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={clx(
|
||||
'relative ',
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
error_text?: string,
|
||||
placeholder?: string,
|
||||
label?: string,
|
||||
isNotRequired?: boolean,
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||
|
||||
const Select: FC<Props> = (props: Props) => {
|
||||
@@ -20,9 +21,14 @@ const Select: FC<Props> = (props: Props) => {
|
||||
<div className='w-full relative'>
|
||||
{
|
||||
props.label &&
|
||||
<div className='flex items-center gap-1'>
|
||||
<label className='text-sm'>
|
||||
{props.label}
|
||||
</label>
|
||||
{props.isNotRequired && (
|
||||
<span className='text-xs text-description'>(اختیاری)</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
<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',
|
||||
|
||||
@@ -9,6 +9,7 @@ type Props = {
|
||||
isMultiple?: boolean,
|
||||
onChange?: (file: File[]) => void;
|
||||
isReset?: boolean;
|
||||
isNotRequired?: boolean;
|
||||
}
|
||||
|
||||
const UploadBox: FC<Props> = (props: Props) => {
|
||||
@@ -55,7 +56,12 @@ const UploadBox: FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
||||
<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 Input from '@/components/Input'
|
||||
import Select from '@/components/Select'
|
||||
import DatePicker from '@/components/DatePicker'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import CustomerStats from '@/components/CustomerStats'
|
||||
import { TickCircle, Wallet2, TicketDiscount } from 'iconsax-react'
|
||||
import { type FC } from 'react'
|
||||
import UploadBox from '@/components/UploadBox'
|
||||
import { useCreateUser } from './hooks/useUsersData'
|
||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
|
||||
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 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 (
|
||||
<div className='mt-5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<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'>
|
||||
<TickCircle
|
||||
size={20}
|
||||
color='#fff'
|
||||
/>
|
||||
<TickCircle size={20} color='#fff' />
|
||||
<span>ثبت مشتری</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-7 mt-9'>
|
||||
<div className='flex-1'>
|
||||
<div className='mt-9'>
|
||||
<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 className='grid grid-cols-2 gap-4 mt-6'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-5'>
|
||||
<Input
|
||||
label='نام'
|
||||
placeholder=''
|
||||
name='name'
|
||||
name='firstName'
|
||||
value={formik.values.firstName}
|
||||
onChange={formik.handleChange}
|
||||
error_text={
|
||||
formik.touched.firstName && formik.errors.firstName
|
||||
? formik.errors.firstName
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label='کد ملی'
|
||||
label='نام خانوادگی'
|
||||
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 className='grid 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>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-5'>
|
||||
<DatePicker
|
||||
label='تاریخ تولد'
|
||||
placeholder='انتخاب'
|
||||
onChange={() => { }}
|
||||
isNotRequired
|
||||
defaulValue={formik.values.birthDate}
|
||||
onChange={(date) => formik.setFieldValue('birthDate', date)}
|
||||
/>
|
||||
</div>
|
||||
<DatePicker
|
||||
label='تاریخ ازدواج'
|
||||
placeholder='انتخاب'
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</div>
|
||||
</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={[]}
|
||||
isNotRequired
|
||||
defaulValue={formik.values.marriageDate}
|
||||
onChange={(date) => formik.setFieldValue('marriageDate', date)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Input
|
||||
label='عنوان آدرس'
|
||||
placeholder=''
|
||||
name='addressTitle'
|
||||
<UploadBox
|
||||
label='تصویر پروفایل'
|
||||
isNotRequired
|
||||
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 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>
|
||||
|
||||
@@ -101,6 +101,10 @@ export type CreateUserType = {
|
||||
phone: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthDate?: string;
|
||||
marriageDate?: string;
|
||||
gender?: boolean;
|
||||
avatarUrl?: string;
|
||||
};
|
||||
|
||||
export type CreateUserResult = {
|
||||
|
||||
@@ -33,11 +33,11 @@ const CustomersSubMenu: FC = () => {
|
||||
isActive={isActive('list')}
|
||||
link={Pages.customers.list}
|
||||
/>
|
||||
{/* <SubMenuItem
|
||||
<SubMenuItem
|
||||
title={t('submenu.add_customer')}
|
||||
isActive={isActive('add')}
|
||||
link={Pages.customers.add}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user