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 { Autocomplete, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, TextField } from "@mui/material";
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 sx={{ minWidth: 160 }} size="small" fullWidth>
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 { Accordion, AccordionSummary, Box, Button, IconButton, Slider, Typography } from "@mui/material";
2
- import { Grid } from "@mui/material";
3
- import { useFormContext } from "react-hook-form";
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
- import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
8
- import AccordionDetails from "@mui/material/AccordionDetails";
9
- import moment from "moment";
10
- import { useEffect, useState } from "react";
11
- import { DateRangePicker } from "../date";
12
- import { ClearIcon, DatePicker } from "@mui/x-date-pickers";
13
- import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
14
- import "dayjs/locale/vi";
15
- import { toast } from "react-toastify";
16
- import { debounce } from "lodash";
17
- export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "small", setPage = () => {} }) => {
18
- const { handleSubmit } = useFormContext();
19
- const onSubmit = (data) => console.log(data);
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
- // Lấy trạng thái từ localStorage hoặc mặc định là true
22
- const [isFilterVisible, setFilterVisible] = useState(
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(isFilterVisible));
29
- }, [isFilterVisible]);
123
+ localStorage.setItem(`${tableName}-isFilterVisible`, JSON.stringify(showAdvanced));
124
+ }, [showAdvanced, tableName]);
30
125
 
31
- const toggleFilterVisibility = () => setFilterVisible((prev) => !prev);
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
- return (
34
- <Box component={"form"}>
35
- <Accordion expanded={isFilterVisible} onChange={toggleFilterVisibility}>
36
- <AccordionSummary
37
- sx={{
38
- minHeight: 40,
39
- "&.Mui-expanded": {
40
- minHeight: 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
- "& .MuiAccordionSummary-content": {
43
- my: 0.5
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
- expandIcon={<ExpandMoreIcon sx={{ fontSize: "0.95rem", minHeight: "40px" }} />}
47
- aria-controls="panel1a-content"
48
- id="panel1a-header"
49
- >
50
- <Typography variant="h6" sx={{ fontSize: "0.75rem" }}>
51
- Tìm kiếm
52
- </Typography>
53
- </AccordionSummary>
54
-
55
- <AccordionDetails>
56
- <Grid container spacing={1}>
57
- {filters.map(({ field, label, placeHolder, size, ...rest }) => {
58
- if (rest.type === "date-range") {
59
- const [label1, label2] = Array.isArray(label) ? label : ["Từ ngày", "Đến ngày"];
60
- const handleClear = (key) => {
61
- setDataSearch((prev) => ({
62
- ...prev,
63
- [key]: null // Xóa giá trị của trường được chọn
64
- }));
65
- setPage(0); // Reset trang khi clear
66
- };
67
-
68
- const handleDateChange = debounce((newValue, fieldKey, compareKey, compareType) => {
69
- let formattedDate = null;
70
-
71
- // Chỉ kiểm tra nếu chuỗi có độ dài tối thiểu phù hợp với định dạng "DD/MM/YYYY"
72
- const isValidDate = moment(newValue, "DD/MM/YYYY", true).isValid();
73
-
74
- const year = moment(newValue, "DD/MM/YYYY").year();
75
-
76
- if (isValidDate && year >= 1000) {
77
- // Kiểm tra năm có lớn hơn hoặc bằng 1000
78
- // Định dạng ngày khi hợp lệ
79
- if (fieldKey.toLowerCase().includes("from")) {
80
- formattedDate = moment(newValue, "DD/MM/YYYY").startOf("day").format("YYYY-MM-DD HH:mm");
81
- } else if (fieldKey.toLowerCase().includes("to")) {
82
- formattedDate = moment(newValue, "DD/MM/YYYY").endOf("day").format("YYYY-MM-DD HH:mm");
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
- const compareDate = dataSearch?.[compareKey] ? moment(dataSearch?.[compareKey]) : null;
87
-
88
- // Kiểm tra ngày so sánh
89
- if (formattedDate && compareDate) {
90
- const isInvalid =
91
- (compareType === "min" && moment(formattedDate).isBefore(compareDate)) ||
92
- (compareType === "max" && moment(formattedDate).isAfter(compareDate));
93
-
94
- if (isInvalid) {
95
- toast.error(
96
- 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."
97
- );
98
- setDataSearch((prev) => ({
99
- ...prev,
100
- [fieldKey]: null
101
- }));
102
- return;
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
- // Lưu ngày hợp lệ
107
- setDataSearch((prev) => ({
108
- ...prev,
109
- [fieldKey]: formattedDate
110
- }));
111
- setPage(0); // Reset page
112
- }, 500); // Debounce 1000ms
113
-
114
- // Gọi handleDateChange
115
- const onInputDateChange = (e) => {
116
- handleDateChange(e.target.value, "from", "to", "min");
117
- };
118
-
119
- const renderDatePicker = (fieldKey, label, compareKey, compareType) => (
120
- <LocalizationProvider adapterLocale="vi" localeText={{ clearButtonLabel: "Xóa" }}>
121
- <DatePicker
122
- label={label}
123
- format="DD/MM/YYYY"
124
- desktopModeMediaQuery="@media (min-width: 0px)"
125
- views={["year", "month", "day"]}
126
- mask="__/__/____"
127
- minDate={
128
- compareType === "min" ? (dataSearch?.[compareKey] ? moment(dataSearch?.[compareKey]) : null) : null
129
- }
130
- maxDate={
131
- compareType === "max" ? (dataSearch?.[compareKey] ? moment(dataSearch?.[compareKey]) : null) : null
132
- }
133
- InputLabelProps={{ shrink: true }}
134
- slotProps={{
135
- textField: {
136
- fullWidth: true,
137
- InputProps: {
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
- if (rest.type === "slider-range") {
163
- return (
164
- <Grid sx={{ mb: "5px" }} key={field.toString()} size={{ ...size }}>
165
- <Slider
166
- onChange={(e, value) => {
167
- setDataSearch({
168
- ...dataSearch,
169
- [field[0]]: value[0],
170
- [field[1]]: value[1]
171
- });
172
- setPage(0);
173
- }}
174
- size={elementSize}
175
- marks={rest.marks}
176
- defaultValue={rest.defaultValue}
177
- valueLabelDisplay="auto"
178
- max={rest.marks[rest.marks.length - 1]?.value}
179
- step={null}
180
- />
181
- </Grid>
182
- );
183
- }
184
- return (
185
- <Grid sx={{ mb: "5px" }} key={field.toString()} size={{ ...size }}>
186
- <FilterElement name={field.toString()} label={label} placeholder={placeHolder} {...rest} setPage={setPage} />
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
- {filterButtons.map(({ title, size, onClick, element, sx }, idx) => (
191
- <Button
192
- key={idx}
193
- size={size}
194
- variant="outlined"
195
- onClick={() => onClick({ dataSearch })}
196
- startIcon={element}
197
- sx={{ ...sx }}
198
- >
199
- {title}
200
- </Button>
201
- ))}
202
- </Grid>
203
- </AccordionDetails>
204
- </Accordion>
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}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trithuc-mvc-react",
3
- "version": "3.5.4",
3
+ "version": "3.5.6",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"