diff --git a/src/components/Badge/__tests__/NewBadges.test.js b/src/components/Badge/__tests__/NewBadges.test.js index 4e73ec0cdf..54e3c359c0 100644 --- a/src/components/Badge/__tests__/NewBadges.test.js +++ b/src/components/Badge/__tests__/NewBadges.test.js @@ -64,8 +64,8 @@ describe('NewBadges component', () => { mockBadges.forEach(async (badge, index) => { fireEvent.mouseEnter(badgeImages[index]); await waitFor(() => { - expect(screen.getByText(badge.badge.badgeName)).toBeInTheDocument(); - expect(screen.getByText(badge.badge.description)).toBeInTheDocument(); + expect(screen.getByText(badge.badge.badge.badgeName)).toBeInTheDocument(); + expect(screen.getByText(badge.badge.badge.description)).toBeInTheDocument(); }); }); }); diff --git a/src/components/LeaderBoard/Leaderboard.jsx b/src/components/LeaderBoard/Leaderboard.jsx index afafbea85b..062b388fc6 100644 --- a/src/components/LeaderBoard/Leaderboard.jsx +++ b/src/components/LeaderBoard/Leaderboard.jsx @@ -2,7 +2,20 @@ import { useEffect, useState, useRef } from 'react'; import './Leaderboard.css'; import { isEqual } from 'lodash'; import { Link } from 'react-router-dom'; -import { Table, Progress, Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; +import { + Table, + Progress, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + Button, + Dropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, + Spinner, +} from 'reactstrap'; import Alert from 'reactstrap/lib/Alert'; import { hasLeaderboardPermissions, @@ -15,10 +28,13 @@ import MouseoverTextTotalTimeEditButton from 'components/mouseoverText/Mouseover import { toast } from 'react-toastify'; import EditableInfoModal from 'components/UserProfile/EditableModal/EditableInfoModal'; import moment from 'moment-timezone'; +import { boxStyle } from 'styles'; +import axios from 'axios'; import { getUserProfile } from 'actions/userProfile'; import { useDispatch } from 'react-redux'; import { boxStyleDark } from 'styles'; import '../Header/DarkMode.css'; +import { ENDPOINTS } from '../../utils/URL'; function useDeepEffect(effectFunc, deps) { const isFirst = useRef(true); @@ -76,6 +92,79 @@ function LeaderBoard({ getMouseoverText(); setMouseoverTextValue(totalTimeMouseoverText); }, [totalTimeMouseoverText]); + const [teams, setTeams] = useState([]); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [selectedTeamName, setSelectedTeamName] = useState('Select a Team'); + const [textButton, setTextButton] = useState('My Team'); + const [usersSelectedTeam, setUsersSelectedTeam] = useState([]); + const [isLoadingTeams, setIsLoadingTeams] = useState(false); + const [userRole, setUserRole] = useState(); + const [teamsUsers, setTeamsUsers] = useState(leaderBoardData); + const [innerWidth, setInnerWidth] = useState(); + + useEffect(() => { + const fetchInitial = async () => { + const url = ENDPOINTS.USER_PROFILE(displayUserId); + try { + const response = await axios.get(url); + setTeams(response.data.teams); + setUserRole(response.data.role); + } catch (error) { + toast.error(error); + } + }; + + fetchInitial(); + }, []); + + useEffect(() => { + if (!isEqual(leaderBoardData, teamsUsers)) { + if (selectedTeamName === 'Select a Team') { + setTeamsUsers(leaderBoardData); + } + } + }, [leaderBoardData]); + + useEffect(() => { + setInnerWidth(window.innerWidth); + }, [window.innerWidth]); + + const toggleDropdown = () => setDropdownOpen(prevState => !prevState); + + const renderTeamsList = async team => { + if (!team) { + setIsLoadingTeams(true); + + setTimeout(() => { + setIsLoadingTeams(false); + setTeamsUsers(leaderBoardData); + }, 1000); + } else { + try { + setIsLoadingTeams(true); + const response = await axios.get(ENDPOINTS.TEAM_MEMBERS(team._id)); + const idUsers = response.data.map(item => item._id); + const usersTaks = leaderBoardData.filter(item => idUsers.includes(item.personId)); + setTeamsUsers(usersTaks); + setIsLoadingTeams(false); + } catch (error) { + toast.error('Error fetching team members:', error); + setIsLoadingTeams(false); + } + } + }; + + const handleToggleButtonClick = () => { + if (textButton === 'View All') { + setTextButton('My Team'); + renderTeamsList(null); + } else if (usersSelectedTeam.length === 0) { + toast.error(`You have not selected a team or the selected team does not have any members.`); + } else { + setTextButton('View All'); + renderTeamsList(usersSelectedTeam); + } + }; const handleMouseoverTextUpdate = text => { setMouseoverTextValue(text); @@ -90,7 +179,7 @@ function LeaderBoard({ if (window.screen.width < 540) { const scrollWindow = document.getElementById('leaderboard'); if (scrollWindow) { - const elem = document.getElementById(`id${userId}`); // + const elem = document.getElementById(`id${userId}`); if (elem) { const topPos = elem.offsetTop; @@ -132,7 +221,14 @@ function LeaderBoard({ }; const updateLeaderboardHandler = async () => { setIsLoading(true); - await getLeaderboardData(userId); + if (isEqual(leaderBoardData, teamsUsers)) { + await getLeaderboardData(userId); + setTeamsUsers(leaderBoardData); + } else { + await getLeaderboardData(userId); + renderTeamsList(usersSelectedTeam); + setTextButton('View All'); + } setIsLoading(false); toast.success('Successfuly updated leaderboard'); }; @@ -141,6 +237,24 @@ function LeaderBoard({ showTimeOffRequestModal(request); }; + const teamName = (name, maxLength) => + setSelectedTeamName(maxLength > 15 ? `${name.substring(0, 15)}...` : name); + + const dropdownName = (name, maxLength) => { + if (innerWidth > 457) { + return maxLength > 50 ? `${name.substring(0, 50)}...` : name; + } + return maxLength > 27 ? `${name.substring(0, 27)}...` : name; + }; + + const TeamSelected = team => { + if (team.teamName.length !== undefined) { + teamName(team.teamName, team.teamName.length); + } + setUsersSelectedTeam(team); + setTextButton('My Team'); + }; + return (

@@ -166,6 +280,47 @@ function LeaderBoard({ />

+ {userRole === 'Administrator' || userRole === 'Owner' ? ( +
+ + + {selectedTeamName} {/* Display selected team or default text */} + + + {teams.length === 0 ? ( + toast.warning('Please, create a team to use the filter.')} + > + Please, create a team to use the filter. + + ) : ( + teams.map(team => ( + TeamSelected(team)}> + {dropdownName(team.teamName, team.teamName.length)} + + )) + )} + + + + {teams.length === 0 ? ( + + + + ) : ( + + )} +
+ ) : null} {!isVisible && (
@@ -255,7 +410,7 @@ function LeaderBoard({ - {leaderBoardData.map(item => ( + {teamsUsers.map(item => (
@@ -304,7 +459,7 @@ function LeaderBoard({ } }} > - {hasLeaderboardPermissions(loggedInUser.role) && + {hasLeaderboardPermissions(item.role) && showStar(item.tangibletime, item.weeklycommittedHours) ? ( 0 ? 'leaderboard-totals-title' : null} + className={item.totalintangibletime_hrs > 0 ? 'boldClass' : null} > {item.totaltime} diff --git a/src/components/TeamMemberTasks/TeamMemberTasks.jsx b/src/components/TeamMemberTasks/TeamMemberTasks.jsx index 9715b9db43..2bfbc1d5d3 100644 --- a/src/components/TeamMemberTasks/TeamMemberTasks.jsx +++ b/src/components/TeamMemberTasks/TeamMemberTasks.jsx @@ -1,6 +1,16 @@ import { Fragment } from 'react'; import { faClock } from '@fortawesome/free-solid-svg-icons'; -import { Table, Row, Col } from 'reactstrap'; +import { + Dropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, + Button, + Spinner, + Table, + Row, + Col, +} from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { fetchTeamMembersTask, deleteTaskNotification } from 'actions/task'; import React, { useEffect, useState, useCallback } from 'react'; @@ -10,7 +20,7 @@ import { TaskDifferenceModal } from './components/TaskDifferenceModal'; import './style.css'; import TaskCompletedModal from './components/TaskCompletedModal'; import EditableInfoModal from 'components/UserProfile/EditableModal/EditableInfoModal'; -import { ENDPOINTS } from 'utils/URL'; +import { boxStyle } from 'styles'; import axios from 'axios'; import moment from 'moment'; import TeamMemberTask from './TeamMemberTask'; @@ -23,9 +33,19 @@ import { fetchAllFollowUps } from '../../actions/followUpActions'; import { MultiSelect } from 'react-multi-select-component'; import { fetchTeamMembersTaskSuccess } from './actions'; +import { Link } from 'react-router-dom'; +import { ENDPOINTS } from 'utils/URL'; + const TeamMemberTasks = React.memo(props => { // props from redux store - const { authUser, displayUser, isLoading, usersWithTasks, usersWithTimeEntries, darkMode } = props; + const { + authUser, + displayUser, + isLoading, + usersWithTasks, + usersWithTimeEntries, + darkMode, + } = props; const [showTaskNotificationModal, setTaskNotificationModal] = useState(false); const [currentTaskNotifications, setCurrentTaskNotifications] = useState([]); @@ -51,11 +71,41 @@ const TeamMemberTasks = React.memo(props => { const [selectedCodes, setSelectedCodes] = useState([]); const [selectedColors, setSelectedColors] = useState([]); + const [teams, setTeams] = useState(displayUser.teams); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [usersSelectedTeam, setUsersSelectedTeam] = useState([]); + const [selectedTeamName, setSelectedTeamName] = useState('Select a Team'); + const [userRole, setUserRole] = useState(displayUser.role); + const [loading, setLoading] = useState(false); + const [textButton, setTextButton] = useState('My Team'); + const [innerWidth, setInnerWidth] = useState(); + const [controlUseEfffect, setControlUseEfffect] = useState(false); + + const handleToggleButtonClick = () => { + if (textButton === 'View All') { + renderTeamsList(null); + setTextButton('My Team'); + setControlUseEfffect(false); + } else if (usersSelectedTeam.length === 0) { + toast.error(`You have not selected a team or the selected team does not have any members.`); + } else { + renderTeamsList(usersSelectedTeam); + setTextButton('View All'); + setControlUseEfffect(true); + } + }; + + useEffect(() => { + setInnerWidth(window.innerWidth); + }, [window.innerWidth]); + + const toggleDropdown = () => setDropdownOpen(prevState => !prevState); + const dispatch = useDispatch(); useEffect(() => { dispatch(getAllTimeOffRequests()); - dispatch(fetchAllFollowUps()) + dispatch(fetchAllFollowUps()); }, []); const closeMarkAsDone = () => { @@ -161,7 +211,7 @@ const TeamMemberTasks = React.memo(props => { handleOpenTaskNotificationModal(); }; - const getTimeEntriesForPeriod = async (selectedPeriod) => { + const getTimeEntriesForPeriod = async selectedPeriod => { const oneDayAgo = moment() .tz('America/Los_Angeles') .subtract(1, 'days') @@ -171,7 +221,7 @@ const TeamMemberTasks = React.memo(props => { .tz('America/Los_Angeles') .subtract(2, 'days') .format('YYYY-MM-DD'); - + const threeDaysAgo = moment() .tz('America/Los_Angeles') .subtract(3, 'days') @@ -184,19 +234,27 @@ const TeamMemberTasks = React.memo(props => { switch (selectedPeriod) { case '1': - const oneDaysList = usersWithTimeEntries.filter(entry => moment(entry.dateOfWork).isAfter(oneDayAgo)); + const oneDaysList = usersWithTimeEntries.filter(entry => + moment(entry.dateOfWork).isAfter(oneDayAgo), + ); setTimeEntriesList(oneDaysList); break; case '2': - const twoDaysList = usersWithTimeEntries.filter(entry => moment(entry.dateOfWork).isAfter(twoDaysAgo)); + const twoDaysList = usersWithTimeEntries.filter(entry => + moment(entry.dateOfWork).isAfter(twoDaysAgo), + ); setTimeEntriesList(twoDaysList); break; case '3': - const threeDaysList = usersWithTimeEntries.filter(entry => moment(entry.dateOfWork).isAfter(threeDaysAgo)); + const threeDaysList = usersWithTimeEntries.filter(entry => + moment(entry.dateOfWork).isAfter(threeDaysAgo), + ); setTimeEntriesList(threeDaysList); break; case '4': - const fourDaysList = usersWithTimeEntries.filter(entry => moment(entry.dateOfWork).isAfter(fourDaysAgo)); + const fourDaysList = usersWithTimeEntries.filter(entry => + moment(entry.dateOfWork).isAfter(fourDaysAgo), + ); setTimeEntriesList(fourDaysList); break; case '7': @@ -220,18 +278,40 @@ const TeamMemberTasks = React.memo(props => { } }; - const renderTeamsList = async () => { - if (usersWithTasks.length > 0) { - //sort all users by their name - usersWithTasks.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)); - //find currentUser - const currentUserIndex = usersWithTasks.findIndex(user => user.personId === displayUser._id); - // if current user doesn't have any task, the currentUser cannot be found - if (usersWithTasks[currentUserIndex]?.tasks.length) { - //conditional variable for moving current user up front. - usersWithTasks.unshift(...usersWithTasks.splice(currentUserIndex, 1)); + const renderTeamsList = async team => { + if (!team) { + if (usersWithTasks.length > 0) { + setLoading(true); + //sort all users by their name + + usersWithTasks.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)); + //find currentUser + const currentUserIndex = usersWithTasks.findIndex( + user => user.personId === displayUser._id, + ); + // if current user doesn't have any task, the currentUser cannot be found + if (usersWithTasks[currentUserIndex]?.tasks.length) { + //conditional variable for moving current user up front. + usersWithTasks.unshift(...usersWithTasks.splice(currentUserIndex, 1)); + } + + setTimeout(() => { + setLoading(false); + setTeamList([...usersWithTasks]); + }, 3000); + } + } else { + try { + setLoading(true); + const response = await axios.get(ENDPOINTS.TEAM_MEMBERS(team._id)); + const idUsers = response.data.map(item => item._id); + const usersTaks = usersWithTasks.filter(item => idUsers.includes(item.personId)); + setTeamList(usersTaks); + setLoading(false); + } catch (error) { + toast.error('Error fetching team members:', error); + setLoading(false); } - setTeamList([...usersWithTasks]); } }; @@ -243,14 +323,14 @@ const TeamMemberTasks = React.memo(props => { const teamCodeOptions = []; const colorOptions = []; - if(usersWithTasks.length > 0) { + if (usersWithTasks.length > 0) { usersWithTasks.forEach(user => { const teamNames = user.teams.map(team => team.teamName); const code = user.teamCode || 'noCodeLabel'; const color = user.weeklySummaryOption || 'noColorLabel'; teamNames.forEach(name => { - if(teamGroup[name]) { + if (teamGroup[name]) { teamGroup[name].push(user.personId); } else { teamGroup[name] = [user.personId]; @@ -270,36 +350,42 @@ const TeamMemberTasks = React.memo(props => { } }); - Object.keys(teamGroup).sort((a,b) => a.localeCompare(b)).forEach(name => { - teamOptions.push({ - value: name, - label: `${name}` + Object.keys(teamGroup) + .sort((a, b) => a.localeCompare(b)) + .forEach(name => { + teamOptions.push({ + value: name, + label: `${name}`, + }); }); - }) - Object.keys(teamCodeGroup).sort((a,b) => a.localeCompare(b)).forEach(code => { - if (code !== 'noCodeLabel') { - teamCodeOptions.push({ - value: code, - label: `${code}` - }); - } - }); + Object.keys(teamCodeGroup) + .sort((a, b) => a.localeCompare(b)) + .forEach(code => { + if (code !== 'noCodeLabel') { + teamCodeOptions.push({ + value: code, + label: `${code}`, + }); + } + }); - Object.keys(colorGroup).sort((a,b) => a.localeCompare(b)).forEach(color => { - if (color !== 'noColorLabel') { - colorOptions.push({ - value: color, - label: `${color}` - }); - } - }); + Object.keys(colorGroup) + .sort((a, b) => a.localeCompare(b)) + .forEach(color => { + if (color !== 'noColorLabel') { + colorOptions.push({ + value: color, + label: `${color}`, + }); + } + }); setTeamNames(teamOptions); setTeamCodes(teamCodeOptions); setColors(colorOptions); } - } + }; useEffect(() => { // TeamMemberTasks is only imported in TimeLog component, in which userId is already definitive @@ -318,9 +404,11 @@ const TeamMemberTasks = React.memo(props => { useEffect(() => { if (!isLoading) { - renderTeamsList(); + renderTeamsList( + !controlUseEfffect || usersSelectedTeam.length === 0 ? null : usersSelectedTeam, + ); closeMarkAsDone(); - if(['Administrator', 'Owner', 'Manager', 'Mentor'].some( role => role === displayUser.role)) { + if (['Administrator', 'Owner', 'Manager', 'Mentor'].some(role => role === displayUser.role)) { renderFilters(); } } @@ -334,112 +422,200 @@ const TeamMemberTasks = React.memo(props => { setShowWhoHasTimeOff(prev => !prev); }; + const TeamSelected = team => { + team.teamName.length !== undefined ? teamName(team.teamName, team.teamName.length) : null; + setUsersSelectedTeam(team); + setTextButton('My Team'); + }; + + const teamName = (name, maxLength) => + setSelectedTeamName(maxLength > 15 ? `${name.substring(0, 15)}...` : name); + + const dropdownName = (name, maxLength) => { + if (innerWidth >= 457) { + return maxLength > 50 ? `${name.substring(0, 50)}...` : name; + } else { + return maxLength > 15 ? `${name.substring(0, 15)}...` : name; + } + }; + const handleSelectTeamNames = event => { setSelectedTeamNames(event); - } + }; const handleSelectCodeChange = event => { setSelectedCodes(event); - } + }; const handleSelectColorChange = event => { setSelectedColors(event); - } + }; - const filterByUserFeatures = (user) => { - if(selectedTeamNames.length === 0 && selectedCodes.length === 0 && selectedColors.length === 0) return true; + const filterByUserFeatures = user => { + if (selectedTeamNames.length === 0 && selectedCodes.length === 0 && selectedColors.length === 0) + return true; - return filterByTeamCodes(user.teamCode) && filterByColors(user.weeklySummaryOption) && filterByTeams(user.teams); - } + return ( + filterByTeamCodes(user.teamCode) && + filterByColors(user.weeklySummaryOption) && + filterByTeams(user.teams) + ); + }; - const filterByTeams = (teams) => { - if(selectedTeamNames.length === 0) return true; + const filterByTeams = teams => { + if (selectedTeamNames.length === 0) return true; let match = false; - teams.forEach(team => match = match || filterByTeamName(team.teamName)); + teams.forEach(team => (match = match || filterByTeamName(team.teamName))); return match; - } + }; - const filterByTeamName = (name) => { + const filterByTeamName = name => { return selectedTeamNames.some(option => option.value === name); - } + }; - const filterByTeamCodes = (code) => { - if(selectedCodes.length === 0) return true; + const filterByTeamCodes = code => { + if (selectedCodes.length === 0) return true; return selectedCodes.some(option => option.value === code); - } + }; - const filterByColors = (color) => { - if(selectedColors.length === 0) return true; + const filterByColors = color => { + if (selectedColors.length === 0) return true; return selectedColors.some(option => option.value === color); - } - + }; return ( -
+
-

Team Member Tasks

- - {finishLoading ? ( -
- + + ) : ( + + )} + + ) : !isLoading && userRole !== 'Administrator' && userRole !== 'Owner' ? null : null} + + {finishLoading ? ( +
+
+ - {Object.entries(hrsFilterBtnColorMap).map(([days, color], idx) => ( - - ))} + {Object.entries(hrsFilterBtnColorMap).map(([days, color], idx) => ( + + ))} +
-
+ ) : ( )} @@ -472,13 +648,10 @@ const TeamMemberTasks = React.memo(props => { darkMode={darkMode} /> )} - { - ['Administrator', 'Owner', 'Manager', 'Mentor'].some( role => role === displayUser.role) && + {['Administrator', 'Owner', 'Manager', 'Mentor'].some(role => role === displayUser.role) && ( - - - Select Team - + + Select Team { }} /> - - - Select Team Code - + + Select Team Code { /> - - Select Color - + Select Color { /> - } + )}
- + {/* Empty column header for hours completed icon */} -
- - - + -
+ + + - -
+ Team Member - + + / {
- - +
+ + - - + + {displayUser.role === 'Administrator' ? : null} @@ -568,32 +763,16 @@ const TeamMemberTasks = React.memo(props => { {isLoading && usersWithTasks.length === 0 ? ( ) : ( - teamList.filter((user) => filterByUserFeatures(user)).map(user => { - if (!isTimeFilterActive) { - return ( - - ); - } else { - return ( - + teamList + .filter(user => filterByUserFeatures(user)) + .map(user => { + if (!isTimeFilterActive) { + return ( { onTimeOff={userOnTimeOff[user.personId]} goingOnTimeOff={userGoingOnTimeOff[user.personId]} /> - {timeEntriesList.length > 0 && - timeEntriesList - .filter(timeEntry => timeEntry.personId === user.personId) - .map(timeEntry => ( - - - - ))} - - ); - } - }) + ); + } else { + return ( + + + {timeEntriesList.length > 0 && + timeEntriesList + .filter(timeEntry => timeEntry.personId === user.personId) + .map(timeEntry => ( + + + + ))} + + ); + } + }) )}
Tasks(s)ProgressTasks(s) + Progress + Status
- -
+ +