Create component select + fix filter
This commit is contained in:
@@ -3,6 +3,7 @@ import { Filter } from 'iconsax-react';
|
|||||||
import DatePicker from './DatePicker';
|
import DatePicker from './DatePicker';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
import Select, { type ItemsSelectType } from './Select';
|
import Select, { type ItemsSelectType } from './Select';
|
||||||
|
import MultiSelect from './MultiSelect';
|
||||||
import DefaulModal from './DefaulModal';
|
import DefaulModal from './DefaulModal';
|
||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
@@ -22,6 +23,14 @@ export type SelectFieldType = {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MultiSelectFieldType = {
|
||||||
|
type: 'multiselect';
|
||||||
|
name: string;
|
||||||
|
placeholder: string;
|
||||||
|
options: ItemsSelectType[];
|
||||||
|
defaultValue?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type InputFieldType = {
|
export type InputFieldType = {
|
||||||
type: 'input';
|
type: 'input';
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,9 +38,9 @@ export type InputFieldType = {
|
|||||||
defaultValue?: string;
|
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 {
|
interface FiltersProps {
|
||||||
fields: FieldType[];
|
fields: FieldType[];
|
||||||
@@ -53,6 +62,7 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
const [filters, setFilters] = useState<FilterValues>({});
|
const [filters, setFilters] = useState<FilterValues>({});
|
||||||
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
|
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
|
||||||
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
||||||
|
const [multiSelectValues, setMultiSelectValues] = useState<Record<string, string[]>>({});
|
||||||
const onChangeRef = useRef(onChange);
|
const onChangeRef = useRef(onChange);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,20 +73,33 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
if (Object.keys(initialValues).length > 0) {
|
if (Object.keys(initialValues).length > 0) {
|
||||||
setFilters(initialValues);
|
setFilters(initialValues);
|
||||||
const inputFields: Record<string, string> = {};
|
const inputFields: Record<string, string> = {};
|
||||||
|
const multiSelectFields: Record<string, string[]> = {};
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
if (field.type === 'input' && initialValues[field.name]) {
|
if (field.type === 'input' && initialValues[field.name]) {
|
||||||
inputFields[field.name] = initialValues[field.name] as string;
|
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);
|
setInputValues(inputFields);
|
||||||
|
setMultiSelectValues(multiSelectFields);
|
||||||
} else {
|
} else {
|
||||||
const defaultValues: FilterValues = {};
|
const defaultValues: FilterValues = {};
|
||||||
const defaultInputs: Record<string, string> = {};
|
const defaultInputs: Record<string, string> = {};
|
||||||
|
const defaultMultiSelects: Record<string, string[]> = {};
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
if ('defaultValue' in field && field.defaultValue !== undefined) {
|
if ('defaultValue' in field && field.defaultValue !== undefined) {
|
||||||
defaultValues[field.name] = field.defaultValue;
|
if (field.type === 'multiselect') {
|
||||||
if (field.type === 'input') {
|
defaultValues[field.name] = field.defaultValue as string[];
|
||||||
defaultInputs[field.name] = field.defaultValue;
|
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) {
|
if (Object.keys(defaultValues).length > 0) {
|
||||||
setFilters(defaultValues);
|
setFilters(defaultValues);
|
||||||
setInputValues(defaultInputs);
|
setInputValues(defaultInputs);
|
||||||
|
setMultiSelectValues(defaultMultiSelects);
|
||||||
onChange(defaultValues);
|
onChange(defaultValues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +143,13 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
onChange(newFilters);
|
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 handleInputChange = (name: string, event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
setInputValues(prev => ({ ...prev, [name]: value }));
|
setInputValues(prev => ({ ...prev, [name]: value }));
|
||||||
@@ -126,6 +157,9 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
|
|
||||||
const renderField = (field: FieldType) => {
|
const renderField = (field: FieldType) => {
|
||||||
const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name];
|
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) {
|
switch (field.type) {
|
||||||
case 'date':
|
case 'date':
|
||||||
@@ -134,7 +168,7 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
key={field.name}
|
key={field.name}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
onChange={(value) => handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))}
|
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}
|
key={field.name}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
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}
|
items={field.options}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -156,7 +202,7 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
variant="search"
|
variant="search"
|
||||||
className="w-full md:min-w-[230px] md:max-w-[230px]"
|
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)}
|
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 { useMemo } from 'react'
|
||||||
import type { FieldType } from '@/components/Filters'
|
import type { FieldType } from '@/components/Filters'
|
||||||
|
import { OrderStatus, PaymentStatusEnum } from '../enum/Enum'
|
||||||
|
|
||||||
export const useOrderFiltersFields = (): FieldType[] => {
|
export const useOrderFiltersFields = (): FieldType[] => {
|
||||||
return useMemo(() => [
|
return useMemo(() => [
|
||||||
@@ -9,30 +10,33 @@ export const useOrderFiltersFields = (): FieldType[] => {
|
|||||||
placeholder: 'جستجو (شماره سفارش یا نام مشتری)',
|
placeholder: 'جستجو (شماره سفارش یا نام مشتری)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'multiselect',
|
||||||
name: 'status',
|
name: 'statuses',
|
||||||
placeholder: 'وضعیت',
|
placeholder: 'وضعیت سفارش',
|
||||||
defaultValue: '',
|
defaultValue: [],
|
||||||
options: [
|
options: [
|
||||||
{ label: 'همه', value: '' },
|
{ label: 'در انتظار پرداخت', value: OrderStatus.PENDING_PAYMENT },
|
||||||
{ label: 'در انتظار تایید', value: 'PENDING' },
|
{ label: 'پرداخت شده', value: OrderStatus.PAID },
|
||||||
{ label: 'در حال آماده سازی', value: 'PREPARING' },
|
{ label: 'در حال آماده سازی', value: OrderStatus.PREPARING },
|
||||||
{ label: 'آماده تحویل', value: 'READY' },
|
{ label: 'آماده', value: OrderStatus.READY },
|
||||||
{ label: 'تحویل داده شده', value: 'DELIVERED' },
|
{ label: 'تحویل به پیشخدمت', value: OrderStatus.DELIVERED_TO_WAITER },
|
||||||
{ label: 'لغو شده', value: 'CANCELLED' },
|
{ label: 'تحویل به پذیرش', value: OrderStatus.DELIVERED_TO_RECEPTIONIST },
|
||||||
{ label: 'تایید نشده', value: 'REJECTED' },
|
{ label: 'ارسال شده', value: OrderStatus.SHIPPED },
|
||||||
|
{ label: 'تکمیل شده', value: OrderStatus.COMPLETED },
|
||||||
|
{ label: 'لغو شده', value: OrderStatus.CANCELED },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'deliveryType',
|
name: 'paymentStatus',
|
||||||
placeholder: 'نوع تحویل',
|
placeholder: 'وضعیت پرداخت',
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'همه', value: '' },
|
{ label: '--', value: '' },
|
||||||
{ label: 'تحویل با پیک', value: 'DELIVERY' },
|
{ label: 'در انتظار', value: PaymentStatusEnum.Pending },
|
||||||
{ label: 'سرو در رستوران', value: 'DINE_IN' },
|
{ label: 'پرداخت شده', value: PaymentStatusEnum.Paid },
|
||||||
{ label: 'تحویل در ماشین', value: 'PICKUP' },
|
{ label: 'ناموفق', value: PaymentStatusEnum.Failed },
|
||||||
|
{ label: 'بازگشت شده', value: PaymentStatusEnum.Refunded },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
], [])
|
], [])
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const useOrderFilters = () => {
|
|||||||
const [limit] = useState(DEFAULT_LIMIT);
|
const [limit] = useState(DEFAULT_LIMIT);
|
||||||
|
|
||||||
const apiParams = useMemo(() => {
|
const apiParams = useMemo(() => {
|
||||||
const params: Record<string, string | number> = {
|
const params: Record<string, string | number | string[]> = {
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
};
|
};
|
||||||
@@ -19,12 +19,12 @@ export const useOrderFilters = () => {
|
|||||||
params.search = filters.search as string;
|
params.search = filters.search as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.status) {
|
if (filters.statuses && Array.isArray(filters.statuses) && filters.statuses.length > 0) {
|
||||||
params.status = filters.status as string;
|
params.statuses = filters.statuses as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.deliveryType) {
|
if (filters.paymentStatus) {
|
||||||
params.deliveryType = filters.deliveryType as string;
|
params.paymentStatus = filters.paymentStatus as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export interface GetOrdersParams {
|
|||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
status?: string;
|
statuses?: string[];
|
||||||
deliveryType?: string;
|
paymentStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeOrderStatusParams {
|
export interface ChangeOrderStatusParams {
|
||||||
@@ -20,9 +20,34 @@ export interface RefundPaymentParams {
|
|||||||
export const getOrders = async (
|
export const getOrders = async (
|
||||||
params?: GetOrdersParams
|
params?: GetOrdersParams
|
||||||
): Promise<GetOrdersResponse> => {
|
): Promise<GetOrdersResponse> => {
|
||||||
const { data } = await axios.get<GetOrdersResponse>("/admin/orders", {
|
const queryParams = new URLSearchParams();
|
||||||
params,
|
|
||||||
});
|
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;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user