From 5b7387145108eacc22d23b988fd8c870f035baa5 Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Tue, 23 Jan 2024 19:57:47 -0500 Subject: [PATCH] Bug squashing (#225) * fix email verification bug * fix overflow and flagged checkbox bugs * show employee full name in log record modals * increase z index on navbar so it doesn't overlap with filter components * fix date bugs with home page and csv convertor * touch up signup, login, and user flows for non-admin users * touchup spinner and employee create/edit form limitations * run ze linter * fix employee name display bug --- backend/app/rest/tags_routes.py | 2 +- backend/app/rest/user_routes.py | 12 +- .../implementations/log_records_service.py | 32 +-- .../implementations/residents_service.py | 10 +- .../services/implementations/user_service.py | 6 +- .../exceptions/duplicate_entity_exceptions.py | 10 + ...5c8f324c_add_last_modified_to_residents.py | 39 ++- frontend/src/APIClients/AuthAPIClient.ts | 7 +- frontend/src/APIClients/CommonAPIClient.ts | 65 ----- frontend/src/APIClients/UserAPIClient.ts | 76 +++++ frontend/src/components/auth/PrivateRoute.tsx | 23 +- frontend/src/components/auth/Verification.tsx | 85 ++++-- .../src/components/common/NavigationBar.tsx | 3 +- .../src/components/forms/CreateEmployee.tsx | 80 +++--- frontend/src/components/forms/CreateLog.tsx | 22 +- .../src/components/forms/EditEmployee.tsx | 25 +- frontend/src/components/forms/EditLog.tsx | 50 +--- frontend/src/components/forms/ExportToCSV.tsx | 21 +- frontend/src/components/forms/Login.tsx | 192 +++++++++---- frontend/src/components/forms/Signup.tsx | 226 ++++++++++----- frontend/src/components/forms/ViewLog.tsx | 61 ++-- .../pages/AdminControls/EmployeeDirectory.tsx | 3 +- .../AdminControls/EmployeeDirectoryTable.tsx | 2 +- .../pages/AdminControls/SignInLogs.tsx | 3 +- .../components/pages/AdminControls/Tags.tsx | 3 +- .../pages/AdminControls/TagsTable.tsx | 2 +- .../components/pages/HomePage/HomePage.tsx | 94 ++++-- .../pages/HomePage/HomePageFilters.tsx | 21 +- .../pages/HomePage/LogRecordsTable.tsx | 27 +- .../pages/ResidentDirectory/Filters.tsx | 268 ------------------ .../ResidentDirectory/ResidentDirectory.tsx | 13 +- .../ResidentDirectoryFilters.tsx | 2 +- frontend/src/helper/CSVConverter.tsx | 2 +- frontend/src/helper/combineDateTime.ts | 14 - frontend/src/helper/dateHelpers.ts | 20 +- frontend/src/helper/error.ts | 12 +- frontend/src/theme/common/spinnerStyles.tsx | 1 - frontend/src/theme/common/tableStyles.tsx | 4 + frontend/src/types/AuthTypes.ts | 2 +- frontend/src/types/LogRecordTypes.ts | 6 +- frontend/src/types/UserTypes.ts | 5 + 41 files changed, 762 insertions(+), 789 deletions(-) delete mode 100644 frontend/src/APIClients/CommonAPIClient.ts delete mode 100644 frontend/src/components/pages/ResidentDirectory/Filters.tsx delete mode 100644 frontend/src/helper/combineDateTime.ts diff --git a/backend/app/rest/tags_routes.py b/backend/app/rest/tags_routes.py index d2fa427c..36077fb4 100644 --- a/backend/app/rest/tags_routes.py +++ b/backend/app/rest/tags_routes.py @@ -8,7 +8,7 @@ @blueprint.route("/", methods=["GET"], strict_slashes=False) -@require_authorization_by_role({"Admin"}) +@require_authorization_by_role({"Relief Staff", "Regular Staff", "Admin"}) def get_tags(): """ Get tags. diff --git a/backend/app/rest/user_routes.py b/backend/app/rest/user_routes.py index 82f236fb..41a3e9c1 100644 --- a/backend/app/rest/user_routes.py +++ b/backend/app/rest/user_routes.py @@ -13,6 +13,7 @@ from ..services.implementations.user_service import UserService from ..utilities.csv_utils import generate_csv_from_list from ..utilities.exceptions.auth_exceptions import UserNotInvitedException +from ..utilities.exceptions.duplicate_entity_exceptions import DuplicateUserException user_service = UserService(current_app.logger) @@ -136,15 +137,12 @@ def create_user(): user = CreateInvitedUserDTO(**request.json) created_user = user_service.create_invited_user(user) return jsonify(created_user.__dict__), 201 + except DuplicateUserException as e: + error_message = getattr(e, "message", None) + return jsonify({"error": (error_message if error_message else str(e))}), 409 except Exception as e: error_message = getattr(e, "message", None) - status_code = None - if str(e) == "User already exists": - status_code = 409 - - return jsonify({"error": (error_message if error_message else str(e))}), ( - status_code if status_code else 500 - ) + return jsonify({"error": (error_message if error_message else str(e))}), 500 @blueprint.route("/activate-user", methods=["POST"], strict_slashes=False) diff --git a/backend/app/services/implementations/log_records_service.py b/backend/app/services/implementations/log_records_service.py index 3abcc50f..5d381a65 100644 --- a/backend/app/services/implementations/log_records_service.py +++ b/backend/app/services/implementations/log_records_service.py @@ -5,7 +5,7 @@ from ...models.tags import Tag from ...models import db from datetime import datetime -from pytz import timezone +from pytz import timezone, utc from sqlalchemy import text @@ -32,6 +32,10 @@ def add_record(self, log_record): del new_log_record["residents"] del new_log_record["tags"] + new_log_record["datetime"] = datetime.fromisoformat( + new_log_record["datetime"].replace("Z", "+00:00") + ).replace(tzinfo=utc) + try: new_log_record = LogRecords(**new_log_record) self.construct_residents(new_log_record, residents) @@ -85,7 +89,7 @@ def to_json_list(self, logs): "tags": log[10] if log[10] else [], "note": log[11], "flagged": log[12], - "datetime": str(log[13].astimezone(timezone("US/Eastern"))), + "datetime": log[13].isoformat(), } ) return logs_list @@ -128,21 +132,15 @@ def filter_by_attn_tos(self, attn_tos): def filter_by_date_range(self, date_range): sql = "" - if len(date_range) > 0: - if date_range[0] != "": - start_date = datetime.strptime(date_range[0], "%Y-%m-%d").replace( - hour=0, minute=0 - ) - sql += f"\ndatetime>='{start_date}'" - if date_range[-1] != "": - end_date = datetime.strptime( - date_range[len(date_range) - 1], "%Y-%m-%d" - ).replace(hour=23, minute=59) - - if sql == "": - sql += f"\ndatetime<='{end_date}'" - else: - sql += f"\nAND datetime<='{end_date}'" + if date_range[0] is not None: + start_date = date_range[0].replace("Z", "+00:00") + sql += f"\ndatetime>='{start_date}'" + if date_range[-1] is not None: + end_date = date_range[-1].replace("Z", "+00:00") + if sql == "": + sql += f"\ndatetime<='{end_date}'" + else: + sql += f"\nAND datetime<='{end_date}'" return sql def filter_by_tags(self, tags): diff --git a/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index 210246ac..9436c913 100644 --- a/backend/app/services/implementations/residents_service.py +++ b/backend/app/services/implementations/residents_service.py @@ -205,11 +205,15 @@ def get_residents(self, return_all, page_number, results_per_page, filters=None) ) if not return_all: - residents_results = residents_results.order_by(Residents.last_modified.desc()).limit(results_per_page).offset( - (page_number - 1) * results_per_page + residents_results = ( + residents_results.order_by(Residents.last_modified.desc()) + .limit(results_per_page) + .offset((page_number - 1) * results_per_page) ) else: - residents_results = residents_results.order_by(Residents.last_modified.desc()).all() + residents_results = residents_results.order_by( + Residents.last_modified.desc() + ).all() return { "residents": self.to_residents_json_list( diff --git a/backend/app/services/implementations/user_service.py b/backend/app/services/implementations/user_service.py index a595e49f..c7273aad 100644 --- a/backend/app/services/implementations/user_service.py +++ b/backend/app/services/implementations/user_service.py @@ -8,6 +8,7 @@ UserNotInvitedException, EmailAlreadyInUseException, ) +from ...utilities.exceptions.duplicate_entity_exceptions import DuplicateUserException class UserService(IUserService): @@ -197,10 +198,11 @@ def create_invited_user(self, user): db.session.add(user_entry) db.session.commit() else: - raise Exception("User already exists") - user_dict = UserService.__user_to_dict_and_remove_auth_id(user_entry) + raise DuplicateUserException(user.email) + user_dict = UserService.__user_to_dict_and_remove_auth_id(user_entry) return UserDTO(**user_dict) + except Exception as e: db.session.rollback() reason = getattr(e, "message", None) diff --git a/backend/app/utilities/exceptions/duplicate_entity_exceptions.py b/backend/app/utilities/exceptions/duplicate_entity_exceptions.py index 96b3b457..02334dc6 100644 --- a/backend/app/utilities/exceptions/duplicate_entity_exceptions.py +++ b/backend/app/utilities/exceptions/duplicate_entity_exceptions.py @@ -6,3 +6,13 @@ class DuplicateTagException(Exception): def __init__(self, tag_name): message = f"Tag with name {tag_name} already exists." super().__init__(message) + + +class DuplicateUserException(Exception): + """ + Raised when an duplicate user is encountered + """ + + def __init__(self, email): + message = f"User with email {email} already exists." + super().__init__(message) diff --git a/backend/migrations/versions/a1f05c8f324c_add_last_modified_to_residents.py b/backend/migrations/versions/a1f05c8f324c_add_last_modified_to_residents.py index 54812617..c7b9d9e4 100644 --- a/backend/migrations/versions/a1f05c8f324c_add_last_modified_to_residents.py +++ b/backend/migrations/versions/a1f05c8f324c_add_last_modified_to_residents.py @@ -10,31 +10,42 @@ # revision identifiers, used by Alembic. -revision = 'a1f05c8f324c' -down_revision = '117790caec65' +revision = "a1f05c8f324c" +down_revision = "117790caec65" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('log_record_tag', schema=None) as batch_op: - batch_op.drop_constraint('log_record_tag_tag_id_fkey', type_='foreignkey') - batch_op.create_foreign_key(None, 'tags', ['tag_id'], ['tag_id'], ondelete='CASCADE') - - with op.batch_alter_table('residents', schema=None) as batch_op: - batch_op.add_column(sa.Column('last_modified', sa.DateTime(), server_default=sa.text('now()'), nullable=False)) + with op.batch_alter_table("log_record_tag", schema=None) as batch_op: + batch_op.drop_constraint("log_record_tag_tag_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + None, "tags", ["tag_id"], ["tag_id"], ondelete="CASCADE" + ) + + with op.batch_alter_table("residents", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "last_modified", + sa.DateTime(), + server_default=sa.text("now()"), + nullable=False, + ) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('residents', schema=None) as batch_op: - batch_op.drop_column('last_modified') - - with op.batch_alter_table('log_record_tag', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('log_record_tag_tag_id_fkey', 'tags', ['tag_id'], ['tag_id']) + with op.batch_alter_table("residents", schema=None) as batch_op: + batch_op.drop_column("last_modified") + + with op.batch_alter_table("log_record_tag", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="foreignkey") + batch_op.create_foreign_key( + "log_record_tag_tag_id_fkey", "tags", ["tag_id"], ["tag_id"] + ) # ### end Alembic commands ### diff --git a/frontend/src/APIClients/AuthAPIClient.ts b/frontend/src/APIClients/AuthAPIClient.ts index 571040ba..f71ae912 100644 --- a/frontend/src/APIClients/AuthAPIClient.ts +++ b/frontend/src/APIClients/AuthAPIClient.ts @@ -35,7 +35,7 @@ const login = async ( } return { errCode: 500, - errMessage: "Error logging in. Please try again.", + errMessage: "Unable to login. Please try again.", }; } }; @@ -112,7 +112,10 @@ const register = async ( errMessage: getAuthErrMessage(axiosErr.response, "SIGNUP"), }; } - return null; + return { + errCode: 500, + errMessage: "Error signing up. Please try again.", + }; } }; diff --git a/frontend/src/APIClients/CommonAPIClient.ts b/frontend/src/APIClients/CommonAPIClient.ts deleted file mode 100644 index da6a4a0e..00000000 --- a/frontend/src/APIClients/CommonAPIClient.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { AxiosError } from "axios"; -import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; -import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; -import baseAPIClient from "./BaseAPIClient"; -import { AuthErrorResponse } from "../types/ErrorTypes"; - -const inviteUser = async ( - email: string, - role: string, - firstName: string, - lastName: string, -): Promise => { - try { - const bearerToken = `Bearer ${getLocalStorageObjProperty( - AUTHENTICATED_USER_KEY, - "accessToken", - )}`; - await baseAPIClient.post( - "/users/invite-user", - { email, role, firstName, lastName }, - { headers: { Authorization: bearerToken } }, - ); - return "Success"; - } catch (error: any) { - return error.message; - } -}; - -const getUserStatus = async ( - email: string, -): Promise => { - try { - if (email === "") { - return ""; - } - const bearerToken = `Bearer ${getLocalStorageObjProperty( - AUTHENTICATED_USER_KEY, - "accessToken", - )}`; - const { data } = await baseAPIClient.get("/users/user-status", { - params: { - email, - }, - headers: { Authorization: bearerToken }, - }); - if (data?.email === email) { - return data.userStatus; - } - return "Not invited"; - } catch (error) { - const axiosErr = (error as any) as AxiosError; - if (axiosErr.response && axiosErr.response.status === 403) { - return { - errCode: axiosErr.response.status, - errMessage: axiosErr.response.data.error, - }; - } - return "Not invited"; - } -}; - -export default { - inviteUser, - isUserInvited: getUserStatus, -}; diff --git a/frontend/src/APIClients/UserAPIClient.ts b/frontend/src/APIClients/UserAPIClient.ts index c7c8137d..65be56ae 100644 --- a/frontend/src/APIClients/UserAPIClient.ts +++ b/frontend/src/APIClients/UserAPIClient.ts @@ -7,7 +7,9 @@ import { CountUsersResponse, UpdateUserParams, UserStatus, + GetUserStatusResponse, } from "../types/UserTypes"; +import { ErrorResponse } from "../types/ErrorTypes"; const getUsers = async ({ returnAll = false, @@ -124,10 +126,84 @@ const deleteUser = async (userId: number): Promise => { } }; +const inviteUser = async ( + email: string, + role: string, + firstName: string, + lastName: string, +): Promise => { + try { + const bearerToken = `Bearer ${getLocalStorageObjProperty( + AUTHENTICATED_USER_KEY, + "accessToken", + )}`; + await baseAPIClient.post( + "/users/invite-user", + { email, role, firstName, lastName }, + { headers: { Authorization: bearerToken } }, + ); + return true; + } catch (error) { + const axiosErr = (error as any) as AxiosError; + + if (axiosErr.response && axiosErr.response.status === 409) { + return { + errMessage: + axiosErr.response.data.error ?? + "User with the specified email already exists.", + }; + } + return false; + } +}; + +const getUserStatus = async ( + email: string, +): Promise => { + try { + const bearerToken = `Bearer ${getLocalStorageObjProperty( + AUTHENTICATED_USER_KEY, + "accessToken", + )}`; + const { data } = await baseAPIClient.get( + "/users/user-status", + { + params: { + email, + }, + headers: { Authorization: bearerToken }, + }, + ); + if (data.email === email) { + return data.userStatus; + } + return { + errMessage: + "This email address has not been invited. Please try again with a different email.", + }; + } catch (error) { + const axiosErr = (error as any) as AxiosError; + + if (axiosErr.response && axiosErr.response.status === 403) { + return { + errMessage: + axiosErr.response.data.error ?? + "This email address has not been invited. Please try again with a different email.", + }; + } + + return { + errMessage: "Unable to get status of this user.", + }; + } +}; + export default { getUsers, countUsers, updateUser, updateUserStatus, deleteUser, + inviteUser, + getUserStatus, }; diff --git a/frontend/src/components/auth/PrivateRoute.tsx b/frontend/src/components/auth/PrivateRoute.tsx index ec71e0d1..8849137b 100644 --- a/frontend/src/components/auth/PrivateRoute.tsx +++ b/frontend/src/components/auth/PrivateRoute.tsx @@ -1,7 +1,15 @@ import React, { useContext, useState, useEffect } from "react"; import { Route, Redirect, useLocation } from "react-router-dom"; import AuthContext from "../../contexts/AuthContext"; -import { LOGIN_PAGE, VERIFICATION_PAGE } from "../../constants/Routes"; +import { + EMPLOYEE_DIRECTORY_PAGE, + HOME_PAGE, + LOGIN_PAGE, + SIGN_IN_LOGS_PAGE, + TAGS_PAGE, + VERIFICATION_PAGE, +} from "../../constants/Routes"; +import { UserRole } from "../../types/UserTypes"; type PrivateRouteProps = { component: React.FC; @@ -23,11 +31,22 @@ const PrivateRoute: React.FC = ({ } if (authenticatedUser.verified === false) { - if (!currentPath.endsWith("/verification")) { + if (!currentPath.endsWith(VERIFICATION_PAGE)) { return ; } } + if ( + authenticatedUser.role !== UserRole.ADMIN && + (currentPath.endsWith(EMPLOYEE_DIRECTORY_PAGE) || + currentPath.endsWith(SIGN_IN_LOGS_PAGE) || + currentPath.endsWith(TAGS_PAGE)) + ) { + return ; + } + + console.log(path, currentPath); + return ; }; diff --git a/frontend/src/components/auth/Verification.tsx b/frontend/src/components/auth/Verification.tsx index c527127c..97dce6d2 100644 --- a/frontend/src/components/auth/Verification.tsx +++ b/frontend/src/components/auth/Verification.tsx @@ -1,64 +1,103 @@ import React, { useState, useContext } from "react"; import { useHistory } from "react-router-dom"; -import { Box, Button, Flex, Text, VStack } from "@chakra-ui/react"; +import { Box, Button, Flex, Spinner, Text, VStack } from "@chakra-ui/react"; import authAPIClient from "../../APIClients/AuthAPIClient"; import CreateToast from "../common/Toasts"; import AuthContext from "../../contexts/AuthContext"; import { HOME_PAGE } from "../../constants/Routes"; +import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; +import { AuthenticatedUser } from "../../types/AuthTypes"; const Verification = (): React.ReactElement => { const newToast = CreateToast(); const history = useHistory(); const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext); + const [isLoading, setIsLoading] = useState(false); + const handleVerification = async () => { if (authenticatedUser) { - const authUser = authenticatedUser; - authUser.verified = await authAPIClient.isVerified(); - setAuthenticatedUser(authUser); + setIsLoading(true); + const isVerified = await authAPIClient.isVerified(); - if (authenticatedUser.verified === false) { + if (isVerified === false) { + setIsLoading(false); newToast( "Not Verified", "Please check your email for the verification email.", "error", ); } else { + const newAuthenticatedUser: AuthenticatedUser = { + ...authenticatedUser, + verified: true, + }; + + localStorage.setItem( + AUTHENTICATED_USER_KEY, + JSON.stringify(newAuthenticatedUser), + ); + setAuthenticatedUser(newAuthenticatedUser); + history.push(HOME_PAGE); } } }; return ( - <> - + + - + Verification + + + In order to start using your SHOW account, you need to confirm your email address. - - + + + + {isLoading ? ( + + + + ) : ( + + )} + - + + + {/* Background */} + + ); }; diff --git a/frontend/src/components/common/NavigationBar.tsx b/frontend/src/components/common/NavigationBar.tsx index 2e419629..7833d46a 100644 --- a/frontend/src/components/common/NavigationBar.tsx +++ b/frontend/src/components/common/NavigationBar.tsx @@ -65,6 +65,7 @@ const NavigationBar = (): React.ReactElement => { display="flex" justifyContent="center" alignItems="center" + zIndex="3" > { Admin Controls {isMenuOpen && ( - + Employee Directory diff --git a/frontend/src/components/forms/CreateEmployee.tsx b/frontend/src/components/forms/CreateEmployee.tsx index 952cdf16..06c67a8e 100644 --- a/frontend/src/components/forms/CreateEmployee.tsx +++ b/frontend/src/components/forms/CreateEmployee.tsx @@ -21,9 +21,10 @@ import { Text, } from "@chakra-ui/react"; import { AddIcon } from "@chakra-ui/icons"; -import commonApiClient from "../../APIClients/CommonAPIClient"; import { INVITE_EMPLOYEE_ERROR } from "../../constants/ErrorMessages"; import CreateToast from "../common/Toasts"; +import UserAPIClient from "../../APIClients/UserAPIClient"; +import { isErrorResponse } from "../../helper/error"; type Props = { getRecords: (pageNumber: number) => Promise; @@ -32,6 +33,8 @@ type Props = { }; const RoleOptions = ["Relief Staff", "Admin", "Regular Staff"]; +const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + const CreateEmployee = ({ getRecords, setUserPageNum, @@ -44,7 +47,7 @@ const CreateEmployee = ({ const [invitedEmail, setInvitedEmail] = useState(""); const [invitedFirstName, setInvitedFirstName] = useState(""); const [invitedLastName, setInvitedLastName] = useState(""); - const [invitedAdminStatus, setinvitedAdminStatus] = useState(""); + const [invitedAdminStatus, setInvitedAdminStatus] = useState(""); const [ isTwoFactorAuthenticated, setIsTwoFactorAuthenticated, @@ -70,30 +73,26 @@ const CreateEmployee = ({ } }, [invitedAdminStatus]); - const handleFirstNameChange = (e: { target: { value: unknown } }) => { + const handleFirstNameChange = (e: React.ChangeEvent) => { const inputValue = e.target.value as string; - if (/^[a-z]{0,}$/i.test(inputValue)) { - setInvitedFirstName(inputValue); - setInvitedFirstNameError(false); - } + setInvitedFirstName(inputValue); + setInvitedFirstNameError(false); }; - const handleLastNameChange = (e: { target: { value: unknown } }) => { + const handleLastNameChange = (e: React.ChangeEvent) => { const inputValue = e.target.value as string; - if (/^[a-z]{0,}$/i.test(inputValue)) { - setInvitedLastName(inputValue); - setInvitedLastNameError(false); - } + setInvitedLastName(inputValue); + setInvitedLastNameError(false); }; - const handleInvitedEmailChange = (e: { target: { value: unknown } }) => { + const handleInvitedEmailChange = (e: React.ChangeEvent) => { const inputValue = e.target.value as string; setInvitedEmail(inputValue); setInvitedEmailError(false); }; const handleAdminStatusChange = (inputValue: string) => { - setinvitedAdminStatus(inputValue); + setInvitedAdminStatus(inputValue); // If admin is selected, uncheck the 2FA checkbox if (inputValue === "1") { @@ -107,7 +106,7 @@ const CreateEmployee = ({ setInvitedEmail(""); setInvitedFirstName(""); setInvitedLastName(""); - setinvitedAdminStatus(""); + setInvitedAdminStatus(""); setIsTwoFactorAuthenticated(false); setInvitedEmailError(false); @@ -132,7 +131,6 @@ const CreateEmployee = ({ !isLastNameError && !isAdminStatusError ) { - let hasInvitedUser: string | undefined; let roleOptionIndex: number | undefined; if (invitedAdminStatus === "1") { @@ -144,42 +142,36 @@ const CreateEmployee = ({ } if (roleOptionIndex !== undefined) { - hasInvitedUser = await commonApiClient.inviteUser( + const res = await UserAPIClient.inviteUser( invitedEmail, RoleOptions[roleOptionIndex], invitedFirstName, invitedLastName, ); - } - if (hasInvitedUser === "Request failed with status code 409") { - newToast("Employee already exists", INVITE_EMPLOYEE_ERROR, "error"); - } else if (hasInvitedUser === "Success") { - newToast( - "Invite sent", - `Your invite has been sent to ${invitedFirstName} ${invitedLastName}`, - "success", - ); - getRecords(1); - setUserPageNum(1); - countUsers(); - handleClose(); - } else { - newToast("Error inviting employee", INVITE_EMPLOYEE_ERROR, "error"); + + if (isErrorResponse(res)) { + newToast("Error inviting user", res.errMessage, "error"); + } else if (res) { + newToast( + "Invite sent", + `Your invite has been sent to ${invitedEmail}`, + "success", + ); + getRecords(1); + setUserPageNum(1); + countUsers(); + handleClose(); + } else { + newToast("Error inviting user", "Unable to invite user.", "error"); + } } } }; const handleSubmit = () => { - const isEmailError = !/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test( - invitedEmail, - ); - const onlyLetters = /^[A-Za-z]+$/; - const isFirstNameError = !( - invitedFirstName && onlyLetters.test(invitedFirstName) - ); - const isLastNameError = !( - invitedLastName && onlyLetters.test(invitedLastName) - ); + const isEmailError = !emailRegex.test(invitedEmail); + const isFirstNameError = invitedFirstName === ""; + const isLastNameError = invitedLastName === ""; const isAdminStatusError = invitedAdminStatus === ""; setInvitedEmailError(isEmailError); @@ -225,7 +217,7 @@ const CreateEmployee = ({ onChange={handleFirstNameChange} maxLength={50} /> - First Name is required. + First name is required. @@ -238,7 +230,7 @@ const CreateEmployee = ({ onChange={handleLastNameChange} maxLength={50} /> - Last Name is required. + Last name is required. diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index b1d6b981..59d4ca63 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -41,8 +41,8 @@ import BuildingAPIClient from "../../APIClients/BuildingAPIClient"; import { selectStyle } from "../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../theme/forms/datePickerStyles"; import { Resident } from "../../types/ResidentTypes"; -import combineDateTime from "../../helper/combineDateTime"; import { SelectLabel } from "../../types/SharedTypes"; +import { combineDateTime, getFormattedTime } from "../../helper/dateHelpers"; type Props = { getRecords: (pageNumber: number) => Promise; @@ -100,9 +100,9 @@ const getCurUserSelectOption = () => { const curUser: AuthenticatedUser | null = getLocalStorageObj( AUTHENTICATED_USER_KEY, ); - if (curUser && curUser.firstName && curUser.id) { + if (curUser) { const userId = curUser.id; - return { label: curUser.firstName, value: userId }; + return { label: `${curUser.firstName} ${curUser.lastName}`, value: userId }; } return { label: "", value: -1 }; }; @@ -241,7 +241,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { const userLabels: SelectLabel[] = usersData.users .filter((user) => user.userStatus === "Active") .map((user) => ({ - label: user.firstName, + label: `${user.firstName} ${user.lastName}`, value: user.id, })); setEmployeeOptions(userLabels); @@ -264,13 +264,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { // reset all states setDate(new Date()); - setTime( - new Date().toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }), - ); + setTime(getFormattedTime(new Date())); setBuildingId(-1); setResidents([]); setTags([]); @@ -374,10 +368,10 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { Employee - diff --git a/frontend/src/components/forms/ExportToCSV.tsx b/frontend/src/components/forms/ExportToCSV.tsx index 066b98be..73a48ad5 100644 --- a/frontend/src/components/forms/ExportToCSV.tsx +++ b/frontend/src/components/forms/ExportToCSV.tsx @@ -55,15 +55,6 @@ const ExportToCSV = (): React.ReactElement => { setOpen(false); }; - const formatDate = (date: Date | undefined) => { - if (date !== undefined) { - return date - .toLocaleString("fr-CA", { timeZone: "America/Toronto" }) - .substring(0, 10); - } - return ""; - }; - const handleSubmit = async () => { if (startDate && endDate && startDate > endDate) { setDateError(true); @@ -71,10 +62,18 @@ const ExportToCSV = (): React.ReactElement => { } setDateError(false); - const dateRange = [formatDate(startDate), formatDate(endDate)]; + let dateRange; + if (startDate || endDate) { + startDate?.setHours(0, 0, 0, 0); + endDate?.setHours(23, 59, 59, 999); + dateRange = [ + startDate ? startDate.toISOString() : null, + endDate ? endDate.toISOString() : null, + ]; + } const data = await LogRecordAPIClient.filterLogRecords({ - dateRange: dateRange[0] === "" && dateRange[1] === "" ? [] : dateRange, + dateRange, returnAll: true, // return all data }); diff --git a/frontend/src/components/forms/Login.tsx b/frontend/src/components/forms/Login.tsx index f8626327..8b761e39 100644 --- a/frontend/src/components/forms/Login.tsx +++ b/frontend/src/components/forms/Login.tsx @@ -7,6 +7,7 @@ import { FormControl, FormErrorMessage, Input, + Spinner, } from "@chakra-ui/react"; import { Redirect, useHistory } from "react-router-dom"; import authAPIClient from "../../APIClients/AuthAPIClient"; @@ -17,10 +18,9 @@ import { VERIFICATION_PAGE, } from "../../constants/Routes"; import AuthContext from "../../contexts/AuthContext"; -import { AuthTokenResponse } from "../../types/AuthTypes"; -import { AuthErrorResponse } from "../../types/ErrorTypes"; -import commonApiClient from "../../APIClients/CommonAPIClient"; -import { isAuthErrorResponse } from "../../helper/error"; +import { isAuthErrorResponse, isErrorResponse } from "../../helper/error"; +import UserAPIClient from "../../APIClients/UserAPIClient"; +import { UserStatus } from "../../types/UserTypes"; type CredentialsProps = { email: string; @@ -32,6 +32,8 @@ type CredentialsProps = { setToggle: (toggle: boolean) => void; }; +const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + const Login = ({ email, setEmail, @@ -43,49 +45,95 @@ const Login = ({ }: CredentialsProps): React.ReactElement => { const { setAuthenticatedUser } = useContext(AuthContext); const history = useHistory(); + const [emailError, setEmailError] = useState(false); - const [passwordError, setPasswordError] = useState(false); - const [passwordErrorStr, setPasswordErrStr] = useState(""); + const [emailErrorStr, setEmailErrorStr] = useState(""); + + const [generalError, setGeneralError] = useState(false); + const [generalErrorStr, setGeneralErrorStr] = useState(""); + const [loginClicked, setLoginClicked] = useState(false); + const [isLoading, setIsLoading] = useState(false); const handleEmailChange = (e: React.ChangeEvent) => { const inputValue = e.target.value as string; - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + setEmail(inputValue); + if (loginClicked) { if (emailRegex.test(inputValue)) { + setEmailErrorStr(""); setEmailError(false); } else { + setEmailErrorStr("Please enter a valid email."); setEmailError(true); } - } - setEmail(inputValue); - // Clear password error on changing the email - setPasswordError(false); - setPasswordErrStr(""); + // Clear general error on changing the email + setGeneralError(false); + setGeneralErrorStr(""); + } }; const handlePasswordChange = (e: React.ChangeEvent) => { const inputValue = e.target.value as string; setPassword(inputValue); - // Clear password error on changing the password - setPasswordError(false); - setPasswordErrStr(""); + if (loginClicked) { + if (inputValue.length === 0) { + setGeneralError(true); + setGeneralErrorStr("Password is required."); + } else { + setGeneralError(false); + setGeneralErrorStr(""); + } + } }; - const onLogInClick = async () => { + const onLoginClick = async () => { setLoginClicked(true); - const isInvited = await commonApiClient.isUserInvited(email); - if (isInvited !== "Not Invited") { - const loginResponse: - | AuthTokenResponse - | AuthErrorResponse = await authAPIClient.login(email, password); + + if (emailError || generalError) { + return; + } + + if (!emailRegex.test(email)) { + setEmailErrorStr("Please enter a valid email."); + setEmailError(true); + return; + } + + if (password.length === 0) { + setGeneralError(true); + setGeneralErrorStr("Password is required."); + return; + } + + setIsLoading(true); + const res = await UserAPIClient.getUserStatus(email); + if (isErrorResponse(res)) { + setGeneralError(true); + setGeneralErrorStr(res.errMessage); + setIsLoading(false); + } else if (res === UserStatus.DEACTIVATED) { + setGeneralError(true); + setGeneralErrorStr( + "This email address has been deactivated. Please try again with another email.", + ); + setIsLoading(false); + } else if (res === UserStatus.INVITED) { + setGeneralError(true); + setGeneralErrorStr( + "This email address has been invited. Sign up first to make an account!", + ); + setIsLoading(false); + } else if (res === UserStatus.ACTIVE) { + const loginResponse = await authAPIClient.login(email, password); if (isAuthErrorResponse(loginResponse)) { - setPasswordError(true); - setPasswordErrStr(loginResponse.errMessage); - } else if (loginResponse) { - const { requiresTwoFa, authUser } = loginResponse; + setGeneralError(true); + setGeneralErrorStr(loginResponse.errMessage); + setIsLoading(false); + } else { + const { authUser, requiresTwoFa } = loginResponse; if (requiresTwoFa) { setToggle(!toggle); } else { @@ -96,6 +144,10 @@ const Login = ({ setAuthenticatedUser(authUser); } } + } else { + setGeneralError(true); + setGeneralErrorStr("Unable to login. Please try again."); + setIsLoading(false); } }; @@ -105,13 +157,19 @@ const Login = ({ if (toggle) { return ( - + - - - - Log In - + + + Log In + + - Please enter a valid email. + {emailErrorStr} - + + + - {passwordErrorStr} + {generalErrorStr} - - - - - Not a member yet? - - - Sign Up Now - + + + {isLoading ? ( + + - - + ) : ( + + )} + + + + + Not a member yet? + + + Sign Up Now + + + diff --git a/frontend/src/components/forms/Signup.tsx b/frontend/src/components/forms/Signup.tsx index 64231df4..b4f49d57 100644 --- a/frontend/src/components/forms/Signup.tsx +++ b/frontend/src/components/forms/Signup.tsx @@ -7,14 +7,16 @@ import { FormControl, FormErrorMessage, Input, + Spinner, Text, } from "@chakra-ui/react"; import authAPIClient from "../../APIClients/AuthAPIClient"; import { HOME_PAGE, LOGIN_PAGE } from "../../constants/Routes"; import AuthContext from "../../contexts/AuthContext"; -import commonApiClient from "../../APIClients/CommonAPIClient"; import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; -import { isAuthErrorResponse } from "../../helper/error"; +import { isAuthErrorResponse, isErrorResponse } from "../../helper/error"; +import UserAPIClient from "../../APIClients/UserAPIClient"; +import { UserStatus } from "../../types/UserTypes"; type SignupProps = { email: string; @@ -44,16 +46,51 @@ const Signup = ({ setToggle, }: SignupProps): React.ReactElement => { const [signupClicked, setSignupClicked] = useState(false); + + const [firstNameError, setFirstNameError] = useState(false); + const [lastNameError, setLastNameError] = useState(false); + const [emailError, setEmailError] = useState(false); const [emailErrorStr, setEmailErrorStr] = useState(""); - const [passwordError, setPasswordError] = useState(false); - const [passwordErrorStr, setPasswordErrorStr] = useState(""); + + const [generalError, setGeneralError] = useState(false); + const [generalErrorStr, setGeneralErrorStr] = useState(""); + + const [isLoading, setIsLoading] = useState(false); const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext); const history = useHistory(); + const handleFirstNameChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value as string; + setFirstName(inputValue); + + if (signupClicked) { + if (inputValue.length === 0) { + setFirstNameError(true); + } else { + setFirstNameError(false); + } + } + }; + + const handleLastNameChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value as string; + setLastName(inputValue); + + if (signupClicked) { + if (inputValue.length === 0) { + setLastNameError(true); + } else { + setLastNameError(false); + } + } + }; + const handleEmailChange = (e: React.ChangeEvent) => { const inputValue = e.target.value as string; + setEmail(inputValue); + if (signupClicked) { if (emailRegex.test(inputValue)) { setEmailErrorStr(""); @@ -62,8 +99,11 @@ const Signup = ({ setEmailErrorStr("Please enter a valid email."); setEmailError(true); } + + // Clear general error on changing the email + setGeneralError(false); + setGeneralErrorStr(""); } - setEmail(inputValue); }; const handlePasswordChange = (e: React.ChangeEvent) => { @@ -72,11 +112,11 @@ const Signup = ({ if (signupClicked) { if (inputValue.length >= 6) { - setPasswordErrorStr(""); - setPasswordError(false); + setGeneralErrorStr(""); + setGeneralError(false); } else { - setPasswordErrorStr("Password must be 6 characters long."); - setPasswordError(true); + setGeneralErrorStr("Password must be at least 6 characters long."); + setGeneralError(true); } } }; @@ -84,6 +124,20 @@ const Signup = ({ const onSignupClick = async () => { setSignupClicked(true); + if (firstNameError || lastNameError || emailError || generalError) { + return; + } + + if (firstName.length === 0) { + setFirstNameError(true); + return; + } + + if (lastName.length === 0) { + setLastNameError(true); + return; + } + if (!emailRegex.test(email)) { setEmailErrorStr("Please enter a valid email."); setEmailError(true); @@ -91,52 +145,58 @@ const Signup = ({ } if (password.length < 6) { - setPasswordErrorStr("Password must be 6 characters long."); - setPasswordError(true); + setGeneralErrorStr("Password must be at least 6 characters long."); + setGeneralError(true); return; } - const isInvited = await commonApiClient.isUserInvited(email); - if (isInvited !== "Not Invited") { - if (isAuthErrorResponse(isInvited)) { - setEmailErrorStr(isInvited.errMessage); + setIsLoading(true); + const res = await UserAPIClient.getUserStatus(email); + + if (isErrorResponse(res)) { + setGeneralError(true); + setGeneralErrorStr(res.errMessage); + setIsLoading(false); + } else if (res === UserStatus.DEACTIVATED) { + setGeneralError(true); + setGeneralErrorStr( + "This email address has been deactivated. Please try again with another email.", + ); + setIsLoading(false); + } else if (res === UserStatus.ACTIVE) { + setGeneralError(true); + setGeneralErrorStr("This email address is already active. Log in now!"); + setIsLoading(false); + } else if (res === UserStatus.INVITED) { + const registerResponse = await authAPIClient.register( + firstName, + lastName, + email, + password, + ); + if (isAuthErrorResponse(registerResponse)) { + setEmailErrorStr(registerResponse.errMessage); setEmailError(true); + setIsLoading(false); } else { - const registerResponse = await authAPIClient.register( - firstName, - lastName, - email, - password, - ); - if (registerResponse) { - if (isAuthErrorResponse(registerResponse)) { - setEmailErrorStr(registerResponse.errMessage); - setEmailError(true); - } else { - const { requiresTwoFa, authUser } = registerResponse; - if (requiresTwoFa) { - setToggle(!toggle); - } else { - localStorage.setItem( - AUTHENTICATED_USER_KEY, - JSON.stringify(authUser), - ); - setAuthenticatedUser(authUser); - } - } + const { requiresTwoFa, authUser } = registerResponse; + if (requiresTwoFa) { + setToggle(!toggle); + } else { + localStorage.setItem( + AUTHENTICATED_USER_KEY, + JSON.stringify(authUser), + ); + setAuthenticatedUser(authUser); } } + } else { + setGeneralError(true); + setGeneralErrorStr("Unable to sign up. Please try again."); + setIsLoading(false); } }; - const isCreateAccountBtnDisabled = () => - emailError || - passwordError || - email === "" || - password === "" || - firstName === "" || - lastName === ""; - const onLogInClick = () => { history.push(LOGIN_PAGE); }; @@ -160,20 +220,26 @@ const Signup = ({ Sign Up - setFirstName(event.target.value)} - /> + + + First name is required. + - setLastName(event.target.value)} - /> + + + Last name is required. + @@ -187,7 +253,7 @@ const Signup = ({ - + - {passwordErrorStr} + {generalErrorStr} - + {isLoading ? ( + + + + ) : ( + + )} diff --git a/frontend/src/components/forms/ViewLog.tsx b/frontend/src/components/forms/ViewLog.tsx index e1b80d78..ecab0f84 100644 --- a/frontend/src/components/forms/ViewLog.tsx +++ b/frontend/src/components/forms/ViewLog.tsx @@ -28,16 +28,16 @@ import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; import { viewStyle} from "../../theme/forms/selectStyles"; import { LogRecord } from "../../types/LogRecordTypes"; import { SelectLabel } from "../../types/SharedTypes"; +import { convertToString, getFormattedTime } from "../../helper/dateHelpers"; type Props = { logRecord: LogRecord; isOpen: boolean; toggleClose: () => void; toggleEdit: () => void; - employeeOptions: SelectLabel[]; residentOptions: SelectLabel[]; - buildingOptions: SelectLabel[]; tagOptions: SelectLabel[]; + allowEdit: boolean; }; // Helper to get the currently logged in user @@ -45,8 +45,8 @@ const getCurUserSelectOption = () => { const curUser: AuthenticatedUser | null = getLocalStorageObj( AUTHENTICATED_USER_KEY, ); - if (curUser && curUser.firstName) { - return curUser.firstName + if (curUser) { + return `${curUser.firstName} ${curUser.lastName}` } return ""; }; @@ -56,47 +56,16 @@ const ViewLog = ({ isOpen, toggleClose, toggleEdit, - employeeOptions, residentOptions, - buildingOptions, tagOptions, + allowEdit }: Props) => { - const [date, setDate] = useState(new Date()); - const [time, setTime] = useState( - date.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }), - ); - - const initializeValues = () => { - // set state variables - setDate(new Date(logRecord.datetime)); - setTime( - date.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }), - ); - }; - - const formatDate = (dateObj: Date) => { - return dateObj.toISOString().slice(0, 10); - } const handleEdit = () => { toggleClose(); setTimeout(toggleEdit, 400); } - useEffect(() => { - if (isOpen) { - initializeValues(); - } - }, [isOpen]); - return ( <> @@ -113,7 +82,7 @@ const ViewLog = ({ Employee @@ -126,7 +95,7 @@ const ViewLog = ({ Date @@ -139,7 +108,7 @@ const ViewLog = ({ isDisabled size="md" type="time" - defaultValue={time} + defaultValue={getFormattedTime(new Date(logRecord.datetime))} _disabled={{ bg: "transparent" }} _hover={{ borderColor: "teal.100" }} /> @@ -199,7 +168,7 @@ const ViewLog = ({ @@ -228,12 +197,16 @@ const ViewLog = ({ - + - + { + allowEdit && ( + + ) + } diff --git a/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx b/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx index 7dd70c84..d04706e4 100644 --- a/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx +++ b/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx @@ -59,7 +59,7 @@ const EmployeeDirectoryPage = (): React.ReactElement => { { speed="0.65s" emptyColor="gray.200" size="xl" + marginTop="5%" /> ) : ( diff --git a/frontend/src/components/pages/AdminControls/EmployeeDirectoryTable.tsx b/frontend/src/components/pages/AdminControls/EmployeeDirectoryTable.tsx index af2f7151..d3ae17c6 100644 --- a/frontend/src/components/pages/AdminControls/EmployeeDirectoryTable.tsx +++ b/frontend/src/components/pages/AdminControls/EmployeeDirectoryTable.tsx @@ -40,7 +40,7 @@ const deactivateConfirmationMessage = (name: string) => const DELETE_CONFIRMATION_HEADER = "Delete Employee"; const deleteConfirmationMessage = (name: string) => - `Are you sure you want to delete ${name}? Deleting an employee will permanently remove it from your system.`; + `Are you sure you want to delete ${name}? Deleting an employee will permanently remove it from the system.`; const constructRole = (user: User): string => { let role = ""; diff --git a/frontend/src/components/pages/AdminControls/SignInLogs.tsx b/frontend/src/components/pages/AdminControls/SignInLogs.tsx index 8b7cff87..935d19c7 100644 --- a/frontend/src/components/pages/AdminControls/SignInLogs.tsx +++ b/frontend/src/components/pages/AdminControls/SignInLogs.tsx @@ -85,7 +85,7 @@ const SignInLogsPage = (): React.ReactElement => { { speed="0.65s" emptyColor="gray.200" size="xl" + marginTop="5%" /> ) : ( diff --git a/frontend/src/components/pages/AdminControls/Tags.tsx b/frontend/src/components/pages/AdminControls/Tags.tsx index 9a3200d9..e8a055e0 100644 --- a/frontend/src/components/pages/AdminControls/Tags.tsx +++ b/frontend/src/components/pages/AdminControls/Tags.tsx @@ -63,7 +63,7 @@ const TagsPage = (): React.ReactElement => { { speed="0.65s" emptyColor="gray.200" size="xl" + marginTop="5%" /> ) : ( diff --git a/frontend/src/components/pages/AdminControls/TagsTable.tsx b/frontend/src/components/pages/AdminControls/TagsTable.tsx index 50c9cac8..aaf000bc 100644 --- a/frontend/src/components/pages/AdminControls/TagsTable.tsx +++ b/frontend/src/components/pages/AdminControls/TagsTable.tsx @@ -32,7 +32,7 @@ type Props = { const DELETE_CONFIRMATION_HEADER = "Delete Tag"; const deleteConfirmationMessage = (name: string) => - `Are you sure you want to delete tag ${name}? Deleting a tag will permanently remove it from your system.`; + `Are you sure you want to delete tag ${name}? Deleting a tag will permanently remove it from the system.`; const TagsTable = ({ tags, diff --git a/frontend/src/components/pages/HomePage/HomePage.tsx b/frontend/src/components/pages/HomePage/HomePage.tsx index 4a02847d..95e3cdf6 100644 --- a/frontend/src/components/pages/HomePage/HomePage.tsx +++ b/frontend/src/components/pages/HomePage/HomePage.tsx @@ -45,22 +45,39 @@ const HomePage = (): React.ReactElement => { // Table reference const tableRef = useRef(null); - const formatDate = (date: Date | undefined) => { - if (date) { - return date - .toLocaleString("fr-CA", { timeZone: "America/Toronto" }) - .substring(0, 10); - } - return ""; - }; - const getLogRecords = async (pageNumber: number) => { - const buildingIds = buildings.map((building) => building.value); - const employeeIds = employees.map((employee) => employee.value); - const attentionToIds = attentionTos.map((attnTo) => attnTo.value); - const residentsIds = residents.map((resident) => resident.value); - const dateRange = [formatDate(startDate), formatDate(endDate)]; - const tagsValues = tags.map((tag) => tag.value); + const buildingIds = + buildings.length > 0 + ? buildings.map((building) => building.value) + : undefined; + + const employeeIds = + employees.length > 0 + ? employees.map((employee) => employee.value) + : undefined; + + const attentionToIds = + attentionTos.length > 0 + ? attentionTos.map((attnTo) => attnTo.value) + : undefined; + + const residentsIds = + residents.length > 0 + ? residents.map((resident) => resident.value) + : undefined; + + const tagIds = tags.length > 0 ? tags.map((tag) => tag.value) : undefined; + + let dateRange; + if (startDate || endDate) { + startDate?.setHours(0, 0, 0, 0); + endDate?.setHours(23, 59, 59, 999); + + dateRange = [ + startDate ? startDate.toISOString() : null, + endDate ? endDate.toISOString() : null, + ]; + } setTableLoaded(false); @@ -68,9 +85,9 @@ const HomePage = (): React.ReactElement => { buildings: buildingIds, employees: employeeIds, attnTos: attentionToIds, - dateRange: dateRange[0] === "" && dateRange[1] === "" ? [] : dateRange, + dateRange, residents: residentsIds, - tags: tagsValues, + tags: tagIds, flagged, resultsPerPage, pageNumber, @@ -92,14 +109,38 @@ const HomePage = (): React.ReactElement => { }; const countLogRecords = async () => { - const buildingIds = buildings.map((building) => building.value); - const employeeIds = employees.map((employee) => employee.value); - const attentionToIds = attentionTos.map((attnTo) => attnTo.value); - const dateRange = - startDate && endDate ? [formatDate(startDate), formatDate(endDate)] : []; - const residentsIds = residents.map((resident) => resident.value); + const buildingIds = + buildings.length > 0 + ? buildings.map((building) => building.value) + : undefined; + + const employeeIds = + employees.length > 0 + ? employees.map((employee) => employee.value) + : undefined; + + const attentionToIds = + attentionTos.length > 0 + ? attentionTos.map((attnTo) => attnTo.value) + : undefined; - const tagsValues = tags.map((tag) => tag.value); + const residentsIds = + residents.length > 0 + ? residents.map((resident) => resident.value) + : undefined; + + const tagIds = tags.length > 0 ? tags.map((tag) => tag.value) : undefined; + + let dateRange; + if (startDate || endDate) { + startDate?.setHours(0, 0, 0, 0); + endDate?.setHours(23, 59, 59, 999); + + dateRange = [ + startDate ? startDate.toISOString() : null, + endDate ? endDate.toISOString() : null, + ]; + } const data = await LogRecordAPIClient.countLogRecords({ buildings: buildingIds, @@ -107,7 +148,7 @@ const HomePage = (): React.ReactElement => { attnTos: attentionToIds, dateRange, residents: residentsIds, - tags: tagsValues, + tags: tagIds, flagged, }); @@ -149,7 +190,7 @@ const HomePage = (): React.ReactElement => { { speed="0.65s" emptyColor="gray.200" size="xl" + marginTop="5%" /> ) : ( diff --git a/frontend/src/components/pages/HomePage/HomePageFilters.tsx b/frontend/src/components/pages/HomePage/HomePageFilters.tsx index 69700e6f..64042338 100644 --- a/frontend/src/components/pages/HomePage/HomePageFilters.tsx +++ b/frontend/src/components/pages/HomePage/HomePageFilters.tsx @@ -208,7 +208,7 @@ const HomePageFilters = ({ }, []); return ( - + FILTER BY @@ -335,7 +335,9 @@ const HomePageFilters = ({ gap={6} > - Tags + + Tags + - Building + + Building + - - - Building - - - - Date - - - - - {startDate && ( - - setStartDate(undefined)} - aria-label="clear" - variant="icon" - icon={ - - } - /> - - )} - - - - - TO - - - - - - {endDate && ( - - setEndDate(undefined)} - aria-label="clear" - variant="icon" - icon={ - - } - /> - - )} - - - - - - - - - ); -}; - -export default Filters; diff --git a/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx b/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx index c84f3070..c605df03 100644 --- a/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx +++ b/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx @@ -30,12 +30,12 @@ const ResidentDirectory = (): React.ReactElement => { [], ); // NOTE: Building 362 will always have ID 2 in the database - const [buildingSelections, setBuildingSelections] = useState( - [{ + const [buildingSelections, setBuildingSelections] = useState([ + { label: "362", - value: 2 - }], - ); + value: 2, + }, + ]); const [statusSelections, setStatusSelections] = useState([]); const [startDate, setStartDate] = useState(); const [endDate, setEndDate] = useState(); @@ -153,7 +153,7 @@ const ResidentDirectory = (): React.ReactElement => { { speed="0.65s" emptyColor="gray.200" size="xl" + marginTop="5%" /> ) : ( diff --git a/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryFilters.tsx b/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryFilters.tsx index 528953c1..c92f6143 100644 --- a/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryFilters.tsx +++ b/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryFilters.tsx @@ -136,7 +136,7 @@ const ResidentDirectoryFilters = ({ }, []); return ( - + FILTER BY diff --git a/frontend/src/helper/CSVConverter.tsx b/frontend/src/helper/CSVConverter.tsx index 1552c2b4..9b0208e5 100644 --- a/frontend/src/helper/CSVConverter.tsx +++ b/frontend/src/helper/CSVConverter.tsx @@ -13,7 +13,7 @@ const convertToCSVLog = (logRecord: LogRecord): CSVLog => { flagged: logRecord.flagged, note: `"${logRecord.note}"`, residents: `"${logRecord.residents.join(", ")}"`, - tags: logRecord.tags != null ? logRecord.tags.join("; ") : "", + tags: logRecord.tags != null ? `"${logRecord.tags.join(", ")}"` : "", }; }; diff --git a/frontend/src/helper/combineDateTime.ts b/frontend/src/helper/combineDateTime.ts deleted file mode 100644 index 8cef82ff..00000000 --- a/frontend/src/helper/combineDateTime.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Combine date and time -const combineDateTime = (dateObj: Date, timeStr: string): Date => { - // Extract time components from timeStr - const [hours, minutes] = timeStr.split(":").map(Number); - - // Create a new Date object with the combined date and time - const newDateObj = new Date(dateObj); - newDateObj.setHours(hours); - newDateObj.setMinutes(minutes); - - return newDateObj; -}; - -export default combineDateTime; diff --git a/frontend/src/helper/dateHelpers.ts b/frontend/src/helper/dateHelpers.ts index 4e97f418..0a32a8b7 100644 --- a/frontend/src/helper/dateHelpers.ts +++ b/frontend/src/helper/dateHelpers.ts @@ -1,5 +1,8 @@ -// Combine date and time -export const combineDateTime = (dateObj: Date, timeStr: string): Date => { +/** + * + * @returns an ISOstring + */ +export const combineDateTime = (dateObj: Date, timeStr: string): string => { // Extract time components from timeStr const [hours, minutes] = timeStr.split(":").map(Number); @@ -8,7 +11,7 @@ export const combineDateTime = (dateObj: Date, timeStr: string): Date => { newDateObj.setHours(hours); newDateObj.setMinutes(minutes); - return newDateObj; + return newDateObj.toISOString(); }; /** @@ -38,3 +41,14 @@ export const convertToString = (date: Date): string => { return `${year}-${month}-${day}`; }; + +/** + * + * @returns date string HH:MM format + */ +export const getFormattedTime = (date: Date): string => { + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + + return `${hours}:${minutes}`; +}; diff --git a/frontend/src/helper/error.ts b/frontend/src/helper/error.ts index fd914a74..05f79440 100644 --- a/frontend/src/helper/error.ts +++ b/frontend/src/helper/error.ts @@ -9,9 +9,9 @@ export const getAuthErrMessage = ( if (axiosErrRes && axiosErrRes.data && axiosErrRes.data.error) { return axiosErrRes.data.error; } - return `Error ${ - flow === "LOGIN" ? "logging in" : "signing up" - }. Please try again later.`; + return `Unable to ${ + flow === "LOGIN" ? "login" : "sign up" + }. Please try again.`; }; export const isAuthErrorResponse = ( @@ -21,7 +21,9 @@ export const isAuthErrorResponse = ( }; export const isErrorResponse = ( - res: boolean | ErrorResponse, + res: boolean | string | ErrorResponse, ): res is ErrorResponse => { - return typeof res !== "boolean" && "errMessage" in res; + return ( + typeof res !== "boolean" && typeof res !== "string" && "errMessage" in res + ); }; diff --git a/frontend/src/theme/common/spinnerStyles.tsx b/frontend/src/theme/common/spinnerStyles.tsx index 5ba5be4d..4e421566 100644 --- a/frontend/src/theme/common/spinnerStyles.tsx +++ b/frontend/src/theme/common/spinnerStyles.tsx @@ -4,7 +4,6 @@ import type { ComponentStyleConfig } from "@chakra-ui/theme"; const Spinner: ComponentStyleConfig = { baseStyle: { color: "teal.400", - marginTop: "5%", }, defaultProps: { size: "xl", diff --git a/frontend/src/theme/common/tableStyles.tsx b/frontend/src/theme/common/tableStyles.tsx index e07cb9b9..6d642aea 100644 --- a/frontend/src/theme/common/tableStyles.tsx +++ b/frontend/src/theme/common/tableStyles.tsx @@ -17,6 +17,10 @@ const Table: ComponentStyleConfig = { thead: { position: "sticky", top: 0, + zIndex: 1, + }, + td: { + wordBreak: "break-word", }, }, }, diff --git a/frontend/src/types/AuthTypes.ts b/frontend/src/types/AuthTypes.ts index 65f2f27b..b9a6b177 100644 --- a/frontend/src/types/AuthTypes.ts +++ b/frontend/src/types/AuthTypes.ts @@ -7,7 +7,7 @@ export type TwoFaResponse = { export type AuthTokenResponse = { requiresTwoFa: boolean; authUser: AuthenticatedUser; -} | null; +}; export type AuthenticatedUser = { id: number; diff --git a/frontend/src/types/LogRecordTypes.ts b/frontend/src/types/LogRecordTypes.ts index 49b834ce..2a2df465 100644 --- a/frontend/src/types/LogRecordTypes.ts +++ b/frontend/src/types/LogRecordTypes.ts @@ -44,7 +44,7 @@ export type CountLogRecordFilters = { buildings?: number[]; employees?: number[]; attnTos?: number[]; - dateRange?: string[]; + dateRange?: (string | null)[]; residents?: number[]; tags?: number[]; flagged?: boolean; @@ -53,7 +53,7 @@ export type CountLogRecordFilters = { export type CreateLogRecordParams = { employeeId: number; residents: number[]; - datetime: Date; + datetime: string; flagged: boolean; note: string; tags: number[]; @@ -65,7 +65,7 @@ export type EditLogRecordParams = { logId: number; employeeId: number; residents: number[]; - datetime: Date; + datetime: string; flagged: boolean; note: string; tags: number[]; diff --git a/frontend/src/types/UserTypes.ts b/frontend/src/types/UserTypes.ts index 51b32730..23b39f09 100644 --- a/frontend/src/types/UserTypes.ts +++ b/frontend/src/types/UserTypes.ts @@ -18,6 +18,11 @@ export type CountUsersResponse = { numResults: number; } | null; +export type GetUserStatusResponse = { + userStatus: UserStatus; + email: string; +}; + export enum UserRole { ADMIN = "Admin", REGULAR_STAFF = "Regular Staff",