diff --git a/pages/accountLists/[accountListId]/reports/partnerGivingAnalysis/[[...contactId]].page.tsx b/pages/accountLists/[accountListId]/reports/partnerGivingAnalysis/[[...contactId]].page.tsx
index 567f4b7a2..2a8986cd7 100644
--- a/pages/accountLists/[accountListId]/reports/partnerGivingAnalysis/[[...contactId]].page.tsx
+++ b/pages/accountLists/[accountListId]/reports/partnerGivingAnalysis/[[...contactId]].page.tsx
@@ -1,8 +1,6 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useEffect, useMemo, useState } from 'react';
-import Box from '@mui/material/Box';
-import { styled } from '@mui/material/styles';
import { sortBy } from 'lodash';
import { useTranslation } from 'react-i18next';
import { ReportContactFilterSetInput } from 'pages/api/graphql-rest.page.generated';
@@ -19,10 +17,6 @@ import { getQueryParam } from 'src/utils/queryParam';
import { useContactFiltersQuery } from '../../contacts/Contacts.generated';
import { ContactsPage } from '../../contacts/ContactsPage';
-const PartnerGivingAnalysisReportPageWrapper = styled(Box)(({ theme }) => ({
- backgroundColor: theme.palette.common.white,
-}));
-
// The order here is also the sort order and the display order
const reportFilters = [
'designation_account_id',
@@ -96,48 +90,46 @@ const PartnerGivingAnalysisReportPage: React.FC = () => {
{accountListId ? (
-
-
- ) : (
- setNavListOpen(false)}
- onSelectedFiltersChanged={setActiveFilters}
- />
- )
- }
- leftOpen={isNavListOpen}
- leftWidth="290px"
- mainContent={
-
+ ) : (
+ setNavListOpen(false)}
+ onSelectedFiltersChanged={setActiveFilters}
/>
- }
- rightPanel={
- selectedContactId ? (
-
- handleSelectContact('')} />
-
- ) : undefined
- }
- rightOpen={typeof selectedContactId !== 'undefined'}
- rightWidth="60%"
- />
-
+ )
+ }
+ leftOpen={isNavListOpen}
+ leftWidth="290px"
+ mainContent={
+
+ }
+ rightPanel={
+ selectedContactId ? (
+
+ handleSelectContact('')} />
+
+ ) : undefined
+ }
+ rightOpen={typeof selectedContactId !== 'undefined'}
+ rightWidth="60%"
+ />
) : (
)}
diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx
index 5e8599093..97fd3f2f3 100644
--- a/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx
+++ b/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx
@@ -1,25 +1,17 @@
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 matchMediaMock from '__tests__/util/matchMediaMock';
+import { render } from '__tests__/util/testingLibraryReactMock';
import theme from 'src/theme';
import {
afterTestResizeObserver,
beforeTestResizeObserver,
} from 'src/utils/tests/windowResizeObserver';
-import { AppointmentResults } from './AppointmentResults/AppointmentResults';
-import {
- AccountListTypeEnum,
- CoachingDetail,
- CoachingPeriodEnum,
-} from './CoachingDetail';
-import {
- LoadAccountListCoachingDetailQuery,
- LoadCoachingDetailQuery,
-} from './LoadCoachingDetail.generated';
+import { AccountListTypeEnum, CoachingDetail } from './CoachingDetail';
+import { LoadCoachingDetailQuery } from './LoadCoachingDetail.generated';
jest.mock('./AppointmentResults/AppointmentResults');
@@ -31,6 +23,44 @@ const router = {
push,
};
+interface TestComponentProps {
+ accountListType?: AccountListTypeEnum;
+ monthlyGoal?: number | null;
+}
+
+const TestComponent: React.FC = ({
+ accountListType = AccountListTypeEnum.Coaching,
+ monthlyGoal = null,
+}) => (
+
+
+
+ mocks={{
+ LoadCoachingDetail: {
+ coachingAccountList: {
+ name: 'John Doe',
+ currency: 'USD',
+ monthlyGoal,
+ },
+ },
+ LoadAccountListCoachingDetail: {
+ accountList: {
+ name: 'John Doe',
+ currency: 'USD',
+ monthlyGoal,
+ },
+ },
+ }}
+ >
+
+
+
+
+);
+
const accountListId = 'account-list-1';
describe('LoadCoachingDetail', () => {
beforeEach(() => {
@@ -41,561 +71,66 @@ describe('LoadCoachingDetail', () => {
afterTestResizeObserver();
});
- it('view', async () => {
- const { findByText } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- id: accountListId,
- name: 'John Doe',
- currency: 'USD',
- monthlyGoal: 55,
- },
- },
- }}
- >
-
-
-
- ,
- );
- expect(await findByText('John Doe')).toBeVisible();
- expect(await findByText('Monthly $55')).toBeVisible();
- expect(await findByText('Monthly Activity')).toBeVisible();
- });
- it('null goal', async () => {
- const { findByText } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- 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();
- });
-
- it('view isAccountList', async () => {
- const { findByText } = render(
-
-
-
- mocks={{
- LoadAccountListCoachingDetail: {
- accountList: {
- id: accountListId,
- name: 'John Doe',
- currency: 'USD',
- monthlyGoal: 55,
- },
- },
- }}
- >
-
-
-
- ,
- );
- expect(await findByText('John Doe')).toBeVisible();
- expect(await findByText('Monthly $55')).toBeVisible();
- expect(await findByText('Monthly Activity')).toBeVisible();
- });
- 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,
- currency: 'USD',
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(AppointmentResults).toHaveBeenLastCalledWith(
- expect.objectContaining({ period: CoachingPeriodEnum.Weekly }),
- expect.anything(),
- ),
- );
-
- userEvent.click(
- getByRole('button', {
- name: 'Monthly',
- }),
- );
-
- expect(AppointmentResults).toHaveBeenLastCalledWith(
- expect.objectContaining({ period: CoachingPeriodEnum.Monthly }),
- expect.anything(),
- );
-
- userEvent.click(
- getByRole('button', {
- name: 'Weekly',
- }),
- );
-
- expect(AppointmentResults).toHaveBeenLastCalledWith(
- expect.objectContaining({ period: CoachingPeriodEnum.Weekly }),
- expect.anything(),
- );
+ describe.each([
+ { type: AccountListTypeEnum.Coaching, name: 'coaching' },
+ { type: AccountListTypeEnum.Own, name: 'own' },
+ ])('$name account list', ({ type: accountListType }) => {
+ it('view', async () => {
+ const { findByRole, getByText } = render(
+ ,
+ );
+ expect(await findByRole('heading', { name: 'John Doe' })).toBeVisible();
+ expect(getByText('Monthly $55')).toBeVisible();
+ expect(getByText('Monthly Activity')).toBeVisible();
});
- });
-
- describe('balance', () => {
- it('displays the account list balance', async () => {
- const { getByTestId } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- balance: 1000,
- currency: 'USD',
- },
- },
- }}
- >
-
-
-
- ,
- );
- await waitFor(() =>
- expect(getByTestId('Balance')).toHaveTextContent('Balance: $1,000'),
+ it('null goal', async () => {
+ const { findByRole, getByText } = render(
+ ,
);
+ expect(await findByRole('heading', { name: 'John Doe' })).toBeVisible();
+ expect(getByText('Monthly $0')).toBeVisible();
+ expect(getByText('Monthly Activity')).toBeVisible();
});
});
- describe('staff ids', () => {
- it('displays comma-separated staff ids', async () => {
- const { getByTestId } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- currency: 'USD',
- designationAccounts: [
- { accountNumber: '123' },
- { accountNumber: '456' },
- ],
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(getByTestId('StaffIds')).toHaveTextContent('123, 456'),
- );
- });
+ describe('sidebar', () => {
+ it('is always visible on large screens', async () => {
+ matchMediaMock({ width: '1024px' });
- it('ignores empty staff ids', async () => {
- const { getByTestId } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- currency: 'USD',
- designationAccounts: [
- { accountNumber: '' },
- { accountNumber: '456' },
- ],
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(getByTestId('StaffIds')).toHaveTextContent('456'),
- );
- });
+ const { findByRole, getByRole, queryByRole } = render();
- it('displays none when there are no staff ids', async () => {
- const { getByTestId } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- currency: 'USD',
- designationAccounts: [
- { accountNumber: '123' },
- { accountNumber: '456' },
- ],
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(getByTestId('StaffIds')).toHaveTextContent('None'),
- );
- });
- });
-
- describe('last prayer letter', () => {
- it('formats the prayer letter date', async () => {
- const { getByTestId } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- currency: 'USD',
- },
- },
- GetTaskAnalytics: {
- taskAnalytics: {
- lastElectronicNewsletterCompletedAt: DateTime.local(
- 2023,
- 1,
- 1,
- ).toISO(),
- lastPhysicalNewsletterCompletedAt: DateTime.local(
- 2023,
- 1,
- 2,
- ).toISO(),
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(getByTestId('LastPrayerLetter')).toHaveTextContent(
- 'Last Prayer Letter: Jan 2, 2023',
- ),
- );
- });
-
- it('displays none when there are no prayer letters', async () => {
- const { getByTestId } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- currency: 'USD',
- },
- },
- GetTaskAnalytics: {
- taskAnalytics: {
- lastElectronicNewsletterCompletedAt: null,
- lastPhysicalNewsletterCompletedAt: null,
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(getByTestId('LastPrayerLetter')).toHaveTextContent(
- 'Last Prayer Letter: None',
- ),
- );
- });
- });
-
- 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,
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(getByTestId('WeeksOnMpd')).toHaveTextContent('Weeks on MPD: 12'),
- );
- expect(getByTestId('MpdStartDate')).toHaveTextContent(
- 'Start Date: Jan 1, 2023',
- );
- expect(getByTestId('MpdEndDate')).toHaveTextContent(
- 'End Date: Jan 1, 2024',
- );
- expect(getByTestId('MpdCommitmentGoal')).toHaveTextContent(
- 'Commitment Goal: $1,000',
- );
+ expect(await findByRole('heading', { name: 'John Doe' })).toBeVisible();
+ expect(getByRole('heading', { name: 'Coaching' })).toBeInTheDocument();
+ expect(
+ queryByRole('button', {
+ name: 'Toggle account details',
+ }),
+ ).not.toBeInTheDocument();
+ expect(queryByRole('button', { name: 'Close' })).not.toBeInTheDocument();
});
- it('displays none when info is missing', async () => {
- const { getByTestId } = render(
-
-
-
- mocks={{
- LoadCoachingDetail: {
- coachingAccountList: {
- currency: 'USD',
- activeMpdStartAt: null,
- activeMpdFinishAt: null,
- activeMpdMonthlyGoal: null,
- },
- },
- }}
- >
-
-
-
- ,
- );
-
- await waitFor(() =>
- expect(getByTestId('MpdStartDate')).toHaveTextContent(
- 'Start Date: None',
- ),
- );
- expect(getByTestId('MpdEndDate')).toHaveTextContent('End Date: None');
- expect(getByTestId('MpdCommitmentGoal')).toHaveTextContent(
- 'Commitment Goal: None',
- );
- });
- });
+ it('is a dismissible drawer on small screens', async () => {
+ matchMediaMock({ width: '256px' });
- 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',
- },
- ],
- },
- },
- ],
- },
- },
- },
- }}
- >
-
-
-
- ,
- );
+ const { findByRole, getByRole, queryByRole } = render();
- await waitFor(() => expect(getByText('John Doe')).toBeInTheDocument());
- expect(getByText('john.doe@cru.org')).toBeInTheDocument();
- expect(getByText('111-111-1111')).toBeInTheDocument();
- });
- });
+ expect(await findByRole('heading', { name: 'John Doe' })).toBeVisible();
+ expect(
+ queryByRole('heading', { name: 'Coaching' }),
+ ).not.toBeInTheDocument();
- 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',
- },
- ],
- },
- },
- ],
- },
- },
- },
- }}
- >
-
-
-
- ,
+ userEvent.click(
+ getByRole('button', {
+ name: 'Toggle account details',
+ }),
);
+ expect(getByRole('heading', { name: 'Coaching' })).toBeInTheDocument();
- await waitFor(() => expect(getByText('John Coach')).toBeInTheDocument());
- expect(getByText('john.coach@cru.org')).toBeInTheDocument();
- expect(getByText('222-222-2222')).toBeInTheDocument();
+ userEvent.click(getByRole('button', { name: 'Close' }));
+ expect(
+ queryByRole('heading', { name: 'Coaching' }),
+ ).not.toBeInTheDocument();
});
});
});
diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx
index 13d22ae2c..add69909f 100644
--- a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx
+++ b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx
@@ -1,33 +1,34 @@
-import React, { Fragment, useMemo, useState } from 'react';
-// TODO: EcoOutlined is not defined on @mui/icons-material, find replacement.
-import AccountCircle from '@mui/icons-material/AccountCircle';
-import { Box, Button, ButtonGroup, Divider, Typography } from '@mui/material';
+import React, { useEffect, useState } from 'react';
+import MenuOpenIcon from '@mui/icons-material/MenuOpen';
+import {
+ Box,
+ Divider,
+ Drawer,
+ Hidden,
+ IconButton,
+ Theme,
+ Typography,
+ useMediaQuery,
+} from '@mui/material';
import { styled } from '@mui/material/styles';
-import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import DonationHistories from 'src/components/Dashboard/DonationHistories';
import { useGetTaskAnalyticsQuery } from 'src/components/Dashboard/ThisWeek/NewsletterMenu/NewsletterMenu.generated';
+import { navBarHeight } from 'src/components/Layouts/Primary/Primary';
import { useGetDonationGraphQuery } from 'src/components/Reports/DonationsReport/GetDonationGraph.generated';
-import { useLocale } from 'src/hooks/useLocale';
-import { currencyFormat } from 'src/lib/intlFormat';
-import { dateFormat } from 'src/lib/intlFormat/intlFormat';
-import theme from 'src/theme';
import { MultilineSkeleton } from '../../Shared/MultilineSkeleton';
import { AppealProgress } from '../AppealProgress/AppealProgress';
import { Activity } from './Activity/Activity';
import { ActivitySummary } from './ActivitySummary/ActivitySummary';
import { AppointmentResults } from './AppointmentResults/AppointmentResults';
-import { CollapsibleEmailList } from './CollapsibleEmailList';
-import { CollapsiblePhoneList } from './CollapsiblePhoneList';
+import { CoachingSidebar } from './CoachingSidebar';
import {
useGetCoachingDonationGraphQuery,
useLoadAccountListCoachingDetailQuery,
useLoadCoachingDetailQuery,
} from './LoadCoachingDetail.generated';
import { MonthlyCommitment } from './MonthlyCommitment/MonthlyCommitment';
-import { SideContainerText } from './StyledComponents';
import { WeeklyReport } from './WeeklyReport/WeeklyReport';
-import { getLastNewsletter } from './helpers';
export enum CoachingPeriodEnum {
Weekly = 'Weekly',
@@ -45,30 +46,16 @@ interface CoachingDetailProps {
accountListType: AccountListTypeEnum;
}
-const CoachingDetailContainer = styled(Box)(({}) => ({
- width: '100%',
- minHeight: '100%',
- display: 'flex',
-}));
-
-const CoachingSideContainer = styled(Box)(({ theme }) => ({
- width: '20rem',
- minHeight: '100%',
- padding: theme.spacing(1),
-}));
-
-const CoachingSideTitleContainer = styled(Box)(({ theme }) => ({
+const CoachingDetailContainer = styled(Box)({
+ height: `calc(100vh - ${navBarHeight})`,
display: 'flex',
- width: '100%',
- margin: theme.spacing(1),
- alignItems: 'center',
- alignContent: 'center',
-}));
+});
const CoachingMainContainer = styled(Box)(({ theme }) => ({
+ flex: 1,
padding: theme.spacing(1),
paddingBottom: theme.spacing(6), // prevent the HelpScout beacon from obscuring content at the bottom
- width: 'calc(100vw - 20rem)',
+ overflowY: 'scroll',
}));
const CoachingItemContainer = styled(Box)(({ theme }) => ({
@@ -82,18 +69,11 @@ const CoachingMainTitleContainer = styled(Box)(({ theme }) => ({
flexGrow: 1,
display: 'flex',
margin: theme.spacing(1),
- alignItems: 'center',
- alignContent: 'center',
-}));
-
-const CoachingMonthYearButtonGroup = styled(ButtonGroup)(({ theme }) => ({
- margin: theme.spacing(2, 0),
- color: theme.palette.primary.contrastText,
-}));
-
-const SideContainerIcon = styled(AccountCircle)(({ theme }) => ({
- color: theme.palette.primary.contrastText,
- margin: theme.spacing(0, 1),
+ flexDirection: 'column',
+ [theme.breakpoints.up('md')]: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
}));
export const CoachingDetail: React.FC = ({
@@ -101,7 +81,6 @@ export const CoachingDetail: React.FC = ({
accountListType,
}) => {
const { t } = useTranslation();
- const locale = useLocale();
const { data: ownData, loading: ownLoading } =
useLoadAccountListCoachingDetailQuery({
@@ -122,14 +101,6 @@ export const CoachingDetail: React.FC = ({
? ownData?.accountList
: coachingData?.coachingAccountList;
- const staffIds = useMemo(
- () =>
- accountListData?.designationAccounts
- .map((account) => account.accountNumber)
- .filter((number) => number.length > 0) ?? [],
- [accountListData],
- );
-
const { data: ownDonationGraphData } = useGetDonationGraphQuery({
variables: {
accountListId,
@@ -156,141 +127,39 @@ export const CoachingDetail: React.FC = ({
});
const [period, setPeriod] = useState(CoachingPeriodEnum.Weekly);
+ const [drawerVisible, setDrawerVisible] = useState(false);
- const formatOptionalDate = (isoDate: string | null | undefined): string =>
- isoDate ? dateFormat(DateTime.fromISO(isoDate), locale) : t('None');
+ const handleCloseDrawer = () => setDrawerVisible(false);
+
+ const sidebarDrawer = useMediaQuery((theme) =>
+ theme.breakpoints.down('md'),
+ );
+ useEffect(() => {
+ if (sidebarDrawer) {
+ handleCloseDrawer();
+ }
+ }, [sidebarDrawer]);
+
+ const sidebar = (
+
+ );
return (
-
-
- {/* TODO: EcoOutlined is not defined on @mui/icons-material, find replacement.
- */}
-
- {t('Coaching')}
-
-
-
-
-
-
-
-
- {t('Balance:')}{' '}
- {accountListData &&
- currencyFormat(
- accountListData.balance,
- accountListData.currency,
- locale,
- )}
-
- {t('Staff IDs:')}
-
- {staffIds.length ? staffIds.join(', ') : t('None')}
-
-
- {t('Last Prayer Letter:')}{' '}
- {taskAnalyticsData &&
- formatOptionalDate(
- getLastNewsletter(
- taskAnalyticsData.taskAnalytics
- .lastElectronicNewsletterCompletedAt,
- taskAnalyticsData.taskAnalytics
- .lastPhysicalNewsletterCompletedAt,
- ),
- )}
-
-
-
- {t('MPD Info')}
-
-
- {t('Weeks on MPD:')} {accountListData?.weeksOnMpd}
-
-
- {t('Start Date:')}{' '}
- {accountListData &&
- formatOptionalDate(accountListData?.activeMpdStartAt)}
-
-
- {t('End Date:')}{' '}
- {accountListData &&
- formatOptionalDate(accountListData?.activeMpdFinishAt)}
-
-
- {t('Commitment Goal:')}{' '}
- {accountListData &&
- (typeof accountListData.activeMpdMonthlyGoal === 'number'
- ? currencyFormat(
- accountListData.activeMpdMonthlyGoal,
- accountListData?.currency,
- locale,
- )
- : t('None'))}
-
-
-
- {t('Users')}
-
- {loading ? (
-
- ) : (
- accountListData?.users.nodes.map((user) => (
-
-
-
- {user.firstName + ' ' + user.lastName}
-
-
-
-
-
- ))
- )}
-
-
- {t('Coaches')}
-
- {loading ? (
-
- ) : (
- accountListData?.coaches.nodes.map((coach) => (
-
-
-
- {coach.firstName + ' ' + coach.lastName}
-
-
-
-
-
- ))
- )}
-
+
+
+ {sidebar}
+
+
+ {sidebar}
{loading ? (
@@ -298,13 +167,15 @@ export const CoachingDetail: React.FC = ({
<>
-
+
+
+ setDrawerVisible(!drawerVisible)}
+ aria-label={t('Toggle account details')}
+ >
+
+
+
{accountListData?.name}
diff --git a/src/components/Coaching/CoachingDetail/CoachingSidebar.test.tsx b/src/components/Coaching/CoachingDetail/CoachingSidebar.test.tsx
new file mode 100644
index 000000000..0bb4308a5
--- /dev/null
+++ b/src/components/Coaching/CoachingDetail/CoachingSidebar.test.tsx
@@ -0,0 +1,364 @@
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+import { DateTime } from 'luxon';
+import { DeepPartial } from 'ts-essentials';
+import { gqlMock } from '__tests__/util/graphqlMocking';
+import { render } from '__tests__/util/testingLibraryReactMock';
+import {
+ GetTaskAnalyticsDocument,
+ GetTaskAnalyticsQuery,
+ GetTaskAnalyticsQueryVariables,
+} from 'src/components/Dashboard/ThisWeek/NewsletterMenu/NewsletterMenu.generated';
+import { AccountListTypeEnum, CoachingPeriodEnum } from './CoachingDetail';
+import { CoachingSidebar } from './CoachingSidebar';
+import {
+ LoadAccountListCoachingDetailDocument,
+ LoadAccountListCoachingDetailQuery,
+ LoadAccountListCoachingDetailQueryVariables,
+ LoadCoachingDetailDocument,
+ LoadCoachingDetailQuery,
+ LoadCoachingDetailQueryVariables,
+} from './LoadCoachingDetail.generated';
+
+interface TestComponentProps {
+ accountListType?: AccountListTypeEnum;
+ showClose?: boolean;
+ accountListMocks?: DeepPartial<
+ Omit<
+ LoadCoachingDetailQuery['coachingAccountList'] | null | undefined,
+ '__typename'
+ >
+ >;
+ taskAnalyticsMocks?: Partial;
+}
+
+const accountListId = 'account-list-1';
+const setPeriodMock = jest.fn();
+const handleCloseMock = jest.fn();
+
+const TestComponent: React.FC = ({
+ accountListType = AccountListTypeEnum.Coaching,
+ showClose = false,
+ accountListMocks,
+ taskAnalyticsMocks,
+}) => {
+ const accountListData =
+ accountListType === AccountListTypeEnum.Coaching
+ ? gqlMock(
+ LoadCoachingDetailDocument,
+ {
+ mocks: {
+ coachingAccountList: {
+ currency: 'USD',
+ ...accountListMocks,
+ },
+ },
+ variables: { coachingAccountListId: accountListId },
+ },
+ ).coachingAccountList
+ : gqlMock<
+ LoadAccountListCoachingDetailQuery,
+ LoadAccountListCoachingDetailQueryVariables
+ >(LoadAccountListCoachingDetailDocument, {
+ mocks: {
+ accountList: {
+ currency: 'USD',
+ ...accountListMocks,
+ },
+ },
+ variables: { accountListId },
+ }).accountList;
+
+ return (
+ (GetTaskAnalyticsDocument, {
+ mocks: {
+ taskAnalytics: taskAnalyticsMocks ?? {},
+ },
+ variables: { accountListId },
+ })}
+ />
+ );
+};
+
+describe('CoachingSidebar', () => {
+ describe('close button', () => {
+ it('calls handleClose', () => {
+ const { getByRole } = render();
+
+ userEvent.click(getByRole('button', { name: 'Close' }));
+ expect(handleCloseMock).toHaveBeenCalled();
+ });
+
+ it('hides when showClose is false', () => {
+ const { queryByRole } = render();
+
+ expect(queryByRole('button', { name: 'Close' })).not.toBeInTheDocument();
+ });
+ });
+
+ describe('period', () => {
+ it('toggles between weekly and monthly', () => {
+ const { getByRole } = render();
+
+ userEvent.click(
+ getByRole('button', {
+ name: 'Monthly',
+ }),
+ );
+ expect(setPeriodMock).toHaveBeenLastCalledWith(
+ CoachingPeriodEnum.Monthly,
+ );
+
+ userEvent.click(
+ getByRole('button', {
+ name: 'Weekly',
+ }),
+ );
+ expect(setPeriodMock).toHaveBeenLastCalledWith(CoachingPeriodEnum.Weekly);
+ });
+ });
+
+ describe.each([
+ { type: AccountListTypeEnum.Coaching, name: 'coaching' },
+ { type: AccountListTypeEnum.Own, name: 'own' },
+ ])('$name account list', ({ type: accountListType }) => {
+ describe('balance', () => {
+ it('displays the account list balance', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('Balance')).toHaveTextContent('Balance: $1,000');
+ });
+ });
+
+ describe('staff ids', () => {
+ it('displays comma-separated staff ids', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('StaffIds')).toHaveTextContent('123, 456');
+ });
+
+ it('ignores empty staff ids', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('StaffIds')).toHaveTextContent('456');
+ });
+
+ it('displays none when there are no staff ids', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('StaffIds')).toHaveTextContent('None');
+ });
+ });
+
+ describe('last prayer letter', () => {
+ it('formats the prayer letter date', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('LastPrayerLetter')).toHaveTextContent(
+ 'Last Prayer Letter: Jan 2, 2023',
+ );
+ });
+
+ it('displays none when there are no prayer letters', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('LastPrayerLetter')).toHaveTextContent(
+ 'Last Prayer Letter: None',
+ );
+ });
+ });
+
+ describe('MPD info', () => {
+ it('displays info', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('WeeksOnMpd')).toHaveTextContent('Weeks on MPD: 12');
+ expect(getByTestId('MpdStartDate')).toHaveTextContent(
+ 'Start Date: Jan 1, 2023',
+ );
+ expect(getByTestId('MpdEndDate')).toHaveTextContent(
+ 'End Date: Jan 1, 2024',
+ );
+ expect(getByTestId('MpdCommitmentGoal')).toHaveTextContent(
+ 'Commitment Goal: $1,000',
+ );
+ });
+
+ it('displays none when info is missing', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('MpdStartDate')).toHaveTextContent(
+ 'Start Date: None',
+ );
+ expect(getByTestId('MpdEndDate')).toHaveTextContent('End Date: None');
+ expect(getByTestId('MpdCommitmentGoal')).toHaveTextContent(
+ 'Commitment Goal: None',
+ );
+ });
+ });
+
+ describe('users', () => {
+ it('shows the user names and contact info', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('John Doe')).toBeInTheDocument();
+ expect(getByText('john.doe@cru.org')).toBeInTheDocument();
+ expect(getByText('111-111-1111')).toBeInTheDocument();
+ });
+ });
+
+ describe('coaches', () => {
+ it('shows the user names and contact info', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('John Coach')).toBeInTheDocument();
+ expect(getByText('john.coach@cru.org')).toBeInTheDocument();
+ expect(getByText('222-222-2222')).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/src/components/Coaching/CoachingDetail/CoachingSidebar.tsx b/src/components/Coaching/CoachingDetail/CoachingSidebar.tsx
new file mode 100644
index 000000000..4d0cb0c7d
--- /dev/null
+++ b/src/components/Coaching/CoachingDetail/CoachingSidebar.tsx
@@ -0,0 +1,238 @@
+import React, { Fragment, useMemo } from 'react';
+// TODO: EcoOutlined is not defined on @mui/icons-material, find replacement.
+import AccountCircleIcon from '@mui/icons-material/AccountCircle';
+import CloseIcon from '@mui/icons-material/Close';
+import { Box, Button, ButtonGroup, Divider, IconButton } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { DateTime } from 'luxon';
+import { useTranslation } from 'react-i18next';
+import { GetTaskAnalyticsQuery } from 'src/components/Dashboard/ThisWeek/NewsletterMenu/NewsletterMenu.generated';
+import { useLocale } from 'src/hooks/useLocale';
+import { currencyFormat } from 'src/lib/intlFormat';
+import { dateFormat } from 'src/lib/intlFormat/intlFormat';
+import theme from 'src/theme';
+import { MultilineSkeleton } from '../../Shared/MultilineSkeleton';
+import { CoachingPeriodEnum } from './CoachingDetail';
+import { CollapsibleEmailList } from './CollapsibleEmailList';
+import { CollapsiblePhoneList } from './CollapsiblePhoneList';
+import {
+ LoadAccountListCoachingDetailQuery,
+ LoadCoachingDetailQuery,
+} from './LoadCoachingDetail.generated';
+import { SideContainerText } from './StyledComponents';
+import { getLastNewsletter } from './helpers';
+
+const Container = styled(Box)({
+ display: 'flex',
+ flexDirection: 'column',
+ width: '20rem',
+ height: '100%',
+});
+
+const TitleContainer = styled('div')(({ theme }) => ({
+ margin: theme.spacing(1),
+ display: 'flex',
+ alignItems: 'center',
+}));
+
+const ContrastDivider = styled(Divider)(({ theme }) => ({
+ background: theme.palette.primary.contrastText,
+}));
+
+const ContentContainer = styled('div')({
+ flex: 1,
+ overflow: 'scroll',
+ padding: theme.spacing(0, 1),
+});
+
+const MonthlyWeeklyButtonGroup = styled(ButtonGroup)(({ theme }) => ({
+ margin: theme.spacing(2, 0),
+ color: theme.palette.primary.contrastText,
+}));
+
+const SectionHeaderText = styled(SideContainerText)(({ theme }) => ({
+ margin: theme.spacing(1),
+}));
+
+const StyledCloseIcon = styled(CloseIcon)(({ theme }) => ({
+ color: theme.palette.primary.contrastText,
+}));
+
+const StyledUserIcon = styled(AccountCircleIcon)(({ theme }) => ({
+ color: theme.palette.primary.contrastText,
+ margin: theme.spacing(0, 1),
+}));
+
+const UserDivider = styled(Divider)(({ theme }) => ({
+ margin: theme.spacing(1),
+}));
+
+interface CoachingSidebarProps {
+ period: CoachingPeriodEnum;
+ setPeriod: React.Dispatch>;
+ showClose: boolean;
+ handleClose: () => void;
+ loading: boolean;
+ accountListData:
+ | LoadCoachingDetailQuery['coachingAccountList']
+ | LoadAccountListCoachingDetailQuery['accountList']
+ | undefined;
+ taskAnalyticsData: GetTaskAnalyticsQuery | undefined;
+}
+
+export const CoachingSidebar: React.FC = ({
+ period,
+ setPeriod,
+ showClose,
+ handleClose,
+ loading,
+ accountListData,
+ taskAnalyticsData,
+}) => {
+ const { t } = useTranslation();
+ const locale = useLocale();
+
+ const staffIds = useMemo(
+ () =>
+ accountListData?.designationAccounts
+ .map((account) => account.accountNumber)
+ .filter((number) => number.length > 0) ?? [],
+ [accountListData],
+ );
+
+ const formatOptionalDate = (isoDate: string | null | undefined): string =>
+ isoDate ? dateFormat(DateTime.fromISO(isoDate), locale) : t('None');
+
+ return (
+
+
+ {/* TODO: EcoOutlined is not defined on @mui/icons-material, find replacement.
+ */}
+
+ {t('Coaching')}
+
+ {showClose && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {t('Balance:')}{' '}
+ {accountListData &&
+ currencyFormat(
+ accountListData.balance,
+ accountListData.currency,
+ locale,
+ )}
+
+ {t('Staff IDs:')}
+
+ {staffIds.length ? staffIds.join(', ') : t('None')}
+
+
+ {t('Last Prayer Letter:')}{' '}
+ {taskAnalyticsData &&
+ formatOptionalDate(
+ getLastNewsletter(
+ taskAnalyticsData.taskAnalytics
+ .lastElectronicNewsletterCompletedAt,
+ taskAnalyticsData.taskAnalytics
+ .lastPhysicalNewsletterCompletedAt,
+ ),
+ )}
+
+
+ {t('MPD Info')}
+
+ {t('Weeks on MPD:')} {accountListData?.weeksOnMpd}
+
+
+ {t('Start Date:')}{' '}
+ {accountListData &&
+ formatOptionalDate(accountListData?.activeMpdStartAt)}
+
+
+ {t('End Date:')}{' '}
+ {accountListData &&
+ formatOptionalDate(accountListData?.activeMpdFinishAt)}
+
+
+ {t('Commitment Goal:')}{' '}
+ {accountListData &&
+ (typeof accountListData.activeMpdMonthlyGoal === 'number'
+ ? currencyFormat(
+ accountListData.activeMpdMonthlyGoal,
+ accountListData?.currency,
+ locale,
+ )
+ : t('None'))}
+
+
+ {t('Users')}
+ {loading ? (
+
+ ) : (
+ accountListData?.users.nodes.map((user) => (
+
+
+
+ {user.firstName + ' ' + user.lastName}
+
+
+
+
+
+ ))
+ )}
+
+ {t('Coaches')}
+ {loading ? (
+
+ ) : (
+ accountListData?.coaches.nodes.map((coach) => (
+
+
+
+ {coach.firstName + ' ' + coach.lastName}
+
+
+
+
+
+ ))
+ )}
+
+
+ );
+};
diff --git a/src/components/Coaching/CoachingDetail/CollapsibleEmailList.tsx b/src/components/Coaching/CoachingDetail/CollapsibleEmailList.tsx
index 280757544..dda90a5e0 100644
--- a/src/components/Coaching/CoachingDetail/CollapsibleEmailList.tsx
+++ b/src/components/Coaching/CoachingDetail/CollapsibleEmailList.tsx
@@ -12,7 +12,9 @@ interface EmailProps {
const Email: React.FC = ({ email }) => (
- {email.email}
+
+ {email.email}
+
{email.location ? ` - ${email.location}` : null}
);
diff --git a/src/components/Coaching/CoachingDetail/CollapsiblePhoneList.tsx b/src/components/Coaching/CoachingDetail/CollapsiblePhoneList.tsx
index e3eac3446..364b40b7e 100644
--- a/src/components/Coaching/CoachingDetail/CollapsiblePhoneList.tsx
+++ b/src/components/Coaching/CoachingDetail/CollapsiblePhoneList.tsx
@@ -12,7 +12,9 @@ interface PhoneProps {
const Phone: React.FC = ({ phone }) => (
- {phone.number}
+
+ {phone.number}
+
{phone.location ? ` - ${phone.location}` : null}
);
diff --git a/src/components/Dashboard/ThisWeek/TasksDueThisWeek/TasksDueThisWeek.tsx b/src/components/Dashboard/ThisWeek/TasksDueThisWeek/TasksDueThisWeek.tsx
index b57477c6f..1bcc485f6 100644
--- a/src/components/Dashboard/ThisWeek/TasksDueThisWeek/TasksDueThisWeek.tsx
+++ b/src/components/Dashboard/ThisWeek/TasksDueThisWeek/TasksDueThisWeek.tsx
@@ -39,7 +39,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
list: {
flex: 1,
padding: 0,
- overflow: 'auto',
+ overflow: 'hidden',
},
card: {
display: 'flex',
diff --git a/src/components/Layouts/Basic/TopBar/TopBar.tsx b/src/components/Layouts/Basic/TopBar/TopBar.tsx
index d6dd3de8b..a34d3a6a0 100644
--- a/src/components/Layouts/Basic/TopBar/TopBar.tsx
+++ b/src/components/Layouts/Basic/TopBar/TopBar.tsx
@@ -1,5 +1,6 @@
import React, { ReactElement, ReactNode } from 'react';
import { AppBar, Grid, Theme, Toolbar, useScrollTrigger } from '@mui/material';
+import { styled } from '@mui/material/styles';
import { makeStyles } from 'tss-react/mui';
const useStyles = makeStyles()((theme: Theme) => ({
@@ -17,6 +18,8 @@ const useStyles = makeStyles()((theme: Theme) => ({
},
}));
+const Offset = styled('div')(({ theme }) => theme.mixins.toolbar);
+
interface Props {
children?: ReactNode;
}
@@ -30,13 +33,16 @@ const TopBar = ({ children }: Props): ReactElement => {
});
return (
-
-
-
- {children}
-
-
-
+ <>
+
+
+
+ {children}
+
+
+
+
+ >
);
};
diff --git a/src/components/Layouts/Primary/NavBar/NavBar.tsx b/src/components/Layouts/Primary/NavBar/NavBar.tsx
index bd2def8b1..fbe515dd6 100644
--- a/src/components/Layouts/Primary/NavBar/NavBar.tsx
+++ b/src/components/Layouts/Primary/NavBar/NavBar.tsx
@@ -2,7 +2,7 @@ import NextLink, { LinkProps } from 'next/link';
import { useRouter } from 'next/router';
import React, { ReactElement, useEffect } from 'react';
import type { FC } from 'react';
-import { Box, Drawer, Hidden, List, Theme } from '@mui/material';
+import { Box, Drawer, Hidden, List, Theme, useMediaQuery } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { reportNavItems } from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems';
@@ -106,7 +106,6 @@ const useStyles = makeStyles()((theme: Theme) => ({
mobileDrawer: {
width: 290,
backgroundColor: theme.palette.cruGrayDark.main,
- zIndex: theme.zIndex.drawer + 200,
},
}));
@@ -154,37 +153,15 @@ export const NavBar: FC = ({ onMobileClose, openMobile }) => {
},
];
+ const drawerHidden = useMediaQuery((theme) =>
+ theme.breakpoints.up('md'),
+ );
+ // Close the drawer when the route changes or when the drawer is hidden because the screen is larger
useEffect(() => {
- if (openMobile && onMobileClose) {
+ if (drawerHidden || (openMobile && onMobileClose)) {
onMobileClose();
}
- }, [pathname]);
-
- const content = (
-
-
-
-
-
-
-
-
-
- {renderNavItems({
- accountListId,
- items: sections,
- pathname,
- })}
-
-
-
-
-
- );
+ }, [pathname, drawerHidden]);
return (
@@ -196,7 +173,25 @@ export const NavBar: FC = ({ onMobileClose, openMobile }) => {
open={openMobile}
variant="temporary"
>
- {content}
+
+
+
+
+
+
+ {renderNavItems({
+ accountListId,
+ items: sections,
+ pathname,
+ })}
+
+
+
+
);
diff --git a/src/components/Layouts/Primary/Primary.tsx b/src/components/Layouts/Primary/Primary.tsx
index 03c957b49..2905ea6fc 100644
--- a/src/components/Layouts/Primary/Primary.tsx
+++ b/src/components/Layouts/Primary/Primary.tsx
@@ -8,15 +8,13 @@ export const navBarHeight = '64px';
const RootContainer = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.common.white,
- display: 'flex',
- height: '100%',
- overflow: 'hidden',
- width: '100%',
+ width: '100vw',
+ height: '100vh',
+ overflow: 'scroll',
}));
const ContentContainer = styled('div')(() => ({
display: 'flex',
- flex: '1 1 auto',
overflow: 'hidden',
}));
@@ -26,13 +24,6 @@ const Content = styled('div')(() => ({
overflow: 'auto',
}));
-const Wrapper = styled('div')(() => ({
- display: 'flex',
- flex: '1 1 auto',
- overflow: 'hidden',
- paddingTop: navBarHeight,
-}));
-
interface Props {
children: ReactNode;
}
@@ -53,11 +44,9 @@ const Primary = ({ children }: Props): ReactElement => {
openMobile={isMobileNavOpen}
/>
)}
-
-
- {children}
-
-
+
+ {children}
+
);
};
diff --git a/src/components/Layouts/Primary/TopBar/TopBar.tsx b/src/components/Layouts/Primary/TopBar/TopBar.tsx
index 243a702d2..563a1d9f9 100644
--- a/src/components/Layouts/Primary/TopBar/TopBar.tsx
+++ b/src/components/Layouts/Primary/TopBar/TopBar.tsx
@@ -22,8 +22,9 @@ interface TopBarProps {
onMobileNavOpen?: () => void;
}
+const Offset = styled('div')(({ theme }) => theme.mixins.toolbar);
+
const StyledAppBar = styled(AppBar)(({ theme }) => ({
- zIndex: theme.zIndex.drawer + 100,
backgroundColor: theme.palette.cruGrayDark.main,
}));
@@ -37,18 +38,18 @@ const TopBar = ({
});
return (
-
-
- {accountListId && (
-
-
-
-
-
-
-
- )}
-
+ <>
+
+
+ {accountListId && (
+
+
+
+
+
+
+
+ )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
};