From ba49f0908518aa817be84d2ac7e5cfb7fa7df660 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Tue, 28 Nov 2023 15:58:36 -0600 Subject: [PATCH 1/7] Add coaching activity component --- .../accountListAnalytics.graphql | 14 +- .../AccountListAnalytics/dataHandler.ts | 48 +- .../CoachingDetail/Activity/Activity.graphql | 43 ++ .../CoachingDetail/Activity/Activity.test.tsx | 289 ++++++++ .../CoachingDetail/Activity/Activity.tsx | 698 ++++++++++++++++++ .../Activity/AppealProgress.test.tsx | 55 ++ .../Activity/AppealProgress.tsx | 121 +++ .../CoachingDetail/CoachingDetail.tsx | 10 +- .../CoachingDetail/LoadCoachingDetail.graphql | 12 +- 9 files changed, 1251 insertions(+), 39 deletions(-) create mode 100644 src/components/Coaching/CoachingDetail/Activity/Activity.graphql create mode 100644 src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx create mode 100644 src/components/Coaching/CoachingDetail/Activity/Activity.tsx create mode 100644 src/components/Coaching/CoachingDetail/Activity/AppealProgress.test.tsx create mode 100644 src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx diff --git a/pages/api/Schema/AccountListAnalytics/accountListAnalytics.graphql b/pages/api/Schema/AccountListAnalytics/accountListAnalytics.graphql index efa92e048..3095b5ae1 100644 --- a/pages/api/Schema/AccountListAnalytics/accountListAnalytics.graphql +++ b/pages/api/Schema/AccountListAnalytics/accountListAnalytics.graphql @@ -4,10 +4,8 @@ extend type Query { dateRange: String ): AccountListAnalytics! } + type AccountListAnalytics { - id: ID! - type: String! - createdAt: ISO8601DateTime! appointments: AppointmentsAccountListAnalytics! contacts: ContactsAccountListAnalytics! correspondence: CorrespondenceAccountListAnalytics! @@ -18,8 +16,6 @@ type AccountListAnalytics { textMessage: TextMessageAccountListAnalytics! startDate: ISO8601DateTime! endDate: ISO8601DateTime! - updatedAt: ISO8601DateTime! - updatedInDbAt: ISO8601DateTime! } type AppointmentsAccountListAnalytics { @@ -29,14 +25,14 @@ type AppointmentsAccountListAnalytics { type ContactsAccountListAnalytics { active: Int! referrals: Int! - referrals_on_hand: Int! + referralsOnHand: Int! } type CorrespondenceAccountListAnalytics { precall: Int! reminders: Int! - support_letters: Int! - thank_yous: Int! + supportLetters: Int! + thankYous: Int! newsletters: Int! } @@ -61,7 +57,7 @@ type PhoneAccountListAnalytics { attempted: Int! completed: Int! received: Int! - talktoinperson: Int! + talkToInPerson: Int! } type TextMessageAccountListAnalytics { diff --git a/pages/api/Schema/AccountListAnalytics/dataHandler.ts b/pages/api/Schema/AccountListAnalytics/dataHandler.ts index 94727b978..f80721d90 100644 --- a/pages/api/Schema/AccountListAnalytics/dataHandler.ts +++ b/pages/api/Schema/AccountListAnalytics/dataHandler.ts @@ -51,10 +51,7 @@ const getAccountListAnalytics = (data: { }; }): AccountListAnalytics => { const { - id, - type, attributes: { - created_at, appointments, contacts, correspondence, @@ -63,29 +60,38 @@ const getAccountListAnalytics = (data: { facebook, phone, text_message, - start_date, - end_date, - updated_at, - updated_in_db_at, + start_date: startDate, + end_date: endDate, }, } = data; return { - id: id, - type: type, - createdAt: created_at, - appointments: appointments, - contacts: contacts, - correspondence: correspondence, - electronic: electronic, - email: email, - facebook: facebook, - phone: phone, + appointments, + contacts: { + active: contacts.active, + referrals: contacts.referrals, + referralsOnHand: contacts.referrals_on_hand, + }, + correspondence: { + precall: correspondence.precall, + reminders: correspondence.reminders, + supportLetters: correspondence.support_letters, + thankYous: correspondence.thank_yous, + newsletters: correspondence.newsletters, + }, + electronic, + email, + facebook, + phone: { + appointments: phone.appointments, + attempted: phone.attempted, + completed: phone.completed, + received: phone.received, + talkToInPerson: phone.talktoinperson, + }, textMessage: text_message, - startDate: start_date, - endDate: end_date, - updatedAt: updated_at, - updatedInDbAt: updated_in_db_at, + startDate, + endDate, }; }; export { getAccountListAnalytics }; diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.graphql b/src/components/Coaching/CoachingDetail/Activity/Activity.graphql new file mode 100644 index 000000000..a346310d8 --- /dev/null +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.graphql @@ -0,0 +1,43 @@ +query CoachingDetailActivity($accountListId: ID!, $dateRange: String!) { + accountListAnalytics(accountListId: $accountListId, dateRange: $dateRange) { + appointments { + completed + } + contacts { + active + referrals + referralsOnHand + } + correspondence { + precall + reminders + supportLetters + thankYous + newsletters + } + electronic { + appointments + received + sent + } + email { + received + sent + } + facebook { + received + sent + } + phone { + attempted + appointments + completed + received + talkToInPerson + } + textMessage { + received + sent + } + } +} diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx new file mode 100644 index 000000000..6247f46a7 --- /dev/null +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx @@ -0,0 +1,289 @@ +import { ThemeProvider } from '@emotion/react'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Settings } from 'luxon'; +import theme from 'src/theme'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import TestRouter from '__tests__/util/TestRouter'; +import { AccountListTypeEnum, CoachingPeriodEnum } from '../CoachingDetail'; +import { Activity } from './Activity'; + +const mocks = { + CoachingDetailActivity: { + accountListAnalytics: { + appointments: { + completed: 10, + }, + contacts: { + active: 20, + referrals: 21, + referralsOnHand: 22, + }, + correspondence: { + precall: 30, + reminders: 31, + supportLetters: 32, + thankYous: 33, + newsletters: 34, + }, + electronic: { + appointments: 40, + received: 41, + sent: 42, + }, + email: { + received: 50, + sent: 51, + }, + facebook: { + received: 60, + sent: 61, + }, + phone: { + attempted: 70, + appointments: 71, + completed: 72, + received: 73, + talkToInPerson: 74, + }, + textMessage: { + received: 80, + sent: 81, + }, + }, + }, +}; +const mutationSpy = jest.fn(); + +const accountListId = 'account-list-1'; +const router = { + query: { + accountListId, + }, + isReady: true, +}; + +interface TestComponentProps { + accountListType?: AccountListTypeEnum; + period?: CoachingPeriodEnum; + noAppeal?: boolean; +} + +const TestComponent: React.FC = ({ + accountListType = AccountListTypeEnum.Coaching, + period = CoachingPeriodEnum.Weekly, + noAppeal = false, +}) => ( + + + + + + + +); + +describe('Activity', () => { + beforeEach(() => { + Settings.now = () => new Date(2020, 0, 8).valueOf(); + }); + + describe('period', () => { + it('is a week long when in weekly mode', async () => { + const { getByTestId } = render(); + + expect(getByTestId('ActivityPeriod')).toHaveTextContent('Jan 6 - Jan 12'); + await waitFor(() => + expect(mutationSpy.mock.calls[0][0].operation).toMatchObject({ + operationName: 'CoachingDetailActivity', + variables: { + dateRange: '2020-01-06..2020-01-12', + }, + }), + ); + }); + + it('is a month long when in monthly mode', async () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('ActivityPeriod')).toHaveTextContent('Jan 1 - Jan 31'); + await waitFor(() => + expect(mutationSpy.mock.calls[0][0].operation).toMatchObject({ + operationName: 'CoachingDetailActivity', + variables: { + dateRange: '2020-01-01..2020-01-31', + }, + }), + ); + }); + + it('shows the year when in a different year', () => { + const { getByRole, getByTestId } = render(); + + userEvent.click(getByRole('button', { name: 'Previous' })); + expect(getByTestId('ActivityPeriod')).toHaveTextContent( + 'Dec 30, 2019 - Jan 5, 2020', + ); + }); + + it('resets when the period changes', () => { + const { getByRole, getByTestId, rerender } = render( + , + ); + + userEvent.click(getByRole('button', { name: 'Previous' })); + rerender(); + expect(getByTestId('ActivityPeriod')).toHaveTextContent('Jan 6 - Jan 12'); + }); + }); + + describe('previous/next buttons', () => { + it('loads a new weekly period', async () => { + const { getByRole } = render(); + + const next = getByRole('button', { name: 'Next' }); + const previous = getByRole('button', { name: 'Previous' }); + + userEvent.click(previous); + await waitFor(() => + expect(mutationSpy.mock.calls[1][0].operation).toMatchObject({ + operationName: 'CoachingDetailActivity', + variables: { + dateRange: '2019-12-30..2020-01-05', + }, + }), + ); + + userEvent.click(next); + await waitFor(() => + expect(mutationSpy.mock.calls[2][0].operation).toMatchObject({ + operationName: 'CoachingDetailActivity', + variables: { + dateRange: '2020-01-06..2020-01-12', + }, + }), + ); + }); + + it('loads a new monthly period', async () => { + const { getByRole } = render( + , + ); + + const next = getByRole('button', { name: 'Next' }); + const previous = getByRole('button', { name: 'Previous' }); + + userEvent.click(previous); + await waitFor(() => + expect(mutationSpy.mock.calls[1][0].operation).toMatchObject({ + operationName: 'CoachingDetailActivity', + variables: { + dateRange: '2019-12-01..2019-12-31', + }, + }), + ); + + userEvent.click(next); + await waitFor(() => + expect(mutationSpy.mock.calls[2][0].operation).toMatchObject({ + operationName: 'CoachingDetailActivity', + variables: { + dateRange: '2020-01-01..2020-01-31', + }, + }), + ); + }); + + it('next button is disabled when on the last possible range', () => { + const { getByRole, getByTestId } = render(); + + const next = getByRole('button', { name: 'Next' }); + const previous = getByRole('button', { name: 'Previous' }); + const period = getByTestId('ActivityPeriod'); + + expect(next).toBeDisabled(); + + userEvent.click(previous); + expect(next).not.toBeDisabled(); + expect(period).toHaveTextContent('Dec 30, 2019 - Jan 5, 2020'); + + userEvent.click(next); + expect(next).toBeDisabled(); + expect(period).toHaveTextContent('Jan 6 - Jan 12'); + }); + }); + + it('renders the activity sections', async () => { + const { findByTestId, getByTestId } = render(); + + expect(await findByTestId('ActivitySectionContacts')).toHaveTextContent( + 'Contacts20Active22Referrals On-hand21Referrals Gained', + ); + expect(getByTestId('ActivitySectionAppointments')).toHaveTextContent( + 'Appointments10Completed', + ); + expect(getByTestId('ActivitySectionCorrespondence')).toHaveTextContent( + 'Correspondence30Pre-call32Support33Thank You31Reminder', + ); + expect(getByTestId('ActivitySectionPhone')).toHaveTextContent( + 'Phone Calls142Outgoing145Talked To71Appts Produced72Completed70Attempted73Received', + ); + expect(getByTestId('ActivitySectionElectronic')).toHaveTextContent( + 'Electronic Messages42Sent41Received40Appts ProducedEmail51 Sent / 50 ReceivedFacebook61 Sent / 60 ReceivedText Message81 Sent / 80 Received', + ); + }); + + describe('appeal', () => { + it('renders when provided', async () => { + const { findByTestId } = render(); + + expect(await findByTestId('ActivitySectionAppeal')).toHaveTextContent( + 'Primary Appeal$200 / $1,000Ask$200 (20%) / $500 (50%) / $600 (60%)', + ); + }); + + it('renders a placeholder when missing', async () => { + const { findByTestId } = render(); + + expect(await findByTestId('ActivitySectionAppeal')).toHaveTextContent( + 'Primary AppealNo Primary Appeal Set$0 (0%) / $0 (0%) / $0 (0%)', + ); + }); + }); + + describe('links', () => { + it('are hidden when viewing coaching account list', async () => { + const { findByTestId, queryByRole } = render(); + + expect(await findByTestId('ActivitySectionContacts')).toBeInTheDocument(); + expect(queryByRole('link')).not.toBeInTheDocument(); + }); + + it('are shown when viewing own account list', async () => { + const { findAllByRole } = render( + , + ); + + expect((await findAllByRole('link')).length).toBeGreaterThan(0); + }); + }); +}); diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx new file mode 100644 index 000000000..e5ef019ca --- /dev/null +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx @@ -0,0 +1,698 @@ +import { useEffect, useMemo, useState } from 'react'; +import NextLink from 'next/link'; +import { + Box, + Button, + ButtonGroup, + CardHeader, + Link as MuiLink, + Typography, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import CalendarMonthOutlined from '@mui/icons-material/CalendarMonthOutlined'; +import ChatBubbleOutline from '@mui/icons-material/ChatBubbleOutline'; +import ChevronLeft from '@mui/icons-material/ChevronLeft'; +import ChevronRight from '@mui/icons-material/ChevronRight'; +import MailOutline from '@mui/icons-material/MailOutline'; +import MoneyOutlined from '@mui/icons-material/MoneyOutlined'; +import PeopleOutline from '@mui/icons-material/PeopleOutline'; +import SmartphoneOutlined from '@mui/icons-material/SmartphoneOutlined'; +import { DateTime, DateTimeUnit } from 'luxon'; +import { useTranslation } from 'react-i18next'; +import { + ActivityTypeEnum, + Appeal, + ContactFilterSetInput, + ContactFilterStatusEnum, + ResultEnum, + TaskFilterSetInput, +} from '../../../../../graphql/types.generated'; +import AnimatedCard from 'src/components/AnimatedCard'; +import HandoffLink from 'src/components/HandoffLink'; +import { useLocale } from 'src/hooks/useLocale'; +import { + currencyFormat, + dateFormat, + dateFormatWithoutYear, +} from 'src/lib/intlFormat'; +import { MultilineSkeleton } from '../../../Shared/MultilineSkeleton'; +import { AccountListTypeEnum, CoachingPeriodEnum } from '../CoachingDetail'; +import { HelpButton } from '../HelpButton'; +import { useCoachingDetailActivityQuery } from './Activity.generated'; +import { AppealProgress } from './AppealProgress'; + +const Header = styled(Typography)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(2), + '@media (max-width: 900px)': { + flexDirection: 'column', + alignItems: 'center', + }, +})); + +const PeriodText = styled('span')({ + textAlign: 'center', + flex: 1, +}); + +const StyledButton = styled(Button)({ + width: '6rem', +}); + +const SectionsContainer = styled('div')({ + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + '@media (max-width: 1500px)': { + gridTemplateColumns: 'repeat(2, 1fr)', + }, + '@media (max-width: 1150px)': { + gridTemplateColumns: '1fr', + }, +}); + +const ActivitySection = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + paddingTop: theme.spacing(2), + // Only apply inner borders to the grid of sections + borderRight: `1px solid ${theme.palette.cruGrayMedium.main}`, + borderBottom: `1px solid ${theme.palette.cruGrayMedium.main}`, + '@media (max-width: 1150px)': { + // One column + ':nth-of-type(n + 6)': { + borderBottom: 'unset', + }, + borderRight: 'unset', + }, + '@media (min-width: 1151px) and (max-width: 1500px)': { + // Two columns + ':nth-of-type(n + 5)': { + borderBottom: 'unset', + }, + ':nth-of-type(2n)': { + borderRight: 'unset', + }, + }, + '@media (min-width: 1501px)': { + // Three columns + ':nth-of-type(n + 4)': { + borderBottom: 'unset', + }, + ':nth-of-type(3n)': { + borderRight: 'unset', + }, + }, +})); + +const SectionTitle = styled(Typography)({ + fontWeight: 'bold', + flex: 1, +}); + +const StatsRow = styled('div')(({ theme }) => ({ + display: 'flex', + width: '100%', + height: '68px', + ':nth-of-type(2)': { + backgroundColor: theme.palette.cruGrayLight.main, + }, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + textAlign: 'center', +})); + +const StatsColumn = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flex: 1, + padding: theme.spacing(0.5), + borderLeft: `1px solid ${theme.palette.cruGrayMedium.main}`, + ':first-of-type': { + borderLeft: 'none', + }, +})); + +const StatsColumnTitle = styled(Typography)({ + fontWeight: 'bold', + fontSize: '0.9em', +}); + +const StatsText = styled(Typography)({ + fontSize: '0.9em', +}); + +interface LinkProps { + accountListType: AccountListTypeEnum; + children: React.ReactNode; + href: string; +} + +const Link: React.FC = ({ accountListType, children, href }) => + // Only show links when the account list belongs to the user + accountListType === AccountListTypeEnum.Own ? ( + + {children} + + ) : ( + {children} + ); + +interface ActivityProps { + accountListId: string; + accountListType: AccountListTypeEnum; + period: CoachingPeriodEnum; + currency?: string; + primaryAppeal?: Pick< + Appeal, + | 'id' + | 'name' + | 'amount' + | 'pledgesAmountNotReceivedNotProcessed' + | 'pledgesAmountProcessed' + | 'pledgesAmountReceivedNotProcessed' + >; +} + +export const Activity: React.FC = ({ + accountListId, + accountListType, + period, + currency, + primaryAppeal, +}) => { + const { t } = useTranslation(); + const locale = useLocale(); + + const periodUnit: DateTimeUnit = + period === CoachingPeriodEnum.Weekly ? 'week' : 'month'; + const periodDuration = + period === CoachingPeriodEnum.Weekly ? { weeks: 1 } : { months: 1 }; + const [start, setStart] = useState(DateTime.now().startOf(periodUnit)); + const end = useMemo(() => start.endOf(periodUnit), [start, periodUnit]); + + useEffect(() => { + setStart(DateTime.now().startOf(periodUnit)); + }, [periodUnit]); + + const { data, loading } = useCoachingDetailActivityQuery({ + variables: { + accountListId, + dateRange: `${start.toISODate()}..${start.endOf(periodUnit).toISODate()}`, + }, + }); + + const formattedDate = useMemo(() => { + const format = + start.year === DateTime.now().year ? dateFormatWithoutYear : dateFormat; + return `${format(start, locale)} - ${format(end, locale)}`; + }, [start, end, locale]); + + const contactsLink = (filter: ContactFilterSetInput) => + `/accountLists/${accountListId}/contacts?filters=${encodeURIComponent( + JSON.stringify(filter), + )}`; + + const tasksLink = (filter: TaskFilterSetInput) => + `/accountLists/${accountListId}/tasks?filters=${encodeURIComponent( + JSON.stringify(filter), + )}`; + + const taskBaseFilter: TaskFilterSetInput = { + completedAt: { + min: start.toISODate(), + max: end.toISODate(), + }, + completed: true, + }; + const taskCallFilter: TaskFilterSetInput = { + ...taskBaseFilter, + activityType: [ActivityTypeEnum.Call], + }; + const taskElectronicFilter: TaskFilterSetInput = { + ...taskBaseFilter, + activityType: [ + ActivityTypeEnum.Email, + ActivityTypeEnum.FacebookMessage, + ActivityTypeEnum.TextMessage, + ], + }; + + return ( + + + {t('Activity')} + + {formattedDate} + + + setStart(start.minus(periodDuration))} + > + + {t('Previous')} + + setStart(start.plus(periodDuration))} + disabled={end > DateTime.now()} + > + {t('Next')} + + + + + + } + /> + {loading ? ( + + + + ) : ( + + + + {t('Contacts')} + + + + + {data?.accountListAnalytics.contacts.active} + + {t('Active')} + + + + + + {data?.accountListAnalytics.contacts.referralsOnHand} + + {t('Referrals On-hand')} + + + + + {data?.accountListAnalytics.contacts.referrals} + + {t('Referrals Gained')} + + + + + + {t('Appointments')} + + + + + {data?.accountListAnalytics.appointments.completed} + + {t('Completed')} + + + + + + + {t('Correspondence')} + + + + + {data?.accountListAnalytics.correspondence.precall} + + {t('Pre-call')} + + + + + + {data?.accountListAnalytics.correspondence.supportLetters} + + {t('Support')} + + + + + + + + {data?.accountListAnalytics.correspondence.thankYous} + + {t('Thank You')} + + + + + + {data?.accountListAnalytics.correspondence.reminders} + + {t('Reminder')} + + + + + + + {t('Phone Calls')} + + + + + {data && + data.accountListAnalytics.phone.attempted + + data.accountListAnalytics.phone.completed} + + {t('Outgoing')} + + + + + + {data && + data.accountListAnalytics.phone.completed + + data.accountListAnalytics.phone.received} + + {t('Talked To')} + + + + + + {data?.accountListAnalytics.phone.appointments} + + {t('Appts Produced')} + + + + + + + + {data?.accountListAnalytics.phone.completed} + + {t('Completed')} + + + + + + {data?.accountListAnalytics.phone.attempted} + + {t('Attempted')} + + + + + + {data?.accountListAnalytics.phone.received} + + {t('Received')} + + + + + + + {t('Electronic Messages')} + + + + + {data?.accountListAnalytics.electronic.sent} + + {t('Sent')} + + + + + + {data?.accountListAnalytics.electronic.received} + + {t('Received')} + + + + + + {data?.accountListAnalytics.electronic.appointments} + + {t('Appts Produced')} + + + + + + {t('Email')} + + + {data?.accountListAnalytics.email.sent} {t('Sent')} + + {' / '} + + {data?.accountListAnalytics.email.received} {t('Received')} + + + + + {t('Facebook')} + + + {data?.accountListAnalytics.facebook.sent} {t('Sent')} + + {' / '} + + {data?.accountListAnalytics.facebook.received}{' '} + {t('Received')} + + + + + {t('Text Message')} + + + {data?.accountListAnalytics.textMessage.sent} {t('Sent')} + + {' / '} + + {data?.accountListAnalytics.textMessage.received}{' '} + {t('Received')} + + + + + + + + + {accountListType === AccountListTypeEnum.Own ? ( + + {t('Primary Appeal')} + + ) : ( + t('Primary Appeal') + )} + + + {primaryAppeal ? ( + + + {currencyFormat( + primaryAppeal.pledgesAmountProcessed, + currency, + locale, + )} + {' / '} + {currencyFormat( + primaryAppeal.amount ?? 0, + currency, + locale, + )} + + + {accountListType === AccountListTypeEnum.Own ? ( + + {primaryAppeal.name} + + ) : ( + primaryAppeal.name + )} + + + ) : ( + {t('No Primary Appeal Set')} + )} + + + + + + + )} + + ); +}; diff --git a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.test.tsx b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.test.tsx new file mode 100644 index 000000000..d46266751 --- /dev/null +++ b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.test.tsx @@ -0,0 +1,55 @@ +import { ThemeProvider } from '@emotion/react'; +import { render } from '@testing-library/react'; +import theme from 'src/theme'; +import { AppealProgress } from './AppealProgress'; + +const appeal = { + amount: 200, + pledgesAmountNotReceivedNotProcessed: 12.34, + pledgesAmountProcessed: 23.45, + pledgesAmountReceivedNotProcessed: 34.56, +}; + +describe('AppealProgress', () => { + it('renders the amounts, percentages, and progress bar', () => { + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('AppealProgressAmounts')).toHaveTextContent( + '$23 (12%) / $58 (29%) / $70 (35%)', + ); + expect(getByTestId('AppealProgressSegmentProcessed')).toHaveStyle( + 'width: 11.72%', + ); + expect(getByTestId('AppealProgressSegmentReceived')).toHaveStyle( + 'width: 29.01%', + ); + expect(getByTestId('AppealProgressSegmentCommitted')).toHaveStyle( + 'width: 35.18%', + ); + }); + + it('renders zeros without an appeal', () => { + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('AppealProgressAmounts')).toHaveTextContent( + '$0 (0%) / $0 (0%) / $0 (0%)', + ); + expect(getByTestId('AppealProgressSegmentProcessed')).toHaveStyle( + 'width: 0.00%', + ); + expect(getByTestId('AppealProgressSegmentReceived')).toHaveStyle( + 'width: 0.00%', + ); + expect(getByTestId('AppealProgressSegmentCommitted')).toHaveStyle( + 'width: 0.00%', + ); + }); +}); diff --git a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx new file mode 100644 index 000000000..61af3e4d2 --- /dev/null +++ b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx @@ -0,0 +1,121 @@ +import { Tooltip } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { Appeal } from '../../../../../graphql/types.generated'; +import { useLocale } from 'src/hooks/useLocale'; +import { currencyFormat, percentageFormat } from 'src/lib/intlFormat'; +import { useTranslation } from 'react-i18next'; + +const Amounts = styled('div')(({ theme }) => ({ + fontSize: '0.9em', + paddingBottom: theme.spacing(0.5), + textAlign: 'right', +})); + +const ProcessedText = styled('span')(({ theme }) => ({ + color: theme.palette.progressBarYellow.main, +})); + +const ReceivedText = styled('span')(({ theme }) => ({ + color: theme.palette.progressBarOrange.main, +})); + +const CommittedText = styled('span')(({ theme }) => ({ + color: theme.palette.progressBarGray.main, +})); + +const ProgressWrapper = styled('div')(({ theme }) => ({ + padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, +})); + +const ProgressBar = styled('div')(({ theme }) => ({ + width: '100%', + height: '24px', + overflowX: 'hidden', + textAlign: 'left', + whiteSpace: 'nowrap', + borderRadius: '10px', + backgroundColor: theme.palette.cruGrayDark.main, +})); + +const ProgressSegment = styled('div')({ + display: 'inline-block', + height: '100%', +}); + +interface AppealProgressProps { + appeal?: Pick< + Appeal, + | 'amount' + | 'pledgesAmountNotReceivedNotProcessed' + | 'pledgesAmountProcessed' + | 'pledgesAmountReceivedNotProcessed' + >; + currency?: string; +} + +export const AppealProgress: React.FC = ({ + appeal, + currency, +}) => { + const { t } = useTranslation(); + const locale = useLocale(); + + const processed = appeal?.pledgesAmountProcessed ?? 0; + const received = processed + (appeal?.pledgesAmountReceivedNotProcessed ?? 0); + const committed = + received + (appeal?.pledgesAmountNotReceivedNotProcessed ?? 0); + const max = appeal?.amount ?? 1; + const processedFraction = processed / max; + const receivedFraction = received / max; + const committedFraction = committed / max; + + return ( + + + + + {currencyFormat(processed, currency, locale)} ( + {percentageFormat(processedFraction, locale)}) + + + {' / '} + + + {currencyFormat(received, currency, locale)} ( + {percentageFormat(receivedFraction, locale)}) + + + {' / '} + + + {currencyFormat(committed, currency, locale)} ( + {percentageFormat(committedFraction, locale)}) + + + + + ({ + width: `${(processedFraction * 100).toFixed(2)}%`, + backgroundColor: theme.palette.progressBarYellow.main, + })} + /> + ({ + width: `${(receivedFraction * 100).toFixed(2)}%`, + backgroundColor: theme.palette.progressBarOrange.main, + })} + /> + ({ + width: `${(committedFraction * 100).toFixed(2)}%`, + backgroundColor: theme.palette.progressBarGray.main, + })} + /> + + + ); +}; diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx index a78c39311..870551381 100644 --- a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx +++ b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx @@ -19,8 +19,9 @@ import { dateFormat } from 'src/lib/intlFormat/intlFormat'; import { useLocale } from 'src/hooks/useLocale'; import DonationHistories from 'src/components/Dashboard/DonationHistories'; import { useGetDonationGraphQuery } from 'src/components/Reports/DonationsReport/GetDonationGraph.generated'; -import { AppointmentResults } from './AppointmentResults/AppointmentResults'; import { MultilineSkeleton } from '../../Shared/MultilineSkeleton'; +import { AppointmentResults } from './AppointmentResults/AppointmentResults'; +import { Activity } from './Activity/Activity'; import { SideContainerText } from './StyledComponents'; import { CollapsibleEmailList } from './CollapsibleEmailList'; import { CollapsiblePhoneList } from './CollapsiblePhoneList'; @@ -335,6 +336,13 @@ export const CoachingDetail: React.FC = ({ currency={accountListData?.currency} period={period} /> + )} diff --git a/src/components/Coaching/CoachingDetail/LoadCoachingDetail.graphql b/src/components/Coaching/CoachingDetail/LoadCoachingDetail.graphql index 005688f28..04488dec6 100644 --- a/src/components/Coaching/CoachingDetail/LoadCoachingDetail.graphql +++ b/src/components/Coaching/CoachingDetail/LoadCoachingDetail.graphql @@ -29,14 +29,12 @@ query LoadCoachingDetail($coachingAccountListId: ID!) { accountNumber } primaryAppeal { - active - amount - amountCurrency id name + amount pledgesAmountNotReceivedNotProcessed pledgesAmountProcessed - pledgesAmountTotal + pledgesAmountReceivedNotProcessed } currency monthlyGoal @@ -69,14 +67,12 @@ query LoadAccountListCoachingDetail($accountListId: ID!) { accountNumber } primaryAppeal { - active - amount - amountCurrency id name + amount pledgesAmountNotReceivedNotProcessed pledgesAmountProcessed - pledgesAmountTotal + pledgesAmountReceivedNotProcessed } currency monthlyGoal From 218542f065059bf06570d73db31f4d2c0865d075 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 29 Nov 2023 12:28:52 -0600 Subject: [PATCH 2/7] Put title below stats --- .../Coaching/CoachingDetail/Activity/Activity.test.tsx | 2 +- .../Coaching/CoachingDetail/Activity/Activity.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx index 6247f46a7..67f923816 100644 --- a/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx @@ -248,7 +248,7 @@ describe('Activity', () => { 'Phone Calls142Outgoing145Talked To71Appts Produced72Completed70Attempted73Received', ); expect(getByTestId('ActivitySectionElectronic')).toHaveTextContent( - 'Electronic Messages42Sent41Received40Appts ProducedEmail51 Sent / 50 ReceivedFacebook61 Sent / 60 ReceivedText Message81 Sent / 80 Received', + 'Electronic Messages42Sent41Received40Appts Produced51 Sent / 50 ReceivedEmail61 Sent / 60 ReceivedFacebook81 Sent / 80 ReceivedText Message', ); }); diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx index e5ef019ca..5055b9f02 100644 --- a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx @@ -565,7 +565,6 @@ export const Activity: React.FC = ({ - {t('Email')} = ({ {data?.accountListAnalytics.email.received} {t('Received')} + {t('Email')} - {t('Facebook')} = ({ {t('Received')} + {t('Facebook')} - {t('Text Message')} = ({ {t('Received')} + {t('Text Message')} From adacf19f987587f7a5d19cedf603888ed3969ed7 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 29 Nov 2023 12:52:22 -0600 Subject: [PATCH 3/7] Prevent layout shift --- .../CoachingDetail/Activity/Activity.test.tsx | 24 +- .../CoachingDetail/Activity/Activity.tsx | 671 +++++++++--------- src/components/Shared/MultilineSkeleton.tsx | 7 +- 3 files changed, 370 insertions(+), 332 deletions(-) diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx index 67f923816..df6d11c78 100644 --- a/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx @@ -233,10 +233,12 @@ describe('Activity', () => { }); it('renders the activity sections', async () => { - const { findByTestId, getByTestId } = render(); + const { getByTestId } = render(); - expect(await findByTestId('ActivitySectionContacts')).toHaveTextContent( - 'Contacts20Active22Referrals On-hand21Referrals Gained', + await waitFor(() => + expect(getByTestId('ActivitySectionContacts')).toHaveTextContent( + 'Contacts20Active22Referrals On-hand21Referrals Gained', + ), ); expect(getByTestId('ActivitySectionAppointments')).toHaveTextContent( 'Appointments10Completed', @@ -254,18 +256,22 @@ describe('Activity', () => { describe('appeal', () => { it('renders when provided', async () => { - const { findByTestId } = render(); + const { getByTestId } = render(); - expect(await findByTestId('ActivitySectionAppeal')).toHaveTextContent( - 'Primary Appeal$200 / $1,000Ask$200 (20%) / $500 (50%) / $600 (60%)', + await waitFor(() => + expect(getByTestId('ActivitySectionAppeal')).toHaveTextContent( + 'Primary Appeal$200 / $1,000Ask$200 (20%) / $500 (50%) / $600 (60%)', + ), ); }); it('renders a placeholder when missing', async () => { - const { findByTestId } = render(); + const { getByTestId } = render(); - expect(await findByTestId('ActivitySectionAppeal')).toHaveTextContent( - 'Primary AppealNo Primary Appeal Set$0 (0%) / $0 (0%) / $0 (0%)', + await waitFor(() => + expect(getByTestId('ActivitySectionAppeal')).toHaveTextContent( + 'Primary AppealNo Primary Appeal Set$0 (0%) / $0 (0%) / $0 (0%)', + ), ); }); }); diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx index 5055b9f02..6800b744b 100644 --- a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx @@ -1,7 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import NextLink from 'next/link'; import { - Box, Button, ButtonGroup, CardHeader, @@ -74,6 +73,7 @@ const ActivitySection = styled('div')(({ theme }) => ({ display: 'flex', flexDirection: 'column', alignItems: 'center', + height: '280px', paddingTop: theme.spacing(2), // Only apply inner borders to the grid of sections borderRight: `1px solid ${theme.palette.cruGrayMedium.main}`, @@ -267,15 +267,13 @@ export const Activity: React.FC = ({ } /> - {loading ? ( - - - - ) : ( - - - - {t('Contacts')} + + + + {t('Contacts')} + {loading ? ( + + ) : ( = ({ {t('Referrals Gained')} - - - - {t('Appointments')} + )} + + + + {t('Appointments')} + {loading ? ( + + ) : ( = ({ - - - - {t('Correspondence')} - - - - - {data?.accountListAnalytics.correspondence.precall} - - {t('Pre-call')} - - - - - - {data?.accountListAnalytics.correspondence.supportLetters} - - {t('Support')} - - - - - - - - {data?.accountListAnalytics.correspondence.thankYous} - - {t('Thank You')} - - - - - - {data?.accountListAnalytics.correspondence.reminders} - - {t('Reminder')} - - - - - - - {t('Phone Calls')} - - - - - {data && - data.accountListAnalytics.phone.attempted + - data.accountListAnalytics.phone.completed} - - {t('Outgoing')} - - - - - - {data && - data.accountListAnalytics.phone.completed + - data.accountListAnalytics.phone.received} - - {t('Talked To')} - - - - - - {data?.accountListAnalytics.phone.appointments} - - {t('Appts Produced')} - - - - - - - - {data?.accountListAnalytics.phone.completed} - - {t('Completed')} - - - - - - {data?.accountListAnalytics.phone.attempted} - - {t('Attempted')} - - - - - - {data?.accountListAnalytics.phone.received} - - {t('Received')} - - - - - - - {t('Electronic Messages')} - - - - - {data?.accountListAnalytics.electronic.sent} - - {t('Sent')} - - - - - - {data?.accountListAnalytics.electronic.received} - - {t('Received')} - - - - - - {data?.accountListAnalytics.electronic.appointments} - - {t('Appts Produced')} - - - - - - + )} + + + + {t('Correspondence')} + {loading ? ( + + ) : ( + <> + + - {data?.accountListAnalytics.email.sent} {t('Sent')} + + {data?.accountListAnalytics.correspondence.precall} + + {t('Pre-call')} - {' / '} + + - {data?.accountListAnalytics.email.received} {t('Received')} + + {data?.accountListAnalytics.correspondence.supportLetters} + + {t('Support')} - - {t('Email')} - - - + + + + - {data?.accountListAnalytics.facebook.sent} {t('Sent')} + + {data?.accountListAnalytics.correspondence.thankYous} + + {t('Thank You')} - {' / '} + + - {data?.accountListAnalytics.facebook.received}{' '} - {t('Received')} + + {data?.accountListAnalytics.correspondence.reminders} + + {t('Reminder')} - - {t('Facebook')} - - - + + + + )} + + + + {t('Phone Calls')} + {loading ? ( + + ) : ( + <> + + + + + {data && + data.accountListAnalytics.phone.attempted + + data.accountListAnalytics.phone.completed} + + {t('Outgoing')} + + + + + + {data && + data.accountListAnalytics.phone.completed + + data.accountListAnalytics.phone.received} + + {t('Talked To')} + + + + + {data?.accountListAnalytics.phone.appointments} + + {t('Appts Produced')} + + + + + + - {data?.accountListAnalytics.textMessage.sent} {t('Sent')} + + {data?.accountListAnalytics.phone.completed} + + {t('Completed')} - {' / '} + + + + {data?.accountListAnalytics.phone.attempted} + + {t('Attempted')} + + + + - {data?.accountListAnalytics.textMessage.received}{' '} - {t('Received')} + + {data?.accountListAnalytics.phone.received} + + {t('Received')} - - {t('Text Message')} - - - - - - - {accountListType === AccountListTypeEnum.Own ? ( - - {t('Primary Appeal')} - - ) : ( - t('Primary Appeal') - )} - - - {primaryAppeal ? ( + + + + )} + + + + {t('Electronic Messages')} + {loading ? ( + + ) : ( + <> + + + + + {data?.accountListAnalytics.electronic.sent} + + {t('Sent')} + + + + + + {data?.accountListAnalytics.electronic.received} + + {t('Received')} + + + + + + {data?.accountListAnalytics.electronic.appointments} + + {t('Appts Produced')} + + + + - {currencyFormat( - primaryAppeal.pledgesAmountProcessed, - currency, - locale, - )} + + {data?.accountListAnalytics.email.sent} {t('Sent')} + {' / '} - {currencyFormat( - primaryAppeal.amount ?? 0, - currency, - locale, - )} + + {data?.accountListAnalytics.email.received}{' '} + {t('Received')} + - - {accountListType === AccountListTypeEnum.Own ? ( - - {primaryAppeal.name} - - ) : ( - primaryAppeal.name - )} - + {t('Email')} - ) : ( - {t('No Primary Appeal Set')} - )} - - - - - - - )} + + + + {data?.accountListAnalytics.facebook.sent} {t('Sent')} + + {' / '} + + {data?.accountListAnalytics.facebook.received}{' '} + {t('Received')} + + + {t('Facebook')} + + + + + {data?.accountListAnalytics.textMessage.sent} {t('Sent')} + + {' / '} + + {data?.accountListAnalytics.textMessage.received}{' '} + {t('Received')} + + + {t('Text Message')} + + + + )} + + + + + {accountListType === AccountListTypeEnum.Own ? ( + + {t('Primary Appeal')} + + ) : ( + t('Primary Appeal') + )} + + {loading ? ( + + ) : ( + <> + + {primaryAppeal ? ( + + + {currencyFormat( + primaryAppeal.pledgesAmountProcessed, + currency, + locale, + )} + {' / '} + {currencyFormat( + primaryAppeal.amount ?? 0, + currency, + locale, + )} + + + {accountListType === AccountListTypeEnum.Own ? ( + + + {primaryAppeal.name} + + + ) : ( + primaryAppeal.name + )} + + + ) : ( + {t('No Primary Appeal Set')} + )} + + + + + + )} + + ); }; diff --git a/src/components/Shared/MultilineSkeleton.tsx b/src/components/Shared/MultilineSkeleton.tsx index 57849eda8..3431815b8 100644 --- a/src/components/Shared/MultilineSkeleton.tsx +++ b/src/components/Shared/MultilineSkeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from '@mui/material'; +import { Skeleton, SkeletonProps } from '@mui/material'; import { styled } from '@mui/material/styles'; const StyledSkeleton = styled(Skeleton)(({ theme }) => ({ @@ -6,16 +6,17 @@ const StyledSkeleton = styled(Skeleton)(({ theme }) => ({ margin: theme.spacing(1), })); -interface MultilineSkeletonProps { +interface MultilineSkeletonProps extends SkeletonProps { lines: number; } export const MultilineSkeleton: React.FC = ({ lines, + ...props }) => ( <> {new Array(lines).fill(undefined).map((_, index) => ( - + ))} ); From e65440e3e8fcb44e5e5279fbd5ae7da42cd306ea Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 29 Nov 2023 13:13:10 -0600 Subject: [PATCH 4/7] Move import --- .../Coaching/CoachingDetail/Activity/AppealProgress.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx index 61af3e4d2..0b80aefad 100644 --- a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx @@ -1,9 +1,9 @@ import { Tooltip } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { useTranslation } from 'react-i18next'; import { Appeal } from '../../../../../graphql/types.generated'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat, percentageFormat } from 'src/lib/intlFormat'; -import { useTranslation } from 'react-i18next'; const Amounts = styled('div')(({ theme }) => ({ fontSize: '0.9em', From 6cd02b5d2e11e4dd6f2754416ad58e79b60d1521 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Tue, 5 Dec 2023 08:11:46 -0600 Subject: [PATCH 5/7] Use multiple argument form of theme.spacing --- .../Coaching/CoachingDetail/Activity/AppealProgress.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx index 0b80aefad..5c7dcb256 100644 --- a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx @@ -24,7 +24,7 @@ const CommittedText = styled('span')(({ theme }) => ({ })); const ProgressWrapper = styled('div')(({ theme }) => ({ - padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, + padding: theme.spacing(0.5, 1), })); const ProgressBar = styled('div')(({ theme }) => ({ From 8e31bda3f4b5d07f3ccef4177c1a22f0023922f6 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Tue, 5 Dec 2023 13:15:01 -0600 Subject: [PATCH 6/7] Fix import order --- .../CoachingDetail/Activity/Activity.test.tsx | 4 +-- .../CoachingDetail/Activity/Activity.tsx | 34 +++++++++---------- .../Activity/AppealProgress.tsx | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx index df6d11c78..7e496d41f 100644 --- a/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.test.tsx @@ -2,9 +2,9 @@ import { ThemeProvider } from '@emotion/react'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Settings } from 'luxon'; -import theme from 'src/theme'; -import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import theme from 'src/theme'; import { AccountListTypeEnum, CoachingPeriodEnum } from '../CoachingDetail'; import { Activity } from './Activity'; diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx index 6800b744b..6f41e5a94 100644 --- a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx @@ -1,13 +1,4 @@ import { useEffect, useMemo, useState } from 'react'; -import NextLink from 'next/link'; -import { - Button, - ButtonGroup, - CardHeader, - Link as MuiLink, - Typography, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; import CalendarMonthOutlined from '@mui/icons-material/CalendarMonthOutlined'; import ChatBubbleOutline from '@mui/icons-material/ChatBubbleOutline'; import ChevronLeft from '@mui/icons-material/ChevronLeft'; @@ -16,16 +7,17 @@ import MailOutline from '@mui/icons-material/MailOutline'; import MoneyOutlined from '@mui/icons-material/MoneyOutlined'; import PeopleOutline from '@mui/icons-material/PeopleOutline'; import SmartphoneOutlined from '@mui/icons-material/SmartphoneOutlined'; +import { + Button, + ButtonGroup, + CardHeader, + Link as MuiLink, + Typography, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; import { DateTime, DateTimeUnit } from 'luxon'; +import NextLink from 'next/link'; import { useTranslation } from 'react-i18next'; -import { - ActivityTypeEnum, - Appeal, - ContactFilterSetInput, - ContactFilterStatusEnum, - ResultEnum, - TaskFilterSetInput, -} from '../../../../../graphql/types.generated'; import AnimatedCard from 'src/components/AnimatedCard'; import HandoffLink from 'src/components/HandoffLink'; import { useLocale } from 'src/hooks/useLocale'; @@ -34,6 +26,14 @@ import { dateFormat, dateFormatWithoutYear, } from 'src/lib/intlFormat'; +import { + ActivityTypeEnum, + Appeal, + ContactFilterSetInput, + ContactFilterStatusEnum, + ResultEnum, + TaskFilterSetInput, +} from '../../../../../graphql/types.generated'; import { MultilineSkeleton } from '../../../Shared/MultilineSkeleton'; import { AccountListTypeEnum, CoachingPeriodEnum } from '../CoachingDetail'; import { HelpButton } from '../HelpButton'; diff --git a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx index 5c7dcb256..2a0743c35 100644 --- a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx +++ b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx @@ -1,9 +1,9 @@ import { Tooltip } from '@mui/material'; import { styled } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; -import { Appeal } from '../../../../../graphql/types.generated'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat, percentageFormat } from 'src/lib/intlFormat'; +import { Appeal } from '../../../../../graphql/types.generated'; const Amounts = styled('div')(({ theme }) => ({ fontSize: '0.9em', From 4bbf4a77cea0163c98580734a49d82b37c92223c Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Tue, 5 Dec 2023 13:23:18 -0600 Subject: [PATCH 7/7] Add theme provider to tests --- .../CoachingDetail/CoachingDetail.test.tsx | 672 +++++++++--------- 1 file changed, 352 insertions(+), 320 deletions(-) diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx index 1e21db2b5..5e8599093 100644 --- a/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx +++ b/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; import userEvent from '@testing-library/user-event'; import { DateTime } from 'luxon'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; +import theme from 'src/theme'; import { afterTestResizeObserver, beforeTestResizeObserver, @@ -41,25 +43,27 @@ describe('LoadCoachingDetail', () => { it('view', async () => { const { findByText } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - id: accountListId, - name: 'John Doe', - currency: 'USD', - monthlyGoal: 55, + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + id: accountListId, + name: 'John Doe', + currency: 'USD', + monthlyGoal: 55, + }, }, - }, - }} - > - - - , + }} + > + + + + , ); expect(await findByText('John Doe')).toBeVisible(); expect(await findByText('Monthly $55')).toBeVisible(); @@ -67,25 +71,27 @@ describe('LoadCoachingDetail', () => { }); it('null goal', async () => { const { findByText } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - id: accountListId, - name: 'John Doe', - currency: 'USD', - monthlyGoal: null, + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + id: accountListId, + name: 'John Doe', + currency: 'USD', + monthlyGoal: null, + }, }, - }, - }} - > - - - , + }} + > + + + + , ); expect(await findByText('John Doe')).toBeVisible(); expect(await findByText('Monthly $0')).toBeVisible(); @@ -94,27 +100,29 @@ describe('LoadCoachingDetail', () => { it('view isAccountList', async () => { const { findByText } = render( - - - mocks={{ - LoadAccountListCoachingDetail: { - accountList: { - id: accountListId, - name: 'John Doe', - currency: 'USD', - monthlyGoal: 55, + + + + mocks={{ + LoadAccountListCoachingDetail: { + accountList: { + id: accountListId, + name: 'John Doe', + currency: 'USD', + monthlyGoal: 55, + }, }, - }, - }} - > - - - , + }} + > + + + + , ); expect(await findByText('John Doe')).toBeVisible(); expect(await findByText('Monthly $55')).toBeVisible(); @@ -122,53 +130,57 @@ describe('LoadCoachingDetail', () => { }); it('null goal isAccountList', async () => { const { findByText } = render( - - - mocks={{ - LoadAccountListCoachingDetail: { - accountList: { - id: accountListId, - name: 'John Doe', - currency: 'USD', - monthlyGoal: null, - }, - }, - }} - > - - - , - ); - expect(await findByText('John Doe')).toBeVisible(); - expect(await findByText('Monthly $0')).toBeVisible(); - expect(await findByText('Monthly Activity')).toBeVisible(); - }); - - describe('period', () => { - it('toggles between weekly and monthly', async () => { - const { getByRole } = render( + - + mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - balance: 1000, + LoadAccountListCoachingDetail: { + accountList: { + id: accountListId, + name: 'John Doe', currency: 'USD', + monthlyGoal: null, }, }, }} > - , + + , + ); + expect(await findByText('John Doe')).toBeVisible(); + expect(await findByText('Monthly $0')).toBeVisible(); + expect(await findByText('Monthly Activity')).toBeVisible(); + }); + + describe('period', () => { + it('toggles between weekly and monthly', async () => { + const { getByRole } = render( + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + balance: 1000, + currency: 'USD', + }, + }, + }} + > + + + + , ); await waitFor(() => @@ -205,23 +217,25 @@ describe('LoadCoachingDetail', () => { describe('balance', () => { it('displays the account list balance', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - balance: 1000, - currency: 'USD', + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + balance: 1000, + currency: 'USD', + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -233,26 +247,28 @@ describe('LoadCoachingDetail', () => { describe('staff ids', () => { it('displays comma-separated staff ids', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', - designationAccounts: [ - { accountNumber: '123' }, - { accountNumber: '456' }, - ], + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + designationAccounts: [ + { accountNumber: '123' }, + { accountNumber: '456' }, + ], + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -262,26 +278,28 @@ describe('LoadCoachingDetail', () => { it('ignores empty staff ids', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', - designationAccounts: [ - { accountNumber: '' }, - { accountNumber: '456' }, - ], + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + designationAccounts: [ + { accountNumber: '' }, + { accountNumber: '456' }, + ], + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -291,26 +309,28 @@ describe('LoadCoachingDetail', () => { it('displays none when there are no staff ids', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', - designationAccounts: [ - { accountNumber: '123' }, - { accountNumber: '456' }, - ], + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + designationAccounts: [ + { accountNumber: '123' }, + { accountNumber: '456' }, + ], + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -322,36 +342,38 @@ describe('LoadCoachingDetail', () => { describe('last prayer letter', () => { it('formats the prayer letter date', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + }, }, - }, - GetTaskAnalytics: { - taskAnalytics: { - lastElectronicNewsletterCompletedAt: DateTime.local( - 2023, - 1, - 1, - ).toISO(), - lastPhysicalNewsletterCompletedAt: DateTime.local( - 2023, - 1, - 2, - ).toISO(), + GetTaskAnalytics: { + taskAnalytics: { + lastElectronicNewsletterCompletedAt: DateTime.local( + 2023, + 1, + 1, + ).toISO(), + lastPhysicalNewsletterCompletedAt: DateTime.local( + 2023, + 1, + 2, + ).toISO(), + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -363,28 +385,30 @@ describe('LoadCoachingDetail', () => { it('displays none when there are no prayer letters', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + }, }, - }, - GetTaskAnalytics: { - taskAnalytics: { - lastElectronicNewsletterCompletedAt: null, - lastPhysicalNewsletterCompletedAt: null, + GetTaskAnalytics: { + taskAnalytics: { + lastElectronicNewsletterCompletedAt: null, + lastPhysicalNewsletterCompletedAt: null, + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -398,26 +422,28 @@ describe('LoadCoachingDetail', () => { describe('MPD info', () => { it('displays info', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', - activeMpdStartAt: DateTime.local(2023, 1, 1).toISO(), - activeMpdFinishAt: DateTime.local(2024, 1, 1).toISO(), - activeMpdMonthlyGoal: 1000, - weeksOnMpd: 12, + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + activeMpdStartAt: DateTime.local(2023, 1, 1).toISO(), + activeMpdFinishAt: DateTime.local(2024, 1, 1).toISO(), + activeMpdMonthlyGoal: 1000, + weeksOnMpd: 12, + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -436,25 +462,27 @@ describe('LoadCoachingDetail', () => { it('displays none when info is missing', async () => { const { getByTestId } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', - activeMpdStartAt: null, - activeMpdFinishAt: null, - activeMpdMonthlyGoal: null, + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + activeMpdStartAt: null, + activeMpdFinishAt: null, + activeMpdMonthlyGoal: null, + }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => @@ -472,44 +500,46 @@ describe('LoadCoachingDetail', () => { describe('users', () => { it('shows the user names and contact info', async () => { const { getByText } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', - users: { - nodes: [ - { - firstName: 'John', - lastName: 'Doe', - emailAddresses: { - nodes: [ - { - email: 'john.doe@cru.org', - }, - ], - }, - phoneNumbers: { - nodes: [ - { - number: '111-111-1111', - }, - ], + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + users: { + nodes: [ + { + firstName: 'John', + lastName: 'Doe', + emailAddresses: { + nodes: [ + { + email: 'john.doe@cru.org', + }, + ], + }, + phoneNumbers: { + nodes: [ + { + number: '111-111-1111', + }, + ], + }, }, - }, - ], + ], + }, }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => expect(getByText('John Doe')).toBeInTheDocument()); @@ -521,44 +551,46 @@ describe('LoadCoachingDetail', () => { describe('coaches', () => { it('shows the user names and contact info', async () => { const { getByText } = render( - - - mocks={{ - LoadCoachingDetail: { - coachingAccountList: { - currency: 'USD', - coaches: { - nodes: [ - { - firstName: 'John', - lastName: 'Coach', - emailAddresses: { - nodes: [ - { - email: 'john.coach@cru.org', - }, - ], - }, - phoneNumbers: { - nodes: [ - { - number: '222-222-2222', - }, - ], + + + + mocks={{ + LoadCoachingDetail: { + coachingAccountList: { + currency: 'USD', + coaches: { + nodes: [ + { + firstName: 'John', + lastName: 'Coach', + emailAddresses: { + nodes: [ + { + email: 'john.coach@cru.org', + }, + ], + }, + phoneNumbers: { + nodes: [ + { + number: '222-222-2222', + }, + ], + }, }, - }, - ], + ], + }, }, }, - }, - }} - > - - - , + }} + > + + + + , ); await waitFor(() => expect(getByText('John Coach')).toBeInTheDocument());