trithuc-mvc-react 1.0.0

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.
@@ -0,0 +1,240 @@
1
+ import {
2
+ Autocomplete,
3
+ Checkbox,
4
+ FormControl,
5
+ FormControlLabel,
6
+ FormLabel,
7
+ InputLabel,
8
+ MenuItem,
9
+ Radio,
10
+ RadioGroup,
11
+ Select,
12
+ Switch,
13
+ TextField
14
+ } from "@mui/material";
15
+ import { Controller, useFormContext } from "react-hook-form";
16
+ // Grid version 2
17
+ import PropTypes from "prop-types";
18
+ import { DatePicker } from "@mui/x-date-pickers";
19
+ import { useCallback, useEffect, } from "react";
20
+ import moment from "moment/moment";
21
+ import { DEFAULT_DATE_FORMAT } from "../../constants";
22
+
23
+
24
+ FormField.propTypes = {
25
+ datas: PropTypes.array,
26
+ loading: PropTypes.bool
27
+ };
28
+ function FormField({
29
+ type = "text",
30
+ control,
31
+ label,
32
+ name,
33
+ loading,
34
+ datas = [],
35
+ onChange,
36
+ keyLabel,
37
+ keyValue,
38
+ keyValueLabel,
39
+ childrenFields,
40
+ size,
41
+ required = false
42
+ }) {
43
+ const { setValue, register } = useFormContext();
44
+ const getValueObject = useCallback(
45
+ (value) => {
46
+ if (!value) return null;
47
+ const result = [...datas].find((item) => item[keyValue] == value);
48
+
49
+ return result;
50
+ },
51
+ [datas]
52
+ );
53
+
54
+ useEffect(() => {
55
+ if (keyValueLabel) {
56
+ register(keyValueLabel);
57
+ }
58
+ }, []);
59
+
60
+
61
+ switch (type) {
62
+ case "textarea":
63
+ return (
64
+ <Controller
65
+ name={name}
66
+ control={control}
67
+ render={({ field, fieldState: { error } }) => {
68
+ return (
69
+ <TextField
70
+ {...field}
71
+ multiline
72
+ label={label + (required ? "*" : "")}
73
+ error={Boolean(error)}
74
+ helperText={error?.message}
75
+ fullWidth
76
+ placeholder={label}
77
+ size={size}
78
+ />
79
+ );
80
+ }}
81
+ />
82
+ );
83
+
84
+ case "text":
85
+ case "number":
86
+ return (
87
+ <Controller
88
+ name={name}
89
+ control={control}
90
+ render={({ field, fieldState: { error } }) => {
91
+ return (
92
+ <TextField
93
+ {...field}
94
+ type={type}
95
+ label={label + (required ? "*" : "")}
96
+ error={Boolean(error)}
97
+ helperText={error?.message}
98
+ fullWidth
99
+ placeholder={label}
100
+ size={size}
101
+ />
102
+ );
103
+ }}
104
+ />
105
+ );
106
+
107
+ case "autocomplete": {
108
+ if (keyValueLabel) {
109
+ register(keyValueLabel);
110
+ }
111
+ return (
112
+ <Controller
113
+ name={name}
114
+ control={control}
115
+ render={({ field: { onChange: onFieldChange, value }, fieldState: { error } }) => {
116
+ return (
117
+ <Autocomplete
118
+ disablePortal
119
+ loading={loading}
120
+ size={size}
121
+ options={datas ?? []}
122
+ onChange={(event, newValue) => {
123
+ onFieldChange(newValue?.[keyValue]);
124
+ onChange(newValue);
125
+ if (keyValueLabel) {
126
+ setValue(keyValueLabel, newValue?.[keyLabel]);
127
+ }
128
+ }}
129
+ value={getValueObject(value)}
130
+ fullWidth
131
+ getOptionLabel={(option) => option[keyLabel]}
132
+ renderInput={(params) => <TextField {...params} label={label} error={Boolean(error)} helperText={error?.message} />}
133
+ />
134
+ );
135
+ }}
136
+ />
137
+ );
138
+ }
139
+
140
+ case "select":
141
+ return (
142
+ <Controller
143
+ name={name}
144
+ control={control}
145
+ render={({ field }) => (
146
+ <FormControl sx={{ minWidth: 160 }} fullWidth size={size}>
147
+ <InputLabel>{label}</InputLabel>
148
+ <Select {...field} label={label}>
149
+ {[...datas].map(({ label, value }) => (
150
+ <MenuItem key={value} value={value}>
151
+ {label}
152
+ </MenuItem>
153
+ ))}
154
+ </Select>
155
+ </FormControl>
156
+ )}
157
+ />
158
+ );
159
+ case "radios":
160
+ return (
161
+ <Controller
162
+ name={name}
163
+ control={control}
164
+ defaultValue={null}
165
+ render={({ field }) => {
166
+ return (
167
+ <FormControl fullWidth size={size}>
168
+ <FormLabel>{label}</FormLabel>
169
+ <RadioGroup row {...field}>
170
+ {[...datas].map(({ label, value }) => (
171
+ <FormControlLabel key={value} value={value} control={<Radio />} label={label} />
172
+ ))}
173
+ </RadioGroup>
174
+ </FormControl>
175
+ );
176
+ }}
177
+ />
178
+ );
179
+ case "checkbox":
180
+ return (
181
+ <>
182
+ <Controller
183
+ name={name}
184
+ control={control}
185
+ render={({ field }) => <FormControlLabel control={<Checkbox {...field} checked={field.value} />} label={label} />}
186
+ />
187
+
188
+ {/* {errors.terms && (
189
+ <FormHelperText sx={{ px: 2 }} error>
190
+ {errors.terms.message}
191
+ </FormHelperText>
192
+ )} */}
193
+ </>
194
+ );
195
+ case "switch":
196
+ return (
197
+ <Controller
198
+ name={name}
199
+ control={control}
200
+ defaultValue={true}
201
+ render={({ field }) => <FormControlLabel control={<Switch {...field} checked={field.value} />} label={label} />}
202
+ />
203
+ );
204
+ case "date":
205
+ return (
206
+ <Controller
207
+ name={name}
208
+ control={control}
209
+ render={({ field, fieldState: { error } }) => {
210
+ if (field.value && typeof field.value === "string") {
211
+ field.value = moment(field.value);
212
+ }
213
+ return (
214
+ <DatePicker
215
+ label={label + (required ? "*" : "")}
216
+ {...field}
217
+ format={DEFAULT_DATE_FORMAT}
218
+ slotProps={{
219
+ textField: {
220
+ fullWidth: true, error: Boolean(error),
221
+ helperText: error?.message
222
+ },
223
+ popper: {
224
+ disablePortal: false,
225
+ popperOptions: {
226
+ strategy: 'fixed'
227
+ },
228
+ }
229
+ }}
230
+
231
+
232
+ />
233
+ );
234
+ }}
235
+ />
236
+ );
237
+ }
238
+ }
239
+
240
+ export default FormField;
@@ -0,0 +1,26 @@
1
+ import { Checkbox, TableRow, TableCell, TableHead as MuiTableHead } from "@mui/material";
2
+ import { useDataTable, usePermission } from "./hooks";
3
+ export function TableHead({ numSelected, rowCount, onSelectAllClick, headLabel }) {
4
+ const { canEdit } = usePermission();
5
+ return (
6
+ <MuiTableHead sx={{ height: 56, visibility: numSelected > 0 ? "hidden" : "visible" }}>
7
+ <TableRow>
8
+ <TableCell padding="checkbox">
9
+ <Checkbox
10
+ indeterminate={numSelected > 0 && numSelected < rowCount}
11
+ checked={rowCount > 0 && numSelected === rowCount}
12
+ onChange={onSelectAllClick}
13
+ />
14
+ </TableCell>
15
+ <TableCell sx={{ fontWeight: 700 }}>STT</TableCell>
16
+ {headLabel.map((headCell) => (
17
+ <TableCell key={headCell.field} align={headCell.alignRight ? "right" : "left"} sx={{ fontWeight: 700 }}>
18
+ {headCell.label}
19
+ </TableCell>
20
+ ))}
21
+ {canEdit && <TableCell>Kích hoạt</TableCell>}
22
+ <TableCell>Thao tác</TableCell>
23
+ </TableRow>
24
+ </MuiTableHead>
25
+ );
26
+ }
@@ -0,0 +1,82 @@
1
+ import { Checkbox, IconButton, Switch, TableCell, TableRow, Tooltip, Toolbar } from "@mui/material";
2
+ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
3
+ import { EditOutlined } from "@mui/icons-material";
4
+ import { useDataTable, usePermission } from "./hooks";
5
+
6
+
7
+ // material
8
+ import { styled } from "@mui/material/styles";
9
+
10
+ export const TableRowRender = ({ index, row, selected, onSelect, onChangeStatus, onDelete, onEdit }) => {
11
+ const { selectedField, columns } = useDataTable();
12
+ const { canEdit, canDelete } = usePermission();
13
+ return (
14
+ <TableRow hover key={row[selectedField]} selected={selected}>
15
+ <TableCell padding="checkbox">
16
+ <Checkbox checked={selected} onChange={(event) => onSelect(event, row[selectedField])} />
17
+ </TableCell>
18
+ <TableCell>{index + 1}</TableCell>
19
+ {columns.map(
20
+ ({
21
+ field,
22
+ alignRight = false,
23
+ valueGetter = (row) => {
24
+ return row[field];
25
+ },
26
+ valueFormat = (e) => e
27
+ }) => {
28
+ return (
29
+ <TableCell key={`${row[selectedField]}-${field}`} align={alignRight ? "right" : "left"}>
30
+ {valueFormat(valueGetter(row))}
31
+ </TableCell>
32
+ );
33
+ }
34
+ )}
35
+ {canEdit && (
36
+ <TableCell>
37
+ <Switch
38
+ checked={row.Status}
39
+ onChange={() => {
40
+ onChangeStatus(row[selectedField]);
41
+ }}
42
+ inputProps={{ "aria-label": "controlled" }}
43
+ />
44
+ </TableCell>
45
+ )}
46
+
47
+ <TableCell>
48
+ {canEdit && (
49
+ <Tooltip title="Chỉnh sửa">
50
+ <IconButton onClick={() => onEdit(row)}>
51
+ <EditOutlined color="primary" />
52
+ </IconButton>
53
+ </Tooltip>
54
+ )}
55
+
56
+ {canDelete && (
57
+ <Tooltip title="Xóa">
58
+ <IconButton
59
+ onClick={() => {
60
+ onDelete(row[selectedField]);
61
+ }}
62
+ >
63
+ <DeleteOutlineIcon color="error" />
64
+ </IconButton>
65
+ </Tooltip>
66
+ )}
67
+ </TableCell>
68
+ </TableRow>
69
+ );
70
+ };
71
+
72
+ // ----------------------------------------------------------------------
73
+
74
+ export const RootStyle = styled(Toolbar)(({ theme }) => ({
75
+ display: "flex",
76
+ justifyContent: "space-between",
77
+ padding: theme.spacing(0, 1, 0, 1),
78
+ height: 56,
79
+ minHeight: 50
80
+ }));
81
+
82
+
@@ -0,0 +1,57 @@
1
+ import { Checkbox, IconButton, Tooltip, Typography, Box } from "@mui/material";
2
+ import { Delete } from "@mui/icons-material";
3
+ import { usePermission } from "./hooks";
4
+ import PropTypes from "prop-types";
5
+ import { useTheme } from "@mui/material/styles";
6
+ import { RootStyle } from "./TableRowRender";
7
+
8
+ // ----------------------------------------------------------------------
9
+ TableToolbar.propTypes = {
10
+ numSelected: PropTypes.number,
11
+ filterName: PropTypes.string,
12
+ onFilterName: PropTypes.func
13
+ };
14
+
15
+ export const TableToolbar = ({ numSelected, onSelectAllClick, rowCount, onDeleteMultiple }) => {
16
+ const theme = useTheme();
17
+ const isLight = theme.palette.mode === "light";
18
+ const { canDeleteMulti } = usePermission();
19
+
20
+ return (
21
+ numSelected > 0 && (
22
+ <RootStyle
23
+ variant="dense"
24
+ disableGutters={true}
25
+ padding="checkbox"
26
+ sx={{
27
+ position: "absolute",
28
+ paddingLeft: 1,
29
+ width: "100%",
30
+ ...(numSelected > 0 && {
31
+ color: isLight ? "primary.main" : "text.primary",
32
+ bgcolor: isLight ? "primary.lighter" : "primary.dark"
33
+ })
34
+ }}
35
+ >
36
+ <Box padding="checkbox" sx={{ display: "flex", alignItems: "center" }}>
37
+ <Checkbox
38
+ indeterminate={numSelected > 0 && numSelected < rowCount}
39
+ checked={rowCount > 0 && numSelected === rowCount}
40
+ onChange={onSelectAllClick}
41
+ />
42
+ <Typography component="div" variant="subtitle1" style={{ p: 4 }}>
43
+ đã chọn {numSelected} mục
44
+ </Typography>
45
+ </Box>
46
+
47
+ {canDeleteMulti && (
48
+ <Tooltip title="Xóa tất cả" onClick={onDeleteMultiple}>
49
+ <IconButton>
50
+ <Delete color="primary" />
51
+ </IconButton>
52
+ </Tooltip>
53
+ )}
54
+ </RootStyle>
55
+ )
56
+ );
57
+ };
@@ -0,0 +1,4 @@
1
+ import { createContext } from "react";
2
+
3
+ export const DataTableContext = createContext({ tableName: null });
4
+ export const PermissionContext = createContext(null);
@@ -0,0 +1,19 @@
1
+ import { useContext, useMemo } from "react";
2
+ import { DataTableContext, PermissionContext } from "./context";
3
+
4
+ export function useDataTable() {
5
+ return useContext(DataTableContext);
6
+ }
7
+ export function usePermission() {
8
+ const { Permission, setPermission } = useContext(PermissionContext);
9
+ const { canEdit, canDelete, canDeleteMulti, canSave, canCreate } = useMemo(() => {
10
+ const canEdit = !Permission || Permission.Edit;
11
+ const canDelete = !Permission || Permission.Delete;
12
+ const canDeleteMulti = !Permission || Permission.DeleteMulti;
13
+ const canSave = !Permission || Permission.Save;
14
+ const canCreate = !Permission || Permission.Create;
15
+ return { canEdit, canDelete, canDeleteMulti, canSave, canCreate };
16
+ }, [Permission]);
17
+
18
+ return { Permission, setPermission, canEdit, canDelete, canDeleteMulti, canSave, canCreate };
19
+ }
@@ -0,0 +1,107 @@
1
+ import { Button, IconButton, Stack, Tooltip, Typography } from "@mui/material";
2
+
3
+ import { useMemo, useState } from "react";
4
+
5
+ import { DataTableContext, PermissionContext } from "./context";
6
+ import PropTypes from "prop-types";
7
+ import DataTable from "./DataTable";
8
+ import { Add, Refresh } from "@mui/icons-material";
9
+
10
+ import { FormProvider, useForm } from "react-hook-form";
11
+ import EditorDialog from "./EditorDialog";
12
+
13
+ import ExportExcelButton from "./ExportExcelButton";
14
+ import { FilterGod } from "./FilterGod";
15
+
16
+ DataManagement.propTypes = {
17
+ columns: PropTypes.array,
18
+ title: PropTypes.string,
19
+ tableName: PropTypes.string,
20
+ selectedField: PropTypes.string,
21
+ filter: PropTypes.arrayOf(
22
+ PropTypes.shape({
23
+ field: PropTypes.string,
24
+ label: PropTypes.string,
25
+ placeHolder: PropTypes.string,
26
+ type: PropTypes.oneOf(["text", "number", "date", "autocomplete", "checkbox", "radio", "switch"]),
27
+ onChangeAfter: PropTypes.func,
28
+ filters: PropTypes.array
29
+ })
30
+ )
31
+ };
32
+
33
+ function DataManagement({ columns = [], title, tableName, selectedField = "Id", filters, editorFields, validationSchema = {} }) {
34
+ const [openEditorDialog, setOpenEditorDialog] = useState(false);
35
+ const [selectedEditItem, setSelectedEditItem] = useState(null);
36
+ const [dataSearch, setDataSearch] = useState({});
37
+ const [Permission, setPermission] = useState(null);
38
+
39
+ const values = useMemo(() => {
40
+ return {
41
+ tableName,
42
+ selectedField,
43
+ columns,
44
+ selectedEditItem,
45
+ setSelectedEditItem,
46
+ setOpenEditorDialog,
47
+ dataSearch,
48
+ setDataSearch,
49
+ validationSchema
50
+ };
51
+ }, [tableName, selectedField, columns, selectedEditItem, dataSearch, setDataSearch, validationSchema]);
52
+ const permissionValues = useMemo(() => {
53
+ return {
54
+ Permission,
55
+ setPermission
56
+ };
57
+ }, [Permission, setPermission]);
58
+ const methods = useForm({ defaultValues: {} });
59
+ const { reset, setValue } = methods;
60
+
61
+ return (
62
+ <>
63
+ <DataTableContext.Provider value={values}>
64
+ <PermissionContext.Provider value={permissionValues}>
65
+ <FormProvider {...methods}>
66
+ <Stack direction="row" justifyContent={"space-between"} sx={{ px: 2, pt: 2 }}>
67
+ <Typography variant="h4">{title}</Typography>
68
+ <Stack direction="row" spacing={1}>
69
+ <Tooltip title="Làm mới">
70
+ <IconButton
71
+ variant="outlined"
72
+ color="primary"
73
+ onClick={() => {
74
+ setDataSearch({});
75
+ reset();
76
+ setValue("Search");
77
+ }}
78
+ >
79
+ <Refresh />
80
+ </IconButton>
81
+ </Tooltip>
82
+
83
+ <ExportExcelButton tableName={tableName} />
84
+ {(!Permission || Permission.Create) && (
85
+ <Button
86
+ variant="contained"
87
+ startIcon={<Add />}
88
+ onClick={() => {
89
+ setOpenEditorDialog(true);
90
+ setSelectedEditItem(null);
91
+ }}
92
+ >
93
+ Thêm
94
+ </Button>
95
+ )}
96
+ </Stack>
97
+ </Stack>
98
+ <FilterGod filters={filters} />
99
+ <DataTable />
100
+ </FormProvider>
101
+ <EditorDialog open={openEditorDialog} onClose={() => setOpenEditorDialog(false)} defaultValues={selectedEditItem} fields={editorFields} />
102
+ </PermissionContext.Provider>
103
+ </DataTableContext.Provider>
104
+ </>
105
+ );
106
+ }
107
+ export default DataManagement;
@@ -0,0 +1,143 @@
1
+ import { DateField } from "@mui/x-date-pickers";
2
+ import { DEFAULT_DATE_FORMAT } from "../../constants";
3
+ import { Popover, Popper, Stack, Typography } from "@mui/material";
4
+ import StaticDateRangePicker from "./StaticDateRangePicker";
5
+ import moment from "moment";
6
+ import { memo, useEffect, useRef, useState } from "react";
7
+ import * as dateFns from "date-fns";
8
+ import ClickAwayListener from "@mui/base/ClickAwayListener";
9
+
10
+ const DateRangePicker = ({ onChange = () => {}, value }) => {
11
+ const [anchorEl, setAnchorEl] = useState(null);
12
+
13
+ const [dateRange, setDateRange] = useState({
14
+ startDate: value?.[0],
15
+ endDate: value?.[1]
16
+ });
17
+
18
+ const handleFocus = (event) => {
19
+ setAnchorEl(containerRef.current);
20
+ };
21
+ const handleClose = () => {
22
+ setAnchorEl(null);
23
+ };
24
+ const handleClickAway = () => {
25
+ setAnchorEl(null);
26
+ };
27
+
28
+ const open = Boolean(anchorEl);
29
+ const containerRef = useRef(null);
30
+
31
+ useEffect(() => {
32
+ if (dateFns.isBefore(dateRange.startDate, dateRange.endDate) || dateFns.isSameDay(dateRange.startDate, dateRange.endDate)) {
33
+ onChange([dateRange.startDate, dateRange.endDate]);
34
+ }
35
+ }, [dateRange]);
36
+
37
+ useEffect(() => {
38
+ if (!value) return;
39
+ if (dateFns.isSameDay(dateRange.startDate, value[0]) && dateFns.isSameDay(dateRange.endDate, value[1])) {
40
+ return;
41
+ }
42
+ setDateRange({
43
+ startDate: value?.[0],
44
+ endDate: value?.[1]
45
+ });
46
+ }, [value]);
47
+
48
+ return (
49
+ <>
50
+ <ClickAwayListener onClickAway={handleClickAway}>
51
+ <Stack ref={containerRef} direction={"row"} alignContent={"center"} spacing={1}>
52
+ <DateField
53
+ sx={{ flex: 1 }}
54
+ format={DEFAULT_DATE_FORMAT}
55
+ onFocus={handleFocus}
56
+ label="From"
57
+ onChange={(value) => {
58
+ setDateRange((d) => ({
59
+ ...d,
60
+ startDate: value.toDate()
61
+ }));
62
+ }}
63
+ value={dateRange.startDate && moment(dateRange.startDate)}
64
+ size="small"
65
+ maxDate={dateRange.endDate ?? moment(dateRange.endDate)}
66
+ />
67
+ <Typography variant="body1" sx={{ height: "24px", alignSelf: "center" }}>
68
+
69
+ </Typography>
70
+ <DateField
71
+ sx={{ flex: 1 }}
72
+ format={DEFAULT_DATE_FORMAT}
73
+ label="To"
74
+ onFocus={handleFocus}
75
+ onChange={(value) => {
76
+ setDateRange((d) => ({
77
+ ...d,
78
+ endDate: value.toDate()
79
+ }));
80
+
81
+ }}
82
+ value={dateRange.endDate && moment(dateRange.endDate)}
83
+ size="small"
84
+ minDate={dateRange.startDate ?? moment(dateRange.startDate)}
85
+ />
86
+
87
+ <Popper
88
+ sx={{
89
+ zIndex: 1036
90
+ }}
91
+ keepMounted
92
+ placement="bottom-start"
93
+ open={open}
94
+ anchorEl={anchorEl}
95
+ onClose={handleClose}
96
+ disablePortal={false}
97
+ modifiers={[
98
+ {
99
+ name: "flip",
100
+ enabled: true,
101
+ options: {
102
+ altBoundary: true,
103
+ rootBoundary: "viewport",
104
+ padding: 8
105
+ }
106
+ },
107
+ {
108
+ name: "preventOverflow",
109
+ enabled: true,
110
+ options: {
111
+ altAxis: true,
112
+ altBoundary: true,
113
+ tether: true,
114
+ rootBoundary: "document",
115
+ padding: 8
116
+ }
117
+ }
118
+ ]}
119
+ >
120
+ <StaticDateRangePicker
121
+ value={dateRange}
122
+ onUpdate={(value) => {
123
+ setDateRange(value);
124
+ }}
125
+ />
126
+ </Popper>
127
+ </Stack>
128
+ </ClickAwayListener>
129
+ </>
130
+ );
131
+ };
132
+ const DateRangePickerMemo = memo(DateRangePicker, (prevProps, nextProps) => {
133
+ try {
134
+ if (dateFns.isSameDay(prevProps.value[0], nextProps.value[0]) && dateFns.isSameDay(prevProps.value[1], nextProps.value[1])) {
135
+ return true; // props are equal
136
+ }
137
+ } catch (error) {
138
+ console.log(error);
139
+ }
140
+
141
+ return false; // props are not equal -> update the component
142
+ });
143
+ export default DateRangePickerMemo;