Create component select + fix filter
This commit is contained in:
@@ -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<string, string | null>;
|
||||
export type FilterValues = Record<string, string | string[] | null>;
|
||||
|
||||
interface FiltersProps {
|
||||
fields: FieldType[];
|
||||
@@ -53,6 +62,7 @@ const Filters: FC<FiltersProps> = ({
|
||||
const [filters, setFilters] = useState<FilterValues>({});
|
||||
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
|
||||
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
||||
const [multiSelectValues, setMultiSelectValues] = useState<Record<string, string[]>>({});
|
||||
const onChangeRef = useRef(onChange);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,20 +73,33 @@ const Filters: FC<FiltersProps> = ({
|
||||
if (Object.keys(initialValues).length > 0) {
|
||||
setFilters(initialValues);
|
||||
const inputFields: Record<string, string> = {};
|
||||
const multiSelectFields: Record<string, string[]> = {};
|
||||
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<string, string> = {};
|
||||
const defaultMultiSelects: Record<string, string[]> = {};
|
||||
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<FiltersProps> = ({
|
||||
if (Object.keys(defaultValues).length > 0) {
|
||||
setFilters(defaultValues);
|
||||
setInputValues(defaultInputs);
|
||||
setMultiSelectValues(defaultMultiSelects);
|
||||
onChange(defaultValues);
|
||||
}
|
||||
}
|
||||
@@ -119,6 +143,13 @@ const Filters: FC<FiltersProps> = ({
|
||||
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<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
setInputValues(prev => ({ ...prev, [name]: value }));
|
||||
@@ -126,6 +157,9 @@ const Filters: FC<FiltersProps> = ({
|
||||
|
||||
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<FiltersProps> = ({
|
||||
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<FiltersProps> = ({
|
||||
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 (
|
||||
<MultiSelect
|
||||
key={field.name}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(values) => handleMultiSelectChange(field.name, values)}
|
||||
value={currentMultiSelectValue}
|
||||
items={field.options}
|
||||
/>
|
||||
);
|
||||
@@ -156,7 +202,7 @@ const Filters: FC<FiltersProps> = ({
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<MultiSelectProps> = ({
|
||||
className,
|
||||
items,
|
||||
placeholder,
|
||||
label,
|
||||
value = [],
|
||||
onChange,
|
||||
error_text,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="w-full min-w-[180px] relative" ref={containerRef}>
|
||||
{label && <label className="text-sm">{label}</label>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={clx(
|
||||
'w-full text-black whitespace-nowrap border appearance-none border-border !bg-white px-2.5 h-10 text-sm rounded-[10px] bg-gray text-right flex items-center justify-between',
|
||||
className,
|
||||
label && 'mt-1',
|
||||
isOpen && 'border-primary'
|
||||
)}
|
||||
>
|
||||
<span className="whitespace-nowrap">{displayText}</span>
|
||||
<ArrowDown2
|
||||
size={16}
|
||||
color="black"
|
||||
className={clx('transition-transform', isOpen && 'rotate-180')}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 w-full mt-1 bg-white border border-border rounded-[10px] shadow-lg max-h-60 overflow-y-auto">
|
||||
{items.map((item) => {
|
||||
const isChecked = value.includes(item.value)
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className="flex items-center gap-2 px-3 py-2 hover:bg-secondary cursor-pointer"
|
||||
onClick={() => handleToggle(item.value)}
|
||||
>
|
||||
<Checkbox checked={isChecked} />
|
||||
<span className="text-sm text-foreground whitespace-nowrap">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error_text && error_text !== '' && (
|
||||
<div className="text-sm text-destructive mt-1">{error_text}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MultiSelect
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
], [])
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useOrderFilters = () => {
|
||||
const [limit] = useState(DEFAULT_LIMIT);
|
||||
|
||||
const apiParams = useMemo(() => {
|
||||
const params: Record<string, string | number> = {
|
||||
const params: Record<string, string | number | string[]> = {
|
||||
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;
|
||||
|
||||
@@ -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<GetOrdersResponse> => {
|
||||
const { data } = await axios.get<GetOrdersResponse>("/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<GetOrdersResponse>(url);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user