From 9e6376400c6f4ca143ad74b66c4a094a19e388e4 Mon Sep 17 00:00:00 2001 From: GabrielChan1 Date: Fri, 16 Aug 2024 19:41:44 -0400 Subject: [PATCH 1/7] Issue 73: Add error messages when servers are unresponsive to the updates and time off pages --- .../SeekPng.com_failure-png_9776670.png | Bin 0 -> 64565 bytes .../PopupComponents/TimeOffRequest.jsx | 258 ++++++++++-------- .../SetupCompanyMenu/SetupDepartmentsMenu.jsx | 2 +- .../NoConnectionComponent.jsx | 39 +++ .../NoConnectionComponent.stories.js | 23 ++ .../NoContentComponent.jsx | 0 .../NoContentLogo.stories.js | 2 +- .../TimeOffPage/BoardTabContent.jsx | 102 ++++--- .../TimeOffPage/HistoryTabContent.jsx | 147 +++++----- src/components/TimeOffPage/TeamTabContent.jsx | 153 ++++++----- src/components/TimeOffPage/TimeOffMenu.jsx | 21 +- src/components/TimeOffPage/TimeOffPage.jsx | 1 + src/components/UpdatesPage/UpdatesMenu.jsx | 91 +++--- 13 files changed, 504 insertions(+), 335 deletions(-) create mode 100644 src/Images/SeekPng.com_failure-png_9776670.png create mode 100644 src/components/StaticComponents/NoConnectionComponent.jsx create mode 100644 src/components/StaticComponents/NoConnectionComponent.stories.js rename src/components/{UpdatesPage => StaticComponents}/NoContentComponent.jsx (100%) rename src/components/{UpdatesPage => StaticComponents}/NoContentLogo.stories.js (92%) diff --git a/src/Images/SeekPng.com_failure-png_9776670.png b/src/Images/SeekPng.com_failure-png_9776670.png new file mode 100644 index 0000000000000000000000000000000000000000..21a9c728b99af126c5d68b59ed6da873d9bc1864 GIT binary patch literal 64565 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 { @@ -112,6 +117,7 @@ export default function TimeOffRequest({ getUpcomingTimeOffDates(); }, []); + //Ensure there is no overlap between existing time off periods useEffect(() => { setValidDates(validateDates()); }, [timeOffDates]); @@ -132,7 +138,7 @@ export default function TimeOffRequest({ } }, [category]); - //Automatically adjust dates to ensure a valid period is set + //Automatically adjust or validate dates to ensure a valid period is set useEffect(() => { if (!isValidPeriod(from, to)) { setTo(from); @@ -151,6 +157,11 @@ export default function TimeOffRequest({ setValidDates(validateDates()); }, [to]); + //Custom style elements + const Header4 = styled("h4")({ + marginBottom: "10px" + }); + const MemoTimeOffTable = memo(TimeOffTable); //ID of the currently logged in employee @@ -166,6 +177,7 @@ export default function TimeOffRequest({ //Retrieve employeeAnnualTimeOff records from database axios.post(timeOffPolicyPOSTURL) .then((response) => { + setServerStatus("Success"); const policies = {}; //We are only interested in the records for the current year const data = response.data.filter((p) => p.year === dayjs().year()); @@ -181,7 +193,12 @@ export default function TimeOffRequest({ //Set each policy as a selectable item in the dropdown setCategoryMenu(Object.values(policies)); }) - .catch((error) => console.log(error)); + .catch((error) => { + console.log(error); + if (!error.response) { + setServerStatus("Failure"); + } + }); }; //Function for retrieving the dates of the upcoming time off periods for ensuring there are @@ -408,6 +425,8 @@ export default function TimeOffRequest({ boxShadow: "0 15px 6px #10182808", paddingX: "36px", paddingY: "30px", + minWidth: "532px", + minHeight: "500px", color: colors.darkGrey, fontFamily: fonts.fontFamily, }, @@ -435,114 +454,129 @@ export default function TimeOffRequest({ }} /> - {/*Set time off category*/} -

Time off category

- `${option.type} - left: ${Math.floor(option.availableHours / 8)} days (${option.availableHours} hours)`} - renderInput={(params) => ( - - )} - value={category} - onChange={(e, newCategory) => setCategory(newCategory)} - sx={{ - width: "100%", - marginBottom: "40px", - }} - /> - {/*Set starting and ending dates of time off period*/} - - -

From

- } - label={formatDate(from)} - variant="outlined" - onClick={() => setOpenFrom(true)} - sx={{ borderRadius: "4px" }} - /> -
- -

To

- } - label={formatDate(to)} - variant="outlined" - onClick={() => setOpenTo(true)} - sx={{ borderRadius: "4px" }} - /> -
-
- {/*Set amount of time off per day*/} -

Amount

- - setEachDay(!eachDay)} - style={{ marginRight: "10px" }} - /> -

Set hours for each day during the time off period

-
- {/*Time off per day table*/} - - {/*Time off summary*/} -

- {fullDaysOff} full days ({fullDaysOff * 8} hrs) and {halfDaysOff} half - day ({halfDaysOff * 4} hrs) will be requested ({totalHoursOff} hrs in - total). -

- {!validDates &&

- Your time off request dates overlap with existing upcoming time off periods. -

} - {!sufficientTime &&

- You are requesting {totalHoursOff} hours off. The selected time off policy only - has {category.availableHours} hours available. -

} - {/*Error message to be displayed if an error occurs*/} - {errorOccurred &&

- An error occurred. Could not send time off request. -

} - {/*Send or cancel*/} - - - Cancel - - {initialRequest ? ( - - Update - - ) : ( - - Send - - )} - - {/*Popup components for setting starting and ending dates*/} - setOpenFrom(false)}> - setOpenFrom(false)} setDate={setFrom} initialValue={from} /> - - setOpenTo(false)}> - setOpenTo(false)} setDate={setTo} initialValue={to} /> - + {serverStatus === "Pending" && + + } + {serverStatus === "Success" && + <> + {/*Set time off category*/} + Time off category + + {/*Set starting and ending dates of time off period*/} + + + From + } + label={formatDate(from)} + variant="outlined" + onClick={() => setOpenFrom(true)} + sx={{ borderRadius: "4px" }} + /> + + + To + } + label={formatDate(to)} + variant="outlined" + onClick={() => setOpenTo(true)} + sx={{ borderRadius: "4px" }} + /> + + + {/*Set amount of time off per day*/} + Amount + + setEachDay(!eachDay)} + style={{ marginRight: "10px" }} + /> +

Set hours for each day during the time off period

+
+ {/*Time off per day table*/} + + {/*Time off summary*/} +

+ {fullDaysOff} full days ({fullDaysOff * 8} hrs) and {halfDaysOff} half + day ({halfDaysOff * 4} hrs) will be requested ({totalHoursOff} hrs in + total). +

+ {!validDates &&

+ Your time off request dates overlap with existing upcoming time off periods. +

} + {!sufficientTime &&

+ You are requesting {totalHoursOff} hours off. The selected time off policy only + has {category.availableHours} hours available. +

} + {/*Error message to be displayed if an error occurs*/} + {errorOccurred &&

+ An error occurred. Could not send time off request. +

} + {/*Send or cancel*/} + + + Cancel + + {initialRequest ? ( + + Update + + ) : ( + + Send + + )} + + {/*Popup components for setting starting and ending dates*/} + setOpenFrom(false)}> + setOpenFrom(false)} setDate={setFrom} initialValue={from} /> + + setOpenTo(false)}> + setOpenTo(false)} setDate={setTo} initialValue={to} /> + + + } + + {/*Error message to be displayed if servers are unresponsive*/} + {serverStatus === "Failure" && + +

Servers are unavailable

+

Cannot send time off requests.

+
+ } ); } diff --git a/src/components/SetupCompanyMenu/SetupDepartmentsMenu.jsx b/src/components/SetupCompanyMenu/SetupDepartmentsMenu.jsx index c965240d7..a0890aea9 100644 --- a/src/components/SetupCompanyMenu/SetupDepartmentsMenu.jsx +++ b/src/components/SetupCompanyMenu/SetupDepartmentsMenu.jsx @@ -3,7 +3,7 @@ import Stack from '@mui/system/Stack'; import SelectItem from './SelectItem'; import HRMButton from '../Button/HRMButton'; import { colors, fonts } from '../../Styles'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import PropTypes from 'prop-types'; const axios = require('axios'); diff --git a/src/components/StaticComponents/NoConnectionComponent.jsx b/src/components/StaticComponents/NoConnectionComponent.jsx new file mode 100644 index 000000000..de81e16d4 --- /dev/null +++ b/src/components/StaticComponents/NoConnectionComponent.jsx @@ -0,0 +1,39 @@ +import Box from '@mui/system/Box'; +import Logo from '../../Images/SeekPng.com_failure-png_9776670.png'; +import { fonts } from '../../Styles'; + +/** + * Component with some text or web elements to be displayed when the servers are unresponsive. + * + * Props: + * - children: Text or web elements to be displayed with the logo. + * + * - style: Optional prop for adding further inline styling. + * Default: {} + */ +export default function NoConnectionComponent({children, style}) { + return ( + + No Connection Logo + {children} + + ); +}; + +//Control panel settings for storybook +NoConnectionComponent.propTypes = {}; + +//Default values for this component +NoConnectionComponent.defaultProps = { + style: {} +}; \ No newline at end of file diff --git a/src/components/StaticComponents/NoConnectionComponent.stories.js b/src/components/StaticComponents/NoConnectionComponent.stories.js new file mode 100644 index 000000000..946be1865 --- /dev/null +++ b/src/components/StaticComponents/NoConnectionComponent.stories.js @@ -0,0 +1,23 @@ +import NoConnectionComponent from './NoConnectionComponent'; + +const content = <> +

Servers are unavailable

+

Cannot retrieve time off policies or periods.

+; + +//Storybook display settings +export default { + title: 'StaticMenus/NoConnectionComponent', + component: NoConnectionComponent, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +}; + +//Stories for each NoConnectionComponent +export const Primary = { + args: { + children: content + } +}; \ No newline at end of file diff --git a/src/components/UpdatesPage/NoContentComponent.jsx b/src/components/StaticComponents/NoContentComponent.jsx similarity index 100% rename from src/components/UpdatesPage/NoContentComponent.jsx rename to src/components/StaticComponents/NoContentComponent.jsx diff --git a/src/components/UpdatesPage/NoContentLogo.stories.js b/src/components/StaticComponents/NoContentLogo.stories.js similarity index 92% rename from src/components/UpdatesPage/NoContentLogo.stories.js rename to src/components/StaticComponents/NoContentLogo.stories.js index 2a610be14..bbefe04b6 100644 --- a/src/components/UpdatesPage/NoContentLogo.stories.js +++ b/src/components/StaticComponents/NoContentLogo.stories.js @@ -8,7 +8,7 @@ const content = <> //Storybook display settings export default { - title: 'HomeMenu/NoContentComponent', + title: 'StaticMenus/NoContentComponent', component: NoContentComponent, parameters: { layout: 'centered' diff --git a/src/components/TimeOffPage/BoardTabContent.jsx b/src/components/TimeOffPage/BoardTabContent.jsx index 0f7317a0d..539833ef3 100644 --- a/src/components/TimeOffPage/BoardTabContent.jsx +++ b/src/components/TimeOffPage/BoardTabContent.jsx @@ -1,9 +1,11 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; +import CircularProgress from '@mui/material/CircularProgress'; import AvailableTimeOffTable from './AvailableTimeOffTable'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; import Label from '../Label/Label'; +import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import { colors, fonts } from '../../Styles'; import { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; @@ -36,6 +38,8 @@ export default function BoardTabContent({style}) { const [timeOffPeriods, setTimeOffPeriods] = useState([]); //Hook for refreshing the list of time off periods const [refresh, setRefresh] = useState(false); + //Flag determining if the database servers can be reached + const [serverStatus, setServerStatus] = useState("Pending"); //ID of the currently logged in employee const currentUser = 1; @@ -54,10 +58,12 @@ export default function BoardTabContent({style}) { //Function for retrieving the time off policies and their respective hours used and available function getTimeOffPolicies() { - //console.log("Running getTimeOffPolicies()"); + //Send Request to database for time off policies axios.post(timeOffPolicyURL) .then((response) => { + setServerStatus("Success"); const policies = {}; + //Only display the information for the current year const data = response.data.filter((p) => p.year === dayjs().year()); data.forEach((p) => { policies[p.category] = { @@ -69,12 +75,16 @@ export default function BoardTabContent({style}) { }); setTimeOffPolicies(policies); }) - .catch((error) => console.log(error)); + .catch((error) => { + console.log(error); + if (!error.response) { + setServerStatus("Failure"); + } + }); } //Function for retrieving any upcoming time off periods function getUpcomingTimeOffPeriods() { - //console.log("Running getUpcomingTimeOffPeriods()"); //Send request to database for time off periods axios.post(timeOffPeriodURL) .then((response) => { @@ -117,43 +127,57 @@ export default function BoardTabContent({style}) { color: colors.darkGrey, fontFamily: fonts.fontFamily }, ...style}}> - {/*Available time off header and table*/} -

Available time offs

- - {/*Upcoming time off header*/} - -

Upcoming time offs

-
- {(timeOffPeriods.length > 0) ? + {serverStatus === "Pending" && + + } + {serverStatus === "Success" && <> - {/*Upcoming time off table*/} - setRefresh(!refresh)} - style={{marginBottom: "30px"}} - /> - {/*Upcoming time off navbar*/} - {timeOffPeriods.length > 10 && - - } - : -

There is no upcoming time off right now.

+ {/*Available time off header and table*/} +

Available time offs

+ + {/*Upcoming time off header*/} + +

Upcoming time offs

+
+ {timeOffPeriods.length > 0 ? + <> + {/*Upcoming time off table*/} + setRefresh(!refresh)} + style={{marginBottom: "30px"}} + /> + {/*Upcoming time off navbar*/} + {timeOffPeriods.length > 10 && + + } + : +

There is no upcoming time off right now.

+ } + + } + {/*Error message to be displayed if servers are unresponsive */} + {serverStatus === "Failure" && + +

Servers are unavailable

+

Cannot retrieve time off policies or periods.

+
} ); diff --git a/src/components/TimeOffPage/HistoryTabContent.jsx b/src/components/TimeOffPage/HistoryTabContent.jsx index 747d0e533..fd79f5095 100644 --- a/src/components/TimeOffPage/HistoryTabContent.jsx +++ b/src/components/TimeOffPage/HistoryTabContent.jsx @@ -1,10 +1,12 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; import TuneIcon from '@mui/icons-material/Tune'; +import CircularProgress from '@mui/material/CircularProgress'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; import MenuToggleButton from '../BasicMenus/MenuToggleButton'; -import NoContentComponent from '../UpdatesPage/NoContentComponent'; +import NoContentComponent from '../StaticComponents/NoContentComponent'; +import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import Label from '../Label/Label'; import { useState, useEffect } from 'react'; import { colors, fonts } from '../../Styles'; @@ -42,6 +44,8 @@ export default function HistoryTabContent({style}) { const [timeOffPeriods, setTimeOffPeriods] = useState([]); //Hook for refreshing the list of time off periods const [refresh, setRefresh] = useState(false); + //Flag determining if the database servers can be reached + const [serverStatus, setServerStatus] = useState("Pending"); //Filter table columns depending on which filters are active //"From", "To" and at least one other column will always be active @@ -67,6 +71,7 @@ export default function HistoryTabContent({style}) { //Send request to database for time off periods axios.post(timeOffPeriodURL) .then((response) => { + setServerStatus("Success") const periods = []; const data = response.data; data.forEach((p) => { @@ -86,6 +91,9 @@ export default function HistoryTabContent({style}) { }) .catch((error) => { console.log(error); + if (!error.response) { + setServerStatus("Failure"); + } }) }; @@ -101,73 +109,88 @@ export default function HistoryTabContent({style}) { return ( - {/*Time off header*/} - - -

Time off history

-
- {/*Customize button*/} - {timeOffPeriods.length > 0 && - { - if (activeFilters.length >= 2 || !typeFilter) {setTypeFilter(value)} - }], - "Amount": [amountFilter, (value) => { - if (activeFilters.length >= 2 || !amountFilter) {setAmountFilter(value)} - }], - "Note": [noteFilter, (value) => { - if (activeFilters.length >= 2 || !noteFilter) {setNoteFilter(value)} - }] - }} - icon={} - /> - } -
- {/*If there are periods of time off, display the time off period list and navbar */} - {(timeOffPeriods.length > 0) ? - <> - {/*Upcoming time off table*/} - setRefresh(!refresh)} - style={{marginBottom: "30px"}} - /> - {/*Upcoming time off navbar*/} - {timeOffPeriods.length > 10 && - - } - : + {serverStatus === "Pending" && + + } + {serverStatus === "Success" && <> - {/*Otherwise, display a message that there is no history*/} - -

There is no time off history

-

Any updates about your time off history will be shown here.

-
+ {/*Time off header*/} + + +

Time off history

+
+ {/*Customize button*/} + {timeOffPeriods.length > 0 && + { + if (activeFilters.length >= 2 || !typeFilter) {setTypeFilter(value)} + }], + "Amount": [amountFilter, (value) => { + if (activeFilters.length >= 2 || !amountFilter) {setAmountFilter(value)} + }], + "Note": [noteFilter, (value) => { + if (activeFilters.length >= 2 || !noteFilter) {setNoteFilter(value)} + }] + }} + icon={} + /> + } +
+ {/*If there are periods of time off, display the time off period list and navbar */} + {(timeOffPeriods.length > 0) ? + <> + {/*Upcoming time off table*/} + setRefresh(!refresh)} + style={{marginBottom: "30px"}} + /> + {/*Upcoming time off navbar*/} + {timeOffPeriods.length > 10 && + + } + : + <> + {/*Otherwise, display a message that there is no history*/} + +

There is no time off history

+

Any updates about your time off history will be shown here.

+
+ + } } + {/*Error message to be displayed if servers are unresponsive*/} + {serverStatus === "Failure" && + +

Servers are unavailable

+

Cannot retrieve time off periods.

+
+ }
); }; diff --git a/src/components/TimeOffPage/TeamTabContent.jsx b/src/components/TimeOffPage/TeamTabContent.jsx index 2b868369d..f9bb9a806 100644 --- a/src/components/TimeOffPage/TeamTabContent.jsx +++ b/src/components/TimeOffPage/TeamTabContent.jsx @@ -1,10 +1,12 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; import FilterListIcon from '@mui/icons-material/FilterList'; +import CircularProgress from '@mui/material/CircularProgress'; import { useState, useEffect } from 'react'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; -import NoContentComponent from '../UpdatesPage/NoContentComponent'; +import NoContentComponent from '../StaticComponents/NoContentComponent'; +import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import MenuToggleButton from '../BasicMenus/MenuToggleButton'; import Label from '../Label/Label'; import { colors, fonts } from '../../Styles'; @@ -42,6 +44,8 @@ export default function TeamTabContent({style}) { const [timeOffPeriods, setTimeOffPeriods] = useState([]); //Hook for refreshing the list of time off periods const [refresh, setRefresh] = useState(false); + //Flag determining if the database servers can be reached + const [serverStatus, setServerStatus] = useState("Pending"); //ID of the currently logged in employee const currentUser = 1; @@ -72,6 +76,7 @@ export default function TeamTabContent({style}) { team.forEach((id) => { axios.post(`${timeOffURL}/${id}`) .then((response) => { + setServerStatus("Success"); data = response.data; data.forEach((p) => { periods.push({ @@ -93,7 +98,12 @@ export default function TeamTabContent({style}) { .catch((error) => console.log(error)); }); }) - .catch((error) => console.log(error)); + .catch((error) => { + console.log(error); + if (!error.response) { + setServerStatus("Failure"); + } + }); }; //Filter out time off periods depending on which filters are active @@ -117,74 +127,89 @@ export default function TeamTabContent({style}) { return ( - {/*Time off header*/} - - -

My team's history

-
- {/*Filter by status button*/} - {timeOffPeriods.length > 0 && - { - if (activeFilters.length >= 2 || !approvedFilter) {setApprovedFilter(value)} - }], - "Waiting": [waitingFilter, (value) => { - if (activeFilters.length >= 2 || !waitingFilter) {setWaitingFilter(value)} - }], - "Rejected": [rejectedFilter, (value) => { - if (activeFilters.length >= 2 || !rejectedFilter) {setRejectedFilter(value)} - }] - }} - icon={} - /> - } -
- {/*If there are periods of time off, display the time off period list and navbar */} - {timeOffPeriods.length > 0 ? + {serverStatus === "Pending" && + + } + {serverStatus === "Success" && <> - - {filteredPeriods.length > 10 && - + {/*Time off header*/} + + +

My team's history

+
+ {/*Filter by status button*/} + {timeOffPeriods.length > 0 && + { + if (activeFilters.length >= 2 || !approvedFilter) {setApprovedFilter(value)} + }], + "Waiting": [waitingFilter, (value) => { + if (activeFilters.length >= 2 || !waitingFilter) {setWaitingFilter(value)} + }], + "Rejected": [rejectedFilter, (value) => { + if (activeFilters.length >= 2 || !rejectedFilter) {setRejectedFilter(value)} + }] + }} + icon={} + /> + } +
+ {/*If there are periods of time off, display the time off period list and navbar */} + {timeOffPeriods.length > 0 ? + <> + + {filteredPeriods.length > 10 && + + } + : + <> + {/*Otherwise, display a message that there is no history*/} + +

There is no time off history

+

Any updates about your time off history will be shown here.

+
+ + } - : - <> - {/*Otherwise, display a message that there is no history*/} - -

There is no time off history

-

Any updates about your time off history will be shown here.

-
- + } + {/*Error message to be displayed if servers are unresponsive*/} + {serverStatus === "Failure" && + +

Servers are unavailable

+

Cannot retrieve time off periods.

+
}
); diff --git a/src/components/TimeOffPage/TimeOffMenu.jsx b/src/components/TimeOffPage/TimeOffMenu.jsx index 29f6ec927..7f0b9d0fa 100644 --- a/src/components/TimeOffPage/TimeOffMenu.jsx +++ b/src/components/TimeOffPage/TimeOffMenu.jsx @@ -37,25 +37,6 @@ export default function TimeOffMenu({style}) { padding: 0 }); - //List of time off policies - const policies = [ - { - type: 'Vacation', - availableDays: '15 days (180 hours)', - hoursUsed: '23 hours used' - }, - { - type: 'Sick', - availableDays: '180 hours left', - hoursUsed: '23 hours used' - }, - { - type: 'Bereavement', - availableDays: '-', - hoursUsed: '23 hours used' - } - ]; - return ( {/*Board tab*/} - + {/*History tab*/} diff --git a/src/components/TimeOffPage/TimeOffPage.jsx b/src/components/TimeOffPage/TimeOffPage.jsx index 8dede6335..bb3ca8a08 100644 --- a/src/components/TimeOffPage/TimeOffPage.jsx +++ b/src/components/TimeOffPage/TimeOffPage.jsx @@ -28,6 +28,7 @@ export default function TimeOffPage({style, innerStyle}) { function sendRequest() { setOpenRequest(false); setRequestSuccess(true); + setTimeout(() => setRequestSuccess(false), 5000); } return ( diff --git a/src/components/UpdatesPage/UpdatesMenu.jsx b/src/components/UpdatesPage/UpdatesMenu.jsx index 365258492..424c90a44 100644 --- a/src/components/UpdatesPage/UpdatesMenu.jsx +++ b/src/components/UpdatesPage/UpdatesMenu.jsx @@ -1,9 +1,11 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; +import CircularProgress from '@mui/material/CircularProgress'; import UpdatesFilter from './UpdatesFilter'; import UpdatesList from './UpdatesList'; import PagesNavBar from './PagesNavBar'; -import NoContentComponent from './NoContentComponent'; +import NoContentComponent from '../StaticComponents/NoContentComponent'; +import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import { useState, useEffect } from 'react'; import { colors, fonts } from '../../Styles'; import axios from 'axios'; @@ -25,6 +27,8 @@ export default function UpdatesMenu({style}) { const [allUpdates, setAllUpdates] = useState([]); //Hook for refreshing the list of notifications const [refresh, setRefresh] = useState(false); + //Flag determining if the database servers can be reached + const [serverStatus, setServerStatus] = useState("Pending"); //ID of the currently logged in employee const currentUserId = 1; @@ -47,6 +51,7 @@ export default function UpdatesMenu({style}) { //Retrieve notification records from database axios.get(notificationsURL) .then((response) => { + setServerStatus("Success"); const updates = []; const data = response.data; data.forEach((up) => { @@ -56,6 +61,7 @@ export default function UpdatesMenu({style}) { }) .catch((error) => { console.log(error); + setServerStatus("Failure"); }); }; @@ -92,44 +98,57 @@ export default function UpdatesMenu({style}) { borderRadius: "10px", backgroundColor: "#FFFFFF" }, ...style}}> - {/*If there are updates, display the updates list and navbar */} - {(allUpdates.length > 0) ? - <> - -

Latest updates

- -
- {/*Updates list*/} - {setRefresh(!refresh)}} - style={{marginBottom: "20px"}} - /> - {/*Updates nav bar*/} - {filteredUpdates.length > 10 && - - } - : + {serverStatus === "Pending" && + + } + {serverStatus === "Success" && <> - {/*Otherwise, display a message that there are no updates*/} - -

You don't have any updates yet

-

Any updates about your company will be shown here.

-
+ {/*If there are updates, display the updates list and navbar */} + {(allUpdates.length > 0) ? + <> + +

Latest updates

+ +
+ {/*Updates list*/} + {setRefresh(!refresh)}} + style={{marginBottom: "20px"}} + /> + {/*Updates nav bar*/} + {filteredUpdates.length > 10 && + + } + : + <> + {/*Otherwise, display a message that there are no updates*/} + +

You don't have any updates yet

+

Any updates about your company will be shown here.

+
+ + } } + {serverStatus === "Failure" && + +

Servers are unavailable

+

Cannot retrieve notifications.

+
+ }
); }; From fa85ee915926ef21bc251bf67f7432099376f49e Mon Sep 17 00:00:00 2001 From: GabrielChan1 Date: Mon, 19 Aug 2024 11:18:37 -0400 Subject: [PATCH 2/7] Issue 73: Display error messages in root component and add button to retry connection --- .../PopupComponents/TimeOffRequest.jsx | 240 ++++++++---------- .../NoConnectionComponent.jsx | 1 + .../TimeOffPage/BoardTabContent.jsx | 94 +++---- .../TimeOffPage/HistoryTabContent.jsx | 145 +++++------ src/components/TimeOffPage/TeamTabContent.jsx | 145 +++++------ src/components/TimeOffPage/TimeOffPage.jsx | 124 ++++++--- src/components/UpdatesPage/UpdatesPage.jsx | 85 ++++++- 7 files changed, 428 insertions(+), 406 deletions(-) diff --git a/src/components/PopupComponents/TimeOffRequest.jsx b/src/components/PopupComponents/TimeOffRequest.jsx index 0c3b0583a..fd4790fd9 100644 --- a/src/components/PopupComponents/TimeOffRequest.jsx +++ b/src/components/PopupComponents/TimeOffRequest.jsx @@ -7,13 +7,11 @@ import Chip from "@mui/material/Chip"; import Dialog from "@mui/material/Dialog"; import { styled } from '@mui/system'; import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; -import CircularProgress from '@mui/material/CircularProgress'; import dayjs from "dayjs"; import DateSelect from "./DateSelect"; import Checkbox from "../Checkbox/Checkbox"; import HRMButton from "../Button/HRMButton"; import TimeOffTable from "./TimeOffTable"; -import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import { colors, fonts } from "../../Styles"; import { useState, useEffect, memo } from "react"; import PropTypes from "prop-types"; @@ -108,8 +106,6 @@ export default function TimeOffRequest({ const [validDates, setValidDates] = useState(true); //Flag determining if an error has occured const [errorOccurred, setErrorOccurred] = useState(false); - //Flag determining if the database servers can be reached - const [serverStatus, setServerStatus] = useState("Pending"); //Retrieve policy category options useEffect(() => { @@ -177,7 +173,6 @@ export default function TimeOffRequest({ //Retrieve employeeAnnualTimeOff records from database axios.post(timeOffPolicyPOSTURL) .then((response) => { - setServerStatus("Success"); const policies = {}; //We are only interested in the records for the current year const data = response.data.filter((p) => p.year === dayjs().year()); @@ -195,9 +190,6 @@ export default function TimeOffRequest({ }) .catch((error) => { console.log(error); - if (!error.response) { - setServerStatus("Failure"); - } }); }; @@ -261,7 +253,8 @@ export default function TimeOffRequest({ if (d.day === "half") { totalHours += 4; halfDays++; - } else { + } + else { totalHours += 8; fullDays++; } @@ -454,129 +447,114 @@ export default function TimeOffRequest({ }} /> - {serverStatus === "Pending" && - - } - {serverStatus === "Success" && - <> - {/*Set time off category*/} - Time off category - - {/*Set starting and ending dates of time off period*/} - - - From - } - label={formatDate(from)} - variant="outlined" - onClick={() => setOpenFrom(true)} - sx={{ borderRadius: "4px" }} - /> - - - To - } - label={formatDate(to)} - variant="outlined" - onClick={() => setOpenTo(true)} - sx={{ borderRadius: "4px" }} - /> - - - {/*Set amount of time off per day*/} - Amount - - setEachDay(!eachDay)} - style={{ marginRight: "10px" }} - /> -

Set hours for each day during the time off period

-
- {/*Time off per day table*/} - Time off category + + {/*Set starting and ending dates of time off period*/} + + + From + } + label={formatDate(from)} + variant="outlined" + onClick={() => setOpenFrom(true)} + sx={{ borderRadius: "4px" }} /> - {/*Time off summary*/} -

- {fullDaysOff} full days ({fullDaysOff * 8} hrs) and {halfDaysOff} half - day ({halfDaysOff * 4} hrs) will be requested ({totalHoursOff} hrs in - total). -

- {!validDates &&

- Your time off request dates overlap with existing upcoming time off periods. -

} - {!sufficientTime &&

- You are requesting {totalHoursOff} hours off. The selected time off policy only - has {category.availableHours} hours available. -

} - {/*Error message to be displayed if an error occurs*/} - {errorOccurred &&

- An error occurred. Could not send time off request. -

} - {/*Send or cancel*/} - - - Cancel - - {initialRequest ? ( - - Update - - ) : ( - - Send - - )} - - {/*Popup components for setting starting and ending dates*/} - setOpenFrom(false)}> - setOpenFrom(false)} setDate={setFrom} initialValue={from} /> - - setOpenTo(false)}> - setOpenTo(false)} setDate={setTo} initialValue={to} /> - - - } - - {/*Error message to be displayed if servers are unresponsive*/} - {serverStatus === "Failure" && - -

Servers are unavailable

-

Cannot send time off requests.

-
- } +
+ + To + } + label={formatDate(to)} + variant="outlined" + onClick={() => setOpenTo(true)} + sx={{ borderRadius: "4px" }} + /> + +
+ {/*Set amount of time off per day*/} + Amount + + setEachDay(!eachDay)} + style={{ marginRight: "10px" }} + /> +

Set hours for each day during the time off period

+
+ {/*Time off per day table*/} + + {/*Time off summary*/} +

+ {fullDaysOff} full days ({fullDaysOff * 8} hrs) and {halfDaysOff} half + day ({halfDaysOff * 4} hrs) will be requested ({totalHoursOff} hrs in + total). +

+ {!validDates &&

+ Your time off request dates overlap with existing upcoming time off periods. +

} + {!sufficientTime &&

+ You are requesting {totalHoursOff} hours off. The selected time off policy only + has {category.availableHours} hours available. +

} + {/*Error message to be displayed if an error occurs*/} + {errorOccurred &&

+ An error occurred. Could not send time off request. +

} + {/*Send or cancel*/} + + + Cancel + + {initialRequest ? ( + + Update + + ) : ( + + Send + + )} + + {/*Popup components for setting starting and ending dates*/} + setOpenFrom(false)}> + setOpenFrom(false)} setDate={setFrom} initialValue={from} /> + + setOpenTo(false)}> + setOpenTo(false)} setDate={setTo} initialValue={to} /> + ); } diff --git a/src/components/StaticComponents/NoConnectionComponent.jsx b/src/components/StaticComponents/NoConnectionComponent.jsx index de81e16d4..80b9bc832 100644 --- a/src/components/StaticComponents/NoConnectionComponent.jsx +++ b/src/components/StaticComponents/NoConnectionComponent.jsx @@ -21,6 +21,7 @@ export default function NoConnectionComponent({children, style}) { }, ...style}}> No Connection Logo { - setServerStatus("Success"); const policies = {}; //Only display the information for the current year const data = response.data.filter((p) => p.year === dayjs().year()); @@ -77,9 +72,6 @@ export default function BoardTabContent({style}) { }) .catch((error) => { console.log(error); - if (!error.response) { - setServerStatus("Failure"); - } }); } @@ -127,57 +119,43 @@ export default function BoardTabContent({style}) { color: colors.darkGrey, fontFamily: fonts.fontFamily }, ...style}}> - {serverStatus === "Pending" && - - } - {serverStatus === "Success" && + {/*Available time off header and table*/} +

Available time offs

+ + {/*Upcoming time off header*/} + +

Upcoming time offs

+
+ {timeOffPeriods.length > 0 ? <> - {/*Available time off header and table*/} -

Available time offs

- - {/*Upcoming time off header*/} - -

Upcoming time offs

-
- {timeOffPeriods.length > 0 ? - <> - {/*Upcoming time off table*/} - setRefresh(!refresh)} - style={{marginBottom: "30px"}} - /> - {/*Upcoming time off navbar*/} - {timeOffPeriods.length > 10 && - - } - : -

There is no upcoming time off right now.

- } - - } - {/*Error message to be displayed if servers are unresponsive */} - {serverStatus === "Failure" && - -

Servers are unavailable

-

Cannot retrieve time off policies or periods.

-
+ {/*Upcoming time off table*/} + setRefresh(!refresh)} + style={{marginBottom: "30px"}} + /> + {/*Upcoming time off navbar*/} + {timeOffPeriods.length > 10 && + + } + : +

There is no upcoming time off right now.

} ); diff --git a/src/components/TimeOffPage/HistoryTabContent.jsx b/src/components/TimeOffPage/HistoryTabContent.jsx index fd79f5095..51bc4e313 100644 --- a/src/components/TimeOffPage/HistoryTabContent.jsx +++ b/src/components/TimeOffPage/HistoryTabContent.jsx @@ -1,12 +1,10 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; import TuneIcon from '@mui/icons-material/Tune'; -import CircularProgress from '@mui/material/CircularProgress'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; import MenuToggleButton from '../BasicMenus/MenuToggleButton'; import NoContentComponent from '../StaticComponents/NoContentComponent'; -import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import Label from '../Label/Label'; import { useState, useEffect } from 'react'; import { colors, fonts } from '../../Styles'; @@ -44,8 +42,6 @@ export default function HistoryTabContent({style}) { const [timeOffPeriods, setTimeOffPeriods] = useState([]); //Hook for refreshing the list of time off periods const [refresh, setRefresh] = useState(false); - //Flag determining if the database servers can be reached - const [serverStatus, setServerStatus] = useState("Pending"); //Filter table columns depending on which filters are active //"From", "To" and at least one other column will always be active @@ -67,11 +63,9 @@ export default function HistoryTabContent({style}) { //Function for retrieving any past time off periods function getTimeOffPeriods() { - //console.log("Running getTimeOffPeriods()"); //Send request to database for time off periods axios.post(timeOffPeriodURL) .then((response) => { - setServerStatus("Success") const periods = []; const data = response.data; data.forEach((p) => { @@ -91,9 +85,6 @@ export default function HistoryTabContent({style}) { }) .catch((error) => { console.log(error); - if (!error.response) { - setServerStatus("Failure"); - } }) }; @@ -113,84 +104,70 @@ export default function HistoryTabContent({style}) { color: colors.darkGrey, fontFamily: fonts.fontFamily }, ...style}}> - {serverStatus === "Pending" && - - } - {serverStatus === "Success" && - <> - {/*Time off header*/} - + +

Time off history

+
+ {/*Customize button*/} + {timeOffPeriods.length > 0 && + { + if (activeFilters.length >= 2 || !typeFilter) {setTypeFilter(value)} + }], + "Amount": [amountFilter, (value) => { + if (activeFilters.length >= 2 || !amountFilter) {setAmountFilter(value)} + }], + "Note": [noteFilter, (value) => { + if (activeFilters.length >= 2 || !noteFilter) {setNoteFilter(value)} + }] }} - > - -

Time off history

-
- {/*Customize button*/} - {timeOffPeriods.length > 0 && - { - if (activeFilters.length >= 2 || !typeFilter) {setTypeFilter(value)} - }], - "Amount": [amountFilter, (value) => { - if (activeFilters.length >= 2 || !amountFilter) {setAmountFilter(value)} - }], - "Note": [noteFilter, (value) => { - if (activeFilters.length >= 2 || !noteFilter) {setNoteFilter(value)} - }] - }} - icon={} - /> - } -
- {/*If there are periods of time off, display the time off period list and navbar */} - {(timeOffPeriods.length > 0) ? - <> - {/*Upcoming time off table*/} - setRefresh(!refresh)} - style={{marginBottom: "30px"}} - /> - {/*Upcoming time off navbar*/} - {timeOffPeriods.length > 10 && - - } - : - <> - {/*Otherwise, display a message that there is no history*/} - -

There is no time off history

-

Any updates about your time off history will be shown here.

-
- - } + icon={} + /> + } + + {/*If there are periods of time off, display the time off period list and navbar */} + {(timeOffPeriods.length > 0) ? + <> + {/*Upcoming time off table*/} + setRefresh(!refresh)} + style={{marginBottom: "30px"}} + /> + {/*Upcoming time off navbar*/} + {timeOffPeriods.length > 10 && + + } + : + <> + {/*Otherwise, display a message that there is no history*/} + +

There is no time off history

+

Any updates about your time off history will be shown here.

+
} - {/*Error message to be displayed if servers are unresponsive*/} - {serverStatus === "Failure" && - -

Servers are unavailable

-

Cannot retrieve time off periods.

-
- } ); }; diff --git a/src/components/TimeOffPage/TeamTabContent.jsx b/src/components/TimeOffPage/TeamTabContent.jsx index f9bb9a806..efd437994 100644 --- a/src/components/TimeOffPage/TeamTabContent.jsx +++ b/src/components/TimeOffPage/TeamTabContent.jsx @@ -1,12 +1,10 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; import FilterListIcon from '@mui/icons-material/FilterList'; -import CircularProgress from '@mui/material/CircularProgress'; import { useState, useEffect } from 'react'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; import NoContentComponent from '../StaticComponents/NoContentComponent'; -import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import MenuToggleButton from '../BasicMenus/MenuToggleButton'; import Label from '../Label/Label'; import { colors, fonts } from '../../Styles'; @@ -44,8 +42,6 @@ export default function TeamTabContent({style}) { const [timeOffPeriods, setTimeOffPeriods] = useState([]); //Hook for refreshing the list of time off periods const [refresh, setRefresh] = useState(false); - //Flag determining if the database servers can be reached - const [serverStatus, setServerStatus] = useState("Pending"); //ID of the currently logged in employee const currentUser = 1; @@ -76,7 +72,6 @@ export default function TeamTabContent({style}) { team.forEach((id) => { axios.post(`${timeOffURL}/${id}`) .then((response) => { - setServerStatus("Success"); data = response.data; data.forEach((p) => { periods.push({ @@ -100,9 +95,6 @@ export default function TeamTabContent({style}) { }) .catch((error) => { console.log(error); - if (!error.response) { - setServerStatus("Failure"); - } }); }; @@ -131,85 +123,70 @@ export default function TeamTabContent({style}) { color: colors.darkGrey, fontFamily: fonts.fontFamily }, ...style}}> - {serverStatus === "Pending" && - - } - {serverStatus === "Success" && + {/*Time off header*/} + + +

My team's history

+
+ {/*Filter by status button*/} + {timeOffPeriods.length > 0 && + { + if (activeFilters.length >= 2 || !approvedFilter) {setApprovedFilter(value)} + }], + "Waiting": [waitingFilter, (value) => { + if (activeFilters.length >= 2 || !waitingFilter) {setWaitingFilter(value)} + }], + "Rejected": [rejectedFilter, (value) => { + if (activeFilters.length >= 2 || !rejectedFilter) {setRejectedFilter(value)} + }] + }} + icon={} + /> + } +
+ {/*If there are periods of time off, display the time off period list and navbar */} + {timeOffPeriods.length > 0 ? <> - {/*Time off header*/} - - -

My team's history

-
- {/*Filter by status button*/} - {timeOffPeriods.length > 0 && - { - if (activeFilters.length >= 2 || !approvedFilter) {setApprovedFilter(value)} - }], - "Waiting": [waitingFilter, (value) => { - if (activeFilters.length >= 2 || !waitingFilter) {setWaitingFilter(value)} - }], - "Rejected": [rejectedFilter, (value) => { - if (activeFilters.length >= 2 || !rejectedFilter) {setRejectedFilter(value)} - }] - }} - icon={} - /> - } -
- {/*If there are periods of time off, display the time off period list and navbar */} - {timeOffPeriods.length > 0 ? - <> - - {filteredPeriods.length > 10 && - - } - : - <> - {/*Otherwise, display a message that there is no history*/} - -

There is no time off history

-

Any updates about your time off history will be shown here.

-
- - + + {filteredPeriods.length > 10 && + } + : + <> + {/*Otherwise, display a message that there is no history*/} + +

There is no time off history

+

Any updates about your time off history will be shown here.

+
- } - {/*Error message to be displayed if servers are unresponsive*/} - {serverStatus === "Failure" && - -

Servers are unavailable

-

Cannot retrieve time off periods.

-
} ); diff --git a/src/components/TimeOffPage/TimeOffPage.jsx b/src/components/TimeOffPage/TimeOffPage.jsx index bb3ca8a08..bf07d37fd 100644 --- a/src/components/TimeOffPage/TimeOffPage.jsx +++ b/src/components/TimeOffPage/TimeOffPage.jsx @@ -1,11 +1,14 @@ import Stack from '@mui/system/Stack'; import Dialog from '@mui/material/Dialog'; +import CircularProgress from '@mui/material/CircularProgress'; import TimeOffMenu from './TimeOffMenu'; import TimeOffRequest from '../PopupComponents/TimeOffRequest'; import TimeOffRequestSent from '../PopupComponents/TimeOffRequestSent'; import HRMButton from '../Button/HRMButton'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import axios from 'axios'; import Page from '../StaticComponents/Page'; +import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; /** * Time off page of the HRM application. Contains the time off menu as well as controls for @@ -23,6 +26,34 @@ export default function TimeOffPage({style, innerStyle}) { //should be displayed const [openRequest, setOpenRequest] = useState(false); const [requestSuccess, setRequestSuccess] = useState(false); + //Flag determining if the database servers can be reached + const [serverStatus, setServerStatus] = useState("Pending"); + + useEffect(() => { + testConnection(); + }, []); + + //ID of the currently logged in employee + const currentUser = 1; + + //URL endpoints to be used for API calls + const timeOffPolicyPOSTURL = `http://localhost:5000/api/employeeannualtimeoffs/${currentUser}`; + + //Function for testing connection to database + function testConnection() { + setServerStatus("Pending"); + axios.post(timeOffPolicyPOSTURL) + .then((response) => { + console.log(response); + setServerStatus("Success"); + }) + .catch((error) => { + console.log(error); + if (!error.response) { + setServerStatus("Failure"); + } + }) + }; //Function for sending a time off request function sendRequest() { @@ -33,42 +64,65 @@ export default function TimeOffPage({style, innerStyle}) { return ( - {/*Main page content*/} - -

Time off

- setOpenRequest(true)} + {serverStatus === "Pending" && + //Show loading logo while connecting to database + + } + {serverStatus === "Success" && + //Show page content if connection is successful + <> + {/*Main page content*/} + - Request new time off - -
- - {/*Time off request menu*/} - setOpenRequest(false)}> - setOpenRequest(false)} - sendRequest={() => sendRequest()} +

Time off

+ setOpenRequest(true)} + > + Request new time off + + + + {/*Time off request menu*/} + setOpenRequest(false)}> + setOpenRequest(false)} + sendRequest={() => sendRequest()} + /> + + {/*Request successful notification*/} + setRequestSuccess(false)} + style={{ + display: (requestSuccess) ? "block" : "none", + position: "fixed", + right: "40px", + bottom: "40px", + zIndex: 999 + }} /> -
- {/*Request successful notification*/} - setRequestSuccess(false)} - style={{ - display: (requestSuccess) ? "block" : "none", - position: "fixed", - right: "40px", - bottom: "40px", - zIndex: 999 - }} - /> + + } + {serverStatus === "Failure" && + //Error message to be displayed if servers are unresponsive + +

Servers are unavailable

+

Cannot retrieve time off policies or periods.

+ + Retry Connection + +
+ }
); }; diff --git a/src/components/UpdatesPage/UpdatesPage.jsx b/src/components/UpdatesPage/UpdatesPage.jsx index 9c8643e45..365b9c69d 100644 --- a/src/components/UpdatesPage/UpdatesPage.jsx +++ b/src/components/UpdatesPage/UpdatesPage.jsx @@ -1,6 +1,11 @@ import Stack from '@mui/system/Stack'; +import CircularProgress from '@mui/material/CircularProgress'; import Page from '../StaticComponents/Page'; +import HRMButton from '../Button/HRMButton'; +import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import UpdatesMenu from './UpdatesMenu'; +import { useState, useEffect } from 'react'; +import axios from 'axios'; /** * Home page of the HRM application. Contains the updates menu. @@ -13,26 +18,78 @@ import UpdatesMenu from './UpdatesMenu'; * Default: {} */ export default function UpdatesPage({style, innerStyle}) { + //Flag determining if the database servers can be reached + const [serverStatus, setServerStatus] = useState("Pending"); + + useEffect(() => { + testConnection(); + }, []); + + //ID of the currently logged in employee + const currentUser = 1; + + //URL endpoints to be used for API calls + const timeOffPolicyPOSTURL = `http://localhost:5000/api/employeeannualtimeoffs/${currentUser}`; + + //Function for testing connection to database + function testConnection() { + setServerStatus("Pending"); + axios.post(timeOffPolicyPOSTURL) + .then((response) => { + console.log(response); + setServerStatus("Success"); + }) + .catch((error) => { + console.log(error); + if (!error.response) { + setServerStatus("Failure"); + } + }) + }; + return ( - -

Hello, Gabriel

-

Today is Monday, June 6, 2024

-
- + {serverStatus === "Pending" && + //Show loading logo while connecting to database + + } + {serverStatus === "Success" && + //Show page content if connection is successful + <> + +

Hello, Gabriel

+

Today is Monday, June 6, 2024

+
+ + + } + {serverStatus === "Failure" && + //Error message to be displayed if servers are unresponsive + +

Servers are unavailable

+

Cannot retrieve notifications.

+ + Retry Connection + +
+ }
); }; -//Control panel settings for storybook +//Control panel settings for storybooks UpdatesPage.propTypes = {}; //Default values for this component From 0d3556f89d5de39ee30d6215a47227d877f518cd Mon Sep 17 00:00:00 2001 From: GabrielChan1 Date: Sun, 25 Aug 2024 13:31:01 -0400 Subject: [PATCH 3/7] Fixed background of home and time off page and implemented transparent header --- .../PopupComponents/TimeOffRequest.jsx | 33 +++---- src/components/StaticComponents/Header.jsx | 23 +++-- src/components/StaticComponents/Page.jsx | 1 + .../TimeOffPage/AvailableTimeOffTable.jsx | 3 +- .../TimeOffPage/BoardTabContent.jsx | 13 +-- .../TimeOffPage/HistoryTabContent.jsx | 12 ++- src/components/TimeOffPage/TeamTabContent.jsx | 10 +- src/components/TimeOffPage/TimeOffPage.jsx | 11 ++- .../TimeOffPage/UpcomingTimeOffTable.jsx | 2 +- src/components/UpdatesPage/UpdatesList.jsx | 20 ++-- src/components/UpdatesPage/UpdatesMenu.jsx | 96 ++++++++----------- src/components/UpdatesPage/UpdatesPage.jsx | 8 +- src/testConfig.js | 1 + 13 files changed, 122 insertions(+), 111 deletions(-) create mode 100644 src/testConfig.js diff --git a/src/components/PopupComponents/TimeOffRequest.jsx b/src/components/PopupComponents/TimeOffRequest.jsx index fd4790fd9..ab2b5e267 100644 --- a/src/components/PopupComponents/TimeOffRequest.jsx +++ b/src/components/PopupComponents/TimeOffRequest.jsx @@ -1,21 +1,22 @@ -import Box from "@mui/system/Box"; -import Stack from "@mui/system/Stack"; -import CloseIcon from "@mui/icons-material/Close"; -import Select from "@mui/material/Select"; +import Box from '@mui/system/Box'; +import Stack from '@mui/system/Stack'; +import CloseIcon from '@mui/icons-material/Close'; +import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; -import Chip from "@mui/material/Chip"; -import Dialog from "@mui/material/Dialog"; +import Chip from '@mui/material/Chip'; +import Dialog from '@mui/material/Dialog'; import { styled } from '@mui/system'; -import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; -import dayjs from "dayjs"; -import DateSelect from "./DateSelect"; -import Checkbox from "../Checkbox/Checkbox"; -import HRMButton from "../Button/HRMButton"; -import TimeOffTable from "./TimeOffTable"; -import { colors, fonts } from "../../Styles"; -import { useState, useEffect, memo } from "react"; -import PropTypes from "prop-types"; +import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; +import { useState, useEffect, memo } from 'react'; +import PropTypes from 'prop-types'; import axios from 'axios'; +import dayjs from 'dayjs'; +import DateSelect from './DateSelect'; +import Checkbox from '../Checkbox/Checkbox'; +import HRMButton from '../Button/HRMButton'; +import TimeOffTable from './TimeOffTable'; +import { colors, fonts } from '../../Styles'; +import { currentUserID } from '../../testConfig'; //Function for determining if a time period is valid. A time period is only valid if the //starting date is before or on the same day as the ending date. @@ -161,7 +162,7 @@ export default function TimeOffRequest({ const MemoTimeOffTable = memo(TimeOffTable); //ID of the currently logged in employee - const currentUser = 1; + const currentUser = currentUserID; //URL endpoints to be used for API calls const timeOffPolicyPOSTURL = `http://localhost:5000/api/employeeannualtimeoffs/${currentUser}`; diff --git a/src/components/StaticComponents/Header.jsx b/src/components/StaticComponents/Header.jsx index e383cd716..e3e1d4b60 100644 --- a/src/components/StaticComponents/Header.jsx +++ b/src/components/StaticComponents/Header.jsx @@ -1,4 +1,5 @@ import Box from '@mui/system/Box'; +import { useScrollTrigger } from '@mui/material'; import UserDropdown from './UserDropdown'; import AvatarImage from '../../Images/a99b7c47182d3a04f5f3ed31db0dd8a6.jpg'; @@ -9,7 +10,13 @@ import AvatarImage from '../../Images/a99b7c47182d3a04f5f3ed31db0dd8a6.jpg'; * - style: Optional prop for adding further inline styling. * Default: {} */ -export default function Header({style}) { +export default function Header({window, style}) { + const trigger = useScrollTrigger({ + target: window ? window() : undefined, + disableHysteresis: true, + threshold: 0 + }); + const user = { avatar: AvatarImage, name: "Gabriel Chan", @@ -17,7 +24,9 @@ export default function Header({style}) { }; return ( - diff --git a/src/components/StaticComponents/Page.jsx b/src/components/StaticComponents/Page.jsx index 0f090c8d0..fcd08ff04 100644 --- a/src/components/StaticComponents/Page.jsx +++ b/src/components/StaticComponents/Page.jsx @@ -36,6 +36,7 @@ export default function Page({children, style, innerStyle}) { paddingBottom: "40px", width: "100%", height: "100%", + minHeight: "100vh", }, ...innerStyle}}> {/*Main page content*/} {children} diff --git a/src/components/TimeOffPage/AvailableTimeOffTable.jsx b/src/components/TimeOffPage/AvailableTimeOffTable.jsx index e9e1ad23a..b9a156d58 100644 --- a/src/components/TimeOffPage/AvailableTimeOffTable.jsx +++ b/src/components/TimeOffPage/AvailableTimeOffTable.jsx @@ -5,8 +5,9 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import { styled } from '@mui/system'; -import { colors, fonts } from '../../Styles'; import PropTypes from 'prop-types'; +import { colors, fonts } from '../../Styles'; + /** * Menu component for listing the available time off policies and the time used and available diff --git a/src/components/TimeOffPage/BoardTabContent.jsx b/src/components/TimeOffPage/BoardTabContent.jsx index 858c082f0..bed2ad271 100644 --- a/src/components/TimeOffPage/BoardTabContent.jsx +++ b/src/components/TimeOffPage/BoardTabContent.jsx @@ -1,15 +1,16 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; +import { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; +import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; +import axios from 'axios'; import AvailableTimeOffTable from './AvailableTimeOffTable'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; import Label from '../Label/Label'; import { colors, fonts } from '../../Styles'; -import { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import dayjs from 'dayjs'; -import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' -import axios from 'axios'; +import { currentUserID } from '../../testConfig'; //Function for parsing a JavaScript date into a string format. function formatDate(date) { @@ -38,7 +39,7 @@ export default function BoardTabContent({style}) { const [refresh, setRefresh] = useState(false); //ID of the currently logged in employee - const currentUser = 1; + const currentUser = currentUserID; //URL endpoints to be used for API calls const timeOffPeriodURL = `http://localhost:5000/api/timeoffhistories/employee/${currentUser}`; diff --git a/src/components/TimeOffPage/HistoryTabContent.jsx b/src/components/TimeOffPage/HistoryTabContent.jsx index 51bc4e313..d84bbe8e2 100644 --- a/src/components/TimeOffPage/HistoryTabContent.jsx +++ b/src/components/TimeOffPage/HistoryTabContent.jsx @@ -1,16 +1,18 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; import TuneIcon from '@mui/icons-material/Tune'; +import { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; +import axios from 'axios'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; import MenuToggleButton from '../BasicMenus/MenuToggleButton'; import NoContentComponent from '../StaticComponents/NoContentComponent'; import Label from '../Label/Label'; -import { useState, useEffect } from 'react'; +import { currentUserID } from '../../testConfig'; import { colors, fonts } from '../../Styles'; -import PropTypes from 'prop-types'; -import dayjs from 'dayjs'; -import axios from 'axios'; + //Function for parsing a JavaScript date into a string format. function formatDate(date) { @@ -51,7 +53,7 @@ export default function HistoryTabContent({style}) { if (noteFilter) { activeFilters.push("Note"); } //ID of the currently logged in employee - const currentUser = 1; + const currentUser = currentUserID; //URL endpoints to be used for API calls const timeOffPeriodURL = `http://localhost:5000/api/timeoffhistories/employee/${currentUser}`; diff --git a/src/components/TimeOffPage/TeamTabContent.jsx b/src/components/TimeOffPage/TeamTabContent.jsx index efd437994..c7a5362d5 100644 --- a/src/components/TimeOffPage/TeamTabContent.jsx +++ b/src/components/TimeOffPage/TeamTabContent.jsx @@ -2,15 +2,17 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; import FilterListIcon from '@mui/icons-material/FilterList'; import { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; +import axios from 'axios'; import UpcomingTimeOffTable from './UpcomingTimeOffTable'; import PagesNavBar from '../UpdatesPage/PagesNavBar'; import NoContentComponent from '../StaticComponents/NoContentComponent'; import MenuToggleButton from '../BasicMenus/MenuToggleButton'; import Label from '../Label/Label'; import { colors, fonts } from '../../Styles'; -import PropTypes from 'prop-types'; -import dayjs from 'dayjs'; -import axios from 'axios'; +import { currentUserID } from '../../testConfig'; + //Function for parsing a JavaScript date into a string format. function formatDate(date) { @@ -44,7 +46,7 @@ export default function TeamTabContent({style}) { const [refresh, setRefresh] = useState(false); //ID of the currently logged in employee - const currentUser = 1; + const currentUser = currentUserID; //URL endpoints to be used in API calls const empUrl = `http://localhost:5000/api/managers/employees/${currentUser}`; diff --git a/src/components/TimeOffPage/TimeOffPage.jsx b/src/components/TimeOffPage/TimeOffPage.jsx index bf07d37fd..acd95e6ae 100644 --- a/src/components/TimeOffPage/TimeOffPage.jsx +++ b/src/components/TimeOffPage/TimeOffPage.jsx @@ -1,14 +1,17 @@ import Stack from '@mui/system/Stack'; import Dialog from '@mui/material/Dialog'; import CircularProgress from '@mui/material/CircularProgress'; +import { useState, useEffect } from 'react'; +import axios from 'axios'; import TimeOffMenu from './TimeOffMenu'; import TimeOffRequest from '../PopupComponents/TimeOffRequest'; import TimeOffRequestSent from '../PopupComponents/TimeOffRequestSent'; -import HRMButton from '../Button/HRMButton'; -import { useState, useEffect } from 'react'; -import axios from 'axios'; import Page from '../StaticComponents/Page'; import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; +import HRMButton from '../Button/HRMButton'; +import { currentUserID } from '../../testConfig'; + + /** * Time off page of the HRM application. Contains the time off menu as well as controls for @@ -34,7 +37,7 @@ export default function TimeOffPage({style, innerStyle}) { }, []); //ID of the currently logged in employee - const currentUser = 1; + const currentUser = currentUserID; //URL endpoints to be used for API calls const timeOffPolicyPOSTURL = `http://localhost:5000/api/employeeannualtimeoffs/${currentUser}`; diff --git a/src/components/TimeOffPage/UpcomingTimeOffTable.jsx b/src/components/TimeOffPage/UpcomingTimeOffTable.jsx index 7b2ef7b8e..50eef9d63 100644 --- a/src/components/TimeOffPage/UpcomingTimeOffTable.jsx +++ b/src/components/TimeOffPage/UpcomingTimeOffTable.jsx @@ -11,12 +11,12 @@ import Dialog from '@mui/material/Dialog'; import { styled } from '@mui/system'; import PropTypes from 'prop-types'; import { useState } from 'react'; +import dayjs from "dayjs"; import HRMButton from '../Button/HRMButton'; import Label from '../Label/Label'; import TimeOffRequest from '../PopupComponents/TimeOffRequest'; import DeleteTimeOff from '../PopupComponents/DeleteTimeOff'; import { colors, fonts } from '../../Styles'; -import dayjs from "dayjs"; /** * Menu component for listing upcoming scheduled periods of time off diff --git a/src/components/UpdatesPage/UpdatesList.jsx b/src/components/UpdatesPage/UpdatesList.jsx index 11670f115..3bc9e9900 100644 --- a/src/components/UpdatesPage/UpdatesList.jsx +++ b/src/components/UpdatesPage/UpdatesList.jsx @@ -4,14 +4,16 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableRow from '@mui/material/TableRow'; import Dialog from '@mui/material/Dialog'; +import { useState } from 'react'; +import PropTypes from 'prop-types'; +import axios from 'axios'; +import dayjs from 'dayjs'; import Label from '../Label/Label'; import HRMButton from '../Button/HRMButton'; import NewTeamMember from '../PopupComponents/NewTeamMember'; import TimeOffRequestSentWindow from '../PopupComponents/TimeOffRequestSentWindow'; import TimeOffApproval from '../PopupComponents/TimeOffApproval'; -import { useState } from 'react'; -import PropTypes from 'prop-types'; -import axios from 'axios'; +import { currentUserID } from '../../testConfig'; /** * Menu component for listing update notifications in the home page. @@ -37,7 +39,7 @@ export default function UpdatesList({updates, refresh, style}) { const [approvalDetails, setApprovalDetails] = useState({}); //ID of the currently logged in employee - const currentUserId = 1; + const currentUserId = currentUserID; //URL endpoints to be used for API calls const notificationURL = `http://localhost:5000/api/notifications/`; @@ -94,11 +96,11 @@ export default function UpdatesList({updates, refresh, style}) { role: up.employee.role.roleTitle, email: up.employee.email, office: up.employee.officeLocation, - effectiveDate: up.employee.effectiveDate, + effectiveDate: dayjs(up.employee.effectiveDate).format("DD/MM/YYYY"), timeOffBalance: (up.employeeAnnualTimeOff.hoursAllowed - up.employeeAnnualTimeOff.cumulativeHoursTaken), - timeOffRequested: `${up.timeOffHistory.startDate} - - ${up.timeOffHistory.endDate}`, + timeOffRequested: `${dayjs(up.timeOffHistory.startDate).format("DD/MM/YYYY")} - + ${dayjs(up.timeOffHistory.endDate).format("DD/MM/YYYY")}`, requestedDaysTotal: Math.ceil(up.timeOffHistory.hours / 24), timeOffCategory: up.timeOff.category, status: up.timeOffHistory.status @@ -110,8 +112,8 @@ export default function UpdatesList({updates, refresh, style}) { const details = { timeOffBalance: (up.employeeAnnualTimeOff.hoursAllowed - up.employeeAnnualTimeOff.cumulativeHoursTaken), - timeOffRequested: `${up.timeOffHistory.startDate} - - ${up.timeOffHistory.endDate}`, + timeOffRequested: `${dayjs(up.timeOffHistory.startDate).format("DD/MM/YYYY")} - + ${dayjs(up.timeOffHistory.endDate).format("DD/MM/YYYY")}`, requestedDaysTotal: Math.ceil(up.timeOffHistory.hours / 24), timeOffCategory: up.timeOff.category, notes: up.timeOffHistory.note diff --git a/src/components/UpdatesPage/UpdatesMenu.jsx b/src/components/UpdatesPage/UpdatesMenu.jsx index 424c90a44..465ffbdee 100644 --- a/src/components/UpdatesPage/UpdatesMenu.jsx +++ b/src/components/UpdatesPage/UpdatesMenu.jsx @@ -1,13 +1,14 @@ import Box from '@mui/system/Box'; import Stack from '@mui/system/Stack'; -import CircularProgress from '@mui/material/CircularProgress'; +import { useState, useEffect } from 'react'; import UpdatesFilter from './UpdatesFilter'; import UpdatesList from './UpdatesList'; import PagesNavBar from './PagesNavBar'; import NoContentComponent from '../StaticComponents/NoContentComponent'; -import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; -import { useState, useEffect } from 'react'; import { colors, fonts } from '../../Styles'; +import { currentUserID } from '../../testConfig'; + + import axios from 'axios'; /** @@ -27,11 +28,9 @@ export default function UpdatesMenu({style}) { const [allUpdates, setAllUpdates] = useState([]); //Hook for refreshing the list of notifications const [refresh, setRefresh] = useState(false); - //Flag determining if the database servers can be reached - const [serverStatus, setServerStatus] = useState("Pending"); //ID of the currently logged in employee - const currentUserId = 1; + const currentUserId = currentUserID; //Refresh the list of notifications whenever the refresh hook is changed useEffect(() => { @@ -51,7 +50,6 @@ export default function UpdatesMenu({style}) { //Retrieve notification records from database axios.get(notificationsURL) .then((response) => { - setServerStatus("Success"); const updates = []; const data = response.data; data.forEach((up) => { @@ -61,7 +59,6 @@ export default function UpdatesMenu({style}) { }) .catch((error) => { console.log(error); - setServerStatus("Failure"); }); }; @@ -98,57 +95,44 @@ export default function UpdatesMenu({style}) { borderRadius: "10px", backgroundColor: "#FFFFFF" }, ...style}}> - {serverStatus === "Pending" && - - } - {serverStatus === "Success" && + {/*If there are updates, display the updates list and navbar */} + {(allUpdates.length > 0) ? + <> + +

Latest updates

+ +
+ {/*Updates list*/} + {setRefresh(!refresh)}} + style={{marginBottom: "20px"}} + /> + {/*Updates nav bar*/} + {filteredUpdates.length > 10 && + + } + : <> - {/*If there are updates, display the updates list and navbar */} - {(allUpdates.length > 0) ? - <> - -

Latest updates

- -
- {/*Updates list*/} - {setRefresh(!refresh)}} - style={{marginBottom: "20px"}} - /> - {/*Updates nav bar*/} - {filteredUpdates.length > 10 && - - } - : - <> - {/*Otherwise, display a message that there are no updates*/} - -

You don't have any updates yet

-

Any updates about your company will be shown here.

-
- - } + {/*Otherwise, display a message that there are no updates*/} + +

You don't have any updates yet

+

Any updates about your company will be shown here.

+
} - {serverStatus === "Failure" && - -

Servers are unavailable

-

Cannot retrieve notifications.

-
- }
); }; diff --git a/src/components/UpdatesPage/UpdatesPage.jsx b/src/components/UpdatesPage/UpdatesPage.jsx index 365b9c69d..1e6ee5b54 100644 --- a/src/components/UpdatesPage/UpdatesPage.jsx +++ b/src/components/UpdatesPage/UpdatesPage.jsx @@ -1,11 +1,13 @@ import Stack from '@mui/system/Stack'; import CircularProgress from '@mui/material/CircularProgress'; +import { useState, useEffect } from 'react'; +import axios from 'axios'; import Page from '../StaticComponents/Page'; import HRMButton from '../Button/HRMButton'; import NoConnectionComponent from '../StaticComponents/NoConnectionComponent'; import UpdatesMenu from './UpdatesMenu'; -import { useState, useEffect } from 'react'; -import axios from 'axios'; +import { currentUserID } from '../../testConfig'; + /** * Home page of the HRM application. Contains the updates menu. @@ -26,7 +28,7 @@ export default function UpdatesPage({style, innerStyle}) { }, []); //ID of the currently logged in employee - const currentUser = 1; + const currentUser = currentUserID; //URL endpoints to be used for API calls const timeOffPolicyPOSTURL = `http://localhost:5000/api/employeeannualtimeoffs/${currentUser}`; diff --git a/src/testConfig.js b/src/testConfig.js new file mode 100644 index 000000000..56514783a --- /dev/null +++ b/src/testConfig.js @@ -0,0 +1 @@ +export const currentUserID = 2; \ No newline at end of file From a1b7f98f83eb1ba004f5e3e223793c93104dd3e2 Mon Sep 17 00:00:00 2001 From: GabrielChan1 Date: Mon, 26 Aug 2024 21:19:10 -0400 Subject: [PATCH 4/7] Added new notifications for time off request approval/rejection --- .../PopupComponents/NewTeamMember.stories.js | 4 +- .../PopupComponents/TimeOffApproval.jsx | 19 +++-- .../TimeOffRequestResolved.jsx | 80 +++++++++++++++++++ .../TimeOffRequestResolved.stories.js | 36 +++++++++ .../PopupComponents/TimeOffRequestSent.jsx | 18 ++--- .../TimeOffRequestSentWindow.jsx | 8 +- src/components/UpdatesPage/UpdatesList.jsx | 58 ++++++++++---- 7 files changed, 189 insertions(+), 34 deletions(-) create mode 100644 src/components/PopupComponents/TimeOffRequestResolved.jsx create mode 100644 src/components/PopupComponents/TimeOffRequestResolved.stories.js diff --git a/src/components/PopupComponents/NewTeamMember.stories.js b/src/components/PopupComponents/NewTeamMember.stories.js index d9c11201e..9d13ad95f 100644 --- a/src/components/PopupComponents/NewTeamMember.stories.js +++ b/src/components/PopupComponents/NewTeamMember.stories.js @@ -1,8 +1,9 @@ import NewTeamMember from './NewTeamMember'; import AvatarImage from '../../Images/a99b7c47182d3a04f5f3ed31db0dd8a6.jpg'; +//Storybook display settings export default { - title: "PopupMenus/NewTeamMember", + title: 'PopupMenus/NewTeamMember', component: NewTeamMember, parameters: { layout: 'centered' @@ -10,6 +11,7 @@ export default { tags: ['autodocs'] }; +//Stories for each NewTeamMember type export const Primary = { args: { employee_details: { diff --git a/src/components/PopupComponents/TimeOffApproval.jsx b/src/components/PopupComponents/TimeOffApproval.jsx index 6f5991bd6..1e5551ce1 100644 --- a/src/components/PopupComponents/TimeOffApproval.jsx +++ b/src/components/PopupComponents/TimeOffApproval.jsx @@ -4,10 +4,12 @@ import Avatar from '@mui/material/Avatar'; import CloseIcon from '@mui/icons-material/Close'; import TextField from '@mui/material/TextField'; import { styled } from '@mui/system'; -import HRMButton from '../Button/HRMButton'; -import { colors, fonts } from '../../Styles'; import PropTypes from 'prop-types'; import axios from 'axios'; +import { useState } from 'react'; +import HRMButton from '../Button/HRMButton'; +import { colors, fonts } from '../../Styles'; + /** * Popup component for displaying the information of a time off request and the options to reject @@ -41,6 +43,11 @@ import axios from 'axios'; * Default: {} */ export default function TimeOffApproval({request_information, close, refresh, style}) { + const [notes, setNotes] = useState(""); + + //URL endpoints to be used for API calls + const timeOffPeriodURL = `http://localhost:5000/api/timeoffhistories`; + //Custom style elements const StyledTD = styled('td')({ textAlign: "start", @@ -51,10 +58,10 @@ export default function TimeOffApproval({request_information, close, refresh, st //Function for sending the PUT request to change the time off request status function resolveRequest(newStatus) { //Update the time off period status - const url = `http://localhost:5000/api/timeoffhistories`; - axios.put(url, { + axios.put(timeOffPeriodURL, { id: request_information.timeOffId, - status: newStatus + status: newStatus, + note: notes }) .then((response) => { console.log(response); @@ -142,6 +149,8 @@ export default function TimeOffApproval({request_information, close, refresh, st setNotes(e.target.value)} sx={{ border: "1px solid #D0D5DD", borderRadius: "8px", diff --git a/src/components/PopupComponents/TimeOffRequestResolved.jsx b/src/components/PopupComponents/TimeOffRequestResolved.jsx new file mode 100644 index 000000000..bf1154612 --- /dev/null +++ b/src/components/PopupComponents/TimeOffRequestResolved.jsx @@ -0,0 +1,80 @@ +import Box from '@mui/system/Box'; +import Stack from '@mui/system/Stack'; +import CloseIcon from '@mui/icons-material/Close'; +import HRMButton from '../Button/HRMButton'; +import { colors, fonts } from '../../Styles'; +import PropTypes from 'prop-types'; + +/** + * Popup component for notifying the user of the result of their time off request. + * + * Props: + * - time_off_information: Contains the details of the time off request being approved or rejected + * Syntax: { + * startDate: + * endDate: + * status: + * notes: + * } + * + * - close: Function for closing this popup component. + * Syntax: close() + * + * - style: Optional prop for adding further inline styling. + * Default: {} + */ +export default function TimeOffRequestResolved({time_off_information, close, style}) { + const result = time_off_information.status === "Approved" ? "approved" : "rejected"; + + return ( + + +

+ Your time off request has been {result} +

+ +
+

Your time off request from {time_off_information.startDate} to {time_off_information.endDate} has been {result}

+ {time_off_information.notes && <> +

Additional notes from your manager:

+

{time_off_information.notes}

+ } + + OK + +
+ ); +}; + +//Control panel settings for storybook +TimeOffRequestResolved.propTypes = { + //Information included in the time off request + time_off_information: PropTypes.objectOf(PropTypes.string), + + //Function for closing this popup + close: PropTypes.func +}; + +//Default values for this component +TimeOffRequestResolved.defaultProps = { + style: {} +}; \ No newline at end of file diff --git a/src/components/PopupComponents/TimeOffRequestResolved.stories.js b/src/components/PopupComponents/TimeOffRequestResolved.stories.js new file mode 100644 index 000000000..391983f6e --- /dev/null +++ b/src/components/PopupComponents/TimeOffRequestResolved.stories.js @@ -0,0 +1,36 @@ +import TimeOffRequestResolved from './TimeOffRequestResolved'; + +//Storybook display settings +export default { + title: 'PopupMenus/TimeOffRequestResolved', + component: TimeOffRequestResolved, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +}; + +//Stories for each TimeOffRequestResolved type +export const Approved = { + args: { + time_off_information: { + startDate: 'Jan 6, 2024', + endDate: 'Jan 8, 2024', + status: 'Approved', + notes: 'Have fun on your vacation!' + }, + close: () => {} + } +}; + +export const Rejected = { + args: { + time_off_information: { + startDate: 'Apr 14, 2024', + endDate: 'Apr 17, 2024', + status: 'Declined', + notes: "You've been slacking off every day for the past week! Get back to work!" + }, + close: () => {} + } +}; \ No newline at end of file diff --git a/src/components/PopupComponents/TimeOffRequestSent.jsx b/src/components/PopupComponents/TimeOffRequestSent.jsx index 7a22e3443..e6ea68b26 100644 --- a/src/components/PopupComponents/TimeOffRequestSent.jsx +++ b/src/components/PopupComponents/TimeOffRequestSent.jsx @@ -16,16 +16,14 @@ import PropTypes from 'prop-types'; */ export default function TimeOffRequestSent({close, style}) { return ( - +

New time off request created successfully

: Contains the request information + * - request_information: Contains the request information. * Syntax: { * timeOffBalance: * timeOffRequested: @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; * notes: * } * - * - close: Function for closing this popup component + * - close: Function for closing this popup component. * Syntax: close() * * - style: Optional prop for adding further inline styling. @@ -96,9 +96,9 @@ TimeOffRequestSentWindow.propTypes = { //Function for closing this popup close: PropTypes.func, -} +}; //Default values for this component TimeOffRequestSentWindow.defaultProps = { style: {} -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/UpdatesPage/UpdatesList.jsx b/src/components/UpdatesPage/UpdatesList.jsx index 3bc9e9900..eaca16f97 100644 --- a/src/components/UpdatesPage/UpdatesList.jsx +++ b/src/components/UpdatesPage/UpdatesList.jsx @@ -13,6 +13,7 @@ import HRMButton from '../Button/HRMButton'; import NewTeamMember from '../PopupComponents/NewTeamMember'; import TimeOffRequestSentWindow from '../PopupComponents/TimeOffRequestSentWindow'; import TimeOffApproval from '../PopupComponents/TimeOffApproval'; +import TimeOffRequestResolved from '../PopupComponents/TimeOffRequestResolved'; import { currentUserID } from '../../testConfig'; /** @@ -33,13 +34,15 @@ export default function UpdatesList({updates, refresh, style}) { const [newMember, setNewMember] = useState(false); const [requestSent, setRequestSent] = useState(false); const [approval, setApproval] = useState(false); + const [requestResolved, setRequestResolved] = useState(false); //Details for each notification popup component const [newMemberDetails, setNewMemberDetails] = useState({}); const [requestSentDetails, setRequestSentDetails] = useState({}); const [approvalDetails, setApprovalDetails] = useState({}); + const [requestResolvedDetails, setRequestResolvedDetails] = useState({}); //ID of the currently logged in employee - const currentUserId = currentUserID; + const currentUser = currentUserID; //URL endpoints to be used for API calls const notificationURL = `http://localhost:5000/api/notifications/`; @@ -56,9 +59,9 @@ export default function UpdatesList({updates, refresh, style}) { notificationURL, { notificationId: up.id, - employeeEmpId: currentUserId, - status: (checkNotificationStatus(up, currentUserId) === "new" - || checkNotificationStatus(up, currentUserId) === "waiting") ? "seen" : "new" + employeeEmpId: currentUser, + status: (checkNotificationStatus(up, currentUser) === "new" + || checkNotificationStatus(up, currentUser) === "waiting") ? "seen" : "new" }, { params: { id: up.id } } ) @@ -73,7 +76,7 @@ export default function UpdatesList({updates, refresh, style}) { //Function for retrieving update details for popup component function retrieveDetails(up) { - console.log("Running retrieveDetails()") + //console.log("Running retrieveDetails()") //Retrieve details for "New team member added" update if (up.subject === "New team member added") { const details = { @@ -120,6 +123,17 @@ export default function UpdatesList({updates, refresh, style}) { }; setRequestSentDetails(details); } + //Retrieve details for "Time off request approved/rejected" update + else if (up.subject === "Your time off request has been approved" || + up.subject === "Your time off request has been rejected") { + const details = { + startDate: dayjs(up.timeOffHistory.startDate).format("MMM D, YYYY"), + endDate: dayjs(up.timeOffHistory.endDate).format("MMM D, YYYY"), + status: up.timeOffHistory.status, + notes: up.timeOffHistory.note + }; + setRequestResolvedDetails(details); + } }; return ( @@ -131,14 +145,14 @@ export default function UpdatesList({updates, refresh, style}) { {updates.map((update) => ( {/*Update status*/} - {checkNotificationStatus(update, currentUserId) === "new" && {/*Update name and description*/} {update.subject} @@ -146,7 +160,7 @@ export default function UpdatesList({updates, refresh, style}) { {/*Mark as read/unread button*/} handleSwitch(update)}> - Mark as {checkNotificationStatus(update, currentUserId) === "seen" && 'un'}read + Mark as {checkNotificationStatus(update, currentUser) === "seen" && 'un'}read {/*View button*/} @@ -158,12 +172,16 @@ export default function UpdatesList({updates, refresh, style}) { if (update.subject === "New team member added") { setNewMember(true); } - if (update.subject === "New time off request") { + else if (update.subject === "New time off request") { setApproval(true); } - if (update.subject === "Your time off request has been sent") { + else if (update.subject === "Your time off request has been sent") { setRequestSent(true); } + else if (update.subject === "Your time off request has been approved" || + update.subject === "Your time off request has been rejected") { + setRequestResolved(true); + } }} > {/*New team member added update popup component*/} setNewMember(false)}> - setNewMember(false)} /> + setNewMember(false)} + /> {/*Time off request sent update popup component*/} setRequestSent(false)}> - setRequestSent(false)} /> + setRequestSent(false)} + /> {/*New time off request update popup component*/} setApproval(false)}> @@ -201,6 +225,12 @@ export default function UpdatesList({updates, refresh, style}) { }} /> + setRequestResolved(false)}> + setRequestResolved(false)} + /> + ); }; From c054aa7c09f6eb07a50b7c04a46fefe1cd831976 Mon Sep 17 00:00:00 2001 From: GabrielChan1 Date: Wed, 28 Aug 2024 14:17:10 -0400 Subject: [PATCH 5/7] Issue 73: Modify table structure and add additional messages to popup components --- src/components/PopupComponents/TimeOffRequest.jsx | 11 ++++++++++- .../PopupComponents/TimeOffRequestResolved.jsx | 2 +- src/components/TimeOffPage/BoardTabContent.jsx | 5 +++-- src/components/TimeOffPage/UpcomingTimeOffTable.jsx | 5 +++-- src/components/UpdatesPage/UpdatesList.jsx | 4 ++-- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/PopupComponents/TimeOffRequest.jsx b/src/components/PopupComponents/TimeOffRequest.jsx index ab2b5e267..deb7adaf7 100644 --- a/src/components/PopupComponents/TimeOffRequest.jsx +++ b/src/components/PopupComponents/TimeOffRequest.jsx @@ -357,7 +357,8 @@ export default function TimeOffRequest({ startDate: dayjs(from).toString(), endDate: dayjs(to).toString(), hours: totalHoursOff, - timeOffId: category.timeOffId + timeOffId: category.timeOffId, + status: "Pending" }; axios.put(timeOffPeriodURL, updatedPeriod) .then((response) => { @@ -518,6 +519,14 @@ export default function TimeOffRequest({ day ({halfDaysOff * 4} hrs) will be requested ({totalHoursOff} hrs in total).

+ {/*Warning message when editing an already approved request*/} + {(initialRequest && initialRequest.status === "Approved") && +

+ This time off request has already been approved. If you make any changes to + this request, it will need to be approved again. +

+ } + {/*Validation messages to be displayed if the request being submitted is invalid */} {!validDates &&

Your time off request dates overlap with existing upcoming time off periods.

} diff --git a/src/components/PopupComponents/TimeOffRequestResolved.jsx b/src/components/PopupComponents/TimeOffRequestResolved.jsx index bf1154612..2d23fc919 100644 --- a/src/components/PopupComponents/TimeOffRequestResolved.jsx +++ b/src/components/PopupComponents/TimeOffRequestResolved.jsx @@ -45,7 +45,7 @@ export default function TimeOffRequestResolved({time_off_information, close, sty

Your time off request has been {result}

- setRefresh(!refresh)} style={{marginBottom: "30px"}} diff --git a/src/components/TimeOffPage/UpcomingTimeOffTable.jsx b/src/components/TimeOffPage/UpcomingTimeOffTable.jsx index 50eef9d63..ff4c85889 100644 --- a/src/components/TimeOffPage/UpcomingTimeOffTable.jsx +++ b/src/components/TimeOffPage/UpcomingTimeOffTable.jsx @@ -75,7 +75,8 @@ export default function UpcomingTimeOffTable({ from: dayjs(period.from).toDate(), to: dayjs(period.to).toDate(), hours: period.hours, - type: period.type + type: period.type, + status: period.status } setTimeOffDetails(details); }; @@ -107,7 +108,7 @@ export default function UpcomingTimeOffTable({ Note } - {tableColumns.includes("Status") && Status} + {tableColumns.includes("Status") && Status}
diff --git a/src/components/UpdatesPage/UpdatesList.jsx b/src/components/UpdatesPage/UpdatesList.jsx index eaca16f97..6c3d47517 100644 --- a/src/components/UpdatesPage/UpdatesList.jsx +++ b/src/components/UpdatesPage/UpdatesList.jsx @@ -104,7 +104,7 @@ export default function UpdatesList({updates, refresh, style}) { up.employeeAnnualTimeOff.cumulativeHoursTaken), timeOffRequested: `${dayjs(up.timeOffHistory.startDate).format("DD/MM/YYYY")} - ${dayjs(up.timeOffHistory.endDate).format("DD/MM/YYYY")}`, - requestedDaysTotal: Math.ceil(up.timeOffHistory.hours / 24), + requestedDaysTotal: dayjs(up.timeOffHistory.endDate).diff(dayjs(up.timeOffHistory.startDate), "day"), timeOffCategory: up.timeOff.category, status: up.timeOffHistory.status }; @@ -117,7 +117,7 @@ export default function UpdatesList({updates, refresh, style}) { up.employeeAnnualTimeOff.cumulativeHoursTaken), timeOffRequested: `${dayjs(up.timeOffHistory.startDate).format("DD/MM/YYYY")} - ${dayjs(up.timeOffHistory.endDate).format("DD/MM/YYYY")}`, - requestedDaysTotal: Math.ceil(up.timeOffHistory.hours / 24), + requestedDaysTotal: dayjs(up.timeOffHistory.endDate).diff(dayjs(up.timeOffHistory.startDate), "day"), timeOffCategory: up.timeOff.category, notes: up.timeOffHistory.note }; From 90053a5c37c0affc53ba5970c3ad837a9fe955de Mon Sep 17 00:00:00 2001 From: GabrielChan1 Date: Wed, 28 Aug 2024 15:12:06 -0400 Subject: [PATCH 6/7] Issue 73: Optimize code to remove warnings --- src/App.js | 2 +- src/components/BasicMenus/MenuToggleButton.jsx | 4 +++- src/components/TimeOffPage/HistoryTabContent.jsx | 4 ++-- src/components/TimeOffPage/TeamTabContent.jsx | 4 ++-- src/components/TimeOffPage/UpcomingTimeOffTable.jsx | 10 ++-------- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/App.js b/src/App.js index fcb565300..0f242ae36 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import logo from './logo.svg'; +//import logo from './logo.svg'; import './App.css'; import TimeOffPage from './components/TimeOffPage/TimeOffPage'; function App() { diff --git a/src/components/BasicMenus/MenuToggleButton.jsx b/src/components/BasicMenus/MenuToggleButton.jsx index 18708935a..02ab62276 100644 --- a/src/components/BasicMenus/MenuToggleButton.jsx +++ b/src/components/BasicMenus/MenuToggleButton.jsx @@ -32,7 +32,9 @@ export default function MenuToggleButton({label, menuItems, icon, style}) { const [selected, setSelected] = useState(false); const [display, setDisplay] = useState("none"); - useEffect(() => {setDisplay((selected) ? "block" : "none")}); + useEffect(() => { + setDisplay((selected) ? "block" : "none") + }, [selected]); const StyledChip = styled(Chip)({ backgroundColor: (selected) ? "#EAECF0" : "#FFFFFF", diff --git a/src/components/TimeOffPage/HistoryTabContent.jsx b/src/components/TimeOffPage/HistoryTabContent.jsx index d84bbe8e2..eb52e7f11 100644 --- a/src/components/TimeOffPage/HistoryTabContent.jsx +++ b/src/components/TimeOffPage/HistoryTabContent.jsx @@ -43,7 +43,7 @@ export default function HistoryTabContent({style}) { //Time off periods to be displayed const [timeOffPeriods, setTimeOffPeriods] = useState([]); //Hook for refreshing the list of time off periods - const [refresh, setRefresh] = useState(false); + //const [refresh, setRefresh] = useState(false); //Filter table columns depending on which filters are active //"From", "To" and at least one other column will always be active @@ -61,7 +61,7 @@ export default function HistoryTabContent({style}) { //Refresh the list of time off periods useEffect(() => { getTimeOffPeriods(); - }, [refresh]); + }, []); //Function for retrieving any past time off periods function getTimeOffPeriods() { diff --git a/src/components/TimeOffPage/TeamTabContent.jsx b/src/components/TimeOffPage/TeamTabContent.jsx index c7a5362d5..f85d98d5f 100644 --- a/src/components/TimeOffPage/TeamTabContent.jsx +++ b/src/components/TimeOffPage/TeamTabContent.jsx @@ -43,7 +43,7 @@ export default function TeamTabContent({style}) { //Time off periods to be displayed const [timeOffPeriods, setTimeOffPeriods] = useState([]); //Hook for refreshing the list of time off periods - const [refresh, setRefresh] = useState(false); + //const [refresh, setRefresh] = useState(false); //ID of the currently logged in employee const currentUser = currentUserID; @@ -60,7 +60,7 @@ export default function TeamTabContent({style}) { //Refresh the list of time off periods useEffect(() => { getTimeOffPeriods(); - }, [refresh]); + }, []); //Function for retrieving all the time off periods for the current manager's team function getTimeOffPeriods() { diff --git a/src/components/TimeOffPage/UpcomingTimeOffTable.jsx b/src/components/TimeOffPage/UpcomingTimeOffTable.jsx index ff4c85889..c0d6c4f8f 100644 --- a/src/components/TimeOffPage/UpcomingTimeOffTable.jsx +++ b/src/components/TimeOffPage/UpcomingTimeOffTable.jsx @@ -171,15 +171,9 @@ export default function UpcomingTimeOffTable({ setEditTimeOff(true); }} > - + Edit - + From f283c05e610875284b6e711020474bec62c608b7 Mon Sep 17 00:00:00 2001 From: GabrielChan1 Date: Wed, 28 Aug 2024 18:08:08 -0400 Subject: [PATCH 7/7] Issue 73: Remove ripple effect from buttons --- src/components/Button/HRMButton.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Button/HRMButton.jsx b/src/components/Button/HRMButton.jsx index 51be3529c..61c384d63 100644 --- a/src/components/Button/HRMButton.jsx +++ b/src/components/Button/HRMButton.jsx @@ -109,6 +109,7 @@ export default function HRMButton({mode, children, startIcon, endIcon, onClick, onClick={onClick} disabled={!enabled} disableElevation + disableRipple > {children} @@ -122,6 +123,7 @@ export default function HRMButton({mode, children, startIcon, endIcon, onClick, variant="outlined" onClick={onClick} disabled={!enabled} + disableRipple > {children} @@ -135,6 +137,7 @@ export default function HRMButton({mode, children, startIcon, endIcon, onClick, variant="text" onClick={onClick} disabled={!enabled} + disableRipple > {children}