@@ -12,9 +12,11 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
} from 'iconsax-react'
|
} from 'iconsax-react'
|
||||||
import {
|
import {
|
||||||
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useId,
|
useId,
|
||||||
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
@@ -47,12 +49,21 @@ type PendingVoice = {
|
|||||||
status: 'uploading' | 'done' | 'error'
|
status: 'uploading' | 'done' | 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChatComposerHandle = {
|
||||||
|
getPayload: () => ChatComposerSubmitPayload
|
||||||
|
hasPendingUploads: () => boolean
|
||||||
|
hasUploadErrors: () => boolean
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: (payload: ChatComposerSubmitPayload) => void | Promise<void>
|
onSubmit: (payload: ChatComposerSubmitPayload) => void | Promise<void>
|
||||||
isSubmitting?: boolean
|
isSubmitting?: boolean
|
||||||
submitLabel?: string
|
submitLabel?: string
|
||||||
label?: string
|
label?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
allowEmptySubmit?: boolean
|
||||||
|
showSubmitButton?: boolean
|
||||||
replyTo?: {
|
replyTo?: {
|
||||||
id: string
|
id: string
|
||||||
content: string
|
content: string
|
||||||
@@ -163,15 +174,17 @@ const VoicePreviewChip: FC<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatComposer: FC<Props> = ({
|
const ChatComposer = forwardRef<ChatComposerHandle, Props>(({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
submitLabel = 'ارسال پیام',
|
submitLabel = 'ارسال پیام',
|
||||||
label = 'پیام شما',
|
label = 'پیام شما',
|
||||||
placeholder = 'متن پیام خود را بنویسید...',
|
placeholder = 'متن پیام خود را بنویسید...',
|
||||||
|
allowEmptySubmit = false,
|
||||||
|
showSubmitButton = true,
|
||||||
replyTo = null,
|
replyTo = null,
|
||||||
onCancelReply,
|
onCancelReply,
|
||||||
}) => {
|
}, ref) => {
|
||||||
const inputId = useId()
|
const inputId = useId()
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
@@ -318,7 +331,7 @@ const ChatComposer: FC<Props> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const buildPayload = useCallback((): ChatComposerSubmitPayload => {
|
||||||
const uploadedFiles = pendingFiles.filter(
|
const uploadedFiles = pendingFiles.filter(
|
||||||
(item) => item.status === 'done' && item.key,
|
(item) => item.status === 'done' && item.key,
|
||||||
)
|
)
|
||||||
@@ -326,10 +339,49 @@ const ChatComposer: FC<Props> = ({
|
|||||||
(item) => item.status === 'done' && item.key,
|
(item) => item.status === 'done' && item.key,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const attachments: ChatComposerAttachment[] = [
|
||||||
|
...uploadedFiles.map((item) => ({
|
||||||
|
type: 'uploads_attach',
|
||||||
|
url: item.key!,
|
||||||
|
})),
|
||||||
|
...uploadedVoices.map((item) => ({
|
||||||
|
type: 'voice',
|
||||||
|
url: item.key!,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: message.trim(),
|
||||||
|
attachments,
|
||||||
|
}
|
||||||
|
}, [message, pendingFiles, pendingVoices])
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
getPayload: buildPayload,
|
||||||
|
hasPendingUploads: () => isUploading,
|
||||||
|
hasUploadErrors: () =>
|
||||||
|
pendingFiles.some((item) => item.status === 'error') ||
|
||||||
|
pendingVoices.some((item) => item.status === 'error'),
|
||||||
|
reset: resetComposer,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
buildPayload,
|
||||||
|
isUploading,
|
||||||
|
pendingFiles,
|
||||||
|
pendingVoices,
|
||||||
|
resetComposer,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const payload = buildPayload()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!message.trim() &&
|
!allowEmptySubmit &&
|
||||||
uploadedFiles.length === 0 &&
|
!payload.content &&
|
||||||
uploadedVoices.length === 0
|
payload.attachments.length === 0
|
||||||
) {
|
) {
|
||||||
toast('لطفاً پیام یا فایل ضمیمه وارد کنید', 'error')
|
toast('لطفاً پیام یا فایل ضمیمه وارد کنید', 'error')
|
||||||
return
|
return
|
||||||
@@ -351,22 +403,8 @@ const ChatComposer: FC<Props> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments: ChatComposerAttachment[] = [
|
|
||||||
...uploadedFiles.map((item) => ({
|
|
||||||
type: 'uploads_attach',
|
|
||||||
url: item.key!,
|
|
||||||
})),
|
|
||||||
...uploadedVoices.map((item) => ({
|
|
||||||
type: 'voice',
|
|
||||||
url: item.key!,
|
|
||||||
})),
|
|
||||||
]
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onSubmit({
|
await onSubmit(payload)
|
||||||
content: message.trim(),
|
|
||||||
attachments,
|
|
||||||
})
|
|
||||||
resetComposer()
|
resetComposer()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast(extractErrorMessage(error), 'error')
|
toast(extractErrorMessage(error), 'error')
|
||||||
@@ -526,16 +564,20 @@ const ChatComposer: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex justify-end">
|
{showSubmitButton && (
|
||||||
<Button
|
<div className="mt-6 flex justify-end">
|
||||||
label={submitLabel}
|
<Button
|
||||||
onClick={handleSubmit}
|
label={submitLabel}
|
||||||
className="w-[150px]"
|
onClick={handleSubmit}
|
||||||
isLoading={isBusy}
|
className="w-[150px]"
|
||||||
/>
|
isLoading={isBusy}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
ChatComposer.displayName = 'ChatComposer'
|
||||||
|
|
||||||
export default ChatComposer
|
export default ChatComposer
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, type FC } from 'react'
|
import { useEffect, useRef, useState, type FC } from 'react'
|
||||||
import Request from './components/Request'
|
import Request from './components/Request'
|
||||||
import RequestItemsList from './components/RequestItemsList'
|
import RequestItemsList from './components/RequestItemsList'
|
||||||
import { useRequestStore } from './store/RequestStore'
|
import { useRequestStore } from './store/RequestStore'
|
||||||
@@ -9,6 +9,10 @@ import { Paths } from '@/config/Paths'
|
|||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { ArrowRight2 } from 'iconsax-react'
|
import { ArrowRight2 } from 'iconsax-react'
|
||||||
import ModalConfirm from '@/components/ModalConfirm'
|
import ModalConfirm from '@/components/ModalConfirm'
|
||||||
|
import ChatComposer, {
|
||||||
|
type ChatComposerHandle,
|
||||||
|
type ChatComposerSubmitPayload,
|
||||||
|
} from '@/pages/chat/components/ChatComposer'
|
||||||
|
|
||||||
const NewRequest: FC = () => {
|
const NewRequest: FC = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -17,6 +21,9 @@ const NewRequest: FC = () => {
|
|||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
||||||
const [formKey, setFormKey] = useState(0)
|
const [formKey, setFormKey] = useState(0)
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||||
|
const composerRef = useRef<ChatComposerHandle>(null)
|
||||||
|
const [pendingNote, setPendingNote] =
|
||||||
|
useState<ChatComposerSubmitPayload | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setItems([])
|
setItems([])
|
||||||
@@ -51,23 +58,50 @@ const NewRequest: FC = () => {
|
|||||||
toast('حداقل یک قلم باید به درخواست اضافه کنید.', 'error')
|
toast('حداقل یک قلم باید به درخواست اضافه کنید.', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (composerRef.current?.hasPendingUploads()) {
|
||||||
|
toast('لطفاً تا پایان آپلود فایلها صبر کنید', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (composerRef.current?.hasUploadErrors()) {
|
||||||
|
toast(
|
||||||
|
'برخی فایلها آپلود نشدند. آنها را حذف یا دوباره انتخاب کنید',
|
||||||
|
'error',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = composerRef.current?.getPayload() ?? null
|
||||||
|
setPendingNote(note)
|
||||||
setShowConfirmModal(true)
|
setShowConfirmModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
submitRequest(items, {
|
submitRequest(
|
||||||
onSuccess: () => {
|
{
|
||||||
setShowConfirmModal(false)
|
items,
|
||||||
setItems([])
|
...(pendingNote?.content ? { description: pendingNote.content } : {}),
|
||||||
setEditingIndex(null)
|
...(pendingNote?.attachments?.length
|
||||||
setFormKey((k) => k + 1)
|
? { attachments: pendingNote.attachments }
|
||||||
toast('سفارش با موفقیت ثبت شد', 'success')
|
: {}),
|
||||||
navigate(Paths.myRequests, { state: { refresh: true } })
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
{
|
||||||
toast(extractErrorMessage(error), 'error')
|
onSuccess: () => {
|
||||||
|
setShowConfirmModal(false)
|
||||||
|
setPendingNote(null)
|
||||||
|
composerRef.current?.reset()
|
||||||
|
setItems([])
|
||||||
|
setEditingIndex(null)
|
||||||
|
setFormKey((k) => k + 1)
|
||||||
|
toast('سفارش با موفقیت ثبت شد', 'success')
|
||||||
|
navigate(Paths.myRequests, { state: { refresh: true } })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +118,9 @@ const NewRequest: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='text-description text-xs mt-2 leading-5'>
|
<p className='text-description text-xs mt-2 leading-5'>
|
||||||
برای هر محصول یک قلم اضافه کنید، سپس درخواست را ثبت نهایی کنید.
|
برای هر محصول یک قلم اضافه کنید. در صورت نیاز توضیحات یا فایل را
|
||||||
|
در بخش پایین وارد کنید و از دکمه «ثبت نهایی درخواست» در کنار لیست
|
||||||
|
استفاده کنید.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='flex flex-col-reverse xl:flex-row gap-6 xl:mt-8 mt-4'>
|
<div className='flex flex-col-reverse xl:flex-row gap-6 xl:mt-8 mt-4'>
|
||||||
@@ -117,6 +153,22 @@ const NewRequest: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section className='bg-white rounded-3xl p-6 mt-6'>
|
||||||
|
<h2 className='text-sm font-light mb-1'>توضیحات درخواست</h2>
|
||||||
|
<p className='text-description text-xs mb-6 leading-5'>
|
||||||
|
توضیحات، فایل یا پیام صوتی خود را اینجا بنویسید. برای ارسال
|
||||||
|
درخواست از دکمه کنار لیست اقلام استفاده کنید.
|
||||||
|
</p>
|
||||||
|
<ChatComposer
|
||||||
|
ref={composerRef}
|
||||||
|
onSubmit={async () => undefined}
|
||||||
|
showSubmitButton={false}
|
||||||
|
label='پیام شما'
|
||||||
|
placeholder='توضیحات یا درخواست خود را بنویسید...'
|
||||||
|
allowEmptySubmit
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<ModalConfirm
|
<ModalConfirm
|
||||||
open={showConfirmModal}
|
open={showConfirmModal}
|
||||||
onClose={() => setShowConfirmModal(false)}
|
onClose={() => setShowConfirmModal(false)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { memo, useCallback, useRef, type FC } from 'react'
|
import { memo, useCallback, useRef, type FC } from 'react'
|
||||||
import type { AttributeType, RequestType } from '../type/Types'
|
import type { AttributeType, RequestItemType } from '../type/Types'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { FieldTypeEnum } from '../enum/RequestEnum'
|
import { FieldTypeEnum } from '../enum/RequestEnum'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
@@ -12,7 +12,7 @@ import type { FormikProps } from 'formik'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
attributes?: AttributeType[],
|
attributes?: AttributeType[],
|
||||||
formik: FormikProps<RequestType>
|
formik: FormikProps<RequestItemType>
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttributeFieldProps = {
|
type AttributeFieldProps = {
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import { useCallback, useState, type ChangeEvent, type FC } from 'react'
|
import { useCallback, useState, type ChangeEvent, type FC } from 'react'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import UploadBox from '@/components/UploadBox'
|
|
||||||
import VoiceRecorder from '@/components/VoiceRecorder'
|
|
||||||
import { COLORS } from '@/constants/colors'
|
import { COLORS } from '@/constants/colors'
|
||||||
import { AddSquare, CloseCircle, 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'
|
||||||
import type { AttachmentsType, RequestType, ProductType } from '../type/Types'
|
import type { RequestItemType, ProductType } from '../type/Types'
|
||||||
import { useGetAttributes } from '../hooks/useRequestData'
|
import { useGetAttributes } from '../hooks/useRequestData'
|
||||||
import ManageAttribute from './ManageAttribute'
|
import ManageAttribute from './ManageAttribute'
|
||||||
import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
|
||||||
import { useRequestStore } from '../store/RequestStore'
|
import { useRequestStore } from '../store/RequestStore'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
|
|
||||||
const emptyValues: RequestType = {
|
const emptyValues: RequestItemType = {
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
attachments: [],
|
|
||||||
description: '',
|
|
||||||
attributes: [],
|
attributes: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editIndex: number | null
|
editIndex: number | null
|
||||||
initialItem?: RequestType
|
initialItem?: RequestItemType
|
||||||
onSaved: () => void
|
onSaved: () => void
|
||||||
onCancelEdit?: () => void
|
onCancelEdit?: () => void
|
||||||
}
|
}
|
||||||
@@ -33,57 +28,24 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
|
|
||||||
const setItems = useRequestStore((state) => state.setItems)
|
const setItems = useRequestStore((state) => state.setItems)
|
||||||
const [productSelected, setProductSelected] = useState<ProductType>()
|
const [productSelected, setProductSelected] = useState<ProductType>()
|
||||||
const [voiceFile, setVoiceFile] = useState<File>()
|
|
||||||
const [files, setFiles] = useState<File[]>([])
|
|
||||||
const singleUpload = useSingleUpload()
|
|
||||||
const multiUpload = useMultiUpload()
|
|
||||||
|
|
||||||
const formik = useFormik<RequestType>({
|
const formik = useFormik<RequestItemType>({
|
||||||
initialValues: initialItem ?? emptyValues,
|
initialValues: initialItem ?? emptyValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
productId: Yup.string().required('این فیلد اجباری می باشد'),
|
productId: Yup.string().required('این فیلد اجباری می باشد'),
|
||||||
}),
|
}),
|
||||||
onSubmit: async (values) => {
|
onSubmit: (values) => {
|
||||||
const attachments: AttachmentsType[] = [...(values.attachments ?? [])]
|
|
||||||
|
|
||||||
if (files.length) {
|
|
||||||
await multiUpload.mutateAsync(files, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
data?.data?.forEach((item) => {
|
|
||||||
attachments.push({
|
|
||||||
type: 'uploads_attach',
|
|
||||||
url: item.key,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (voiceFile) {
|
|
||||||
await singleUpload.mutateAsync(voiceFile, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
attachments.push({
|
|
||||||
type: 'voice',
|
|
||||||
url: data?.data?.key,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: RequestType = { ...values, attachments }
|
|
||||||
|
|
||||||
if (isEditing && editIndex !== null) {
|
if (isEditing && editIndex !== null) {
|
||||||
const items = useRequestStore.getState().items
|
const items = useRequestStore.getState().items
|
||||||
const updated = [...items]
|
const updated = [...items]
|
||||||
updated[editIndex] = payload
|
updated[editIndex] = values
|
||||||
setItems(updated)
|
setItems(updated)
|
||||||
} else {
|
} else {
|
||||||
const items = useRequestStore.getState().items
|
const items = useRequestStore.getState().items
|
||||||
setItems([...items, payload])
|
setItems([...items, values])
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles([])
|
|
||||||
setVoiceFile(undefined)
|
|
||||||
onSaved()
|
onSaved()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -92,36 +54,25 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
|
|
||||||
const { data: attributes } = useGetAttributes(resolvedProduct?.id)
|
const { data: attributes } = useGetAttributes(resolvedProduct?.id)
|
||||||
|
|
||||||
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
const handleProductChange = useCallback(
|
||||||
const productId = e.target.value
|
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const productId = e.target.value
|
||||||
|
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
setProductSelected(undefined)
|
setProductSelected(undefined)
|
||||||
formik.setFieldValue('productId', undefined)
|
formik.setFieldValue('productId', undefined)
|
||||||
|
formik.setFieldValue('attributes', [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldValue('productId', productId)
|
||||||
formik.setFieldValue('attributes', [])
|
formik.setFieldValue('attributes', [])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldValue('productId', productId)
|
|
||||||
formik.setFieldValue('attributes', [])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleProductSelect = (product: ProductType | undefined) => {
|
|
||||||
setProductSelected(product)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUploading = singleUpload.isPending || multiUpload.isPending
|
|
||||||
|
|
||||||
const handleDescriptionChange = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
formik.setFieldValue('description', value)
|
|
||||||
},
|
},
|
||||||
[formik.setFieldValue]
|
[formik]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleRecordingComplete = useCallback((blob: Blob) => {
|
const handleProductSelect = useCallback((product: ProductType | undefined) => {
|
||||||
const file = new File([blob], 'recording.wav', { type: blob.type })
|
setProductSelected(product)
|
||||||
setVoiceFile(file)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -163,25 +114,6 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<VoiceRecorder
|
|
||||||
value={formik.values.description ?? ''}
|
|
||||||
onChange={handleDescriptionChange}
|
|
||||||
label='پیام صوتی'
|
|
||||||
onRecordingComplete={handleRecordingComplete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<UploadBox label='آپلود فایل' isMultiple onChange={setFiles} />
|
|
||||||
</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(
|
||||||
@@ -191,7 +123,6 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
: 'bg-transparent border border-primary text-primary'
|
: 'bg-transparent border border-primary text-primary'
|
||||||
)}
|
)}
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
isLoading={isUploading}
|
|
||||||
>
|
>
|
||||||
<div className='flex gap-1 items-center'>
|
<div className='flex gap-1 items-center'>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { type FC } from 'react'
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { Edit, ShoppingCart, TickSquare, Trash } from 'iconsax-react'
|
import { Edit, ShoppingCart, TickSquare, Trash } from 'iconsax-react'
|
||||||
import { useGetProducts } from '../hooks/useRequestData'
|
import { useGetProducts } from '../hooks/useRequestData'
|
||||||
import type { RequestType } from '../type/Types'
|
import type { RequestItemType } from '../type/Types'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
items: RequestType[]
|
items: RequestItemType[]
|
||||||
editingIndex: number | null
|
editingIndex: number | null
|
||||||
onEdit: (index: number) => void
|
onEdit: (index: number) => void
|
||||||
onRemove: (index: number) => void
|
onRemove: (index: number) => void
|
||||||
@@ -58,11 +58,6 @@ const RequestItemsList: FC<Props> = ({
|
|||||||
<div className='text-sm font-medium truncate'>
|
<div className='text-sm font-medium truncate'>
|
||||||
{getProductTitle(item.productId)}
|
{getProductTitle(item.productId)}
|
||||||
</div>
|
</div>
|
||||||
{item.attachments.length > 0 && (
|
|
||||||
<div className='text-description text-xs mt-1'>
|
|
||||||
{item.attachments.length} پیوست
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='flex gap-2 mt-3'>
|
<div className='flex gap-2 mt-3'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import axios from "@/config/axios";
|
|||||||
import type {
|
import type {
|
||||||
AttributeResponseType,
|
AttributeResponseType,
|
||||||
CategoryType,
|
CategoryType,
|
||||||
|
CreateRequestPayload,
|
||||||
MyRequestsResponseType,
|
MyRequestsResponseType,
|
||||||
RequestDetailResponseType,
|
RequestDetailResponseType,
|
||||||
RequestType,
|
|
||||||
ProductsResponseType,
|
ProductsResponseType,
|
||||||
} from "../type/Types";
|
} from "../type/Types";
|
||||||
|
|
||||||
@@ -26,8 +26,24 @@ export const getAttributes = async (productId?: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const submitRequest = async (params: RequestType[]) => {
|
export const submitRequest = async (payload: CreateRequestPayload) => {
|
||||||
const { data } = await axios.post(`/public/request`, { items: params });
|
const { data } = await axios.post(`/public/request`, {
|
||||||
|
items: payload.items.map((item) => ({
|
||||||
|
productId: item.productId,
|
||||||
|
...(item.attributes?.length
|
||||||
|
? {
|
||||||
|
attributes: item.attributes.map((attr) => ({
|
||||||
|
fieldId: attr.fieldId,
|
||||||
|
value: String(attr.value),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
})),
|
||||||
|
...(payload.description ? { description: payload.description } : {}),
|
||||||
|
...(payload.attachments?.length
|
||||||
|
? { attachments: payload.attachments }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,18 @@ export type AttachmentsType = {
|
|||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestType = {
|
export type RequestItemType = {
|
||||||
productId?: string;
|
productId?: string;
|
||||||
attributes: {
|
attributes: {
|
||||||
fieldId: string;
|
fieldId: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
}[];
|
}[];
|
||||||
attachments: AttachmentsType[];
|
};
|
||||||
|
|
||||||
|
export type CreateRequestPayload = {
|
||||||
|
items: RequestItemType[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
attachments?: AttachmentsType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AttributeValueType = {
|
export type AttributeValueType = {
|
||||||
@@ -64,8 +68,8 @@ export type ProductsResponseType = BaseResponse<ProductType[]>;
|
|||||||
export type CategoriesResponseType = BaseResponse<CategoryType[]>;
|
export type CategoriesResponseType = BaseResponse<CategoryType[]>;
|
||||||
|
|
||||||
export type StoreType = {
|
export type StoreType = {
|
||||||
items: RequestType[];
|
items: RequestItemType[];
|
||||||
setItems: (value: RequestType[]) => void;
|
setItems: (value: RequestItemType[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MyRequestType extends RowDataType {
|
export interface MyRequestType extends RowDataType {
|
||||||
|
|||||||
Reference in New Issue
Block a user