@reach/router#navigate TypeScript Examples

The following examples show how to use @reach/router#navigate. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: PageNotFound.tsx    From office-booker with MIT License 6 votes vote down vote up
PageNotFound: React.FC<RouteComponentProps> = () => {
  // Local state
  const [activateRedirect, setActivateRedirect] = useState(false);

  // Effects
  useEffect(() => {
    setTimeout(() => {
      setActivateRedirect(true);
    }, 5000);
  }, []);

  // Render
  return (
    <Layout>
      <ErrorPageStyles>
        <h2>Page Not Found</h2>
        <p>We&apos;ll redirect you to home in 5 seconds</p>

        {activateRedirect && <Redirect to="/" noThrow />}

        <OurButton
          type="submit"
          variant="contained"
          color="primary"
          onClick={() => {
            navigate(`/`);
          }}
        >
          Redirect Now
        </OurButton>
      </ErrorPageStyles>
    </Layout>
  );
}
Example #2
Source File: Header.tsx    From office-booker with MIT License 6 votes vote down vote up
Header: React.FC = () => (
  <HeaderStyles>
    <div className="app-icon">
      <Link component="button" onClick={() => navigate('/')}>
        <img src={OfficesIcon} alt="Office Logo" width={30} height={30} />
      </Link>
    </div>

    <div className="title">
      <h1>
        <Link href="/">Office Booker</Link>
      </h1>
    </div>

    <div className="lab-logo">
      <Link href="https://github.com/o2Labs/" rel="noopener noreferrer" target="_blank">
        <img src={LabIcon} alt="The Lab Logo" width={32} height={32} />
      </Link>
    </div>
  </HeaderStyles>
)
Example #3
Source File: header.tsx    From nyxo-website with MIT License 5 votes vote down vote up
signOut = () => {
  Auth.signOut()
    .then(() => navigate("/me/login"))
    .catch((err) => console.log(err))
}
Example #4
Source File: NextBooking.tsx    From office-booker with MIT License 5 votes vote down vote up
NextBooking: React.FC<Props> = (props) => {
  // Local state
  const [todaysBooking, setTodaysBooking] = useState<Booking | undefined>();

  // Effects
  useEffect(() => {
    if (props.bookings.length > 0) {
      // Find a booking for today
      setTodaysBooking(
        props.bookings.find((b) => isToday(parse(b.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS)))
      );
    }
  }, [props.bookings]);

  // Render
  if (!todaysBooking) {
    return null;
  }

  return (
    <NextBookingStyles>
      <h2>Today&apos;s Booking</h2>

      <h3>
        {format(
          parse(todaysBooking.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
          'do LLL',
          DATE_FNS_OPTIONS
        )}{' '}
        <span>@</span> {todaysBooking.office.name}
      </h3>

      <OurButton
        type="submit"
        variant="contained"
        color="primary"
        onClick={() => {
          navigate(`./booking/${todaysBooking?.id}`);
        }}
      >
        View pass
      </OurButton>

      <div className="upcoming-header">
        <Link component="button" color="inherit" onClick={() => navigate('/bookings')}>
          View upcoming bookings
        </Link>
      </div>
    </NextBookingStyles>
  );
}
Example #5
Source File: Home.tsx    From listo with MIT License 5 votes vote down vote up
Home = (props: HomeProps) => {
  const isSlack = props.listoMeta
    ? Boolean(props.listoMeta.slackChannelLink && props.listoMeta.slackChannel)
    : false;
  const isTrelloBoard = props.listoMeta
    ? Boolean(props.listoMeta.exampleTrelloBoardLink)
    : false;
  return (
    <React.Fragment>
      <Grid container spacing={5}>
        <Grid item xs={12}>
          <Typography variant="h4" gutterBottom>
            Welcome to Listo
          </Typography>
        </Grid>
      </Grid>
      <Grid container spacing={5}>
        <Grid item xs={12}>
          <Typography variant="body1" gutterBottom>
            This tool provides advice on security, reliability and architecture
            requirements when developing products. Think of it as insight into
            the collective technical knowledge of an engineering community.
          </Typography>
        </Grid>
        <Grid item xs={12}>
          <Typography variant="body1" gutterBottom>
            At the end of the review we’ll provide a link to a Trello board
            which contains the checklists for your project.{' '}
            {isTrelloBoard && (
              <a
                href={props.listoMeta.exampleTrelloBoardLink}
                target="_blank"
                rel="noopener noreferrer"
              >
                Here is an example of a full Trello board.
              </a>
            )}
          </Typography>
        </Grid>
        <Grid item xs={12}>
          <Typography variant="body1" gutterBottom>
            Got questions? <a href={`/faq`}>Check out the FAQ's</a> or message
            us on Slack{' '}
            {isSlack && (
              <a
                href={props.listoMeta.slackChannelLink}
                target="_blank"
                rel="noopener noreferrer"
              >
                {props.listoMeta.slackChannel}
              </a>
            )}
          </Typography>
        </Grid>
        <Grid item xs={12} justify="center">
          <Button
            variant="contained"
            color="primary"
            onClick={() => navigate('/assessment')}
          >
            Do self assessment
          </Button>
        </Grid>
      </Grid>
    </React.Fragment>
  );
}
Example #6
Source File: activate.tsx    From midway with MIT License 4 votes vote down vote up
Activate = (props: { id: string; token: string }) => {
  const [passwordField1, setPasswordField1] = useState("")
  const [passwordField2, setPasswordField2] = useState("")
  const [attempts, setAttempts] = useState(0)

  const verifyAccount = useCallback(
    async e => {
      setAttempts(attempts + 1)

      if (passwordField1 !== passwordField2) {
        await Timeout.set(400)
        throw new Error("Passwords do not match.")
      }

      if (!PasswordSchema.validate(passwordField1)) {
        throw new Error(
          "Your password should be between 8 and 100 characters, and have at least one lowercase and one uppercase letter."
        )
      }

      const body = {
        id: encode("Customer", props.id),
        input: {
          activationToken: props.token,
          password: passwordField1,
        },
      }

      const res = await fetch(`/.netlify/functions/activate`, {
        method: "POST",
        body: JSON.stringify(body),
      })

      try {
        const customer = await res.json()

        if (customer.error) {
          const err = new Error("Server Error: Account verification fetch response error.")

          throw err
        } else if (!res.ok) {
          const err = new Error("Server Error: Account verification fetch not OK.")

     
          throw err
        }

        await Timeout.set(400)
        await navigate("/account/login")
      } catch (err) {
        throw err
      }
      // hacky way to get it to run anew every time
      // and maybe in the future redirect them if they
      // need help.
    },
    [passwordField1, passwordField2, attempts]
  )

  const { error, load, isReloading, isPending } = useDeferredLoads(
    "verifyAccount",
    verifyAccount,
    {}
  )

  return (
    <div>
      <Helmet title="activate account" />
      <div className="nav-spacer" />
      <div className="accounts__wrapper f col aic jcc y px1">
        <div className="x">
          <div className="container--xl ma ac">
            <h5 className="pb0 caps sans ls">Activate Account</h5>
            <h2 className="my0">Almost there.</h2>
          </div>
          <div className="x container--s al mxa">
            {error && !isReloading && (
              <div className="studio mt1 error f col">
                <span>{error.message}</span>
              </div>
            )}

            {(isPending || isReloading) && (
              <div className="f jcc p1">
                <span>Loading</span>
              </div>
            )}

            <div
              className={cx(
                "x container--s col mya",
                isPending || isReloading ? "invisible" : "visible"
              )}
            >

              <div className="pb1  x pya">
                <div className="caps sans ls my1">Password</div>
                <input
                  className="py1 px1  x s16 s16"
                  type="password"
                  value={passwordField1}
                  onChange={e => setPasswordField1(e.target.value)}
                  placeholder="Password"
                />
              </div>
              
              <div className="pb1 mb1 x pya">
                <div className="caps sans ls mt1 py05">Confirm Password</div>
                <input
                  className="py1 px1 x mb1 s16"
                  type="password"
                  value={passwordField2}
                  onChange={e => setPasswordField2(e.target.value)}
                  placeholder="Confirm Password"
                />
              </div>
            </div>
            <div className="ac x">
              
              <button
                type="submit"
                className="button button--wide cg ac akz ls-s mt1 inline-block caps s14"
                onClick={load}
              >
                Activate
              </button>

              <div className="container--m mya pt1 aic">
                <p className=" sans s14  pt1 ac">
                  Already have an account?{" "}
                  <Link className="underline active" to="/account/login">
                    Login
                  </Link>
                </p>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}
Example #7
Source File: Bookings.tsx    From office-booker with MIT License 4 votes vote down vote up
Bookings: React.FC<RouteComponentProps> = () => {
  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { user } = state;

  // Local state
  const [loading, setLoading] = useState(true);
  const [offices, setOffices] = useState<Office[] | undefined>();

  const [selectedOffice, setSelectedOffice] = useState<Office | undefined>();
  const [selectedDate, setSelectedDate] = useState(new Date());

  const [dbBookings, setDbBookings] = useState<Booking[] | undefined>();
  const [sortedBookings, setSortedBookings] = useState<Booking[] | undefined>();

  const [sortBy, setSortBy] = useState<keyof Booking>('user');
  const [sortOrder, setSortOrder] = useState<SortOrder>('asc');

  const [bookingToCancel, setBookingToCancel] = useState<undefined | Booking>();

  // Theme
  const theme = useTheme();
  const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));

  // Helpers
  const getAllBookings = useCallback(() => {
    if (selectedOffice) {
      getBookings({ office: selectedOffice, date: format(selectedDate, 'yyyy-MM-dd') })
        .then((data) => setDbBookings(data))
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [selectedOffice, selectedDate, dispatch]);

  const findOffice = useCallback(
    (name: OfficeWithSlots['name']) => offices && offices.find((o) => o.name === name),
    [offices]
  );

  // Effects
  useEffect(() => {
    if (user) {
      // Get all offices admin can manage
      getOffices()
        .then((data) =>
          setOffices(
            data.filter((office) =>
              user.permissions.officesCanManageBookingsFor.find(
                (userOffice) => userOffice.name === office.name
              )
            )
          )
        )
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [user, dispatch]);

  useEffect(() => {
    if (user && offices && offices.length > 0 && !selectedOffice) {
      // Retrieve first office user can manage bookings for
      setSelectedOffice(user.permissions.officesCanManageBookingsFor[0]);
    }
  }, [user, offices, selectedOffice, findOffice]);

  useEffect(() => {
    if (selectedOffice) {
      // Retrieve bookings
      getAllBookings();
    }
  }, [dispatch, selectedOffice, selectedDate, getAllBookings]);

  useEffect(() => {
    if (dbBookings) {
      // Sort it!
      setSortedBookings(sortData([...dbBookings], sortBy, sortOrder));
    }
  }, [dbBookings, sortBy, sortOrder]);

  useEffect(() => {
    if (loading && sortedBookings) {
      // Wait for local state to be ready
      setLoading(false);
    }
  }, [loading, sortedBookings]);

  // Handlers
  const handleSort = (key: keyof Booking) => {
    if (key === sortBy) {
      setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
    } else {
      setSortBy(key);
    }
  };

  const handleCancelBooking = (booking: Booking) => {
    cancelBooking(booking.id, booking.user)
      .then(() => {
        // Clear selected booking
        setBookingToCancel(undefined);

        // Retrieve updated bookings
        getAllBookings();

        // Show confirmation alert
        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: 'Booking cancelled',
            color: 'success',
          },
        });
      })
      .catch((err) =>
        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: formatError(err),
            color: 'error',
          },
        })
      );
  };

  // Render
  if (!user) {
    return null;
  }

  return (
    <AdminLayout currentRoute="bookings">
      <BookingStyles>
        {loading ? (
          <Loading />
        ) : (
          <>
            <h3>Bookings</h3>

            <OurButton
              startIcon={<AddCircleIcon />}
              type="submit"
              color="secondary"
              onClick={() => navigate('/admin/booking')}
              variant="contained"
              size="small"
            >
              New Booking
            </OurButton>

            {selectedOffice && (
              <Paper square className="table-container">
                <div className="filter">
                  <FormControl className="filter-office">
                    <InputLabel id="office-label">Office</InputLabel>
                    <Select
                      labelId="office-label"
                      id="office"
                      value={selectedOffice.name}
                      onChange={(e) => setSelectedOffice(findOffice(e.target.value as string))}
                    >
                      {user.permissions.officesCanManageBookingsFor.map((office, index) => (
                        <MenuItem value={office.name} key={index}>
                          {office.name}
                        </MenuItem>
                      ))}
                    </Select>
                  </FormControl>

                  <div className="filter-date">
                    <IconButton
                      onClick={() => setSelectedDate(addDays(new Date(selectedDate), -1))}
                      className="date-arrow"
                    >
                      <KeyboardArrowLeftIcon />
                    </IconButton>

                    <DatePicker
                      autoOk
                      disableToolbar
                      variant="inline"
                      label="Date"
                      format="dd/MM/yyyy"
                      value={selectedDate}
                      onChange={(date) => setSelectedDate(date as Date)}
                      className="date-picker"
                    />

                    <IconButton
                      onClick={() => setSelectedDate(addDays(new Date(selectedDate), 1))}
                      className="date-arrow"
                    >
                      <KeyboardArrowRightIcon />
                    </IconButton>
                  </div>
                </div>

                <div className="total-bookings">
                  <InputLabel className="bookings-count-label">Bookings:</InputLabel>
                  <span>{sortedBookings && sortedBookings.length}</span>
                </div>

                <TableContainer className="table">
                  <Table>
                    <TableHead>
                      <TableRow>
                        <TableCell className="table-header">
                          <TableSortLabel
                            active={sortBy === 'user'}
                            direction={sortOrder}
                            onClick={() => handleSort('user')}
                          >
                            User
                          </TableSortLabel>
                        </TableCell>
                        {selectedOffice.parkingQuota > 0 && (
                          <TableCell className="table-header">
                            <TableSortLabel
                              active={sortBy === 'parking'}
                              direction={sortOrder}
                              onClick={() => handleSort('parking')}
                            >
                              Parking
                            </TableSortLabel>
                          </TableCell>
                        )}
                        <TableCell />
                      </TableRow>
                    </TableHead>
                    <TableBody>
                      {sortedBookings && sortedBookings.length > 0 ? (
                        sortedBookings.map((booking, index) => {
                          const parsedDate = parseISO(booking.date);

                          return (
                            <TableRow key={index}>
                              <TableCell>{booking.user}</TableCell>
                              {selectedOffice.parkingQuota > 0 && (
                                <TableCell>{booking.parking ? 'Yes' : 'No'}</TableCell>
                              )}
                              {isToday(parsedDate) || !isPast(parsedDate) ? (
                                <TableCell align="right">
                                  <div className="btn-container">
                                    <OurButton
                                      type="submit"
                                      variant="contained"
                                      color="secondary"
                                      size="small"
                                      onClick={() => setBookingToCancel(booking)}
                                    >
                                      Cancel
                                    </OurButton>
                                  </div>
                                </TableCell>
                              ) : (
                                <TableCell />
                              )}
                            </TableRow>
                          );
                        })
                      ) : (
                        <TableRow>
                          <TableCell>No bookings found</TableCell>
                          {selectedOffice.parkingQuota > 0 && <TableCell />}
                          <TableCell />
                        </TableRow>
                      )}
                    </TableBody>
                  </Table>
                </TableContainer>
              </Paper>
            )}
          </>
        )}

        {bookingToCancel && (
          <Dialog fullScreen={fullScreen} open={true} onClose={() => setBookingToCancel(undefined)}>
            <DialogTitle>{'Are you sure you want to cancel this booking?'}</DialogTitle>
            <DialogContent>
              <DialogContentText>
                Booking for <strong>{bookingToCancel.user}</strong> on{' '}
                <strong>{format(parseISO(bookingToCancel.date), 'do LLLL')}</strong> for{' '}
                <strong>{bookingToCancel.office.name}</strong>
              </DialogContentText>
            </DialogContent>
            <DialogActions>
              <Button onClick={() => setBookingToCancel(undefined)} color="primary" autoFocus>
                No
              </Button>
              <Button
                autoFocus
                onClick={() => handleCancelBooking(bookingToCancel)}
                color="primary"
              >
                Yes
              </Button>
            </DialogActions>
          </Dialog>
        )}
      </BookingStyles>
    </AdminLayout>
  );
}
Example #8
Source File: User.tsx    From office-booker with MIT License 4 votes vote down vote up
UserAdmin: React.FC<RouteComponentProps<{ email: string }>> = (props) => {
  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { config, user } = state;
  const canEdit = user?.permissions.canEditUsers === true;

  // Local state
  const [loading, setLoading] = useState(true);
  const [offices, setOffices] = useState<Office[] | undefined>();
  const [selectedUser, setSelectedUser] = useState<User | undefined>();
  const [autoApprovedSelected, setAutoApprovedSelected] = useState(false);

  // Effects
  useEffect(() => {
    if (user && !user.permissions.canViewUsers) {
      // No permissions - Bounce to home page
      navigate('/');
    }
  }, [user]);

  useEffect(() => {
    if (user) {
      // Get selected user
      getUser(props.email || '')
        .then((selectedUser) => setSelectedUser(selectedUser))
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [user, props.email, dispatch]);

  useEffect(() => {
    if (user && selectedUser) {
      // Get all offices admin can manage
      setOffices(user.permissions.officesCanManageBookingsFor);

      if (config?.reasonToBookRequired && selectedUser.autoApproved !== undefined) {
        setAutoApprovedSelected(selectedUser.autoApproved);
      }
    }
  }, [user, selectedUser, dispatch, config?.reasonToBookRequired]);

  useEffect(() => {
    if (offices) {
      // Wait for global state to be ready
      setLoading(false);
    }
  }, [offices]);

  // Handlers
  const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    // Validate
    if (selectedUser === undefined) {
      return;
    }

    if (selectedUser.role.name === 'Office Admin' && selectedUser.role.offices.length === 0) {
      return dispatch({
        type: 'SET_ALERT',
        payload: {
          message: 'Please select at least one office',
          color: 'error',
        },
      });
    }

    // Create/update user
    const role = selectedUser.role.name === 'System Admin' ? undefined : selectedUser.role;

    const putBody = config?.reasonToBookRequired
      ? {
          email: selectedUser.email,
          quota: selectedUser.quota,
          role,
          autoApproved: selectedUser.autoApproved,
        }
      : {
          email: selectedUser.email,
          quota: selectedUser.quota,
          role,
        };

    putUser(putBody)
      .then((updatedUser) => {
        // Update local state
        setSelectedUser(updatedUser);

        // Confirmation alert
        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: `${updatedUser.email} updated`,
            color: 'success',
          },
        });
      })
      .catch((err) => {
        // Handle errors
        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: formatError(err),
            color: 'error',
          },
        });
      });
  };

  // Render
  if (!user) {
    return null;
  }

  return (
    <AdminLayout currentRoute="users">
      <UserStyles>
        {loading || !selectedUser || !offices ? (
          <Loading />
        ) : (
          <>
            <h3>Users</h3>

            <Paper square className="form-container">
              <h4>Edit user</h4>
              <h5>{selectedUser.email}</h5>

              <form onSubmit={handleFormSubmit}>
                <div className="field">
                  <FormControl variant="outlined" className="input">
                    <InputLabel id="role-label" shrink>
                      Role
                    </InputLabel>
                    <Select
                      labelId="role-label"
                      id="role"
                      value={selectedUser.role.name}
                      disabled={selectedUser.role.name === 'System Admin' || !canEdit}
                      onChange={(e) => {
                        const { value } = e.target;

                        setSelectedUser((selectedUser) => {
                          if (selectedUser === undefined) {
                            return;
                          }

                          if (value === 'Default') {
                            return { ...selectedUser, role: { name: 'Default' } };
                          }

                          if (value === 'Office Admin') {
                            return {
                              ...selectedUser,
                              role: { name: 'Office Admin', offices: [] },
                            };
                          }

                          return selectedUser;
                        });
                      }}
                      label="Role"
                    >
                      <MenuItem value={'Default'}>Default</MenuItem>
                      <MenuItem value={'Office Admin'}>Office Admin</MenuItem>
                      <MenuItem value={'System Admin'} disabled>
                        System Admin
                      </MenuItem>
                    </Select>
                  </FormControl>
                </div>

                {selectedUser.role.name === 'Office Admin' && (
                  <div className="field">
                    <Autocomplete
                      multiple
                      disabled={!canEdit}
                      options={offices.map((o) => o.name)}
                      value={
                        selectedUser.role.name === 'Office Admin'
                          ? selectedUser.role.offices.map((office) => office.name)
                          : []
                      }
                      onChange={(_e, value) =>
                        setSelectedUser((selectedUser) => {
                          if (selectedUser === undefined) {
                            return;
                          }

                          return {
                            ...selectedUser,
                            role: {
                              name: 'Office Admin',
                              offices: offices.filter((office) => value.includes(office.name)),
                            },
                          };
                        })
                      }
                      renderInput={(params) => (
                        <TextField
                          {...params}
                          variant="outlined"
                          label="Offices"
                          fullWidth={false}
                          className="input"
                        />
                      )}
                      renderTags={(selectedOffices, tagProps) =>
                        selectedOffices.map((office, index: number) => (
                          <Chip
                            variant="outlined"
                            key={index}
                            label={office}
                            {...tagProps({ index })}
                          />
                        ))
                      }
                    />
                  </div>
                )}

                <div className="field">
                  <TextField
                    type="number"
                    variant="outlined"
                    disabled={!canEdit}
                    label="Weekly quota"
                    value={selectedUser.quota}
                    onChange={(e) => {
                      // Between 0 and 7
                      const quota = Number.parseInt(e.target.value);

                      setSelectedUser(
                        (selectedUser) =>
                          selectedUser && {
                            ...selectedUser,
                            quota: quota >= 0 && quota <= 7 ? quota : quota > 7 ? 7 : 0,
                          }
                      );
                    }}
                    className="input"
                  />
                </div>

                {config?.reasonToBookRequired && (
                  <div className="field">
                    <FormControlLabel
                      control={
                        <Checkbox
                          checked={autoApprovedSelected}
                          onChange={(event) =>
                            setSelectedUser(
                              (selectedUser) =>
                                selectedUser && {
                                  ...selectedUser,
                                  autoApproved: event.target.checked,
                                }
                            )
                          }
                          name="checkedB"
                          color="primary"
                        />
                      }
                      label="Auto Approved"
                    />
                  </div>
                )}

                {canEdit && (
                  <div className="buttons">
                    <OurButton
                      type="submit"
                      color="primary"
                      variant="contained"
                      disabled={selectedUser.quota < 0}
                    >
                      Save
                    </OurButton>
                  </div>
                )}
              </form>
            </Paper>

            <section className="help">
              <h3>About Roles</h3>

              <h4>Default</h4>

              <ul>
                <li>Any user with a valid email address gets this role.</li>
                <li>Can manage their own bookings only.</li>
              </ul>

              <h4>System Admin</h4>

              <ul>
                <li>Must be configured in infrastructure.</li>
                <li>Can view and edit all bookings in the system.</li>
                <li>Can view and edit all users</li>
              </ul>

              <h4>Office Admin</h4>

              <ul>
                <li>Must be assigned by a System Admin.</li>
                <li>Can view and edit bookings for their assigned offices.</li>
                <li>Can view other users (but can&apos;t edit).</li>
              </ul>

              <p>A default quota is applied to all users regardless of role.</p>
            </section>
          </>
        )}
      </UserStyles>
    </AdminLayout>
  );
}
Example #9
Source File: UserBookings.tsx    From office-booker with MIT License 4 votes vote down vote up
UserBookings: React.FC<RouteComponentProps<{ email: string }>> = (props) => {
  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { user } = state;

  // Local state
  const [loading, setLoading] = useState(true);
  const [selectedUser, setSelectedUser] = useState<User | undefined>();
  const [bookings, setBookings] = useState<Booking[] | undefined>();
  const [bookingToCancel, setBookingToCancel] = useState<undefined | Booking>();
  const [sortedBookings, setSortedBookings] = useState<Booking[] | undefined>();

  const [sortBy, setSortBy] = useState<keyof Booking>('date');
  const [sortOrder, setSortOrder] = useState<SortOrder>('asc');

  // Theme
  const theme = useTheme();
  const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));

  // Effects
  useEffect(() => {
    if (props.email) {
      getBookings({ user: props.email })
        .then((data) => {
          // Split for previous and upcoming
          setBookings(data);
        })
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [props.email, dispatch]);

  useEffect(() => {
    if (user && !user.permissions.canViewUsers) {
      // No permissions - Bounce to home page
      navigate('/');
    }
  }, [user]);

  useEffect(() => {
    if (user) {
      // Get selected user
      getUser(props.email || '')
        .then((selectedUser) => setSelectedUser(selectedUser))
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [user, props.email, dispatch]);

  useEffect(() => {
    if (bookings) {
      // Sort it!
      setSortedBookings(sortData([...bookings], sortBy, sortOrder));
    }
  }, [bookings, sortBy, sortOrder]);

  useEffect(() => {
    if (bookings) {
      // Wait for global state to be ready
      setLoading(false);
    }
  }, [bookings]);

  // Handlers
  const handleSort = (key: keyof Booking) => {
    if (key === sortBy) {
      setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
    } else {
      setSortBy(key);
    }
  };

  const getAllBookings = useCallback(() => {
    if (state.user) {
      getBookings({ user: state.user.email })
        .then((data) => {
          // Split for previous and upcoming
          setBookings(data);
        })
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [state.user, dispatch]);

  const handleCancelBooking = (booking: Booking) => {
    cancelBooking(booking.id, booking.user)
      .then(() => {
        // Clear selected booking
        setBookingToCancel(undefined);

        // Retrieve updated bookings
        getAllBookings();

        // Show confirmation alert
        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: 'Booking cancelled',
            color: 'success',
          },
        });
      })
      .catch((err) =>
        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: formatError(err),
            color: 'error',
          },
        })
      );
  };

  // Render
  if (!user) {
    return null;
  }

  return (
    <AdminLayout currentRoute="users">
      <UserBookingsStyles>
        {loading || !selectedUser ? (
          <Loading />
        ) : (
          <>
            <h3>User Bookings</h3>

            <Paper square className="table-container">
              <h4>{selectedUser.email}</h4>
              <TableContainer className="table">
                <Table>
                  <TableHead>
                    <TableRow>
                      <TableCell className="table-header">
                        <TableSortLabel
                          active={sortBy === 'office'}
                          direction={sortOrder}
                          onClick={() => handleSort('office')}
                        >
                          Office
                        </TableSortLabel>
                      </TableCell>
                      <TableCell className="table-header">
                        <TableSortLabel
                          active={sortBy === 'date'}
                          direction={sortOrder}
                          onClick={() => handleSort('date')}
                        >
                          Date
                        </TableSortLabel>
                      </TableCell>
                      <TableCell className="table-header">
                        <TableSortLabel
                          active={sortBy === 'parking'}
                          direction={sortOrder}
                          onClick={() => handleSort('parking')}
                        >
                          Parking
                        </TableSortLabel>
                      </TableCell>
                      <TableCell />
                    </TableRow>
                  </TableHead>
                  <TableBody>
                    {sortedBookings && sortedBookings.length > 0 ? (
                      sortedBookings.map((booking, index) => {
                        const parsedDate = parseISO(booking.date);

                        return (
                          <TableRow key={index}>
                            <TableCell>{booking.office.name}</TableCell>
                            <TableCell>
                              {' '}
                              {format(
                                parse(booking.date, 'yyyy-MM-dd', new Date(), DATE_FNS_OPTIONS),
                                'do LLLL yyyy',
                                DATE_FNS_OPTIONS
                              )}
                            </TableCell>
                            <TableCell>{booking.parking ? 'Yes' : 'No'}</TableCell>
                            {isToday(parsedDate) || !isPast(parsedDate) ? (
                              <TableCell align="right">
                                <div className="btn-container">
                                  <OurButton
                                    type="submit"
                                    variant="contained"
                                    color="secondary"
                                    size="small"
                                    onClick={() => setBookingToCancel(booking)}
                                  >
                                    Cancel
                                  </OurButton>
                                </div>
                              </TableCell>
                            ) : (
                              <TableCell />
                            )}
                          </TableRow>
                        );
                      })
                    ) : (
                      <TableRow>
                        <TableCell>No bookings found</TableCell>
                        <TableCell />
                      </TableRow>
                    )}
                  </TableBody>
                </Table>
              </TableContainer>
            </Paper>
          </>
        )}

        {bookingToCancel && (
          <Dialog fullScreen={fullScreen} open={true} onClose={() => setBookingToCancel(undefined)}>
            <DialogTitle>{'Are you sure you want to cancel this booking?'}</DialogTitle>
            <DialogContent>
              <DialogContentText>
                Booking for <strong>{bookingToCancel.user}</strong> on{' '}
                <strong>{format(parseISO(bookingToCancel.date), 'do LLLL')}</strong> for{' '}
                <strong>{bookingToCancel.office.name}</strong>
              </DialogContentText>
            </DialogContent>
            <DialogActions>
              <Button onClick={() => setBookingToCancel(undefined)} color="primary" autoFocus>
                No
              </Button>
              <Button
                autoFocus
                onClick={() => handleCancelBooking(bookingToCancel)}
                color="primary"
              >
                Yes
              </Button>
            </DialogActions>
          </Dialog>
        )}
      </UserBookingsStyles>
    </AdminLayout>
  );
}
Example #10
Source File: Users.tsx    From office-booker with MIT License 4 votes vote down vote up
Users: React.FC<RouteComponentProps> = () => {
  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { config, user } = state;

  // Local state
  const [loading, setLoading] = useState(true);
  const [dbUsers, setDbUsers] = useState<User[] | undefined>(undefined);
  const [sortedUsers, setSortedUsers] = useState<User[] | undefined>(undefined);
  const [paginationToken, setPaginationToken] = useState<string | undefined>();
  const [selectedFilter, setSelectedFilter] = useState<UserFilter>({
    user: 'active',
  });
  const [email, setEmail] = useState<string>('');

  const [sortBy, setSortBy] = useState<keyof User>('email');
  const [sortOrder, setSortOrder] = useState<SortOrder>('asc');

  const [showAddUser, setShowAddUser] = useState(false);
  const [addUserEmail, setAddUserEmail] = useState('');
  const [addUserError, setAddUserError] = useState(false);

  // Effects
  useEffect(() => {
    // Retrieve results
    queryUsers(userFilterToQuery(selectedFilter))
      .then((result) => {
        setDbUsers(result.users);
        setPaginationToken(result.paginationToken);
      })
      .catch((err) => {
        // Handle errors
        setLoading(false);

        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: formatError(err),
            color: 'error',
          },
        });
      });
  }, [selectedFilter, dispatch]);

  useEffect(() => {
    if (dbUsers) {
      // Sort it!
      setSortedUsers(sortData([...dbUsers], sortBy, sortOrder));
    }
  }, [dbUsers, sortBy, sortOrder]);

  useEffect(() => {
    if (loading && sortedUsers) {
      // Wait for local state to be ready
      setLoading(false);
    }
  }, [loading, sortedUsers]);

  useEffect(() => {
    // Only allow the following
    // - Different to value in local state
    // AND
    // - Not blank OR blank when there was a previous value
    const sanitisedEmail = email.trim().toLowerCase();

    if (
      sanitisedEmail !== selectedFilter.email &&
      (sanitisedEmail !== '' || (selectedFilter.email && sanitisedEmail === ''))
    ) {
      // Search after typing has stopped
      const searchEmailTimer = setTimeout(() => {
        setSelectedFilter((filter) => ({
          ...filter,
          email: sanitisedEmail,
        }));
      }, 1000);

      // Cleanup
      return () => clearTimeout(searchEmailTimer);
    }

    return;
  }, [selectedFilter.email, email]);

  // Handlers
  const handleChangeUser = (e: React.ChangeEvent<{ value: unknown }>) => {
    const user = e.target.value as string;

    if (
      user === 'active' ||
      user === 'System Admin' ||
      user === 'Office Admin' ||
      user === 'custom' ||
      user === 'Auto Approved'
    ) {
      setSelectedFilter((filter) => ({ ...filter, user }));
    }
  };

  const handleSort = (key: keyof User) => {
    if (key === sortBy) {
      setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
    } else {
      setSortBy(key);
    }
  };

  const handleLoadMore = () => {
    if (paginationToken && selectedFilter.user === 'active') {
      // Clear the current token
      setPaginationToken(undefined);

      // Retrieve data after pagination token
      queryUsers({}, paginationToken)
        .then((result) => {
          // Merge new results with existing
          setDbUsers((previousUsers) => [...(previousUsers ?? []), ...result.users]);

          setPaginationToken(result.paginationToken);
        })
        .catch((error) =>
          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(error),
              color: 'error',
            },
          })
        );
    }
  };

  const handleAdduser = () => {
    if (validateEmail(state.config?.emailRegex, addUserEmail)) {
      setAddUserError(false);

      navigate(`/admin/users/${addUserEmail}`);
    } else {
      setAddUserError(true);
    }
  };

  // Render
  if (!user) {
    return null;
  }

  return (
    <AdminLayout currentRoute="users">
      <UsersStyles>
        {loading ? (
          <Loading />
        ) : (
          <>
            <h3>Users</h3>

            <OurButton
              startIcon={<AddCircleIcon />}
              type="submit"
              color="secondary"
              onClick={() => setShowAddUser(true)}
              variant="contained"
              size="small"
            >
              New user
            </OurButton>

            <Paper square className="table-container">
              <section className="filters">
                <div className="filter-role">
                  <FormControl>
                    <InputLabel>User</InputLabel>
                    <Select value={selectedFilter.user} onChange={handleChangeUser}>
                      <MenuItem value="active">All active users</MenuItem>
                      <MenuItem value="System Admin">System Admins</MenuItem>
                      <MenuItem value="Office Admin">Office Admins</MenuItem>
                      <MenuItem value="custom">With custom quota</MenuItem>
                      {config?.reasonToBookRequired && (
                        <MenuItem value="Auto Approved">With auto approved</MenuItem>
                      )}
                    </Select>
                  </FormControl>
                </div>

                <div className="search-user">
                  <TextField
                    placeholder="Start of email address"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    InputProps={{
                      startAdornment: (
                        <InputAdornment position="start">
                          <SearchIcon />
                        </InputAdornment>
                      ),
                    }}
                  />
                </div>
              </section>

              {selectedFilter.user === 'active' && (
                <p className="note">
                  Please note, a user is only considered &quot;active&quot; after logging into the
                  app the first time.
                </p>
              )}

              <TableContainer className="table">
                <Table>
                  <TableHead>
                    <TableRow>
                      <TableCell className="table-header">
                        <TableSortLabel
                          active={sortBy === 'email'}
                          direction={sortOrder}
                          onClick={() => handleSort('email')}
                        >
                          User Email
                        </TableSortLabel>
                      </TableCell>
                      <TableCell className="table-header">
                        <TableSortLabel
                          active={sortBy === 'quota'}
                          direction={sortOrder}
                          onClick={() => handleSort('quota')}
                        >
                          Quota
                        </TableSortLabel>
                      </TableCell>
                      <TableCell className="table-header">
                        <TableSortLabel
                          active={sortBy === 'role'}
                          direction={sortOrder}
                          onClick={() => handleSort('role')}
                        >
                          Role
                        </TableSortLabel>
                      </TableCell>
                      {config?.reasonToBookRequired && (
                        <TableCell className="table-header">
                          <TableSortLabel
                            active={sortBy === 'autoApproved'}
                            direction={sortOrder}
                            onClick={() => handleSort('autoApproved')}
                          >
                            Auto Approved
                          </TableSortLabel>
                        </TableCell>
                      )}
                      <TableCell className="table-header" />
                    </TableRow>
                  </TableHead>
                  <TableBody>
                    {sortedUsers && sortedUsers.length > 0 ? (
                      sortedUsers.map((user) => (
                        <TableRow key={user.email}>
                          <TableCell>{user.email}</TableCell>
                          <TableCell>{user.quota}</TableCell>
                          <TableCell>{user.role.name}</TableCell>
                          {config?.reasonToBookRequired && (
                            <TableCell>
                              {user.autoApproved ? <CheckIcon /> : <ClearIcon color="disabled" />}
                            </TableCell>
                          )}
                          <TableCell align="right">
                            <Tooltip title={`Edit`} arrow>
                              <IconButton onClick={() => navigate(`/admin/users/${user.email}`)}>
                                <CreateIcon />
                              </IconButton>
                            </Tooltip>
                            <Tooltip title={`View Bookings`} arrow>
                              <IconButton
                                onClick={() => navigate(`/admin/users/bookings/${user.email}`)}
                              >
                                <TodayIcon />
                              </IconButton>
                            </Tooltip>
                          </TableCell>
                        </TableRow>
                      ))
                    ) : (
                      <TableRow>
                        <TableCell>No users found</TableCell>
                        <TableCell />
                        <TableCell />
                        <TableCell />
                        {config?.reasonToBookRequired && <TableCell />}
                      </TableRow>
                    )}
                  </TableBody>
                </Table>
              </TableContainer>

              {paginationToken && (
                <section className="load-more-container">
                  <OurButton onClick={handleLoadMore} color="primary" variant="contained">
                    Load More
                  </OurButton>
                </section>
              )}

              {selectedFilter.user === 'active' &&
                selectedFilter.email !== undefined &&
                validateEmail(state.config?.emailRegex, selectedFilter.email) &&
                dbUsers?.length === 0 && (
                  <section className="unregistered-user">
                    <div className="link">
                      <p>
                        User <span>{selectedFilter.email}</span> not yet registered
                      </p>

                      <OurButton
                        startIcon={<AddCircleIcon />}
                        type="submit"
                        variant="contained"
                        color="primary"
                        onClick={() => navigate(`/admin/users/${selectedFilter.email}`)}
                        size="small"
                      >
                        Add user
                      </OurButton>
                    </div>

                    <p>
                      Please note the user will not be considered &quot;active&quot; until they
                      login for the first time.
                    </p>
                  </section>
                )}
            </Paper>

            <Dialog open={showAddUser} onClose={() => setShowAddUser(false)}>
              <DialogTitle>Enter the email address for the user you wish to create.</DialogTitle>
              <DialogContent>
                <DialogContentText>
                  Please note the user will not be considered &quot;active&quot; until they login
                  for the first time.
                </DialogContentText>

                <TextField
                  autoFocus
                  label="Email Address"
                  type="email"
                  value={addUserEmail}
                  onChange={(e) => setAddUserEmail(e.target.value)}
                  error={addUserError}
                  helperText={addUserError && `Invalid email address`}
                  fullWidth
                />
              </DialogContent>
              <DialogActions>
                <Button onClick={() => setShowAddUser(false)} color="primary" autoFocus>
                  Cancel
                </Button>
                <Button
                  startIcon={<AddCircleIcon />}
                  variant="contained"
                  onClick={() => handleAdduser()}
                  color="secondary"
                >
                  New user
                </Button>
              </DialogActions>
            </Dialog>
          </>
        )}
      </UsersStyles>
    </AdminLayout>
  );
}
Example #11
Source File: Help.tsx    From office-booker with MIT License 4 votes vote down vote up
Help: React.FC<RouteComponentProps> = () => {
  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { user, office } = state;

  const [currentOffice, setCurrentOffice] = useState<OfficeWithSlots | undefined>();

  useEffect(() => {
    if (user && office && 'id' in office) {
      getOffice(office.id)
        .then(setCurrentOffice)
        .catch((err) =>
          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          })
        );
    }
    if (user && office && 'name' in office) {
      getOffices()
        .then((offices) =>
          dispatch({
            type: 'SET_OFFICE',
            payload: offices.find((o) => o.name === office.name),
          })
        )
        .catch((err) =>
          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          })
        );
    }
  }, [user, office, dispatch]);

  // Effects
  useEffect(() => {
    // Retrieve user if not already available
    if (!user) {
      // Retrieve cognito session
      getAuthState()
        .then((username) => {
          // Retrieve DB user
          if (username) {
            getUserCached(username)
              .then((data) =>
                dispatch({
                  type: 'SET_USER',
                  payload: data,
                })
              )
              .catch((err) =>
                dispatch({
                  type: 'SET_ALERT',
                  payload: {
                    message: formatError(err),
                    color: 'error',
                  },
                })
              );
          }
        })
        .catch((err) =>
          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          })
        );
    }
  }, [dispatch, user]);

  useEffect(() => {
    // Validate global office
    if (user && office) {
      getOffices()
        .then((data) => {
          // Validate and update local storage if not
          const findOffice =
            'id' in office
              ? data.find((o) => o.id === office.id)
              : data.find((o) => o.name === office.name);

          if (!findOffice) {
            dispatch({
              type: 'SET_OFFICE',
              payload: undefined,
            });
          }
        })
        .catch((err) =>
          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          })
        );
    }
  }, [dispatch, user, office]);

  // Handlers
  const handleClearOffice = () => {
    // Update global state
    dispatch({
      type: 'SET_OFFICE',
      payload: undefined,
    });

    // Change page if required
    navigate('/');
  };

  // Render
  if (!state.config) {
    return null;
  }

  return (
    <Layout>
      <HelpStyles>
        <h2>Help</h2>
        <p>
          The purpose of the Office Booker app is to help coordinate the safe use of office
          facilities by proactively managing demand.
        </p>

        <h3>General Information</h3>

        <p>Bookings can only be made {state.config.advancedBookingDays} days in advance.</p>

        {user && currentOffice && (
          <div className="change-office">
            <p>
              You are currently booking for <span>{currentOffice.name}</span>.
            </p>
            <Link
              component="button"
              underline="always"
              color="primary"
              onClick={() => handleClearOffice()}
            >
              Change office
            </Link>
          </div>
        )}

        <h3>Credits</h3>

        <p>
          Office Booker was built by The Lab at O2, visit us on{' '}
          <a href="https://github.com/o2Labs/" rel="noopener noreferrer" target="_blank">
            GitHub
          </a>
          .
        </p>

        <p>Icon made by Freepik from www.flaticon.com</p>
      </HelpStyles>
    </Layout>
  );
}
Example #12
Source File: MakeBooking.tsx    From office-booker with MIT License 4 votes vote down vote up
MakeBooking: React.FC<Props> = (props) => {
  const { office, bookings, refreshBookings } = props;

  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { config, user } = state;

  // Local state
  const [weeks, setWeeks] = useState<Week[]>([]);
  const [rows, setRows] = useState<Row[]>([]);
  const [selectedWeek, setSelectedWeek] = useState<number | undefined>();
  const [buttonsLoading, setButtonsLoading] = useState(true);
  const [showTodayConfirmation, setShowTodayConfirmation] = useState(false);
  const [showReasonConfirmation, setShowReasonConfirmation] = useState(false);
  const [bookingDate, setBookingDate] = useState<Date | undefined>();
  const [bookingParking, setBookingParking] = useState(false);
  const [bookingReason, setBookingReason] = useState<string | undefined>();
  const [isAutoApprovedUser, setIsAutoApprovedUser] = useState<boolean>(false);
  const [searchedUser, setSearchedUser] = useState<User | undefined>();

  // Refs
  const reloadTimerRef = useRef<ReturnType<typeof setInterval> | undefined>();

  // Helper
  const setReloadTimer = useCallback(() => {
    reloadTimerRef.current = setInterval(() => {
      setButtonsLoading(true);

      refreshBookings();
    }, RELOAD_TIME);
  }, [refreshBookings]);

  const resetReloadTimer = () => {
    reloadTimerRef.current && clearInterval(reloadTimerRef.current);

    setReloadTimer();
  };

  const isReasonRequired = () => config?.reasonToBookRequired && !isAutoApprovedUser;

  // Effects
  useEffect(() => {
    // Periodically refresh bookings
    setReloadTimer();

    // Handle browser visibility
    const handleVisibilityChange = () => {
      if (document.hidden) {
        // Clear timer
        reloadTimerRef.current && clearInterval(reloadTimerRef.current);
      } else {
        // Reload data & restart timer
        setButtonsLoading(true);

        setReloadTimer();
        refreshBookings();
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      reloadTimerRef.current && clearInterval(reloadTimerRef.current);

      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [setReloadTimer, refreshBookings]);

  useEffect(() => {
    // Set week numbers from available slots
    const weeks: Week[] = [];

    office.slots.forEach((s) => {
      // Get week
      const date = parse(s.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS);
      const weekStart = startOfWeek(date, DATE_FNS_OPTIONS);

      // Is it already in the list?
      if (!weeks.find((w) => w.startOfWeek.getTime() === weekStart.getTime())) {
        // Find total user bookings for this week
        const userBookings = bookings.filter((b) =>
          isSameWeek(parse(b.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS), date, DATE_FNS_OPTIONS)
        );

        weeks.push({
          startOfWeek: weekStart,
          bookings: userBookings.length,
        });
      }
    });

    setWeeks(weeks);
  }, [bookings, office]);

  useEffect(() => {
    if (weeks.length > 0) {
      setButtonsLoading(false);

      if (selectedWeek === undefined) {
        setSelectedWeek(0);
      }
    }
  }, [selectedWeek, weeks]);

  const getWeekLabel = (week: Week) => {
    const startDate = week.startOfWeek;
    const endDate = endOfWeek(startDate, DATE_FNS_OPTIONS);

    // Are dates in the same month
    const sameMonth = isSameMonth(startDate, endDate);

    // Set label
    const startLabel = format(startDate, 'LLL d', DATE_FNS_OPTIONS);
    const endLabel = sameMonth
      ? format(endDate, 'd', DATE_FNS_OPTIONS)
      : format(endDate, 'LLL d', DATE_FNS_OPTIONS);

    return `${startLabel} - ${endLabel}`;
  };

  useEffect(() => {
    if (user && weeks && weeks.length > 0) {
      const { id, quota: officeQuota, parkingQuota, slots } = office;
      const { quota: userQuota } = user;

      const rows: Row[] = [];

      // For each week
      weeks.forEach((w) => {
        // Find all days in the week
        const startDate = w.startOfWeek;
        const endDate = endOfWeek(w.startOfWeek, DATE_FNS_OPTIONS);

        const weekDays = eachDayOfInterval({
          start: startDate,
          end: endDate,
        });

        // For each day
        const days: Row['days'] = [];

        weekDays.forEach((d) => {
          // Is the date in the slots?
          const slot = slots.find((s) => format(d, 'y-MM-dd', DATE_FNS_OPTIONS) === s.date);

          if (slot) {
            // Calculate available spaces
            const available = officeQuota - slot.booked;
            const availableCarPark = parkingQuota - slot.bookedParking;

            // Find any user booking for this office/slot
            const booking = bookings.find((b) => b.office.id === id && b.date === slot.date);

            // Total user bookings for this week
            const userWeekBookings = bookings.filter((b) =>
              isSameWeek(
                parse(b.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
                parse(slot.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
                DATE_FNS_OPTIONS
              )
            );

            // Add day
            days.push({
              date: d,
              isBookable: true,
              available,
              availableCarPark,
              userCanBook: available > 0 && userWeekBookings.length < userQuota,
              booking,
            });
          } else {
            // Did we have a booking on this day?
            const booking = bookings.find(
              (b) => b.office.id === id && b.date === format(d, 'y-MM-dd', DATE_FNS_OPTIONS)
            );

            // Not in range
            days.push({
              date: d,
              isBookable: false,
              booking,
            });
          }
        });

        // Add to rows
        rows.push({
          week: w,
          start: startDate,
          end: endDate,
          days,
        });
      });

      setRows(rows);
    }
  }, [bookings, office, user, weeks]);

  useEffect(() => {
    if (user && config?.reasonToBookRequired) {
      // Get selected user
      getUser(user?.email)
        .then((searchedUser) => setSearchedUser(searchedUser))
        .catch((err) => {
          // Handle errors

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [user, dispatch, config?.reasonToBookRequired]);

  useEffect(() => {
    if (user) {
      if (searchedUser?.autoApproved) {
        setIsAutoApprovedUser(true);
      }
    }
  }, [searchedUser?.autoApproved, user]);

  // Handlers
  const handleChangeWeek = (direction: 'forward' | 'backward') => {
    // Find index of current selected
    if (selectedWeek !== undefined) {
      setSelectedWeek(
        direction === 'forward'
          ? selectedWeek + 1
          : direction === 'backward'
          ? selectedWeek - 1
          : selectedWeek
      );
    }
  };

  const confirmTodayBooking = (
    handleCreateBooking: (date: Date, withParking: boolean) => void,
    date: Date,
    withParking: boolean
  ) => {
    if (isReasonRequired()) {
      setBookingDate(date);
      setBookingParking(withParking);

      if (isToday(date)) {
        setShowTodayConfirmation(true);
      } else {
        setShowReasonConfirmation(true);
      }
    } else {
      handleCreateBooking(date, withParking);
    }
  };

  const handleCreateBooking = (date: Date, withParking: boolean, reason?: string) => {
    if (user) {
      setButtonsLoading(true);

      // Reset auto-reload timer
      resetReloadTimer();

      // Create new booking
      const formattedDate = format(date, 'yyyy-MM-dd', DATE_FNS_OPTIONS);

      createBooking(user.email, formattedDate, office, withParking, reason)
        .then(() => {
          // Clear form
          setBookingDate(undefined);
          setBookingParking(false);
          setBookingReason(undefined);

          // Refresh DB
          refreshBookings();
        })
        .catch((err) => {
          // Refresh DB
          refreshBookings();

          // Handle errors
          setButtonsLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  };

  const handleCancelBooking = (booking: Booking) => {
    if (user) {
      setButtonsLoading(true);

      // Reset auto-reload timer
      resetReloadTimer();

      // Cancel existing booking
      cancelBooking(booking.id, user.email)
        .then(() => refreshBookings())
        .catch((err) => {
          // Refresh DB
          refreshBookings();

          // Handle errors
          setButtonsLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  };

  const handleClearOffice = () => {
    // Update global state
    dispatch({
      type: 'SET_OFFICE',
      payload: undefined,
    });
  };

  // Render
  if (!user) {
    return null;
  }

  return (
    <MakeBookingStyles>
      <div className="title">
        <h2>{office.name}</h2>

        <Link
          component="button"
          underline="always"
          color="primary"
          onClick={() => handleClearOffice()}
        >
          Change office
        </Link>
      </div>

      {isReasonRequired() && (
        <p className="notice">
          You will be asked to record your reason for going to work when you make a booking.
        </p>
      )}

      <ul>
        <li>
          You can make <span>{user.quota}</span> booking per week.
        </li>
        <li>
          {office.name} has a daily capacity of <span>{office.quota}</span>
          {office.parkingQuota > 0 ? (
            <>
              {` `}and car park capacity of <span>{office.parkingQuota}</span>.
            </>
          ) : (
            `.`
          )}
        </li>
      </ul>

      {weeks && weeks.length > 0 && selectedWeek !== undefined && (
        <>
          <Paper square className="bookings">
            <div className="menu">
              <div className="back">
                <IconButton
                  disabled={selectedWeek === 0}
                  onClick={() => handleChangeWeek('backward')}
                  size="small"
                >
                  <ArrowLeftIcon
                    fontSize="inherit"
                    className="icon"
                    color={selectedWeek === 0 ? 'disabled' : 'secondary'}
                  />
                </IconButton>
              </div>

              <div className="date">
                <h3>{getWeekLabel(weeks[selectedWeek])}</h3>
              </div>

              <div className="forward">
                <IconButton
                  disabled={selectedWeek === weeks.length - 1}
                  onClick={() => handleChangeWeek('forward')}
                  size="small"
                >
                  <ArrowRightIcon
                    fontSize="inherit"
                    className="icon"
                    color={selectedWeek === weeks.length - 1 ? 'disabled' : 'secondary'}
                  />
                </IconButton>
              </div>

              <div className="refresh">
                <Tooltip title="Refresh availability">
                  <IconButton
                    onClick={() => {
                      setButtonsLoading(true);

                      resetReloadTimer();
                      refreshBookings();
                    }}
                    disabled={buttonsLoading}
                  >
                    <CachedIcon color="primary" />
                  </IconButton>
                </Tooltip>
              </div>
            </div>

            <div className="details">
              <p className="quota">
                <span>{user.quota - weeks[selectedWeek].bookings}</span>{' '}
                {user.quota - weeks[selectedWeek].bookings === 1 ? 'booking' : 'bookings'} remaining
              </p>

              <p className="upcoming-bookings">
                <Link component="button" color="inherit" onClick={() => navigate('/bookings')}>
                  View bookings
                </Link>
              </p>
            </div>

            {rows
              .filter((row, index) => selectedWeek === index)
              .map((row, weekIndex) => (
                <div key={weekIndex} className="grid">
                  {row.days.map((day, dayIndex) => (
                    <div
                      key={dayIndex}
                      className="row"
                      data-today={isToday(day.date)}
                      data-bookable={
                        day.isBookable && (day.userCanBook || day.booking) ? true : false
                      }
                    >
                      <div className="left">
                        <p className="date">{format(day.date, 'E do', DATE_FNS_OPTIONS)}</p>
                      </div>

                      {day.isBookable && (
                        <div className="right">
                          {day.booking ? (
                            <>
                              <Tooltip
                                title={
                                  isToday(day.date)
                                    ? "Today's booking can only be cancelled by administrators"
                                    : ''
                                }
                                placement="top-end"
                              >
                                <Link
                                  component="button"
                                  underline="always"
                                  className={`${buttonsLoading ? 'loading ' : ''}${
                                    isToday(day.date) ? 'disabled ' : ''
                                  }cancelBtn`}
                                  onClick={() =>
                                    !buttonsLoading &&
                                    day.booking &&
                                    !isToday(day.date) &&
                                    handleCancelBooking(day.booking)
                                  }
                                >
                                  Cancel
                                </Link>
                              </Tooltip>

                              <OurButton
                                size="small"
                                variant="contained"
                                color="primary"
                                onClick={() => {
                                  navigate(`./booking/${day.booking?.id}`);
                                }}
                                endIcon={
                                  day.booking?.parking ? (
                                    <EmojiTransportationIcon />
                                  ) : (
                                    <BusinessIcon />
                                  )
                                }
                              >
                                View Pass
                              </OurButton>
                            </>
                          ) : (
                            <div className="no-booking">
                              <div className="availability">
                                <BookingStatus
                                  officeQuota={office.quota}
                                  officeAvailable={day.available}
                                  parkingQuota={office.parkingQuota}
                                  parkingAvailable={day.availableCarPark}
                                />
                              </div>

                              {day.userCanBook && (
                                <div className="book">
                                  <BookButton
                                    onClick={(withParking) =>
                                      confirmTodayBooking(
                                        handleCreateBooking,
                                        day.date,
                                        withParking
                                      )
                                    }
                                    parkingQuota={office.parkingQuota}
                                    parkingAvailable={day.availableCarPark}
                                    buttonsLoading={buttonsLoading}
                                  />
                                </div>
                              )}
                            </div>
                          )}
                        </div>
                      )}
                    </div>
                  ))}
                </div>
              ))}
          </Paper>

          <Dialog
            open={showTodayConfirmation}
            onClose={() => setShowTodayConfirmation(false)}
            disableBackdropClick
          >
            <DialogContent>
              <DialogContentText color="secondary">
                Today's booking can only be cancelled by administrators
              </DialogContentText>
            </DialogContent>
            <DialogActions>
              <OurButton
                onClick={() => setShowTodayConfirmation(false)}
                size="small"
                color="primary"
              >
                Cancel
              </OurButton>
              <OurButton
                onClick={() => {
                  if (isReasonRequired()) {
                    setShowTodayConfirmation(false);
                    setShowReasonConfirmation(true);
                  } else {
                    bookingDate && handleCreateBooking(bookingDate, bookingParking);
                  }
                }}
                variant="contained"
                size="small"
                color="secondary"
              >
                Confirm{bookingParking ? ` + Parking` : null}
              </OurButton>
            </DialogActions>
          </Dialog>

          <Dialog
            open={showReasonConfirmation}
            onClose={() => setShowReasonConfirmation(false)}
            disableBackdropClick
          >
            <DialogContent>
              <DialogContentText color="secondary">
                You can only leave home for work purposes where it is unreasonable for you to do
                your job from home. Please briefly explain why you cannot work from home.
              </DialogContentText>
              <TextField
                autoFocus
                label="Details"
                type="text"
                margin="normal"
                fullWidth
                multiline
                required
                value={bookingReason}
                onChange={(e) => setBookingReason(e.target.value)}
              />
            </DialogContent>
            <DialogActions>
              <OurButton
                onClick={() => setShowReasonConfirmation(false)}
                size="small"
                color="primary"
              >
                Cancel
              </OurButton>
              <OurButton
                onClick={() => {
                  if (bookingDate && bookingReason && bookingReason.trim().length > 0) {
                    setShowReasonConfirmation(false);
                    handleCreateBooking(bookingDate, bookingParking, bookingReason);
                  }
                }}
                variant="contained"
                size="small"
                color="secondary"
              >
                Confirm{bookingParking ? ` + Parking` : null}
              </OurButton>
            </DialogActions>
          </Dialog>
        </>
      )}
    </MakeBookingStyles>
  );
}
Example #13
Source File: UpcomingBookings.tsx    From office-booker with MIT License 4 votes vote down vote up
UpcomingBookings: React.FC<RouteComponentProps> = () => {
  // Global state
  const { state, dispatch } = useContext(AppContext);

  // Local state
  const [loading, setLoading] = useState(true);
  const [upcomingBookings, setUpcomingBookings] = useState<Booking[] | undefined>();
  const [previousBookings, setPreviousBookings] = useState<Booking[] | undefined>();

  // Effects
  useEffect(() => {
    if (state.user) {
      getBookings({ user: state.user.email })
        .then((data) => {
          // Split for previous and upcoming
          const today = format(startOfDay(new Date()), 'yyyy-MM-dd');

          setUpcomingBookings(data.filter((b) => b.date >= today));
          setPreviousBookings(data.filter((b) => b.date < today));
        })
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [state.user, dispatch]);

  useEffect(() => {
    // Find booking
    if (upcomingBookings && previousBookings) {
      setLoading(false);
    }
  }, [upcomingBookings, previousBookings]);

  // Render
  return (
    <Layout>
      {loading ? (
        <LoadingSpinner />
      ) : (
        <UpcomingBookingsStyles>
          <h2>Upcoming Bookings</h2>

          {upcomingBookings && upcomingBookings.length > 0 ? (
            <Paper square className="bookings">
              {upcomingBookings.map((row, index) => (
                <div key={row.id} className="grid">
                  <div key={index} className="row">
                    <div className="left">
                      <p className="date">
                        {format(
                          parse(row.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
                          'do LLL',
                          DATE_FNS_OPTIONS
                        )}
                      </p>
                      <p className="office">{row.office.name}</p>
                    </div>
                    <div className="right">
                      <OurButton
                        size="small"
                        variant="contained"
                        color="primary"
                        onClick={() => {
                          navigate(`./booking/${row?.id}`);
                        }}
                        endIcon={row.parking ? <EmojiTransportationIcon /> : <BusinessIcon />}
                      >
                        View Pass
                      </OurButton>
                    </div>
                  </div>
                </div>
              ))}
            </Paper>
          ) : (
            <p>No upcoming bookings found.</p>
          )}

          {previousBookings && previousBookings.length > 0 && (
            <>
              <h3>Previous Bookings</h3>

              <ul>
                {previousBookings.map((row) => (
                  <li key={row.id}>
                    {format(
                      parse(row.date, 'yyyy-MM-dd', new Date(), DATE_FNS_OPTIONS),
                      'do LLLL',
                      DATE_FNS_OPTIONS
                    )}
                    {` `}
                    <span>at {row.office.name}</span>
                    {` `}
                    <span>{row.parking ? '(+ Parking)' : ''}</span>
                  </li>
                ))}
              </ul>
            </>
          )}

          <div className="button">
            <OurButton
              type="button"
              variant="contained"
              color="primary"
              onClick={() => {
                navigate(`/`);
              }}
            >
              Home
            </OurButton>
          </div>
        </UpcomingBookingsStyles>
      )}
    </Layout>
  );
}
Example #14
Source File: ViewBooking.tsx    From office-booker with MIT License 4 votes vote down vote up
ViewBooking: React.FC<RouteComponentProps<Props>> = (props) => {
  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { user } = state;

  // Local state
  const [loading, setLoading] = useState(true);
  const [booking, setBooking] = useState<Booking | undefined>(undefined);

  // Effects
  useEffect(() => {
    if (user) {
      getBookings({ user: user.email })
        .then((data) => {
          const findBooking = data.find((b) => b.id === props.id);

          if (findBooking) {
            setBooking(findBooking);
          } else {
            setLoading(false);

            // Bounce back to home
            setTimeout(() => navigate('/'), 2500);
          }
        })
        .catch((err) => {
          // Handle errors
          setLoading(false);

          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          });
        });
    }
  }, [dispatch, user, props.id]);

  useEffect(() => {
    // Finished loading
    if (booking) {
      setLoading(false);
    }
  }, [booking]);

  // Render
  if (!user) {
    return null;
  }

  const emailSplit = user.email.split('@');

  return (
    <Layout>
      {loading ? (
        <LoadingSpinner />
      ) : (
        <ViewBookingStyles>
          {booking ? (
            <Paper square className="card">
              <p className="day">
                {format(
                  parse(booking.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
                  'eeee',
                  DATE_FNS_OPTIONS
                )}
              </p>
              <h2>
                {format(
                  parse(booking.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
                  'do LLLL',
                  DATE_FNS_OPTIONS
                )}
              </h2>

              <h3>{booking.office.name}</h3>
              {booking.parking && <p className="parking">+ parking</p>}

              <div className="breaker"></div>

              <h4>{emailSplit[0]}</h4>
              <p className="domain">@{emailSplit[1]}</p>
            </Paper>
          ) : (
            <p className="not-found">
              Sorry, we can&apos;t find your booking reference. <br />
              Redirecting you to home...
            </p>
          )}

          <div className="button">
            <OurButton
              type="submit"
              variant="contained"
              color="primary"
              onClick={() => {
                navigate(`/`);
              }}
            >
              Home
            </OurButton>
          </div>
        </ViewBookingStyles>
      )}
    </Layout>
  );
}
Example #15
Source File: Footer.tsx    From office-booker with MIT License 4 votes vote down vote up
Footer: React.FC = () => {
  // Global state
  const { state, dispatch } = useContext(AppContext);
  const { user } = state;

  // Effects
  useEffect(() => {
    // Retrieve user if not already available
    if (!user) {
      // Retrieve cognito session
      getAuthState()
        .then((username) => {
          // Retrieve DB user
          if (username) {
            // Retrieve cached user
            getUserCached(username)
              .then((data) =>
                dispatch({
                  type: 'SET_USER',
                  payload: data,
                })
              )
              .catch((err) =>
                dispatch({
                  type: 'SET_ALERT',
                  payload: {
                    message: formatError(err),
                    color: 'error',
                  },
                })
              );
          }
        })
        .catch((err) =>
          dispatch({
            type: 'SET_ALERT',
            payload: {
              message: formatError(err),
              color: 'error',
            },
          })
        );
    }
  }, [dispatch, user]);

  // Handlers
  const handleSignOut = () => {
    // Sign out
    signOut()
      .then(() => {
        dispatch({ type: 'SET_USER', payload: undefined });

        navigate('/');
      })
      .catch((err) =>
        dispatch({
          type: 'SET_ALERT',
          payload: {
            message: formatError(err),
            color: 'error',
          },
        })
      );
  };

  // Render
  return (
    <FooterStyles>
      <div className="link">
        <ReachLink to="/help">Help</ReachLink>
      </div>

      <div className="link">
        <ReachLink to="/privacy">Privacy</ReachLink>
      </div>
      {user?.permissions.canViewAdminPanel && (
        <div className="link">
          <ReachLink to="/admin">Admin</ReachLink>
        </div>
      )}
      {user && (
        <div className="link">
          <Link component="button" color="inherit" onClick={() => handleSignOut()}>
            Sign Out
          </Link>
        </div>
      )}
    </FooterStyles>
  );
}