-
Notifications
You must be signed in to change notification settings - Fork 6
Feature 2295 review assignment percentages #2391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7617a3c
e3f249e
86cda65
b2f7203
d9c8dbc
1a232ef
41f8ba5
a6f1f17
bb502b9
0beeac6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| .review-period-card { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The UI that renders each review period in |
||
| .MuiListItemAvatar-root { | ||
| padding: 0.7rem 0; | ||
| } | ||
|
|
||
| .bottom-row { | ||
| padding: 0.5rem; | ||
| } | ||
|
|
||
| .row { | ||
| display: flex; | ||
| align-items: center; | ||
| } | ||
|
|
||
| .top-row { | ||
| padding-left: 0.7rem; | ||
|
|
||
| &:hover { | ||
| cursor: pointer; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| import PropTypes from 'prop-types'; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the new component that was factored out of |
||
| import React, { useCallback, useContext, useEffect, useState } from 'react'; | ||
|
|
||
| import { | ||
| BorderColor, | ||
| DoorFront, | ||
| HourglassTop, | ||
| MeetingRoom, | ||
| QuestionMark | ||
| } from '@mui/icons-material'; | ||
|
|
||
| import { | ||
| Avatar, | ||
| Card, | ||
| CardActions, | ||
| CardContent, | ||
| Collapse, | ||
| ListItemAvatar, | ||
| ListItemText | ||
| } from '@mui/material'; | ||
|
|
||
| import ExpandMore from '../../expand-more/ExpandMore'; | ||
| import { resolve } from '../../../api/api.js'; | ||
| import { AppContext } from '../../../context/AppContext'; | ||
| import { | ||
| selectCsrfToken, | ||
| selectCurrentMembers, | ||
| selectHasUpdateReviewAssignmentsPermission, | ||
| selectReviewPeriods | ||
| } from '../../../context/selectors'; | ||
| import { titleCase } from '../../../helpers/strings.js'; | ||
|
|
||
| import './ReviewPeriodCard.css'; | ||
|
|
||
| const propTypes = { | ||
| mode: PropTypes.string, | ||
| onSelect: PropTypes.func.isRequired, | ||
| periodId: PropTypes.string.isRequired, | ||
| selfReviews: PropTypes.object.isRequired | ||
| }; | ||
| const displayName = 'ReviewPeriodCard'; | ||
|
|
||
| const ReviewStatus = { | ||
| PLANNING: 'PLANNING', | ||
| AWAITING_APPROVAL: 'AWAITING_APPROVAL', | ||
| OPEN: 'OPEN', | ||
| CLOSED: 'CLOSED', | ||
| UNKNOWN: 'UNKNOWN' | ||
| }; | ||
|
|
||
| const reviewStatusIconMap = { | ||
| [ReviewStatus.PLANNING]: <BorderColor />, | ||
| [ReviewStatus.AWAITING_APPROVAL]: <HourglassTop />, | ||
| [ReviewStatus.OPEN]: <MeetingRoom />, | ||
| [ReviewStatus.CLOSED]: <DoorFront />, | ||
| [ReviewStatus.UNKNOWN]: <QuestionMark /> | ||
| }; | ||
|
|
||
| const ReviewPeriodCard = ({ mode, onSelect, periodId, selfReviews }) => { | ||
| const { state } = useContext(AppContext); | ||
| const [approvalStats, setApprovalStats] = useState([]); | ||
| const [overallApprovalPercentage, setOverallApprovalPercentage] = useState(0); | ||
| const [expanded, setExpanded] = useState(false); | ||
|
|
||
| const csrf = selectCsrfToken(state); | ||
| const currentMembers = selectCurrentMembers(state); | ||
| const canUpdateReviewAssignments = | ||
| selectHasUpdateReviewAssignmentsPermission(state); | ||
| const periods = selectReviewPeriods(state); | ||
|
|
||
| const period = periods.find(period => period.id === periodId); | ||
| const showPercentages = | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This determines whether we should show percentages. The user must have a certain permission AND the review period must have a status of "AWAITING APPROVAL". |
||
| canUpdateReviewAssignments && | ||
| period.reviewStatus === ReviewStatus.AWAITING_APPROVAL; | ||
| const { name, reviewStatus } = period; | ||
|
|
||
| const handleExpandClick = () => setExpanded(!expanded); | ||
|
|
||
| const loadApprovalStats = async () => { | ||
| try { | ||
| // Get all the review assignments for this period. | ||
| const res = await resolve({ | ||
| method: 'GET', | ||
| url: `/services/review-assignments/period/${periodId}`, | ||
| headers: { | ||
| 'X-CSRF-Header': csrf, | ||
| Accept: 'application/json', | ||
| 'Content-Type': 'application/json;charset=UTF-8' | ||
| } | ||
| }); | ||
| if (res.error) throw new Error(res.error.message); | ||
| const assignments = res.payload.data; | ||
| const approvedCount = assignments.filter(a => a.approved).length; | ||
| setOverallApprovalPercentage((100 * approvedCount) / assignments.length); | ||
|
|
||
| // Get a list of all the reviewers in this period. | ||
| const reviewerIds = new Set(); | ||
| for (const assignment of assignments) { | ||
| reviewerIds.add(assignment.reviewerId); | ||
| } | ||
| const reviewers = [...reviewerIds].map(id => | ||
| currentMembers.find(m => m.id === id) | ||
| ); | ||
| reviewers.sort((a, b) => a.name.localeCompare(b.name)); | ||
|
|
||
| // Build an array containing statistics for each reviewer. | ||
| const stats = reviewers.map(reviewer => { | ||
| const { id } = reviewer; | ||
| const assignmentsForReviewer = assignments.filter( | ||
| assignment => assignment.reviewerId === id | ||
| ); | ||
| const approved = assignmentsForReviewer.filter( | ||
| assignment => assignment.approved | ||
| ).length; | ||
| return { | ||
| name: reviewer.name, | ||
| percent: | ||
| ((100 * approved) / assignmentsForReviewer.length).toFixed(0) + '%' | ||
| }; | ||
| }); | ||
|
|
||
| setApprovalStats(stats); | ||
| } catch (err) { | ||
| console.error('ReviewPeriods.jsx getApprovalStats:', err); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (csrf && currentMembers.length) loadApprovalStats(); | ||
| }, [csrf, currentMembers]); | ||
|
|
||
| const getSecondaryLabel = useCallback( | ||
| periodId => { | ||
| if (mode === 'self') { | ||
| if ( | ||
| selectReviewPeriod(state, periodId)?.reviewStatus === | ||
| ReviewStatus.OPEN | ||
| ) { | ||
| if (!selfReviews[periodId] || selfReviews[periodId] === null) { | ||
| return 'Click to start your review.'; | ||
| } else { | ||
| if (selfReviews[periodId].status.toUpperCase() === 'SUBMITTED') { | ||
| return 'Your review has been submitted. Thank you!'; | ||
| } else { | ||
| return 'Click to finish your review.'; | ||
| } | ||
| } | ||
| } else { | ||
| return 'This review period is closed.'; | ||
| } | ||
| } | ||
| }, | ||
| [mode, selfReviews, state] | ||
| ); | ||
|
|
||
| return ( | ||
| <Card className="review-period-card" key={`period-${periodId}`}> | ||
| <div className="row top-row"> | ||
| <ListItemAvatar | ||
| key={`period-lia-${periodId}`} | ||
| onClick={() => onSelect(periodId)} | ||
| > | ||
| <Avatar>{reviewStatusIconMap[reviewStatus]}</Avatar> | ||
| </ListItemAvatar> | ||
| <ListItemText | ||
| key={`period-lit-${periodId}`} | ||
| onClick={() => onSelect(periodId)} | ||
| primary={`${name} - ${titleCase(ReviewStatus[reviewStatus])}`} | ||
| secondary={getSecondaryLabel(periodId)} | ||
| /> | ||
| {showPercentages && ( | ||
| <div className="row"> | ||
| <ListItemText | ||
| key={`period-percent-${periodId}`} | ||
| primary={overallApprovalPercentage.toFixed(0) + '%'} | ||
| /> | ||
| <CardActions disableSpacing> | ||
| <ExpandMore | ||
| expand={expanded} | ||
| onClick={handleExpandClick} | ||
| aria-expanded={expanded} | ||
| aria-label={expanded ? 'show less' : 'show more'} | ||
| size="large" | ||
| /> | ||
| </CardActions> | ||
| </div> | ||
| )} | ||
| </div> | ||
| {showPercentages && ( | ||
| <Collapse | ||
| className="bottom-row" | ||
| in={expanded} | ||
| timeout="auto" | ||
| unmountOnExit | ||
| > | ||
| {approvalStats.map(stats => ( | ||
| <div key={stats.name}> | ||
| {stats.name} - {stats.percent} | ||
| </div> | ||
| ))} | ||
| </Collapse> | ||
| )} | ||
| </Card> | ||
| ); | ||
| }; | ||
|
|
||
| ReviewPeriodCard.propTypes = propTypes; | ||
| ReviewPeriodCard.displayName = displayName; | ||
|
|
||
| export default ReviewPeriodCard; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,6 @@ | ||
| import PropTypes from 'prop-types'; | ||
| import React, { useCallback, useContext, useEffect, useState } from 'react'; | ||
|
|
||
| import { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the deleted code was moved to the new component |
||
| BorderColor, | ||
| DoorFront, | ||
| HourglassTop, | ||
| MeetingRoom, | ||
| QuestionMark | ||
| } from '@mui/icons-material'; | ||
|
|
||
| import { | ||
| Avatar, | ||
| Box, | ||
|
|
@@ -51,7 +43,7 @@ import { | |
| selectUserProfile | ||
| } from '../../../context/selectors'; | ||
|
|
||
| import { titleCase } from '../../../helpers/strings.js'; | ||
| import ReviewPeriodCard from './ReviewPeriodCard.jsx'; | ||
|
|
||
| const propTypes = { | ||
| message: PropTypes.string, | ||
|
|
@@ -114,14 +106,6 @@ const ReviewStatus = { | |
| UNKNOWN: 'UNKNOWN' | ||
| }; | ||
|
|
||
| const reviewStatusIconMap = { | ||
| [ReviewStatus.PLANNING]: <BorderColor />, | ||
| [ReviewStatus.AWAITING_APPROVAL]: <HourglassTop />, | ||
| [ReviewStatus.OPEN]: <MeetingRoom />, | ||
| [ReviewStatus.CLOSED]: <DoorFront />, | ||
| [ReviewStatus.UNKNOWN]: <QuestionMark /> | ||
| }; | ||
|
|
||
| const ReviewPeriods = ({ onPeriodSelected, mode }) => { | ||
| const { state, dispatch } = useContext(AppContext); | ||
|
|
||
|
|
@@ -135,7 +119,7 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => { | |
| closeDate: null | ||
| }); | ||
| const [reviewStatus, setReviewStatus] = useState(ReviewStatus.CLOSED); | ||
| const [selfReviews, setSelfReviews] = useState(null); | ||
| const [selfReviews, setSelfReviews] = useState({}); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This holds a map of review data. Initializing it to an empty object simplifies the code a bit. |
||
| const [templates, setTemplates] = useState([]); | ||
|
|
||
| const currentUserId = selectCurrentUserId(state); | ||
|
|
@@ -216,34 +200,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => { | |
| ] | ||
| ); | ||
|
|
||
| const getSecondaryLabel = useCallback( | ||
| periodId => { | ||
| if (mode === 'self') { | ||
| if ( | ||
| selectReviewPeriod(state, periodId)?.reviewStatus === | ||
| ReviewStatus.OPEN | ||
| ) { | ||
| if ( | ||
| !selfReviews || | ||
| !selfReviews[periodId] || | ||
| selfReviews[periodId] === null | ||
| ) { | ||
| return 'Click to start your review.'; | ||
| } else { | ||
| if (selfReviews[periodId].status.toUpperCase() === 'SUBMITTED') { | ||
| return 'Your review has been submitted. Thank you!'; | ||
| } else { | ||
| return 'Click to finish your review.'; | ||
| } | ||
| } | ||
| } else { | ||
| return 'This review period is closed.'; | ||
| } | ||
| } | ||
| }, | ||
| [selfReviews, state, mode] | ||
| ); | ||
|
|
||
| const loadFeedbackTemplates = useCallback(async () => { | ||
| const res = await getAllFeedbackTemplates(csrf); | ||
| const templates = res?.payload?.data; | ||
|
|
@@ -335,7 +291,7 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => { | |
| periods && | ||
| periods.length > 0 && | ||
| currentUserId && | ||
| selfReviews == null | ||
| Object.keys(selfReviews).length === 0 | ||
| ) { | ||
| getSelfReviews(); | ||
| } | ||
|
|
@@ -426,22 +382,13 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => { | |
| : 1; | ||
| }) | ||
| .map(({ name, reviewStatus, id }, i) => ( | ||
| <div key={i} className="reviewPeriodSection"> | ||
| <ListItem key={`period-${id}`}> | ||
| <ListItemAvatar | ||
| key={`period-lia-${id}`} | ||
| onClick={() => onPeriodClick(id)} | ||
| > | ||
| <Avatar>{reviewStatusIconMap[reviewStatus]}</Avatar> | ||
| </ListItemAvatar> | ||
| <ListItemText | ||
| key={`period-lit-${id}`} | ||
| onClick={() => onPeriodClick(id)} | ||
| primary={`${name} - ${titleCase(ReviewStatus[reviewStatus])}`} | ||
| secondary={getSecondaryLabel(id)} | ||
| /> | ||
| </ListItem> | ||
| </div> | ||
| <ReviewPeriodCard | ||
| key={`review-period-card-${id}`} | ||
| mode={mode} | ||
| onSelect={onPeriodClick} | ||
| periodId={id} | ||
| selfReviews={selfReviews} | ||
| /> | ||
| )) | ||
| ) : ( | ||
| <Typography variant="body1"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This CSS class is no longer used.