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.
+
)}