index.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import React, { useState, useEffect, useReducer, useContext } from "react";
  2. import { toast } from "react-toastify";
  3. import { makeStyles } from "@material-ui/core/styles";
  4. import Paper from "@material-ui/core/Paper";
  5. import Button from "@material-ui/core/Button";
  6. import Table from "@material-ui/core/Table";
  7. import TableBody from "@material-ui/core/TableBody";
  8. import TableCell from "@material-ui/core/TableCell";
  9. import TableHead from "@material-ui/core/TableHead";
  10. import TableRow from "@material-ui/core/TableRow";
  11. import IconButton from "@material-ui/core/IconButton";
  12. import SearchIcon from "@material-ui/icons/Search";
  13. import TextField from "@material-ui/core/TextField";
  14. import InputAdornment from "@material-ui/core/InputAdornment";
  15. import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
  16. import EditIcon from "@material-ui/icons/Edit";
  17. import MainContainer from "../../components/MainContainer";
  18. import MainHeader from "../../components/MainHeader";
  19. import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
  20. import Title from "../../components/Title";
  21. import api from "../../services/api";
  22. import { i18n } from "../../translate/i18n";
  23. import TableRowSkeleton from "../../components/TableRowSkeleton";
  24. import UserModal from "../../components/UserModal";
  25. import ConfirmationModal from "../../components/ConfirmationModal";
  26. import toastError from "../../errors/toastError";
  27. import { SocketContext } from "../../context/Socket/SocketContext";
  28. const reducer = (state, action) => {
  29. if (action.type === "LOAD_USERS") {
  30. const users = action.payload;
  31. const newUsers = [];
  32. users.forEach((user) => {
  33. const userIndex = state.findIndex((u) => u.id === user.id);
  34. if (userIndex !== -1) {
  35. state[userIndex] = user;
  36. } else {
  37. newUsers.push(user);
  38. }
  39. });
  40. return [...state, ...newUsers];
  41. }
  42. if (action.type === "UPDATE_USERS") {
  43. const user = action.payload;
  44. const userIndex = state.findIndex((u) => u.id === user.id);
  45. if (userIndex !== -1) {
  46. state[userIndex] = user;
  47. return [...state];
  48. } else {
  49. return [user, ...state];
  50. }
  51. }
  52. if (action.type === "DELETE_USER") {
  53. const userId = action.payload;
  54. const userIndex = state.findIndex((u) => u.id === userId);
  55. if (userIndex !== -1) {
  56. state.splice(userIndex, 1);
  57. }
  58. return [...state];
  59. }
  60. if (action.type === "RESET") {
  61. return [];
  62. }
  63. };
  64. const useStyles = makeStyles((theme) => ({
  65. mainPaper: {
  66. flex: 1,
  67. padding: theme.spacing(1),
  68. overflowY: "scroll",
  69. ...theme.scrollbarStyles,
  70. },
  71. }));
  72. const Users = () => {
  73. const classes = useStyles();
  74. const [loading, setLoading] = useState(false);
  75. const [pageNumber, setPageNumber] = useState(1);
  76. const [hasMore, setHasMore] = useState(false);
  77. const [selectedUser, setSelectedUser] = useState(null);
  78. const [deletingUser, setDeletingUser] = useState(null);
  79. const [userModalOpen, setUserModalOpen] = useState(false);
  80. const [confirmModalOpen, setConfirmModalOpen] = useState(false);
  81. const [searchParam, setSearchParam] = useState("");
  82. const [users, dispatch] = useReducer(reducer, []);
  83. const socketManager = useContext(SocketContext);
  84. useEffect(() => {
  85. dispatch({ type: "RESET" });
  86. setPageNumber(1);
  87. }, [searchParam]);
  88. useEffect(() => {
  89. setLoading(true);
  90. const delayDebounceFn = setTimeout(() => {
  91. const fetchUsers = async () => {
  92. try {
  93. const { data } = await api.get("/users/", {
  94. params: { searchParam, pageNumber },
  95. });
  96. dispatch({ type: "LOAD_USERS", payload: data.users });
  97. setHasMore(data.hasMore);
  98. setLoading(false);
  99. } catch (err) {
  100. toastError(err);
  101. }
  102. };
  103. fetchUsers();
  104. }, 500);
  105. return () => clearTimeout(delayDebounceFn);
  106. }, [searchParam, pageNumber]);
  107. useEffect(() => {
  108. const companyId = localStorage.getItem("companyId");
  109. const socket = socketManager.getSocket(companyId);
  110. socket.on(`company-${companyId}-user`, (data) => {
  111. if (data.action === "update" || data.action === "create") {
  112. dispatch({ type: "UPDATE_USERS", payload: data.user });
  113. }
  114. if (data.action === "delete") {
  115. dispatch({ type: "DELETE_USER", payload: +data.userId });
  116. }
  117. });
  118. return () => {
  119. socket.disconnect();
  120. };
  121. }, [socketManager]);
  122. const handleOpenUserModal = () => {
  123. setSelectedUser(null);
  124. setUserModalOpen(true);
  125. };
  126. const handleCloseUserModal = () => {
  127. setSelectedUser(null);
  128. setUserModalOpen(false);
  129. };
  130. const handleSearch = (event) => {
  131. setSearchParam(event.target.value.toLowerCase());
  132. };
  133. const handleEditUser = (user) => {
  134. setSelectedUser(user);
  135. setUserModalOpen(true);
  136. };
  137. const handleDeleteUser = async (userId) => {
  138. try {
  139. await api.delete(`/users/${userId}`);
  140. toast.success(i18n.t("users.toasts.deleted"));
  141. } catch (err) {
  142. toastError(err);
  143. }
  144. setDeletingUser(null);
  145. setSearchParam("");
  146. setPageNumber(1);
  147. };
  148. const loadMore = () => {
  149. setPageNumber((prevState) => prevState + 1);
  150. };
  151. const handleScroll = (e) => {
  152. if (!hasMore || loading) return;
  153. const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
  154. if (scrollHeight - (scrollTop + 100) < clientHeight) {
  155. loadMore();
  156. }
  157. };
  158. return (
  159. <MainContainer>
  160. <ConfirmationModal
  161. title={
  162. deletingUser &&
  163. `${i18n.t("users.confirmationModal.deleteTitle")} ${
  164. deletingUser.name
  165. }?`
  166. }
  167. open={confirmModalOpen}
  168. onClose={setConfirmModalOpen}
  169. onConfirm={() => handleDeleteUser(deletingUser.id)}
  170. >
  171. {i18n.t("users.confirmationModal.deleteMessage")}
  172. </ConfirmationModal>
  173. <UserModal
  174. open={userModalOpen}
  175. onClose={handleCloseUserModal}
  176. aria-labelledby="form-dialog-title"
  177. userId={selectedUser && selectedUser.id}
  178. />
  179. <MainHeader>
  180. <Title>{i18n.t("users.title")}</Title>
  181. <MainHeaderButtonsWrapper>
  182. <TextField
  183. placeholder={i18n.t("contacts.searchPlaceholder")}
  184. type="search"
  185. value={searchParam}
  186. onChange={handleSearch}
  187. InputProps={{
  188. startAdornment: (
  189. <InputAdornment position="start">
  190. <SearchIcon style={{ color: "gray" }} />
  191. </InputAdornment>
  192. ),
  193. }}
  194. />
  195. <Button
  196. variant="contained"
  197. color="primary"
  198. onClick={handleOpenUserModal}
  199. >
  200. {i18n.t("users.buttons.add")}
  201. </Button>
  202. </MainHeaderButtonsWrapper>
  203. </MainHeader>
  204. <Paper
  205. className={classes.mainPaper}
  206. variant="outlined"
  207. onScroll={handleScroll}
  208. >
  209. <Table size="small">
  210. <TableHead>
  211. <TableRow>
  212. <TableCell align="center">
  213. {i18n.t("users.table.id")}
  214. </TableCell>
  215. <TableCell align="center">{i18n.t("users.table.name")}</TableCell>
  216. <TableCell align="center">
  217. {i18n.t("users.table.email")}
  218. </TableCell>
  219. <TableCell align="center">
  220. {i18n.t("users.table.profile")}
  221. </TableCell>
  222. <TableCell align="center">
  223. {i18n.t("users.table.actions")}
  224. </TableCell>
  225. </TableRow>
  226. </TableHead>
  227. <TableBody>
  228. <>
  229. {users.map((user) => (
  230. <TableRow key={user.id}>
  231. <TableCell align="center">{user.id}</TableCell>
  232. <TableCell align="center">{user.name}</TableCell>
  233. <TableCell align="center">{user.email}</TableCell>
  234. <TableCell align="center">{user.profile}</TableCell>
  235. <TableCell align="center">
  236. <IconButton
  237. size="small"
  238. onClick={() => handleEditUser(user)}
  239. >
  240. <EditIcon />
  241. </IconButton>
  242. <IconButton
  243. size="small"
  244. onClick={(e) => {
  245. setConfirmModalOpen(true);
  246. setDeletingUser(user);
  247. }}
  248. >
  249. <DeleteOutlineIcon />
  250. </IconButton>
  251. </TableCell>
  252. </TableRow>
  253. ))}
  254. {loading && <TableRowSkeleton columns={4} />}
  255. </>
  256. </TableBody>
  257. </Table>
  258. </Paper>
  259. </MainContainer>
  260. );
  261. };
  262. export default Users;