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 { 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,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 { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
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 = ({ tableName, filters, filterButtons, elementSize = "small", setPage = () => {} }) => {
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
- // Đoạn code hoàn chỉnh cho useMemo trong FilterGod
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}-showAdvanced`)) ?? false
119
+ () => JSON.parse(localStorage.getItem(`${tableName}-isFilterVisible`)) ?? false
108
120
  );
109
121
 
110
122
  useEffect(() => {
111
- localStorage.setItem(`${tableName}-showAdvanced`, JSON.stringify(showAdvanced));
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, lbl) => (
172
+ const innerDatePicker = (fieldKey, label, compareKey, compareType) => (
130
173
  <LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="vi">
131
174
  <DatePicker
132
- label={lbl}
175
+ label={label}
133
176
  format="DD/MM/YYYY"
134
- value={dataSearch?.[fieldKey] ? moment(dataSearch?.[fieldKey]) : null}
135
- onChange={(val) => handleDateChange(val, fieldKey)}
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
- sx: {
141
- "& .MuiOutlinedInput-root": {
142
- borderRadius: "6px",
143
- backgroundColor: alpha(theme.palette.common.white, 0.5)
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 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.85rem",
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
- Bộ Lọc Dữ Liệu
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.5}>
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.5}>
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 }} />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trithuc-mvc-react",
3
- "version": "3.5.5",
3
+ "version": "3.5.6",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"