new UX for new request
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { type FC, useState, useMemo, useEffect } from 'react'
|
import { type FC, useState, useMemo, useEffect } from 'react'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
import type { ItemsSelectType } from '@/components/Select'
|
import type { ItemsSelectType } from '@/components/Select'
|
||||||
@@ -39,10 +41,12 @@ const PayInvoice: FC = () => {
|
|||||||
|
|
||||||
const { data: user } = useGetMe()
|
const { data: user } = useGetMe()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { data, isPending } = useGetInvoiceDetail(id ?? '')
|
const { data, isPending } = useGetInvoiceDetail(id ?? '')
|
||||||
const { mutate: payInvoice, isPending: isPaying } = usePayInvoice()
|
const { mutate: payInvoice, isPending: isPaying } = usePayInvoice()
|
||||||
const multiUpload = useMultiUpload()
|
const multiUpload = useMultiUpload()
|
||||||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([])
|
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([])
|
||||||
|
const [isPaymentDone, setIsPaymentDone] = useState(false)
|
||||||
|
|
||||||
const userCredit = user?.maxCredit ?? 0
|
const userCredit = user?.maxCredit ?? 0
|
||||||
const methodItems = useMemo(() => getMethodItems(userCredit), [userCredit])
|
const methodItems = useMemo(() => getMethodItems(userCredit), [userCredit])
|
||||||
@@ -103,13 +107,25 @@ const PayInvoice: FC = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (res) => {
|
onSuccess: (res, variables) => {
|
||||||
const paymentUrl = res?.data?.paymentUrl
|
const paymentUrl = res?.data?.paymentUrl
|
||||||
if (paymentUrl) {
|
if (paymentUrl) {
|
||||||
toast('در حال انتقال به درگاه پرداخت', 'success')
|
toast('در حال انتقال به درگاه پرداخت', 'success')
|
||||||
window.location.href = paymentUrl
|
window.location.href = paymentUrl
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const method = variables.params.method
|
||||||
|
if (
|
||||||
|
method === PaymentMethodEnum.Cash ||
|
||||||
|
method === PaymentMethodEnum.Credit
|
||||||
|
) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['invoice', id] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['payment', id] })
|
||||||
|
setIsPaymentDone(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
toast('پرداخت با موفقیت ثبت شد', 'success')
|
toast('پرداخت با موفقیت ثبت شد', 'success')
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
@@ -139,6 +155,27 @@ const PayInvoice: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPaymentDone) {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 text-sm">
|
||||||
|
<div className="bg-white rounded-2xl p-8 flex flex-col items-center justify-center min-h-[280px] gap-6">
|
||||||
|
<TickCircle size={64} color="#22C55E" variant="Bold" />
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg font-medium text-black mb-2">پرداخت انجام شد</div>
|
||||||
|
<p className="text-xs text-[#8C90A3]">
|
||||||
|
پرداخت شما با موفقیت ثبت شد.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link to={`${Paths.proformaInvoice}/${id}`} className="w-full max-w-xs">
|
||||||
|
<Button type="button" className="w-full">
|
||||||
|
بازگشت به جزئیات صورت حساب
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 text-sm">
|
<div className="mt-4 text-sm">
|
||||||
<div className="text-sm mb-6">پرداخت صورت حساب</div>
|
<div className="text-sm mb-6">پرداخت صورت حساب</div>
|
||||||
|
|||||||
@@ -1,64 +1,115 @@
|
|||||||
import { useState, type FC } from 'react'
|
import { useEffect, useState, type FC } from 'react'
|
||||||
import Request from './components/Request'
|
import Request from './components/Request'
|
||||||
import Button from '@/components/Button'
|
import RequestItemsList from './components/RequestItemsList'
|
||||||
import { TickSquare } from 'iconsax-react'
|
|
||||||
import { useRequestStore } from './store/RequestStore'
|
import { useRequestStore } from './store/RequestStore'
|
||||||
import { useSubmitRequest } from './hooks/useRequestData'
|
import { useSubmitRequest } from './hooks/useRequestData'
|
||||||
import { toast } from '@/shared/toast'
|
import { toast } from '@/shared/toast'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { ArrowRight2 } from 'iconsax-react'
|
||||||
|
|
||||||
const NewRequest: FC = () => {
|
const NewRequest: FC = () => {
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { mutate: submitRequest, isPending } = useSubmitRequest()
|
const { mutate: submitRequest, isPending } = useSubmitRequest()
|
||||||
const [showRequests, setShowRequests] = useState([0])
|
|
||||||
const { items, setItems } = useRequestStore()
|
const { items, setItems } = useRequestStore()
|
||||||
|
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
||||||
|
const [formKey, setFormKey] = useState(0)
|
||||||
|
|
||||||
const onSubmit = () => {
|
useEffect(() => {
|
||||||
if (items.length > 0) {
|
setItems([])
|
||||||
submitRequest(items, {
|
return () => setItems([])
|
||||||
onSuccess: () => {
|
}, [setItems])
|
||||||
setItems([])
|
|
||||||
setShowRequests([0])
|
const handleSaved = () => {
|
||||||
toast('سفارش با موفقیت ثبت شد', 'success')
|
const wasEditing = editingIndex !== null
|
||||||
navigate(Paths.myRequests, { state: { refresh: true } })
|
setEditingIndex(null)
|
||||||
},
|
setFormKey((k) => k + 1)
|
||||||
onError: (error) => {
|
toast(wasEditing ? 'قلم ویرایش شد' : 'قلم به لیست اضافه شد', 'success')
|
||||||
toast(extractErrorMessage(error), 'error')
|
}
|
||||||
}
|
|
||||||
})
|
const handleRemove = (index: number) => {
|
||||||
} else {
|
const next = items.filter((_, i) => i !== index)
|
||||||
toast('حداقل یک سفارش باید اضافه کنید.', 'error')
|
setItems(next)
|
||||||
|
|
||||||
|
if (editingIndex === index) {
|
||||||
|
setEditingIndex(null)
|
||||||
|
setFormKey((k) => k + 1)
|
||||||
|
} else if (editingIndex !== null && editingIndex > index) {
|
||||||
|
setEditingIndex(editingIndex - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEdit = (index: number) => {
|
||||||
|
setEditingIndex(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
toast('حداقل یک قلم باید به درخواست اضافه کنید.', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitRequest(items, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setItems([])
|
||||||
|
setEditingIndex(null)
|
||||||
|
setFormKey((k) => k + 1)
|
||||||
|
toast('سفارش با موفقیت ثبت شد', 'success')
|
||||||
|
navigate(Paths.myRequests, { state: { refresh: true } })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-4 pb-12'>
|
||||||
<div className='justify-between items-center flex'>
|
<div className='flex items-center justify-between gap-4'>
|
||||||
<h1 className='text-lg font-light'>
|
<h1 className='text-lg font-light'>درخواست جدید</h1>
|
||||||
درخواست جدید
|
<Link
|
||||||
</h1>
|
to={Paths.myRequests}
|
||||||
<Button
|
className='flex items-center gap-1 text-sm text-description hover:text-black shrink-0'
|
||||||
className='w-fit px-5'
|
|
||||||
onClick={onSubmit}
|
|
||||||
isLoading={isPending}
|
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<ArrowRight2 size={18} color='currentColor' />
|
||||||
<TickSquare size={20} color='black' />
|
بازگشت به لیست
|
||||||
<div>ثبت نهایی درخواست</div>
|
</Link>
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showRequests.map((item) => {
|
<p className='text-description text-xs mt-2 leading-5'>
|
||||||
return (
|
برای هر محصول یک قلم اضافه کنید، سپس درخواست را ثبت نهایی کنید.
|
||||||
<Request addNewItem={() => setShowRequests([...showRequests, showRequests.length])} key={item} />
|
</p>
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
|
<div className='flex flex-col-reverse xl:flex-row gap-6 xl:mt-8 mt-4'>
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<Request
|
||||||
|
key={editingIndex !== null ? `edit-${editingIndex}` : `new-${formKey}`}
|
||||||
|
editIndex={editingIndex}
|
||||||
|
initialItem={
|
||||||
|
editingIndex !== null ? items[editingIndex] : undefined
|
||||||
|
}
|
||||||
|
onSaved={handleSaved}
|
||||||
|
onCancelEdit={
|
||||||
|
editingIndex !== null
|
||||||
|
? () => {
|
||||||
|
setEditingIndex(null)
|
||||||
|
setFormKey((k) => k + 1)
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RequestItemsList
|
||||||
|
items={items}
|
||||||
|
editingIndex={editingIndex}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
isPending={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,20 @@ const ManageAttribute: FC<Props> = (props) => {
|
|||||||
|
|
||||||
const { attributes, formik } = props
|
const { attributes, formik } = props
|
||||||
|
|
||||||
|
const getAttributeValue = (fieldId: string) => {
|
||||||
|
const attr = formik.values.attributes.find((o) => o.fieldId === fieldId)
|
||||||
|
return attr?.value != null ? String(attr.value) : ''
|
||||||
|
}
|
||||||
|
|
||||||
const handleChange = (attributeId: string, value: string) => {
|
const handleChange = (attributeId: string, value: string) => {
|
||||||
const attribute = formik.values.attributes
|
const next = [...formik.values.attributes]
|
||||||
const index: number = attribute.findIndex(o => o.fieldId === attributeId)
|
const index = next.findIndex((o) => o.fieldId === attributeId)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
attribute[index].value = value
|
next[index] = { ...next[index], value }
|
||||||
} else {
|
} else {
|
||||||
attribute.push({ fieldId: attributeId, value: value })
|
next.push({ fieldId: attributeId, value })
|
||||||
}
|
}
|
||||||
formik.setFieldValue('attributes', attribute)
|
formik.setFieldValue('attributes', next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ const ManageAttribute: FC<Props> = (props) => {
|
|||||||
<Select
|
<Select
|
||||||
placeholder={`انتخاب ${item.isRequired ? '(اجباری)' : '(اختاری)'}`}
|
placeholder={`انتخاب ${item.isRequired ? '(اجباری)' : '(اختاری)'}`}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
|
value={getAttributeValue(item.id)}
|
||||||
items={item.options?.map((value) => {
|
items={item.options?.map((value) => {
|
||||||
return {
|
return {
|
||||||
label: value.value,
|
label: value.value,
|
||||||
@@ -100,7 +105,9 @@ const ManageAttribute: FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div key={item.id} className='mt-6'>
|
<div key={item.id} className='mt-6'>
|
||||||
<DatePickerComponent
|
<DatePickerComponent
|
||||||
|
key={`${item.id}-${getAttributeValue(item.id)}`}
|
||||||
onChange={(date) => handleChange(item.id, date)}
|
onChange={(date) => handleChange(item.id, date)}
|
||||||
|
defaultValue={getAttributeValue(item.id) || undefined}
|
||||||
placeholder=''
|
placeholder=''
|
||||||
label={item.name}
|
label={item.name}
|
||||||
/>
|
/>
|
||||||
@@ -112,6 +119,7 @@ const ManageAttribute: FC<Props> = (props) => {
|
|||||||
<Input
|
<Input
|
||||||
label={item.name}
|
label={item.name}
|
||||||
type={item.type}
|
type={item.type}
|
||||||
|
value={getAttributeValue(item.id)}
|
||||||
onChange={(e) => handleChange(item.id, e.target.value)}
|
onChange={(e) => handleChange(item.id, e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,6 +129,7 @@ const ManageAttribute: FC<Props> = (props) => {
|
|||||||
<div key={item.id} className='mt-6'>
|
<div key={item.id} className='mt-6'>
|
||||||
<Textarea
|
<Textarea
|
||||||
label={item.name}
|
label={item.name}
|
||||||
|
value={getAttributeValue(item.id)}
|
||||||
onChange={(e) => handleChange(item.id, e.target.value)}
|
onChange={(e) => handleChange(item.id, e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, type ChangeEvent, type FC } from 'react'
|
import { useEffect, useMemo, useState, type ChangeEvent, type FC } from 'react'
|
||||||
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 UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import VoiceRecorder from '@/components/VoiceRecorder'
|
import VoiceRecorder from '@/components/VoiceRecorder'
|
||||||
import { COLORS } from '@/constants/colors'
|
import { COLORS } from '@/constants/colors'
|
||||||
import { AddSquare, Edit } from 'iconsax-react'
|
import { AddSquare, CloseCircle, Edit } from 'iconsax-react'
|
||||||
import ProductsSelect from './ProductsSelect'
|
import ProductsSelect from './ProductsSelect'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
@@ -16,49 +16,53 @@ import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploa
|
|||||||
import { useRequestStore } from '../store/RequestStore'
|
import { useRequestStore } from '../store/RequestStore'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
|
|
||||||
type Props = {
|
const emptyValues: RequestType = {
|
||||||
addNewItem: () => void,
|
productId: undefined,
|
||||||
|
attachments: [],
|
||||||
|
quantity: undefined,
|
||||||
|
description: '',
|
||||||
|
attributes: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const Request: FC<Props> = ({ addNewItem }) => {
|
type Props = {
|
||||||
|
editIndex: number | null
|
||||||
|
initialItem?: RequestType
|
||||||
|
onSaved: () => void
|
||||||
|
onCancelEdit?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) => {
|
||||||
|
const isEditing = editIndex !== null
|
||||||
|
|
||||||
const { data } = useGetProducts()
|
const { data } = useGetProducts()
|
||||||
const { setItems, items } = useRequestStore()
|
const { setItems, items } = useRequestStore()
|
||||||
const [indexRequest, setIndexRequest] = useState<number>(-1)
|
|
||||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
|
||||||
const [productSelected, setProductSelected] = useState<ProductType>()
|
const [productSelected, setProductSelected] = useState<ProductType>()
|
||||||
/** مقدار انتخابشده در دراپداون تعداد؛ '0' = انتخاب دلخواه تا Input قابل ویرایش بماند */
|
|
||||||
const [quantitySelect, setQuantitySelect] = useState<string>('')
|
const [quantitySelect, setQuantitySelect] = useState<string>('')
|
||||||
const [voiceFile, setVoiceFile] = useState<File>()
|
const [voiceFile, setVoiceFile] = useState<File>()
|
||||||
const [files, setFiles] = useState<File[]>([])
|
const [files, setFiles] = useState<File[]>([])
|
||||||
const { data: attributes } = useGetAttributes(productSelected?.id)
|
|
||||||
const singleUpload = useSingleUpload()
|
const singleUpload = useSingleUpload()
|
||||||
const multiUpload = useMultiUpload()
|
const multiUpload = useMultiUpload()
|
||||||
|
|
||||||
const formik = useFormik<RequestType>({
|
const formik = useFormik<RequestType>({
|
||||||
initialValues: {
|
initialValues: initialItem ?? emptyValues,
|
||||||
productId: undefined,
|
enableReinitialize: true,
|
||||||
attachments: [],
|
|
||||||
quantity: undefined,
|
|
||||||
description: '',
|
|
||||||
attributes: []
|
|
||||||
},
|
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
productId: Yup.string().required('این فیلد اجباری می باشد'),
|
productId: Yup.string().required('این فیلد اجباری می باشد'),
|
||||||
quantity: Yup.number().required('این فیلد اجباری می باشد'),
|
quantity: Yup.number().required('این فیلد اجباری می باشد'),
|
||||||
}),
|
}),
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const attachments: AttachmentsType[] = []
|
const attachments: AttachmentsType[] = [...(values.attachments ?? [])]
|
||||||
|
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
await multiUpload.mutateAsync(files, {
|
await multiUpload.mutateAsync(files, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
data?.data?.map((item) => {
|
data?.data?.forEach((item) => {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
type: 'uploads_attach',
|
type: 'uploads_attach',
|
||||||
url: item.url
|
url: item.url,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (voiceFile) {
|
if (voiceFile) {
|
||||||
@@ -66,57 +70,110 @@ const Request: FC<Props> = ({ addNewItem }) => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
type: 'voice',
|
type: 'voice',
|
||||||
url: data?.data?.url
|
url: data?.data?.url,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (indexRequest === -1) {
|
|
||||||
values.attachments = attachments
|
|
||||||
setIndexRequest(items.length)
|
|
||||||
setItems([...items, values])
|
|
||||||
addNewItem()
|
|
||||||
setIsEditMode(true)
|
|
||||||
} else {
|
|
||||||
if (values.attachments.length) {
|
|
||||||
values.attachments = attachments
|
|
||||||
|
|
||||||
}
|
const payload: RequestType = { ...values, attachments }
|
||||||
const items_edit = [...items]
|
|
||||||
items_edit[indexRequest] = values
|
if (isEditing && editIndex !== null) {
|
||||||
setItems(items_edit)
|
const updated = [...items]
|
||||||
|
updated[editIndex] = payload
|
||||||
|
setItems(updated)
|
||||||
|
} else {
|
||||||
|
setItems([...items, payload])
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
setFiles([])
|
||||||
|
setVoiceFile(undefined)
|
||||||
|
onSaved()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
const activeProductId = formik.values.productId ?? initialItem?.productId
|
||||||
const productId: string = e.target.value
|
|
||||||
const product = data?.data?.find(o => o.id === productId)
|
const resolvedProduct = useMemo(() => {
|
||||||
|
if (productSelected) return productSelected
|
||||||
|
if (!activeProductId || !data?.data) return undefined
|
||||||
|
const product = data.data.find((p) => p.id === activeProductId)
|
||||||
|
if (!product) return undefined
|
||||||
|
const quantities = [...product.quantities]
|
||||||
|
if (quantities[0] !== 0) quantities.unshift(0)
|
||||||
|
return { ...product, quantities }
|
||||||
|
}, [productSelected, activeProductId, data])
|
||||||
|
|
||||||
|
const { data: attributes } = useGetAttributes(resolvedProduct?.id)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialItem?.productId || !data?.data) return
|
||||||
|
|
||||||
|
const product = data.data.find((p) => p.id === initialItem.productId)
|
||||||
|
if (!product) return
|
||||||
|
|
||||||
|
const quantities = [...product.quantities]
|
||||||
|
if (quantities[0] !== 0) quantities.unshift(0)
|
||||||
|
|
||||||
|
setProductSelected({ ...product, quantities })
|
||||||
|
|
||||||
|
const qty = initialItem.quantity
|
||||||
|
if (qty !== undefined && quantities.includes(qty)) {
|
||||||
|
setQuantitySelect(String(qty))
|
||||||
|
} else {
|
||||||
|
setQuantitySelect('0')
|
||||||
|
}
|
||||||
|
}, [initialItem, data])
|
||||||
|
|
||||||
|
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const productId = e.target.value
|
||||||
|
const product = data?.data?.find((o) => o.id === productId)
|
||||||
|
|
||||||
if (product) {
|
if (product) {
|
||||||
if (product.quantities[0] !== 0) {
|
const quantities = [...product.quantities]
|
||||||
product.quantities.unshift(0)
|
if (quantities[0] !== 0) quantities.unshift(0)
|
||||||
}
|
setProductSelected({ ...product, quantities })
|
||||||
setProductSelected(product)
|
|
||||||
formik.setFieldValue('productId', productId)
|
formik.setFieldValue('productId', productId)
|
||||||
|
formik.setFieldValue('attributes', [])
|
||||||
setQuantitySelect('')
|
setQuantitySelect('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUploading = singleUpload.isPending || multiUpload.isPending
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white rounded-3xl p-6 mt-8'>
|
<div className='bg-white rounded-3xl p-6'>
|
||||||
<div className='font-light'>اطلاعات درخواست</div>
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='font-light'>
|
||||||
|
{isEditing ? 'ویرایش قلم' : 'افزودن قلم جدید'}
|
||||||
|
</div>
|
||||||
|
{isEditing && onCancelEdit && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={onCancelEdit}
|
||||||
|
className='flex items-center gap-1 text-xs text-description hover:text-black'
|
||||||
|
>
|
||||||
|
<CloseCircle size={18} color='currentColor' />
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 rowTwoInput'>
|
<div className='mt-6 rowTwoInput'>
|
||||||
<ProductsSelect
|
<ProductsSelect
|
||||||
onChange={handleChange}
|
value={formik.values.productId ?? ''}
|
||||||
error_text={formik.touched.productId && formik.errors.productId ? formik.errors.productId : ''}
|
onChange={handleProductChange}
|
||||||
|
error_text={
|
||||||
|
formik.touched.productId && formik.errors.productId
|
||||||
|
? formik.errors.productId
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ManageAttribute
|
<ManageAttribute
|
||||||
|
key={`${resolvedProduct?.id ?? 'none'}-${editIndex ?? 'new'}`}
|
||||||
attributes={attributes?.data}
|
attributes={attributes?.data}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
/>
|
/>
|
||||||
@@ -124,12 +181,12 @@ const Request: FC<Props> = ({ addNewItem }) => {
|
|||||||
|
|
||||||
<div className='mt-6 rowTwoInput'>
|
<div className='mt-6 rowTwoInput'>
|
||||||
<Select
|
<Select
|
||||||
items={productSelected?.quantities?.map((item) => {
|
items={
|
||||||
return {
|
resolvedProduct?.quantities?.map((item) => ({
|
||||||
label: item === 0 ? 'انتخاب دلخواه' : item + '',
|
label: item === 0 ? 'انتخاب دلخواه' : String(item),
|
||||||
value: item + ''
|
value: String(item),
|
||||||
}
|
})) ?? []
|
||||||
}) || []}
|
}
|
||||||
label='تعداد'
|
label='تعداد'
|
||||||
placeholder='انتخاب'
|
placeholder='انتخاب'
|
||||||
value={quantitySelect}
|
value={quantitySelect}
|
||||||
@@ -142,7 +199,11 @@ const Request: FC<Props> = ({ addNewItem }) => {
|
|||||||
}}
|
}}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
name='quantity'
|
name='quantity'
|
||||||
error_text={formik.touched.quantity && formik.errors.quantity ? formik.errors.quantity : ''}
|
error_text={
|
||||||
|
formik.touched.quantity && formik.errors.quantity
|
||||||
|
? formik.errors.quantity
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -150,49 +211,54 @@ const Request: FC<Props> = ({ addNewItem }) => {
|
|||||||
readOnly={quantitySelect !== '0'}
|
readOnly={quantitySelect !== '0'}
|
||||||
{...formik.getFieldProps('quantity')}
|
{...formik.getFieldProps('quantity')}
|
||||||
type='number'
|
type='number'
|
||||||
error_text={formik.touched.quantity && formik.errors.quantity ? formik.errors.quantity : ''}
|
error_text={
|
||||||
|
formik.touched.quantity && formik.errors.quantity
|
||||||
|
? formik.errors.quantity
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<VoiceRecorder
|
<VoiceRecorder
|
||||||
|
value={formik.values.description ?? ''}
|
||||||
onChange={(value) => formik.setFieldValue('description', value)}
|
onChange={(value) => formik.setFieldValue('description', value)}
|
||||||
label='پیام صوتی'
|
label='پیام صوتی'
|
||||||
onRecordingComplete={(blob) => {
|
onRecordingComplete={(blob) => {
|
||||||
const file = new File([blob], "recording.wav", { type: blob.type });
|
const file = new File([blob], 'recording.wav', { type: blob.type })
|
||||||
setVoiceFile(file);
|
setVoiceFile(file)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<UploadBox
|
<UploadBox label='آپلود فایل' isMultiple onChange={setFiles} />
|
||||||
label='آپلود فایل'
|
|
||||||
isMultiple
|
|
||||||
onChange={setFiles}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isEditing && (initialItem?.attachments?.length ?? 0) > 0 && files.length === 0 && (
|
||||||
|
<p className='text-description text-xs mt-3'>
|
||||||
|
{initialItem!.attachments.length} پیوست قبلی حفظ میشود. برای جایگزینی، فایل جدید آپلود کنید.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='mt-6 flex justify-end'>
|
<div className='mt-6 flex justify-end'>
|
||||||
<Button
|
<Button
|
||||||
className={clx(
|
className={clx(
|
||||||
'bg-transparent border border-primary text-primary w-fit px-6',
|
'w-fit px-6',
|
||||||
isEditMode && 'bg-transparent border-[#3B82F6] text-[#3B82F6] '
|
isEditing
|
||||||
|
? 'bg-transparent border border-[#3B82F6] text-[#3B82F6]'
|
||||||
|
: 'bg-transparent border border-primary text-primary'
|
||||||
)}
|
)}
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
isLoading={singleUpload.isPending || multiUpload.isPending}
|
isLoading={isUploading}
|
||||||
>
|
>
|
||||||
<div className='flex gap-1'>
|
<div className='flex gap-1 items-center'>
|
||||||
{
|
{isEditing ? (
|
||||||
isEditMode ?
|
<Edit color='#3B82F6' size={20} />
|
||||||
<Edit color='#3B82F6' size={20} />
|
) : (
|
||||||
:
|
<AddSquare color={COLORS.primary} size={20} />
|
||||||
<AddSquare color={COLORS.primary} size={20} />
|
)}
|
||||||
}
|
<span>{isEditing ? 'ذخیره تغییرات' : 'افزودن به لیست'}</span>
|
||||||
<div>
|
|
||||||
{isEditMode ? 'ویرایش کردن' : 'اضافه کردن'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { Edit, ShoppingCart, TickSquare, Trash } from 'iconsax-react'
|
||||||
|
import { useGetProducts } from '../hooks/useRequestData'
|
||||||
|
import type { RequestType } from '../type/Types'
|
||||||
|
import { clx } from '@/helpers/utils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: RequestType[]
|
||||||
|
editingIndex: number | null
|
||||||
|
onEdit: (index: number) => void
|
||||||
|
onRemove: (index: number) => void
|
||||||
|
onSubmit: () => void
|
||||||
|
isPending: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequestItemsList: FC<Props> = ({
|
||||||
|
items,
|
||||||
|
editingIndex,
|
||||||
|
onEdit,
|
||||||
|
onRemove,
|
||||||
|
onSubmit,
|
||||||
|
isPending,
|
||||||
|
}) => {
|
||||||
|
const { data: productsData } = useGetProducts()
|
||||||
|
|
||||||
|
const getProductTitle = (productId?: string) =>
|
||||||
|
productsData?.data?.find((p) => p.id === productId)?.title ?? 'محصول'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-white w-full xl:w-[320px] shrink-0 py-6 px-5 h-fit rounded-3xl xl:sticky xl:top-6'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<ShoppingCart size={20} color='black' />
|
||||||
|
<span className='text-sm font-medium'>اقلام درخواست</span>
|
||||||
|
{items.length > 0 && (
|
||||||
|
<span className='text-xs bg-primary/20 text-black rounded-full px-2 py-0.5 mr-auto'>
|
||||||
|
{items.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<p className='text-description text-xs mt-6 leading-6'>
|
||||||
|
هنوز قلمی اضافه نشده. فرم را پر کنید و روی «افزودن به لیست» بزنید.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className='mt-4 space-y-3 max-h-[360px] overflow-y-auto'>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className={clx(
|
||||||
|
'border rounded-2xl p-3 transition-colors',
|
||||||
|
editingIndex === index
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-[#f0f2f8]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='text-sm font-medium truncate'>
|
||||||
|
{getProductTitle(item.productId)}
|
||||||
|
</div>
|
||||||
|
<div className='text-description text-xs mt-1'>
|
||||||
|
تعداد: {item.quantity}
|
||||||
|
{item.attachments.length > 0 && (
|
||||||
|
<span className='mr-2'>
|
||||||
|
· {item.attachments.length} پیوست
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2 mt-3'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => onEdit(index)}
|
||||||
|
className='flex items-center gap-1 text-xs text-[#3B82F6] hover:opacity-80'
|
||||||
|
>
|
||||||
|
<Edit size={16} color='#3B82F6' />
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
className='flex items-center gap-1 text-xs text-red-500 hover:opacity-80 mr-auto'
|
||||||
|
>
|
||||||
|
<Trash size={16} color='#ef4444' />
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='mt-6 pt-4 border-t border-[#f0f2f8]'>
|
||||||
|
<Button
|
||||||
|
className='w-full'
|
||||||
|
onClick={onSubmit}
|
||||||
|
isLoading={isPending}
|
||||||
|
disabled={items.length === 0}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center justify-center'>
|
||||||
|
<TickSquare size={20} color='black' />
|
||||||
|
<span>ثبت نهایی درخواست</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
{items.length === 0 && (
|
||||||
|
<p className='text-description text-[10px] text-center mt-2'>
|
||||||
|
حداقل یک قلم برای ثبت لازم است
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RequestItemsList
|
||||||
Reference in New Issue
Block a user