diff --git a/src/components/Filters.tsx b/src/components/Filters.tsx index 3da47d4..076dc61 100644 --- a/src/components/Filters.tsx +++ b/src/components/Filters.tsx @@ -3,6 +3,7 @@ import { Filter } from 'iconsax-react'; import DatePicker from './DatePicker'; import Input from './Input'; import Select, { type ItemsSelectType } from './Select'; +import MultiSelect from './MultiSelect'; import DefaulModal from './DefaulModal'; import moment from 'moment-jalaali' import Button from './Button'; @@ -22,6 +23,14 @@ export type SelectFieldType = { defaultValue?: string; }; +export type MultiSelectFieldType = { + type: 'multiselect'; + name: string; + placeholder: string; + options: ItemsSelectType[]; + defaultValue?: string[]; +}; + export type InputFieldType = { type: 'input'; name: string; @@ -29,9 +38,9 @@ export type InputFieldType = { defaultValue?: string; }; -export type FieldType = DateFieldType | SelectFieldType | InputFieldType; +export type FieldType = DateFieldType | SelectFieldType | MultiSelectFieldType | InputFieldType; -export type FilterValues = Record; +export type FilterValues = Record; interface FiltersProps { fields: FieldType[]; @@ -53,6 +62,7 @@ const Filters: FC = ({ const [filters, setFilters] = useState({}); const [isFiltersOpen, setIsFiltersOpen] = useState(false); const [inputValues, setInputValues] = useState>({}); + const [multiSelectValues, setMultiSelectValues] = useState>({}); const onChangeRef = useRef(onChange); useEffect(() => { @@ -63,20 +73,33 @@ const Filters: FC = ({ if (Object.keys(initialValues).length > 0) { setFilters(initialValues); const inputFields: Record = {}; + const multiSelectFields: Record = {}; fields.forEach(field => { if (field.type === 'input' && initialValues[field.name]) { inputFields[field.name] = initialValues[field.name] as string; } + if (field.type === 'multiselect' && initialValues[field.name]) { + multiSelectFields[field.name] = Array.isArray(initialValues[field.name]) + ? (initialValues[field.name] as string[]) + : []; + } }); setInputValues(inputFields); + setMultiSelectValues(multiSelectFields); } else { const defaultValues: FilterValues = {}; const defaultInputs: Record = {}; + const defaultMultiSelects: Record = {}; fields.forEach(field => { if ('defaultValue' in field && field.defaultValue !== undefined) { - defaultValues[field.name] = field.defaultValue; - if (field.type === 'input') { - defaultInputs[field.name] = field.defaultValue; + if (field.type === 'multiselect') { + defaultValues[field.name] = field.defaultValue as string[]; + defaultMultiSelects[field.name] = field.defaultValue as string[]; + } else { + defaultValues[field.name] = field.defaultValue; + if (field.type === 'input') { + defaultInputs[field.name] = field.defaultValue; + } } } }); @@ -84,6 +107,7 @@ const Filters: FC = ({ if (Object.keys(defaultValues).length > 0) { setFilters(defaultValues); setInputValues(defaultInputs); + setMultiSelectValues(defaultMultiSelects); onChange(defaultValues); } } @@ -119,6 +143,13 @@ const Filters: FC = ({ onChange(newFilters); }; + const handleMultiSelectChange = (name: string, values: string[]) => { + const newFilters = { ...filters, [name]: values.length > 0 ? values : null }; + setFilters(newFilters); + setMultiSelectValues(prev => ({ ...prev, [name]: values })); + onChange(newFilters); + }; + const handleInputChange = (name: string, event: ChangeEvent) => { const value = event.target.value; setInputValues(prev => ({ ...prev, [name]: value })); @@ -126,6 +157,9 @@ const Filters: FC = ({ const renderField = (field: FieldType) => { const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name]; + const currentMultiSelectValue = field.type === 'multiselect' + ? (multiSelectValues[field.name] || (field.defaultValue as string[]) || []) + : undefined; switch (field.type) { case 'date': @@ -134,7 +168,7 @@ const Filters: FC = ({ key={field.name} placeholder={field.placeholder} onChange={(value) => handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))} - defaulValue={currentValue || field.defaultValue || ''} + defaulValue={currentValue as string || field.defaultValue || ''} /> ); @@ -144,7 +178,19 @@ const Filters: FC = ({ key={field.name} placeholder={field.placeholder} onChange={(e) => handleChange(field.name, e.target.value)} - defaultValue={currentValue || field.defaultValue || ''} + defaultValue={currentValue as string || field.defaultValue || ''} + items={field.options} + className='min-w-[170px]' + /> + ); + + case 'multiselect': + return ( + handleMultiSelectChange(field.name, values)} + value={currentMultiSelectValue} items={field.options} /> ); @@ -156,7 +202,7 @@ const Filters: FC = ({ placeholder={field.placeholder} variant="search" className="w-full md:min-w-[230px] md:max-w-[230px]" - value={currentValue || field.defaultValue || ''} + value={currentValue as string || field.defaultValue || ''} onChange={(e) => handleInputChange(field.name, e)} /> ); diff --git a/src/components/MultiSelect.tsx b/src/components/MultiSelect.tsx new file mode 100644 index 0000000..7c0f00d --- /dev/null +++ b/src/components/MultiSelect.tsx @@ -0,0 +1,108 @@ +import { type FC, useState, useRef, useEffect } from 'react' +import { ArrowDown2 } from 'iconsax-react' +import { clx } from '../helpers/utils' +import { Checkbox } from './ui/checkbox' +import type { ItemsSelectType } from './Select' + +type MultiSelectProps = { + className?: string + items: ItemsSelectType[] + placeholder?: string + label?: string + value?: string[] + onChange?: (values: string[]) => void + error_text?: string +} + +const MultiSelect: FC = ({ + className, + items, + placeholder, + label, + value = [], + onChange, + error_text, +}) => { + const [isOpen, setIsOpen] = useState(false) + const containerRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setIsOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + const handleToggle = (itemValue: string) => { + const newValues = value.includes(itemValue) + ? value.filter((v) => v !== itemValue) + : [...value, itemValue] + onChange?.(newValues) + } + + const displayText = + value.length === 0 + ? placeholder || 'انتخاب کنید' + : value.length === 1 + ? items.find((item) => item.value === value[0])?.label || placeholder + : `${value.length} مورد انتخاب شده` + + return ( +
+ {label && } + + + {isOpen && ( +
+ {items.map((item) => { + const isChecked = value.includes(item.value) + return ( +
handleToggle(item.value)} + > + + + {item.label} + +
+ ) + })} +
+ )} + + {error_text && error_text !== '' && ( +
{error_text}
+ )} +
+ ) +} + +export default MultiSelect + diff --git a/src/pages/orders/components/OrderFiltersFields.tsx b/src/pages/orders/components/OrderFiltersFields.tsx index cb4494c..8d79d0a 100644 --- a/src/pages/orders/components/OrderFiltersFields.tsx +++ b/src/pages/orders/components/OrderFiltersFields.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react' import type { FieldType } from '@/components/Filters' +import { OrderStatus, PaymentStatusEnum } from '../enum/Enum' export const useOrderFiltersFields = (): FieldType[] => { return useMemo(() => [ @@ -9,30 +10,33 @@ export const useOrderFiltersFields = (): FieldType[] => { placeholder: 'جستجو (شماره سفارش یا نام مشتری)', }, { - type: 'select', - name: 'status', - placeholder: 'وضعیت', - defaultValue: '', + type: 'multiselect', + name: 'statuses', + placeholder: 'وضعیت سفارش', + defaultValue: [], options: [ - { label: 'همه', value: '' }, - { label: 'در انتظار تایید', value: 'PENDING' }, - { label: 'در حال آماده سازی', value: 'PREPARING' }, - { label: 'آماده تحویل', value: 'READY' }, - { label: 'تحویل داده شده', value: 'DELIVERED' }, - { label: 'لغو شده', value: 'CANCELLED' }, - { label: 'تایید نشده', value: 'REJECTED' }, + { label: 'در انتظار پرداخت', value: OrderStatus.PENDING_PAYMENT }, + { label: 'پرداخت شده', value: OrderStatus.PAID }, + { label: 'در حال آماده سازی', value: OrderStatus.PREPARING }, + { label: 'آماده', value: OrderStatus.READY }, + { label: 'تحویل به پیشخدمت', value: OrderStatus.DELIVERED_TO_WAITER }, + { label: 'تحویل به پذیرش', value: OrderStatus.DELIVERED_TO_RECEPTIONIST }, + { label: 'ارسال شده', value: OrderStatus.SHIPPED }, + { label: 'تکمیل شده', value: OrderStatus.COMPLETED }, + { label: 'لغو شده', value: OrderStatus.CANCELED }, ], }, { type: 'select', - name: 'deliveryType', - placeholder: 'نوع تحویل', + name: 'paymentStatus', + placeholder: 'وضعیت پرداخت', defaultValue: '', options: [ - { label: 'همه', value: '' }, - { label: 'تحویل با پیک', value: 'DELIVERY' }, - { label: 'سرو در رستوران', value: 'DINE_IN' }, - { label: 'تحویل در ماشین', value: 'PICKUP' }, + { label: '--', value: '' }, + { label: 'در انتظار', value: PaymentStatusEnum.Pending }, + { label: 'پرداخت شده', value: PaymentStatusEnum.Paid }, + { label: 'ناموفق', value: PaymentStatusEnum.Failed }, + { label: 'بازگشت شده', value: PaymentStatusEnum.Refunded }, ], }, ], []) diff --git a/src/pages/orders/hooks/useOrderFilters.ts b/src/pages/orders/hooks/useOrderFilters.ts index 3d99ed0..c8d5f19 100644 --- a/src/pages/orders/hooks/useOrderFilters.ts +++ b/src/pages/orders/hooks/useOrderFilters.ts @@ -10,7 +10,7 @@ export const useOrderFilters = () => { const [limit] = useState(DEFAULT_LIMIT); const apiParams = useMemo(() => { - const params: Record = { + const params: Record = { page: currentPage, limit: limit, }; @@ -19,12 +19,12 @@ export const useOrderFilters = () => { params.search = filters.search as string; } - if (filters.status) { - params.status = filters.status as string; + if (filters.statuses && Array.isArray(filters.statuses) && filters.statuses.length > 0) { + params.statuses = filters.statuses as string[]; } - if (filters.deliveryType) { - params.deliveryType = filters.deliveryType as string; + if (filters.paymentStatus) { + params.paymentStatus = filters.paymentStatus as string; } return params; diff --git a/src/pages/orders/service/OrderService.ts b/src/pages/orders/service/OrderService.ts index d838b10..b79241a 100644 --- a/src/pages/orders/service/OrderService.ts +++ b/src/pages/orders/service/OrderService.ts @@ -5,8 +5,8 @@ export interface GetOrdersParams { page?: number; limit?: number; search?: string; - status?: string; - deliveryType?: string; + statuses?: string[]; + paymentStatus?: string; } export interface ChangeOrderStatusParams { @@ -20,9 +20,34 @@ export interface RefundPaymentParams { export const getOrders = async ( params?: GetOrdersParams ): Promise => { - const { data } = await axios.get("/admin/orders", { - params, - }); + const queryParams = new URLSearchParams(); + + if (params?.page) { + queryParams.append('page', params.page.toString()); + } + + if (params?.limit) { + queryParams.append('limit', params.limit.toString()); + } + + if (params?.search) { + queryParams.append('search', params.search); + } + + if (params?.statuses && Array.isArray(params.statuses) && params.statuses.length > 0) { + params.statuses.forEach((status) => { + queryParams.append('statuses', status); + }); + } + + if (params?.paymentStatus) { + queryParams.append('paymentStatus', params.paymentStatus); + } + + const queryString = queryParams.toString(); + const url = queryString ? `/admin/orders?${queryString}` : '/admin/orders'; + + const { data } = await axios.get(url); return data; };