Files
dmail-front/src/components/Filters.tsx
T
2025-07-30 09:47:21 +03:30

237 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FC, useEffect, useState, ChangeEvent, useRef } from 'react';
import { Filter } from 'iconsax-react';
import DatePicker from './DatePicker';
import Input from './Input';
import Select, { ItemsSelectType } from './Select';
import DefaulModal from './DefaulModal';
import moment from 'moment-jalaali'
import Button from './Button';
// تعریف نوع داده برای فیلدهای مختلف
export type DateFieldType = {
type: 'date';
name: string;
placeholder: string;
defaultValue?: string;
};
export type SelectFieldType = {
type: 'select';
name: string;
placeholder: string;
options: ItemsSelectType[];
defaultValue?: string;
};
export type InputFieldType = {
type: 'input';
name: string;
placeholder: string;
defaultValue?: string;
};
export type FieldType = DateFieldType | SelectFieldType | InputFieldType;
// تعریف نوع داده برای مقادیر فیلترها
export type FilterValues = Record<string, string | null>;
interface FiltersProps {
fields: FieldType[];
onChange: (filters: FilterValues) => void;
initialValues?: FilterValues;
className?: string;
fieldClassName?: string;
searchField?: string; // نام فیلد جستجو که باید در انتها نمایش داده شود
}
const Filters: FC<FiltersProps> = ({
fields,
onChange,
initialValues = {},
className = "mt-6 md:mt-8 xl:mt-10 flex flex-col md:flex-row justify-between items-start md:items-center gap-4",
fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto",
searchField = "search" // پیش‌فرض فیلد سرچ با نام search
}) => {
const [filters, setFilters] = useState<FilterValues>({});
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
const [inputValues, setInputValues] = useState<Record<string, string>>({});
const onChangeRef = useRef(onChange);
// آپدیت کردن ref وقتی onChange تغییر می‌کند
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
// تنظیم مقادیر اولیه
useEffect(() => {
if (Object.keys(initialValues).length > 0) {
setFilters(initialValues);
// تنظیم مقادیر input برای فیلدهای نوع input
const inputFields: Record<string, string> = {};
fields.forEach(field => {
if (field.type === 'input' && initialValues[field.name]) {
inputFields[field.name] = initialValues[field.name] as string;
}
});
setInputValues(inputFields);
} else {
// تنظیم مقادیر پیش‌فرض از فیلدها
const defaultValues: FilterValues = {};
const defaultInputs: 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 (Object.keys(defaultValues).length > 0) {
setFilters(defaultValues);
setInputValues(defaultInputs);
onChange(defaultValues);
}
}
}, [fields, initialValues, onChange]);
// Debounce برای فیلدهای input
useEffect(() => {
const timeouts: Record<string, number> = {};
Object.keys(inputValues).forEach(fieldName => {
timeouts[fieldName] = setTimeout(() => {
setFilters(currentFilters => {
const currentFilter = currentFilters[fieldName];
const newValue = inputValues[fieldName];
if (currentFilter !== newValue) {
const newFilters = { ...currentFilters, [fieldName]: newValue || null };
onChangeRef.current(newFilters);
return newFilters;
}
return currentFilters;
});
}, 1000);
});
return () => {
Object.values(timeouts).forEach(timeout => clearTimeout(timeout));
};
}, [inputValues]);
const handleChange = (name: string, value: string | null) => {
const newFilters = { ...filters, [name]: value };
setFilters(newFilters);
onChange(newFilters);
};
const handleInputChange = (name: string, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setInputValues(prev => ({ ...prev, [name]: value }));
};
const renderField = (field: FieldType) => {
const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name];
switch (field.type) {
case 'date':
return (
<DatePicker
key={field.name}
placeholder={field.placeholder}
onChange={(value) => handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))}
defaultValue={currentValue || field.defaultValue || ''}
/>
);
case 'select':
return (
<Select
key={field.name}
placeholder={field.placeholder}
onChange={(e) => handleChange(field.name, e.target.value)}
defaultValue={currentValue || field.defaultValue || ''}
items={field.options}
/>
);
case 'input':
return (
<Input
key={field.name}
placeholder={field.placeholder}
variant="search"
className="w-full md:max-w-[200px]"
value={currentValue || field.defaultValue || ''}
onChange={(e) => handleInputChange(field.name, e)}
/>
);
}
};
// جداسازی فیلد جستجو از سایر فیلدها
const searchFieldObj = fields.find(field => field.name === searchField);
const otherFields = fields.filter(field => field.name !== searchField);
return (
<div className={className}>
{/* آیکون فیلتر برای موبایل */}
<div className="flex md:hidden items-center justify-between w-full mb-2">
<button
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
className="flex items-center gap-2 px-3 py-2 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<Filter size={18} color="#000" />
<span className="text-sm">فیلترها</span>
</button>
{searchFieldObj && (
<div className="flex-1 mr-4">
{renderField(searchFieldObj)}
</div>
)}
</div>
{/* فیلترها برای دسکتاپ (همیشه نمایش داده می‌شوند) */}
<div className="hidden md:flex md:flex-row justify-between items-center gap-4 w-full">
<div className={fieldClassName}>
{otherFields.map(renderField)}
</div>
{searchFieldObj && (
<div className="w-full md:w-auto">
{renderField(searchFieldObj)}
</div>
)}
</div>
{/* فیلترها برای موبایل (در قالب مودال) */}
<DefaulModal
open={isFiltersOpen}
close={() => setIsFiltersOpen(false)}
isHeader={true}
title_header="فیلتر کردن نتایج"
>
<div className="pb-5 gap-4 flex flex-col-reverse">
{otherFields.map(field => (
<div key={field.name} className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-2">
{field.placeholder}
</label>
<div className="w-full">
{renderField(field)}
</div>
</div>
))}
</div>
<div className='mt-6'>
<Button
label='بستن'
onClick={() => setIsFiltersOpen(false)}
/>
</div>
</DefaulModal>
</div>
);
};
export default Filters;