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.
package/api/index.js ADDED
@@ -0,0 +1,57 @@
1
+ import axios from "axios";
2
+ const api = axios.create({
3
+ baseURL: "/",
4
+ transitional: {
5
+ silentJSONParsing: false
6
+ },
7
+ responseType: "json"
8
+ });
9
+ export default api;
10
+
11
+ export const getDatasFromTable = async ({ tableName, page, pageSize, data }) => {
12
+ const res = await api.get(`/Admin/${tableName}/LoadData`, {
13
+ params: {
14
+ json: JSON.stringify(data),
15
+ page,
16
+ pageSize
17
+ }
18
+ });
19
+ return res.data;
20
+ };
21
+ export const getDataFromTable = async ({ tableName, id }) => {
22
+ const res = await api.get(`/Admin/${tableName}/GetDetail`, {
23
+ params: {
24
+ id
25
+ }
26
+ });
27
+ return res.data;
28
+ };
29
+ export const deleteDataFromTable = async ({ tableName, id }) => {
30
+ const res = await api.post(`/Admin/${tableName}/Delete`, {
31
+ id
32
+ });
33
+ return res.data;
34
+ };
35
+ export const deleteMultipleDataFromTable = async ({ tableName, ids }) => {
36
+ const res = await api.post(`/Admin/${tableName}/DeleteMulti`, {
37
+ ids
38
+ });
39
+ return res.data;
40
+ };
41
+ export const saveDataToTable = async ({ tableName, data }) => {
42
+ const res = await api.post(`/Admin/${tableName}/SaveData`, {
43
+ json: JSON.stringify({ ...data })
44
+ });
45
+ return res.data;
46
+ };
47
+ export const changeStatusDataToTable = async ({ tableName, id }) => {
48
+ const res = await api.post(`/Admin/${tableName}/ChangeStatus`, {
49
+ id
50
+ });
51
+ return res.data;
52
+ };
53
+
54
+ export const exportExcel = async ({ tableName }) => {
55
+ const res = await api.get(`/Admin/${tableName}/ExportData`);
56
+ return res.data;
57
+ };
@@ -0,0 +1,186 @@
1
+ import { Table, TableBody, TableContainer } from "@mui/material";
2
+ import TablePaginationCustom from "../table/TablePagination";
3
+ import { useMemo, useState } from "react";
4
+
5
+ import { TableHead } from "./TableHead";
6
+ import { useMutation, useQuery, useQueryClient } from "react-query";
7
+ import { changeStatusDataToTable, deleteDataFromTable, deleteMultipleDataFromTable, getDatasFromTable } from "../../api";
8
+ import TableRowsLoader from "../table/TableRowsLoader";
9
+ import { toast } from "react-toastify";
10
+ import { useConfirm } from "material-ui-confirm";
11
+
12
+ import { TableRowRender } from "./TableRowRender";
13
+ import { TableToolbar } from "./TableToolbar";
14
+ import { useDataTable, usePermission } from "./hooks";
15
+
16
+ const DataTable = () => {
17
+ const { tableName, selectedField, columns, dataSearch, setOpenEditorDialog, setSelectedEditItem } = useDataTable();
18
+ const { setPermission, Permission } = usePermission();
19
+ const queryClient = useQueryClient();
20
+ const confirm = useConfirm();
21
+ const [selected, setSelected] = useState([]);
22
+ const [page, setPage] = useState(0);
23
+ const [rowsPerPage, setRowsPerPage] = useState(5);
24
+ const { data, isLoading } = useQuery({
25
+ queryKey: [tableName, page, rowsPerPage, dataSearch],
26
+ queryFn: () =>
27
+ getDatasFromTable({
28
+ tableName: tableName,
29
+ page: page + 1,
30
+ pageSize: rowsPerPage,
31
+ data: dataSearch
32
+ }),
33
+ keepPreviousData: true,
34
+ onSuccess: ({ PermissionModel, status }) => {
35
+ if (!Permission && status) {
36
+ setPermission(PermissionModel);
37
+ }
38
+ }
39
+ });
40
+ const changeStatusMutation = useMutation(changeStatusDataToTable, {
41
+ onSuccess: () => {
42
+ toast.success("Thay đổi trạng thái thành công !");
43
+ queryClient.invalidateQueries({ queryKey: [tableName] });
44
+ },
45
+ onError: () => {
46
+ toast.error(" Có lỗi xảy ra !");
47
+ }
48
+ });
49
+ const deleteMutation = useMutation(deleteDataFromTable, {
50
+ onSuccess: ({ status }) => {
51
+ if (status) {
52
+ toast.success("Xóa thành công !");
53
+ } else {
54
+ toast.error(" Có lỗi xảy ra !");
55
+ }
56
+
57
+ queryClient.invalidateQueries({ queryKey: [tableName] });
58
+ },
59
+ onError: () => {
60
+ toast.error(" Có lỗi xảy ra !");
61
+ }
62
+ });
63
+ const deleteMultipleMutation = useMutation(deleteMultipleDataFromTable, {
64
+ onSuccess: () => {
65
+ toast.success("Xóa thành công !");
66
+ setSelected([]);
67
+ queryClient.invalidateQueries({ queryKey: [tableName] });
68
+ },
69
+ onError: () => {
70
+ toast.error(" Có lỗi xảy ra !");
71
+ }
72
+ });
73
+
74
+ const handleDelete = (id) => {
75
+ confirm({ description: "Bạn có chắc chắn muốn xóa bản ghi này không?", title: "Xác nhận" })
76
+ .then(() => {
77
+ deleteMutation.mutate({
78
+ id,
79
+ tableName
80
+ });
81
+ })
82
+ .catch(() => {});
83
+ };
84
+ const handleChangeStatus = (Id) => {
85
+ changeStatusMutation.mutate({
86
+ tableName,
87
+ id: Id
88
+ });
89
+ };
90
+ const handlEdit = (item) => {
91
+ setOpenEditorDialog(true);
92
+ setSelectedEditItem(item);
93
+ };
94
+ const { rows, total } = useMemo(() => {
95
+ let rows = data?.data ?? [];
96
+ let total = data?.total ?? 0;
97
+ return {
98
+ rows: rows,
99
+ total
100
+ };
101
+ }, [data]);
102
+
103
+ const handleChangePage = (event, newPage) => {
104
+ setPage(newPage);
105
+ };
106
+ const isSelected = (Id) => selected.indexOf(Id) !== -1;
107
+ const handleSelect = (event, Id) => {
108
+ const selectedIndex = selected.indexOf(Id);
109
+ let newSelected = [];
110
+
111
+ if (selectedIndex === -1) {
112
+ newSelected = newSelected.concat(selected, Id);
113
+ } else if (selectedIndex === 0) {
114
+ newSelected = newSelected.concat(selected.slice(1));
115
+ } else if (selectedIndex === selected.length - 1) {
116
+ newSelected = newSelected.concat(selected.slice(0, -1));
117
+ } else if (selectedIndex > 0) {
118
+ newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1));
119
+ }
120
+
121
+ setSelected(newSelected);
122
+ };
123
+ const handleSelectAllClick = (event) => {
124
+ if (event.target.checked) {
125
+ const newSelected = rows.map((n) => n[selectedField]);
126
+ setSelected(newSelected);
127
+ return;
128
+ }
129
+ setSelected([]);
130
+ };
131
+ const handleChangeRowsPerPage = (event) => {
132
+ setRowsPerPage(parseInt(event.target.value, 10));
133
+ setPage(0);
134
+ };
135
+ const handleDeleteMultiple = () => {
136
+ confirm({ description: `Bạn có chắc chắn muốn xóa ${selected?.length} bản ghi này không?`, title: "Xác nhận" })
137
+ .then(() => {
138
+ deleteMultipleMutation.mutate({
139
+ tableName,
140
+ ids: selected
141
+ });
142
+ })
143
+ .catch(() => {});
144
+ };
145
+ return (
146
+ <>
147
+ <TableContainer sx={{ position: "relative" }}>
148
+ <TableToolbar
149
+ onSelectAllClick={handleSelectAllClick}
150
+ numSelected={selected?.length}
151
+ rowCount={rows.length}
152
+ onDeleteMultiple={handleDeleteMultiple}
153
+ />
154
+ <Table className="border">
155
+ <TableHead headLabel={columns} onSelectAllClick={handleSelectAllClick} numSelected={selected?.length} rowCount={rows.length} />
156
+ {isLoading ? (
157
+ <TableRowsLoader rowsNum={5} colsNum={columns.length + 4} />
158
+ ) : (
159
+ <TableBody>
160
+ {[...rows].map((row, index) => (
161
+ <TableRowRender
162
+ key={row.Id}
163
+ index={index}
164
+ row={row}
165
+ selected={isSelected(row[selectedField])}
166
+ onSelect={handleSelect}
167
+ onEdit={handlEdit}
168
+ onChangeStatus={handleChangeStatus}
169
+ onDelete={handleDelete}
170
+ />
171
+ ))}
172
+ </TableBody>
173
+ )}
174
+ </Table>
175
+ </TableContainer>
176
+ <TablePaginationCustom
177
+ count={total}
178
+ rowsPerPage={rowsPerPage}
179
+ page={page}
180
+ onPageChange={handleChangePage}
181
+ onRowsPerPageChange={handleChangeRowsPerPage}
182
+ />
183
+ </>
184
+ );
185
+ };
186
+ export default DataTable;
@@ -0,0 +1,64 @@
1
+ import Button from "@mui/material/Button";
2
+
3
+ import Dialog from "@mui/material/Dialog";
4
+ import DialogActions from "@mui/material/DialogActions";
5
+ import DialogContent from "@mui/material/DialogContent";
6
+ import DialogTitle from "@mui/material/DialogTitle";
7
+ import IconButton from "@mui/material/IconButton";
8
+ import CloseIcon from "@mui/icons-material/Close";
9
+
10
+ import { useEffect, useRef, useState } from "react";
11
+ import PropTypes from "prop-types";
12
+
13
+ import EditorForm from "./EditorForm";
14
+ import { usePermission } from "./hooks";
15
+ EditorDialog.propTypes = {
16
+ open: PropTypes.bool,
17
+ onClose: PropTypes.func,
18
+ defaultValues: PropTypes.objectOf({})
19
+ };
20
+ function EditorDialog({ open, onClose = () => {}, defaultValues = {}, fields = [] }) {
21
+ const [isDisableBtnSave, setIsDisableBtnSave] = useState(false);
22
+ const { canSave } = usePermission();
23
+
24
+ useEffect(() => {}, [defaultValues]);
25
+
26
+ const handleSave = async () => {
27
+ submitRef.current.click();
28
+ setIsDisableBtnSave(true);
29
+
30
+ setIsDisableBtnSave(false);
31
+ };
32
+ const submitRef = useRef();
33
+ return (
34
+ <Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth={true} scroll={'body'}>
35
+ <DialogTitle>
36
+ {defaultValues?.Id ? "Cập nhật" : "Thêm mới"}
37
+ <IconButton
38
+ aria-label="close"
39
+ onClick={onClose}
40
+ sx={{
41
+ position: "absolute",
42
+ right: 8,
43
+ top: 8,
44
+ color: (theme) => theme.palette.grey[500]
45
+ }}
46
+ >
47
+ <CloseIcon />
48
+ </IconButton>
49
+ </DialogTitle>
50
+ <DialogContent dividers={true}>
51
+ <EditorForm fields={fields} submitRef={submitRef} />
52
+ </DialogContent>
53
+ <DialogActions>
54
+ <Button onClick={onClose}>Đóng</Button>
55
+ {canSave && (
56
+ <Button onClick={handleSave} disabled={isDisableBtnSave}>
57
+ Lưu
58
+ </Button>
59
+ )}
60
+ </DialogActions>
61
+ </Dialog>
62
+ );
63
+ }
64
+ export default EditorDialog;
@@ -0,0 +1,147 @@
1
+ import { Box } from "@mui/material";
2
+ import { FormProvider, useForm } from "react-hook-form";
3
+ import Grid from "@mui/material/Unstable_Grid2"; // Grid version 2
4
+ import PropTypes from "prop-types";
5
+
6
+ import { useEffect } from "react";
7
+ import moment from "moment/moment";
8
+
9
+ import { useMutation, useQueryClient } from "react-query";
10
+ import { saveDataToTable } from "../../api";
11
+ import { toast } from "react-toastify";
12
+ import { useDataTable } from "./hooks";
13
+ import FormField from "./FormField";
14
+ import { yupResolver } from "@hookform/resolvers/yup";
15
+ EditorForm.propTypes = {
16
+ fields: PropTypes.array
17
+ };
18
+ function EditorForm({ fields, elementSize = "medium", submitRef }) {
19
+ const queryClient = useQueryClient();
20
+ const { tableName, selectedEditItem, setOpenEditorDialog, validationSchema } = useDataTable();
21
+
22
+ const methods = useForm({ defaultValues: {}, resolver: yupResolver(validationSchema) });
23
+
24
+ useEffect(() => {
25
+ if (selectedEditItem) {
26
+ methods.setValue("Id", selectedEditItem.Id);
27
+
28
+ fields.forEach(({ field, onChange, type, keyValue, keyValueLabel, defaultValue }) => {
29
+ if (type == "autocomplete") {
30
+ onChange?.({
31
+ [keyValue]: selectedEditItem[field]
32
+ });
33
+
34
+ methods.setValue(field, selectedEditItem ? selectedEditItem[field] : defaultValue );
35
+ methods.setValue(keyValueLabel, selectedEditItem[keyValueLabel]);
36
+ }else if(type === 'date'){
37
+ methods.setValue(field, selectedEditItem[field]);
38
+ } else {
39
+ methods.setValue(field, selectedEditItem[field]);
40
+ }
41
+ });
42
+
43
+ }else{
44
+ fields.forEach(({ field, defaultValue }) => {
45
+ methods.setValue(field, defaultValue );
46
+ });
47
+ }
48
+ }, [selectedEditItem]);
49
+
50
+ const saveMutation = useMutation(saveDataToTable, {
51
+ onSuccess: ({ status = false }, { data: { Id } }) => {
52
+ if (status) {
53
+ toast.success(Id == 0 ? "Thêm thành công!" : "Cập nhật thành công!");
54
+ queryClient.invalidateQueries(tableName);
55
+ setOpenEditorDialog(false);
56
+ } else {
57
+ toast.error(" Có lỗi xảy ra !");
58
+ }
59
+ },
60
+ onError: () => {
61
+ toast.error(" Có lỗi xảy ra !");
62
+ }
63
+ });
64
+ const onSubmit = (data) => {
65
+
66
+ fields
67
+ .filter(({ type }) => type === "date")
68
+ .forEach(({ field }) => {
69
+ if (data[field]) {
70
+ data[field] = moment(data[field]).toDate();
71
+ }
72
+ });
73
+ fields
74
+ .filter(({ type }) => type === "autocomplete")
75
+ .forEach(({ field, datas, keyValueLabel, keyValue, keyLabel }) => {
76
+ if (data[field] && !data[keyValueLabel] && keyValueLabel) {
77
+ data[keyValueLabel] = datas.find((item) => item[keyValue] == data[field])?.[keyLabel];
78
+ }
79
+ });
80
+
81
+ saveMutation.mutate({
82
+ tableName,
83
+ data: {
84
+ Id: 0,
85
+ ...data
86
+ }
87
+ });
88
+ };
89
+ return (
90
+ <FormProvider {...methods}>
91
+ <Box component={"form"} sx={{ pt: 2 }} onSubmit={methods.handleSubmit(onSubmit)}>
92
+ <Grid container spacing={2}>
93
+ {fields.map(
94
+ ({
95
+ field,
96
+ type = "text",
97
+ label,
98
+ childrenFields,
99
+ datas,
100
+ loading = false,
101
+ onChange = () => { },
102
+ keyLabel,
103
+ keyValue,
104
+ keyValueLabel,
105
+ required
106
+ }) => {
107
+ let sizes = {
108
+ xs: 12,
109
+ sm: 6,
110
+ md: 4
111
+ };
112
+ if (type == 'textarea') {
113
+ sizes = {
114
+ xs: 12,
115
+ sm: 12,
116
+ md: 12
117
+ };
118
+ }
119
+ return (
120
+ <Grid {...sizes} key={field}>
121
+ <FormField
122
+ type={type}
123
+ label={label}
124
+ control={methods.control}
125
+ name={field}
126
+ loading={loading}
127
+ onChange={onChange}
128
+ keyLabel={keyLabel}
129
+ keyValueLabel={keyValueLabel}
130
+ datas={datas}
131
+ childrenFields={childrenFields}
132
+ keyValue={keyValue}
133
+ size={elementSize}
134
+ required={required}
135
+ />
136
+ </Grid>
137
+ );
138
+ }
139
+ )}
140
+ </Grid>
141
+ <button ref={submitRef} type="submit" style={{ display: "none" }} />
142
+ </Box>
143
+ </FormProvider>
144
+ );
145
+ }
146
+
147
+ export default EditorForm;
@@ -0,0 +1,27 @@
1
+ import { toast } from "react-toastify";
2
+ import { exportExcel } from "../../api";
3
+ import { Button } from "@mui/material";
4
+ import { Download } from "@mui/icons-material";
5
+
6
+ const ExportExcelButton = ({ tableName })=>{
7
+ const handleExportExcel = async (tableName) => {
8
+ const data = await exportExcel({ tableName });
9
+ if (data.status) {
10
+ window.open(data.url, "_blank").focus();
11
+ } else {
12
+ toast.error("Xuất file thất bại!");
13
+ }
14
+ };
15
+ return (
16
+ <Button
17
+ variant="outlined"
18
+ startIcon={<Download />}
19
+ onClick={() => {
20
+ handleExportExcel(tableName);
21
+ }}
22
+ >
23
+ Excel
24
+ </Button>
25
+ );
26
+ }
27
+ export default ExportExcelButton;
@@ -0,0 +1,102 @@
1
+ import {
2
+ Autocomplete, FormControl, InputAdornment,
3
+ InputLabel,
4
+ MenuItem,
5
+ OutlinedInput,
6
+ Select, TextField
7
+ } from "@mui/material";
8
+ import { useCallback } from "react";
9
+ import { SearchOutlined } from "@mui/icons-material";
10
+ import { useController, useFormContext } from "react-hook-form";
11
+ import { debounce } from "lodash";
12
+ import { useDataTable } from "./hooks";
13
+
14
+ export function FilterElement({ name, type, label, keyValue, keyLabel, childrenFields, datas, loading = false, onChange = () => { } }) {
15
+ const { control, setValue } = useFormContext();
16
+
17
+ const {
18
+ field: { value, onChange: onFieldChange, ...rest }
19
+ } = useController({ control, name });
20
+
21
+ const { dataSearch, setDataSearch } = useDataTable();
22
+
23
+ const handleFilterChangeDebounce = useCallback(
24
+ debounce((name, value) => {
25
+ setDataSearch({ ...dataSearch, [name]: value });
26
+ }, 500),
27
+ [dataSearch]
28
+ );
29
+
30
+ switch (type) {
31
+ case "search":
32
+ return (
33
+ <OutlinedInput
34
+ {...rest}
35
+ fullWidth
36
+ value={value ?? ""}
37
+ onChange={(e) => {
38
+ onFieldChange(e);
39
+ handleFilterChangeDebounce(name, e.target.value);
40
+ }}
41
+ size="small"
42
+ placeholder={label}
43
+ startAdornment={<InputAdornment position="start">
44
+ <SearchOutlined />
45
+ </InputAdornment>} />
46
+ );
47
+
48
+ case "autocomplete":
49
+ return (
50
+ <Autocomplete
51
+ {...name}
52
+ disablePortal
53
+ loading={loading}
54
+ size="small"
55
+ fullWidth
56
+ options={datas ?? []}
57
+ onChange={(event, newValue) => {
58
+ let updateObject = { [name]: newValue?.[keyValue] };
59
+
60
+ onFieldChange(newValue);
61
+ onChange(newValue);
62
+ childrenFields?.forEach((childrenField) => {
63
+ setValue(childrenField, null, { shouldTouch: true });
64
+ updateObject[childrenField] = null;
65
+ });
66
+ setDataSearch({ ...dataSearch, ...updateObject });
67
+ }}
68
+ value={value || null}
69
+ getOptionLabel={(option) => option?.[keyLabel]}
70
+ renderInput={(params) => <TextField {...params} label={label} />} />
71
+ );
72
+
73
+ case "select":
74
+ return (
75
+ <FormControl sx={{ minWidth: 160 }} size="small" fullWidth>
76
+ <InputLabel id="demo-simple-select-label">{label}</InputLabel>
77
+ <Select
78
+ {...rest}
79
+ labelId="demo-simple-select-label"
80
+ id="demo-simple-select"
81
+ name={name}
82
+ fullWidth
83
+ label={label}
84
+ value={value ?? ""}
85
+ onChange={(e) => {
86
+ onFieldChange(e);
87
+ setDataSearch({ ...dataSearch, [name]: e.target.value });
88
+ }}
89
+ >
90
+ {[...datas].map(({ label, value }) => (
91
+ <MenuItem key={value} value={value}>
92
+ {label}
93
+ </MenuItem>
94
+ ))}
95
+ </Select>
96
+ </FormControl>
97
+ );
98
+
99
+ default:
100
+ return "";
101
+ }
102
+ }
@@ -0,0 +1,83 @@
1
+ import { Slider, Toolbar } from "@mui/material";
2
+ import { useFormContext } from "react-hook-form";
3
+ import Grid from "@mui/material/Unstable_Grid2";
4
+ import DateRangePicker from "../date/DateRangePicker";
5
+ import { FilterElement } from "./FilterElement";
6
+ import { useDataTable } from "./hooks";
7
+
8
+ export const FilterGod = ({ filters }) => {
9
+ const { handleSubmit } = useFormContext();
10
+ const onSubmit = (data) => console.log(data);
11
+ const { setDataSearch, dataSearch } = useDataTable();
12
+
13
+ return (
14
+ <Toolbar
15
+ component={"form"}
16
+ disableGutters
17
+ onSubmit={handleSubmit(onSubmit)}
18
+ sx={{
19
+ px: 1,
20
+ my: 2,
21
+ display: "block"
22
+ }}
23
+ >
24
+ <Grid
25
+ container
26
+ rowSpacing={2}
27
+ columnSpacing={{ xs: 2 }}
28
+ sx={{
29
+ px: 1
30
+ }}
31
+ >
32
+ {filters.map(({ field, ...rest }) => {
33
+ if (rest.type === "date-range") {
34
+ return (
35
+ <Grid key={field.toString()} xs={12} sm={6} md={4} xl={3}>
36
+ <DateRangePicker
37
+ onChange={(value) => {
38
+ setDataSearch(({ previousState }) => ({
39
+ ...previousState,
40
+ [field[0]]: value[0],
41
+ [field[1]]: value[1]
42
+ }));
43
+ }}
44
+ size="small"
45
+ value={[dataSearch?.[field[0]], dataSearch?.[field[1]]]}
46
+ />
47
+ </Grid>
48
+ );
49
+ }
50
+
51
+ if (rest.type === "slider-range") {
52
+ return (
53
+ <Grid key={field.toString()} xs={12} sm={6} md={4} xl={3} sx={{
54
+ px:4
55
+ }}>
56
+ <Slider
57
+ onChange={(e, value) => {
58
+ setDataSearch({
59
+ ...dataSearch,
60
+ [field[0]]: value[0],
61
+ [field[1]]: value[1]
62
+ });
63
+ }}
64
+ size="small"
65
+ marks={rest.marks}
66
+ defaultValue={rest.defaultValue}
67
+ valueLabelDisplay="auto"
68
+ max={rest.marks[rest.marks.length - 1]?.value}
69
+ step={null}
70
+ />
71
+ </Grid>
72
+ );
73
+ }
74
+ return (
75
+ <Grid key={field} xs={12} sm={6} md={4} xl={3}>
76
+ <FilterElement name={field} {...rest} />
77
+ </Grid>
78
+ );
79
+ })}
80
+ </Grid>
81
+ </Toolbar>
82
+ );
83
+ };