trithuc-mvc-react 3.5.5 → 3.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { SearchOutlined } from "@mui/icons-material";
|
|
2
2
|
import ClearIcon from "@mui/icons-material/Clear";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Autocomplete,
|
|
5
|
+
FormControl,
|
|
6
|
+
IconButton,
|
|
7
|
+
InputAdornment,
|
|
8
|
+
InputLabel,
|
|
9
|
+
MenuItem,
|
|
10
|
+
Select,
|
|
11
|
+
TextField
|
|
12
|
+
} from "@mui/material";
|
|
4
13
|
import { debounce } from "lodash";
|
|
5
14
|
import { useCallback } from "react";
|
|
6
15
|
import { useController, useFormContext } from "react-hook-form";
|
|
@@ -78,7 +87,7 @@ export function FilterElement({
|
|
|
78
87
|
if (multiple) {
|
|
79
88
|
const autocompleteValue = multiple
|
|
80
89
|
? datas.filter((item) => (value || []).includes(item[keyValue]))
|
|
81
|
-
: datas.find((item) => item[keyValue] === value) ?? null;
|
|
90
|
+
: (datas.find((item) => item[keyValue] === value) ?? null);
|
|
82
91
|
|
|
83
92
|
const parsedValue = multiple ? autocompleteValue : autocompleteValue;
|
|
84
93
|
|
|
@@ -155,7 +164,28 @@ export function FilterElement({
|
|
|
155
164
|
}
|
|
156
165
|
case "select":
|
|
157
166
|
return (
|
|
158
|
-
<FormControl
|
|
167
|
+
<FormControl
|
|
168
|
+
sx={{
|
|
169
|
+
minWidth: 160,
|
|
170
|
+
// Ép chiều cao của toàn bộ FormControl
|
|
171
|
+
"& .MuiInputBase-root": {
|
|
172
|
+
height: "37px" // Bạn có thể chỉnh 36px hoặc 38px cho vừa mắt
|
|
173
|
+
},
|
|
174
|
+
"& .MuiOutlinedInput-input": {
|
|
175
|
+
paddingY: "0px" // Đảm bảo text bên trong căn giữa khi thu nhỏ chiều cao
|
|
176
|
+
},
|
|
177
|
+
"& .MuiInputLabel-root": {
|
|
178
|
+
// Điều chỉnh vị trí label khi thu nhỏ để không bị đè lên border
|
|
179
|
+
lineHeight: "1em",
|
|
180
|
+
top: "-2px"
|
|
181
|
+
},
|
|
182
|
+
"& .MuiInputLabel-shrink": {
|
|
183
|
+
top: "0px"
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
size="small"
|
|
187
|
+
fullWidth
|
|
188
|
+
>
|
|
159
189
|
<InputLabel shrink={true}>{label}</InputLabel>
|
|
160
190
|
<Select
|
|
161
191
|
{...rest}
|
|
@@ -166,6 +196,11 @@ export function FilterElement({
|
|
|
166
196
|
placeholder={placeholder}
|
|
167
197
|
value={value}
|
|
168
198
|
displayEmpty
|
|
199
|
+
sx={{
|
|
200
|
+
"& .MuiOutlinedInput-root": {
|
|
201
|
+
height: "40px !important"
|
|
202
|
+
}
|
|
203
|
+
}}
|
|
169
204
|
onChange={(e) => {
|
|
170
205
|
handleChange(e);
|
|
171
206
|
onFieldChange(e);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useState, useMemo } from "react";
|
|
1
|
+
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
4
|
Button,
|
|
@@ -11,10 +11,14 @@ import {
|
|
|
11
11
|
Grid,
|
|
12
12
|
useTheme,
|
|
13
13
|
Tooltip,
|
|
14
|
-
Zoom
|
|
14
|
+
Zoom,
|
|
15
|
+
IconButton,
|
|
16
|
+
Slider
|
|
15
17
|
} from "@mui/material";
|
|
16
18
|
import { styled } from "@mui/material/styles";
|
|
17
|
-
import {
|
|
19
|
+
import { Clear as ClearIcon } from "@mui/icons-material";
|
|
20
|
+
import { DatePicker } from "@mui/x-date-pickers";
|
|
21
|
+
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
|
18
22
|
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
|
19
23
|
import SearchRoundedIcon from "@mui/icons-material/SearchRounded";
|
|
20
24
|
import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
|
|
@@ -22,6 +26,7 @@ import KeyboardArrowUpRoundedIcon from "@mui/icons-material/KeyboardArrowUpRound
|
|
|
22
26
|
import TuneRoundedIcon from "@mui/icons-material/TuneRounded";
|
|
23
27
|
import moment from "moment";
|
|
24
28
|
import { debounce } from "lodash";
|
|
29
|
+
import { toast } from "react-toastify";
|
|
25
30
|
import "dayjs/locale/vi";
|
|
26
31
|
|
|
27
32
|
import { FilterElement } from "./FilterElement";
|
|
@@ -87,10 +92,17 @@ const ActionButton = styled(Button)(({ theme }) => ({
|
|
|
87
92
|
}
|
|
88
93
|
}));
|
|
89
94
|
|
|
90
|
-
export const FilterGod = ({
|
|
95
|
+
export const FilterGod = ({
|
|
96
|
+
tableName,
|
|
97
|
+
filters = [],
|
|
98
|
+
filterButtons = [],
|
|
99
|
+
elementSize = "small",
|
|
100
|
+
setPage = () => {}
|
|
101
|
+
}) => {
|
|
91
102
|
const theme = useTheme();
|
|
92
103
|
const { setDataSearch, dataSearch } = useDataTable();
|
|
93
|
-
|
|
104
|
+
|
|
105
|
+
// 1. Phân loại bộ lọc (Sửa logic: isAdvanced là bộ lọc ẩn)
|
|
94
106
|
const { basicFilters, advancedFilters } = useMemo(() => {
|
|
95
107
|
return {
|
|
96
108
|
// Ưu tiên hiển thị các bộ lọc có gắn tag isAdvanced: true
|
|
@@ -104,63 +116,138 @@ export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "sm
|
|
|
104
116
|
}, [filters]);
|
|
105
117
|
|
|
106
118
|
const [showAdvanced, setShowAdvanced] = useState(
|
|
107
|
-
() => JSON.parse(localStorage.getItem(`${tableName}-
|
|
119
|
+
() => JSON.parse(localStorage.getItem(`${tableName}-isFilterVisible`)) ?? false
|
|
108
120
|
);
|
|
109
121
|
|
|
110
122
|
useEffect(() => {
|
|
111
|
-
localStorage.setItem(`${tableName}-
|
|
112
|
-
}, [showAdvanced]);
|
|
123
|
+
localStorage.setItem(`${tableName}-isFilterVisible`, JSON.stringify(showAdvanced));
|
|
124
|
+
}, [showAdvanced, tableName]);
|
|
125
|
+
|
|
126
|
+
// 2. Hàm xóa giá trị lọc
|
|
127
|
+
const handleClear = useCallback(
|
|
128
|
+
(fieldKey) => {
|
|
129
|
+
setDataSearch((prev) => ({ ...prev, [fieldKey]: null }));
|
|
130
|
+
setPage(0);
|
|
131
|
+
},
|
|
132
|
+
[setDataSearch, setPage]
|
|
133
|
+
);
|
|
113
134
|
|
|
135
|
+
// 3. Render Date Range với Validation
|
|
114
136
|
const renderDateRange = (filter) => {
|
|
115
137
|
const { field, label } = filter;
|
|
116
138
|
const [label1, label2] = Array.isArray(label) ? label : ["Từ ngày", "Đến ngày"];
|
|
117
139
|
|
|
118
|
-
const handleDateChange = debounce((newValue, fieldKey) => {
|
|
140
|
+
const handleDateChange = debounce((newValue, fieldKey, compareKey, compareType) => {
|
|
119
141
|
let formattedDate = null;
|
|
142
|
+
|
|
120
143
|
if (newValue && moment(newValue).isValid()) {
|
|
144
|
+
const year = moment(newValue).year();
|
|
145
|
+
if (year < 1000) return; // Tránh xử lý khi đang gõ dở năm
|
|
146
|
+
|
|
121
147
|
formattedDate = fieldKey.toLowerCase().includes("from")
|
|
122
148
|
? moment(newValue).startOf("day").format("YYYY-MM-DD HH:mm")
|
|
123
149
|
: moment(newValue).endOf("day").format("YYYY-MM-DD HH:mm");
|
|
150
|
+
|
|
151
|
+
// Logic so sánh ngày
|
|
152
|
+
const compareDate = dataSearch?.[compareKey] ? moment(dataSearch[compareKey]) : null;
|
|
153
|
+
if (compareDate) {
|
|
154
|
+
const isInvalid =
|
|
155
|
+
(compareType === "min" && moment(formattedDate).isBefore(compareDate)) ||
|
|
156
|
+
(compareType === "max" && moment(formattedDate).isAfter(compareDate));
|
|
157
|
+
|
|
158
|
+
if (isInvalid) {
|
|
159
|
+
toast.error(
|
|
160
|
+
compareType === "min" ? "Ngày đến không được nhỏ hơn ngày từ." : "Ngày từ không được lớn hơn ngày đến."
|
|
161
|
+
);
|
|
162
|
+
setDataSearch((prev) => ({ ...prev, [fieldKey]: null }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
124
166
|
}
|
|
167
|
+
|
|
125
168
|
setDataSearch((prev) => ({ ...prev, [fieldKey]: formattedDate }));
|
|
126
169
|
setPage(0);
|
|
127
170
|
}, 500);
|
|
128
171
|
|
|
129
|
-
const innerDatePicker = (fieldKey,
|
|
172
|
+
const innerDatePicker = (fieldKey, label, compareKey, compareType) => (
|
|
130
173
|
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="vi">
|
|
131
174
|
<DatePicker
|
|
132
|
-
label={
|
|
175
|
+
label={label}
|
|
133
176
|
format="DD/MM/YYYY"
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
desktopModeMediaQuery="@media (min-width: 0px)"
|
|
178
|
+
views={["year", "month", "day"]}
|
|
179
|
+
mask="__/__/____"
|
|
180
|
+
minDate={compareType === "min" ? (dataSearch?.[compareKey] ? moment(dataSearch?.[compareKey]) : null) : null}
|
|
181
|
+
maxDate={compareType === "max" ? (dataSearch?.[compareKey] ? moment(dataSearch?.[compareKey]) : null) : null}
|
|
182
|
+
InputLabelProps={{ shrink: true }}
|
|
136
183
|
slotProps={{
|
|
137
184
|
textField: {
|
|
138
185
|
fullWidth: true,
|
|
139
186
|
size: elementSize,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
InputLabelProps: { shrink: true },
|
|
188
|
+
InputProps: {
|
|
189
|
+
startAdornment: (
|
|
190
|
+
<IconButton size="small" onClick={() => handleClear(fieldKey)} sx={{ ml: -1 }}>
|
|
191
|
+
<ClearIcon fontSize="small" />
|
|
192
|
+
</IconButton>
|
|
193
|
+
)
|
|
145
194
|
}
|
|
146
|
-
}
|
|
195
|
+
},
|
|
196
|
+
actionBar: { actions: ["clear"] }
|
|
147
197
|
}}
|
|
198
|
+
value={dataSearch?.[fieldKey] ? moment(dataSearch[fieldKey]) : null}
|
|
199
|
+
onChange={(val) => handleDateChange(val, fieldKey, compareKey, compareType)}
|
|
148
200
|
/>
|
|
149
201
|
</LocalizationProvider>
|
|
150
202
|
);
|
|
151
203
|
|
|
152
204
|
return (
|
|
153
205
|
<Grid container spacing={1} key={field.toString()}>
|
|
154
|
-
<Grid size={{ xs: 12, sm: 6 }}>{innerDatePicker(field[0], label1)}</Grid>
|
|
155
|
-
<Grid size={{ xs: 12, sm: 6 }}>{innerDatePicker(field[1], label2)}</Grid>
|
|
206
|
+
<Grid size={{ xs: 12, sm: 6 }}>{innerDatePicker(field[0], label1, field[1], "max")}</Grid>
|
|
207
|
+
<Grid size={{ xs: 12, sm: 6 }}>{innerDatePicker(field[1], label2, field[0], "min")}</Grid>
|
|
156
208
|
</Grid>
|
|
157
209
|
);
|
|
158
210
|
};
|
|
159
211
|
|
|
212
|
+
// 4. Render Slider Range
|
|
213
|
+
const renderSliderRange = (filter) => {
|
|
214
|
+
const { field, label, marks, defaultValue } = filter;
|
|
215
|
+
|
|
216
|
+
const handleSliderChange = debounce((value) => {
|
|
217
|
+
setDataSearch((prev) => ({
|
|
218
|
+
...prev,
|
|
219
|
+
[field[0]]: value[0],
|
|
220
|
+
[field[1]]: value[1]
|
|
221
|
+
}));
|
|
222
|
+
setPage(0);
|
|
223
|
+
}, 400);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<Box sx={{ px: 2, pt: 1 }} key={field.toString()}>
|
|
227
|
+
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, mb: 1, display: "block" }}>
|
|
228
|
+
{label}
|
|
229
|
+
</Typography>
|
|
230
|
+
<Slider
|
|
231
|
+
onChange={(_, value) => handleSliderChange(value)}
|
|
232
|
+
size={elementSize}
|
|
233
|
+
marks={marks}
|
|
234
|
+
defaultValue={defaultValue}
|
|
235
|
+
valueLabelDisplay="auto"
|
|
236
|
+
max={marks?.[marks.length - 1]?.value || 100}
|
|
237
|
+
step={null}
|
|
238
|
+
sx={{
|
|
239
|
+
"& .MuiSlider-markLabel": { fontSize: "0.7rem" },
|
|
240
|
+
"& .MuiSlider-valueLabel": { borderRadius: "6px" }
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
</Box>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
160
247
|
return (
|
|
161
248
|
<Box sx={{ mb: 1 }}>
|
|
162
249
|
<MainCard elevation={0}>
|
|
163
|
-
{/* Header
|
|
250
|
+
{/* Header Section */}
|
|
164
251
|
<Box sx={{ mb: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
165
252
|
<Tooltip
|
|
166
253
|
title={
|
|
@@ -237,7 +324,7 @@ export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "sm
|
|
|
237
324
|
<Typography
|
|
238
325
|
sx={{
|
|
239
326
|
fontWeight: 700,
|
|
240
|
-
fontSize: "0.
|
|
327
|
+
fontSize: "0.75rem !important",
|
|
241
328
|
letterSpacing: "0.2px",
|
|
242
329
|
color: "text.primary",
|
|
243
330
|
position: "relative",
|
|
@@ -254,7 +341,7 @@ export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "sm
|
|
|
254
341
|
}
|
|
255
342
|
}}
|
|
256
343
|
>
|
|
257
|
-
|
|
344
|
+
BỘ LỌC DỮ LIỆU
|
|
258
345
|
</Typography>
|
|
259
346
|
</Box>
|
|
260
347
|
</Tooltip>
|
|
@@ -293,11 +380,13 @@ export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "sm
|
|
|
293
380
|
</Box>
|
|
294
381
|
|
|
295
382
|
{/* 1. Basic Filters */}
|
|
296
|
-
<Grid container spacing={2
|
|
383
|
+
<Grid container spacing={2}>
|
|
297
384
|
{basicFilters.map((f) => (
|
|
298
385
|
<Grid key={f.field.toString()} size={f.size || { xs: 12, md: 4, lg: 3 }}>
|
|
299
386
|
{f.type === "date-range" ? (
|
|
300
387
|
renderDateRange(f)
|
|
388
|
+
) : f.type === "slider-range" ? (
|
|
389
|
+
renderSliderRange(f)
|
|
301
390
|
) : (
|
|
302
391
|
<FilterElement {...f} name={f.field.toString()} setPage={setPage} size={elementSize} />
|
|
303
392
|
)}
|
|
@@ -308,11 +397,13 @@ export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "sm
|
|
|
308
397
|
{/* 2. Advanced Filters */}
|
|
309
398
|
<Collapse in={showAdvanced} timeout={400}>
|
|
310
399
|
<AdvancedSection>
|
|
311
|
-
<Grid container spacing={2
|
|
400
|
+
<Grid container spacing={2}>
|
|
312
401
|
{advancedFilters.map((f) => (
|
|
313
402
|
<Grid key={f.field.toString()} size={f.size || { xs: 12, md: 4, lg: 3 }}>
|
|
314
403
|
{f.type === "date-range" ? (
|
|
315
404
|
renderDateRange(f)
|
|
405
|
+
) : f.type === "slider-range" ? (
|
|
406
|
+
renderSliderRange(f)
|
|
316
407
|
) : (
|
|
317
408
|
<FilterElement {...f} name={f.field.toString()} setPage={setPage} size={elementSize} />
|
|
318
409
|
)}
|
|
@@ -321,6 +412,8 @@ export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "sm
|
|
|
321
412
|
</Grid>
|
|
322
413
|
</AdvancedSection>
|
|
323
414
|
</Collapse>
|
|
415
|
+
|
|
416
|
+
{/* Action Buttons */}
|
|
324
417
|
{filterButtons.length > 0 && (
|
|
325
418
|
<>
|
|
326
419
|
<Divider sx={{ my: 3, opacity: 0.6 }} />
|