Feature/change filter report map

This commit is contained in:
AmirHossein Mahmoodi
2025-04-14 11:11:53 +00:00
parent 0b719c9043
commit a56e123d47
27 changed files with 918 additions and 596 deletions

View File

@@ -0,0 +1,25 @@
import { Controller } from "react-hook-form";
import FilterField from "./FilterField";
const FilterController = ({ item, control, reset, errors }) => {
return (
<Controller
name={`${item.id}`}
control={control}
render={({ field: { value, onBlur, onChange } }) => {
return (
<FilterField
item={item}
filterParameters={value}
handleChange={onChange}
handleBlur={onBlur}
dependencyFieldValue={null}
resetForm={() => reset()}
errors={errors}
/>
);
}}
/>
);
};
export default FilterController;

View File

@@ -0,0 +1,27 @@
import { Controller, useWatch } from "react-hook-form";
import FilterField from "./FilterField";
const FilterControllerWithDependency = ({ item, control, reset, errors }) => {
const dependencyField = useWatch({ control, name: item.dependencyId });
return (
<Controller
name={`${item.id}`}
control={control}
render={({ field: { value, name, onBlur, onChange } }) => {
return (
<FilterField
item={item}
filterParameters={value}
handleChange={onChange}
handleBlur={onBlur}
dependencyFieldValue={dependencyField}
resetForm={() => reset()}
errors={errors}
/>
);
}}
/>
);
};
export default FilterControllerWithDependency;

View File

@@ -0,0 +1,103 @@
import { useState } from "react";
import CustomTextFieldRange from "./fieldsType/CustomTextFieldRange";
import CustomSelect from "./fieldsType/CustomSelect";
import CustomDatePicker from "../CustomDatePicker";
import CustomDatePickerRange from "./fieldsType/CustomDate/CustomDatePickerRange";
import CustomSelectMultiple from "./fieldsType/CustomSelectMultiple";
import CustomTextField from "./fieldsType/CustomTextField";
import FilterOptionList from "./FilterOptionList";
const filterModeOptionFa = {
equals: "برابر",
notEquals: "نابرابر",
contains: "شامل",
lessThan: "کوچکتر",
greaterThan: "بزرگتر",
fuzzy: "فازی",
between: "مابین",
};
function FilterField({
item,
filterParameters,
handleChange,
handleBlur,
dependencyFieldValue,
setFieldValue,
resetForm,
errors,
}) {
const [anchorEl, setAnchorEl] = useState(null);
const defaultFilterTranslation = filterModeOptionFa[filterParameters.filterMode] || filterParameters.filterMode;
const handleOpenFilterBox = (event) => {
setAnchorEl(event.currentTarget);
};
const renderField = () => {
const commonProps = {
item,
filterParameters,
defaultFilterTranslation,
handleOpenFilterBox,
dependencyFieldValue,
setFieldValue,
handleChange,
handleBlur,
errors,
};
switch (filterParameters.datatype) {
case "numeric":
if (filterParameters.filterMode === "between") {
return <CustomTextFieldRange {...commonProps} />;
}
if (filterParameters.filterMode === "equals") {
return item.SelectComponent ? (
<item.SelectComponent {...commonProps} />
) : (
<CustomSelect {...commonProps} />
);
}
break;
case "date":
if (filterParameters.filterMode === "equals") {
return <CustomDatePicker {...commonProps} />;
}
if (filterParameters.filterMode === "between") {
return <CustomDatePickerRange {...commonProps} />;
}
break;
case "array":
if (filterParameters.filterMode === "equals") {
return <CustomSelectMultiple {...commonProps} />;
}
break;
default:
return <CustomTextField {...commonProps} />;
}
};
return (
<>
{renderField()}
{Array.isArray(item.filterModeOptions) && (
<FilterOptionList
item={item}
anchorEl={anchorEl}
setAnchorEl={setAnchorEl}
filterParameters={filterParameters}
filterType={filterParameters.filterMode}
handleChange={handleChange}
resetForm={resetForm}
filterOption={item.filterModeOptions}
filterModeOptionFa={filterModeOptionFa}
/>
)}
</>
);
}
export default FilterField;

View File

@@ -0,0 +1,47 @@
"use client";
import { ListItem, Menu } from "@mui/material";
import { useState } from "react";
function FilterOptionList({
filterType,
filterOption,
filterParameters,
anchorEl,
filterModeOptionFa,
setAnchorEl,
handleChange,
}) {
const [selectedFilter, setSelectedFilter] = useState(filterType);
const handleChangeItem = (event, index) => {
handleChange({
...filterParameters,
value: filterOption[index] === "between" ? ["", ""] : "",
filterMode: filterOption[index],
});
setSelectedFilter(filterOption[index]);
setAnchorEl(null);
};
const handleCloseFilterBox = () => {
setAnchorEl(null);
};
return (
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleCloseFilterBox}>
{filterOption.map((option, index) => (
<ListItem
key={index}
onClick={(event) => handleChangeItem(event, index)}
selected={option === selectedFilter}
sx={{ cursor: "pointer", width: "100px", display: "flex", justifyContent: "center" }}
>
{filterModeOptionFa[option]}
</ListItem>
))}
</Menu>
);
}
export default FilterOptionList;

View File

@@ -0,0 +1,23 @@
import React from "react";
import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker";
import { Typography } from "@mui/material";
function CustomDatePicker({ column, filterParameters, defaultFilterTranslation, handleChange }) {
return (
<MuiDatePicker
name={`${column.id}.value`}
value={filterParameters.value}
setFieldValue={(name, formattedDate) => {
handleChange({ ...filterParameters, value: formattedDate });
}}
placeholder={column.header}
helperText={
<Typography variant="button" sx={{ color: "#155175" }}>
نوع فیلتر: {defaultFilterTranslation} (تاریخ)
</Typography>
}
/>
);
}
export default CustomDatePicker;

View File

@@ -0,0 +1,41 @@
"use client";
import React from "react";
import { Box, Typography } from "@mui/material";
import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker";
function CustomDatePickerRange({ item, filterParameters, defaultFilterTranslation, handleChange, errors }) {
return (
<Box sx={{ display: "flex", gap: 1 }}>
<MuiDatePicker
label={item.header}
name={`${item.id}.value[0]`}
value={filterParameters.value[0]}
setFieldValue={(name, formattedDate) => {
handleChange({ ...filterParameters, value: [formattedDate, filterParameters.value[1]] });
}}
maxDate={filterParameters.value[1]}
placeholder={`از تاریخ`}
helperText={
<Typography variant="button" sx={{ color: "#155175" }}>
نوع فیلتر: {defaultFilterTranslation} (تاریخ)
</Typography>
}
/>
<MuiDatePicker
label={item.header}
name={`${item.id}.value[1]`}
value={filterParameters.value[1]}
setFieldValue={(name, formattedDate) => {
handleChange({ ...filterParameters, value: [filterParameters.value[0], formattedDate] });
}}
minDate={filterParameters.value[0]}
placeholder={`تا تاریخ`}
helperText={errors?.[`${item.id}`]?.value ? errors?.[`${item.id}`]?.value.message : null}
error={Boolean(errors?.[`${item.id}`]?.value)}
/>
</Box>
);
}
export default CustomDatePickerRange;

View File

@@ -0,0 +1,72 @@
import React from "react";
import { LocalizationProvider, MobileDatePicker } from "@mui/x-date-pickers";
import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali";
import { faIR } from "@mui/x-date-pickers/locales";
import { Box, FormHelperText, IconButton, InputAdornment } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear";
import moment from "jalali-moment";
function MuiDatePicker({ label, value, setFieldValue, name, minDate, maxDate, helperText, placeholder, error }) {
return (
<Box sx={{ display: "flex", flexDirection: "column", my: 1 }}>
<LocalizationProvider
dateAdapter={AdapterDateFnsJalali}
localeText={faIR.components.MuiLocalizationProvider.defaultProps.localeText}
>
<MobileDatePicker
value={value ? new Date(value) : null}
sx={{ width: "100%" }}
id={name}
label={label}
name={name}
closeOnSelect={true}
aria-describedby="component-helper-text"
onChange={(value) => {
const date = new Date(value);
const formattedDate = moment(date).locale("en").format("YYYY-MM-DD");
setFieldValue(name, formattedDate);
}}
minDate={minDate ? new Date(minDate) : null}
maxDate={maxDate ? new Date(maxDate) : null}
slotProps={{
textField: {
size: "small",
error: error,
placeholder: placeholder,
InputProps: {
endAdornment: (
<InputAdornment position="end">
<IconButton
size="small"
onClick={(event) => {
event.stopPropagation();
setFieldValue(name, "");
}}
sx={{
color: "#bfbfbf",
"&:hover": {
backgroundColor: "rgba(189, 189, 189, 0.1)",
color: "#363434",
},
}}
>
<ClearIcon />
</IconButton>
</InputAdornment>
),
},
InputLabelProps: {
shrink: true,
},
},
}}
/>
{/* <FormHelperText id="component-helper-text" variant="caption" sx={{ ml: 2, color: error ? "#d32f2f" : "unset", fontWeight: 'bold' }}>
{helperText ? helperText : ""}
</FormHelperText> */}
</LocalizationProvider>
</Box>
);
}
export default MuiDatePicker;

View File

@@ -0,0 +1,30 @@
"use client";
import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
function CustomSelect({ item, filterParameters, handleChange }) {
return (
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel id={`label${item.id}`} shrink>
{item.header}
</InputLabel>
<Select
labelId={`label${item.id}`}
id={item.id}
name={`${item.id}.value`}
value={filterParameters.value}
input={<OutlinedInput label={item.header} />}
size="small"
onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
displayEmpty
>
{item.selectOption().map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelect;

View File

@@ -0,0 +1,30 @@
"use client";
import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
function CustomSelectByDependency({ item, filterParameters, value, handleChange, selectOption }) {
return (
<FormControl fullWidth sx={{ my: 1 }} size="small">
<InputLabel id={`label${item.id}`} shrink>
{item.header}
</InputLabel>
<Select
labelId={`label${item.id}`}
id={item.id}
name={`${item.id}.value`}
value={value}
size="small"
input={<OutlinedInput label={item.header} />}
onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
displayEmpty
>
{selectOption.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelectByDependency;

View File

@@ -0,0 +1,55 @@
"use client";
import {
Box,
Chip,
FormControl,
FormHelperText,
InputLabel,
MenuItem,
OutlinedInput,
Select,
Typography,
} from "@mui/material";
function CustomSelectMultiple({ item, filterParameters, handleChange }) {
const selectOption = item.selectOption;
const getLabelForValue = (value) => {
const option = selectOption.find((opt) => opt.value === value);
return option ? option.label : value;
};
return (
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel htmlFor="uncontrolled-native" id={`label${item.id}`}>
{item.header}
</InputLabel>
<Select
labelId={`label${item.id}`}
id={item.id}
value={filterParameters.value}
multiple
size="small"
onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={getLabelForValue(value)} />
))}
</Box>
)}
input={<OutlinedInput label={item.header} />}
InputLabelProps={{ shrink: true }}
>
{selectOption.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelectMultiple;

View File

@@ -0,0 +1,29 @@
import { InputAdornment, TextField, Typography } from "@mui/material";
import FilterListIcon from "@mui/icons-material/FilterList";
function CustomTextField({ item, filterParameters, handleOpenFilterBox, handleBlur, handleChange }) {
return (
<TextField
id={item.id}
name={item.id}
label={item.header}
value={filterParameters.value}
onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
onBlur={handleBlur}
fullWidth
variant="outlined"
size="small"
sx={{ my: 1 }}
InputProps={{
endAdornment: (
<InputAdornment position="end" sx={{ cursor: "pointer" }} onClick={handleOpenFilterBox}>
<FilterListIcon />
</InputAdornment>
),
}}
InputLabelProps={{ shrink: true }}
/>
);
}
export default CustomTextField;

View File

@@ -0,0 +1,62 @@
import { Box, InputAdornment, TextField, Typography } from "@mui/material";
import FilterListIcon from "@mui/icons-material/FilterList";
function CustomTextFieldRange({
item,
defaultFilterTranslation,
handleOpenFilterBox,
handleChange,
filterParameters,
handleBlur,
errors,
}) {
return (
<Box sx={{ display: "flex" }}>
<TextField
id={`${item.id}.value[0]`}
name={`${item.id}.value[0]`}
onChange={(e) =>
handleChange({ ...filterParameters, value: [e.target.value, filterParameters.value[1]] })
}
onBlur={handleBlur}
label={<Typography>از {item.header}</Typography>}
value={filterParameters.value[0]}
fullWidth
error={touched?.[`${item.id}`]?.value && Boolean(errors?.[`${item.id}`]?.value)}
helperText={
<Typography variant="caption" sx={{ color: "#155175", fontWeight: "bold" }}>
نوع فیلتر: {defaultFilterTranslation}
</Typography>
}
variant="outlined"
size="small"
sx={{ my: 1, marginRight: 1 }}
/>
<TextField
id={`${item.id}.value[1]`}
name={`${item.id}.value[1]`}
onChange={(e) =>
handleChange({ ...filterParameters, value: [filterParameters.value[0], e.target.value] })
}
onBlur={handleBlur}
error={Boolean(errors?.[`${item.id}`]?.value)}
helperText={errors?.[`${item.id}`]?.value ? errors?.[`${item.id}`]?.value.message : null}
label={<Typography>تا {item.header}</Typography>}
value={filterParameters.value[1]}
fullWidth
variant="outlined"
size="small"
sx={{ my: 1 }}
InputProps={{
endAdornment: (
<InputAdornment position="end" sx={{ cursor: "pointer" }} onClick={handleOpenFilterBox}>
<FilterListIcon />
</InputAdornment>
),
}}
/>
</Box>
);
}
export default CustomTextFieldRange;

View File

@@ -0,0 +1,151 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Cancel, FilterAlt } from "@mui/icons-material";
import { Box, Button, IconButton, Typography } from "@mui/material";
import { useForm } from "react-hook-form";
import * as Yup from "yup";
import ScrollBox from "../ScrollBox";
import FilterController from "./FilterController";
import FilterControllerWithDependency from "./FilterControllerWithDependency";
const headerSx = {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
px: 2,
py: 1,
backgroundColor: "#155175",
boxShadow: "rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px",
maxWidth: "450px",
};
const headerTitleSx = { display: "flex", alignItems: "center" };
const headerIconSx = { color: "#fff", mr: 1 };
const iconButtonSx = { color: "#fff" };
const formContainerSx = { px: 2, py: 3 };
const footerSx = { display: "flex", justifyContent: "center", alignItems: "center", pb: 2 };
const submitButtonSx = {
px: 8,
boxShadow: "rgba(0, 0, 0, 0.23) 0px 6px 6px, rgba(0, 0, 0, 0.19) 10px 0px 20px",
backgroundColor: "primary2",
":hover": { backgroundColor: "primary2" },
};
const validationSchema = Yup.object({
activity_date_time: Yup.array()
.of(Yup.string().nullable())
.test({
test(value, ctx) {
const [start, end] = value || ["", ""];
if ((start && !end) || (!start && end)) {
return ctx.createError({ message: "این بخش را تکمیل نمایید" });
}
return true;
},
}),
});
const FilterDrawer = ({ defaultValues, setFilterData, closeDrawer }) => {
const {
control,
errors,
reset,
handleSubmit,
formState: { isDirty },
} = useForm({
defaultValues,
resolver: yupResolver(
Yup.object(
Object.keys(defaultValues).reduce((acc, key) => {
const initialValue = defaultValues[key];
if (initialValue.filterMode === "between") {
acc[key] = Yup.object().shape({
value: Yup.array()
.of(Yup.string().nullable())
.test({
test(value, ctx) {
const [start, end] = value || ["", ""];
if (
initialValue.datatype === "numeric" &&
parseInt(end, 10) <= parseInt(start, 10)
) {
return ctx.createError({
message: `مقدار وارده باید بیشتر از (${start}) باشد`,
});
} else if ((start && !end) || (!start && end)) {
return ctx.createError({
message: "این بخش را تکمیل نمایید",
});
}
return true;
},
}),
});
}
return acc;
}, {})
)
),
mode: "onBlur",
});
const onSubmit = (data) => {
setFilterData(data);
closeDrawer();
};
return (
<>
<Box sx={headerSx}>
<Box sx={headerTitleSx}>
<FilterAlt sx={headerIconSx} />
<Typography variant="h6" sx={{ color: "#fff" }}>
فیلتر
</Typography>
</Box>
<IconButton sx={iconButtonSx} onClick={closeDrawer}>
<Cancel sx={{ fontSize: "1em" }} />
</IconButton>
</Box>
<ScrollBox>
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={formContainerSx}>
{Object.values(defaultValues).map((item) =>
item.dependencyId ? (
<FilterControllerWithDependency
key={item.id}
item={item}
control={control}
errors={errors}
reset={reset}
/>
) : (
<FilterController
key={item.id}
item={item}
control={control}
errors={errors}
reset={reset}
/>
)
)}
<Box sx={footerSx}>
<Button
type="submit"
variant="contained"
size="large"
disabled={!isDirty}
sx={submitButtonSx}
>
اعمال فیلتر
</Button>
</Box>
</Box>
</form>
</ScrollBox>
</>
);
};
export default FilterDrawer;