trithuc-mvc-react 3.5.4 → 3.5.5

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,207 +1,348 @@
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 } 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
+ } from "@mui/material";
16
+ import { styled } from "@mui/material/styles";
17
+ import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
18
+ import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
19
+ import SearchRoundedIcon from "@mui/icons-material/SearchRounded";
20
+ import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
21
+ import KeyboardArrowUpRoundedIcon from "@mui/icons-material/KeyboardArrowUpRounded";
22
+ import TuneRoundedIcon from "@mui/icons-material/TuneRounded";
23
+ import moment from "moment";
24
+ import { debounce } from "lodash";
25
+ import "dayjs/locale/vi";
26
+
4
27
  import { FilterElement } from "./FilterElement";
5
28
  import { useDataTable } from "./hooks";
6
29
 
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";
30
+ // --- Styled Components ---
31
+
32
+ const MainCard = styled(Paper)(({ theme }) => ({
33
+ padding: theme.spacing(1),
34
+ borderRadius: "12px",
35
+ border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
36
+ boxShadow: "0 10px 40px -10px rgba(0,0,0,0.05)",
37
+ backgroundColor: theme.palette.background.paper
38
+ }));
39
+
40
+ const AdvancedSection = styled(Box)(({ theme }) => ({
41
+ marginTop: theme.spacing(2),
42
+ padding: theme.spacing(2),
43
+ borderRadius: "10px",
44
+ backgroundColor: alpha(theme.palette.primary.main, 0.02),
45
+ border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
46
+ position: "relative",
47
+ "&::before": {
48
+ content: '""',
49
+ position: "absolute",
50
+ top: 0,
51
+ left: 0,
52
+ right: 0,
53
+ height: "4px",
54
+ background: `linear-gradient(90deg, ${theme.palette.primary.main}, ${theme.palette.info.main})`,
55
+ borderRadius: "20px 20px 0 0",
56
+ opacity: 0.6
57
+ }
58
+ }));
59
+
60
+ const StyledToggleButton = styled(Button, {
61
+ shouldForwardProp: (prop) => prop !== "expanded"
62
+ })(({ theme, expanded }) => ({
63
+ textTransform: "none",
64
+ fontWeight: 600,
65
+ fontSize: "0.75rem",
66
+ borderRadius: "6px",
67
+ padding: theme.spacing(1, 2),
68
+ color: expanded ? theme.palette.primary.main : theme.palette.text.secondary,
69
+ backgroundColor: expanded ? alpha(theme.palette.primary.main, 0.1) : "transparent",
70
+ "&:hover": {
71
+ backgroundColor: alpha(theme.palette.primary.main, 0.15)
72
+ }
73
+ }));
74
+
75
+ const ActionButton = styled(Button)(({ theme }) => ({
76
+ borderRadius: "14px",
77
+ padding: "10px 24px",
78
+ fontWeight: 700,
79
+ textTransform: "none",
80
+ transition: "all 0.2s ease-in-out",
81
+ "&:hover": {
82
+ transform: "translateY(-2px)",
83
+ boxShadow: `0 8px 20px -6px ${alpha(theme.palette.primary.main, 0.5)}`
84
+ },
85
+ "&:active": {
86
+ transform: "translateY(0)"
87
+ }
88
+ }));
89
+
17
90
  export const FilterGod = ({ tableName, filters, filterButtons, elementSize = "small", setPage = () => {} }) => {
18
- const { handleSubmit } = useFormContext();
19
- const onSubmit = (data) => console.log(data);
91
+ const theme = useTheme();
20
92
  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(
23
- () => JSON.parse(localStorage.getItem(`${tableName}-isFilterVisible`)) ?? false
93
+ // Đoạn code hoàn chỉnh cho useMemo trong FilterGod
94
+ const { basicFilters, advancedFilters } = useMemo(() => {
95
+ return {
96
+ // Ưu tiên hiển thị các bộ lọc có gắn tag isAdvanced: true
97
+ basicFilters: filters.filter((f) => f.isAdvanced === true),
98
+
99
+ // Đưa các bộ lọc còn lại (không phải params ẩn) vào vùng Collapse
100
+ advancedFilters: filters.filter(
101
+ (f) => f.isAdvanced !== true && f.type !== "default" && f.field // Đảm bảo có field để hiển thị
102
+ )
103
+ };
104
+ }, [filters]);
105
+
106
+ const [showAdvanced, setShowAdvanced] = useState(
107
+ () => JSON.parse(localStorage.getItem(`${tableName}-showAdvanced`)) ?? false
24
108
  );
25
109
 
26
- // Lưu trạng thái vào localStorage mỗi khi thay đổi
27
110
  useEffect(() => {
28
- localStorage.setItem(`${tableName}-isFilterVisible`, JSON.stringify(isFilterVisible));
29
- }, [isFilterVisible]);
111
+ localStorage.setItem(`${tableName}-showAdvanced`, JSON.stringify(showAdvanced));
112
+ }, [showAdvanced]);
30
113
 
31
- const toggleFilterVisibility = () => setFilterVisible((prev) => !prev);
114
+ const renderDateRange = (filter) => {
115
+ const { field, label } = filter;
116
+ const [label1, label2] = Array.isArray(label) ? label : ["Từ ngày", "Đến ngày"];
32
117
 
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
41
- },
42
- "& .MuiAccordionSummary-content": {
43
- my: 0.5
118
+ const handleDateChange = debounce((newValue, fieldKey) => {
119
+ let formattedDate = null;
120
+ if (newValue && moment(newValue).isValid()) {
121
+ formattedDate = fieldKey.toLowerCase().includes("from")
122
+ ? moment(newValue).startOf("day").format("YYYY-MM-DD HH:mm")
123
+ : moment(newValue).endOf("day").format("YYYY-MM-DD HH:mm");
124
+ }
125
+ setDataSearch((prev) => ({ ...prev, [fieldKey]: formattedDate }));
126
+ setPage(0);
127
+ }, 500);
128
+
129
+ const innerDatePicker = (fieldKey, lbl) => (
130
+ <LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="vi">
131
+ <DatePicker
132
+ label={lbl}
133
+ format="DD/MM/YYYY"
134
+ value={dataSearch?.[fieldKey] ? moment(dataSearch?.[fieldKey]) : null}
135
+ onChange={(val) => handleDateChange(val, fieldKey)}
136
+ slotProps={{
137
+ textField: {
138
+ fullWidth: true,
139
+ size: elementSize,
140
+ sx: {
141
+ "& .MuiOutlinedInput-root": {
142
+ borderRadius: "6px",
143
+ backgroundColor: alpha(theme.palette.common.white, 0.5)
144
+ }
145
+ }
44
146
  }
45
147
  }}
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
- }
148
+ />
149
+ </LocalizationProvider>
150
+ );
151
+
152
+ return (
153
+ <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>
156
+ </Grid>
157
+ );
158
+ };
159
+
160
+ return (
161
+ <Box sx={{ mb: 1 }}>
162
+ <MainCard elevation={0}>
163
+ {/* Header Header */}
164
+ <Box sx={{ mb: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
165
+ <Tooltip
166
+ title={
167
+ <Box sx={{ p: 1 }}>
168
+ <Typography
169
+ variant="caption"
170
+ display="block"
171
+ sx={{
172
+ fontWeight: 800,
173
+ mb: 0.5,
174
+ color: theme.palette.primary.light, // Làm nổi bật tiêu đề hướng dẫn
175
+ textTransform: "uppercase",
176
+ letterSpacing: "0.5px"
177
+ }}
178
+ >
179
+ Hướng dẫn lọc dữ liệu
180
+ </Typography>
181
+ <Typography variant="caption" sx={{ lineHeight: 1.6, opacity: 0.9 }}>
182
+ 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
183
+ <b style={{ color: theme.palette.primary.light }}> Bộ lọc nâng cao </b>
184
+ để tìm kiếm kết quả chính xác nhất.
185
+ </Typography>
186
+ </Box>
187
+ }
188
+ placement="right"
189
+ arrow
190
+ TransitionComponent={Zoom}
191
+ slotProps={{
192
+ tooltip: {
193
+ sx: {
194
+ backgroundColor: alpha(theme.palette.grey[900], 0.95), // Màu nền tối sang trọng
195
+ color: "#fff",
196
+ borderRadius: "12px",
197
+ padding: "10px 14px",
198
+ boxShadow: `0 8px 24px ${alpha(theme.palette.common.black, 0.2)}`,
199
+ maxWidth: 250,
200
+ border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`
201
+ }
202
+ },
203
+ arrow: {
204
+ sx: { color: alpha(theme.palette.grey[900], 0.95) }
205
+ }
206
+ }}
207
+ >
208
+ <Box
209
+ sx={{
210
+ display: "flex",
211
+ alignItems: "center",
212
+ gap: 1.2,
213
+ cursor: "help",
214
+ p: "4px 8px",
215
+ ml: -1, // Cân bằng lề trái khi hover
216
+ borderRadius: "8px",
217
+ transition: "all 0.2s ease",
218
+ "&:hover": {
219
+ backgroundColor: alpha(theme.palette.primary.main, 0.05), // Hiệu ứng nền nhẹ khi hover
220
+ "& .search-icon": {
221
+ transform: "scale(1.1) rotate(-5deg)", // Icon động nhẹ
222
+ color: theme.palette.primary.dark
84
223
  }
224
+ }
225
+ }}
226
+ >
227
+ <SearchRoundedIcon
228
+ className="search-icon"
229
+ sx={{
230
+ fontSize: "1.35rem",
231
+ color: theme.palette.primary.main,
232
+ transition: "all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)",
233
+ opacity: 0.9
234
+ }}
235
+ />
85
236
 
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
- }
237
+ <Typography
238
+ sx={{
239
+ fontWeight: 700,
240
+ fontSize: "0.85rem",
241
+ letterSpacing: "0.2px",
242
+ color: "text.primary",
243
+ position: "relative",
244
+ "&::after": {
245
+ // Thanh trang trí nhỏ dưới chữ
246
+ content: '""',
247
+ position: "absolute",
248
+ bottom: -2,
249
+ left: 0,
250
+ width: "40%",
251
+ height: "2px",
252
+ backgroundColor: alpha(theme.palette.primary.main, 0.3),
253
+ borderRadius: "2px"
104
254
  }
255
+ }}
256
+ >
257
+ Bộ Lọc Dữ Liệu
258
+ </Typography>
259
+ </Box>
260
+ </Tooltip>
105
261
 
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
- }
262
+ <Box sx={{ display: "flex", gap: 1 }}>
263
+ {advancedFilters.length > 0 && (
264
+ <Tooltip
265
+ title={showAdvanced ? "" : "Mở rộng bộ lọc với các tiêu chí nâng cao"}
266
+ placement="top"
267
+ arrow
268
+ disableInteractive
269
+ >
270
+ <Box component="span">
271
+ {" "}
272
+ {/* Box span giúp Tooltip hoạt động ổn định kể cả khi Badge có vấn đề */}
273
+ <Badge
274
+ badgeContent={advancedFilters.length}
275
+ color="primary"
276
+ variant="dot"
277
+ invisible={showAdvanced}
278
+ sx={{ "& .MuiBadge-dot": { top: 4, right: 4 } }}
279
+ >
280
+ <StyledToggleButton
281
+ expanded={showAdvanced}
282
+ onClick={() => setShowAdvanced(!showAdvanced)}
283
+ startIcon={<TuneRoundedIcon />}
284
+ endIcon={showAdvanced ? <KeyboardArrowUpRoundedIcon /> : <KeyboardArrowDownRoundedIcon />}
285
+ >
286
+ {showAdvanced ? "Thu gọn" : "Nâng cao"}
287
+ </StyledToggleButton>
288
+ </Badge>
289
+ </Box>
290
+ </Tooltip>
291
+ )}
292
+ </Box>
293
+ </Box>
161
294
 
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} />
295
+ {/* 1. Basic Filters */}
296
+ <Grid container spacing={2.5}>
297
+ {basicFilters.map((f) => (
298
+ <Grid key={f.field.toString()} size={f.size || { xs: 12, md: 4, lg: 3 }}>
299
+ {f.type === "date-range" ? (
300
+ renderDateRange(f)
301
+ ) : (
302
+ <FilterElement {...f} name={f.field.toString()} setPage={setPage} size={elementSize} />
303
+ )}
304
+ </Grid>
305
+ ))}
306
+ </Grid>
307
+
308
+ {/* 2. Advanced Filters */}
309
+ <Collapse in={showAdvanced} timeout={400}>
310
+ <AdvancedSection>
311
+ <Grid container spacing={2.5}>
312
+ {advancedFilters.map((f) => (
313
+ <Grid key={f.field.toString()} size={f.size || { xs: 12, md: 4, lg: 3 }}>
314
+ {f.type === "date-range" ? (
315
+ renderDateRange(f)
316
+ ) : (
317
+ <FilterElement {...f} name={f.field.toString()} setPage={setPage} size={elementSize} />
318
+ )}
187
319
  </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>
320
+ ))}
321
+ </Grid>
322
+ </AdvancedSection>
323
+ </Collapse>
324
+ {filterButtons.length > 0 && (
325
+ <>
326
+ <Divider sx={{ my: 3, opacity: 0.6 }} />
327
+ <Box sx={{ display: "flex", justifyContent: "flex-end", gap: 2 }}>
328
+ {filterButtons.map((btn, idx) => (
329
+ <ActionButton
330
+ key={idx}
331
+ size={elementSize}
332
+ variant={btn.variant || "contained"}
333
+ color={btn.color || "primary"}
334
+ onClick={() => btn.onClick({ dataSearch })}
335
+ startIcon={btn.element}
336
+ disableElevation
337
+ sx={btn.sx}
338
+ >
339
+ {btn.title}
340
+ </ActionButton>
341
+ ))}
342
+ </Box>
343
+ </>
344
+ )}
345
+ </MainCard>
205
346
  </Box>
206
347
  );
207
348
  };
@@ -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.5",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"