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 {
|
|
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,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 {
|
|
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 = ({
|
|
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
|
-
|
|
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}-
|
|
119
|
+
() => JSON.parse(localStorage.getItem(`${tableName}-isFilterVisible`)) ?? false
|
|
108
120
|
);
|
|
109
121
|
|
|
110
122
|
useEffect(() => {
|
|
111
|
-
localStorage.setItem(`${tableName}-
|
|
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,
|
|
172
|
+
const innerDatePicker = (fieldKey, label, compareKey, compareType) => (
|
|
130
173
|
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="vi">
|
|
131
174
|
<DatePicker
|
|
132
|
-
label={
|
|
175
|
+
label={label}
|
|
133
176
|
format="DD/MM/YYYY"
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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 || "
|
|
333
|
-
color={btn.color || "
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
>
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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 */}
|