Create component select + fix filter

This commit is contained in:
hamid zarghami
2025-12-24 10:51:39 +03:30
parent f0d5082723
commit 85a17e42d1
5 changed files with 218 additions and 35 deletions
+54 -8
View File
@@ -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)}
/>
);
+108
View File
@@ -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 },
],
},
], [])
+5 -5
View File
@@ -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;
+30 -5
View File
@@ -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;
};