trithuc-mvc-react 3.5.5 → 3.5.7

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,16 +412,18 @@ 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
- <Divider sx={{ my: 3, opacity: 0.6 }} />
419
+ <Divider sx={{ my: "5px", opacity: 0.6 }} />
327
420
  <Box sx={{ display: "flex", justifyContent: "flex-end", gap: 2 }}>
328
421
  {filterButtons.map((btn, idx) => (
329
422
  <ActionButton
330
423
  key={idx}
331
424
  size={elementSize}
332
- variant={btn.variant || "contained"}
333
- color={btn.color || "primary"}
425
+ variant={btn.variant || "outlined"}
426
+ color={btn.color || "#1976d2 !important"}
334
427
  onClick={() => btn.onClick({ dataSearch })}
335
428
  startIcon={btn.element}
336
429
  disableElevation
@@ -135,18 +135,20 @@ export const TableRowRender = ({
135
135
  </IconButton>
136
136
  </Tooltip>
137
137
  )}
138
- {tableActionsOnTable.map(({ title, onClick, element, visible }) => (
139
- <Tooltip key={title} title={title} hidden={typeof visible === "function" && !visible(row)}>
140
- <IconButton
141
- size={downXl ? "small" : "medium"}
142
- onClick={() => {
143
- onClick(row);
144
- }}
145
- >
146
- {element}
147
- </IconButton>
148
- </Tooltip>
149
- ))}
138
+ {tableActionsOnTable.map(({ title, onClick, element, visible }) => {
139
+ // Kiểm tra điều kiện hiển thị đây
140
+ if (typeof visible === "function" && !visible(row)) {
141
+ return null;
142
+ }
143
+
144
+ return (
145
+ <Tooltip key={title} title={title}>
146
+ <IconButton size={downXl ? "small" : "medium"} onClick={() => onClick(row)}>
147
+ {element}
148
+ </IconButton>
149
+ </Tooltip>
150
+ );
151
+ })}
150
152
 
151
153
  {<MoreMenu actions={tableActionsOnMoreMenu} data={row} />}
152
154
  </TableCell>
@@ -148,15 +148,7 @@ export const TableRowRenderSM = ({
148
148
  )}
149
149
 
150
150
  {/* Các nút thao tác */}
151
- <Box
152
- mt={2}
153
- pt={1}
154
- borderTop="1px solid"
155
- borderColor="divider"
156
- display="flex"
157
- flexDirection="column"
158
- gap={1}
159
- >
151
+ <Box mt={2} pt={1} borderTop="1px solid" borderColor="divider" display="flex" flexDirection="column" gap={1}>
160
152
  {!disableCellThaoTac && (
161
153
  <>
162
154
  <Box display="grid" gridTemplateColumns={isMobile ? "repeat(3, 1fr)" : "auto"} gap={0.5}>
@@ -295,52 +287,56 @@ export const TableRowRenderSM = ({
295
287
  </Tooltip>
296
288
  )}
297
289
 
298
- {tableActionsOnTable.map(
299
- ({ title, onClick, element, visible = true }, idx) =>
300
- (typeof visible === "function" ? visible(row) : visible) && (
301
- <Tooltip key={idx} title={title} arrow placement="top">
302
- <span>
303
- <Button
304
- fullWidth
305
- size="small"
306
- variant="outlined"
307
- onClick={() => onClick(row)}
308
- startIcon={element}
309
- sx={{
310
- ...modernButtonStyle,
311
- minHeight: 36,
312
- paddingX: 1,
313
- gap: 0.5,
314
- whiteSpace: "normal",
290
+ {tableActionsOnTable.map(({ title, onClick, element, visible = true }, idx) => {
291
+ // Kiểm tra điều kiện hiển thị: nếu là function thì thực thi, nếu không thì lấy giá trị bool
292
+ const isVisible = typeof visible === "function" ? visible(row) : visible;
315
293
 
316
- "& .MuiButton-startIcon": {
317
- marginRight: 0.5,
318
- marginLeft: 0,
319
- alignSelf: "center"
320
- },
321
- "& .MuiButton-startIcon svg": {
322
- fontSize: 18
323
- }
294
+ if (!isVisible) return null;
295
+
296
+ return (
297
+ <Tooltip key={idx} title={title} arrow placement="top">
298
+ <span>
299
+ <Button
300
+ fullWidth
301
+ size="small"
302
+ variant="outlined"
303
+ onClick={() => onClick(row)}
304
+ startIcon={element}
305
+ sx={{
306
+ ...modernButtonStyle,
307
+ minHeight: 36,
308
+ paddingX: 1,
309
+ gap: 0.5,
310
+ whiteSpace: "normal",
311
+ "& .MuiButton-startIcon": {
312
+ marginRight: 0.5,
313
+ marginLeft: 0,
314
+ alignSelf: "center"
315
+ },
316
+ "& .MuiButton-startIcon svg": {
317
+ fontSize: 18
318
+ }
319
+ }}
320
+ >
321
+ <span
322
+ style={{
323
+ display: "-webkit-box",
324
+ WebkitLineClamp: 2,
325
+ WebkitBoxOrient: "vertical",
326
+ overflow: "hidden",
327
+ textOverflow: "ellipsis",
328
+ lineHeight: 1.25,
329
+ textAlign: "center"
324
330
  }}
325
331
  >
326
- <span
327
- style={{
328
- display: "-webkit-box",
329
- WebkitLineClamp: 2,
330
- WebkitBoxOrient: "vertical",
331
- overflow: "hidden",
332
- textOverflow: "ellipsis",
333
- lineHeight: 1.25,
334
- textAlign: "center"
335
- }}
336
- >
337
- {title === "xem chi tiết" ? "Xem" : title}
338
- </span>
339
- </Button>
340
- </span>
341
- </Tooltip>
342
- )
343
- )}
332
+ {/* Logic đổi tên title nếu cần */}
333
+ {title.toLowerCase() === "xem chi tiết" ? "Xem" : title}
334
+ </span>
335
+ </Button>
336
+ </span>
337
+ </Tooltip>
338
+ );
339
+ })}
344
340
  </Box>
345
341
 
346
342
  {/* More menu luôn nằm riêng 1 hàng */}
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.7",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"