diff --git a/web-ui/src/app.d.js b/web-ui/src/app.d.js index 4f1c64f760..e1eaac7e87 100644 --- a/web-ui/src/app.d.js +++ b/web-ui/src/app.d.js @@ -26,3 +26,16 @@ * @typedef {MemberProfile} PDLProfile * @property {string} pdlId */ + +/** + * Check-In related types + * + * @typedef {Object} Checkin + * @property {string} id - The ID of the check-in. + * @property {boolean} completed - Indicates whether the check-in is completed. + * @property {Array} checkinDate - The date of the check-in. + * @property {string} pdlId - The ID of the PDL. + * @property {string} teamMemberId - The ID of the team member. + * + * @typedef {("Done" | "In Progress" | "Not Started")} CheckinStatus + */ diff --git a/web-ui/src/components/member_selector/MemberSelector.jsx b/web-ui/src/components/member_selector/MemberSelector.jsx index 60ebb5ff80..0c8ad7e2fe 100644 --- a/web-ui/src/components/member_selector/MemberSelector.jsx +++ b/web-ui/src/components/member_selector/MemberSelector.jsx @@ -73,6 +73,7 @@ const MemberSelector = ({ selected, onChange, title = 'Selected Members', + expand = true, outlined = false, exportable = false, listHeight = 400, @@ -84,7 +85,7 @@ const MemberSelector = ({ const csrf = selectCsrfToken(state); const [dialogOpen, setDialogOpen] = useState(false); - const [expanded, setExpanded] = useState(true); + const [expanded, setExpanded] = useState(expand); const [menuAnchor, setMenuAnchor] = useState(null); const [filters, setFilters] = useState(initialFilters); diff --git a/web-ui/src/components/reports-section/CheckinReport.css b/web-ui/src/components/reports-section/CheckinReport.css deleted file mode 100644 index 5840bb7d72..0000000000 --- a/web-ui/src/components/reports-section/CheckinReport.css +++ /dev/null @@ -1,49 +0,0 @@ -.checkin-report-card { - display: flex; - flex-direction: column; - margin: 1rem 0; - width: 100vw; - - .checkin-report-accordion-summary { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 0.5rem 1rem; - margin-top: 0.5rem; - cursor: pointer; - } - - .member-sub-card-summmary-content { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - width: 100%; - } - - .checkin-report-stepper { - margin-bottom: 1rem; - } - - .checkin-report-link { - display: flex; - flex-direction: row; - justify-content: space-between; - } - - .checkin-report-card-name { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - max-width: 20rem; - } -} - -.checkin-report-no-data { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 10rem; -} diff --git a/web-ui/src/components/reports-section/CheckinReport.jsx b/web-ui/src/components/reports-section/CheckinReport.jsx deleted file mode 100644 index 54766b175a..0000000000 --- a/web-ui/src/components/reports-section/CheckinReport.jsx +++ /dev/null @@ -1,359 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { Link } from 'react-router-dom'; -import PropTypes from 'prop-types'; - -import { getAvatarURL } from '../../api/api.js'; -import { AppContext } from '../../context/AppContext'; -import { selectFilteredCheckinsForTeamMemberAndPDL } from '../../context/selectors'; - -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ExpandMore from '../expand-more/ExpandMore'; - -import { - Accordion, - AccordionDetails, - AccordionSummary, - Avatar, - Box, - Card, - CardContent, - CardHeader, - Chip, - Collapse, - Container, - Divider, - Stepper, - Step, - StepLabel, - Typography -} from '@mui/material'; - -import './CheckinReport.css'; - -/** - * @typedef {Object} Checkin - * @property {string} id - The ID of the check-in. - * @property {boolean} completed - Indicates whether the check-in is completed. - * @property {Array} checkinDate - The date of the check-in. - * @property {string} pdlId - The ID of the PDL. - * @property {string} teamMemberId - The ID of the team member. - */ - -/** - * @typedef {("Done" | "In Progress" | "Not Started")} CheckinStatus - * @typedef {("Not Yet Scheduled" | "Scheduled" | "Completed")} SchedulingStatus - */ - -/** @type {CheckinStatus[]} */ -const steps = ['Not Started', 'In Progress', 'Done']; - -/** @type {SchedulingStatus[]} */ -const schedulingSteps = ['Not Yet Scheduled', 'Scheduled', 'Completed']; - -function HorizontalLinearStepper({ step = 0 }) { - const [activeStep, setActiveStep] = React.useState(step); - const [skipped, setSkipped] = React.useState(new Set()); - - const isStepOptional = step => step === -1; - const isStepSkipped = step => skipped.has(step); - - return ( - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (isStepOptional(index)) { - labelProps.optional = ( - Optional - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - {label} - - ); - })} - - - ); -} - -const propTypes = { - closed: PropTypes.bool, - pdl: PropTypes.shape({ - name: PropTypes.string, - id: PropTypes.string, - members: PropTypes.array, - workEmail: PropTypes.string, - title: PropTypes.string - }), - planned: PropTypes.bool -}; - -const CheckinsReport = ({ closed, pdl, planned }) => { - const { state } = useContext(AppContext); - const [expanded, setExpanded] = useState(true); - const [statusForPeriod, setStatusForPeriod] = useState( - /** @type CheckinStatus */ ('Not Started') - ); - - const { name, id, members, workEmail, title } = pdl; - - const handleExpandClick = () => setExpanded(!expanded); - - const getCheckinDate = checkin => { - if (!checkin || !checkin.checkInDate) return; - const [year, month, day, hour, minute] = checkin.checkInDate; - return new Date(year, month - 1, day, hour, minute, 0); - }; - - /** - * Determine the status of the check-ins for a member. - * @param {Object} params - The parameters object. - * @param {Checkin[]} params.checkins - Checkins for a member. - * @returns {CheckinStatus} The status of check-ins. - */ - const statusForPeriodByMember = ({ checkins = [] }) => { - if (checkins.length === 0) return 'Not Started'; - const completed = checkins.filter(checkin => checkin.completed); - if (completed.length === checkins.length) return 'Done'; - const now = new Date(); - const inProgress = checkins.filter( - checkin => !checkin.completed && getCheckinDate(checkin) < now - ); - if (inProgress.length > 0) return 'Open'; - return 'Not Started'; - }; - - /** - * Get the date of the last check-in. - * @param {Checkin[]} checkins - Check-ins for a member. - * @returns {Date} The date of the last check-in. - */ - const getLastCheckinDate = checkins => { - if (checkins.length === 0) return; - return checkins.reduce((acc, checkin) => { - const checkinDate = getCheckinDate(checkin); - return checkinDate > acc ? checkinDate : acc; - }, new Date(0)); - }; - - /** - * Determine the status of the check-ins for a PDL. - * @param {Object} params - The parameters object. - * @param {Array} params.members - Members of the PDL. - * @returns {CheckinStatus} The status of check-ins. - */ - const statusForPeriodByPDL = ({ members = [] }) => { - if (members.length === 0) return 'Not Started'; - const completed = members.filter(member => { - const checkins = selectFilteredCheckinsForTeamMemberAndPDL( - state, - member.id, - id, - closed, - planned - ); - return ( - checkins.filter(checkin => checkin.completed).length === checkins.length - ); - }); - if (completed.length === members.length) return 'Done'; - const inProgress = members.filter(member => { - const checkins = selectFilteredCheckinsForTeamMemberAndPDL( - state, - member.id, - id, - closed, - planned - ); - const now = new Date(); - return ( - checkins.filter( - checkin => !checkin.completed && getCheckinDate(checkin) < now - ).length > 0 - ); - }); - if (inProgress.length > 0) return 'In Progress'; - return 'Not Started'; - }; - - const LinkSection = ({ checkin, member }) => { - const now = new Date(); - let checkinDate = new Date(getCheckinDate(checkin)); - let dateString = new Date(getCheckinDate(checkin)).toString(); - dateString = dateString.split(' ').slice(0, 5).join(' '); - return ( - -
- {dateString} - now - ? 'Planned' - : 'Open' - } - /> -
- - ); - }; - - const TeamMemberMap = () => { - const filtered = - members && - members.filter(member => { - const checkins = selectFilteredCheckinsForTeamMemberAndPDL( - state, - member.id, - id, - closed, - planned - ); - return checkins && checkins.length > 0; - }); - if (filtered && filtered.length > 0) { - return filtered.map(member => { - const checkins = selectFilteredCheckinsForTeamMemberAndPDL( - state, - member.id, - id, - closed, - planned - ); - return ( - - } - className="checkin-report-accordion-summary" - > -
- - {member.name} - - Last connected:{' '} - {getLastCheckinDate(checkins).toLocaleDateString( - navigator.language, - { - year: 'numeric', - month: '2-digit', - day: 'numeric' - } - )} - - -
-
- - {checkins.map(checkin => ( - - ))} - -
- ); - }); - } else - return ( -
- - No assigned check-ins available for display during this period. - -
- ); - }; - - return ( - - - - - {name} - - - {title} - - - } - disableTypography - avatar={} - action={ -
- - -
- } - /> - - - - -
- -
- -
-
-
-
-
- ); -}; - -CheckinsReport.propTypes = propTypes; -export default CheckinsReport; diff --git a/web-ui/src/components/reports-section/checkin-report/CheckinReport.css b/web-ui/src/components/reports-section/checkin-report/CheckinReport.css new file mode 100644 index 0000000000..3a9ffedbb0 --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/CheckinReport.css @@ -0,0 +1,24 @@ +.checkin-report-card { + display: flex; + flex-direction: column; + margin: 1rem 0; + width: 100vw; + + .checkin-report-stepper { + margin-bottom: 1rem; + } + + .checkin-report-card-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 20rem; + } + + .checkin-report-card-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + } +} diff --git a/web-ui/src/components/reports-section/checkin-report/CheckinReport.jsx b/web-ui/src/components/reports-section/checkin-report/CheckinReport.jsx new file mode 100644 index 0000000000..ea7e987e31 --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/CheckinReport.jsx @@ -0,0 +1,213 @@ +import React, { useContext, useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import { getAvatarURL } from '../../../api/api.js'; +import { AppContext } from '../../../context/AppContext.jsx'; +import { selectFilteredCheckinsForTeamMemberAndPDL } from '../../../context/selectors.js'; + +import ExpandMore from '../../expand-more/ExpandMore.jsx'; +import HorizontalLinearStepper from './HorizontalLinearStepper.jsx'; +import TeamMemberMap from './TeamMemberMap.jsx'; +import { getCheckinDate } from './checkin-utils.js'; + +import { getQuarterBeginEnd } from '../../../helpers/index.js'; + +import { + Avatar, + Box, + Card, + CardContent, + CardHeader, + Chip, + Collapse, + Container, + Divider, + Typography, + Badge +} from '@mui/material'; + +import './CheckinReport.css'; + +const CheckinsReport = ({ closed, pdl, planned, reportDate }) => { + const { state } = useContext(AppContext); + const [expanded, setExpanded] = useState(true); + const [statusForPeriod, setStatusForPeriod] = useState( + /** @type CheckinStatus */ ('Not Started') + ); + + const { name, id, members, workEmail, title } = pdl; + + const handleExpandClick = () => setExpanded(!expanded); + + /** + * Determine the status of the check-ins for a PDL during the reporting period. + * @param {Array} members - Members of the PDL. + * @returns {string} The status of check-ins. + */ + const statusForPeriodByMembers = (members = []) => { + if (members.length === 0) return 'No Members'; + + const isCheckinCompletedDuringPeriod = (checkin, start, end) => { + const checkinDate = getCheckinDate(checkin); + return checkinDate >= start && checkinDate <= end && checkin.completed; + }; + + const { startOfQuarter, endOfQuarter } = getQuarterBeginEnd(reportDate); + + const allCheckinsCompleted = member => { + const checkins = selectFilteredCheckinsForTeamMemberAndPDL( + state, + member.id, + id, + closed, + planned + ); + return checkins.every(checkin => + isCheckinCompletedDuringPeriod(checkin, startOfQuarter, endOfQuarter) + ); + }; + + const allMembersCompleted = members.every(allCheckinsCompleted); + if (allMembersCompleted) return 'Done'; + + const isCheckinInProgress = (checkin, start, end) => { + const checkinDate = getCheckinDate(checkin); + const now = new Date(); + return ( + checkinDate >= start && + checkinDate <= end && + !checkin.completed && + checkinDate < now + ); + }; + + const isInProgress = member => { + let checkins = selectFilteredCheckinsForTeamMemberAndPDL( + state, + member.id, + id, + closed, + planned + ); + checkins = checkins.filter(checkin => + isCheckinInProgress(checkin, startOfQuarter, endOfQuarter) + ); + return checkins.length > 0; + }; + + const anyInProgress = members.some(isInProgress); + if (anyInProgress) return 'In Progress'; + + return 'Not Started'; + }; + + /** + * Set the expanded state based on the status of the check-ins and number of members. + * @param {CheckinStatus} status - The status of the check-ins. + * @param {PDLProfile} pdl - The PDL object. + * @modifies {expanded} + */ + const setExpandedByStatusAndMembers = (status, pdl) => { + const isStatusDone = status === 'Done'; + const hasMembers = pdl.members && pdl.members.length !== 0; + + if (isStatusDone || !hasMembers) { + setExpanded(false); + } else { + setExpanded(true); + } + }; + + // Set status for the period based on members + useEffect(() => { + setStatusForPeriod(statusForPeriodByMembers(members)); + }, [members, reportDate]); + + // Set expanded state based on status and number of members + useEffect(() => { + setExpandedByStatusAndMembers(statusForPeriod, pdl); + }, [statusForPeriod, pdl]); + + return ( + + + + + {name} + + + {title} + + + } + disableTypography + avatar={} + action={ +
+ + + + +
+ } + /> + + + + +
+ +
+ +
+
+
+
+
+ ); +}; + +const propTypes = { + closed: PropTypes.bool, + pdl: PropTypes.shape({ + name: PropTypes.string, + id: PropTypes.string, + members: PropTypes.array, + workEmail: PropTypes.string, + title: PropTypes.string + }), + planned: PropTypes.bool, + reportDate: PropTypes.instanceOf(Date) +}; + +CheckinsReport.propTypes = propTypes; +export default CheckinsReport; diff --git a/web-ui/src/components/reports-section/checkin-report/HorizontalLinearStepper.jsx b/web-ui/src/components/reports-section/checkin-report/HorizontalLinearStepper.jsx new file mode 100644 index 0000000000..3dc8a1c206 --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/HorizontalLinearStepper.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Box, Stepper, Step, StepLabel, Typography } from '@mui/material'; + +/** @type {CheckinStatus[]} */ +const steps = ['Not Started', 'In Progress', 'Done']; + +export default function HorizontalLinearStepper({ step = 0 }) { + const [activeStep, setActiveStep] = React.useState(step); + const [skipped, setSkipped] = React.useState(new Set()); + + const isStepOptional = step => step === -1; + const isStepSkipped = step => skipped.has(step); + + return ( + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (isStepOptional(index)) { + labelProps.optional = ( + Optional + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label} + + ); + })} + + + ); +} diff --git a/web-ui/src/components/reports-section/checkin-report/LinkSection.css b/web-ui/src/components/reports-section/checkin-report/LinkSection.css new file mode 100644 index 0000000000..8a8fb2676c --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/LinkSection.css @@ -0,0 +1,6 @@ +.link-section-link-body { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 0.5rem; +} diff --git a/web-ui/src/components/reports-section/checkin-report/LinkSection.jsx b/web-ui/src/components/reports-section/checkin-report/LinkSection.jsx new file mode 100644 index 0000000000..0ffef2e7c2 --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/LinkSection.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Chip, Typography } from '@mui/material'; +import { getCheckinDate } from './checkin-utils'; +import './LinkSection.css'; + +const LinkSection = ({ checkin, member }) => { + const now = new Date(); + let checkinDate = new Date(getCheckinDate(checkin)); + let dateString = new Date(getCheckinDate(checkin)).toString(); + dateString = dateString.split(' ').slice(0, 5).join(' '); + + return ( + +
+ {dateString} + now + ? 'Planned' + : 'Open' + } + /> +
+ + ); +}; + +export default LinkSection; diff --git a/web-ui/src/components/reports-section/checkin-report/TeamMemberMap.css b/web-ui/src/components/reports-section/checkin-report/TeamMemberMap.css new file mode 100644 index 0000000000..4aa40f78ef --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/TeamMemberMap.css @@ -0,0 +1,28 @@ +.team-member-map-accordion { + .team-member-map-accordion-summary { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1rem; + margin-top: 0.5rem; + cursor: pointer; + + .team-member-map-summmary-content { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + margin: 0 0.75rem; + } + } +} + +.team-member-map-no-data { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 10rem; +} diff --git a/web-ui/src/components/reports-section/checkin-report/TeamMemberMap.jsx b/web-ui/src/components/reports-section/checkin-report/TeamMemberMap.jsx new file mode 100644 index 0000000000..514b120d6d --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/TeamMemberMap.jsx @@ -0,0 +1,115 @@ +import React, { useContext } from 'react'; +import { + Accordion, + AccordionSummary, + Avatar, + Chip, + Typography, + AccordionDetails +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { getAvatarURL } from '../../../api/api.js'; +import { AppContext } from '../../../context/AppContext.jsx'; +import { selectFilteredCheckinsForTeamMemberAndPDL } from '../../../context/selectors.js'; +import { + getLastCheckinDate, + statusForPeriodByMemberScheduling +} from './checkin-utils.js'; +import LinkSection from './LinkSection.jsx'; +import './TeamMemberMap.css'; + +const TeamMemberMap = ({ members, id, closed, planned, reportDate }) => { + const { state } = useContext(AppContext); + + const filteredMembers = members?.filter(member => { + const checkins = selectFilteredCheckinsForTeamMemberAndPDL( + state, + member.id, + id, + closed, + planned + ); + return checkins && checkins.length > 0; + }); + + return ( + <> + {filteredMembers?.length > 0 ? ( + filteredMembers.map(member => { + const checkins = selectFilteredCheckinsForTeamMemberAndPDL( + state, + member.id, + id, + closed, + planned + ); + + return ( + + } + className="team-member-map-accordion-summary" + > + +
+ {member.name} + + Activity:{' '} + {getLastCheckinDate(checkins).toLocaleDateString( + navigator.language, + { + year: 'numeric', + month: '2-digit', + day: 'numeric' + } + )} + + +
+
+ + {checkins.map(checkin => ( + + ))} + +
+ ); + }) + ) : ( +
+ No team members associated with this PDL. +
+ )} + + ); +}; + +export default TeamMemberMap; diff --git a/web-ui/src/components/reports-section/checkin-report/checkin-utils.js b/web-ui/src/components/reports-section/checkin-report/checkin-utils.js new file mode 100644 index 0000000000..1b348b8024 --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/checkin-utils.js @@ -0,0 +1,51 @@ +import { getQuarterBeginEnd } from '../../../helpers'; + +/** + * @typedef {("Not Scheduled" | "Scheduled" | "Completed")} SchedulingStatus + */ + +/** + * Get the date of a check-in. + * @param {Checkin} checkin - A check-in. + * @returns {Date} The date of the check-in. + */ +export const getCheckinDate = checkin => { + if (!checkin || !checkin.checkInDate) return; + const [year, month, day, hour, minute] = checkin.checkInDate; + return new Date(year, month - 1, day, hour, minute, 0); +}; + +/** + * Get the date of the last check-in. + * @param {Checkin[]} checkins - Check-ins for a member. + * @returns {Date} The date of the last check-in or epoch if no check-ins. + */ +export const getLastCheckinDate = checkins => { + if (checkins.length === 0) return new Date(0); + return checkins.reduce((acc, checkin) => { + const checkinDate = getCheckinDate(checkin); + return checkinDate > acc ? checkinDate : acc; + }, new Date(0)); +}; + +/** + * Determine check-in status for a member during the reporting period. + * @param {Checkin[]} checkins - Check-ins for a member. + * @param {Date} reportDate - The date of the report. + * @returns {SchedulingStatus} The status of the check-ins. + */ +export const statusForPeriodByMemberScheduling = ( + checkins = [], + reportDate +) => { + if (checkins.length === 0) return 'Not Scheduled'; + const { startOfQuarter, endOfQuarter } = getQuarterBeginEnd(reportDate); + const scheduled = checkins.filter(checkin => { + const checkinDate = getCheckinDate(checkin); + return checkinDate >= startOfQuarter && checkinDate <= endOfQuarter; + }); + if (scheduled.length === 0) return 'Not Scheduled'; + const completed = scheduled.filter(checkin => checkin.completed); + if (completed.length === scheduled.length) return 'Completed'; + return 'Scheduled'; +}; diff --git a/web-ui/src/components/reports-section/checkin-report/checkin-utils.test.js b/web-ui/src/components/reports-section/checkin-report/checkin-utils.test.js new file mode 100644 index 0000000000..156c842aa1 --- /dev/null +++ b/web-ui/src/components/reports-section/checkin-report/checkin-utils.test.js @@ -0,0 +1,103 @@ +import { + getCheckinDate, + getLastCheckinDate, + statusForPeriodByMemberScheduling +} from './checkin-utils'; + +describe('getCheckinDate', () => { + test('returns correct date when check-in is provided', () => { + const checkin = { + checkInDate: [2024, 4, 9, 10, 30] // [year, month, day, hour, minute] + }; + + const result = getCheckinDate(checkin); + + expect(result).toBeInstanceOf(Date); + expect(result.getFullYear()).toBe(2024); + expect(result.getMonth()).toBe(3); // Months are zero-indexed (April is 3) + expect(result.getDate()).toBe(9); + expect(result.getHours()).toBe(10); + expect(result.getMinutes()).toBe(30); + }); + + test('returns undefined when check-in is not provided', () => { + const result = getCheckinDate(undefined); + expect(result).toBeUndefined(); + }); + + test('returns undefined when check-in date is not available', () => { + const checkin = {}; + const result = getCheckinDate(checkin); + expect(result).toBeUndefined(); + }); +}); + +describe('getLastCheckinDate', () => { + test('returns correct last check-in date from multiple check-ins', () => { + const checkins = [ + { checkInDate: [2024, 4, 9, 10, 30] }, + { checkInDate: [2024, 4, 10, 11, 0] }, + { checkInDate: [2024, 4, 8, 9, 45] } + ]; + + const result = getLastCheckinDate(checkins); + + expect(result).toBeInstanceOf(Date); + expect(result.getFullYear()).toBe(2024); + expect(result.getMonth()).toBe(3); // April is 3 (zero-indexed) + expect(result.getDate()).toBe(10); // Latest date in the array + expect(result.getHours()).toBe(11); + expect(result.getMinutes()).toBe(0); + }); + + test('returns default date (epoch) when no check-ins provided', () => { + const checkins = []; + const result = getLastCheckinDate(checkins); + expect(result).toEqual(new Date(0)); // Date at epoch (Jan 1, 1970) + }); + + test('returns default date (epoch) when check-ins array is empty', () => { + const checkins = []; + const result = getLastCheckinDate(checkins); + expect(result).toEqual(new Date(0)); // Date at epoch (Jan 1, 1970) + }); +}); + +describe('statusForPeriodByMemberScheduling', () => { + const mockReportDate = new Date(2024, 3, 15); // April 15, 2024 (example report date) + + test('returns "Not Scheduled" when no check-ins are provided', () => { + const result = statusForPeriodByMemberScheduling([], mockReportDate); + expect(result).toBe('Not Scheduled'); + }); + + test('returns "Not Scheduled" when all check-ins are outside the reporting period', () => { + const checkins = [ + { checkInDate: [2024, 2, 1, 10, 30], completed: false }, // Feb 1, 2024 + { checkInDate: [2024, 3, 31, 9, 0], completed: false }, // March 31, 2024 + { checkInDate: [2024, 7, 2, 11, 15], completed: false } // July 2, 2024 + ]; + const result = statusForPeriodByMemberScheduling(checkins, mockReportDate); + expect(result).toBe('Not Scheduled'); + }); + + test('returns "Scheduled" when some check-ins within the reporting period are completed', () => { + const checkins = [ + { checkInDate: [2024, 3, 1, 10, 0], completed: true }, // March 1, 2024 (within reporting period, completed) + { checkInDate: [2024, 4, 1, 9, 0], completed: false }, // April 1, 2024 (within reporting period, not completed) + { checkInDate: [2024, 4, 15, 14, 30], completed: false } // April 15, 2024 (within reporting period, not completed) + ]; + const result = statusForPeriodByMemberScheduling(checkins, mockReportDate); + expect(result).toBe('Scheduled'); + }); + + test('returns "Scheduled" when all check-ins within the reporting period are not completed', () => { + const checkins = [ + { checkInDate: [2024, 3, 1, 10, 0], completed: false }, // March 1, 2024 (within reporting period, not completed) + { checkInDate: [2024, 4, 1, 9, 0], completed: false }, // April 1, 2024 (within reporting period, not completed) + { checkInDate: [2024, 4, 15, 14, 30], completed: false } // April 15, 2024 (within reporting period, not completed) + ]; + const result = statusForPeriodByMemberScheduling(checkins, mockReportDate); + expect(result).toBe('Scheduled'); + }); +}); diff --git a/web-ui/src/pages/CheckinsReportPage.jsx b/web-ui/src/pages/CheckinsReportPage.jsx index 46070fba8e..37cad3d3a4 100644 --- a/web-ui/src/pages/CheckinsReportPage.jsx +++ b/web-ui/src/pages/CheckinsReportPage.jsx @@ -18,7 +18,7 @@ import { import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import CheckinReport from '../components/reports-section/CheckinReport'; +import CheckinReport from '../components/reports-section/checkin-report/CheckinReport'; import MemberSelector from '../components/member_selector/MemberSelector'; import { FilterType } from '../components/member_selector/member_selector_dialog/MemberSelectorDialog'; import { @@ -28,6 +28,7 @@ import { } from '../helpers'; import './CheckinsReportPage.css'; + /** * Sort Members by the last name extracted from the full name. * @param {MemberProfile} a - First Member object @@ -41,13 +42,27 @@ function sortByLastName(a, b) { return lastNameA.localeCompare(lastNameB); } +/** + * Sort PDLs by the number of members they have (if available), + * with PDLs having more members sorted first. + * PDLs without a 'members' property are treated as having 0 members. + * @param {PDLProfile} a - First PDL object + * @param {PDLProfile} b - Second PDL object + * @returns {number} - Comparison result for sorting + */ +function sortByMembers(a, b) { + const membersA = a.members ? a.members.length : 0; + const membersB = b.members ? b.members.length : 0; + return membersB - membersA; +} + const CheckinsReportPage = () => { const { state } = useContext(AppContext); const [selectedPdls, setSelectedPdls] = useState( /** @type {PDLProfile[]} */ ([]) ); - const [planned, setPlanned] = useState(false); - const [closed, setClosed] = useState(false); + const [planned, setPlanned] = useState(true); + const [closed, setClosed] = useState(true); const [reportDate, setReportDate] = useState(new Date()); const { startOfQuarter, endOfQuarter } = getQuarterBeginEnd(reportDate); @@ -69,7 +84,7 @@ const CheckinsReportPage = () => { }; /** @type {PDLProfile[]} */ - const pdls = selectCheckinPDLS(state, closed, planned).sort(sortByLastName); + const pdls = selectCheckinPDLS(state, closed, planned); const processedQPs = useRef(false); useQueryParameters( @@ -79,7 +94,10 @@ const CheckinsReportPage = () => { default: [], value: selectedPdls, setter(ids) { - const newPdls = ids.map(id => pdls.find(pdl => pdl.id === id)); + console.log(ids); + const newPdls = ids + .map(id => pdls.find(pdl => pdl.id === id)) + .filter(Boolean); setSelectedPdls(newPdls); }, toQP(newPdls) { @@ -121,32 +139,27 @@ const CheckinsReportPage = () => { onChange={setSelectedPdls} listHeight={180} exportable + expand={false} /> - <> - - - - + + + - <> - = new Date()} - aria-label="Next quarter`" - onClick={handleQuarterClick} - size="large" - > - - - + + + @@ -170,19 +183,26 @@ const CheckinsReportPage = () => {
{selectedPdls.length > 0 ? ( - selectedPdls.sort(sortByLastName).map(pdl => { - return ( - - ); - }) + selectedPdls + .sort(sortByLastName) + .sort(sortByMembers) + .map(pdl => { + return ( + + ); + }) ) : (
-

No data to display

+

No PDLs selected

+ + Please select some PDLs using the Member Selector. +
)}