trithuc-mvc-react 3.5.4 → 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,207 +1,441 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Typography,
|
|
6
|
+
Paper,
|
|
7
|
+
Divider,
|
|
8
|
+
alpha,
|
|
9
|
+
Collapse,
|
|
10
|
+
Badge,
|
|
11
|
+
Grid,
|
|
12
|
+
useTheme,
|
|
13
|
+
Tooltip,
|
|
14
|
+
Zoom,
|
|
15
|
+
IconButton,
|
|
16
|
+
Slider
|
|
17
|
+
} from "@mui/material";
|
|
18
|
+
import { styled } from "@mui/material/styles";
|
|
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";
|
|
22
|
+
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
|
23
|
+
import SearchRoundedIcon from "@mui/icons-material/SearchRounded";
|
|
24
|
+
import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
|
|
25
|
+
import KeyboardArrowUpRoundedIcon from "@mui/icons-material/KeyboardArrowUpRounded";
|
|
26
|
+
import TuneRoundedIcon from "@mui/icons-material/TuneRounded";
|
|
27
|
+
import moment from "moment";
|
|
28
|
+
import { debounce } from "lodash";
|
|
29
|
+
import { toast } from "react-toastify";
|
|
30
|
+
import "dayjs/locale/vi";
|
|
31
|
+
|
|
4
32
|
import { FilterElement } from "./FilterElement";
|
|
5
33
|
import { useDataTable } from "./hooks";
|
|
6
34
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
// --- Styled Components ---
|
|
36
|
+
|
|
37
|
+
const MainCard = styled(Paper)(({ theme }) => ({
|
|
38
|
+
padding: theme.spacing(1),
|
|
39
|
+
borderRadius: "12px",
|
|
40
|
+
border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
|
|
41
|
+
boxShadow: "0 10px 40px -10px rgba(0,0,0,0.05)",
|
|
42
|
+
backgroundColor: theme.palette.background.paper
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const AdvancedSection = styled(Box)(({ theme }) => ({
|
|
46
|
+
marginTop: theme.spacing(2),
|
|
47
|
+
padding: theme.spacing(2),
|
|
48
|
+
borderRadius: "10px",
|
|
49
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.02),
|
|
50
|
+
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
|
|
51
|
+
position: "relative",
|
|
52
|
+
"&::before": {
|
|
53
|
+
content: '""',
|
|
54
|
+
position: "absolute",
|
|
55
|
+
top: 0,
|
|
56
|
+
left: 0,
|
|
57
|
+
right: 0,
|
|
58
|
+
height: "4px",
|
|
59
|
+
background: `linear-gradient(90deg, ${theme.palette.primary.main}, ${theme.palette.info.main})`,
|
|
60
|
+
borderRadius: "20px 20px 0 0",
|
|
61
|
+
opacity: 0.6
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const StyledToggleButton = styled(Button, {
|
|
66
|
+
shouldForwardProp: (prop) => prop !== "expanded"
|
|
67
|
+
})(({ theme, expanded }) => ({
|
|
68
|
+
textTransform: "none",
|
|
69
|
+
fontWeight: 600,
|
|
70
|
+
fontSize: "0.75rem",
|
|
71
|
+
borderRadius: "6px",
|
|
72
|
+
padding: theme.spacing(1, 2),
|
|
73
|
+
color: expanded ? theme.palette.primary.main : theme.palette.text.secondary,
|
|
74
|
+
backgroundColor: expanded ? alpha(theme.palette.primary.main, 0.1) : "transparent",
|
|
75
|
+
"&:hover": {
|
|
76
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.15)
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const ActionButton = styled(Button)(({ theme }) => ({
|
|
81
|
+
borderRadius: "14px",
|
|
82
|
+
padding: "10px 24px",
|
|
83
|
+
fontWeight: 700,
|
|
84
|
+
textTransform: "none",
|
|
85
|
+
transition: "all 0.2s ease-in-out",
|
|
86
|
+
"&:hover": {
|
|
87
|
+
transform: "translateY(-2px)",
|
|
88
|
+
boxShadow: `0 8px 20px -6px ${alpha(theme.palette.primary.main, 0.5)}`
|
|
89
|
+
},
|
|
90
|
+
"&:active": {
|
|
91
|
+
transform: "translateY(0)"
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
export const FilterGod = ({
|
|
96
|
+
tableName,
|
|
97
|
+
filters = [],
|
|
98
|
+
filterButtons = [],
|
|
99
|
+
elementSize = "small",
|
|
100
|
+
setPage = () => {}
|
|
101
|
+
}) => {
|
|
102
|
+
const theme = useTheme();
|
|
20
103
|
const { setDataSearch, dataSearch } = useDataTable();
|
|
21
|
-
|
|
22
|
-
|
|
104
|
+
|
|
105
|
+
// 1. Phân loại bộ lọc (Sửa logic: isAdvanced là bộ lọc ẩn)
|
|
106
|
+
const { basicFilters, advancedFilters } = useMemo(() => {
|
|
107
|
+
return {
|
|
108
|
+
// Ưu tiên hiển thị các bộ lọc có gắn tag isAdvanced: true
|
|
109
|
+
basicFilters: filters.filter((f) => f.isAdvanced === true),
|
|
110
|
+
|
|
111
|
+
// Đưa các bộ lọc còn lại (không phải params ẩn) vào vùng Collapse
|
|
112
|
+
advancedFilters: filters.filter(
|
|
113
|
+
(f) => f.isAdvanced !== true && f.type !== "default" && f.field // Đảm bảo có field để hiển thị
|
|
114
|
+
)
|
|
115
|
+
};
|
|
116
|
+
}, [filters]);
|
|
117
|
+
|
|
118
|
+
const [showAdvanced, setShowAdvanced] = useState(
|
|
23
119
|
() => JSON.parse(localStorage.getItem(`${tableName}-isFilterVisible`)) ?? false
|
|
24
120
|
);
|
|
25
121
|
|
|
26
|
-
// Lưu trạng thái vào localStorage mỗi khi thay đổi
|
|
27
122
|
useEffect(() => {
|
|
28
|
-
localStorage.setItem(`${tableName}-isFilterVisible`, JSON.stringify(
|
|
29
|
-
}, [
|
|
123
|
+
localStorage.setItem(`${tableName}-isFilterVisible`, JSON.stringify(showAdvanced));
|
|
124
|
+
}, [showAdvanced, tableName]);
|
|
30
125
|
|
|
31
|
-
|
|
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
|
+
);
|
|
32
134
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
135
|
+
// 3. Render Date Range với Validation
|
|
136
|
+
const renderDateRange = (filter) => {
|
|
137
|
+
const { field, label } = filter;
|
|
138
|
+
const [label1, label2] = Array.isArray(label) ? label : ["Từ ngày", "Đến ngày"];
|
|
139
|
+
|
|
140
|
+
const handleDateChange = debounce((newValue, fieldKey, compareKey, compareType) => {
|
|
141
|
+
let formattedDate = null;
|
|
142
|
+
|
|
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
|
+
|
|
147
|
+
formattedDate = fieldKey.toLowerCase().includes("from")
|
|
148
|
+
? moment(newValue).startOf("day").format("YYYY-MM-DD HH:mm")
|
|
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
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setDataSearch((prev) => ({ ...prev, [fieldKey]: formattedDate }));
|
|
169
|
+
setPage(0);
|
|
170
|
+
}, 500);
|
|
171
|
+
|
|
172
|
+
const innerDatePicker = (fieldKey, label, compareKey, compareType) => (
|
|
173
|
+
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="vi">
|
|
174
|
+
<DatePicker
|
|
175
|
+
label={label}
|
|
176
|
+
format="DD/MM/YYYY"
|
|
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 }}
|
|
183
|
+
slotProps={{
|
|
184
|
+
textField: {
|
|
185
|
+
fullWidth: true,
|
|
186
|
+
size: elementSize,
|
|
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
|
+
)
|
|
194
|
+
}
|
|
41
195
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
196
|
+
actionBar: { actions: ["clear"] }
|
|
197
|
+
}}
|
|
198
|
+
value={dataSearch?.[fieldKey] ? moment(dataSearch[fieldKey]) : null}
|
|
199
|
+
onChange={(val) => handleDateChange(val, fieldKey, compareKey, compareType)}
|
|
200
|
+
/>
|
|
201
|
+
</LocalizationProvider>
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<Grid container spacing={1} key={field.toString()}>
|
|
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>
|
|
208
|
+
</Grid>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
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" }
|
|
45
241
|
}}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
242
|
+
/>
|
|
243
|
+
</Box>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<Box sx={{ mb: 1 }}>
|
|
249
|
+
<MainCard elevation={0}>
|
|
250
|
+
{/* Header Section */}
|
|
251
|
+
<Box sx={{ mb: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
252
|
+
<Tooltip
|
|
253
|
+
title={
|
|
254
|
+
<Box sx={{ p: 1 }}>
|
|
255
|
+
<Typography
|
|
256
|
+
variant="caption"
|
|
257
|
+
display="block"
|
|
258
|
+
sx={{
|
|
259
|
+
fontWeight: 800,
|
|
260
|
+
mb: 0.5,
|
|
261
|
+
color: theme.palette.primary.light, // Làm nổi bật tiêu đề hướng dẫn
|
|
262
|
+
textTransform: "uppercase",
|
|
263
|
+
letterSpacing: "0.5px"
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
Hướng dẫn lọc dữ liệu
|
|
267
|
+
</Typography>
|
|
268
|
+
<Typography variant="caption" sx={{ lineHeight: 1.6, opacity: 0.9 }}>
|
|
269
|
+
Sử dụng các tiêu chí <b>Cơ bản</b> phía dưới hoặc kết hợp thêm
|
|
270
|
+
<b style={{ color: theme.palette.primary.light }}> Bộ lọc nâng cao </b>
|
|
271
|
+
để tìm kiếm kết quả chính xác nhất.
|
|
272
|
+
</Typography>
|
|
273
|
+
</Box>
|
|
274
|
+
}
|
|
275
|
+
placement="right"
|
|
276
|
+
arrow
|
|
277
|
+
TransitionComponent={Zoom}
|
|
278
|
+
slotProps={{
|
|
279
|
+
tooltip: {
|
|
280
|
+
sx: {
|
|
281
|
+
backgroundColor: alpha(theme.palette.grey[900], 0.95), // Màu nền tối sang trọng
|
|
282
|
+
color: "#fff",
|
|
283
|
+
borderRadius: "12px",
|
|
284
|
+
padding: "10px 14px",
|
|
285
|
+
boxShadow: `0 8px 24px ${alpha(theme.palette.common.black, 0.2)}`,
|
|
286
|
+
maxWidth: 250,
|
|
287
|
+
border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
arrow: {
|
|
291
|
+
sx: { color: alpha(theme.palette.grey[900], 0.95) }
|
|
292
|
+
}
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<Box
|
|
296
|
+
sx={{
|
|
297
|
+
display: "flex",
|
|
298
|
+
alignItems: "center",
|
|
299
|
+
gap: 1.2,
|
|
300
|
+
cursor: "help",
|
|
301
|
+
p: "4px 8px",
|
|
302
|
+
ml: -1, // Cân bằng lề trái khi hover
|
|
303
|
+
borderRadius: "8px",
|
|
304
|
+
transition: "all 0.2s ease",
|
|
305
|
+
"&:hover": {
|
|
306
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.05), // Hiệu ứng nền nhẹ khi hover
|
|
307
|
+
"& .search-icon": {
|
|
308
|
+
transform: "scale(1.1) rotate(-5deg)", // Icon động nhẹ
|
|
309
|
+
color: theme.palette.primary.dark
|
|
84
310
|
}
|
|
311
|
+
}
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<SearchRoundedIcon
|
|
315
|
+
className="search-icon"
|
|
316
|
+
sx={{
|
|
317
|
+
fontSize: "1.35rem",
|
|
318
|
+
color: theme.palette.primary.main,
|
|
319
|
+
transition: "all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
320
|
+
opacity: 0.9
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
85
323
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
324
|
+
<Typography
|
|
325
|
+
sx={{
|
|
326
|
+
fontWeight: 700,
|
|
327
|
+
fontSize: "0.75rem !important",
|
|
328
|
+
letterSpacing: "0.2px",
|
|
329
|
+
color: "text.primary",
|
|
330
|
+
position: "relative",
|
|
331
|
+
"&::after": {
|
|
332
|
+
// Thanh trang trí nhỏ dưới chữ
|
|
333
|
+
content: '""',
|
|
334
|
+
position: "absolute",
|
|
335
|
+
bottom: -2,
|
|
336
|
+
left: 0,
|
|
337
|
+
width: "40%",
|
|
338
|
+
height: "2px",
|
|
339
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.3),
|
|
340
|
+
borderRadius: "2px"
|
|
104
341
|
}
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
BỘ LỌC DỮ LIỆU
|
|
345
|
+
</Typography>
|
|
346
|
+
</Box>
|
|
347
|
+
</Tooltip>
|
|
105
348
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
startAdornment: (
|
|
139
|
-
<IconButton onClick={() => handleClear(fieldKey)} aria-label="clear">
|
|
140
|
-
<ClearIcon />
|
|
141
|
-
</IconButton>
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
actionBar: { actions: ["clear"] }
|
|
146
|
-
}}
|
|
147
|
-
value={dataSearch?.[fieldKey] ? moment(dataSearch?.[fieldKey]) : null}
|
|
148
|
-
onChange={(newValue) => handleDateChange(newValue, fieldKey, compareKey, compareType)}
|
|
149
|
-
size={elementSize}
|
|
150
|
-
/>
|
|
151
|
-
</LocalizationProvider>
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<Grid sx={{ mb: "5px" }} container key={field.toString()} size={{ ...size }}>
|
|
156
|
-
<Grid size={{ xs: 12, md: 6 }}>{renderDatePicker(field[0], label1, field[1], "max")}</Grid>
|
|
157
|
-
<Grid size={{ xs: 12, md: 6 }}>{renderDatePicker(field[1], label2, field[0], "min")}</Grid>
|
|
158
|
-
</Grid>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
349
|
+
<Box sx={{ display: "flex", gap: 1 }}>
|
|
350
|
+
{advancedFilters.length > 0 && (
|
|
351
|
+
<Tooltip
|
|
352
|
+
title={showAdvanced ? "" : "Mở rộng bộ lọc với các tiêu chí nâng cao"}
|
|
353
|
+
placement="top"
|
|
354
|
+
arrow
|
|
355
|
+
disableInteractive
|
|
356
|
+
>
|
|
357
|
+
<Box component="span">
|
|
358
|
+
{" "}
|
|
359
|
+
{/* Box span giúp Tooltip hoạt động ổn định kể cả khi Badge có vấn đề */}
|
|
360
|
+
<Badge
|
|
361
|
+
badgeContent={advancedFilters.length}
|
|
362
|
+
color="primary"
|
|
363
|
+
variant="dot"
|
|
364
|
+
invisible={showAdvanced}
|
|
365
|
+
sx={{ "& .MuiBadge-dot": { top: 4, right: 4 } }}
|
|
366
|
+
>
|
|
367
|
+
<StyledToggleButton
|
|
368
|
+
expanded={showAdvanced}
|
|
369
|
+
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
370
|
+
startIcon={<TuneRoundedIcon />}
|
|
371
|
+
endIcon={showAdvanced ? <KeyboardArrowUpRoundedIcon /> : <KeyboardArrowDownRoundedIcon />}
|
|
372
|
+
>
|
|
373
|
+
{showAdvanced ? "Thu gọn" : "Nâng cao"}
|
|
374
|
+
</StyledToggleButton>
|
|
375
|
+
</Badge>
|
|
376
|
+
</Box>
|
|
377
|
+
</Tooltip>
|
|
378
|
+
)}
|
|
379
|
+
</Box>
|
|
380
|
+
</Box>
|
|
161
381
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
382
|
+
{/* 1. Basic Filters */}
|
|
383
|
+
<Grid container spacing={2}>
|
|
384
|
+
{basicFilters.map((f) => (
|
|
385
|
+
<Grid key={f.field.toString()} size={f.size || { xs: 12, md: 4, lg: 3 }}>
|
|
386
|
+
{f.type === "date-range" ? (
|
|
387
|
+
renderDateRange(f)
|
|
388
|
+
) : f.type === "slider-range" ? (
|
|
389
|
+
renderSliderRange(f)
|
|
390
|
+
) : (
|
|
391
|
+
<FilterElement {...f} name={f.field.toString()} setPage={setPage} size={elementSize} />
|
|
392
|
+
)}
|
|
393
|
+
</Grid>
|
|
394
|
+
))}
|
|
395
|
+
</Grid>
|
|
396
|
+
|
|
397
|
+
{/* 2. Advanced Filters */}
|
|
398
|
+
<Collapse in={showAdvanced} timeout={400}>
|
|
399
|
+
<AdvancedSection>
|
|
400
|
+
<Grid container spacing={2}>
|
|
401
|
+
{advancedFilters.map((f) => (
|
|
402
|
+
<Grid key={f.field.toString()} size={f.size || { xs: 12, md: 4, lg: 3 }}>
|
|
403
|
+
{f.type === "date-range" ? (
|
|
404
|
+
renderDateRange(f)
|
|
405
|
+
) : f.type === "slider-range" ? (
|
|
406
|
+
renderSliderRange(f)
|
|
407
|
+
) : (
|
|
408
|
+
<FilterElement {...f} name={f.field.toString()} setPage={setPage} size={elementSize} />
|
|
409
|
+
)}
|
|
187
410
|
</Grid>
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
411
|
+
))}
|
|
412
|
+
</Grid>
|
|
413
|
+
</AdvancedSection>
|
|
414
|
+
</Collapse>
|
|
415
|
+
|
|
416
|
+
{/* Action Buttons */}
|
|
417
|
+
{filterButtons.length > 0 && (
|
|
418
|
+
<>
|
|
419
|
+
<Divider sx={{ my: 3, opacity: 0.6 }} />
|
|
420
|
+
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 2 }}>
|
|
421
|
+
{filterButtons.map((btn, idx) => (
|
|
422
|
+
<ActionButton
|
|
423
|
+
key={idx}
|
|
424
|
+
size={elementSize}
|
|
425
|
+
variant={btn.variant || "contained"}
|
|
426
|
+
color={btn.color || "primary"}
|
|
427
|
+
onClick={() => btn.onClick({ dataSearch })}
|
|
428
|
+
startIcon={btn.element}
|
|
429
|
+
disableElevation
|
|
430
|
+
sx={btn.sx}
|
|
431
|
+
>
|
|
432
|
+
{btn.title}
|
|
433
|
+
</ActionButton>
|
|
434
|
+
))}
|
|
435
|
+
</Box>
|
|
436
|
+
</>
|
|
437
|
+
)}
|
|
438
|
+
</MainCard>
|
|
205
439
|
</Box>
|
|
206
440
|
);
|
|
207
441
|
};
|
|
@@ -12,7 +12,7 @@ function TablePaginationCustom({ count, rowsPerPage, page, onPageChange, onRowsP
|
|
|
12
12
|
return !isSmallScreen ? (
|
|
13
13
|
<TablePagination
|
|
14
14
|
{...rest}
|
|
15
|
-
rowsPerPageOptions={[5, 10, 20, 25, 50, 100]}
|
|
15
|
+
rowsPerPageOptions={[5, 10, 20, 25, 50, 100, 1000]}
|
|
16
16
|
component="div"
|
|
17
17
|
count={count}
|
|
18
18
|
rowsPerPage={rowsPerPage}
|
|
@@ -42,7 +42,7 @@ function TablePaginationCustom({ count, rowsPerPage, page, onPageChange, onRowsP
|
|
|
42
42
|
) : (
|
|
43
43
|
<TablePagination
|
|
44
44
|
{...rest}
|
|
45
|
-
rowsPerPageOptions={[5, 10, 20, 25, 50, 100]}
|
|
45
|
+
rowsPerPageOptions={[5, 10, 20, 25, 50, 100, 1000]}
|
|
46
46
|
component="div"
|
|
47
47
|
count={count}
|
|
48
48
|
rowsPerPage={rowsPerPage}
|