This commit is contained in:
@@ -22,8 +22,7 @@ const NewRequest: FC = () => {
|
|||||||
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 composerRef = useRef<ChatComposerHandle>(null)
|
||||||
const [pendingNote, setPendingNote] =
|
const [pendingNote, setPendingNote] = useState<ChatComposerSubmitPayload | null>(null)
|
||||||
useState<ChatComposerSubmitPayload | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setItems([])
|
setItems([])
|
||||||
@@ -49,9 +48,7 @@ const NewRequest: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = (index: number) => {
|
const handleEdit = (index: number) => setEditingIndex(index)
|
||||||
setEditingIndex(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openSubmitConfirm = () => {
|
const openSubmitConfirm = () => {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -65,15 +62,11 @@ const NewRequest: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (composerRef.current?.hasUploadErrors()) {
|
if (composerRef.current?.hasUploadErrors()) {
|
||||||
toast(
|
toast('برخی فایلها آپلود نشدند. آنها را حذف یا دوباره انتخاب کنید', 'error')
|
||||||
'برخی فایلها آپلود نشدند. آنها را حذف یا دوباره انتخاب کنید',
|
|
||||||
'error',
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const note = composerRef.current?.getPayload() ?? null
|
setPendingNote(composerRef.current?.getPayload() ?? null)
|
||||||
setPendingNote(note)
|
|
||||||
setShowConfirmModal(true)
|
setShowConfirmModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,9 +75,7 @@ const NewRequest: FC = () => {
|
|||||||
{
|
{
|
||||||
items,
|
items,
|
||||||
...(pendingNote?.content ? { description: pendingNote.content } : {}),
|
...(pendingNote?.content ? { description: pendingNote.content } : {}),
|
||||||
...(pendingNote?.attachments?.length
|
...(pendingNote?.attachments?.length ? { attachments: pendingNote.attachments } : {}),
|
||||||
? { attachments: pendingNote.attachments }
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -105,32 +96,31 @@ const NewRequest: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4 pb-12'>
|
<div className='pb-16'>
|
||||||
<div className='flex items-center justify-between gap-4'>
|
{/* Page header */}
|
||||||
<h1 className='text-lg font-light'>درخواست جدید</h1>
|
<div className='flex items-center justify-between gap-4 mb-1'>
|
||||||
|
<h1 className='text-lg font-semibold'>درخواست جدید</h1>
|
||||||
<Link
|
<Link
|
||||||
to={Paths.myRequests}
|
to={Paths.myRequests}
|
||||||
className='flex items-center gap-1 text-sm text-description hover:text-black shrink-0'
|
className='flex items-center gap-1 text-sm text-description hover:text-black transition-colors shrink-0'
|
||||||
>
|
>
|
||||||
<ArrowRight2 size={18} color='currentColor' />
|
<ArrowRight2 size={18} color='currentColor' />
|
||||||
بازگشت به لیست
|
بازگشت به لیست
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='text-description text-xs mt-2 leading-5'>
|
<p className='text-description text-xs leading-6 mt-1 max-w-xl'>
|
||||||
برای هر محصول یک قلم اضافه کنید. در صورت نیاز توضیحات یا فایل را
|
برای هر محصول یک قلم اضافه کنید. در صورت نیاز توضیحات یا فایل را در بخش پایین وارد کنید و از دکمه «ثبت نهایی درخواست» استفاده کنید.
|
||||||
در بخش پایین وارد کنید و از دکمه «ثبت نهایی درخواست» در کنار لیست
|
|
||||||
استفاده کنید.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='flex flex-col-reverse xl:flex-row gap-6 xl:mt-8 mt-4'>
|
{/* Main content */}
|
||||||
<div className='flex-1 min-w-0'>
|
<div className='flex flex-col xl:flex-row gap-6 mt-6'>
|
||||||
|
{/* Left column: form + notes */}
|
||||||
|
<div className='flex-1 min-w-0 flex flex-col gap-6'>
|
||||||
<Request
|
<Request
|
||||||
key={editingIndex !== null ? `edit-${editingIndex}` : `new-${formKey}`}
|
key={editingIndex !== null ? `edit-${editingIndex}` : `new-${formKey}`}
|
||||||
editIndex={editingIndex}
|
editIndex={editingIndex}
|
||||||
initialItem={
|
initialItem={editingIndex !== null ? items[editingIndex] : undefined}
|
||||||
editingIndex !== null ? items[editingIndex] : undefined
|
|
||||||
}
|
|
||||||
onSaved={handleSaved}
|
onSaved={handleSaved}
|
||||||
onCancelEdit={
|
onCancelEdit={
|
||||||
editingIndex !== null
|
editingIndex !== null
|
||||||
@@ -141,8 +131,24 @@ const NewRequest: FC = () => {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<section className='bg-white rounded-3xl p-6'>
|
||||||
|
<h2 className='text-sm font-medium mb-1'>توضیحات درخواست</h2>
|
||||||
|
<p className='text-description text-xs mb-5 leading-6'>
|
||||||
|
توضیحات، فایل یا پیام صوتی خود را اینجا بنویسید.
|
||||||
|
</p>
|
||||||
|
<ChatComposer
|
||||||
|
ref={composerRef}
|
||||||
|
onSubmit={async () => undefined}
|
||||||
|
showSubmitButton={false}
|
||||||
|
label='پیام شما'
|
||||||
|
placeholder='توضیحات یا درخواست خود را بنویسید...'
|
||||||
|
allowEmptySubmit
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right column: items list (sticky on xl) */}
|
||||||
<RequestItemsList
|
<RequestItemsList
|
||||||
items={items}
|
items={items}
|
||||||
editingIndex={editingIndex}
|
editingIndex={editingIndex}
|
||||||
@@ -153,22 +159,6 @@ 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)}
|
||||||
|
|||||||
@@ -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<Props> = ({
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
selectedId,
|
||||||
|
onSelect,
|
||||||
|
isLoading = false,
|
||||||
|
emptyMessage = 'موردی یافت نشد',
|
||||||
|
error_text,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<label className='text-sm font-medium text-primary-content'>{label}</label>
|
||||||
|
|
||||||
|
<div className='mt-3 w-full overflow-x-auto scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent pb-1'>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className='flex flex-nowrap gap-3'>
|
||||||
|
{Array.from({ length: SKELETON_COUNT }).map((_, index) => (
|
||||||
|
<div key={index} className='flex w-[72px] shrink-0 flex-col items-center gap-2'>
|
||||||
|
<div className='size-14 animate-pulse rounded-full bg-gray-200' />
|
||||||
|
<div className='h-6 w-full animate-pulse rounded-md bg-gray-100' />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<p className='text-sm font-light text-[#7B7E8B] py-2'>{emptyMessage}</p>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-nowrap gap-3'>
|
||||||
|
{items.map((item) => {
|
||||||
|
const isSelected = selectedId === item.id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type='button'
|
||||||
|
onClick={() => onSelect(item.id)}
|
||||||
|
className={clx(
|
||||||
|
'flex w-[72px] shrink-0 flex-col items-center gap-2 text-center rounded-2xl p-1.5 transition-all',
|
||||||
|
isSelected
|
||||||
|
? 'bg-primary/8'
|
||||||
|
: 'hover:bg-gray-50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
'size-14 overflow-hidden rounded-full border-2 bg-[#F5F7FC] transition-all',
|
||||||
|
isSelected
|
||||||
|
? 'border-primary shadow-sm shadow-primary/20'
|
||||||
|
: 'border-transparent',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.imageUrl ? (
|
||||||
|
<PresignedImage
|
||||||
|
src={item.imageUrl}
|
||||||
|
alt={item.title}
|
||||||
|
className='size-full object-cover'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={placeholderImage}
|
||||||
|
alt={item.title}
|
||||||
|
className='size-full object-cover'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={clx(
|
||||||
|
'line-clamp-2 w-full text-[11px] leading-4',
|
||||||
|
isSelected ? 'text-primary font-medium' : 'text-[#292D32] font-light',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error_text && (
|
||||||
|
<p className='mt-2 mr-1 text-right text-xs font-medium text-red-500'>
|
||||||
|
{error_text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarSelectionGrid
|
||||||
@@ -1,46 +1,39 @@
|
|||||||
import { type ChangeEvent, type FC, type SelectHTMLAttributes, useEffect, useMemo, useState } from 'react'
|
import { type ChangeEvent, type FC, type SelectHTMLAttributes, useEffect, useMemo, useState } from 'react'
|
||||||
import { useGetCategories, useGetProducts } from '../hooks/useRequestData'
|
import { useGetCategories, useGetProducts } from '../hooks/useRequestData'
|
||||||
import Select from '@/components/Select'
|
|
||||||
import type { CategoryType, ProductType } from '../type/Types'
|
import type { CategoryType, ProductType } from '../type/Types'
|
||||||
import {
|
import {
|
||||||
extractList,
|
extractList,
|
||||||
findCategoryById,
|
findCategoryById,
|
||||||
findCategoryPath,
|
findCategoryPath,
|
||||||
getCategoriesAtLevel,
|
getCategoriesAtLevel,
|
||||||
|
getLevelsToShow,
|
||||||
} from '../utils/categoryUtils'
|
} from '../utils/categoryUtils'
|
||||||
|
import AvatarSelectionGrid from './AvatarSelectionGrid'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error_text?: string,
|
error_text?: string
|
||||||
onProductSelect?: (product: ProductType | undefined) => void,
|
onProductSelect?: (product: ProductType | undefined) => void
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
const ProductsSelect: FC<Props> = (props) => {
|
const ProductsSelect: FC<Props> = (props) => {
|
||||||
const { error_text, onProductSelect, value, onChange, ...rest } = props
|
const { error_text, onProductSelect, value, onChange } = props
|
||||||
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
||||||
|
|
||||||
const { data: categoriesData } = useGetCategories()
|
const { data: categoriesData, isLoading: isCategoriesLoading } = useGetCategories()
|
||||||
const categories = useMemo(
|
const categories = useMemo(
|
||||||
() => extractList<CategoryType>(categoriesData),
|
() => extractList<CategoryType>(categoriesData),
|
||||||
[categoriesData],
|
[categoriesData],
|
||||||
)
|
)
|
||||||
|
|
||||||
const levelsToShow = useMemo(() => {
|
const levelsToShow = useMemo(
|
||||||
const levels = [0]
|
() => getLevelsToShow(categories, selectedPath),
|
||||||
|
[categories, selectedPath],
|
||||||
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 activeCategoryId = selectedPath.at(-1) ?? ''
|
const activeCategoryId = selectedPath.at(-1) ?? ''
|
||||||
const canLoadProducts = !!activeCategoryId
|
const canLoadProducts = !!activeCategoryId
|
||||||
|
|
||||||
const { data: productsData } = useGetProducts(activeCategoryId || undefined, {
|
const { data: productsData, isLoading: isProductsLoading } = useGetProducts(activeCategoryId || undefined, {
|
||||||
enabled: canLoadProducts,
|
enabled: canLoadProducts,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -59,14 +52,7 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
[resolveProductsData],
|
[resolveProductsData],
|
||||||
)
|
)
|
||||||
|
|
||||||
const productItems = useMemo(
|
// Resolve initial selectedPath when editing an existing item
|
||||||
() => products.map((product) => ({
|
|
||||||
label: product.title,
|
|
||||||
value: product.id,
|
|
||||||
})),
|
|
||||||
[products],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!value || selectedPath.length || !categories.length) return
|
if (!value || selectedPath.length || !categories.length) return
|
||||||
|
|
||||||
@@ -83,6 +69,7 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
}, [value, resolveProducts, categories, selectedPath.length])
|
}, [value, resolveProducts, categories, selectedPath.length])
|
||||||
|
|
||||||
|
// Emit onProductSelect whenever the selected product changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
onProductSelect?.(undefined)
|
onProductSelect?.(undefined)
|
||||||
@@ -101,47 +88,59 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
} as ChangeEvent<HTMLSelectElement>)
|
} as ChangeEvent<HTMLSelectElement>)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCategoryChange = (level: number, nextValue: string) => {
|
const handleCategorySelect = (level: number, categoryId: string) => {
|
||||||
setSelectedPath((prev) => [...prev.slice(0, level), nextValue])
|
setSelectedPath((prev) => [...prev.slice(0, level), categoryId])
|
||||||
emitProductChange('')
|
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<HTMLSelectElement>) => {
|
const handleProductSelect = (productId: string) => {
|
||||||
onChange?.(e)
|
emitProductChange(productId)
|
||||||
|
const product = products.find((item) => item.id === productId)
|
||||||
const product = products.find((item) => item.id === e.target.value)
|
|
||||||
onProductSelect?.(product)
|
onProductSelect?.(product)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4'>
|
<div className='space-y-6'>
|
||||||
{levelsToShow.map((level) => {
|
{levelsToShow.map((level) => {
|
||||||
const items = getCategoriesAtLevel(categories, level, selectedPath).map((category) => ({
|
const levelCategories = getCategoriesAtLevel(categories, level, selectedPath)
|
||||||
label: category.title,
|
const levelLabel = level === 0 ? 'دستهبندی' : 'زیر دستهبندی'
|
||||||
value: category.id,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<AvatarSelectionGrid
|
||||||
key={level}
|
key={level}
|
||||||
items={items}
|
label={levelLabel}
|
||||||
label={level === 0 ? 'دستهبندی' : 'زیر دستهبندی'}
|
isLoading={isCategoriesLoading}
|
||||||
placeholder={level === 0 ? 'انتخاب دستهبندی' : 'انتخاب زیر دستهبندی'}
|
items={levelCategories.map((cat) => ({
|
||||||
value={selectedPath[level] ?? ''}
|
id: cat.id,
|
||||||
onChange={(e) => handleCategoryChange(level, e.target.value)}
|
title: cat.title,
|
||||||
|
imageUrl: cat.avatarUrl,
|
||||||
|
}))}
|
||||||
|
selectedId={selectedPath[level]}
|
||||||
|
onSelect={(id) => handleCategorySelect(level, id)}
|
||||||
|
emptyMessage='دستهبندی یافت نشد'
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<Select
|
|
||||||
items={productItems}
|
{canLoadProducts && (
|
||||||
label='محصول'
|
<AvatarSelectionGrid
|
||||||
placeholder='انتخاب محصول'
|
label='محصول'
|
||||||
value={value}
|
isLoading={isProductsLoading}
|
||||||
onChange={handleProductChange}
|
items={products.map((product) => ({
|
||||||
error_text={error_text}
|
id: product.id,
|
||||||
disabled={!activeCategoryId}
|
title: product.title,
|
||||||
{...rest}
|
imageUrl: product.images?.[0],
|
||||||
/>
|
}))}
|
||||||
|
selectedId={typeof value === 'string' ? value : undefined}
|
||||||
|
onSelect={handleProductSelect}
|
||||||
|
error_text={error_text}
|
||||||
|
emptyMessage='محصولی برای این دستهبندی یافت نشد'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { COLORS } from '@/constants/colors'
|
import { COLORS } from '@/constants/colors'
|
||||||
import { AddSquare, CloseCircle, Edit } from 'iconsax-react'
|
import { AddSquare, CloseCircle, Edit2 } 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'
|
||||||
@@ -25,7 +25,6 @@ type Props = {
|
|||||||
|
|
||||||
const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) => {
|
const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) => {
|
||||||
const isEditing = editIndex !== null
|
const isEditing = editIndex !== null
|
||||||
|
|
||||||
const setItems = useRequestStore((state) => state.setItems)
|
const setItems = useRequestStore((state) => state.setItems)
|
||||||
const [productSelected, setProductSelected] = useState<ProductType>()
|
const [productSelected, setProductSelected] = useState<ProductType>()
|
||||||
|
|
||||||
@@ -45,14 +44,11 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
const items = useRequestStore.getState().items
|
const items = useRequestStore.getState().items
|
||||||
setItems([...items, values])
|
setItems([...items, values])
|
||||||
}
|
}
|
||||||
|
|
||||||
onSaved()
|
onSaved()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolvedProduct = productSelected
|
const { data: attributes } = useGetAttributes(productSelected?.id)
|
||||||
|
|
||||||
const { data: attributes } = useGetAttributes(resolvedProduct?.id)
|
|
||||||
|
|
||||||
const handleProductChange = useCallback(
|
const handleProductChange = useCallback(
|
||||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
@@ -68,7 +64,7 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
formik.setFieldValue('productId', productId)
|
formik.setFieldValue('productId', productId)
|
||||||
formik.setFieldValue('attributes', [])
|
formik.setFieldValue('attributes', [])
|
||||||
},
|
},
|
||||||
[formik]
|
[formik],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleProductSelect = useCallback((product: ProductType | undefined) => {
|
const handleProductSelect = useCallback((product: ProductType | undefined) => {
|
||||||
@@ -77,23 +73,36 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white rounded-3xl p-6'>
|
<div className='bg-white rounded-3xl p-6'>
|
||||||
<div className='flex items-center justify-between'>
|
{/* Section header */}
|
||||||
<div className='font-light'>
|
<div className='flex items-center justify-between mb-6'>
|
||||||
{isEditing ? 'ویرایش قلم' : 'افزودن قلم جدید'}
|
<div className='flex items-center gap-2'>
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
'size-8 rounded-full flex items-center justify-center text-white text-xs font-bold',
|
||||||
|
isEditing ? 'bg-blue-500' : 'bg-primary',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isEditing ? <Edit2 size={15} color='white' /> : <AddSquare size={15} color='white' />}
|
||||||
|
</div>
|
||||||
|
<span className='text-sm font-medium'>
|
||||||
|
{isEditing ? 'ویرایش قلم' : 'افزودن قلم جدید'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditing && onCancelEdit && (
|
{isEditing && onCancelEdit && (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={onCancelEdit}
|
onClick={onCancelEdit}
|
||||||
className='flex items-center gap-1 text-xs text-description hover:text-black'
|
className='flex items-center gap-1 text-xs text-description hover:text-black transition-colors'
|
||||||
>
|
>
|
||||||
<CloseCircle size={18} color='currentColor' />
|
<CloseCircle size={16} color='currentColor' />
|
||||||
انصراف
|
انصراف
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 rowTwoInput'>
|
{/* Product selector */}
|
||||||
|
<div className='rowTwoInput'>
|
||||||
<ProductsSelect
|
<ProductsSelect
|
||||||
value={formik.values.productId ?? ''}
|
value={formik.values.productId ?? ''}
|
||||||
onChange={handleProductChange}
|
onChange={handleProductChange}
|
||||||
@@ -106,32 +115,30 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Attributes */}
|
||||||
<ManageAttribute
|
<ManageAttribute
|
||||||
key={`${resolvedProduct?.id ?? 'none'}-${editIndex ?? 'new'}`}
|
key={`${productSelected?.id ?? 'none'}-${editIndex ?? 'new'}`}
|
||||||
attributes={attributes?.data}
|
attributes={attributes?.data}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Action button */}
|
||||||
<div className='mt-6 flex justify-end'>
|
<div className='mt-6 flex justify-end'>
|
||||||
<Button
|
<Button
|
||||||
className={clx(
|
className={clx(
|
||||||
'w-fit px-6',
|
'w-fit px-8 gap-2',
|
||||||
isEditing
|
isEditing
|
||||||
? 'bg-transparent border border-[#3B82F6] text-[#3B82F6]'
|
? 'bg-transparent border border-blue-500 text-blue-500 hover:bg-blue-50'
|
||||||
: 'bg-transparent border border-primary text-primary'
|
: 'bg-transparent border border-primary text-primary hover:bg-primary/5',
|
||||||
)}
|
)}
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
>
|
>
|
||||||
<div className='flex gap-1 items-center'>
|
{isEditing ? (
|
||||||
{isEditing ? (
|
<Edit2 color='currentColor' size={18} />
|
||||||
<Edit color='#3B82F6' size={20} />
|
) : (
|
||||||
) : (
|
<AddSquare color={COLORS.primary} size={18} />
|
||||||
<AddSquare color={COLORS.primary} size={20} />
|
)}
|
||||||
)}
|
<span>{isEditing ? 'ذخیره تغییرات' : 'افزودن به لیست'}</span>
|
||||||
<span>{isEditing ? 'ذخیره تغییرات' : 'افزودن به لیست'}</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import Button from '@/components/Button'
|
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 { useGetProducts } from '../hooks/useRequestData'
|
||||||
import type { RequestItemType } from '../type/Types'
|
import type { RequestItemType } from '../type/Types'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
@@ -28,60 +28,75 @@ const RequestItemsList: FC<Props> = ({
|
|||||||
productsData?.data?.find((p) => p.id === productId)?.title ?? 'محصول'
|
productsData?.data?.find((p) => p.id === productId)?.title ?? 'محصول'
|
||||||
|
|
||||||
return (
|
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='bg-white w-full xl:w-[340px] shrink-0 rounded-3xl xl:sticky xl:top-6 h-fit'>
|
||||||
<div className='flex items-center gap-2'>
|
{/* Header */}
|
||||||
<ShoppingCart size={20} color='black' />
|
<div className='flex items-center gap-2 px-5 pt-5 pb-4 border-b border-[#f0f2f8]'>
|
||||||
<span className='text-sm font-medium'>اقلام درخواست</span>
|
<ShoppingCart size={20} color='black' variant='Bold' />
|
||||||
|
<span className='text-sm font-semibold'>اقلام درخواست</span>
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
<span className='text-xs bg-primary/20 text-black rounded-full px-2 py-0.5 mr-auto'>
|
<span className='mr-auto text-xs bg-primary/15 text-primary font-medium rounded-full px-2.5 py-0.5 tabular-nums'>
|
||||||
{items.length}
|
{items.length} قلم
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{items.length === 0 ? (
|
{/* Items */}
|
||||||
<p className='text-description text-xs mt-6 leading-6'>
|
<div className='px-5 py-4'>
|
||||||
هنوز قلمی اضافه نشده. فرم را پر کنید و روی «افزودن به لیست» بزنید.
|
{items.length === 0 ? (
|
||||||
</p>
|
<div className='flex flex-col items-center justify-center gap-3 py-8 text-center'>
|
||||||
) : (
|
<div className='size-12 rounded-full bg-gray-50 flex items-center justify-center'>
|
||||||
<ul className='mt-4 space-y-3 max-h-[360px] overflow-y-auto'>
|
<ShoppingCart size={22} color='#c0c4d0' />
|
||||||
{items.map((item, index) => (
|
</div>
|
||||||
<li
|
<p className='text-description text-xs leading-6 max-w-[200px]'>
|
||||||
key={index}
|
هنوز قلمی اضافه نشده. فرم را پر کنید و روی «افزودن به لیست» بزنید.
|
||||||
className={clx(
|
</p>
|
||||||
'border rounded-2xl p-3 transition-colors',
|
</div>
|
||||||
editingIndex === index
|
) : (
|
||||||
? 'border-primary bg-primary/5'
|
<ul className='space-y-2 max-h-[380px] overflow-y-auto -mx-1 px-1'>
|
||||||
: 'border-[#f0f2f8]'
|
{items.map((item, index) => (
|
||||||
)}
|
<li
|
||||||
>
|
key={index}
|
||||||
<div className='text-sm font-medium truncate'>
|
className={clx(
|
||||||
{getProductTitle(item.productId)}
|
'border rounded-2xl px-4 py-3 transition-all',
|
||||||
</div>
|
editingIndex === index
|
||||||
<div className='flex gap-2 mt-3'>
|
? 'border-primary bg-primary/5 shadow-sm'
|
||||||
<button
|
: 'border-[#eef0f6] hover:border-gray-300',
|
||||||
type='button'
|
)}
|
||||||
onClick={() => onEdit(index)}
|
>
|
||||||
className='flex items-center gap-1 text-xs text-[#3B82F6] hover:opacity-80'
|
<div className='flex items-start justify-between gap-2'>
|
||||||
>
|
<span className='text-sm font-medium truncate leading-6'>
|
||||||
<Edit size={16} color='#3B82F6' />
|
{getProductTitle(item.productId)}
|
||||||
ویرایش
|
</span>
|
||||||
</button>
|
<span className='text-[10px] text-description shrink-0 bg-gray-100 rounded-full px-2 py-0.5'>
|
||||||
<button
|
#{index + 1}
|
||||||
type='button'
|
</span>
|
||||||
onClick={() => onRemove(index)}
|
</div>
|
||||||
className='flex items-center gap-1 text-xs text-red-500 hover:opacity-80 mr-auto'
|
<div className='flex gap-3 mt-2'>
|
||||||
>
|
<button
|
||||||
<Trash size={16} color='#ef4444' />
|
type='button'
|
||||||
حذف
|
onClick={() => onEdit(index)}
|
||||||
</button>
|
className='flex items-center gap-1 text-xs text-primary hover:opacity-75 transition-opacity'
|
||||||
</div>
|
>
|
||||||
</li>
|
<Edit2 size={14} color='currentColor' />
|
||||||
))}
|
ویرایش
|
||||||
</ul>
|
</button>
|
||||||
)}
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
className='flex items-center gap-1 text-xs text-red-500 hover:opacity-75 transition-opacity mr-auto'
|
||||||
|
>
|
||||||
|
<Trash size={14} color='currentColor' />
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 pt-4 border-t border-[#f0f2f8]'>
|
{/* Submit footer */}
|
||||||
|
<div className='px-5 pb-5 pt-3 border-t border-[#f0f2f8]'>
|
||||||
<Button
|
<Button
|
||||||
className='w-full'
|
className='w-full'
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
@@ -89,7 +104,7 @@ const RequestItemsList: FC<Props> = ({
|
|||||||
disabled={items.length === 0}
|
disabled={items.length === 0}
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center justify-center'>
|
<div className='flex gap-2 items-center justify-center'>
|
||||||
<TickSquare size={20} color='black' />
|
<TickSquare size={20} color='currentColor' />
|
||||||
<span>ثبت نهایی درخواست</span>
|
<span>ثبت نهایی درخواست</span>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user