diff --git a/src/pages/request/NewRequest.tsx b/src/pages/request/NewRequest.tsx index 9818753..a3e34d0 100644 --- a/src/pages/request/NewRequest.tsx +++ b/src/pages/request/NewRequest.tsx @@ -22,8 +22,7 @@ const NewRequest: FC = () => { const [formKey, setFormKey] = useState(0) const [showConfirmModal, setShowConfirmModal] = useState(false) const composerRef = useRef(null) - const [pendingNote, setPendingNote] = - useState(null) + const [pendingNote, setPendingNote] = useState(null) useEffect(() => { setItems([]) @@ -49,9 +48,7 @@ const NewRequest: FC = () => { } } - const handleEdit = (index: number) => { - setEditingIndex(index) - } + const handleEdit = (index: number) => setEditingIndex(index) const openSubmitConfirm = () => { if (items.length === 0) { @@ -65,15 +62,11 @@ const NewRequest: FC = () => { } if (composerRef.current?.hasUploadErrors()) { - toast( - 'برخی فایل‌ها آپلود نشدند. آن‌ها را حذف یا دوباره انتخاب کنید', - 'error', - ) + toast('برخی فایل‌ها آپلود نشدند. آن‌ها را حذف یا دوباره انتخاب کنید', 'error') return } - const note = composerRef.current?.getPayload() ?? null - setPendingNote(note) + setPendingNote(composerRef.current?.getPayload() ?? null) setShowConfirmModal(true) } @@ -82,9 +75,7 @@ const NewRequest: FC = () => { { items, ...(pendingNote?.content ? { description: pendingNote.content } : {}), - ...(pendingNote?.attachments?.length - ? { attachments: pendingNote.attachments } - : {}), + ...(pendingNote?.attachments?.length ? { attachments: pendingNote.attachments } : {}), }, { onSuccess: () => { @@ -105,32 +96,31 @@ const NewRequest: FC = () => { } return ( -
-
-

درخواست جدید

+
+ {/* Page header */} +
+

درخواست جدید

بازگشت به لیست
-

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

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

-
-
+ {/* Main content */} +
+ {/* Left column: form + notes */} +
{ : undefined } /> + +
+

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

+

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

+ undefined} + showSubmitButton={false} + label='پیام شما' + placeholder='توضیحات یا درخواست خود را بنویسید...' + allowEmptySubmit + /> +
+ {/* Right column: items list (sticky on xl) */} { />
-
-

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

-

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

- undefined} - showSubmitButton={false} - label='پیام شما' - placeholder='توضیحات یا درخواست خود را بنویسید...' - allowEmptySubmit - /> -
- setShowConfirmModal(false)} diff --git a/src/pages/request/components/AvatarSelectionGrid.tsx b/src/pages/request/components/AvatarSelectionGrid.tsx new file mode 100644 index 0000000..6a66d0a --- /dev/null +++ b/src/pages/request/components/AvatarSelectionGrid.tsx @@ -0,0 +1,112 @@ +import { type FC } from 'react' +import { clx } from '@/helpers/utils' +import PresignedImage from '@/components/PresignedImage' +import placeholderImage from '@/assets/images/placeholder-product.svg' + +export type AvatarSelectionOption = { + id: string + title: string + imageUrl?: string +} + +type Props = { + label: string + items: AvatarSelectionOption[] + selectedId?: string + onSelect: (id: string) => void + isLoading?: boolean + emptyMessage?: string + error_text?: string +} + +const SKELETON_COUNT = 6 + +const AvatarSelectionGrid: FC = ({ + label, + items, + selectedId, + onSelect, + isLoading = false, + emptyMessage = 'موردی یافت نشد', + error_text, +}) => { + return ( +
+ + +
+ {isLoading ? ( +
+ {Array.from({ length: SKELETON_COUNT }).map((_, index) => ( +
+
+
+
+ ))} +
+ ) : items.length === 0 ? ( +

{emptyMessage}

+ ) : ( +
+ {items.map((item) => { + const isSelected = selectedId === item.id + + return ( + + ) + })} +
+ )} +
+ + {error_text && ( +

+ {error_text} +

+ )} +
+ ) +} + +export default AvatarSelectionGrid diff --git a/src/pages/request/components/ProductsSelect.tsx b/src/pages/request/components/ProductsSelect.tsx index 0afc5b6..77b619b 100644 --- a/src/pages/request/components/ProductsSelect.tsx +++ b/src/pages/request/components/ProductsSelect.tsx @@ -1,46 +1,39 @@ import { type ChangeEvent, type FC, type SelectHTMLAttributes, useEffect, useMemo, useState } from 'react' import { useGetCategories, useGetProducts } from '../hooks/useRequestData' -import Select from '@/components/Select' import type { CategoryType, ProductType } from '../type/Types' import { extractList, findCategoryById, findCategoryPath, getCategoriesAtLevel, + getLevelsToShow, } from '../utils/categoryUtils' +import AvatarSelectionGrid from './AvatarSelectionGrid' type Props = { - error_text?: string, - onProductSelect?: (product: ProductType | undefined) => void, + error_text?: string + onProductSelect?: (product: ProductType | undefined) => void } & SelectHTMLAttributes const ProductsSelect: FC = (props) => { - const { error_text, onProductSelect, value, onChange, ...rest } = props + const { error_text, onProductSelect, value, onChange } = props const [selectedPath, setSelectedPath] = useState([]) - const { data: categoriesData } = useGetCategories() + const { data: categoriesData, isLoading: isCategoriesLoading } = useGetCategories() const categories = useMemo( () => extractList(categoriesData), [categoriesData], ) - const levelsToShow = useMemo(() => { - const levels = [0] - - for (let level = 0; level < selectedPath.length; level++) { - const category = findCategoryById(categories, selectedPath[level]) - if (category?.children?.length) { - levels.push(level + 1) - } - } - - return levels - }, [categories, selectedPath]) + const levelsToShow = useMemo( + () => getLevelsToShow(categories, selectedPath), + [categories, selectedPath], + ) const activeCategoryId = selectedPath.at(-1) ?? '' const canLoadProducts = !!activeCategoryId - const { data: productsData } = useGetProducts(activeCategoryId || undefined, { + const { data: productsData, isLoading: isProductsLoading } = useGetProducts(activeCategoryId || undefined, { enabled: canLoadProducts, }) @@ -59,14 +52,7 @@ const ProductsSelect: FC = (props) => { [resolveProductsData], ) - const productItems = useMemo( - () => products.map((product) => ({ - label: product.title, - value: product.id, - })), - [products], - ) - + // Resolve initial selectedPath when editing an existing item useEffect(() => { if (!value || selectedPath.length || !categories.length) return @@ -83,6 +69,7 @@ const ProductsSelect: FC = (props) => { setSelectedPath(path) }, [value, resolveProducts, categories, selectedPath.length]) + // Emit onProductSelect whenever the selected product changes useEffect(() => { if (!value) { onProductSelect?.(undefined) @@ -101,47 +88,59 @@ const ProductsSelect: FC = (props) => { } as ChangeEvent) } - const handleCategoryChange = (level: number, nextValue: string) => { - setSelectedPath((prev) => [...prev.slice(0, level), nextValue]) + const handleCategorySelect = (level: number, categoryId: string) => { + setSelectedPath((prev) => [...prev.slice(0, level), categoryId]) emitProductChange('') + + // If the selected category has children, keep drilling; don't auto-select product + const category = findCategoryById(categories, categoryId) + if (category?.children?.length) return } - const handleProductChange = (e: ChangeEvent) => { - onChange?.(e) - - const product = products.find((item) => item.id === e.target.value) + const handleProductSelect = (productId: string) => { + emitProductChange(productId) + const product = products.find((item) => item.id === productId) onProductSelect?.(product) } return ( -
+
{levelsToShow.map((level) => { - const items = getCategoriesAtLevel(categories, level, selectedPath).map((category) => ({ - label: category.title, - value: category.id, - })) + const levelCategories = getCategoriesAtLevel(categories, level, selectedPath) + const levelLabel = level === 0 ? 'دسته‌بندی' : 'زیر دسته‌بندی' return ( - + + {canLoadProducts && ( + ({ + id: product.id, + title: product.title, + imageUrl: product.images?.[0], + }))} + selectedId={typeof value === 'string' ? value : undefined} + onSelect={handleProductSelect} + error_text={error_text} + emptyMessage='محصولی برای این دسته‌بندی یافت نشد' + /> + )}
) } diff --git a/src/pages/request/components/Request.tsx b/src/pages/request/components/Request.tsx index f20c493..771a168 100644 --- a/src/pages/request/components/Request.tsx +++ b/src/pages/request/components/Request.tsx @@ -1,7 +1,7 @@ import { useCallback, useState, type ChangeEvent, type FC } from 'react' import Button from '@/components/Button' import { COLORS } from '@/constants/colors' -import { AddSquare, CloseCircle, Edit } from 'iconsax-react' +import { AddSquare, CloseCircle, Edit2 } from 'iconsax-react' import ProductsSelect from './ProductsSelect' import { useFormik } from 'formik' import * as Yup from 'yup' @@ -25,7 +25,6 @@ type Props = { const Request: FC = ({ editIndex, initialItem, onSaved, onCancelEdit }) => { const isEditing = editIndex !== null - const setItems = useRequestStore((state) => state.setItems) const [productSelected, setProductSelected] = useState() @@ -45,14 +44,11 @@ const Request: FC = ({ editIndex, initialItem, onSaved, onCancelEdit }) = const items = useRequestStore.getState().items setItems([...items, values]) } - onSaved() }, }) - const resolvedProduct = productSelected - - const { data: attributes } = useGetAttributes(resolvedProduct?.id) + const { data: attributes } = useGetAttributes(productSelected?.id) const handleProductChange = useCallback( (e: ChangeEvent) => { @@ -68,7 +64,7 @@ const Request: FC = ({ editIndex, initialItem, onSaved, onCancelEdit }) = formik.setFieldValue('productId', productId) formik.setFieldValue('attributes', []) }, - [formik] + [formik], ) const handleProductSelect = useCallback((product: ProductType | undefined) => { @@ -77,23 +73,36 @@ const Request: FC = ({ editIndex, initialItem, onSaved, onCancelEdit }) = return (
-
-
- {isEditing ? 'ویرایش قلم' : 'افزودن قلم جدید'} + {/* Section header */} +
+
+
+ {isEditing ? : } +
+ + {isEditing ? 'ویرایش قلم' : 'افزودن قلم جدید'} +
+ {isEditing && onCancelEdit && ( )}
-
+ {/* Product selector */} +
= ({ editIndex, initialItem, onSaved, onCancelEdit }) = />
-
- -
+ {/* Attributes */} + + {/* Action button */}
diff --git a/src/pages/request/components/RequestItemsList.tsx b/src/pages/request/components/RequestItemsList.tsx index 0af7ea6..0e6cbcd 100644 --- a/src/pages/request/components/RequestItemsList.tsx +++ b/src/pages/request/components/RequestItemsList.tsx @@ -1,6 +1,6 @@ import { type FC } from 'react' import Button from '@/components/Button' -import { Edit, ShoppingCart, TickSquare, Trash } from 'iconsax-react' +import { Edit2, ShoppingCart, TickSquare, Trash } from 'iconsax-react' import { useGetProducts } from '../hooks/useRequestData' import type { RequestItemType } from '../type/Types' import { clx } from '@/helpers/utils' @@ -28,60 +28,75 @@ const RequestItemsList: FC = ({ productsData?.data?.find((p) => p.id === productId)?.title ?? 'محصول' return ( -
-
- - اقلام درخواست +
+ {/* Header */} +
+ + اقلام درخواست {items.length > 0 && ( - - {items.length} + + {items.length} قلم )}
- {items.length === 0 ? ( -

- هنوز قلمی اضافه نشده. فرم را پر کنید و روی «افزودن به لیست» بزنید. -

- ) : ( -
    - {items.map((item, index) => ( -
  • -
    - {getProductTitle(item.productId)} -
    -
    - - -
    -
  • - ))} -
- )} + {/* Items */} +
+ {items.length === 0 ? ( +
+
+ +
+

+ هنوز قلمی اضافه نشده. فرم را پر کنید و روی «افزودن به لیست» بزنید. +

+
+ ) : ( +
    + {items.map((item, index) => ( +
  • +
    + + {getProductTitle(item.productId)} + + + #{index + 1} + +
    +
    + + +
    +
  • + ))} +
+ )} +
-
+ {/* Submit footer */} +