diff --git a/.circleci/config.yml b/.circleci/config.yml index f9f4c69ea7..276f1efb6b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: command: 'sudo npm install' - run: name: Run react build - command: 'sudo REACT_APP_APIENDPOINT=$APIENDPOINT_PROD REACT_APP_DEF_PWD=$REACT_APP_DEF_PWD REACT_APP_SENTRY_URL=$SENTRY_URL_PROD SKIP_PREFLIGHT_CHECK=true NODE_OPTIONS=$NODE_OPTIONS npm run build && sudo mv build/index.html build/200.html' + command: 'sudo REACT_APP_APIENDPOINT=$APIENDPOINT_PROD REACT_APP_DEF_PWD=$REACT_APP_DEF_PWD REACT_APP_SENTRY_URL=$SENTRY_URL_PROD SKIP_PREFLIGHT_CHECK=true NODE_OPTIONS="$NODE_OPTIONS --max_old_space_size=4096" npm run build --prod && sudo mv build/index.html build/200.html' - run: name: Export error log if 'Build the React client' failed command: | @@ -62,7 +62,7 @@ jobs: command: npm install - run: name: Build the React client - command: export REACT_APP_APIENDPOINT=$APIENDPOINT_DEV REACT_APP_DEF_PWD=$REACT_APP_DEF_PWD REACT_APP_SENTRY_URL=$SENTRY_URL_DEV SKIP_PREFLIGHT_CHECK=true NODE_OPTIONS=$NODE_OPTIONS && npm run build + command: export REACT_APP_APIENDPOINT=$APIENDPOINT_DEV REACT_APP_DEF_PWD=$REACT_APP_DEF_PWD REACT_APP_SENTRY_URL=$SENTRY_URL_DEV SKIP_PREFLIGHT_CHECK=true NODE_OPTIONS="$NODE_OPTIONS --max_old_space_size=4096" && npm run build --prod - run: name: Export error log if 'Build the React client' failed command: | @@ -84,7 +84,7 @@ jobs: name: Deploy compiled app to surge.sh on $SURGE_DOMAIN_DEV command: ./node_modules/.bin/surge --domain $SURGE_DOMAIN_DEV --project ./build - run: - name: Deploy compiled app to surge.sh on $SURGE_DOMAIN_DEV + name: Deploy compiled app to surge.sh on $SURGE_DOMAIN_DEV1 command: ./node_modules/.bin/surge --domain $SURGE_DOMAIN_DEV1 --project ./build - save_cache: key: v1.1-dependencies-{{ checksum "package-lock.json" }} @@ -106,7 +106,7 @@ jobs: command: npm install - run: name: Build the React client - command: export REACT_APP_APIENDPOINT=$APIENDPOINT_BETA REACT_APP_DEF_PWD=$REACT_APP_DEF_PWD SKIP_PREFLIGHT_CHECK=true NODE_OPTIONS=$NODE_OPTIONS && npm run build + command: export REACT_APP_APIENDPOINT=$APIENDPOINT_BETA REACT_APP_DEF_PWD=$REACT_APP_DEF_PWD SKIP_PREFLIGHT_CHECK=true NODE_OPTIONS="$NODE_OPTIONS --max_old_space_size=4096" && npm run build --prod - run: name: Export error log if 'Build the React client' failed command: | diff --git a/.eslintignore b/.eslintignore index 1ecdc0e014..8e8e6fee4f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,7 +26,6 @@ src/components/TaskEditSuggestions/** src/components/TeamMemberTasks/** src/components/Teams/** src/components/TeamLocations/** -src/components/Timelog/** src/components/UserManagement/** src/components/UserProfile/** src/components/Announcements/** diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 676369fd15..b880bcf7d3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,10 +11,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '14' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3509ed2dbb..a426f3d1f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '14' @@ -25,7 +25,7 @@ jobs: - name: Upload test results if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results path: test-results # Adjust the path to your test results if necessary diff --git a/.prettierignore b/.prettierignore index 03e1f8649b..7e56f6393e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,7 +27,6 @@ src/components/TaskEditSuggestions/** src/components/TeamLocations/** src/components/TeamMemberTasks/** src/components/Teams/** -src/components/Timelog/** src/components/UserManagement/** src/components/UserProfile/** src/components/Announcements/** diff --git a/src/components/Badge/BadgeHistory.jsx b/src/components/Badge/BadgeHistory.jsx index e3fcddcea5..7020079c57 100644 --- a/src/components/Badge/BadgeHistory.jsx +++ b/src/components/Badge/BadgeHistory.jsx @@ -3,21 +3,26 @@ import { WEEK_DIFF } from '../../constants/badge'; function BadgeHistory({ badges, personalBestMaxHrs }) { const filterBadges = allBadges => { - const filteredList = allBadges.filter( - value => Date.now() - new Date(value.lastModified).getTime() > WEEK_DIFF, + if (!Array.isArray(allBadges)) return []; + + const filteredList = allBadges.filter(value => + value && value.lastModified && + (Date.now() - new Date(value.lastModified).getTime() > WEEK_DIFF) ); filteredList.sort((a, b) => { - if (a.badge.ranking === 0) return 1; - if (b.badge.ranking === 0) return -1; - if (a.badge.ranking > b.badge.ranking) return 1; - if (a.badge.ranking < b.badge.ranking) return -1; - if (a.badge.badgeName > b.badge.badgeName) return 1; - if (a.badge.badgeName < b.badge.badgeName) return -1; - - // If all conditions fail, return 0 to indicate that elements are equal - return 0; + const rankingA = a?.badge?.ranking ?? Infinity; + const rankingB = b?.badge?.ranking ?? Infinity; + const nameA = a?.badge?.badgeName ?? ''; + const nameB = b?.badge?.badgeName ?? ''; + + if (rankingA === 0) return 1; + if (rankingB === 0) return -1; + if (rankingA > rankingB) return 1; + if (rankingA < rankingB) return -1; + return nameA.localeCompare(nameB); }); + return filteredList; }; @@ -26,17 +31,19 @@ function BadgeHistory({ badges, personalBestMaxHrs }) { return (
{filteredBadges.map((value, index) => ( - + value && value.badge ? ( + + ) : null ))}
); } -export default BadgeHistory; +export default BadgeHistory; \ No newline at end of file diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css index 39b7507efb..5418405f7a 100644 --- a/src/components/Header/Header.css +++ b/src/components/Header/Header.css @@ -3,15 +3,15 @@ width: clamp(100vw, 0.1rem + 1vw, 100%); } -.close-button { +close-button { position: absolute; top: 0; left: 0; - padding: 12px; + padding: 12px; /* Add some padding for easier clicking */ } .card-content { - padding: 5px; + padding: 5px; /* Add padding around the card content */ padding-left: 40px; padding-right: 40px; white-space: pre-line; @@ -20,7 +20,7 @@ } .navbar { - z-index: 100; + /* z-index: 100; */ white-space: nowrap; /* margin-bottom: 20px; */ } @@ -59,12 +59,12 @@ align-items: center; } -.dropdown-item-hover:hover{ +.dropdown-item-hover:hover { background-color: #2f4157 !important; } @media (max-width: 1400px) { - .nav-links{ + .nav-links { flex-direction: column !important; } } @@ -98,7 +98,7 @@ } @media screen and (max-width: 769px) { - .responsive-spacing{ + .responsive-spacing { margin-right: 5px; } } diff --git a/src/components/LBDashboard/Auth/Auth.css b/src/components/LBDashboard/Auth/Auth.css index 487c75ff5f..386af92433 100644 --- a/src/components/LBDashboard/Auth/Auth.css +++ b/src/components/LBDashboard/Auth/Auth.css @@ -20,7 +20,7 @@ min-width: 250px; } - .form-container { + .lb-auth-page .form-container { background-color: white; border: 1px solid #ccc; border-radius: 8px; @@ -68,7 +68,7 @@ min-width: 140px; } - form { + .lb-auth-page form { display: flex; flex-direction: column; width: 100%; diff --git a/src/components/LBDashboard/Auth/Login.jsx b/src/components/LBDashboard/Auth/Login.jsx index 8a7dac29d4..272eff4e99 100644 --- a/src/components/LBDashboard/Auth/Login.jsx +++ b/src/components/LBDashboard/Auth/Login.jsx @@ -59,7 +59,7 @@ function Login() { }; return ( -
+
One Community Logo
diff --git a/src/components/LBDashboard/Auth/Register.jsx b/src/components/LBDashboard/Auth/Register.jsx index af72d76296..66d52b46b1 100644 --- a/src/components/LBDashboard/Auth/Register.jsx +++ b/src/components/LBDashboard/Auth/Register.jsx @@ -65,7 +65,7 @@ function Register() { }; return ( -
+
One Community Logo
diff --git a/src/components/Projects/WBS/WBSDetail/WBSTasks.jsx b/src/components/Projects/WBS/WBSDetail/WBSTasks.jsx index b5978dd5e9..8d0cf04ca9 100644 --- a/src/components/Projects/WBS/WBSDetail/WBSTasks.jsx +++ b/src/components/Projects/WBS/WBSDetail/WBSTasks.jsx @@ -195,10 +195,8 @@ function WBSTasks(props) {
{/* */} {canPostTask ? ( diff --git a/src/components/Projects/WBS/WBSDetail/wbs.css b/src/components/Projects/WBS/WBSDetail/wbs.css index 175944b660..ccc8e4b6b4 100644 --- a/src/components/Projects/WBS/WBSDetail/wbs.css +++ b/src/components/Projects/WBS/WBSDetail/wbs.css @@ -265,9 +265,18 @@ .tasks-detail-actions { /* action column width reduced */ width: 6%; } -@media (max-width: 768px) { /* arranging the buttons */ - .button-group button { - margin-bottom: 10px; - } +.wbs-button-group { + justify-content: flex-start; + gap: 10px; + margin-top: 20px; } +@media (max-width: 968px) { + .wbs-button-group { + align-items: flex-start; + margin-bottom: 10px; + } + .wbs-button-group button { + margin-bottom: 10px; + } +} \ No newline at end of file diff --git a/src/components/Reports/BadgeSummaryViz.jsx b/src/components/Reports/BadgeSummaryViz.jsx index 235e4816e4..3a143f1902 100644 --- a/src/components/Reports/BadgeSummaryViz.jsx +++ b/src/components/Reports/BadgeSummaryViz.jsx @@ -33,21 +33,28 @@ function BadgeSummaryViz({ authId, userId, badges, dashboard }) { useEffect(() => { try { if (badges && badges.length) { - const sortBadges = [...badges].sort((a, b) => { - if (a?.badge?.ranking === 0) return 1; - if (b?.badge?.ranking === 0) return -1; - if (a?.badge?.ranking > b?.badge?.ranking) return 1; - if (a?.badge?.ranking < b?.badge?.ranking) return -1; - if (a?.badge?.badgeName > b?.badge?.badgeName) return 1; - if (a?.badge?.badgeName < b?.badge?.badgeName) return -1; - return 0; - }); + const sortBadges = badges + .filter(badge => badge && badge.badge) // Filter out null or undefined badges + .sort((a, b) => { + const rankingA = a.badge?.ranking ?? Infinity; + const rankingB = b.badge?.ranking ?? Infinity; + const nameA = a.badge?.badgeName ?? ''; + const nameB = b.badge?.badgeName ?? ''; + + if (rankingA === 0) return 1; + if (rankingB === 0) return -1; + if (rankingA > rankingB) return 1; + if (rankingA < rankingB) return -1; + return nameA.localeCompare(nameB); + }); setSortedBadges(sortBadges); + } else { + setSortedBadges([]); } } catch (error) { - console.log(error); + console.error("Error sorting badges:", error); + setSortedBadges([]); } - }, [badges]); const toggle = () => setIsOpen(prev => !prev); @@ -81,15 +88,16 @@ function BadgeSummaryViz({ authId, userId, badges, dashboard }) { {badges && badges.length>0 ? ( sortedBadges && - sortedBadges.map(value => value &&( - + sortedBadges.map(value => value && value.badge && ( + - {' '} - badge + {value.badge.imageUrl && ( + badge + )} {badges && badges.length ? ( sortedBadges && - sortedBadges.map(value => value &&( - + sortedBadges.map(value => value && value.badge && ( + - {' '} - badge + {value.badge.imageUrl && ( + badge + )} + // ... rest of the code diff --git a/src/components/Timelog/DeleteModal.jsx b/src/components/Timelog/DeleteModal.jsx index d92d711d3b..ac9772ebe5 100644 --- a/src/components/Timelog/DeleteModal.jsx +++ b/src/components/Timelog/DeleteModal.jsx @@ -1,24 +1,24 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap'; import { useDispatch } from 'react-redux'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import { toast } from 'react-toastify'; import { deleteTimeEntry } from '../../actions/timeEntries'; -import {toast} from 'react-toastify'; -const DeleteModal = ({ timeEntry, projectCategory, taskClassification,userProfile }) => { +function DeleteModal({ timeEntry }) { const [isOpen, setOpen] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const dispatch = useDispatch(); - const toggle = () => setOpen(isOpen => !isOpen); + const toggle = () => setOpen(_isOpen => !_isOpen); const deleteEntry = async () => { setIsProcessing(true); try { await dispatch(deleteTimeEntry(timeEntry)); } catch (error) { - toast.error(`An error occurred while dispatching delete time entry action: ${error.message}`) + toast.error(`An error occurred while dispatching delete time entry action: ${error.message}`); } setIsProcessing(false); toggle(); @@ -40,6 +40,6 @@ const DeleteModal = ({ timeEntry, projectCategory, taskClassification,userProfil ); -}; +} export default DeleteModal; diff --git a/src/components/Timelog/EffortBar.jsx b/src/components/Timelog/EffortBar.jsx index c9c5fd956a..6bea70e67f 100644 --- a/src/components/Timelog/EffortBar.jsx +++ b/src/components/Timelog/EffortBar.jsx @@ -1,13 +1,12 @@ -import React from 'react'; import { useSelector } from 'react-redux'; -const EffortBar = ({ activeTab, projectsSelected }) => { +function EffortBar({ activeTab, projectsSelected }) { const data = useSelector(state => activeTab === 4 ? state.timeEntries.period : state.timeEntries.weeks[activeTab - 1], ); - const calculateTotalTime = (data, isTangible) => { - const filteredData = data.filter( + const calculateTotalTime = (d, isTangible) => { + const filteredData = d.filter( entry => entry.isTangible === isTangible && (projectsSelected.includes('all') || projectsSelected.includes(entry.projectId)), @@ -33,6 +32,6 @@ const EffortBar = ({ activeTab, projectsSelected }) => { Total Effort: {totalTime.toFixed(2)} hrs
); -}; +} export default EffortBar; diff --git a/src/components/Timelog/TimeEntry.jsx b/src/components/Timelog/TimeEntry.jsx index 2829132f5a..0f04ea6263 100644 --- a/src/components/Timelog/TimeEntry.jsx +++ b/src/components/Timelog/TimeEntry.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Card, Row, Col } from 'reactstrap'; import { useDispatch, connect } from 'react-redux'; import ReactHtmlParser from 'react-html-parser'; @@ -6,85 +6,81 @@ import moment from 'moment-timezone'; import './Timelog.css'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEdit } from '@fortawesome/free-regular-svg-icons'; +import hasPermission from 'utils/permissions'; +import { hrsFilterBtnColorMap } from 'constants/colors'; +import { cantUpdateDevAdminDetails } from 'utils/permissions'; +import { toast } from 'react-toastify'; import TimeEntryForm from './TimeEntryForm'; import DeleteModal from './DeleteModal'; import { editTimeEntry, getTimeEntriesForWeek } from '../../actions/timeEntries'; import { editTeamMemberTimeEntry } from '../../actions/task'; -import hasPermission from 'utils/permissions'; -import { hrsFilterBtnColorMap } from 'constants/colors'; -import { cantUpdateDevAdminDetails } from 'utils/permissions'; -import { toast } from 'react-toastify'; /** * This component can be imported in TimeLog component's week tabs and Tasks tab * 1. In TimeLog - current week time log, last week, week before ... tabs: * time entry data are from state.timeEntries; * time entry user profile is from state.userProfile - * + * * 2. In TimeLog - Tasks tab: * time entry data and user profile are both from state.teamMemberTasks.usersWithTimeEntries - * + * * check string value of from to decide which state to change upon time entry edit */ -const TimeEntry = (props) => { +function TimeEntry(props) { // props from parent - const { from, data, displayYear, timeEntryUserProfile, displayUserProjects, displayUserTasks, tab } = props + const { from, data, displayYear, timeEntryUserProfile, tab } = props; // props from store const { authUser } = props; const { _id: timeEntryUserId } = timeEntryUserProfile; const { _id: timeEntryId } = data; - const { - dateOfWork, - isTangible, - hours, - minutes, - projectName, - taskName, - taskId, - notes, - } = data; + const { dateOfWork, isTangible, hours, minutes, projectName, taskName, taskId, notes } = data; const [timeEntryFormModal, setTimeEntryFormModal] = useState(false); const [isProcessing, setIsProcessing] = useState(false); - const [filteredColor,setFilteredColor] = useState(hrsFilterBtnColorMap[7]); + const [filteredColor, setFilteredColor] = useState(hrsFilterBtnColorMap[7]); const dispatch = useDispatch(); - const hasATimeEntryEditPermission = props.hasPermission('editTimeEntryTime') || + const hasATimeEntryEditPermission = + props.hasPermission('editTimeEntryTime') || props.hasPermission('editTimeEntryDescription') || props.hasPermission('editTimeEntryDate'); - - const cantEditJaeRelatedRecord = cantUpdateDevAdminDetails(timeEntryUserProfile?.email ? timeEntryUserProfile.email : '', authUser.email); + const cantEditJaeRelatedRecord = cantUpdateDevAdminDetails( + timeEntryUserProfile?.email ? timeEntryUserProfile.email : '', + authUser.email, + ); const toggle = () => setTimeEntryFormModal(modal => !modal); const isAuthUser = timeEntryUserId === authUser.userid; - const isSameDay = moment().tz('America/Los_Angeles').format('YYYY-MM-DD') === dateOfWork; + const isSameDay = + moment() + .tz('America/Los_Angeles') + .format('YYYY-MM-DD') === dateOfWork; - //default permission: auth use can edit own sameday timelog entry, but not tangibility + // default permission: auth use can edit own sameday timelog entry, but not tangibility const isAuthUserAndSameDayEntry = isAuthUser && isSameDay; - //permission to edit any time log entry (from other user's Dashboard - // For Administrator/Owner role, hasPermission('editTimelogInfo') should be true by default - const canEditTangibility = ( - isAuthUser ? - dispatch(hasPermission('toggleTangibleTime')): - dispatch(hasPermission('editTimeEntryToggleTangible')) - ) && !cantEditJaeRelatedRecord; + // permission to edit any time log entry (from other user's Dashboard + // For Administrator/Owner role, hasPermission('editTimelogInfo') should be true by default + const canEditTangibility = + (isAuthUser + ? dispatch(hasPermission('toggleTangibleTime')) + : dispatch(hasPermission('editTimeEntryToggleTangible'))) && !cantEditJaeRelatedRecord; - //permission to Delete any time entry from other user's Dashboard - const canDeleteOther = (dispatch(hasPermission('deleteTimeEntryOthers'))); + // permission to Delete any time entry from other user's Dashboard + const canDeleteOther = dispatch(hasPermission('deleteTimeEntryOthers')); - //permission to delete any time entry on their own time logs tab - const canDeleteOwn=(dispatch(hasPermission('deleteTimeEntryOwn'))); + // permission to delete any time entry on their own time logs tab + const canDeleteOwn = dispatch(hasPermission('deleteTimeEntryOwn')); // condition for allowing delete in delete model - //default permission: delete own sameday tangible entry = isAuthUserAndSameDayEntry + // default permission: delete own sameday tangible entry = isAuthUserAndSameDayEntry const canDelete = canDeleteOther || canDeleteOwn; - + const toggleTangibility = async () => { setIsProcessing(true); const newData = { @@ -107,36 +103,20 @@ const TimeEntry = (props) => { const editFilteredColor = () => { try { const daysPast = moment().diff(dateOfWork, 'days'); - let choosenColor = ""; - switch (daysPast) { - case 0: - choosenColor = hrsFilterBtnColorMap[1] - break; - case 1: - choosenColor = hrsFilterBtnColorMap[2]; - break; - case 2: - choosenColor = hrsFilterBtnColorMap[3]; - break; - case 3: - choosenColor = hrsFilterBtnColorMap[4]; - break; - default: - choosenColor = hrsFilterBtnColorMap[7]; - } + const choosenColor = hrsFilterBtnColorMap[daysPast + 1] || hrsFilterBtnColorMap[7]; setFilteredColor(choosenColor); } catch (error) { - console.log(error); + // eslint-disable-next-line no-console + console.error('Error in editFilteredColor:', error); } - } - - + }; + useEffect(() => { editFilteredColor(); - }, []) + }, []); return ( -
+
{ border: `5px solid ${filteredColor}`, backgroundColor: taskId ? filteredColor : 'white', }} - >
- + /> +
@@ -161,29 +148,27 @@ const TimeEntry = (props) => { {hours}h {minutes}m
Project/Task:
-

- {projectName} +

+ {projectName}
- {taskName && `\u2003 ↳ ${taskName}`} + {taskName && `\u2003 ↳ ${taskName}`}

-
- { - canEditTangibility - ? ( - <> - Tangible:  - - {isProcessing ? Processing... : null} - - ) - : {isTangible ? 'Tangible' : 'Intangible'} - } +
+ {canEditTangibility ? ( + <> + Tangible:  + + {isProcessing ? Processing... : null} + + ) : ( + {isTangible ? 'Tangible' : 'Intangible'} + )}
@@ -193,13 +178,14 @@ const TimeEntry = (props) => { {ReactHtmlParser(notes)}
- {((hasATimeEntryEditPermission || isAuthUserAndSameDayEntry) && !cantEditJaeRelatedRecord) && ( - - )} + {(hasATimeEntryEditPermission || isAuthUserAndSameDayEntry) && + !cantEditJaeRelatedRecord && ( + + )} {canDelete && ( - )} @@ -211,7 +197,7 @@ const TimeEntry = (props) => { {/* this TimeEntryForm could be rendered from either weekly tab or task tab */} { />
); -}; +} -const mapStateToProps = (state) => ({ +const mapStateToProps = state => ({ authUser: state.auth.user, -}) +}); const mapDispatchToProps = dispatch => ({ hasPermission: permission => dispatch(hasPermission(permission)), diff --git a/src/components/Timelog/TimeEntryForm/AboutModal.jsx b/src/components/Timelog/TimeEntryForm/AboutModal.jsx index 6a0a157a4f..95ae5a4992 100644 --- a/src/components/Timelog/TimeEntryForm/AboutModal.jsx +++ b/src/components/Timelog/TimeEntryForm/AboutModal.jsx @@ -6,7 +6,7 @@ import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from 'reactstrap'; * @param {Boolean} props.visible * @param {Func} props.setVisible */ -const AboutModal = props => { +function AboutModal(props) { return ( Info @@ -72,6 +72,6 @@ const AboutModal = props => { ); -}; +} export default AboutModal; diff --git a/src/components/Timelog/TimeEntryForm/ReminderModal.jsx b/src/components/Timelog/TimeEntryForm/ReminderModal.jsx index 8305f49a7f..f730101752 100644 --- a/src/components/Timelog/TimeEntryForm/ReminderModal.jsx +++ b/src/components/Timelog/TimeEntryForm/ReminderModal.jsx @@ -10,7 +10,7 @@ import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from 'reactstrap'; * @param {*} props.inputs * @param {Func} cancelChange */ -const ReminderModal = props => { +function ReminderModal(props) { const { edit, visible, data, inputs, reminder, cancelChange, setVisible, darkMode } = props; return ( @@ -20,16 +20,14 @@ const ReminderModal = props => { - {edit && - (data.hours !== inputs.hours || - data.minutes !== inputs.minutes) && ( - - )} + {edit && (data.hours !== inputs.hours || data.minutes !== inputs.minutes) && ( + + )} ); -}; +} export default ReminderModal; diff --git a/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx b/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx index aedfc869cb..ba205aa313 100644 --- a/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx +++ b/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx @@ -7,7 +7,7 @@ import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from 'reactstrap'; * @param {Func} props.setVisible * @param {Boolean} props.darkMode */ -const TangibleInfoModal = props => { +function TangibleInfoModal(props) { return ( Info @@ -26,6 +26,6 @@ const TangibleInfoModal = props => { ); -}; +} export default TangibleInfoModal; diff --git a/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx b/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx index c4206c6895..6862ccc96f 100644 --- a/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx +++ b/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx @@ -1,4 +1,8 @@ -import React, { useState, useEffect } from 'react'; +/* eslint-disable react/require-default-props */ +/* eslint-disable react/no-unused-prop-types */ +/* eslint-disable react/forbid-prop-types */ +/* eslint-disable no-param-reassign */ +import { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { @@ -15,21 +19,21 @@ import { ModalFooter, } from 'reactstrap'; import moment from 'moment-timezone'; -import { isEmpty, isEqual, set } from 'lodash'; +import { isEmpty, isEqual } from 'lodash'; import { Editor } from '@tinymce/tinymce-react'; import { toast } from 'react-toastify'; import ReactTooltip from 'react-tooltip'; -import { postTimeEntry, editTimeEntry, getTimeEntriesForWeek } from '../../../actions/timeEntries'; import { getUserProfile } from 'actions/userProfile'; +import axios from 'axios'; +import hasPermission from 'utils/permissions'; +import { boxStyle, boxStyleDark } from 'styles'; +import { postTimeEntry, editTimeEntry, getTimeEntriesForWeek } from '../../../actions/timeEntries'; import AboutModal from './AboutModal'; import TangibleInfoModal from './TangibleInfoModal'; import ReminderModal from './ReminderModal'; import TimeLogConfirmationModal from './TimeLogConfirmationModal'; -import axios from 'axios'; import { ENDPOINTS } from '../../../utils/URL'; -import hasPermission from 'utils/permissions'; -import { boxStyle, boxStyleDark } from 'styles'; -import '../../Header/DarkMode.css' +import '../../Header/DarkMode.css'; // Images are not allowed in timelog const customImageUploadHandler = () => @@ -44,6 +48,7 @@ const TINY_MCE_INIT_OPTIONS = { placeholder: 'Description (10-word minimum) and reference link', plugins: 'advlist autolink autoresize lists link charmap table paste help wordcount', toolbar: + // eslint-disable-next-line no-multi-str 'bold italic underline link removeformat | bullist numlist outdent indent |\ styleselect fontsizeselect | table| strikethrough forecolor backcolor |\ subscript superscript charmap | help', @@ -74,36 +79,32 @@ const TINY_MCE_INIT_OPTIONS = { * @returns */ -const TimeEntryForm = props => { - /*---------------- variables -------------- */ +function TimeEntryForm(props) { + /* ---------------- variables -------------- */ // props from parent - const { from, sendStop, edit, data, toggle, isOpen, tab, userProfile, darkMode } = props; + const { from, sendStop, edit, data, toggle, isOpen, tab, darkMode } = props; // props from store const { authUser } = props; const viewingUser = JSON.parse(sessionStorage.getItem('viewingUser') ?? '{}'); - const initialFormValues = Object.assign( - { - dateOfWork: moment() - .tz('America/Los_Angeles') - .format('YYYY-MM-DD'), - personId: viewingUser.userId ?? authUser.userid, - projectId: '', - wbsId: '', - taskId: '', - hours: 0, - minutes: 0, - notes: '', - isTangible: from === 'Timer' ? true : false, - entryType: 'default', - }, - data, - ); + const initialFormValues = { + dateOfWork: moment() + .tz('America/Los_Angeles') + .format('YYYY-MM-DD'), + personId: viewingUser.userId ?? authUser.userid, + projectId: '', + wbsId: '', + taskId: '', + hours: 0, + minutes: 0, + notes: '', + isTangible: from === 'Timer', + entryType: 'default', + ...data, + }; - const timeEntryUserId = from === 'Timer' - ? (viewingUser.userId ?? authUser.userid) - : data.personId; + const timeEntryUserId = from === 'Timer' ? viewingUser.userId ?? authUser.userid : data.personId; const { dateOfWork: initialDateOfWork, @@ -118,8 +119,8 @@ const TimeEntryForm = props => { const timeEntryInitialProjectOrTaskId = edit ? initialProjectId + - (!!initialwbsId ? '/' + initialwbsId : '') + - (!!initialTaskId ? '/' + initialTaskId : '') + (initialwbsId ? `/${initialwbsId}` : '') + + (initialTaskId ? `/${initialTaskId}` : '') : 'defaultProject'; const initialReminder = { @@ -133,7 +134,6 @@ const TimeEntryForm = props => { const [formValues, setFormValues] = useState(initialFormValues); const [timeEntryFormUserProfile, setTimeEntryFormUserProfile] = useState(null); const [timeEntryFormUserProjects, setTimeEntryFormUserProjects] = useState([]); - const [timeEntryFormUserWBSs, setTimeEntryFormUserWBSs] = useState([]); const [timeEntryFormUserTasks, setTimeEntryFormUserTasks] = useState([]); const [projectOrTaskId, setProjectOrTaskId] = useState(timeEntryInitialProjectOrTaskId); const [isAsyncDataLoaded, setIsAsyncDataLoaded] = useState(false); @@ -163,17 +163,17 @@ const TimeEntryForm = props => { const canChangeTime = from !== 'Timer' && (from === 'TimeLog' || canEditTimeEntryTime || isSameDayAuthUserEdit); - /*---------------- methods -------------- */ + /* ---------------- methods -------------- */ const toggleRemainder = () => - setReminder(reminder => ({ - ...reminder, - openModal: !reminder.openModal, + setReminder(r => ({ + ...r, + openModal: !r.openModal, })); const cancelChange = () => { setReminder(initialReminder); - setFormValues(formValues => ({ - ...formValues, + setFormValues(fv => ({ + ...fv, hours: initialHours, minutes: initialMinutes, })); @@ -194,18 +194,23 @@ const TimeEntryForm = props => { const handleInputChange = event => { event.persist(); - const target = event.target; - switch (target.name) { - case 'hours': - if (+target.value < 0 || +target.value > 40) return; - return setFormValues(formValues => ({ ...formValues, hours: +target.value })); - case 'minutes': - if (+target.value < 0 || +target.value > 59) return; - return setFormValues(formValues => ({ ...formValues, minutes: +target.value })); - case 'isTangible': - return setFormValues(formValues => ({ ...formValues, isTangible: target.checked })); - default: - return setFormValues(formValues => ({ ...formValues, [target.name]: target.value })); + const { name, value, checked } = event.target; + + const updateFormValues = (key, val) => { + setFormValues(fv => ({ ...fv, [key]: val })); + }; + + if (name === 'hours' || name === 'minutes') { + const numValue = +value; + const isValid = + name === 'hours' ? numValue >= 0 && numValue <= 40 : numValue >= 0 && numValue <= 59; + if (isValid) { + updateFormValues(name, numValue); + } + } else if (name === 'isTangible') { + updateFormValues(name, checked); + } else { + updateFormValues(name, value); } }; @@ -213,8 +218,8 @@ const TimeEntryForm = props => { const optionValue = event.target.value; const ids = optionValue.split('/'); const [projectId, wbsId, taskId] = ids.length > 1 ? ids : [ids[0], null, null]; - setFormValues(formValues => ({ - ...formValues, + setFormValues(fv => ({ + ...fv, projectId, wbsId, taskId, @@ -226,15 +231,14 @@ const TimeEntryForm = props => { const { wordcount } = editor.plugins; const hasLink = content.indexOf('http://') > -1 || content.indexOf('https://') > -1; const enoughWords = wordcount.body.getWordCount() > 10; - setFormValues(formValues => ({ ...formValues, [editor.id]: content })); - setReminder(reminder => ({ - ...reminder, + setFormValues(fv => ({ ...fv, [editor.id]: content })); + setReminder(r => ({ + ...r, enoughWords, hasLink, })); }; - const validateForm = isTimeModified => { const errorObj = {}; const remindObj = { ...initialReminder }; @@ -295,63 +299,42 @@ const TimeEntryForm = props => { if (closed === true && isOpen) toggle(); }; - const handleSubmit = async (event) => { - if (event) { - event.preventDefault(); - } - setSubmitting(true); - - if (edit && isEqual(formValues, initialFormValues)) { - toast.info(`Nothing is changed for this time entry`); - setSubmitting(false); - return; - } - - if (!edit && !formValues.isTangible) { - setTimelogConfirmationModalVisible(true); - setSubmitting(false); - return; - } - - await submitTimeEntry(); - }; - const submitTimeEntry = async () => { const { hours: formHours, minutes: formMinutes } = formValues; const timeEntry = { ...formValues }; const isTimeModified = edit && (initialHours !== formHours || initialMinutes !== formMinutes); - + if (!validateForm(isTimeModified)) { setSubmitting(false); return; } - - try { - if (edit) { - await props.editTimeEntry(data._id, timeEntry, initialDateOfWork); - } else { - await props.postTimeEntry(timeEntry); - } + const handleFormReset = () => { setFormValues(initialFormValues); + setReminder(initialReminder); + if (isOpen) toggle(); + setSubmitting(false); + }; - //Clear the form and clean up. + const handleError = error => { + toast.error(`An error occurred while attempting to submit your time entry. Error: ${error}`); + setSubmitting(false); + }; + + const handlePostSubmitActions = async () => { switch (from) { - case 'Timer': // log time entry from Timer + case 'Timer': sendStop(); clearForm(); break; - case 'TimeLog': // add intangible time entry + case 'TimeLog': { const date = moment(formValues.dateOfWork); const today = moment().tz('America/Los_Angeles'); const offset = today.week() - date.week(); - if (offset < 3) { - props.getTimeEntriesForWeek(timeEntryUserId, offset); - } else { - props.getTimeEntriesForWeek(timeEntryUserId, 3); - } + props.getTimeEntriesForWeek(timeEntryUserId, Math.min(offset, 3)); clearForm(); break; + } case 'WeeklyTab': await Promise.all([ props.getUserProfile(timeEntryUserId), @@ -363,26 +346,53 @@ const TimeEntryForm = props => { } if (from !== 'Timer' && !reminder.editLimitNotification) { - setReminder(reminder => ({ - ...reminder, - editLimitNotification: !reminder.editLimitNotification, + setReminder(r => ({ + ...r, + editLimitNotification: !r.editLimitNotification, })); } + }; - setReminder(initialReminder); - if (isOpen) toggle(); - setSubmitting(false); + try { + if (edit) { + await props.editTimeEntry(data._id, timeEntry, initialDateOfWork); + } else { + await props.postTimeEntry(timeEntry); + } + + await handlePostSubmitActions(); + handleFormReset(); } catch (error) { - toast.error(`An error occurred while attempting to submit your time entry. Error: ${error}`); + handleError(error); + } + }; + + const handleSubmit = async event => { + if (event) { + event.preventDefault(); + } + setSubmitting(true); + + if (edit && isEqual(formValues, initialFormValues)) { + toast.info(`Nothing is changed for this time entry`); setSubmitting(false); - }; + return; + } + + if (!edit && !formValues.isTangible) { + setTimelogConfirmationModalVisible(true); + setSubmitting(false); + return; + } + + await submitTimeEntry(); }; const handleTangibleTimelogConfirm = async () => { setTimelogConfirmationModalVisible(false); await submitTimeEntry(); }; - + const handleTangibleTimelogCancel = () => { setTimelogConfirmationModalVisible(false); }; @@ -399,80 +409,99 @@ const TimeEntryForm = props => { const buildOptions = () => { const projectsObject = {}; + + // Initialize default option const options = [ , ]; - timeEntryFormUserProjects.forEach(project => { - const { projectId } = project; - project.WBSObject = {}; - projectsObject[projectId] = project; - }); - timeEntryFormUserTasks.forEach(task => { - const { projectId, wbsId, _id: taskId, wbsName, projectName } = task; - if (!projectsObject[projectId]) { - projectsObject[projectId] = { - projectName, - WBSObject: { - [wbsId]: { - wbsName, - taskObject: { - [taskId]: task, + + // Build projectsObject with WBS and tasks + const buildProjectsObject = () => { + timeEntryFormUserProjects.forEach(project => { + const { projectId } = project; + project.WBSObject = {}; + projectsObject[projectId] = project; + }); + + timeEntryFormUserTasks.forEach(task => { + const { projectId, wbsId, _id: taskId, wbsName, projectName } = task; + if (!projectsObject[projectId]) { + projectsObject[projectId] = { + projectName, + WBSObject: { + [wbsId]: { + wbsName, + taskObject: { [taskId]: task }, }, }, - }, - }; - } else if (!projectsObject[projectId].WBSObject[wbsId]) { - projectsObject[projectId].WBSObject[wbsId] = { - wbsName, - taskObject: { - [taskId]: task, - }, - }; - } else { - projectsObject[projectId].WBSObject[wbsId].taskObject[taskId] = task; - } - }); - - for (const [projectId, project] of Object.entries(projectsObject)) { - const { projectName, WBSObject } = project; - options.push( - , - ); - for (const [wbsId, WBS] of Object.entries(WBSObject)) { - const { wbsName, taskObject } = WBS; - if (Object.keys(taskObject).length) { - options.push( - , - ); - for (const [taskId, task] of Object.entries(taskObject)) { - const { taskName } = task; + }; + } else if (!projectsObject[projectId].WBSObject[wbsId]) { + projectsObject[projectId].WBSObject[wbsId] = { + wbsName, + taskObject: { [taskId]: task }, + }; + } else { + projectsObject[projectId].WBSObject[wbsId].taskObject[taskId] = task; + } + }); + }; + + // Add options for tasks, WBS, and projects + const buildOptionsFromProjects = () => { + Object.entries(projectsObject).forEach(([projectId, project]) => { + const { projectName, WBSObject } = project; + + // Add project option + options.push( + , + ); + + Object.entries(WBSObject).forEach(([wbsId, WBS]) => { + const { wbsName, taskObject } = WBS; + + // Add WBS option if it has tasks + if (Object.keys(taskObject).length) { options.push( - , ); + + Object.entries(taskObject).forEach(([taskId, task]) => { + const { taskName } = task; + + // Add task option + options.push( + , + ); + }); } - } - } - } + }); + }); + }; + + // Build the projects object and options + buildProjectsObject(); + buildOptionsFromProjects(); + return options; }; /** * Rectify: This will run whenever TimeEntryForm is opened, since time entry data does not bound to store states (e.g., userProfile, userProjects, userTasks..) * */ - const loadAsyncData = async timeEntryUserId => { + const loadAsyncData = async tuid => { setIsAsyncDataLoaded(false); try { - const profileURL = ENDPOINTS.USER_PROFILE(timeEntryUserId); - const projectURL = ENDPOINTS.USER_PROJECTS(timeEntryUserId); - const taskURL = ENDPOINTS.TASKS_BY_USERID(timeEntryUserId); + const profileURL = ENDPOINTS.USER_PROFILE(tuid); + const projectURL = ENDPOINTS.USER_PROJECTS(tuid); + const taskURL = ENDPOINTS.TASKS_BY_USERID(tuid); const profilePromise = axios.get(profileURL); const projectPromise = axios.get(projectURL); @@ -488,12 +517,11 @@ const TimeEntryForm = props => { setTimeEntryFormUserTasks(userTasksRes.data); setIsAsyncDataLoaded(true); } catch (e) { - console.log(e); toast.error('An error occurred while loading the form data. Please try again later.'); } }; - /*---------------- useEffects -------------- */ + /* ---------------- useEffects -------------- */ useEffect(() => { if (isAsyncDataLoaded) { const options = buildOptions(); @@ -501,7 +529,7 @@ const TimeEntryForm = props => { } }, [isAsyncDataLoaded]); - //grab form data before editing + // grab form data before editing useEffect(() => { if (isOpen) { loadAsyncData(timeEntryUserId); @@ -533,7 +561,8 @@ const TimeEntryForm = props => { ) : ( Intangible )} - Time Entry{viewingUser.userId ? ` for ${viewingUser.firstName} ${viewingUser.lastName} ` : ' '} + Time Entry + {viewingUser.userId ? ` for ${viewingUser.firstName} ${viewingUser.lastName} ` : ' '} { style={darkMode ? boxStyleDark : boxStyle} disabled={submitting} > - {edit ? (submitting ? 'Saving...' : 'Save') : submitting ? 'Submitting...' : 'Submit'} + {(() => { + if (edit) { + return submitting ? 'Saving...' : 'Save'; + } + return submitting ? 'Submitting...' : 'Submit'; + })()} @@ -711,7 +745,7 @@ const TimeEntryForm = props => { cancelChange={cancelChange} darkMode={darkMode} /> - { /> ); -}; +} TimeEntryForm.propTypes = { edit: PropTypes.bool.isRequired, diff --git a/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx b/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx index 385a49e0c5..574d891f35 100644 --- a/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx +++ b/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx @@ -1,43 +1,62 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; -function TimeLogConfirmationModal({ isOpen, toggleModal, onConfirm, onReject, onIntangible, darkMode }) { - const [userChoice, setUserChoice] = useState(''); +function TimeLogConfirmationModal({ + isOpen, + toggleModal, + onConfirm, + onReject, + onIntangible, + darkMode, +}) { + const [, setUserChoice] = useState(''); - const handleUserAction = (choice) => { - setUserChoice(choice); - toggleModal(); + const handleUserAction = choice => { + setUserChoice(choice); + toggleModal(); - // Call corresponding action based on user's choice - if (choice === 'confirmed') { - onConfirm(); - } else if (choice === 'rejected') { - onReject(); - } else if (choice === 'intangible') { - onIntangible(); - } - }; + // Call corresponding action based on user's choice + if (choice === 'confirmed') { + onConfirm(); + } else if (choice === 'rejected') { + onReject(); + } else if (choice === 'intangible') { + onIntangible(); + } + }; - return ( - - HOLD ON TIGER! - - If you are logging Intangible Time that you want converted to Tangible, you must include the reason why you didn’t use the required timer. - - - - - - - - ); + return ( + + HOLD ON TIGER! + + If you are logging Intangible Time that you want converted to Tangible, you must include the + reason why you didn’t use the required timer. + + + + + + + + ); } export default TimeLogConfirmationModal; - diff --git a/src/components/Timelog/Timelog.jsx b/src/components/Timelog/Timelog.jsx index 3f4c421c88..08ee8cfa7f 100644 --- a/src/components/Timelog/Timelog.jsx +++ b/src/components/Timelog/Timelog.jsx @@ -1,4 +1,7 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +/* eslint-disable no-param-reassign */ +/* eslint-disable no-alert */ +/* eslint-disable no-console */ +import { useState, useEffect } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { Container, @@ -32,6 +35,17 @@ import ReactTooltip from 'react-tooltip'; import ActiveCell from 'components/UserManagement/ActiveCell'; import { ProfileNavDot } from 'components/UserManagement/ProfileNavDot'; import TeamMemberTasks from 'components/TeamMemberTasks'; +import { boxStyle, boxStyleDark } from 'styles'; +import { formatDate } from 'utils/formatDate'; +import EditableInfoModal from 'components/UserProfile/EditableModal/EditableInfoModal'; +import { cantUpdateDevAdminDetails } from 'utils/permissions'; +import axios from 'axios'; +import { + DEV_ADMIN_ACCOUNT_EMAIL_DEV_ENV_ONLY, + DEV_ADMIN_ACCOUNT_CUSTOM_WARNING_MESSAGE_DEV_ENV_ONLY, + PROTECTED_ACCOUNT_MODIFICATION_WARNING_MESSAGE, +} from 'utils/constants'; +import PropTypes from 'prop-types'; import { getTimeEntriesForWeek, getTimeEntriesForPeriod } from '../../actions/timeEntries'; import { getUserProfile, updateUserProfile, getUserTasks } from '../../actions/userProfile'; import { getUserProjects } from '../../actions/userProjects'; @@ -45,17 +59,6 @@ import WeeklySummary from '../WeeklySummary/WeeklySummary'; import LoadingSkeleton from '../common/SkeletonLoading'; import hasPermission from '../../utils/permissions'; import WeeklySummaries from './WeeklySummaries'; -import { boxStyle, boxStyleDark } from 'styles'; -import { formatDate } from 'utils/formatDate'; -import EditableInfoModal from 'components/UserProfile/EditableModal/EditableInfoModal'; -import { cantUpdateDevAdminDetails } from 'utils/permissions'; -import axios from 'axios'; -import { - DEV_ADMIN_ACCOUNT_EMAIL_DEV_ENV_ONLY, - DEV_ADMIN_ACCOUNT_CUSTOM_WARNING_MESSAGE_DEV_ENV_ONLY, - PROTECTED_ACCOUNT_MODIFICATION_WARNING_MESSAGE, -} from 'utils/constants'; -import PropTypes from 'prop-types'; import Badge from '../Badge'; import { ENDPOINTS } from '../../utils/URL'; @@ -79,7 +82,7 @@ const endOfWeek = offset => { .format('YYYY-MM-DD'); }; -const Timelog = props => { +function Timelog(props) { const darkMode = useSelector(state => state.theme.darkMode); const location = useLocation(); @@ -129,25 +132,14 @@ const Timelog = props => { const { userId: urlId } = useParams(); const [userprofileId, setUserProfileId] = useState(urlId || authUser.userid); - const doesUserHaveTaskWithWBS = userHaveTask => { - return userHaveTask.reduce((acc, item) => { - const hasIncompleteTask = item.resources.some( - val => - (viewingUser.userId === val.userID || val.userID === userprofileId) && - val.completedTask === false, - ); - if (hasIncompleteTask) acc.push(item); - return acc; - }, []); - }; - const checkSessionStorage = () => JSON.parse(sessionStorage.getItem('viewingUser')) ?? false; const [viewingUser, setViewingUser] = useState(checkSessionStorage()); const getUserId = () => { try { if (viewingUser) { return viewingUser.userId; - } else if (userId != null) { + } + if (userId != null) { return userId; } return authUser.userid; @@ -156,28 +148,53 @@ const Timelog = props => { } }; + const doesUserHaveTaskWithWBS = userHaveTask => { + return userHaveTask.reduce((acc, item) => { + const hasIncompleteTask = item.resources.some( + val => + (viewingUser.userId === val.userID || val.userID === userprofileId) && + val.completedTask === false, + ); + if (hasIncompleteTask) acc.push(item); + return acc; + }, []); + }; + const [displayUserId, setDisplayUserId] = useState(getUserId()); const isAuthUser = authUser.userid === displayUserId; const fullName = `${displayUserProfile.firstName} ${displayUserProfile.lastName}`; + const tabMapping = { + '#tasks': 0, + '#currentWeek': 1, + '#lastWeek': 2, + '#beforeLastWeek': 3, + '#dateRange': 4, + '#weeklySummaries': 5, + '#badgesearned': 6, + }; + const defaultTab = data => { const userHaveTask = doesUserHaveTaskWithWBS(data); - //change default to time log tab(1) in the following cases: - const role = authUser.role; + // change default to time log tab(1) in the following cases: + const { role } = authUser; let tab = 0; /* To set the Task tab as defatult this.userTask is being watched. Accounts with no tasks assigned to it return an empty array. Accounts assigned with tasks with no wbs return and empty array. Accounts assigned with tasks with wbs return an array with that wbs data. The problem: even after unassigning tasks the array keeps the wbs data. - That breaks this feature. Necessary to check if this array should keep data or be reset when unassinging tasks.*/ - - //if user role is volunteer or core team and they don't have tasks assigned, then default tab is timelog. - role === 'Volunteer' && userHaveTask.length > 0 - ? (tab = 0) - : role === 'Volunteer' && userHaveTask.length === 0 - ? (tab = 1) - : null; + That breaks this feature. Necessary to check if this array should keep data or be reset when unassinging tasks. */ + + // if user role is volunteer or core team and they don't have tasks assigned, then default tab is timelog. + if (role === 'Volunteer' && userHaveTask.length > 0) { + tab = 0; + } else if (role === 'Volunteer' && userHaveTask.length === 0) { + tab = 1; + } else { + tab = null; + } + // Sets active tab to "Current Week Timelog" when the Progress bar in Leaderboard is clicked if (!props.isDashboard) { tab = 1; @@ -192,42 +209,11 @@ const Timelog = props => { return tab; }; - const tabMapping = { - '#tasks': 0, - '#currentWeek': 1, - '#lastWeek': 2, - '#beforeLastWeek': 3, - '#dateRange': 4, - '#weeklySummaries': 5, - '#badgesearned': 6, - }; - - useEffect(() => { - const tab = tabMapping[location.hash]; - if (tab !== undefined) { - changeTab(tab); - } - }, [location.hash]); // This effect will run whenever the hash changes - - /*---------------- methods -------------- */ - const updateTimeEntryItems = () => { - const allTimeEntryItems = generateAllTimeEntryItems(); - setCurrentWeekEntries(allTimeEntryItems[0]); - setLastWeekEntries(allTimeEntryItems[1]); - setBeforeLastEntries(allTimeEntryItems[2]); - setPeriodEntries(allTimeEntryItems[3]); - }; - - const generateAllTimeEntryItems = () => { - const currentWeekEntries = generateTimeEntries(timeEntries.weeks[0], 0); - const lastWeekEntries = generateTimeEntries(timeEntries.weeks[1], 1); - const beforeLastEntries = generateTimeEntries(timeEntries.weeks[2], 2); - const periodEntries = generateTimeEntries(timeEntries.period, 3); - return [currentWeekEntries, lastWeekEntries, beforeLastEntries, periodEntries]; - }; + /* ---------------- methods -------------- */ const generateTimeEntries = (data, tab) => { if (!timeLogState.projectsSelected.includes('all')) { + // eslint-disable-next-line no-param-reassign data = data.filter( entry => timeLogState.projectsSelected.includes(entry.projectId) || @@ -255,22 +241,38 @@ const Timelog = props => { )); }; - const loadAsyncData = async userId => { - //load the timelog data + const generateAllTimeEntryItems = () => { + const currentWeekEntry = generateTimeEntries(timeEntries.weeks[0], 0); + const lastWeekEntry = generateTimeEntries(timeEntries.weeks[1], 1); + const beforeLastEntry = generateTimeEntries(timeEntries.weeks[2], 2); + const periodEntry = generateTimeEntries(timeEntries.period, 3); + return [currentWeekEntry, lastWeekEntry, beforeLastEntry, periodEntry]; + }; + + const updateTimeEntryItems = () => { + const allTimeEntryItems = generateAllTimeEntryItems(); + setCurrentWeekEntries(allTimeEntryItems[0]); + setLastWeekEntries(allTimeEntryItems[1]); + setBeforeLastEntries(allTimeEntryItems[2]); + setPeriodEntries(allTimeEntryItems[3]); + }; + + const loadAsyncData = async uid => { + // load the timelog data setTimeLogState({ ...timeLogState, isTimeEntriesLoading: true }); try { await Promise.all([ - props.getUserProfile(userId), - props.getTimeEntriesForWeek(userId, 0), - props.getTimeEntriesForWeek(userId, 1), - props.getTimeEntriesForWeek(userId, 2), - props.getTimeEntriesForPeriod(userId, timeLogState.fromDate, timeLogState.toDate), + props.getUserProfile(uid), + props.getTimeEntriesForWeek(uid, 0), + props.getTimeEntriesForWeek(uid, 1), + props.getTimeEntriesForWeek(uid, 2), + props.getTimeEntriesForPeriod(uid, timeLogState.fromDate, timeLogState.toDate), props.getAllRoles(), - props.getUserProjects(userId), - props.getUserTasks(userId), + props.getUserProjects(uid), + props.getUserTasks(uid), ]); - const url = ENDPOINTS.TASKS_BY_USERID(userId); + const url = ENDPOINTS.TASKS_BY_USERID(uid); const res = await axios.get(url); const data = res.data.length > 0 ? res.data : []; @@ -294,8 +296,8 @@ const Timelog = props => { setTimeLogState({ ...timeLogState, timeEntryFormModal: !timeLogState.timeEntryFormModal }); }; - const showSummary = isAuthUser => { - if (isAuthUser) { + const showSummary = isAuth => { + if (isAuth) { setTimeLogState({ ...timeLogState, summary: true }); setTimeout(() => { const elem = document.getElementById('weeklySum'); @@ -323,7 +325,7 @@ const Timelog = props => { setTimeLogState({ ...timeLogState, infoModal: !timeLogState.infoModal, - information: str.split('\n').map((item, i) =>

{item}

), + information: str.split('\n').map(item =>

{item}

), }); }; @@ -343,12 +345,19 @@ const Timelog = props => { }); }; + useEffect(() => { + const tab = tabMapping[location.hash]; + if (tab !== undefined) { + changeTab(tab); + } + }, [location.hash]); // This effect will run whenever the hash changes + const handleInputChange = e => { setTimeLogState({ ...timeLogState, [e.target.name]: e.target.value }); }; const handleSearch = e => { - //check if the toDate is before the fromDate + // check if the toDate is before the fromDate if (moment(timeLogState.fromDate).isAfter(moment(timeLogState.toDate))) { alert('Invalid Date Range: the From Date must be before the To Date'); } else { @@ -369,32 +378,32 @@ const Timelog = props => { timeLogState.activeTab === 5 || timeLogState.activeTab === 6 ) { - return <>; - } else if (timeLogState.activeTab === 4) { + return null; + } + if (timeLogState.activeTab === 4) { return (

Viewing time Entries from {formatDate(timeLogState.fromDate)} to{' '} {formatDate(timeLogState.toDate)}

); - } else { - return ( -

- Viewing time Entries from {formatDate(startOfWeek(timeLogState.activeTab - 1))} to{' '} - {formatDate(endOfWeek(timeLogState.activeTab - 1))} -

- ); } + return ( +

+ Viewing time Entries from {formatDate(startOfWeek(timeLogState.activeTab - 1))} to{' '} + {formatDate(endOfWeek(timeLogState.activeTab - 1))} +

+ ); }; - const makeBarData = userId => { - //pass the data to summary bar + const makeBarData = uid => { + // pass the data to summary bar const weekEffort = calculateTotalTime(timeEntries.weeks[0], true); setTimeLogState({ ...timeLogState, currentWeekEffort: weekEffort }); if (props.isDashboard) { - props.passSummaryBarData({ personId: userId, tangibletime: weekEffort }); + props.passSummaryBarData({ personId: uid, tangibletime: weekEffort }); } else { - setSummaryBarData({ personId: userId, tangibletime: weekEffort }); + setSummaryBarData({ personId: uid, tangibletime: weekEffort }); } }; @@ -405,11 +414,16 @@ const Timelog = props => { Select Project/Task (all) , ]; + + // Build the projectsObject structure displayUserProjects.forEach(project => { const { projectId } = project; - project.WBSObject = {}; - projectsObject[projectId] = project; + projectsObject[projectId] = { + ...project, + WBSObject: {}, + }; }); + disPlayUserTasks.forEach(task => { const { projectId, wbsId, _id: taskId, wbsName, projectName } = task; if (!projectsObject[projectId]) { @@ -418,33 +432,35 @@ const Timelog = props => { WBSObject: { [wbsId]: { wbsName, - taskObject: { - [taskId]: task, - }, + taskObject: { [taskId]: task }, }, }, }; } else if (!projectsObject[projectId].WBSObject[wbsId]) { projectsObject[projectId].WBSObject[wbsId] = { wbsName, - taskObject: { - [taskId]: task, - }, + taskObject: { [taskId]: task }, }; } else { projectsObject[projectId].WBSObject[wbsId].taskObject[taskId] = task; } }); - for (const [projectId, project] of Object.entries(projectsObject)) { + // Convert projectsObject to options + Object.entries(projectsObject).forEach(([projectId, project]) => { const { projectName, WBSObject } = project; + + // Add project option options.push( , ); - for (const [wbsId, WBS] of Object.entries(WBSObject)) { + + Object.entries(WBSObject).forEach(([wbsId, WBS]) => { const { wbsName, taskObject } = WBS; + + // Add WBS option options.push( , ); - for (const [taskId, task] of Object.entries(taskObject)) { + + Object.entries(taskObject).forEach(([taskId, task]) => { const { taskName } = task; + + // Add task option options.push( , ); - } - } - } + }); + }); + }); + return options; }; - const generateTimeLogItems = userId => { - //build the time log component + const generateTimeLogItems = uid => { + // build the time log component const options = buildOptions(); setProjectOrTaskOptions(options); updateTimeEntryItems(); - makeBarData(userId); + makeBarData(uid); }; - const handleUpdateTask = useCallback(() => { - setShouldFetchData(true); - }, []); - const handleStorageEvent = () => { const sessionStorageData = checkSessionStorage(); setViewingUser(sessionStorageData || false); - if (sessionStorageData && sessionStorageData.userId != authUser.userId) { + if (sessionStorageData && sessionStorageData.userId !== authUser.userId) { setDisplayUserId(sessionStorageData.userId); } }; - /*---------------- useEffects -------------- */ + /* ---------------- useEffects -------------- */ // Update user ID if it changes in the URL useEffect(() => { @@ -536,10 +552,17 @@ const Timelog = props => { }; }, []); + const containerStyle = () => { + if (darkMode) { + return props.isDashboard ? {} : { padding: '0 15px 300px 15px' }; + } + return {}; + }; + return (
{!props.isDashboard ? ( @@ -553,14 +576,12 @@ const Timelog = props => { ) : ( - {props.isDashboard ? ( - <> - ) : ( + {props.isDashboard ? null : ( @@ -604,7 +625,7 @@ const Timelog = props => { areaName="TasksAndTimelogInfoPoint" areaTitle="Tasks and Timelogs" fontSize={24} - isPermissionPage={true} + isPermissionPage role={authUser.role} // Pass the 'role' prop to EditableInfoModal darkMode={darkMode} /> @@ -906,14 +927,12 @@ const Timelog = props => { )} {timeLogState.activeTab === 0 || timeLogState.activeTab === 5 || - timeLogState.activeTab === 6 ? ( - <> - ) : ( + timeLogState.activeTab === 6 ? null : (
@@ -943,9 +962,7 @@ const Timelog = props => { {timeLogState.activeTab === 0 || timeLogState.activeTab === 5 || - timeLogState.activeTab === 6 ? ( - <> - ) : ( + timeLogState.activeTab === 6 ? null : ( { )}
); -}; +} Timelog.prototype = { userId: PropTypes.string, diff --git a/src/components/Timelog/TimelogNavbar.jsx b/src/components/Timelog/TimelogNavbar.jsx index 3535fcc3ab..e9f6b5c0b4 100644 --- a/src/components/Timelog/TimelogNavbar.jsx +++ b/src/components/Timelog/TimelogNavbar.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Link } from 'react-router-dom'; import { Progress, @@ -13,14 +13,15 @@ import { import { useSelector } from 'react-redux'; import { getProgressColor, getProgressValue } from '../../utils/effortColors'; -const TimelogNavbar = ({ userId }) => { +function TimelogNavbar({ userId }) { const { firstName, lastName } = useSelector(state => state.userProfile); const [collapsed, setCollapsed] = useState(true); const toggleNavbar = () => setCollapsed(!collapsed); const timeEntries = useSelector(state => state.timeEntries.weeks[0]); - const reducer = (total, entry) => total + parseInt(entry.hours) + parseInt(entry.minutes) / 60; + const reducer = (total, entry) => + total + parseInt(entry.hours, 10) + parseInt(entry.minutes, 10) / 60; const totalEffort = timeEntries.reduce(reducer, 0); const weeklycommittedHours = useSelector(state => state.userProfile.weeklycommittedHours); @@ -79,6 +80,6 @@ const TimelogNavbar = ({ userId }) => {
); -}; +} export default TimelogNavbar; diff --git a/src/components/Timelog/WeeklySummaries.jsx b/src/components/Timelog/WeeklySummaries.jsx index 982fadd1b3..b002cbe552 100644 --- a/src/components/Timelog/WeeklySummaries.jsx +++ b/src/components/Timelog/WeeklySummaries.jsx @@ -1,29 +1,18 @@ -import React, { useState, useEffect} from 'react'; +import { useState, useEffect } from 'react'; import parse from 'html-react-parser'; -import './Timelog.css' -import {updateWeeklySummaries} from '../../actions/weeklySummaries'; +import './Timelog.css'; import { getUserProfile, updateUserProfile } from 'actions/userProfile'; import hasPermission from 'utils/permissions'; -import { connect, useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Editor } from '@tinymce/tinymce-react'; -import { userProfileByIdReducer } from 'reducers/userProfileByIdReducer'; import Spinner from 'react-bootstrap/Spinner'; +import { updateWeeklySummaries } from '../../actions/weeklySummaries'; -const WeeklySummaries = ({ userProfile }) => { - - useEffect(() => { - setEditedSummaries([ - userProfile.weeklySummaries[0]?.summary || '', - userProfile.weeklySummaries[1]?.summary || '', - userProfile.weeklySummaries[2]?.summary || '', - ]); - - }, [userProfile]); - - const darkMode = useSelector(state => state.theme.darkMode) +function WeeklySummaries({ userProfile }) { + const darkMode = useSelector(state => state.theme.darkMode); // Initialize state variables for editing and original summaries - + const [editing, setEditing] = useState([false, false, false]); const [editedSummaries, setEditedSummaries] = useState([ @@ -31,7 +20,6 @@ const WeeklySummaries = ({ userProfile }) => { userProfile.weeklySummaries[1]?.summary || '', userProfile.weeklySummaries[2]?.summary || '', ]); - const [originalSummaries, setOriginalSummaries] = useState([...editedSummaries]); const [LoadingHandleSave, setLoadingHandleSave] = useState(null); @@ -40,6 +28,14 @@ const WeeklySummaries = ({ userProfile }) => { const dispatch = useDispatch(); const canEdit = dispatch(hasPermission('putUserProfile')); + useEffect(() => { + setEditedSummaries([ + userProfile.weeklySummaries[0]?.summary || '', + userProfile.weeklySummaries[1]?.summary || '', + userProfile.weeklySummaries[2]?.summary || '', + ]); + }, [userProfile]); + const currentUserID = userProfile._id; const { user } = useSelector(state => state.auth); const loggedInUserId = user.userid; @@ -48,58 +44,57 @@ const WeeklySummaries = ({ userProfile }) => { return
No weekly summaries available
; } - const toggleEdit = (index) => { - const newEditing = editing.map((value, i) => (i === index ? !value : false)); - setEditing(newEditing); + const toggleEdit = index => { + const newEditing = editing.map((value, i) => (i === index ? !value : false)); + setEditing(newEditing); }; const handleSummaryChange = (event, index, editor) => { const wordCounter = editor.plugins.wordcount.getCount(); - setWordCount(wordCounter) + setWordCount(wordCounter); const newEditedSummaries = [...editedSummaries]; newEditedSummaries[index] = event.target.value; setEditedSummaries(newEditedSummaries); }; - const handleCancel = (index) => { + const handleCancel = index => { // Revert to the original summary content and toggle off editing mode const newEditedSummaries = [...editedSummaries]; newEditedSummaries[index] = userProfile.weeklySummaries[index]?.summary || ''; setEditedSummaries(newEditedSummaries); - + // Toggle off editing mode toggleEdit(index); - }; - - const handleSave = async (index) => { + + const handleSave = async index => { // Save the edited summary content and toggle off editing mode const editedSummary = editedSummaries[index]; - + if (editedSummary.trim() !== '' && wordCount >= 50) { setLoadingHandleSave(index); const updatedUserProfile = { ...userProfile, weeklySummaries: userProfile.weeklySummaries.map((item, i) => - i === index ? { ...item, summary: editedSummary } : item - ) + i === index ? { ...item, summary: editedSummary } : item, + ), }; - - // This code updates the summary. - await dispatch(updateUserProfile(userProfile)); - - // This code saves edited weekly summaries in MongoDB. - await dispatch(updateWeeklySummaries(userProfile._id, updatedUserProfile)); - await dispatch(getUserProfile(userProfile._id)); - await setLoadingHandleSave(null); - setLoadingHandleSave(null); - // Toggle off editing mode - toggleEdit(index); - } else { - // Invalid summary, show an error message or handle it as needed - alert('Please enter a valid summary with at least 50 words.'); - } + // This code updates the summary. + await dispatch(updateUserProfile(userProfile)); + + // This code saves edited weekly summaries in MongoDB. + await dispatch(updateWeeklySummaries(userProfile._id, updatedUserProfile)); + await dispatch(getUserProfile(userProfile._id)); + await setLoadingHandleSave(null); + setLoadingHandleSave(null); + // Toggle off editing mode + toggleEdit(index); + } else { + // Invalid summary, show an error message or handle it as needed + // eslint-disable-next-line no-alert + alert('Please enter a valid summary with at least 50 words.'); + } }; // Images are not allowed while editing weekly summaries @@ -120,7 +115,7 @@ const WeeklySummaries = ({ userProfile }) => { max_height: 500, autoresize_bottom_margin: 1, images_upload_handler: customImageUploadHandler, - }; + }; const renderSummary = (title, summary, index) => { if (editing[index]) { @@ -131,32 +126,46 @@ const WeeklySummaries = ({ userProfile }) => { tinymceScriptSrc="/tinymce/tinymce.min.js" init={TINY_MCE_INIT_OPTIONS} value={editedSummaries[index]} - onEditorChange={(content, editor) => handleSummaryChange({ target: { value: content } }, index, editor)} - onGetContent={(content, editor) => setWordCount(editor.plugins.wordcount.getCount())} + onEditorChange={(content, editor) => + handleSummaryChange({ target: { value: content } }, index, editor) + } + onGetContent={(content, editor) => setWordCount(editor.plugins.wordcount.getCount())} /> -
- - - - +
+ + +
-
); - } else if (summary && (canEdit || currentUserID == loggedInUserId)) { + } + if (summary && (canEdit || currentUserID === loggedInUserId)) { // Display the summary with an "Edit" button return (

{title}

{parse(editedSummaries[index])} - +
); - } else if (summary){ + } + if (summary) { // Display the summary with an "Edit" button return (
@@ -164,28 +173,27 @@ const WeeklySummaries = ({ userProfile }) => { {parse(editedSummaries[index])}
); - } else { - // Display a message when there's no summary - return ( -
-

{title}

-

- {userProfile.firstName} {userProfile.lastName} did not submit a summary. -

-
- ); } + // Display a message when there's no summary + return ( +
+

{title}

+

+ {userProfile.firstName} {userProfile.lastName} did not submit a summary. +

+
+ ); }; return (
{renderSummary("This week's summary", userProfile.weeklySummaries[0]?.summary, 0)} {renderSummary("Last week's summary", userProfile.weeklySummaries[1]?.summary, 1)} - {renderSummary("The week before last's summary",userProfile.weeklySummaries[2]?.summary,2)} + {renderSummary("The week before last's summary", userProfile.weeklySummaries[2]?.summary, 2)}
); -}; +} // const mapStateToProps = state => state; // export default connect(mapStateToProps, { hasPermission })(WeeklySummaries); -export default WeeklySummaries; \ No newline at end of file +export default WeeklySummaries; diff --git a/src/components/Timelog/index.js b/src/components/Timelog/index.js index ea234d0999..8fcf6cdc69 100644 --- a/src/components/Timelog/index.js +++ b/src/components/Timelog/index.js @@ -1 +1,3 @@ -export { default } from './Timelog'; +import Timelog from './Timelog'; + +export default Timelog; diff --git a/src/components/Timer/__test__/Countdown.test.js b/src/components/Timer/__test__/Countdown.test.js new file mode 100644 index 0000000000..202a04546c --- /dev/null +++ b/src/components/Timer/__test__/Countdown.test.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Countdown from '../Countdown'; + +describe('Countdown Component', () => { + const defaultProps = { + message: { + started: false, + goal: 3600000, // 1 hour in milliseconds + initialGoal: 3600000, + }, + timerRange: { + MAX_HOURS: 24, + MIN_MINS: 1, + }, + running: false, + wsMessageHandler: { + sendPause: jest.fn(), + sendSetGoal: jest.fn(), + }, + remaining: 1800000, // 30 minutes in milliseconds + setConfirmationResetModal: jest.fn(), + checkBtnAvail: jest.fn(() => true), + handleStartButton: jest.fn(), + handleAddButton: jest.fn(), + handleSubtractButton: jest.fn(), + handleStopButton: jest.fn(), + toggleTimer: jest.fn(), + }; + + it('renders the countdown component with the correct initial values', () => { + render(); + expect(screen.getByText('Goal: 01:00:00')).toBeInTheDocument(); + expect(screen.getByText('Elapsed: 00:30:00')).toBeInTheDocument(); + expect(screen.getByText('Time Remaining')).toBeInTheDocument(); + expect(screen.getByText('00')).toBeInTheDocument(); + expect(screen.getByText('30')).toBeInTheDocument(); + }); + + it('calls toggleTimer when the close button is clicked', () => { + render(); + const closeButton = screen.getByTitle('close timer dropdown'); + fireEvent.click(closeButton); + expect(defaultProps.toggleTimer).toHaveBeenCalled(); + }); + + it('displays correct remaining time based on props', () => { + const { rerender } = render(); + expect(screen.getByText('00')).toBeInTheDocument(); // Hours + expect(screen.getByText('30')).toBeInTheDocument(); // Minutes + expect(screen.getByText('00')).toBeInTheDocument(); // Seconds + + rerender( + , + ); + expect(screen.getByText('01')).toBeInTheDocument(); // Updated hours + expect(screen.getByText('30')).toBeInTheDocument(); // Updated minutes + }); + + it('calls handleStartButton when the start button is clicked', () => { + render(); + const startButton = screen.getByLabelText('Start timer'); + fireEvent.click(startButton); + expect(defaultProps.handleStartButton).toHaveBeenCalled(); + }); + + it('calls handleStopButton when the stop button is clicked', () => { + const props = { + ...defaultProps, + message: { ...defaultProps.message, started: true }, + running: true, + }; + render(); + const stopButton = screen.getByLabelText('Stop timer and log timer'); + fireEvent.click(stopButton); + expect(defaultProps.handleStopButton).toHaveBeenCalled(); + }); + + it('calls wsMessageHandler.sendSetGoal when a new goal is validated', () => { + const { rerender } = render(); + const editButton = screen.getByTitle('edit initial goal'); + fireEvent.click(editButton); + + const hourInput = screen.getByDisplayValue('1'); + fireEvent.change(hourInput, { target: { value: '2' } }); + + const saveButton = screen.getByTitle('save initial goal'); + fireEvent.click(saveButton); + + expect(defaultProps.wsMessageHandler.sendSetGoal).toHaveBeenCalledWith(7200000); // 2 hours in milliseconds + }); +}); \ No newline at end of file diff --git a/src/components/UserManagement/UserManagement.jsx b/src/components/UserManagement/UserManagement.jsx index 0122a157d7..d6412ad067 100644 --- a/src/components/UserManagement/UserManagement.jsx +++ b/src/components/UserManagement/UserManagement.jsx @@ -55,6 +55,7 @@ class UserManagement extends React.PureComponent { firstNameSearchText: '', lastNameSearchText: '', roleSearchText: '', + titleSearchText: '', weeklyHrsSearchText: '', emailSearchText: '', wildCardSearchText: '', @@ -116,6 +117,7 @@ class UserManagement extends React.PureComponent { const searchStateChanged = (prevState.firstNameSearchText !== this.state.firstNameSearchText) || (prevState.lastNameSearchText !== this.state.lastNameSearchText) || (prevState.roleSearchText !== this.state.roleSearchText) || + prevState.titleSearchText !== this.state.titleSearchText || (prevState.weeklyHrsSearchText !== this.state.weeklyHrsSearchText) || (prevState.emailSearchText !== this.state.emailSearchText); @@ -247,6 +249,7 @@ class UserManagement extends React.PureComponent { onResetClick={that.onResetClick} authEmail={this.props.state.userProfile.email} user={user} + jobTitle={this.props.state.userProfile.jobTitle} role={this.props.state.auth.user.role} roles={rolesPermissions} timeOffRequests={timeOffRequests[user._id] || []} @@ -315,6 +318,7 @@ class UserManagement extends React.PureComponent { return ( nameMatches && user.role.toLowerCase().includes(this.state.roleSearchText.toLowerCase()) && + user.jobTitle.toLowerCase().includes(this.state.titleSearchText.toLowerCase()) && user.email.toLowerCase().includes(this.state.emailSearchText.toLowerCase()) && (this.state.weeklyHrsSearchText === '' || user.weeklycommittedHours === Number(this.state.weeklyHrsSearchText)) && @@ -578,6 +582,16 @@ class UserManagement extends React.PureComponent { }); }; + /** + * Call back for search filter - Job Title + */ + onTitleSearch = searchText => { + this.setState({ + titleSearchText: searchText.trim(), + selectedPage: 1, + }); + }; + /** * Call back for search filter - email */ @@ -774,6 +788,7 @@ class UserManagement extends React.PureComponent { onFirstNameSearch={this.onFirstNameSearch} onLastNameSearch={this.onLastNameSearch} onRoleSearch={this.onRoleSearch} + onTitleSearch={this.onTitleSearch} onEmailSearch={this.onEmailSearch} onWeeklyHrsSearch={this.onWeeklyHrsSearch} roles={roles} diff --git a/src/components/UserManagement/UserTableData.jsx b/src/components/UserManagement/UserTableData.jsx index 5776e1de87..eeadb59791 100644 --- a/src/components/UserManagement/UserTableData.jsx +++ b/src/components/UserManagement/UserTableData.jsx @@ -33,6 +33,7 @@ const UserTableData = React.memo(props => { lastName: props.user.lastName, id: props.user._id, role: props.user.role, + jobTitle: props.user.jobTitle, email: props.user.email, weeklycommittedHours: props.user.weeklycommittedHours, startDate: formatDate(props.user.startDate), @@ -77,6 +78,7 @@ const UserTableData = React.memo(props => { lastName: props.user.lastName, id: props.user._id, role: props.user.role, + jobTitle:props.user.jobTitle, email: props.user.email, weeklycommittedHours: props.user.weeklycommittedHours, startDate: formatDateYYYYMMDD(props.user.startDate), @@ -179,7 +181,7 @@ const UserTableData = React.memo(props => { /> )} - + {editUser?.role && roles !== undefined ? ( formData.role ) : ( @@ -202,6 +204,33 @@ const UserTableData = React.memo(props => { )} + + + {editUser?.jobTitle ? ( +
+ {formData.jobTitle} + { + navigator.clipboard.writeText(formData.jobTitle); + toast.success('Title Copied!'); + }} + /> +
+ ) : ( + { + updateFormData({ ...formData, jobTitle: e.target.value }); + addUserInformation('jobTitle', e.target.value, props.user._id); + }} + /> + )} + + {editUser?.email ? (
diff --git a/src/components/UserManagement/UserTableHeader.jsx b/src/components/UserManagement/UserTableHeader.jsx index 3a60cdfe3b..fcb7693544 100644 --- a/src/components/UserManagement/UserTableHeader.jsx +++ b/src/components/UserManagement/UserTableHeader.jsx @@ -13,6 +13,7 @@ import { FIRST_NAME, LAST_NAME, ROLE, + TITLE, EMAIL, WKLY_COMMITTED_HRS, PAUSE, @@ -143,6 +144,32 @@ const UserTableHeader = React.memo( })()}
+ +
+ {TITLE} + {(() => { + if (authRole === 'Owner') { + if (editFlag.jobTitle === 1) { + return ( + enableEdit({ ...editFlag, jobTitle: 0 })} + /> + ); + } + return ( + disableEdit({ ...editFlag, jobTitle: 1 })} + /> + ); + } + return <> ; + })()} +
+
{EMAIL} diff --git a/src/components/UserManagement/UserTableSearchHeader.jsx b/src/components/UserManagement/UserTableSearchHeader.jsx index 8d6679c535..5c77e54993 100644 --- a/src/components/UserManagement/UserTableSearchHeader.jsx +++ b/src/components/UserManagement/UserTableSearchHeader.jsx @@ -21,6 +21,10 @@ const UserTableSearchHeader = React.memo(props => { props.onRoleSearch(text); }; + const onTitleSearch = text => { + props.onTitleSearch(text); + }; + const onEmailSearch = text => { props.onEmailSearch(text); }; @@ -51,6 +55,14 @@ const UserTableSearchHeader = React.memo(props => { + + + { let onFirstNameSearch; let onLastNameSearch; let onRoleSearch; + let onTitleSearch; let onEmailSearch; let onWeeklyHrsSearch; beforeEach(() => { onFirstNameSearch = jest.fn(); onLastNameSearch = jest.fn(); onRoleSearch = jest.fn(); + onTitleSearch = jest.fn(); onEmailSearch = jest.fn(); onWeeklyHrsSearch = jest.fn(); render( @@ -22,6 +24,7 @@ describe('user table search header row', () => { onFirstNameSearch={onFirstNameSearch} onLastNameSearch={onLastNameSearch} onRoleSearch={onRoleSearch} + onTitleSearch={onTitleSearch} onEmailSearch={onEmailSearch} onWeeklyHrsSearch={onWeeklyHrsSearch} roles={['1', '2', '3', '4', 'Volunteer', 'Owner', 'Manager']} @@ -35,7 +38,7 @@ describe('user table search header row', () => { expect(screen.getByRole('row')).toBeInTheDocument(); }); it('should render 4 text field', () => { - expect(screen.getAllByRole('textbox')).toHaveLength(4); + expect(screen.getAllByRole('textbox')).toHaveLength(5); }); it('should render one dropdown box', () => { expect(screen.getByRole('combobox')).toBeInTheDocument(); @@ -70,22 +73,26 @@ describe('user table search header row', () => { expect(onRoleSearch).toHaveBeenCalled(); expect(onRoleSearch).toHaveBeenCalledWith('Manager'); }); - it('should fire Email search once the user type something in the email search box', async () => { + it('should fire Title search once the user type something in the title search box', async () => { await userEvent.type(screen.getAllByRole('textbox')[2], 'test', { allAtOnce: false }); + expect(onTitleSearch).toHaveBeenCalledTimes(4); + }); + it('should fire Email search once the user type something in the email search box', async () => { + await userEvent.type(screen.getAllByRole('textbox')[3], 'test', { allAtOnce: false }); expect(onEmailSearch).toHaveBeenCalledTimes(4); }); it('should fire Email search once the user type something in the email search box', async () => { - await userEvent.type(screen.getAllByRole('textbox')[2], 'Jhon.wick@email.com', { + await userEvent.type(screen.getAllByRole('textbox')[3], 'Jhon.wick@email.com', { allAtOnce: true, }); expect(onEmailSearch).toHaveBeenCalled(); }); it('should fire onWeeklyHrsSearch once the user type something in the weeklycommitted hrs search box', async () => { - await userEvent.type(screen.getAllByRole('textbox')[3], 'test', { allAtOnce: false }); + await userEvent.type(screen.getAllByRole('textbox')[4], 'test', { allAtOnce: false }); expect(onWeeklyHrsSearch).toHaveBeenCalledTimes(4); }); it('should fire onWeeklyHrsSearch once the user type something in the weeklycommitted hrs search box', async () => { - await userEvent.type(screen.getAllByRole('textbox')[3], '15', { allAtOnce: true }); + await userEvent.type(screen.getAllByRole('textbox')[4], '15', { allAtOnce: true }); expect(onWeeklyHrsSearch).toHaveBeenCalled(); }); }); diff --git a/src/components/UserManagement/usermanagement.css b/src/components/UserManagement/usermanagement.css index b2acccfe17..c15348f1ec 100644 --- a/src/components/UserManagement/usermanagement.css +++ b/src/components/UserManagement/usermanagement.css @@ -89,6 +89,16 @@ thead { } +#usermanagement_role, +#user_role, +td#usermanagement_role { + width: 100px; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .copy_icon { cursor: pointer; position: absolute; diff --git a/src/components/UserProfile/Badges.jsx b/src/components/UserProfile/Badges.jsx index a8381b981c..2cce7ca814 100644 --- a/src/components/UserProfile/Badges.jsx +++ b/src/components/UserProfile/Badges.jsx @@ -63,29 +63,37 @@ export const Badges = (props) => { useEffect(() => { try { if (props.userProfile.badgeCollection && props.userProfile.badgeCollection.length) { - const sortBadges = [...props.userProfile.badgeCollection].sort((a, b) => { - if (a?.badge?.ranking === 0) return 1; - if (b?.badge?.ranking === 0) return -1; - if (a?.badge?.ranking > b?.badge?.ranking) return 1; - if (a?.badge?.ranking < b?.badge?.ranking) return -1; - if (a?.badge?.badgeName > b?.badge?.badgeName) return 1; - if (a?.badge?.badgeName < b?.badge?.badgeName) return -1; - return 0; - }); + const sortBadges = [...props.userProfile.badgeCollection] + .filter(badge => badge && badge.badge) // Filter out any null or undefined badges + .sort((a, b) => { + const rankingA = a.badge?.ranking ?? Infinity; + const rankingB = b.badge?.ranking ?? Infinity; + const nameA = a.badge?.badgeName ?? ''; + const nameB = b.badge?.badgeName ?? ''; + + if (rankingA === 0) return 1; + if (rankingB === 0) return -1; + if (rankingA > rankingB) return 1; + if (rankingA < rankingB) return -1; + return nameA.localeCompare(nameB); + }); setSortedBadges(sortBadges); + } else { + setSortedBadges([]); } } catch (error) { - console.log(error); + console.error("Error sorting badges:", error); + setSortedBadges([]); } - }, [props.userProfile.badgeCollection]); // Determines what congratulatory text should displayed. const badgesEarned = props.userProfile.badgeCollection.reduce((acc, badge) => { - if (badge?.badge?.badgeName === 'Personal Max' || badge?.badge?.type === 'Personal Max') { + if (!badge || !badge.badge) return acc; + if (badge.badge.badgeName === 'Personal Max' || badge.badge.type === 'Personal Max') { return acc + 1; } - return acc + Math.round(Number(badge.count)); + return acc + (Math.round(Number(badge.count)) || 0); }, 0); const subject = props.isUserSelf ? 'You have' : 'This person has'; @@ -225,7 +233,7 @@ export const Badges = (props) => { {props.userProfile.badgeCollection && props.userProfile.badgeCollection.length>0 ? ( sortedBadges && - sortedBadges.map(value => value &&( + sortedBadges.map(value => value && value.badge &&( {' '} diff --git a/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx b/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx index 5d2322a836..c84a525432 100644 --- a/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx +++ b/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx @@ -2,22 +2,31 @@ import React, { useEffect, useState } from 'react'; import BadgeImage from '../BadgeImage'; const FeaturedBadges = props => { - let [filteredBadges, setFilteredBadges] = useState([]); + const [filteredBadges, setFilteredBadges] = useState([]); + const filterBadges = allBadges => { - let filteredList = allBadges || []; - - filteredList = filteredList.sort((a, b) => { - if (a.featured > b.featured) return -1; - if (a.featured < b.featured) return 1; - if (a.badge.ranking > b.badge.ranking) return 1; - if (a.badge.ranking < b.badge.ranking) return -1; - if (a.badge.badgeName > b.badge.badgeName) return 1; - if (a.badge.badgeName < b.badge.badgeName) return -1; - return 0; + if (!Array.isArray(allBadges)) return []; + + let filteredList = allBadges.filter(badge => badge && badge.badge); + + filteredList.sort((a, b) => { + const featuredA = a.featured ?? false; + const featuredB = b.featured ?? false; + const rankingA = a.badge?.ranking ?? 0; + const rankingB = b.badge?.ranking ?? 0; + const nameA = a.badge?.badgeName ?? ''; + const nameB = b.badge?.badgeName ?? ''; + + if (featuredA > featuredB) return -1; + if (featuredA < featuredB) return 1; + if (rankingA > rankingB) return 1; + if (rankingA < rankingB) return -1; + return nameA.localeCompare(nameB); }); return filteredList.slice(0, 5); }; + useEffect(() => { setFilteredBadges(filterBadges(props.badges)); }, [props.badges]); @@ -25,10 +34,16 @@ const FeaturedBadges = props => { return (
{filteredBadges.map((value, index) => ( - + ))}
); }; -export default FeaturedBadges; +export default FeaturedBadges; \ No newline at end of file diff --git a/src/components/UserProfile/UserProfile.jsx b/src/components/UserProfile/UserProfile.jsx index fbcc082160..c99db62d23 100644 --- a/src/components/UserProfile/UserProfile.jsx +++ b/src/components/UserProfile/UserProfile.jsx @@ -1055,7 +1055,7 @@ function UserProfile(props) { > {showSelect ? 'Hide Team Weekly Summaries' : 'Show Team Weekly Summaries'} - {canGetProjectMembers && teams.length !== 0 ? ( + {(canGetProjectMembers && teams.length !== 0) || ['Owner','Administrator','Manager'].includes(requestorRole) ? (