From 9434d5608b9968b7f0d1f93263d617d3ef4fc643 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Tue, 28 Nov 2023 16:27:34 -0600 Subject: [PATCH 1/3] Add coaching activity summary --- .../ActivitySummary/ActivitySummary.graphql | 16 + .../ActivitySummary/ActivitySummary.test.tsx | 186 ++++++++++++ .../ActivitySummary/ActivitySummary.tsx | 278 ++++++++++++++++++ .../CoachingDetail/CoachingDetail.tsx | 2 + 4 files changed, 482 insertions(+) create mode 100644 src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.graphql create mode 100644 src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.test.tsx create mode 100644 src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx diff --git a/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.graphql b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.graphql new file mode 100644 index 000000000..da49c7779 --- /dev/null +++ b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.graphql @@ -0,0 +1,16 @@ +query ActivitySummary($accountListId: ID!, $range: String!) { + reportsActivityResults(accountListId: $accountListId, range: $range) { + periods { + callsWithAppointmentNext + completedCall + completedPreCallLetter + completedReminderLetter + completedSupportLetter + completedThank + dials + electronicMessageSent + electronicMessageWithAppointmentNext + startDate + } + } +} diff --git a/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.test.tsx b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.test.tsx new file mode 100644 index 000000000..f06afd6c3 --- /dev/null +++ b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.test.tsx @@ -0,0 +1,186 @@ +import { render, waitFor } from '@testing-library/react'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { CoachingPeriodEnum } from '../CoachingDetail'; +import { ActivitySummary } from './ActivitySummary'; + +const mocks = { + ActivitySummary: { + reportsActivityResults: { + periods: [ + { + startDate: '2023-09-01', + callsWithAppointmentNext: 1, + completedCall: 2, + completedPreCallLetter: 3, + completedReminderLetter: 4, + completedSupportLetter: 5, + completedThank: 6, + dials: 77, + electronicMessageSent: 8, + electronicMessageWithAppointmentNext: 9, + }, + { + startDate: '2023-10-01', + callsWithAppointmentNext: 11, + completedCall: 12, + completedPreCallLetter: 13, + completedReminderLetter: 14, + completedSupportLetter: 15, + completedThank: 16, + dials: 97, + electronicMessageSent: 18, + electronicMessageWithAppointmentNext: 19, + }, + { + startDate: '2023-11-01', + callsWithAppointmentNext: 31, + completedCall: 32, + completedPreCallLetter: 33, + completedReminderLetter: 34, + completedSupportLetter: 35, + completedThank: 36, + dials: 107, + electronicMessageSent: 38, + electronicMessageWithAppointmentNext: 39, + }, + ], + }, + }, +}; + +const mutationSpy = jest.fn(); + +describe('ActivitySummary', () => { + it('renders the table data', async () => { + const { findByRole, getAllByRole } = render( + + + , + ); + + expect( + await findByRole('cell', { name: 'Phone Dials' }), + ).toBeInTheDocument(); + + const headers = getAllByRole('rowheader'); + const phoneRow = headers[0]; + expect(phoneRow.children[0]).toHaveTextContent('Phone Dials'); + expect(phoneRow.children[1]).toHaveTextContent('Sep 1'); + expect(phoneRow.children[2]).toHaveTextContent('Oct 1'); + expect(phoneRow.children[3]).toHaveTextContent('Nov 1'); + expect(phoneRow.children[4]).toHaveTextContent('Average'); + expect(headers[1]).toHaveTextContent('Electronic Messages'); + expect(headers[2]).toHaveTextContent('Correspondence'); + + const rows = getAllByRole('row'); + + const dialsRow = rows[0]; + expect(dialsRow.children[0]).toHaveTextContent('Dials (Weekly Goal: 100)'); + expect(dialsRow.children[1]).toHaveTextContent('77'); + expect(dialsRow.children[1].firstChild).toHaveStyle( + 'background-color: #A94442', + ); + expect(dialsRow.children[2]).toHaveTextContent('97'); + expect(dialsRow.children[2].firstChild).toHaveStyle( + 'background-color: #8A6D3B', + ); + expect(dialsRow.children[3]).toHaveTextContent('107'); + expect(dialsRow.children[3].firstChild).toHaveStyle( + 'background-color: #5CB85C', + ); + expect(dialsRow.children[4]).toHaveTextContent('94'); + expect(dialsRow.children[4].firstChild).toHaveStyle( + 'background-color: #8A6D3B', + ); + + const completedRow = rows[1]; + expect(completedRow.children[0]).toHaveTextContent('Completed'); + expect(completedRow.children[1]).toHaveTextContent('2'); + expect(completedRow.children[2]).toHaveTextContent('12'); + expect(completedRow.children[3]).toHaveTextContent('32'); + expect(completedRow.children[4]).toHaveTextContent('15'); + + const phoneAppointmentsRow = rows[2]; + expect(phoneAppointmentsRow.children[0]).toHaveTextContent( + 'Resulting Appointments', + ); + expect(phoneAppointmentsRow.children[1]).toHaveTextContent('1'); + expect(phoneAppointmentsRow.children[2]).toHaveTextContent('11'); + expect(phoneAppointmentsRow.children[3]).toHaveTextContent('31'); + expect(phoneAppointmentsRow.children[4]).toHaveTextContent('14'); + + const electronicRow = rows[4]; + expect(electronicRow.children[0]).toHaveTextContent('Sent'); + expect(electronicRow.children[1]).toHaveTextContent('8'); + expect(electronicRow.children[2]).toHaveTextContent('18'); + expect(electronicRow.children[3]).toHaveTextContent('38'); + expect(electronicRow.children[4]).toHaveTextContent('21'); + + const electronicAppointmentsRow = rows[5]; + expect(electronicAppointmentsRow.children[0]).toHaveTextContent( + 'Resulting Appointments', + ); + expect(electronicAppointmentsRow.children[1]).toHaveTextContent('9'); + expect(electronicAppointmentsRow.children[2]).toHaveTextContent('19'); + expect(electronicAppointmentsRow.children[3]).toHaveTextContent('39'); + expect(electronicAppointmentsRow.children[4]).toHaveTextContent('22'); + + const preCallRow = rows[7]; + expect(preCallRow.children[0]).toHaveTextContent('Pre-Call Letters'); + expect(preCallRow.children[1]).toHaveTextContent('3'); + expect(preCallRow.children[2]).toHaveTextContent('13'); + expect(preCallRow.children[3]).toHaveTextContent('33'); + expect(preCallRow.children[4]).toHaveTextContent('16'); + + const supportRow = rows[8]; + expect(supportRow.children[0]).toHaveTextContent('Support Letters'); + expect(supportRow.children[1]).toHaveTextContent('5'); + expect(supportRow.children[2]).toHaveTextContent('15'); + expect(supportRow.children[3]).toHaveTextContent('35'); + expect(supportRow.children[4]).toHaveTextContent('18'); + + const thankYouRow = rows[9]; + expect(thankYouRow.children[0]).toHaveTextContent('Thank Yous'); + expect(thankYouRow.children[1]).toHaveTextContent('6'); + expect(thankYouRow.children[2]).toHaveTextContent('16'); + expect(thankYouRow.children[3]).toHaveTextContent('36'); + expect(thankYouRow.children[4]).toHaveTextContent('19'); + }); + + it('loads data for the weekly period', async () => { + render( + + + , + ); + + await waitFor(() => + expect(mutationSpy.mock.calls[0][0].operation.variables).toMatchObject({ + range: '4w', + }), + ); + }); + + it('loads data for the monthly period', async () => { + render( + + + , + ); + + await waitFor(() => + expect(mutationSpy.mock.calls[0][0].operation.variables).toMatchObject({ + range: '4m', + }), + ); + }); +}); diff --git a/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx new file mode 100644 index 000000000..2066d5a4b --- /dev/null +++ b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx @@ -0,0 +1,278 @@ +import { useMemo } from 'react'; +import { + Box, + CardContent, + CardHeader, + Divider, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { DateTime } from 'luxon'; +import { useTranslation } from 'react-i18next'; +import { useLocale } from 'src/hooks/useLocale'; +import { dateFormatWithoutYear } from 'src/lib/intlFormat'; +import AnimatedCard from 'src/components/AnimatedCard'; +import { MultilineSkeleton } from '../../../Shared/MultilineSkeleton'; +import { CoachingPeriodEnum } from '../CoachingDetail'; +import { getResultColor } from '../helpers'; +import { HelpButton } from '../HelpButton'; +import { useActivitySummaryQuery } from './ActivitySummary.generated'; + +const DialsLabel = styled('span')(({ theme }) => ({ + padding: `${theme.spacing(0.375)} ${theme.spacing(0.75)}`, + borderRadius: theme.spacing(0.5), + color: theme.palette.primary.contrastText, + fontSize: '80%', + fontWeight: 'bold', +})); + +interface DialCountProps { + dials: number; + goal: number; +} + +const DialCount: React.FC = ({ dials, goal }) => ( + + {dials} + +); + +const ContentContainer = styled(CardContent)(({ theme }) => ({ + padding: theme.spacing(2), + overflowX: 'scroll', +})); + +const DividerRow = styled(TableRow)(({ theme }) => ({ + td: { + border: 'none', + padding: `${theme.spacing(2)} 0`, + }, +})); + +const HeaderRow = styled(TableRow)({ + td: { + fontWeight: 'bold', + }, +}); + +const AlignedTableCell = styled(TableCell)({ + border: 'none', + textAlign: 'right', + ':first-of-type': { + textAlign: 'unset', + }, +}); + +interface ActivitySummaryProps { + accountListId: string; + period: CoachingPeriodEnum; +} + +export const ActivitySummary: React.FC = ({ + accountListId, + period, +}) => { + const { t } = useTranslation(); + const locale = useLocale(); + + const { data, loading } = useActivitySummaryQuery({ + variables: { + accountListId, + range: period === CoachingPeriodEnum.Weekly ? '4w' : '4m', + }, + }); + const periods = data?.reportsActivityResults.periods ?? []; + + const averages = useMemo( + () => + [ + 'callsWithAppointmentNext', + 'completedCall', + 'completedPreCallLetter', + 'completedReminderLetter', + 'completedSupportLetter', + 'completedThank', + 'dials', + 'electronicMessageSent', + 'electronicMessageWithAppointmentNext', + ].reduce>( + (averages, field) => ({ + ...averages, + [field]: periods.reduce( + (total, period) => total + period[field] / periods.length, + 0, + ), + }), + {}, + ), + [data], + ); + + const dialGoal = period === CoachingPeriodEnum.Weekly ? 100 : 400; + + return ( + + + {t('Activity Summary')} + + + } + /> + + {loading ? ( + + ) : ( + + + + + {t('Phone Dials')} + {periods.map(({ startDate }) => ( + + {dateFormatWithoutYear( + DateTime.fromISO(startDate), + locale, + )} + + ))} + {t('Average')} + + + + {t('Dials ({{goalText}}: {{goal}})', { + goalText: + period === CoachingPeriodEnum.Weekly + ? t('Weekly Goal') + : t('Monthly Goal'), + goal: dialGoal, + })} + + {periods.map(({ startDate, dials }) => ( + + + + ))} + + + + + + {t('Completed')} + {periods.map(({ startDate, completedCall }) => ( + + {completedCall} + + ))} + + {Math.round(averages.completedCall)} + + + + + {t('Resulting Appointments')} + + {periods.map(({ startDate, callsWithAppointmentNext }) => ( + + {callsWithAppointmentNext} + + ))} + + {Math.round(averages.callsWithAppointmentNext)} + + + + + + + + + + {t('Electronic Messages')} + + + + {t('Sent')} + {periods.map(({ startDate, electronicMessageSent }) => ( + + {electronicMessageSent} + + ))} + + {Math.round(averages.electronicMessageSent)} + + + + + {t('Resulting Appointments')} + + {periods.map( + ({ startDate, electronicMessageWithAppointmentNext }) => ( + + {electronicMessageWithAppointmentNext} + + ), + )} + + {Math.round(averages.electronicMessageWithAppointmentNext)} + + + + + + + + + + {t('Correspondence')} + + + + {t('Pre-Call Letters')} + {periods.map(({ startDate, completedPreCallLetter }) => ( + + {completedPreCallLetter} + + ))} + + {Math.round(averages.completedPreCallLetter)} + + + + {t('Support Letters')} + {periods.map(({ startDate, completedSupportLetter }) => ( + + {completedSupportLetter} + + ))} + + {Math.round(averages.completedSupportLetter)} + + + + {t('Thank Yous')} + {periods.map(({ startDate, completedThank }) => ( + + {completedThank} + + ))} + + {Math.round(averages.completedThank)} + + + +
+
+ )} +
+
+ ); +}; diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx index a78c39311..2ec1abb62 100644 --- a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx +++ b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx @@ -25,6 +25,7 @@ import { SideContainerText } from './StyledComponents'; import { CollapsibleEmailList } from './CollapsibleEmailList'; import { CollapsiblePhoneList } from './CollapsiblePhoneList'; import { getLastNewsletter } from './helpers'; +import { ActivitySummary } from './ActivitySummary/ActivitySummary'; export enum CoachingPeriodEnum { Weekly = 'Weekly', @@ -335,6 +336,7 @@ export const CoachingDetail: React.FC = ({ currency={accountListData?.currency} period={period} /> + )} From 8aca93611b84de43ba02cf569b8619613f502607 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 29 Nov 2023 08:35:27 -0600 Subject: [PATCH 2/3] Use shared styled components --- .../ActivitySummary/ActivitySummary.tsx | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx index 2066d5a4b..499df8178 100644 --- a/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx +++ b/src/components/Coaching/CoachingDetail/ActivitySummary/ActivitySummary.tsx @@ -19,6 +19,7 @@ import AnimatedCard from 'src/components/AnimatedCard'; import { MultilineSkeleton } from '../../../Shared/MultilineSkeleton'; import { CoachingPeriodEnum } from '../CoachingDetail'; import { getResultColor } from '../helpers'; +import { HeaderRow, AlignedTableCell, DividerRow } from '../StyledComponents'; import { HelpButton } from '../HelpButton'; import { useActivitySummaryQuery } from './ActivitySummary.generated'; @@ -46,27 +47,6 @@ const ContentContainer = styled(CardContent)(({ theme }) => ({ overflowX: 'scroll', })); -const DividerRow = styled(TableRow)(({ theme }) => ({ - td: { - border: 'none', - padding: `${theme.spacing(2)} 0`, - }, -})); - -const HeaderRow = styled(TableRow)({ - td: { - fontWeight: 'bold', - }, -}); - -const AlignedTableCell = styled(TableCell)({ - border: 'none', - textAlign: 'right', - ':first-of-type': { - textAlign: 'unset', - }, -}); - interface ActivitySummaryProps { accountListId: string; period: CoachingPeriodEnum; From 59406e8f3140bad46c9bbd566f93c579a631644e Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 29 Nov 2023 15:16:27 -0600 Subject: [PATCH 3/3] Push content above HelpScout beacon --- src/components/Coaching/CoachingDetail/CoachingDetail.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx index 2ec1abb62..404816cd3 100644 --- a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx +++ b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx @@ -65,6 +65,7 @@ const CoachingSideTitleContainer = styled(Box)(({ theme }) => ({ const CoachingMainContainer = styled(Box)(({ theme }) => ({ padding: theme.spacing(1), + paddingBottom: theme.spacing(6), // prevent the HelpScout beacon from obscuring content at the bottom width: 'calc(100vw - 20rem)', }));