Plans.jsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import React, { useEffect, useState } from 'react';
  2. import { Box, Grid, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@material-ui/core';
  3. import { ExpandLess, History, VisibilityOff } from '@material-ui/icons';
  4. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  5. import { faCheck, faShoppingBasket, faTimes } from '@fortawesome/free-solid-svg-icons';
  6. import { makeStyles } from "@material-ui/styles";
  7. import { useTranslation } from "react-i18next";
  8. import { bool, string } from "prop-types";
  9. import { dateStringOptions, withLoginRequired } from "../util";
  10. import MissingIngredients from "./MissingIngredients";
  11. import { addPlan, getPlansOfUser, getSinglePlan } from "./plans.util";
  12. import MealAvatar from "../Meals/MealAvatar";
  13. import { useLocation, useNavigate, useParams } from "react-router-dom";
  14. import useSnackbars from "../util/useSnackbars";
  15. const useStyles = makeStyles((theme) => ({
  16. plansTable: {
  17. maxHeight: `calc(100% - ${process.env.REACT_APP_NAV_BOTTOM_HEIGHT}px)`,
  18. },
  19. pastPlanRow: {
  20. color: theme.palette.text.disabled,
  21. },
  22. goToPastPlansRow: {
  23. padding: '5px',
  24. textAlign: "center",
  25. lineHeight: '90%',
  26. },
  27. thCell: {
  28. fontSize: '1rem',
  29. },
  30. tableCell: {
  31. padding: '12px 16px',
  32. fontSize: '1rem',
  33. color: "inherit",
  34. },
  35. narrowCell: {
  36. padding: '12px 0 !important',
  37. },
  38. green: {
  39. color: theme.palette.primary[theme.palette.type],
  40. },
  41. infoText: {
  42. textAlign: "center",
  43. margin: "3rem 2rem",
  44. fontFamily: "Neucha",
  45. fontSize: "1.3rem",
  46. lineHeight: "1.4rem",
  47. },
  48. }));
  49. /** Component displays all Plans of any given user. Also handles routing to show
  50. * * Shopping List
  51. * * Edit Plan item */
  52. const Plans = (props) => {
  53. const classes = useStyles();
  54. let navigate = useNavigate();
  55. const params = useParams();
  56. const { t } = useTranslation();
  57. const { pathname } = useLocation();
  58. const { own, userId } = props;
  59. const [missingIngredientsDialogOpen, setMissingIngredientsDialogOpen] = useState(false);
  60. const [plans, setPlans] = useState([]);
  61. const [itemBeingEdited, setItemBeingEdited] = useState(null);
  62. const [emptyListFound, setEmptyListFound] = useState(false);
  63. const [deletedItem, setDeletedItem] = useState(null);
  64. const [pastPlansOpen, setPastPlansOpen] = useState(false);
  65. const fetchAndUpdatePlans = () => {
  66. getPlansOfUser(userId, plansFound => {
  67. setPlans(plansFound);
  68. if (plansFound.length === 0) setEmptyListFound(true);
  69. if (itemBeingEdited) {
  70. setItemBeingEdited(plansFound.find(p => p._id === itemBeingEdited._id)); // update itemBeingEdited
  71. }
  72. });
  73. }
  74. useEffect(() => {
  75. if (own && params.planId && (!itemBeingEdited || itemBeingEdited._id !== params.planId)) {
  76. getSinglePlan(params.planId, setItemBeingEdited);
  77. }
  78. // eslint-disable-next-line
  79. }, [own, itemBeingEdited, pathname, params]);
  80. // eslint-disable-next-line
  81. useEffect(fetchAndUpdatePlans, [userId, pathname]);
  82. const openMealDetailView = (meal) => {
  83. navigate('/meals/detail/' + meal._id, {state: { prevRoute: pathname }});
  84. };
  85. const goToEdit = (planItem) => {
  86. if (own) {
  87. setItemBeingEdited(planItem);
  88. navigate('edit/' + planItem._id, { state: { planItem } });
  89. }
  90. }
  91. const openShoppingList = () => {
  92. if (own) {
  93. navigate('shoppingList/' + userId, { state: { plans } });
  94. }
  95. }
  96. const undoDeletion = () => {
  97. addPlan(deletedItem, () => {
  98. fetchAndUpdatePlans();
  99. showReaddedItemMessage();
  100. });
  101. }
  102. const { Snackbars, showDeletedItemMessage, showReaddedItemMessage } = useSnackbars('Plan', deletedItem, undoDeletion);
  103. // to be put in the wrapper to display deletion correctly
  104. const onDeletePlan = (planItem) => {
  105. setDeletedItem(planItem);
  106. showDeletedItemMessage();
  107. }
  108. function openMissingIngredientDialog(planItem) {
  109. if (own && planItem.missingIngredients.length > 0) {
  110. setItemBeingEdited(planItem);
  111. setMissingIngredientsDialogOpen(true);
  112. }
  113. }
  114. const toggleHistory = (event) => {
  115. setPastPlansOpen(prevState => {
  116. // this scrolling mechanism is probably not the best possible behaviour to show the history of plans
  117. if (prevState === false) { // open history
  118. setTimeout(() => {event.target.scrollIntoView({ behavior: "smooth", block: "center" });}, 300);
  119. } else { // close history
  120. setTimeout(() => {event.target.scrollIntoView({ behavior: "smooth", block: "end" });}, 300);
  121. }
  122. return !prevState;
  123. });
  124. }
  125. const getPlanRows = () => {
  126. let pastPlansDone = false;
  127. const pastPlans = [];
  128. const futurePlans = [];
  129. plans.forEach((plan, index) => {
  130. const planIsInPast = plan.hasDate && new Date(plan.date).setHours(0, 0, 0, 0) < new Date().setHours(0, 0, 0, 0);
  131. if (!planIsInPast) {
  132. pastPlansDone = true;
  133. }
  134. const currentRow = (
  135. <TableRow key={plan._id + index} className={pastPlansDone ? '' : classes.pastPlanRow}>
  136. <TableCell className={classes.tableCell}>
  137. {plan.connectedMeal ?
  138. <Grid container spacing={1} justifyContent="space-between" alignItems="center">
  139. <Grid item xs={9} onClick={() => {goToEdit(plan);}}>{plan.title}</Grid>
  140. <Grid item xs={3} onClick={() => {openMealDetailView(plan.connectedMeal);}}><MealAvatar meal={plan.connectedMeal} /></Grid>
  141. </Grid>
  142. : <Box onClick={() => {goToEdit(plan);}}>{plan.title}</Box>
  143. }
  144. </TableCell>
  145. <TableCell onClick={() => {goToEdit(plan);}} align="center" className={classes.tableCell + ' ' + classes.narrowCell}>
  146. {(plan.hasDate && plan.date) ? new Date(plan.date).toLocaleDateString(t('DATE_LOCALE'), dateStringOptions) : ''}
  147. </TableCell>
  148. <TableCell className={classes.tableCell} align="center" onClick={() => {plan.missingIngredients.length === 0 ? goToEdit(plan) : openMissingIngredientDialog(plan);}}>
  149. <FontAwesomeIcon icon={plan.gotEverything ? faCheck : faTimes} />
  150. </TableCell>
  151. </TableRow>
  152. );
  153. if (!pastPlansDone) {
  154. pastPlans.push(currentRow);
  155. } else {
  156. futurePlans.push(currentRow);
  157. }
  158. });
  159. const rows = [];
  160. if (pastPlans.length > 0) {
  161. rows.push(
  162. <TableBody key='pastPlansTableBody' style={{ display: pastPlansOpen ? 'table-row-group' : 'none' }}>
  163. {pastPlans}
  164. </TableBody>
  165. );
  166. rows.push(
  167. <TableBody key='historyButtonTableBody'>
  168. <TableRow key={'openOrClosePlanHistoryButton'}>
  169. <TableCell colSpan={3} className={classes.goToPastPlansRow} onClick={toggleHistory}>
  170. {pastPlansOpen ? <VisibilityOff color="disabled" /> : <><ExpandLess /><History /><ExpandLess /></>}
  171. </TableCell>
  172. </TableRow>
  173. </TableBody>
  174. );
  175. }
  176. rows.push(<TableBody key='futurePlansTableBody'>{futurePlans}</TableBody>);
  177. return rows;
  178. }
  179. return (
  180. <>
  181. <>
  182. {plans.length === 0 ? <Typography className={classes.infoText}>{emptyListFound ? t("Currently nothing planned") : t('Loading') + '...'} </Typography> :
  183. <TableContainer className={classes.plansTable}>
  184. <Table aria-label="table of all plans" stickyHeader>
  185. <TableHead>
  186. <TableRow key='planListHeader'>
  187. <TableCell className={classes.thCell}>{t('Title')}</TableCell>
  188. <TableCell align="center" className={classes.narrowCell + ' ' + classes.thCell}>{t('Due Date')}</TableCell>
  189. <TableCell align="center" className={classes.thCell} onClick={openShoppingList}>
  190. <span className="fa-layers fa-fw">
  191. <FontAwesomeIcon icon={faShoppingBasket} transform="grow-6" />
  192. <FontAwesomeIcon icon={faCheck} className={classes.green} transform="down-2" />
  193. </span>
  194. </TableCell>
  195. </TableRow>
  196. </TableHead>
  197. {getPlanRows()}
  198. </Table>
  199. </TableContainer>
  200. }
  201. <MissingIngredients planItem={itemBeingEdited} closeDialog={() => {
  202. setItemBeingEdited(null);
  203. setMissingIngredientsDialogOpen(false);
  204. }} onDoneEditing={fetchAndUpdatePlans} open={missingIngredientsDialogOpen} />
  205. {Snackbars}
  206. </>
  207. </>
  208. );
  209. }
  210. Plans.propTypes = {
  211. /** are these the user's own plans or is another user watching foreign plans? In the latter case editing will be prohibited. */
  212. own: bool.isRequired,
  213. /** userId of user whose plans are to be displayed */
  214. userId: string.isRequired,
  215. }
  216. export default withLoginRequired(Plans);