From cd8677648e10970e968478e61e4951358b15f61f Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 21 Jul 2026 12:17:54 +0330 Subject: [PATCH] up --- src/pages/chat/components/ChatComposer.tsx | 102 +++++++++++----- src/pages/request/NewRequest.tsx | 78 ++++++++++-- .../request/components/ManageAttribute.tsx | 4 +- src/pages/request/components/Request.tsx | 111 ++++-------------- .../request/components/RequestItemsList.tsx | 9 +- src/pages/request/service/RequestService.ts | 22 +++- src/pages/request/type/Types.ts | 12 +- 7 files changed, 189 insertions(+), 149 deletions(-) diff --git a/src/pages/chat/components/ChatComposer.tsx b/src/pages/chat/components/ChatComposer.tsx index 4003710..6837330 100644 --- a/src/pages/chat/components/ChatComposer.tsx +++ b/src/pages/chat/components/ChatComposer.tsx @@ -12,9 +12,11 @@ import { Play, } from 'iconsax-react' import { + forwardRef, useCallback, useEffect, useId, + useImperativeHandle, useRef, useState, type ChangeEvent, @@ -47,12 +49,21 @@ type PendingVoice = { status: 'uploading' | 'done' | 'error' } +export type ChatComposerHandle = { + getPayload: () => ChatComposerSubmitPayload + hasPendingUploads: () => boolean + hasUploadErrors: () => boolean + reset: () => void +} + type Props = { onSubmit: (payload: ChatComposerSubmitPayload) => void | Promise isSubmitting?: boolean submitLabel?: string label?: string placeholder?: string + allowEmptySubmit?: boolean + showSubmitButton?: boolean replyTo?: { id: string content: string @@ -163,15 +174,17 @@ const VoicePreviewChip: FC<{ ) } -const ChatComposer: FC = ({ +const ChatComposer = forwardRef(({ onSubmit, isSubmitting = false, submitLabel = 'ارسال پیام', label = 'پیام شما', placeholder = 'متن پیام خود را بنویسید...', + allowEmptySubmit = false, + showSubmitButton = true, replyTo = null, onCancelReply, -}) => { +}, ref) => { const inputId = useId() const fileInputRef = useRef(null) const [message, setMessage] = useState('') @@ -318,7 +331,7 @@ const ChatComposer: FC = ({ }) } - const handleSubmit = async () => { + const buildPayload = useCallback((): ChatComposerSubmitPayload => { const uploadedFiles = pendingFiles.filter( (item) => item.status === 'done' && item.key, ) @@ -326,10 +339,49 @@ const ChatComposer: FC = ({ (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 ( - !message.trim() && - uploadedFiles.length === 0 && - uploadedVoices.length === 0 + !allowEmptySubmit && + !payload.content && + payload.attachments.length === 0 ) { toast('لطفاً پیام یا فایل ضمیمه وارد کنید', 'error') return @@ -351,22 +403,8 @@ const ChatComposer: FC = ({ return } - const attachments: ChatComposerAttachment[] = [ - ...uploadedFiles.map((item) => ({ - type: 'uploads_attach', - url: item.key!, - })), - ...uploadedVoices.map((item) => ({ - type: 'voice', - url: item.key!, - })), - ] - try { - await onSubmit({ - content: message.trim(), - attachments, - }) + await onSubmit(payload) resetComposer() } catch (error) { toast(extractErrorMessage(error), 'error') @@ -526,16 +564,20 @@ const ChatComposer: FC = ({ -
-
+ {showSubmitButton && ( +
+
+ )} ) -} +}) + +ChatComposer.displayName = 'ChatComposer' export default ChatComposer diff --git a/src/pages/request/NewRequest.tsx b/src/pages/request/NewRequest.tsx index dff2e0a..9818753 100644 --- a/src/pages/request/NewRequest.tsx +++ b/src/pages/request/NewRequest.tsx @@ -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 RequestItemsList from './components/RequestItemsList' import { useRequestStore } from './store/RequestStore' @@ -9,6 +9,10 @@ import { Paths } from '@/config/Paths' import { extractErrorMessage } from '@/config/func' import { ArrowRight2 } from 'iconsax-react' import ModalConfirm from '@/components/ModalConfirm' +import ChatComposer, { + type ChatComposerHandle, + type ChatComposerSubmitPayload, +} from '@/pages/chat/components/ChatComposer' const NewRequest: FC = () => { const navigate = useNavigate() @@ -17,6 +21,9 @@ const NewRequest: FC = () => { const [editingIndex, setEditingIndex] = useState(null) const [formKey, setFormKey] = useState(0) const [showConfirmModal, setShowConfirmModal] = useState(false) + const composerRef = useRef(null) + const [pendingNote, setPendingNote] = + useState(null) useEffect(() => { setItems([]) @@ -51,23 +58,50 @@ const NewRequest: FC = () => { toast('حداقل یک قلم باید به درخواست اضافه کنید.', 'error') 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) } const onSubmit = () => { - submitRequest(items, { - onSuccess: () => { - setShowConfirmModal(false) - setItems([]) - setEditingIndex(null) - setFormKey((k) => k + 1) - toast('سفارش با موفقیت ثبت شد', 'success') - navigate(Paths.myRequests, { state: { refresh: true } }) + submitRequest( + { + items, + ...(pendingNote?.content ? { description: pendingNote.content } : {}), + ...(pendingNote?.attachments?.length + ? { attachments: pendingNote.attachments } + : {}), }, - 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 ( @@ -84,7 +118,9 @@ const NewRequest: FC = () => {

- برای هر محصول یک قلم اضافه کنید، سپس درخواست را ثبت نهایی کنید. + برای هر محصول یک قلم اضافه کنید. در صورت نیاز توضیحات یا فایل را + در بخش پایین وارد کنید و از دکمه «ثبت نهایی درخواست» در کنار لیست + استفاده کنید.

@@ -117,6 +153,22 @@ const NewRequest: FC = () => { />
+
+

توضیحات درخواست

+

+ توضیحات، فایل یا پیام صوتی خود را اینجا بنویسید. برای ارسال + درخواست از دکمه کنار لیست اقلام استفاده کنید. +

+ undefined} + showSubmitButton={false} + label='پیام شما' + placeholder='توضیحات یا درخواست خود را بنویسید...' + allowEmptySubmit + /> +
+ setShowConfirmModal(false)} diff --git a/src/pages/request/components/ManageAttribute.tsx b/src/pages/request/components/ManageAttribute.tsx index 196afae..2c10c9c 100644 --- a/src/pages/request/components/ManageAttribute.tsx +++ b/src/pages/request/components/ManageAttribute.tsx @@ -1,5 +1,5 @@ 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 { FieldTypeEnum } from '../enum/RequestEnum' import Select from '@/components/Select' @@ -12,7 +12,7 @@ import type { FormikProps } from 'formik' type Props = { attributes?: AttributeType[], - formik: FormikProps + formik: FormikProps } type AttributeFieldProps = { diff --git a/src/pages/request/components/Request.tsx b/src/pages/request/components/Request.tsx index 1b34bcf..f20c493 100644 --- a/src/pages/request/components/Request.tsx +++ b/src/pages/request/components/Request.tsx @@ -1,29 +1,24 @@ import { useCallback, useState, type ChangeEvent, type FC } from 'react' import Button from '@/components/Button' -import UploadBox from '@/components/UploadBox' -import VoiceRecorder from '@/components/VoiceRecorder' import { COLORS } from '@/constants/colors' import { AddSquare, CloseCircle, Edit } from 'iconsax-react' import ProductsSelect from './ProductsSelect' import { useFormik } from 'formik' 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 ManageAttribute from './ManageAttribute' -import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader' import { useRequestStore } from '../store/RequestStore' import { clx } from '@/helpers/utils' -const emptyValues: RequestType = { +const emptyValues: RequestItemType = { productId: undefined, - attachments: [], - description: '', attributes: [], } type Props = { editIndex: number | null - initialItem?: RequestType + initialItem?: RequestItemType onSaved: () => void onCancelEdit?: () => void } @@ -33,57 +28,24 @@ const Request: FC = ({ editIndex, initialItem, onSaved, onCancelEdit }) = const setItems = useRequestStore((state) => state.setItems) const [productSelected, setProductSelected] = useState() - const [voiceFile, setVoiceFile] = useState() - const [files, setFiles] = useState([]) - const singleUpload = useSingleUpload() - const multiUpload = useMultiUpload() - const formik = useFormik({ + const formik = useFormik({ initialValues: initialItem ?? emptyValues, enableReinitialize: true, validationSchema: Yup.object({ productId: Yup.string().required('این فیلد اجباری می باشد'), }), - onSubmit: async (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 } - + onSubmit: (values) => { if (isEditing && editIndex !== null) { const items = useRequestStore.getState().items const updated = [...items] - updated[editIndex] = payload + updated[editIndex] = values setItems(updated) } else { const items = useRequestStore.getState().items - setItems([...items, payload]) + setItems([...items, values]) } - setFiles([]) - setVoiceFile(undefined) onSaved() }, }) @@ -92,36 +54,25 @@ const Request: FC = ({ editIndex, initialItem, onSaved, onCancelEdit }) = const { data: attributes } = useGetAttributes(resolvedProduct?.id) - const handleProductChange = (e: ChangeEvent) => { - const productId = e.target.value + const handleProductChange = useCallback( + (e: ChangeEvent) => { + const productId = e.target.value - if (!productId) { - setProductSelected(undefined) - formik.setFieldValue('productId', undefined) + if (!productId) { + setProductSelected(undefined) + formik.setFieldValue('productId', undefined) + formik.setFieldValue('attributes', []) + return + } + + formik.setFieldValue('productId', productId) 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 file = new File([blob], 'recording.wav', { type: blob.type }) - setVoiceFile(file) + const handleProductSelect = useCallback((product: ProductType | undefined) => { + setProductSelected(product) }, []) return ( @@ -163,25 +114,6 @@ const Request: FC = ({ editIndex, initialItem, onSaved, onCancelEdit }) = /> -
- -
- -
- -
- - {isEditing && (initialItem?.attachments?.length ?? 0) > 0 && files.length === 0 && ( -

- {initialItem!.attachments.length} پیوست قبلی حفظ می‌شود. برای جایگزینی، فایل جدید آپلود کنید. -

- )} -