diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f04d8d5548..b50419ca65 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
env:
# Use production API for codegen to make sure production is compatible with the code to be merged
- API_URL: 'https://api.mpdx.org/graphql'
+ API_URL: 'https://api.stage.mpdx.org/graphql' # TODO: Revert this before merging!
SITE_URL: 'http://stage.mpdx.org'
jobs:
diff --git a/__tests__/util/setup.ts b/__tests__/util/setup.ts
index 9cf96b9197..b77da9a814 100644
--- a/__tests__/util/setup.ts
+++ b/__tests__/util/setup.ts
@@ -60,6 +60,12 @@ Object.defineProperty(window, 'location', {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
+window.ResizeObserver = jest.fn().mockReturnValue({
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+});
+
window.URL.revokeObjectURL = jest.fn();
beforeEach(() => {
diff --git a/__tests__/util/windowResizeObserver.ts b/__tests__/util/windowResizeObserver.ts
deleted file mode 100644
index 3b276f9fc4..0000000000
--- a/__tests__/util/windowResizeObserver.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-const { ResizeObserver } = window;
-
-export const beforeTestResizeObserver = () => {
- window.ResizeObserver = jest.fn().mockImplementation(() => ({
- observe: jest.fn(),
- unobserve: jest.fn(),
- disconnect: jest.fn(),
- }));
-};
-
-export const afterTestResizeObserver = () => {
- window.ResizeObserver = ResizeObserver;
- jest.restoreAllMocks();
-};
diff --git a/graphql.config.json b/graphql.config.json
new file mode 100644
index 0000000000..9ff3207be8
--- /dev/null
+++ b/graphql.config.json
@@ -0,0 +1,4 @@
+{
+ "schema": "src/graphql/schema.graphql",
+ "documents": "**/*.graphql"
+}
diff --git a/pages/404.page.tsx b/pages/404.page.tsx
index e85941ea7f..e215564abb 100644
--- a/pages/404.page.tsx
+++ b/pages/404.page.tsx
@@ -8,7 +8,7 @@ import BaseLayout from 'src/components/Layouts/Basic';
import useGetAppSettings from 'src/hooks/useGetAppSettings';
const BoxWrapper = styled(Box)(({ theme }) => ({
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
height: 300,
minWidth: 700,
margin: 'auto',
diff --git a/pages/500.page.tsx b/pages/500.page.tsx
index 1e122abcb4..f4509352ed 100644
--- a/pages/500.page.tsx
+++ b/pages/500.page.tsx
@@ -8,7 +8,7 @@ import BaseLayout from 'src/components/Layouts/Basic';
import useGetAppSettings from 'src/hooks/useGetAppSettings';
const BoxWrapper = styled(Box)(({ theme }) => ({
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
height: 300,
minWidth: 700,
margin: 'auto',
diff --git a/pages/GetAccountLists.graphql b/pages/GetAccountLists.graphql
index 834e1a2288..3432993df7 100644
--- a/pages/GetAccountLists.graphql
+++ b/pages/GetAccountLists.graphql
@@ -8,9 +8,15 @@ query GetAccountLists {
id
name
monthlyGoal
+ monthlyGoalUpdatedAt
receivedPledges
totalPledges
currency
+ healthIndicatorData {
+ id
+ machineCalculatedGoal
+ machineCalculatedGoalCurrency
+ }
}
}
}
diff --git a/pages/accountLists/GetDashboard.graphql b/pages/accountLists/GetDashboard.graphql
index 33f41c256a..4963e5c8da 100644
--- a/pages/accountLists/GetDashboard.graphql
+++ b/pages/accountLists/GetDashboard.graphql
@@ -1,11 +1,13 @@
-query GetDashboard($accountListId: ID!) {
+query GetDashboard($accountListId: ID!, $periodBegin: ISO8601Date!) {
user {
+ id
firstName
}
accountList(id: $accountListId) {
id
name
monthlyGoal
+ monthlyGoalUpdatedAt
receivedPledges
totalPledges
currency
@@ -21,6 +23,7 @@ query GetDashboard($accountListId: ID!) {
averageIgnoreCurrent
periods {
startDate
+ endDate
convertedTotal
totals {
currency
@@ -28,4 +31,11 @@ query GetDashboard($accountListId: ID!) {
}
}
}
+ healthIndicatorData(accountListId: $accountListId, beginDate: $periodBegin) {
+ id
+ indicationPeriodBegin
+ machineCalculatedGoal
+ machineCalculatedGoalCurrency
+ staffEnteredGoal
+ }
}
diff --git a/pages/accountLists/[accountListId].page.test.tsx b/pages/accountLists/[accountListId].page.test.tsx
index c72bc7991c..39f4feb22e 100644
--- a/pages/accountLists/[accountListId].page.test.tsx
+++ b/pages/accountLists/[accountListId].page.test.tsx
@@ -79,6 +79,7 @@ describe('AccountListsId page', () => {
contacts: {
totalCount: 5,
},
+ healthIndicatorData: [],
},
}),
});
diff --git a/pages/accountLists/[accountListId].page.tsx b/pages/accountLists/[accountListId].page.tsx
index 9e349b83b3..367ab56fc6 100644
--- a/pages/accountLists/[accountListId].page.tsx
+++ b/pages/accountLists/[accountListId].page.tsx
@@ -1,6 +1,7 @@
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import { ApolloError } from '@apollo/client';
+import { DateTime } from 'luxon';
import {
GetDefaultAccountDocument,
GetDefaultAccountQuery,
@@ -106,6 +107,10 @@ export const getServerSideProps = makeGetServerSideProps(
query: GetDashboardDocument,
variables: {
accountListId: query.accountListId,
+ periodBegin: DateTime.now()
+ .startOf('month')
+ .minus({ years: 1 })
+ .toISODate(),
// TODO: implement these variables in query
// endOfDay: DateTime.local().endOf('day').toISO(),
// today: DateTime.local().endOf('day').toISODate(),
diff --git a/pages/accountLists/[accountListId]/reports/healthIndicator/index.page.test.tsx b/pages/accountLists/[accountListId]/reports/healthIndicator/index.page.test.tsx
new file mode 100644
index 0000000000..f88c25dc21
--- /dev/null
+++ b/pages/accountLists/[accountListId]/reports/healthIndicator/index.page.test.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { ThemeProvider } from '@mui/material/styles';
+import { LocalizationProvider } from '@mui/x-date-pickers';
+import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
+import { render } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { SnackbarProvider } from 'notistack';
+import { I18nextProvider } from 'react-i18next';
+import TestRouter from '__tests__/util/TestRouter';
+import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
+import i18n from 'src/lib/i18n';
+import theme from 'src/theme';
+import HealthIndicatorPage from './index.page';
+
+const accountListId = 'account-list-1';
+const router = {
+ query: { accountListId },
+ isReady: true,
+};
+
+const Components = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+describe('MPD Health Indicator Page', () => {
+ it('should show initial financial accounts page', async () => {
+ const { findByText } = render( );
+
+ expect(await findByText('Overall Staff MPD Health')).toBeInTheDocument();
+ });
+
+ it('should open and close menu', async () => {
+ const { findByRole, getByRole, queryByRole } = render( );
+
+ userEvent.click(
+ await findByRole('button', { name: 'Toggle Navigation Panel' }),
+ );
+ expect(getByRole('heading', { name: 'Reports' })).toBeInTheDocument();
+ userEvent.click(getByRole('img', { name: 'Close' }));
+ expect(queryByRole('heading', { name: 'Reports' })).not.toBeInTheDocument();
+ });
+});
diff --git a/pages/accountLists/[accountListId]/reports/healthIndicator/index.page.tsx b/pages/accountLists/[accountListId]/reports/healthIndicator/index.page.tsx
new file mode 100644
index 0000000000..a05948a4dc
--- /dev/null
+++ b/pages/accountLists/[accountListId]/reports/healthIndicator/index.page.tsx
@@ -0,0 +1,66 @@
+import Head from 'next/head';
+import React, { useState } from 'react';
+import { Box } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { ensureSessionAndAccountList } from 'pages/api/utils/pagePropsHelpers';
+import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout';
+import Loading from 'src/components/Loading';
+import { HealthIndicatorReport } from 'src/components/Reports/HealthIndicatorReport/HealthIndicatorReport';
+import { headerHeight } from 'src/components/Shared/Header/ListHeader';
+import {
+ MultiPageMenu,
+ NavTypeEnum,
+} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu';
+import { useAccountListId } from 'src/hooks/useAccountListId';
+import useGetAppSettings from 'src/hooks/useGetAppSettings';
+
+const HealthIndicatorPage: React.FC = () => {
+ const { t } = useTranslation();
+ const accountListId = useAccountListId();
+ const { appName } = useGetAppSettings();
+ const [navListOpen, setNavListOpen] = useState(false);
+
+ const handleNavListToggle = () => {
+ setNavListOpen(!navListOpen);
+ };
+ return (
+ <>
+
+ {`${appName} | ${t('Reports - MPD Health')}`}
+
+
+ {accountListId ? (
+
+
+ }
+ leftPanel={
+
+ }
+ />
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export const getServerSideProps = ensureSessionAndAccountList;
+
+export default HealthIndicatorPage;
diff --git a/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx b/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx
index 33c1b08f2d..f42e45df40 100644
--- a/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx
+++ b/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx
@@ -15,6 +15,7 @@ import {
} from 'src/components/Settings/preferences/GetAccountPreferences.generated';
import { GetPersonalPreferencesQuery } from 'src/components/Settings/preferences/GetPersonalPreferences.generated';
import { GetProfileInfoQuery } from 'src/components/Settings/preferences/GetProfileInfo.generated';
+import { MachineCalculatedGoalQuery } from 'src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MachineCalculatedGoal.generated';
import { TestSetupProvider } from 'src/components/Setup/SetupProvider';
import theme from 'src/theme';
import Preferences from './preferences.page';
@@ -68,6 +69,7 @@ const MocksProviders: React.FC = ({
GetPersonalPreferences: GetPersonalPreferencesQuery;
GetProfileInfo: GetProfileInfoQuery;
CanUserExportData: CanUserExportDataQuery;
+ MachineCalculatedGoal: MachineCalculatedGoalQuery;
}>
mocks={{
GetAccountPreferences: {
@@ -79,10 +81,10 @@ const MocksProviders: React.FC = ({
name: 'test',
activeMpdMonthlyGoal: null,
activeMpdFinishAt: null,
+ currency: 'USD',
activeMpdStartAt: null,
salaryOrganizationId: null,
settings: {
- currency: 'USD',
homeCountry: 'USA',
monthlyGoal: 100,
tester: true,
@@ -132,6 +134,11 @@ const MocksProviders: React.FC = ({
exportedAt: null,
},
},
+ MachineCalculatedGoal: {
+ accountList: {
+ healthIndicatorData: null,
+ },
+ },
}}
onCall={mutationSpy}
>
diff --git a/pages/accountLists/[accountListId]/settings/preferences.page.tsx b/pages/accountLists/[accountListId]/settings/preferences.page.tsx
index 53675228fd..07f68743af 100644
--- a/pages/accountLists/[accountListId]/settings/preferences.page.tsx
+++ b/pages/accountLists/[accountListId]/settings/preferences.page.tsx
@@ -248,10 +248,12 @@ const Preferences: React.FC = () => {
accountPreferencesData?.accountList?.settings?.monthlyGoal ||
null
}
- accountListId={accountListId}
- currency={
- accountPreferencesData?.accountList?.settings?.currency || ''
+ monthlyGoalUpdatedAt={
+ accountPreferencesData?.accountList?.monthlyGoalUpdatedAt ??
+ null
}
+ accountListId={accountListId}
+ currency={accountPreferencesData?.accountList?.currency || null}
disabled={onSetupTour && setup !== 1}
handleSetupChange={handleSetupChange}
/>
@@ -269,9 +271,7 @@ const Preferences: React.FC = () => {
@@ -312,9 +312,7 @@ const Preferences: React.FC = () => {
accountPreferencesData?.accountList?.activeMpdMonthlyGoal ||
null
}
- currency={
- accountPreferencesData?.accountList?.settings?.currency || ''
- }
+ currency={accountPreferencesData?.accountList?.currency || ''}
accountListId={accountListId}
disabled={onSetupTour}
/>
diff --git a/pages/api/Schema/CoachingAnswerSets/dataHandler.ts b/pages/api/Schema/CoachingAnswerSets/dataHandler.ts
index bf312a99ae..e752eb3d71 100644
--- a/pages/api/Schema/CoachingAnswerSets/dataHandler.ts
+++ b/pages/api/Schema/CoachingAnswerSets/dataHandler.ts
@@ -3,8 +3,7 @@ import {
CoachingAnswerSet,
CoachingQuestion,
} from 'src/graphql/types.generated';
-
-const isNotNull = (item: T | null): item is T => item !== null;
+import { isNotNullish } from 'src/lib/typeGuards';
interface CoachingAnswerSetData {
id: string;
@@ -77,10 +76,10 @@ const parseCoachingAnswerSet = (
};
const answers = relationships.answers.data
.map(({ id }) => getIncludedAnswer(id, included))
- .filter(isNotNull);
+ .filter(isNotNullish);
const questions = relationships.questions.data
.map(({ id }) => getIncludedQuestion(id, included))
- .filter(isNotNull);
+ .filter(isNotNullish);
return {
id,
diff --git a/pages/logout.page.tsx b/pages/logout.page.tsx
index ee7b20d92f..978dd57f35 100644
--- a/pages/logout.page.tsx
+++ b/pages/logout.page.tsx
@@ -11,7 +11,7 @@ import { clearDataDogUser } from 'src/lib/dataDog';
import { ensureSessionAndAccountList } from './api/utils/pagePropsHelpers';
const BoxWrapper = styled(Box)(({ theme }) => ({
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
height: 300,
minWidth: 700,
margin: 'auto',
diff --git a/src/components/AccountLists/AccountLists.test.tsx b/src/components/AccountLists/AccountLists.test.tsx
index 1ab7e5ea2d..5c0ca23e05 100644
--- a/src/components/AccountLists/AccountLists.test.tsx
+++ b/src/components/AccountLists/AccountLists.test.tsx
@@ -1,53 +1,270 @@
import React from 'react';
import { ThemeProvider } from '@mui/material/styles';
-import { render } from '@testing-library/react';
+import { render, within } from '@testing-library/react';
+import { gqlMock } from '__tests__/util/graphqlMocking';
+import {
+ GetAccountListsDocument,
+ GetAccountListsQuery,
+} from 'pages/GetAccountLists.generated';
import theme from '../../theme';
import AccountLists from '.';
describe('AccountLists', () => {
it('has correct defaults', () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ { id: '1', name: 'My Personal Staff Account' },
+ { id: '2', name: 'My Ministry Account' },
+ { id: '3', name: "My Friend's Staff Account" },
+ ],
+ },
+ },
+ });
+
const { getByTestId } = render(
-
+ ,
+ );
+ expect(
+ within(getByTestId('account-list-1')).getByRole('heading', {
+ name: 'My Personal Staff Account',
+ }),
+ ).toBeInTheDocument();
+ expect(
+ within(getByTestId('account-list-2')).getByRole('heading', {
+ name: 'My Ministry Account',
+ }),
+ ).toBeInTheDocument();
+ expect(
+ within(getByTestId('account-list-3')).getByRole('heading', {
+ name: "My Friend's Staff Account",
+ }),
+ ).toBeInTheDocument();
+ });
+
+ it('ignores machine-calculated goal when monthly goal is set', () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ {
+ name: 'Account',
+ monthlyGoal: 1000,
+ monthlyGoalUpdatedAt: '2024-01-01T00:00:00Z',
+ receivedPledges: 600,
+ totalPledges: 800,
+ healthIndicatorData: {
+ machineCalculatedGoal: 2000,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ currency: 'USD',
},
- accountLists: {
- nodes: [
- {
- id: 'abc',
- name: 'My Personal Staff Account',
- monthlyGoal: 100,
- receivedPledges: 10,
- totalPledges: 20,
- currency: 'USD',
- },
- {
- id: 'def',
- name: 'My Ministry Account',
- monthlyGoal: null,
- receivedPledges: 10,
- totalPledges: 20,
- currency: 'USD',
- },
- {
- id: 'ghi',
- name: "My Friend's Staff Account",
- monthlyGoal: 100,
- receivedPledges: 0,
- totalPledges: 0,
- currency: 'USD',
- },
- ],
+ ],
+ },
+ },
+ });
+
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('link')).toHaveTextContent(
+ 'AccountGoal$1,000*Gifts Started60%Committed80%*Below NetSuite-calculated goal',
+ );
+ });
+
+ it('uses machine-calculated goal when goal is not set', () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ {
+ name: 'Account',
+ monthlyGoal: null,
+ receivedPledges: 600,
+ totalPledges: 800,
+ healthIndicatorData: {
+ machineCalculatedGoal: 2000,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ currency: 'USD',
+ },
+ ],
+ },
+ },
+ });
+
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('link')).toHaveTextContent(
+ 'AccountGoal$2,000*Gifts Started30%Committed40%*NetSuite-calculated',
+ );
+ });
+
+ it('adds color and label to machine-calculated goals', () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ {
+ name: 'Account',
+ monthlyGoal: null,
+ healthIndicatorData: {
+ machineCalculatedGoal: 2000,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ currency: 'USD',
},
- }}
- />
+ ],
+ },
+ },
+ });
+
+ const { getByLabelText, getByText } = render(
+
+
+ ,
+ );
+ expect(
+ getByLabelText(/^Your current goal of \$2,000 is NetSuite-calculated/),
+ ).toBeInTheDocument();
+ expect(getByText('NetSuite-calculated')).toHaveStyle(
+ 'color: rgb(211, 68, 0);',
+ );
+ });
+
+ it("hides percentages when machine-calculated goal currency differs from user's currency", () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ {
+ name: 'Account',
+ monthlyGoal: null,
+ receivedPledges: 600,
+ totalPledges: 800,
+ healthIndicatorData: {
+ machineCalculatedGoal: 2000,
+ machineCalculatedGoalCurrency: 'EUR',
+ },
+ currency: 'USD',
+ },
+ ],
+ },
+ },
+ });
+
+ const { getByRole } = render(
+
+
,
);
- expect(getByTestId('abc')).toHaveTextContent('My Personal Staff Account');
- expect(getByTestId('def')).toHaveTextContent('My Ministry Account');
- expect(getByTestId('ghi')).toHaveTextContent("My Friend's Staff Account");
+ expect(getByRole('link')).toHaveTextContent(
+ 'AccountGifts Started-Committed-',
+ );
+ });
+
+ describe('updated date', () => {
+ it('is the date the goal was last updated', () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [{ monthlyGoalUpdatedAt: '2019-12-30T00:00:00Z' }],
+ },
+ },
+ });
+
+ const { getByText } = render(
+
+
+ ,
+ );
+ expect(getByText('Last updated Dec 30, 2019')).toBeInTheDocument();
+ });
+
+ it('is hidden if the goal is missing', () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ {
+ monthlyGoal: null,
+ monthlyGoalUpdatedAt: '2019-12-30T00:00:00Z',
+ },
+ ],
+ },
+ },
+ });
+
+ const { queryByText } = render(
+
+
+ ,
+ );
+ expect(queryByText('Last updated Dec 30, 2019')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('below machine-calculated warning', () => {
+ it('is shown if goal is less than the machine-calculated goal', () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ {
+ currency: 'USD',
+ monthlyGoal: 5000,
+ healthIndicatorData: {
+ machineCalculatedGoal: 10000,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ const { getByText } = render(
+
+
+ ,
+ );
+ expect(getByText('Below NetSuite-calculated goal')).toBeInTheDocument();
+ });
+
+ it('is hidden if goal is greater than or equal to the machine-calculated goal', async () => {
+ const data = gqlMock(GetAccountListsDocument, {
+ mocks: {
+ accountLists: {
+ nodes: [
+ {
+ currency: 'USD',
+ monthlyGoal: 5000,
+ healthIndicatorData: {
+ machineCalculatedGoal: 5000,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ const { queryByText } = render(
+
+
+ ,
+ );
+ expect(
+ queryByText('Below NetSuite-calculated goal'),
+ ).not.toBeInTheDocument();
+ });
});
});
diff --git a/src/components/AccountLists/AccountLists.tsx b/src/components/AccountLists/AccountLists.tsx
index 1c90d86c38..1edc00dc3e 100644
--- a/src/components/AccountLists/AccountLists.tsx
+++ b/src/components/AccountLists/AccountLists.tsx
@@ -8,17 +8,30 @@ import {
Grid,
Link,
Theme,
+ Tooltip,
Typography,
+ TypographyProps,
} from '@mui/material';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { GetAccountListsQuery } from 'pages/GetAccountLists.generated';
import { useLocale } from 'src/hooks/useLocale';
-import { currencyFormat, percentageFormat } from '../../lib/intlFormat';
+import { GoalSource, getHealthIndicatorInfo } from 'src/lib/healthIndicator';
+import {
+ currencyFormat,
+ dateFormat,
+ percentageFormat,
+} from 'src/lib/intlFormat';
import AnimatedCard from '../AnimatedCard';
import PageHeading from '../PageHeading';
+interface Annotation {
+ label: string;
+ color?: TypographyProps['color'];
+ variant?: TypographyProps['variant'];
+}
+
interface Props {
data: GetAccountListsQuery;
}
@@ -72,37 +85,88 @@ const AccountLists = ({ data }: Props): ReactElement => {
>
- {data.accountLists.nodes.map(
- ({
+ {data.accountLists.nodes.map((accountList) => {
+ const {
id,
name,
- monthlyGoal,
receivedPledges,
totalPledges,
currency,
- }) => {
- const receivedPercentage =
- receivedPledges / (monthlyGoal ?? NaN);
- const totalPercentage = totalPledges / (monthlyGoal ?? NaN);
+ healthIndicatorData,
+ } = accountList;
- return (
-
-
-
-
-
-
-
- {name}
-
-
-
- {monthlyGoal && (
+ const {
+ goal,
+ goalSource,
+ preferencesGoalUpdatedAt,
+ preferencesGoalLow,
+ preferencesGoalOld,
+ } = getHealthIndicatorInfo(accountList, healthIndicatorData);
+
+ const hasValidGoal = goal !== null;
+ const receivedPercentage = hasValidGoal
+ ? receivedPledges / goal
+ : NaN;
+ const totalPercentage = hasValidGoal ? totalPledges / goal : NaN;
+
+ const annotation: Annotation | null = preferencesGoalLow
+ ? {
+ label: t('Below NetSuite-calculated goal'),
+ color: 'statusWarning.main',
+ }
+ : goalSource === GoalSource.MachineCalculated
+ ? {
+ label: t('NetSuite-calculated'),
+ color: 'statusWarning.main',
+ }
+ : preferencesGoalUpdatedAt
+ ? {
+ label: t('Last updated {{date}}', {
+ date: dateFormat(preferencesGoalUpdatedAt, locale),
+ }),
+ color: preferencesGoalOld
+ ? 'statusWarning.main'
+ : undefined,
+ }
+ : null;
+ const annotationId = `annotation-${id}`;
+
+ return (
+
+
+
+
+
+
+
+ {name}
+
+
+
+ {goal && (
+
{
>
{t('Goal')}
-
- {currencyFormat(
- monthlyGoal,
- currency,
- locale,
+
+ {currencyFormat(goal, currency, locale)}
+ {annotation && (
+
+ *
+
)}
- )}
-
-
- {t('Gifts Started')}
-
-
- {Number.isFinite(receivedPercentage)
- ? percentageFormat(
- receivedPercentage,
- locale,
- )
- : '-'}
-
-
-
-
- {t('Committed')}
-
-
- {Number.isFinite(totalPercentage)
- ? percentageFormat(totalPercentage, locale)
- : '-'}
-
-
+
+ )}
+
+
+ {t('Gifts Started')}
+
+
+ {Number.isFinite(receivedPercentage)
+ ? percentageFormat(receivedPercentage, locale)
+ : '-'}
+
+
+
+
+ {t('Committed')}
+
+
+ {Number.isFinite(totalPercentage)
+ ? percentageFormat(totalPercentage, locale)
+ : '-'}
+
-
-
-
-
-
- );
- },
- )}
+
+ {annotation && (
+
+ *
+ {annotation.label}
+
+ )}
+
+
+
+
+
+ );
+ })}
diff --git a/src/components/Announcements/AnnouncementAction/AnnouncementAction.test.tsx b/src/components/Announcements/AnnouncementAction/AnnouncementAction.test.tsx
index 0d36620b52..11451ce5b1 100644
--- a/src/components/Announcements/AnnouncementAction/AnnouncementAction.test.tsx
+++ b/src/components/Announcements/AnnouncementAction/AnnouncementAction.test.tsx
@@ -120,7 +120,7 @@ describe('AnnouncementAction', () => {
const button = getByText('Contacts');
expect(button).toHaveStyle({
- 'background-color': '#ED6C02',
+ 'background-color': '#D34400',
color: '#FFFFFF',
});
});
@@ -131,7 +131,7 @@ describe('AnnouncementAction', () => {
const button = getByText('Contacts');
expect(button).toHaveStyle({
- 'background-color': '#ED6C02',
+ 'background-color': '#D34400',
color: '#FFFFFF',
});
});
diff --git a/src/components/Announcements/AnnouncementBanner/AnnouncementBanner.tsx b/src/components/Announcements/AnnouncementBanner/AnnouncementBanner.tsx
index ef37fc7756..3970e757ed 100644
--- a/src/components/Announcements/AnnouncementBanner/AnnouncementBanner.tsx
+++ b/src/components/Announcements/AnnouncementBanner/AnnouncementBanner.tsx
@@ -18,7 +18,7 @@ const Banner = styled(Box)(({ theme }) => ({
padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
boxShadow: `0px 0px 10px 0px ${
theme.palette.augmentColor({
- color: { main: theme.palette.cruGrayDark.main },
+ color: { main: theme.palette.mpdxGrayDark.main },
}).light
}`,
position: 'fixed',
@@ -61,7 +61,7 @@ const ButtonContainer = styled(Box)(() => ({
const createAnnouncementStyles = (announcementStyle?: StyleEnum | null) => {
const defaultStyles = {
background: theme.palette.primary.main,
- textAndIconColor: theme.palette.cruGrayLight.main,
+ textAndIconColor: theme.palette.mpdxGrayLight.main,
};
switch (announcementStyle) {
case StyleEnum.Danger:
diff --git a/src/components/Coaching/AppealProgress/AppealProgress.test.tsx b/src/components/Coaching/AppealProgress/AppealProgress.test.tsx
index cfaddf3f14..3bffdd33fa 100644
--- a/src/components/Coaching/AppealProgress/AppealProgress.test.tsx
+++ b/src/components/Coaching/AppealProgress/AppealProgress.test.tsx
@@ -6,8 +6,12 @@ describe('AppealProgress', () => {
it('has correct defaults', () => {
const { getByTestId, queryByTestId } = render( );
expect(queryByTestId('styledProgressLoading')).toBeNull();
- expect(getByTestId('styledProgressPrimary')).toHaveStyle('width: 0%;');
- expect(getByTestId('styledProgressSecondary')).toHaveStyle('width: 0%;');
+ expect(getByTestId('styledProgressPrimary')).toHaveStyle(
+ 'width: calc(0% - 4px);',
+ );
+ expect(getByTestId('styledProgressSecondary')).toHaveStyle(
+ 'width: calc(0% - 4px);',
+ );
});
it('has correct overrides', () => {
@@ -15,8 +19,12 @@ describe('AppealProgress', () => {
,
);
expect(queryByTestId('styledProgressLoading')).toBeNull();
- expect(getByTestId('styledProgressPrimary')).toHaveStyle('width: 50%;');
- expect(getByTestId('styledProgressSecondary')).toHaveStyle('width: 75%;');
+ expect(getByTestId('styledProgressPrimary')).toHaveStyle(
+ 'width: calc(50% - 4px);',
+ );
+ expect(getByTestId('styledProgressSecondary')).toHaveStyle(
+ 'width: calc(75% - 4px);',
+ );
});
it('allows loading', () => {
diff --git a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx
index f1730f2958..4162c88e2d 100644
--- a/src/components/Coaching/CoachingDetail/Activity/Activity.tsx
+++ b/src/components/Coaching/CoachingDetail/Activity/Activity.tsx
@@ -73,8 +73,8 @@ const ActivitySection = styled('div')(({ theme }) => ({
height: '280px',
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}`,
+ borderRight: `1px solid ${theme.palette.mpdxGrayMedium.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayMedium.main}`,
'@media (max-width: 1150px)': {
// One column
':nth-of-type(n + 6)': {
@@ -125,7 +125,7 @@ const StatsRow = styled('div')(({ theme }) => ({
width: '100%',
maxHeight: '88px',
':nth-of-type(2)': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
@@ -138,7 +138,7 @@ const StatsColumn = styled('div')(({ theme }) => ({
alignItems: 'center',
flex: 1,
padding: theme.spacing(0.5),
- borderLeft: `1px solid ${theme.palette.cruGrayMedium.main}`,
+ borderLeft: `1px solid ${theme.palette.mpdxGrayMedium.main}`,
':first-of-type': {
borderLeft: 'none',
},
diff --git a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx
index 173a2f6cc2..9190959da9 100644
--- a/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx
+++ b/src/components/Coaching/CoachingDetail/Activity/AppealProgress.tsx
@@ -34,7 +34,7 @@ const ProgressBar = styled('div')(({ theme }) => ({
textAlign: 'left',
whiteSpace: 'nowrap',
borderRadius: '10px',
- backgroundColor: theme.palette.cruGrayDark.main,
+ backgroundColor: theme.palette.mpdxGrayDark.main,
}));
const ProgressSegment = styled('div')({
diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx
index 598a2bfad0..94db866072 100644
--- a/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx
+++ b/src/components/Coaching/CoachingDetail/CoachingDetail.test.tsx
@@ -5,10 +5,6 @@ import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import matchMediaMock from '__tests__/util/matchMediaMock';
import { render } from '__tests__/util/testingLibraryReactMock';
-import {
- afterTestResizeObserver,
- beforeTestResizeObserver,
-} from '__tests__/util/windowResizeObserver';
import theme from 'src/theme';
import { AccountListTypeEnum, CoachingDetail } from './CoachingDetail';
import { LevelOfEffortQuery } from './LevelOfEffort/LevelOfEffort.generated';
@@ -121,14 +117,6 @@ const TestComponent: React.FC = ({
const accountListId = 'account-list-1';
describe('LoadCoachingDetail', () => {
- beforeEach(() => {
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
- });
-
describe.each([
{ type: AccountListTypeEnum.Coaching, name: 'coaching' },
{ type: AccountListTypeEnum.Own, name: 'own' },
diff --git a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx
index 4aa8a0d579..55ea8db1f3 100644
--- a/src/components/Coaching/CoachingDetail/CoachingDetail.tsx
+++ b/src/components/Coaching/CoachingDetail/CoachingDetail.tsx
@@ -1,3 +1,4 @@
+import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
import {
@@ -11,6 +12,7 @@ import {
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';
@@ -85,6 +87,7 @@ export const CoachingDetail: React.FC = ({
accountListType,
}) => {
const { t } = useTranslation();
+ const { push } = useRouter();
const { data: ownData, loading: ownLoading } =
useLoadAccountListCoachingDetailQuery({
@@ -105,9 +108,15 @@ export const CoachingDetail: React.FC = ({
? ownData?.accountList
: coachingData?.coachingAccountList;
+ const periodBegin = DateTime.now()
+ .startOf('month')
+ .minus({ years: 1 })
+ .toISODate();
+
const { data: ownDonationGraphData } = useGetDonationGraphQuery({
variables: {
accountListId,
+ periodBegin,
},
skip: accountListType !== AccountListTypeEnum.Own,
});
@@ -115,6 +124,7 @@ export const CoachingDetail: React.FC = ({
const { data: coachingDonationGraphData } = useGetCoachingDonationGraphQuery({
variables: {
coachingAccountListId: accountListId,
+ periodBegin,
},
skip: accountListType !== AccountListTypeEnum.Coaching,
});
@@ -148,6 +158,17 @@ export const CoachingDetail: React.FC = ({
}
}, [sidebarDrawer]);
+ const handlePeriodClick = (period: DateTime) => {
+ if (accountListType === AccountListTypeEnum.Own) {
+ push({
+ pathname: `/accountLists/${accountListId}/reports/donations`,
+ query: {
+ month: period.toISODate(),
+ },
+ });
+ }
+ };
+
const sidebar = (
= ({
= ({
);
describe('MonthlyCommitment', () => {
- beforeEach(() => {
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
- });
-
it('renders', async () => {
const { findByTestId } = render( );
diff --git a/src/components/Coaching/CoachingDetail/MonthlyCommitment/MonthlyCommitment.tsx b/src/components/Coaching/CoachingDetail/MonthlyCommitment/MonthlyCommitment.tsx
index d2cd59c788..1582e4cf3d 100644
--- a/src/components/Coaching/CoachingDetail/MonthlyCommitment/MonthlyCommitment.tsx
+++ b/src/components/Coaching/CoachingDetail/MonthlyCommitment/MonthlyCommitment.tsx
@@ -9,7 +9,6 @@ import {
import { useTranslation } from 'react-i18next';
import {
Bar,
- BarChart,
CartesianGrid,
Legend,
ReferenceLine,
@@ -20,6 +19,7 @@ import {
YAxis,
} from 'recharts';
import AnimatedCard from 'src/components/AnimatedCard';
+import { StyledBarChart } from 'src/components/common/StyledBarChart/StyledBarChart';
import { AccountList, Maybe } from 'src/graphql/types.generated';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
@@ -147,7 +147,7 @@ export const MonthlyCommitment: React.FC = ({
>
) : (
-
+
= ({
barSize={30}
fill={theme.palette.progressBarOrange.main}
/>
-
+
)}
diff --git a/src/components/Coaching/CoachingDetail/helpers.test.ts b/src/components/Coaching/CoachingDetail/helpers.test.ts
index 246506a6c8..c8a135e2ab 100644
--- a/src/components/Coaching/CoachingDetail/helpers.test.ts
+++ b/src/components/Coaching/CoachingDetail/helpers.test.ts
@@ -1,4 +1,4 @@
-import { getLastNewsletter, getResultColor } from './helpers';
+import { getLastNewsletter } from './helpers';
describe('getLastNewsletter', () => {
it('returns the latest date when two dates are provided', () => {
@@ -20,19 +20,3 @@ describe('getLastNewsletter', () => {
expect(getLastNewsletter(null, null)).toBeNull();
});
});
-
-describe('getResultColor', () => {
- it('is green when at or above the goal', () => {
- expect(getResultColor(10, 10)).toBe('#5CB85C');
- expect(getResultColor(11, 10)).toBe('#5CB85C');
- });
-
- it('is yellow when at or above 80% of the goal', () => {
- expect(getResultColor(8, 10)).toBe('#8A6D3B');
- expect(getResultColor(9, 10)).toBe('#8A6D3B');
- });
-
- it('is red when below 80% of the goal', () => {
- expect(getResultColor(7, 10)).toBe('#A94442');
- });
-});
diff --git a/src/components/Coaching/CoachingDetail/helpers.ts b/src/components/Coaching/CoachingDetail/helpers.ts
index 6b14bd7904..51d62a6a92 100644
--- a/src/components/Coaching/CoachingDetail/helpers.ts
+++ b/src/components/Coaching/CoachingDetail/helpers.ts
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { dateFormatMonthOnly } from 'src/lib/intlFormat';
-import theme from 'src/theme';
import { CoachingPeriodEnum } from './CoachingDetail';
export const getLastNewsletter = (
@@ -18,17 +17,6 @@ export const getLastNewsletter = (
}
};
-// Calculate the color of a result based on how close it is to the goal
-export const getResultColor = (amount: number, goal: number): string => {
- if (amount >= goal) {
- return theme.palette.statusSuccess.main;
- } else if (amount >= goal * 0.8) {
- return theme.palette.statusWarning.main;
- } else {
- return theme.palette.statusDanger.main;
- }
-};
-
export const getMonthOrWeekDateRange = (
locale: string,
period: CoachingPeriodEnum,
diff --git a/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderNewsletterSection.tsx b/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderNewsletterSection.tsx
index 7110eac15e..28708fcec7 100644
--- a/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderNewsletterSection.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderNewsletterSection.tsx
@@ -25,7 +25,7 @@ const NewsletterIcon: React.FC = () => (
);
diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/Mailing/ContactDetailsTabMailing.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/Mailing/ContactDetailsTabMailing.tsx
index c1611e36a4..57477cd027 100644
--- a/src/components/Contacts/ContactDetails/ContactDetailsTab/Mailing/ContactDetailsTabMailing.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/Mailing/ContactDetailsTabMailing.tsx
@@ -26,7 +26,7 @@ const ContactDetailsMailingMainContainer = styled(Box)(({ theme }) => ({
const ContactDetailsMailingIcon = styled(LocationOn)(({ theme }) => ({
margin: theme.spacing(1, 2),
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
const ContactDetailsMailingTextContainer = styled(Box)(({}) => ({
@@ -57,7 +57,7 @@ const AddressEditIcon = styled(CreateIcon)(({ theme }) => ({
width: '18px',
height: '18px',
margin: theme.spacing(0),
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
const AddressEditIconContainer = styled(IconButton)(({ theme }) => ({
diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/PartnerAccounts/ContactDetailsPartnerAccounts.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/PartnerAccounts/ContactDetailsPartnerAccounts.tsx
index f65a137a93..bc13d1de1c 100644
--- a/src/components/Contacts/ContactDetails/ContactDetailsTab/PartnerAccounts/ContactDetailsPartnerAccounts.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/PartnerAccounts/ContactDetailsPartnerAccounts.tsx
@@ -29,7 +29,7 @@ const ContactPartnerAccountsContainer = styled(Box)(({ theme }) => ({
const OrganizationName = styled(Typography)(({ theme }) => ({
fontSize: theme.spacing(1.5),
- color: theme.palette.cruGrayDark.main,
+ color: theme.palette.mpdxGrayDark.main,
}));
const ContactPartnerAccountsTextContainer = styled(Box)(({ theme }) => ({
diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/EditContactDetailsModal/EditContactDetailsModal.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/EditContactDetailsModal/EditContactDetailsModal.tsx
index 091a6434fe..02b93e2e53 100644
--- a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/EditContactDetailsModal/EditContactDetailsModal.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/EditContactDetailsModal/EditContactDetailsModal.tsx
@@ -44,7 +44,7 @@ const PrimaryContactIcon = styled(BookmarkIcon)(({ theme }) => ({
top: '50%',
left: 8,
transform: 'translateY(-50%)',
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
const LoadingIndicator = styled(CircularProgress)(({ theme }) => ({
diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionDeleteIcon/ModalSectionDeleteIcon.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionDeleteIcon/ModalSectionDeleteIcon.tsx
index 22503ecab4..445fc49568 100644
--- a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionDeleteIcon/ModalSectionDeleteIcon.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionDeleteIcon/ModalSectionDeleteIcon.tsx
@@ -9,7 +9,7 @@ const ContactEditDeleteIconButton = styled(IconButton)(({ theme }) => ({
top: '50%',
right: 0,
transform: 'translateY(-50%)',
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
'&:disabled': {
cursor: 'not-allowed',
pointerEvents: 'all',
diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionIcon/ModalSectionIcon.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionIcon/ModalSectionIcon.tsx
index 9d50a234e5..a98674d92b 100644
--- a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionIcon/ModalSectionIcon.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/ModalSectionIcon/ModalSectionIcon.tsx
@@ -12,7 +12,7 @@ const IconContainer = styled(Box, {
top: '50%',
left: 8,
transform: transform,
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
interface ModalSectionIconProps {
diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents.ts b/src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents.ts
index 788305ad59..df8e95e960 100644
--- a/src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents.ts
+++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents.ts
@@ -12,7 +12,7 @@ export const EditIcon = styled(Create)(({ theme }) => ({
width: '18px',
height: '18px',
margin: theme.spacing(0),
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
export const AddButton = styled(Button)(({ theme }) => ({
@@ -29,7 +29,7 @@ export const LockIcon = styled(Lock)(({ theme }) => ({
width: '18px',
height: '18px',
margin: theme.spacing(0),
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
export const ContactDetailLoadingPlaceHolder = styled(Skeleton)(
diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.graphql b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.graphql
index af94cdc8e7..70e21e0a5e 100644
--- a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.graphql
+++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.graphql
@@ -1,5 +1,6 @@
query GetDonationsGraph($accountListId: ID!, $donorAccountIds: [ID!]) {
accountList(id: $accountListId) {
+ id
currency
}
reportsDonationHistories(
diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx
index 69f608603d..f99a7c99c0 100644
--- a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx
@@ -2,10 +2,6 @@ import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import { render } from '__tests__/util/testingLibraryReactMock';
-import {
- afterTestResizeObserver,
- beforeTestResizeObserver,
-} from '__tests__/util/windowResizeObserver';
import { DonationsGraph } from './DonationsGraph';
import {
GetDonationsGraphQuery,
@@ -30,14 +26,6 @@ const donorAccountIds = ['donor-Account-Id'];
const currency = 'USD';
describe('Donations Graph', () => {
- beforeEach(() => {
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
- });
-
it('test renderer', async () => {
const { findByText } = render(
diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx
index 125de8e12d..d6535a43b9 100644
--- a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx
@@ -5,7 +5,6 @@ import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import {
Bar,
- BarChart,
CartesianGrid,
Legend,
ResponsiveContainer,
@@ -14,6 +13,7 @@ import {
XAxis,
YAxis,
} from 'recharts';
+import { StyledBarChart } from 'src/components/common/StyledBarChart/StyledBarChart';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
import theme from 'src/theme';
@@ -109,7 +109,7 @@ export const DonationsGraph: React.FC = ({
/>
) : (
-
+
= ({
dataKey="thisYear"
fill={theme.palette.primary.main}
/>
-
+
)}
diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx
index f1748974aa..e1c2a4450f 100644
--- a/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx
+++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx
@@ -59,7 +59,7 @@ const PartnershipEditIcon = styled(CreateIcon)(({ theme }) => ({
width: '18px',
height: '18px',
margin: theme.spacing(0),
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
const ClearIcon = styled(Clear)(({ theme }) => ({
diff --git a/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/ContactTaskRow.tsx b/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/ContactTaskRow.tsx
index 1594403238..3767f3e912 100644
--- a/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/ContactTaskRow.tsx
+++ b/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/ContactTaskRow.tsx
@@ -39,7 +39,7 @@ const TaskRowWrap = styled(Box, {
justifyContent: 'space-between',
margin: theme.spacing(0),
height: theme.spacing(8),
- ...(isChecked ? { backgroundColor: theme.palette.cruGrayLight.main } : {}),
+ ...(isChecked ? { backgroundColor: theme.palette.mpdxGrayLight.main } : {}),
}));
const TaskItemWrap = styled(Box)(({ theme }) => ({
diff --git a/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/TaskDate/TaskDate.test.tsx b/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/TaskDate/TaskDate.test.tsx
index d574a7270f..9fd935e32c 100644
--- a/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/TaskDate/TaskDate.test.tsx
+++ b/src/components/Contacts/ContactDetails/ContactTasksTab/ContactTaskRow/TaskDate/TaskDate.test.tsx
@@ -17,13 +17,7 @@ describe('TaskCommentsButton', () => {
,
);
- const dateText = getByText('Oct 12, 21');
-
- expect(dateText).toBeInTheDocument();
-
- const style = dateText && window.getComputedStyle(dateText);
-
- expect(style?.color).toMatchInlineSnapshot(`"rgb(56, 63, 67)"`);
+ expect(getByText('Oct 12, 21')).toHaveStyle('color: #383F43');
});
it('should render complete', () => {
@@ -33,13 +27,7 @@ describe('TaskCommentsButton', () => {
,
);
- const dateText = getByText('Oct 12, 21');
-
- expect(dateText).toBeInTheDocument();
-
- const style = dateText && window.getComputedStyle(dateText);
-
- expect(style?.color).toMatchInlineSnapshot(`"rgb(156, 159, 161)"`);
+ expect(getByText('Oct 12, 21')).toHaveStyle('color: #9C9FA1');
});
it('should render late', () => {
@@ -49,13 +37,7 @@ describe('TaskCommentsButton', () => {
,
);
- const dateText = getByText('Oct 12, 19');
-
- expect(dateText).toBeInTheDocument();
-
- const style = dateText && window.getComputedStyle(dateText);
-
- expect(style?.color).toMatchInlineSnapshot(`"rgb(211, 47, 47)"`);
+ expect(getByText('Oct 12, 19')).toHaveStyle('color: #991313');
});
it('should not render year', () => {
@@ -65,7 +47,6 @@ describe('TaskCommentsButton', () => {
,
);
- const dateText = getByText('Oct 12');
- expect(dateText).toBeInTheDocument();
+ expect(getByText('Oct 12')).toBeInTheDocument();
});
});
diff --git a/src/components/Contacts/ContactFlow/ContactFlow.tsx b/src/components/Contacts/ContactFlow/ContactFlow.tsx
index 9aa81c2ce8..af285f3bb0 100644
--- a/src/components/Contacts/ContactFlow/ContactFlow.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlow.tsx
@@ -39,7 +39,7 @@ export const colorMap: { [key: string]: string } = {
'color-warning': theme.palette.progressBarYellow.main,
'color-success': theme.palette.success.main,
'color-info': theme.palette.mpdxBlue.main,
- 'color-text': theme.palette.cruGrayDark.main,
+ 'color-text': theme.palette.mpdxGrayDark.main,
};
export const ContactFlow: React.FC = ({
diff --git a/src/components/Contacts/ContactFlow/ContactFlowColumn/ContactFlowColumn.tsx b/src/components/Contacts/ContactFlow/ContactFlowColumn/ContactFlowColumn.tsx
index 8779f24a60..a79804ce6d 100644
--- a/src/components/Contacts/ContactFlow/ContactFlowColumn/ContactFlowColumn.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlowColumn/ContactFlowColumn.tsx
@@ -46,7 +46,7 @@ export const StyledCardContent = styled(CardContent)(() => ({
position: 'relative',
height: 'calc(100vh - 260px)',
padding: 0,
- background: theme.palette.cruGrayLight.main,
+ background: theme.palette.mpdxGrayLight.main,
}));
export const CardContentInner = styled(Box, {
diff --git a/src/components/Contacts/ContactFlow/ContactFlowDragLayer/ContactFlowDragLayer.tsx b/src/components/Contacts/ContactFlow/ContactFlowDragLayer/ContactFlowDragLayer.tsx
index 9c53537296..dda1b4bc31 100644
--- a/src/components/Contacts/ContactFlow/ContactFlowDragLayer/ContactFlowDragLayer.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlowDragLayer/ContactFlowDragLayer.tsx
@@ -17,7 +17,7 @@ export const layerStyles: CSSProperties = {
export const dragPreviewStyle: CSSProperties = {
display: 'inline-block',
- border: `1px solid ${theme.palette.cruGrayMedium.main}`,
+ border: `1px solid ${theme.palette.mpdxGrayMedium.main}`,
};
export function getItemStyles(
diff --git a/src/components/Contacts/ContactFlow/ContactFlowDropZone/ContactFlowDropZone.tsx b/src/components/Contacts/ContactFlow/ContactFlowDropZone/ContactFlowDropZone.tsx
index e06e8cefb4..16b540840f 100644
--- a/src/components/Contacts/ContactFlow/ContactFlowDropZone/ContactFlowDropZone.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlowDropZone/ContactFlowDropZone.tsx
@@ -18,14 +18,14 @@ export const DropZoneBox = styled(Box, {
width: '100%',
border: canDrop
? `3px dashed ${theme.palette.mpdxBlue.main}`
- : `3px solid ${theme.palette.cruGrayMedium.main}`,
+ : `3px solid ${theme.palette.mpdxGrayMedium.main}`,
zIndex: canDrop ? 1 : 0,
- color: canDrop ? theme.palette.common.white : theme.palette.cruGrayDark.main,
+ color: canDrop ? theme.palette.common.white : theme.palette.mpdxGrayDark.main,
backgroundColor: canDrop
? isOver
? theme.palette.info.main
: theme.palette.info.light
- : theme.palette.cruGrayLight.main,
+ : theme.palette.mpdxGrayLight.main,
justifyContent: 'center',
alignItems: 'center',
}));
diff --git a/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/ContactFlowSetupColumn.tsx b/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/ContactFlowSetupColumn.tsx
index c288273d0a..29bb1f7fe0 100644
--- a/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/ContactFlowSetupColumn.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/ContactFlowSetupColumn.tsx
@@ -25,7 +25,7 @@ const DeleteColumnButton = styled(IconButton)(() => ({
color: theme.palette.error.main,
padding: theme.spacing(1),
'&:hover': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
color: theme.palette.error.dark,
},
}));
@@ -221,7 +221,7 @@ export const ContactFlowSetupColumn: React.FC = ({
position: 'relative',
height: 'calc(100vh - 230px)',
padding: 0,
- background: theme.palette.cruGrayLight.main,
+ background: theme.palette.mpdxGrayLight.main,
overflowY: 'auto',
}}
>
@@ -237,7 +237,7 @@ export const ContactFlowSetupColumn: React.FC = ({
display="flex"
data-testid="color-selector-box"
justifyContent="center"
- borderBottom={`1px solid ${theme.palette.cruGrayMedium.main}`}
+ borderBottom={`1px solid ${theme.palette.mpdxGrayMedium.main}`}
style={{ backgroundColor: theme.palette.common.white }}
>
{Object.entries(colorMap).map(([colorKey, colorValue]) => (
diff --git a/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.test.tsx b/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.test.tsx
index 9913a0999f..f1cb693bf3 100644
--- a/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.test.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.test.tsx
@@ -39,7 +39,7 @@ describe('UnusedStatusesColumn', () => {
);
await waitFor(() =>
expect(getByTestId('column-header')).toHaveStyle({
- borderBottom: `5px solid ${theme.palette.cruGrayMedium.main}`,
+ borderBottom: `5px solid ${theme.palette.mpdxGrayMedium.main}`,
}),
);
expect(getByText('Unused Statuses')).toBeInTheDocument();
diff --git a/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.tsx b/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.tsx
index 8781b1abe0..ce40f8787f 100644
--- a/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn.tsx
@@ -35,7 +35,7 @@ export const UnusedStatusesColumn: React.FC = ({
alignItems="center"
justifyContent="space-between"
data-testid="column-header"
- borderBottom={`5px solid ${theme.palette.cruGrayMedium.main}`}
+ borderBottom={`5px solid ${theme.palette.mpdxGrayMedium.main}`}
height={theme.spacing(7)}
>
{t('Unused Statuses')}
@@ -45,7 +45,7 @@ export const UnusedStatusesColumn: React.FC = ({
position: 'relative',
height: 'calc(100vh - 230px)',
padding: 0,
- background: theme.palette.cruGrayLight.main,
+ background: theme.palette.mpdxGrayLight.main,
overflowY: 'auto',
}}
>
@@ -62,7 +62,7 @@ export const UnusedStatusesColumn: React.FC = ({
alignItems="center"
height={theme.spacing(4)}
width="100%"
- borderBottom={`1px solid ${theme.palette.cruGrayMedium.main}`}
+ borderBottom={`1px solid ${theme.palette.mpdxGrayMedium.main}`}
style={{
backgroundColor: theme.palette.common.white,
padding: theme.spacing(2.5),
@@ -75,7 +75,7 @@ export const UnusedStatusesColumn: React.FC = ({
>
({
padding: theme.spacing(2),
justifyContent: 'space-between',
alignItems: 'center',
- borderBottom: `1px solid ${theme.palette.cruGrayMedium.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayMedium.main}`,
}));
const ButtonWrap = styled(Box)(() => ({
@@ -27,14 +27,14 @@ const ButtonWrap = styled(Box)(() => ({
}));
const BackButton = styled(Button)(({ theme }) => ({
- color: theme.palette.cruGrayDark.main,
- borderColor: theme.palette.cruGrayDark.main,
+ color: theme.palette.mpdxGrayDark.main,
+ borderColor: theme.palette.mpdxGrayDark.main,
paddingLeft: theme.spacing(1),
textTransform: 'none',
}));
const ResetButton = styled(Button)(({ theme }) => ({
- color: theme.palette.cruGrayDark.main,
+ color: theme.palette.mpdxGrayDark.main,
textTransform: 'none',
'@media (min-width: 381px)': {
marginRight: theme.spacing(3),
diff --git a/src/components/Contacts/ContactFlow/ContactFlowSetup/Row/ContactFlowSetupStatusRow.tsx b/src/components/Contacts/ContactFlow/ContactFlowSetup/Row/ContactFlowSetupStatusRow.tsx
index e1fc696595..b4ef60ba2b 100644
--- a/src/components/Contacts/ContactFlow/ContactFlowSetup/Row/ContactFlowSetupStatusRow.tsx
+++ b/src/components/Contacts/ContactFlow/ContactFlowSetup/Row/ContactFlowSetupStatusRow.tsx
@@ -9,7 +9,7 @@ import theme from '../../../../../theme';
const StatusRow = styled(Box)(() => ({
padding: theme.spacing(1.5),
- borderBottom: `1px solid ${theme.palette.cruGrayMedium.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayMedium.main}`,
'&:hover': {
backgroundColor: theme.palette.mpdxYellow.main,
cursor: 'move',
diff --git a/src/components/Contacts/ContactPartnershipStatus/ContactLateStatusLabel/ContactLateStatusLabel.tsx b/src/components/Contacts/ContactPartnershipStatus/ContactLateStatusLabel/ContactLateStatusLabel.tsx
index e0cf5451d0..215e3af6e2 100644
--- a/src/components/Contacts/ContactPartnershipStatus/ContactLateStatusLabel/ContactLateStatusLabel.tsx
+++ b/src/components/Contacts/ContactPartnershipStatus/ContactLateStatusLabel/ContactLateStatusLabel.tsx
@@ -44,9 +44,9 @@ export const ContactLateStatusLabel: React.FC = ({
lateStatusEnum === ContactLateStatusEnum.OnTime
? theme.palette.mpdxGreen.main
: lateStatusEnum === ContactLateStatusEnum.LateLessThirty
- ? theme.palette.cruGrayMedium.main
+ ? theme.palette.mpdxGrayMedium.main
: lateStatusEnum === ContactLateStatusEnum.LateMoreThirty
- ? theme.palette.cruYellow.main
+ ? theme.palette.yellow.main
: lateStatusEnum === ContactLateStatusEnum.LateMoreSixty
? theme.palette.error.main
: undefined,
diff --git a/src/components/Contacts/ContactRow/ContactRow.tsx b/src/components/Contacts/ContactRow/ContactRow.tsx
index 7328211357..b069bddf7b 100644
--- a/src/components/Contacts/ContactRow/ContactRow.tsx
+++ b/src/components/Contacts/ContactRow/ContactRow.tsx
@@ -40,7 +40,7 @@ export const ListItemButton = styled(ButtonBase)(({ theme }) => ({
marginTop: 16,
},
'&.checked': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
})) as typeof ButtonBase;
diff --git a/src/components/Contacts/ContactUncompletedTasksCount/ContactUncompletedTasksCount.tsx b/src/components/Contacts/ContactUncompletedTasksCount/ContactUncompletedTasksCount.tsx
index 2192461b06..4829e7115d 100644
--- a/src/components/Contacts/ContactUncompletedTasksCount/ContactUncompletedTasksCount.tsx
+++ b/src/components/Contacts/ContactUncompletedTasksCount/ContactUncompletedTasksCount.tsx
@@ -12,9 +12,9 @@ interface ContactUncompletedTasksCountProps {
}
const LogTaskIcon = styled(CheckCircleOutlineIcon)(({ theme }) => ({
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
'&:hover': {
- color: theme.palette.cruGrayDark.main,
+ color: theme.palette.mpdxGrayDark.main,
},
}));
diff --git a/src/components/Contacts/ContactsMap/ContactsMapPanel.tsx b/src/components/Contacts/ContactsMap/ContactsMapPanel.tsx
index 979fc1a525..3bf4ab5a68 100644
--- a/src/components/Contacts/ContactsMap/ContactsMapPanel.tsx
+++ b/src/components/Contacts/ContactsMap/ContactsMapPanel.tsx
@@ -47,7 +47,7 @@ const StatusAccordion = styled(Accordion)(() => ({
const StatusHeader = styled(AccordionSummary)(() => ({
minHeight: '58px !important',
- boxShadow: `0px 0px 1px 1px ${theme.palette.cruGrayMedium.main}`,
+ boxShadow: `0px 0px 1px 1px ${theme.palette.mpdxGrayMedium.main}`,
'& .MuiAccordion-root.Mui-expanded': {
margin: 'auto',
},
@@ -66,16 +66,16 @@ const ContactWrapper = styled(Box, {
})(({ current }: { current: boolean }) => ({
display: 'flex',
justifyContent: 'space-between',
- borderBottom: `1px solid ${theme.palette.cruGrayMedium.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayMedium.main}`,
alignItems: 'center',
width: '100%',
padding: theme.spacing(1),
paddingLeft: theme.spacing(2),
backgroundColor: current
- ? theme.palette.cruGrayLight.main
+ ? theme.palette.mpdxGrayLight.main
: theme.palette.common.white,
'&:hover': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
cursor: 'pointer',
},
}));
diff --git a/src/components/Contacts/MassActions/Merge/MassActionsMergeModal.tsx b/src/components/Contacts/MassActions/Merge/MassActionsMergeModal.tsx
index 2bc8e4f30d..7ca193d788 100644
--- a/src/components/Contacts/MassActions/Merge/MassActionsMergeModal.tsx
+++ b/src/components/Contacts/MassActions/Merge/MassActionsMergeModal.tsx
@@ -141,7 +141,7 @@ export const MassActionsMergeModal: React.FC = ({
borderColor:
primaryContactId === contact.id
? theme.palette.mpdxGreen.main
- : theme.palette.cruGrayLight.main,
+ : theme.palette.mpdxGrayLight.main,
})}
data-testid="MassActionsMergeModalContact"
>
diff --git a/src/components/Contacts/MassActions/MergePeople/MergePeopleModal.tsx b/src/components/Contacts/MassActions/MergePeople/MergePeopleModal.tsx
index 9cd3b6f9f8..bf0e24d9f5 100644
--- a/src/components/Contacts/MassActions/MergePeople/MergePeopleModal.tsx
+++ b/src/components/Contacts/MassActions/MergePeople/MergePeopleModal.tsx
@@ -95,7 +95,7 @@ export const MergePeopleModal: React.FC = ({
borderColor:
winnerId === person.id
? theme.palette.mpdxGreen.main
- : theme.palette.cruGrayLight.main,
+ : theme.palette.mpdxGrayLight.main,
})}
data-testid="MergePeopleModalPerson"
>
diff --git a/src/components/Contacts/MassActions/RemoveTags/MassActionsRemoveTagsModal.tsx b/src/components/Contacts/MassActions/RemoveTags/MassActionsRemoveTagsModal.tsx
index 9b25e7f8e2..e11fc9e6ff 100644
--- a/src/components/Contacts/MassActions/RemoveTags/MassActionsRemoveTagsModal.tsx
+++ b/src/components/Contacts/MassActions/RemoveTags/MassActionsRemoveTagsModal.tsx
@@ -40,7 +40,7 @@ const ExistingTagButton = styled(Button)(() => ({
const SelectedTagButton = styled(Button)(() => ({
textTransform: 'none',
textDecoration: 'line-through',
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
width: 'fit-content',
'&:hover': {
textDecoration: 'line-through',
diff --git a/src/components/Dashboard/Dashboard.test.tsx b/src/components/Dashboard/Dashboard.test.tsx
index de12a92a2c..c01ed9258a 100644
--- a/src/components/Dashboard/Dashboard.test.tsx
+++ b/src/components/Dashboard/Dashboard.test.tsx
@@ -4,10 +4,6 @@ import { ThemeProvider } from '@mui/material/styles';
import { render, waitFor } from '@testing-library/react';
import { SnackbarProvider } from 'notistack';
import matchMediaMock from '__tests__/util/matchMediaMock';
-import {
- afterTestResizeObserver,
- beforeTestResizeObserver,
-} from '__tests__/util/windowResizeObserver';
import { GetDashboardQuery } from 'pages/accountLists/GetDashboard.generated';
import useTaskModal from '../../hooks/useTaskModal';
import theme from '../../theme';
@@ -33,6 +29,7 @@ beforeEach(() => {
const data: GetDashboardQuery = {
user: {
+ id: 'user-1',
firstName: 'Roger',
},
accountList: {
@@ -51,7 +48,8 @@ const data: GetDashboardQuery = {
periods: [
{
convertedTotal: 200,
- startDate: '2011-12-1',
+ startDate: '2011-12-01',
+ endDate: '2011-12-31',
totals: [
{
currency: 'USD',
@@ -61,7 +59,8 @@ const data: GetDashboardQuery = {
},
{
convertedTotal: 400,
- startDate: '2012-1-1',
+ startDate: '2012-01-01',
+ endDate: '2012-01-31',
totals: [
{
currency: 'USD',
@@ -71,7 +70,8 @@ const data: GetDashboardQuery = {
},
{
convertedTotal: 900,
- startDate: '2012-2-1',
+ startDate: '2012-02-01',
+ endDate: '2012-02-29',
totals: [
{
currency: 'USD',
@@ -93,7 +93,8 @@ const data: GetDashboardQuery = {
},
{
convertedTotal: 1100,
- startDate: '2012-3-1',
+ startDate: '2012-03-01',
+ endDate: '2012-03-31',
totals: [
{
currency: 'USD',
@@ -120,16 +121,12 @@ const data: GetDashboardQuery = {
],
averageIgnoreCurrent: 750,
},
+ healthIndicatorData: [],
};
describe('Dashboard', () => {
beforeEach(() => {
matchMediaMock({ width: '1024px' });
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
});
it('default', async () => {
@@ -206,9 +203,9 @@ describe('Dashboard', () => {
'$400',
);
expect(getByTestId('BalanceTypography').textContent).toEqual('$1,000');
- expect(
- queryByTestId('DonationHistoriesTypographyGoal'),
- ).not.toBeInTheDocument();
+ expect(getByTestId('DonationHistoriesTypographyGoal')).toHaveTextContent(
+ 'Goal',
+ );
expect(
getByTestId('DonationHistoriesTypographyAverage').textContent,
).toEqual('Average $750');
diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx
index a0abbdf054..99e0a87287 100644
--- a/src/components/Dashboard/Dashboard.tsx
+++ b/src/components/Dashboard/Dashboard.tsx
@@ -1,6 +1,8 @@
+import { useRouter } from 'next/router';
import React, { ReactElement } from 'react';
import { Box, Container, Grid } from '@mui/material';
import { motion } from 'framer-motion';
+import { DateTime } from 'luxon';
import { GetDashboardQuery } from 'pages/accountLists/GetDashboard.generated';
import Balance from './Balance';
import DonationHistories from './DonationHistories';
@@ -28,6 +30,17 @@ const variants = {
};
const Dashboard = ({ data, accountListId }: Props): ReactElement => {
+ const { push } = useRouter();
+
+ const handlePeriodClick = (period: DateTime) => {
+ push({
+ pathname: `/accountLists/${accountListId}/reports/donations`,
+ query: {
+ month: period.toISODate(),
+ },
+ });
+ };
+
return (
<>
@@ -39,15 +52,13 @@ const Dashboard = ({ data, accountListId }: Props): ReactElement => {
exit="exit"
variants={variants}
>
-
+
@@ -58,10 +69,8 @@ const Dashboard = ({ data, accountListId }: Props): ReactElement => {
diff --git a/src/components/Dashboard/DonationHistories/DonationHistories.test.tsx b/src/components/Dashboard/DonationHistories/DonationHistories.test.tsx
index aff7170cbe..8ec6c972e5 100644
--- a/src/components/Dashboard/DonationHistories/DonationHistories.test.tsx
+++ b/src/components/Dashboard/DonationHistories/DonationHistories.test.tsx
@@ -1,13 +1,43 @@
import React from 'react';
+import { ThemeProvider } from '@mui/material/styles';
import { render } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import {
+ CategoricalChartProps,
+ CategoricalChartState,
+} from 'recharts/types/chart/generateCategoricalChart.d';
import TestRouter from '__tests__/util/TestRouter';
+import theme from 'src/theme';
import {
- afterTestResizeObserver,
- beforeTestResizeObserver,
-} from '__tests__/util/windowResizeObserver';
+ DonationHistoriesData,
+ DonationHistoriesProps,
+} from './DonationHistories';
import DonationHistories from '.';
-const setTime = jest.fn();
+jest.mock('recharts', () => ({
+ ...jest.requireActual('recharts'),
+ ComposedChart: (props: CategoricalChartProps) => (
+ <>
+
+ props.onClick?.(
+ { activePayload: [{ payload: props.data![0] }] },
+ null,
+ )
+ }
+ >
+ Period 1
+
+
+ props.onClick?.(null as unknown as CategoricalChartState, null)
+ }
+ >
+ Outside Period
+
+ >
+ ),
+}));
const push = jest.fn();
@@ -17,98 +47,89 @@ const router = {
push,
};
-describe('DonationHistories', () => {
- let reportsDonationHistories: Parameters<
- typeof DonationHistories
- >[0]['reportsDonationHistories'];
+const donationsData: DonationHistoriesData = {
+ accountList: {
+ currency: 'USD',
+ monthlyGoal: 100,
+ totalPledges: 2500,
+ },
+ reportsDonationHistories: {
+ periods: [
+ {
+ convertedTotal: 50,
+ startDate: '2019-01-01',
+ endDate: '2019-01-31',
+ totals: [{ currency: 'USD', convertedAmount: 50 }],
+ },
+ {
+ convertedTotal: 60,
+ startDate: '2019-02-01',
+ endDate: '2019-02-31',
+ totals: [{ currency: 'NZD', convertedAmount: 60 }],
+ },
+ ],
+ averageIgnoreCurrent: 1000,
+ },
+ healthIndicatorData: [],
+};
+const TestComponent: React.FC = (props) => (
+
+
+
+
+
+);
+
+describe('DonationHistories', () => {
it('default', () => {
const { getByTestId, queryByTestId } = render(
-
- ,
- ,
+ ,
);
expect(getByTestId('DonationHistoriesBoxEmpty')).toBeInTheDocument();
- expect(
- queryByTestId('DonationHistoriesGridLoading'),
- ).not.toBeInTheDocument();
+ expect(queryByTestId('BarChartSkeleton')).not.toBeInTheDocument();
});
it('empty periods', () => {
- reportsDonationHistories = {
- periods: [
- {
- convertedTotal: 0,
- startDate: '1-1-2019',
- totals: [{ currency: 'USD', convertedAmount: 0 }],
- },
- {
- convertedTotal: 0,
- startDate: '1-2-2019',
- totals: [{ currency: 'NZD', convertedAmount: 0 }],
- },
- ],
- averageIgnoreCurrent: 0,
+ const data: DonationHistoriesData = {
+ ...donationsData,
+ reportsDonationHistories: {
+ periods: [
+ {
+ convertedTotal: 0,
+ startDate: '2019-01-01',
+ endDate: '2019-01-31',
+ totals: [{ currency: 'USD', convertedAmount: 0 }],
+ },
+ {
+ convertedTotal: 0,
+ startDate: '2019-02-01',
+ endDate: '2019-02-28',
+ totals: [{ currency: 'NZD', convertedAmount: 0 }],
+ },
+ ],
+ averageIgnoreCurrent: 0,
+ },
};
+
const { getByTestId, queryByTestId } = render(
-
-
- ,
+ ,
);
expect(getByTestId('DonationHistoriesBoxEmpty')).toBeInTheDocument();
- expect(
- queryByTestId('DonationHistoriesGridLoading'),
- ).not.toBeInTheDocument();
+ expect(queryByTestId('BarChartSkeleton')).not.toBeInTheDocument();
});
it('loading', () => {
- const { getByTestId, queryByTestId } = render(
-
-
- ,
+ const { getAllByTestId, queryByTestId } = render(
+ ,
);
- expect(getByTestId('DonationHistoriesGridLoading')).toBeInTheDocument();
+ expect(getAllByTestId('BarChartSkeleton')).toHaveLength(2);
expect(queryByTestId('DonationHistoriesBoxEmpty')).not.toBeInTheDocument();
});
describe('populated periods', () => {
- beforeEach(() => {
- reportsDonationHistories = {
- periods: [
- {
- convertedTotal: 50,
- startDate: '1-1-2019',
- totals: [{ currency: 'USD', convertedAmount: 50 }],
- },
- {
- convertedTotal: 60,
- startDate: '1-2-2019',
- totals: [{ currency: 'NZD', convertedAmount: 60 }],
- },
- ],
- averageIgnoreCurrent: 1000,
- };
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
- });
-
it('shows references', () => {
- const { getByTestId } = render(
-
-
- ,
- );
+ const { getByTestId } = render( );
expect(
getByTestId('DonationHistoriesTypographyGoal').textContent,
).toEqual('Goal $100');
@@ -120,4 +141,35 @@ describe('DonationHistories', () => {
).toEqual('Committed $2,500');
});
});
+
+ describe('onPeriodClick', () => {
+ it('is called when a period is clicked', () => {
+ const handlePeriodClick = jest.fn();
+ const { getByRole } = render(
+ ,
+ );
+
+ userEvent.click(getByRole('button', { name: 'Period 1' }));
+ expect(handlePeriodClick).toHaveBeenCalledTimes(1);
+ expect(handlePeriodClick.mock.calls[0][0].toISODate()).toBe(
+ donationsData.reportsDonationHistories.periods[0].startDate,
+ );
+ });
+
+ it('is not called when the chart is clicked', () => {
+ const handlePeriodClick = jest.fn();
+ const { getByRole } = render(
+ ,
+ );
+
+ userEvent.click(getByRole('button', { name: 'Outside Period' }));
+ expect(handlePeriodClick).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/components/Dashboard/DonationHistories/DonationHistories.tsx b/src/components/Dashboard/DonationHistories/DonationHistories.tsx
index 22a7989547..19d1152a81 100644
--- a/src/components/Dashboard/DonationHistories/DonationHistories.tsx
+++ b/src/components/Dashboard/DonationHistories/DonationHistories.tsx
@@ -1,4 +1,3 @@
-import { useRouter } from 'next/router';
import React, { ReactElement } from 'react';
import {
Box,
@@ -8,14 +7,15 @@ import {
Skeleton,
Theme,
Typography,
+ useTheme,
} from '@mui/material';
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import {
Bar,
- BarChart,
CartesianGrid,
Legend,
+ Line,
ReferenceLine,
ResponsiveContainer,
Text,
@@ -25,34 +25,25 @@ import {
} from 'recharts';
import { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart.d';
import { makeStyles } from 'tss-react/mui';
-import { useAccountListId } from 'src/hooks/useAccountListId';
+import { BarChartSkeleton } from 'src/components/common/BarChartSkeleton/BarChartSkeleton';
+import { LegendReferenceLine } from 'src/components/common/LegendReferenceLine/LegendReferenceLine';
+import {
+ StyledBarChart,
+ StyledComposedChart,
+} from 'src/components/common/StyledBarChart/StyledBarChart';
+import * as Types from 'src/graphql/types.generated';
import { useLocale } from 'src/hooks/useLocale';
+import { GoalSource, getHealthIndicatorInfo } from 'src/lib/healthIndicator';
import illustration15 from '../../../images/drawkit/grape/drawkit-grape-pack-illustration-15.svg';
import { currencyFormat } from '../../../lib/intlFormat';
import AnimatedBox from '../../AnimatedBox';
import AnimatedCard from '../../AnimatedCard';
+import { calculateGraphData } from './graphData';
const useStyles = makeStyles()((theme: Theme) => ({
cardHeader: {
textAlign: 'center',
},
- lineKey: {
- display: 'inline-block',
- height: '5px',
- width: '20px',
- marginRight: '10px',
- marginBottom: '4px',
- borderRadius: '5px',
- },
- lineKeyGoal: {
- backgroundColor: '#17AEBF',
- },
- lineKeyAverage: {
- backgroundColor: '#9C9FA1',
- },
- lineKeyPledged: {
- backgroundColor: '#FFCF07',
- },
boxImg: {
display: 'flex',
flex: 1,
@@ -72,84 +63,87 @@ const useStyles = makeStyles()((theme: Theme) => ({
},
}));
-interface Props {
- loading?: boolean;
- reportsDonationHistories?: {
- periods: {
- convertedTotal: number;
- startDate: string;
- totals: { currency: string; convertedAmount: number }[];
- }[];
- averageIgnoreCurrent: number;
+export interface DonationHistoriesData {
+ accountList: Pick<
+ Types.AccountList,
+ 'currency' | 'monthlyGoal' | 'totalPledges'
+ >;
+ reportsDonationHistories: Pick<
+ Types.DonationHistories,
+ 'averageIgnoreCurrent'
+ > & {
+ periods: Array<
+ Pick<
+ Types.DonationHistoriesPeriod,
+ 'startDate' | 'endDate' | 'convertedTotal'
+ > & {
+ totals: Array>;
+ }
+ >;
};
- currencyCode?: string;
- goal?: number;
- pledged?: number;
- setTime?: (time: DateTime) => void;
+ healthIndicatorData: Array<
+ Pick<
+ Types.HealthIndicatorData,
+ | 'indicationPeriodBegin'
+ | 'machineCalculatedGoal'
+ | 'machineCalculatedGoalCurrency'
+ | 'staffEnteredGoal'
+ >
+ >;
+}
+
+export interface DonationHistoriesProps {
+ loading?: boolean;
+ data: DonationHistoriesData | undefined;
+ onPeriodClick?: (period: DateTime) => void;
}
const DonationHistories = ({
loading,
- reportsDonationHistories,
- goal,
- pledged,
- currencyCode = 'USD',
- setTime,
-}: Props): ReactElement => {
+ data,
+ onPeriodClick,
+}: DonationHistoriesProps): ReactElement => {
const { classes } = useStyles();
- const { push } = useRouter();
+ const { palette } = useTheme();
const { t } = useTranslation();
const locale = useLocale();
- const accountListId = useAccountListId();
- const fills = ['#FFCF07', '#30F2F2', '#1FC0D2', '#007398'];
- const currencies: { dataKey: string; fill: string }[] = [];
- const periods = reportsDonationHistories?.periods?.map((period) => {
- const data: {
- [key: string]: string | number | DateTime;
- startDate: string;
- total: number;
- period: DateTime;
- } = {
- startDate: DateTime.fromISO(period.startDate)
- .toJSDate()
- .toLocaleDateString(locale, { month: 'short', year: '2-digit' }),
- total: period.convertedTotal,
- period: DateTime.fromISO(period.startDate),
- };
- period.totals.forEach((total) => {
- if (!currencies.find((currency) => total.currency === currency.dataKey)) {
- currencies.push({ dataKey: total.currency, fill: fills.pop() ?? '' });
- }
- data[total.currency] = total.convertedAmount;
- });
- return data;
- });
- const empty =
- !loading &&
- (periods === undefined ||
- periods.reduce((result, { total }) => result + total, 0) === 0);
- const domainMax = Math.max(
- ...(periods?.map((period) => period.total) || []),
- goal ?? 0,
- pledged ?? 0,
- reportsDonationHistories?.averageIgnoreCurrent ?? 0,
+ const fills = [
+ palette.cyan.main,
+ palette.pink.main,
+ palette.green.main,
+ palette.orange.main,
+ ];
+ const goalColor = palette.graphite.main;
+ const averageColor = palette.graphite.main;
+ const pledgedColor = palette.yellow.main;
+ const goalLineStyles = {
+ stroke: goalColor,
+ strokeDasharray: '5,8',
+ strokeLinecap: 'round' as const,
+ strokeWidth: 3,
+ };
+
+ const { totalPledges: pledged, currency } = data?.accountList ?? {};
+ const { goal, goalSource } = getHealthIndicatorInfo(
+ data?.accountList,
+ data?.healthIndicatorData.at(-1),
);
- const handleClick: CategoricalChartFunc = (period) => {
- if (!period?.activePayload) {
+ const {
+ periods,
+ currencies,
+ empty: periodsEmpty,
+ domainMax,
+ } = calculateGraphData({ locale, data, currencyColors: fills });
+ const empty = !loading && periodsEmpty;
+
+ const handleClick: CategoricalChartFunc = (state) => {
+ if (!state?.activePayload) {
// The click was inside the chart but wasn't on a period
return;
}
- if (setTime) {
- setTime(period.activePayload[0].payload.period);
- } else {
- push({
- pathname: `/accountLists/${accountListId}/reports/donations`,
- query: {
- month: period.activePayload[0].payload.period.toISO(),
- },
- });
- }
+
+ onPeriodClick?.(state.activePayload[0].payload.period);
};
return (
@@ -166,72 +160,48 @@ const DonationHistories = ({
className={classes.cardHeader}
title={
- {goal ? (
- <>
-
-
-
- {t('Goal')} {' '}
- {currencyFormat(goal, currencyCode, locale)}
-
-
- |
- >
- ) : null}
-
-
+
-
- {t('Average')} {' '}
- {loading || !reportsDonationHistories ? (
-
- ) : (
- currencyFormat(
- reportsDonationHistories.averageIgnoreCurrent,
- currencyCode,
- locale,
+
+ |
+
+
+ ) : (
+ currencyFormat(
+ data.reportsDonationHistories.averageIgnoreCurrent,
+ currency,
+ locale,
+ )
)
- )}
-
+ }
+ color={averageColor}
+ />
{pledged ? (
<>
|
-
-
+
-
- {t('Committed')} {' '}
- {currencyFormat(pledged, currencyCode, locale)}
-
>
) : null}
@@ -256,30 +226,12 @@ const DonationHistories = ({
) : (
<>
-
+
{loading ? (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
) : (
-
-
+
- {goal && (
+ {goalSource === GoalSource.Preferences ? (
+ ) : (
+
)}
{pledged && (
)}
@@ -320,8 +279,8 @@ const DonationHistories = ({
offset={0}
angle={-90}
>
- {t('Amount ({{ currencyCode }})', {
- currencyCode,
+ {t('Amount ({{ currency }})', {
+ currency,
})}
}
@@ -329,44 +288,28 @@ const DonationHistories = ({
{currencies.map((currency) => (
))}
-
+
)}
-
+
{loading ? (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
) : (
-
-
+
+
-
-
+
+
)}
diff --git a/src/components/Dashboard/DonationHistories/graphData.test.ts b/src/components/Dashboard/DonationHistories/graphData.test.ts
new file mode 100644
index 0000000000..f86e8387b8
--- /dev/null
+++ b/src/components/Dashboard/DonationHistories/graphData.test.ts
@@ -0,0 +1,350 @@
+import { Settings } from 'luxon';
+import { gqlMock } from '__tests__/util/graphqlMocking';
+import {
+ GetDonationGraphDocument,
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables,
+} from 'src/components/Reports/DonationsReport/GetDonationGraph.generated';
+import { calculateGraphData } from './graphData';
+
+const variables: GetDonationGraphQueryVariables = {
+ accountListId: 'account-list-1',
+ periodBegin: '2020-12-01',
+ designationAccountIds: [],
+};
+
+const graphOptions = {
+ locale: 'en-US',
+ currencyColors: ['red', 'green', 'blue'],
+};
+
+describe('calculateGraphData', () => {
+ beforeEach(() => {
+ Settings.now = () => new Date(2020, 11, 1).valueOf();
+ });
+
+ describe('periods', () => {
+ it('is the period data for the graph', () => {
+ const data = gqlMock<
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables
+ >(GetDonationGraphDocument, {
+ mocks: {
+ accountList: {
+ monthlyGoal: 100,
+ },
+ reportsDonationHistories: {
+ periods: [
+ {
+ convertedTotal: 30,
+ startDate: '2020-11-01',
+ endDate: '2020-11-30',
+ totals: [
+ { currency: 'USD', convertedAmount: 10 },
+ { currency: 'EUR', convertedAmount: 20 },
+ ],
+ },
+ {
+ convertedTotal: 40,
+ startDate: '2020-12-01',
+ endDate: '2020-12-31',
+ totals: [
+ { currency: 'EUR', convertedAmount: 15 },
+ { currency: 'CAD', convertedAmount: 15 },
+ ],
+ },
+ ],
+ },
+ healthIndicatorData: [
+ { indicationPeriodBegin: '2020-11-01', staffEnteredGoal: 60 },
+ { indicationPeriodBegin: '2020-12-01', staffEnteredGoal: 200 },
+ ],
+ },
+ variables,
+ });
+
+ expect(
+ calculateGraphData({ ...graphOptions, data }).periods,
+ ).toMatchObject([
+ {
+ currencies: { USD: 10, EUR: 20 },
+ goal: 60,
+ startDate: 'Nov 20',
+ total: 30,
+ },
+ {
+ currencies: { EUR: 15, CAD: 15 },
+ goal: 100,
+ startDate: 'Dec 20',
+ total: 40,
+ },
+ ]);
+ });
+
+ it('uses the machine-calculated goal when the staff entered goal is missing', () => {
+ const data = gqlMock<
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables
+ >(GetDonationGraphDocument, {
+ mocks: {
+ accountList: {
+ currency: 'USD',
+ monthlyGoal: null,
+ },
+ reportsDonationHistories: {
+ periods: [
+ { startDate: '2020-08-01', endDate: '2020-08-31' },
+ { startDate: '2020-09-01', endDate: '2020-09-30' },
+ { startDate: '2020-10-01', endDate: '2020-10-31' },
+ { startDate: '2020-11-01', endDate: '2020-11-30' },
+ { startDate: '2020-12-01', endDate: '2020-12-31' },
+ ],
+ },
+ healthIndicatorData: [
+ {
+ indicationPeriodBegin: '2020-09-01',
+ staffEnteredGoal: null,
+ machineCalculatedGoal: null,
+ },
+ {
+ indicationPeriodBegin: '2020-10-01',
+ staffEnteredGoal: null,
+ machineCalculatedGoal: 100,
+ machineCalculatedGoalCurrency: 'EUR',
+ },
+ {
+ indicationPeriodBegin: '2020-11-01',
+ staffEnteredGoal: null,
+ machineCalculatedGoal: 100,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ {
+ indicationPeriodBegin: '2020-12-01',
+ staffEnteredGoal: 200,
+ machineCalculatedGoal: 100,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ ],
+ },
+ variables,
+ });
+
+ expect(
+ calculateGraphData({ ...graphOptions, data }).periods,
+ ).toMatchObject([
+ { goal: null, startDate: 'Aug 20' }, // no health indicator period
+ { goal: null, startDate: 'Sep 20' }, // no staff-entered or estimated goal
+ { goal: null, startDate: 'Oct 20' }, // ignoring estimated goal because currency doesn't match
+ { goal: 100, startDate: 'Nov 20' }, // using estimated goal because no staff-entered goal available
+ { goal: 200, startDate: 'Dec 20' }, // using staff-entered goal
+ ]);
+ });
+
+ it('extrapolates missing health indicator periods', () => {
+ const data = gqlMock<
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables
+ >(GetDonationGraphDocument, {
+ mocks: {
+ accountList: {
+ currency: 'USD',
+ monthlyGoal: null,
+ },
+ reportsDonationHistories: {
+ periods: [
+ { startDate: '2020-07-01', endDate: '2020-07-31' },
+ { startDate: '2020-08-01', endDate: '2020-08-31' },
+ { startDate: '2020-09-01', endDate: '2020-09-30' },
+ { startDate: '2020-10-01', endDate: '2020-10-31' },
+ { startDate: '2020-11-01', endDate: '2020-11-30' },
+ { startDate: '2020-12-01', endDate: '2020-12-31' },
+ ],
+ },
+ // August, September, and November are missing
+ // December has two periods
+ healthIndicatorData: [
+ {
+ indicationPeriodBegin: '2020-07-01',
+ staffEnteredGoal: 200,
+ machineCalculatedGoal: 100,
+ },
+ {
+ indicationPeriodBegin: '2020-10-01',
+ staffEnteredGoal: null,
+ machineCalculatedGoal: 110,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ {
+ indicationPeriodBegin: '2020-12-01',
+ staffEnteredGoal: 220,
+ machineCalculatedGoal: 120,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ {
+ indicationPeriodBegin: '2020-12-02',
+ staffEnteredGoal: 240,
+ machineCalculatedGoal: 120,
+ machineCalculatedGoalCurrency: 'USD',
+ },
+ ],
+ },
+ variables,
+ });
+
+ expect(
+ calculateGraphData({ ...graphOptions, data }).periods,
+ ).toMatchObject([
+ { goal: 200, startDate: 'Jul 20' },
+ { goal: 200, startDate: 'Aug 20' }, // extrapolated from July
+ { goal: 200, startDate: 'Sep 20' }, // extrapolated from July
+ { goal: 110, startDate: 'Oct 20' },
+ { goal: 110, startDate: 'Nov 20' }, // extrapolated from October
+ { goal: 240, startDate: 'Dec 20' }, // uses latest December period
+ ]);
+ });
+ });
+
+ describe('empty', () => {
+ it('is true when data is undefined', () => {
+ expect(
+ calculateGraphData({ ...graphOptions, data: undefined }).empty,
+ ).toBe(true);
+ });
+
+ it('is true when periods are empty', () => {
+ const data = gqlMock<
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables
+ >(GetDonationGraphDocument, {
+ mocks: {
+ reportsDonationHistories: {
+ periods: [],
+ },
+ },
+ variables,
+ });
+ expect(calculateGraphData({ ...graphOptions, data }).empty).toBe(true);
+ });
+
+ it('is true when periods are all zeros', () => {
+ const data = gqlMock<
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables
+ >(GetDonationGraphDocument, {
+ mocks: {
+ reportsDonationHistories: {
+ periods: [{ convertedTotal: 0 }, { convertedTotal: 0 }],
+ },
+ },
+ variables,
+ });
+ expect(calculateGraphData({ ...graphOptions, data }).empty).toBe(true);
+ });
+
+ it('is false when a period has a non-zero total', () => {
+ const data = gqlMock<
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables
+ >(GetDonationGraphDocument, {
+ mocks: {
+ reportsDonationHistories: {
+ periods: [{ convertedTotal: 0 }, { convertedTotal: 1 }],
+ },
+ },
+ variables,
+ });
+ expect(calculateGraphData({ ...graphOptions, data }).empty).toBe(false);
+ });
+ });
+
+ describe('domainMax', () => {
+ const data = gqlMock(
+ GetDonationGraphDocument,
+ {
+ mocks: {
+ accountList: {
+ monthlyGoal: 10,
+ totalPledges: 20,
+ },
+ reportsDonationHistories: {
+ periods: [
+ {
+ startDate: '2020-11-01',
+ endDate: '2020-11-30',
+ convertedTotal: 30,
+ },
+ {
+ startDate: '2020-12-01',
+ endDate: '2020-12-31',
+ convertedTotal: 40,
+ },
+ ],
+ averageIgnoreCurrent: 50,
+ },
+ healthIndicatorData: [
+ { indicationPeriodBegin: '2020-11-01', staffEnteredGoal: 60 },
+ { indicationPeriodBegin: '2020-12-01', staffEnteredGoal: 200 },
+ ],
+ },
+ variables,
+ },
+ );
+
+ it('is zero when data is undefined', () => {
+ data.healthIndicatorData[0].staffEnteredGoal = 100;
+
+ expect(
+ calculateGraphData({ ...graphOptions, data: undefined }).domainMax,
+ ).toBe(0);
+ });
+
+ it('is the period with the greatest total', () => {
+ data.reportsDonationHistories.periods[0].convertedTotal = 100;
+
+ expect(calculateGraphData({ ...graphOptions, data }).domainMax).toBe(100);
+ });
+
+ it('is the period with the greatest goal', () => {
+ expect(calculateGraphData({ ...graphOptions, data }).domainMax).toBe(100);
+ });
+
+ it('is the monthly goal', () => {
+ data.accountList.monthlyGoal = 100;
+
+ expect(calculateGraphData({ ...graphOptions, data }).domainMax).toBe(100);
+ });
+
+ it('is the total pledges', () => {
+ data.accountList.totalPledges = 100;
+
+ expect(calculateGraphData({ ...graphOptions, data }).domainMax).toBe(100);
+ });
+ });
+
+ describe('currencies', () => {
+ it('is the currencies used by all periods', () => {
+ const data = gqlMock<
+ GetDonationGraphQuery,
+ GetDonationGraphQueryVariables
+ >(GetDonationGraphDocument, {
+ mocks: {
+ reportsDonationHistories: {
+ periods: [
+ { totals: [{ currency: 'USD' }, { currency: 'EUR' }] },
+ { totals: [{ currency: 'EUR' }, { currency: 'CAD' }] },
+ { totals: [{ currency: 'MXN' }, { currency: 'CAD' }] },
+ ],
+ },
+ },
+ variables,
+ });
+
+ expect(calculateGraphData({ ...graphOptions, data }).currencies).toEqual([
+ { fill: 'red', name: 'USD' },
+ { fill: 'green', name: 'EUR' },
+ { fill: 'blue', name: 'CAD' },
+ { fill: 'red', name: 'MXN' },
+ ]);
+ });
+ });
+});
diff --git a/src/components/Dashboard/DonationHistories/graphData.ts b/src/components/Dashboard/DonationHistories/graphData.ts
new file mode 100644
index 0000000000..c1a2d5c340
--- /dev/null
+++ b/src/components/Dashboard/DonationHistories/graphData.ts
@@ -0,0 +1,101 @@
+import { DateTime } from 'luxon';
+import { getHealthIndicatorInfo } from 'src/lib/healthIndicator';
+import { monthYearFormat } from 'src/lib/intlFormat';
+import { DonationHistoriesData } from './DonationHistories';
+
+export interface CalculateGraphDataOptions {
+ locale: string;
+ data: DonationHistoriesData | undefined;
+ currencyColors: string[];
+}
+
+export interface Period {
+ currencies: Record;
+ startDate: string;
+ total: number;
+ goal: number | null;
+ period: DateTime;
+}
+
+export interface CurrencyBar {
+ name: string;
+ fill: string;
+}
+
+export interface CalculateGraphDataResult {
+ periods: Period[] | undefined;
+ currencies: CurrencyBar[];
+ empty: boolean;
+ domainMax: number;
+}
+
+export const calculateGraphData = ({
+ locale,
+ data,
+ currencyColors,
+}: CalculateGraphDataOptions): CalculateGraphDataResult => {
+ const pledged = data?.accountList?.totalPledges;
+ const { healthIndicatorData, reportsDonationHistories } = data ?? {};
+ const currentMonth = DateTime.now();
+
+ const currencies: CurrencyBar[] = [];
+ const periods = reportsDonationHistories?.periods?.map((period) => {
+ const startDate = DateTime.fromISO(period.startDate);
+
+ // Look up the latest health indicator period in or before the current report period, without
+ // going over. This handles potentially missing periods because health indicator data is not
+ // guaranteed to be available for every day. Because health indicator periods are sorted
+ // in ascending order, if e.g. March has no health indicator data, February will be used
+ // instead. If there are multiple health indicator periods in a report period, the latest one
+ // will be selected.
+ const hiPeriod = healthIndicatorData?.findLast(
+ (item) => item.indicationPeriodBegin <= period.endDate,
+ );
+
+ const { machineCalculatedGoal, preferencesGoal } = getHealthIndicatorInfo(
+ data?.accountList,
+ hiPeriod,
+ );
+
+ const periodGoal =
+ // In the current month, give the goal from preferences the highest precedence
+ (startDate.hasSame(currentMonth, 'month') ? preferencesGoal : null) ??
+ // Fall back to the staff-entered goal if the preferences goal is unavailable or it is not the current month
+ hiPeriod?.staffEnteredGoal ??
+ // Finally, fall back to the machine-calculated goal as a last resort
+ machineCalculatedGoal;
+
+ const periodData: Period = {
+ currencies: {},
+ startDate: monthYearFormat(startDate, locale, false),
+ total: period.convertedTotal,
+ goal: periodGoal ?? null,
+ period: startDate,
+ };
+ period.totals.forEach((total) => {
+ if (!currencies.find((currency) => total.currency === currency.name)) {
+ currencies.push({
+ name: total.currency,
+ fill: currencyColors[currencies.length % currencyColors.length],
+ });
+ }
+ periodData.currencies[total.currency] = total.convertedAmount;
+ });
+ return periodData;
+ });
+
+ const empty =
+ (periods ?? []).reduce((result, { total }) => result + total, 0) === 0;
+
+ const domainMax = Math.max(
+ ...(periods ?? [])?.flatMap((period) => [
+ period.total,
+ // Include the goal if it is present
+ ...(period.goal === null ? [] : [period.goal]),
+ ]),
+ pledged ?? 0,
+ reportsDonationHistories?.averageIgnoreCurrent ?? 0,
+ );
+
+ return { periods, currencies, empty, domainMax };
+};
diff --git a/src/components/Dashboard/MonthlyGoal/HealthIndicator.graphql b/src/components/Dashboard/MonthlyGoal/HealthIndicator.graphql
new file mode 100644
index 0000000000..c8059b1489
--- /dev/null
+++ b/src/components/Dashboard/MonthlyGoal/HealthIndicator.graphql
@@ -0,0 +1,15 @@
+query HealthIndicator($accountListId: ID!) {
+ accountList(id: $accountListId) {
+ id
+ healthIndicatorData {
+ id
+ overallHi
+ ownershipHi
+ consistencyHi
+ successHi
+ depthHi
+ machineCalculatedGoal
+ machineCalculatedGoalCurrency
+ }
+ }
+}
diff --git a/src/components/Dashboard/MonthlyGoal/MonthlyGoal.test.tsx b/src/components/Dashboard/MonthlyGoal/MonthlyGoal.test.tsx
index ca2f2c32b0..8671bcc3dc 100644
--- a/src/components/Dashboard/MonthlyGoal/MonthlyGoal.test.tsx
+++ b/src/components/Dashboard/MonthlyGoal/MonthlyGoal.test.tsx
@@ -1,17 +1,73 @@
import React from 'react';
-import { render } from '@testing-library/react';
+import { ThemeProvider } from '@mui/material/styles';
+import { render, waitFor } from '@testing-library/react';
+import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import matchMediaMock from '__tests__/util/matchMediaMock';
-import MonthlyGoal from './MonthlyGoal';
+import theme from 'src/theme';
+import { HealthIndicatorQuery } from './HealthIndicator.generated';
+import MonthlyGoal, { MonthlyGoalProps } from './MonthlyGoal';
+
+const accountListId = 'account-list-1';
+
+const mutationSpy = jest.fn();
+interface ComponentsProps {
+ healthIndicatorData?: Partial<
+ HealthIndicatorQuery['accountList']['healthIndicatorData']
+ >;
+ accountList?: Partial | null;
+}
+
+const Components = ({
+ healthIndicatorData = null,
+ accountList,
+}: ComponentsProps) => (
+
+
+ mocks={{
+ HealthIndicator: {
+ accountList: {
+ healthIndicatorData:
+ healthIndicatorData === null
+ ? null
+ : {
+ machineCalculatedGoalCurrency: 'USD',
+ ...healthIndicatorData,
+ },
+ },
+ },
+ }}
+ onCall={mutationSpy}
+ >
+
+
+
+);
describe('MonthlyGoal', () => {
beforeEach(() => {
matchMediaMock({ width: '1024px' });
});
- it('default', () => {
+ it('zeros', () => {
const { getByTestId, queryByTestId } = render(
- ,
+ ,
);
+
expect(
queryByTestId('MonthlyGoalTypographyGoalMobile'),
).not.toBeInTheDocument();
@@ -43,9 +99,7 @@ describe('MonthlyGoal', () => {
});
it('loading', () => {
- const { getByTestId } = render(
- ,
- );
+ const { getByTestId } = render( );
expect(
getByTestId('MonthlyGoalTypographyGoal').children[0].className,
).toContain('MuiSkeleton-root');
@@ -74,13 +128,7 @@ describe('MonthlyGoal', () => {
it('props', () => {
const { getByTestId, queryByTestId } = render(
- ,
+ ,
);
expect(getByTestId('MonthlyGoalTypographyGoal').textContent).toEqual(
'€999.50',
@@ -113,12 +161,12 @@ describe('MonthlyGoal', () => {
it('props above goal', () => {
const { getByTestId, queryByTestId } = render(
- ,
);
expect(
@@ -148,7 +196,7 @@ describe('MonthlyGoal', () => {
it('default', () => {
const { getByTestId, queryByTestId } = render(
- ,
+ ,
);
expect(
getByTestId('MonthlyGoalTypographyGoalMobile').textContent,
@@ -166,17 +214,170 @@ describe('MonthlyGoal', () => {
it('props', () => {
const { getByTestId } = render(
- ,
+ ,
);
expect(
getByTestId('MonthlyGoalTypographyGoalMobile').textContent,
).toEqual('€999.50');
});
});
+
+ describe('HealthIndicator', () => {
+ it('does not render HI widget if no data', async () => {
+ const { queryByText } = render( );
+
+ await waitFor(() => {
+ expect(mutationSpy).toHaveGraphqlOperation('HealthIndicator');
+ });
+ expect(queryByText('MPD Health Indicator')).not.toBeInTheDocument();
+ });
+
+ it('does not change Grid styles if no data', async () => {
+ const { getByTestId } = render( );
+
+ await waitFor(() => {
+ expect(mutationSpy).toHaveGraphqlOperation('HealthIndicator');
+ });
+ expect(getByTestId('containerGrid')).not.toHaveClass(
+ 'MuiGrid-spacing-xs-2',
+ );
+ expect(getByTestId('goalGrid')).not.toHaveClass('MuiGrid-grid-xs-6');
+ });
+
+ it('should show the health indicator and change Grid styles', async () => {
+ const { getByTestId, getByText } = render(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(mutationSpy).toHaveGraphqlOperation('HealthIndicator');
+ });
+ expect(getByText('MPD Health Indicator')).toBeInTheDocument();
+ expect(getByTestId('containerGrid')).toHaveClass('MuiGrid-spacing-xs-2');
+ expect(getByTestId('goalGrid')).toHaveClass('MuiGrid-grid-xs-6');
+ });
+ });
+
+ describe('Monthly Goal', () => {
+ it('should set the monthly goal to the user-entered goal if it exists', async () => {
+ const { findByLabelText, findByRole, queryByRole } = render(
+ ,
+ );
+
+ expect(
+ await findByLabelText(
+ /^Your current goal of \$999.50 is staff-entered/,
+ ),
+ ).not.toHaveStyle('color: rgb(169, 68, 66)');
+
+ expect(
+ await findByRole('heading', { name: '$999.50' }),
+ ).toBeInTheDocument();
+
+ expect(
+ queryByRole('link', { name: 'Set Monthly Goal' }),
+ ).not.toBeInTheDocument();
+ });
+
+ describe('updated date', () => {
+ it('is the date that the monthly goal was updated', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Last updated Jan 1, 2024')).toBeInTheDocument();
+ });
+
+ it('is hidden if the goal is missing', () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('Last updated Jan 1, 2024')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('below machine-calculated warning', () => {
+ it('is shown if goal is less than the machine-calculated goal', async () => {
+ const { findByText } = render(
+ ,
+ );
+
+ expect(
+ await findByText('Below NetSuite-calculated goal'),
+ ).toBeInTheDocument();
+ });
+
+ it('is hidden if goal is greater than or equal to the machine-calculated goal', async () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(
+ queryByText('Below NetSuite-calculated goal'),
+ ).not.toBeInTheDocument(),
+ );
+ });
+ });
+
+ it('should set the monthly goal to the machine-calculated goal', async () => {
+ const { findByRole, getByRole, queryByRole, queryByText } = render(
+ ,
+ );
+
+ expect(
+ await findByRole('heading', { name: '$7,000' }),
+ ).toBeInTheDocument();
+
+ expect(
+ queryByRole('heading', { name: '$999.50' }),
+ ).not.toBeInTheDocument();
+
+ expect(queryByText('Last updated Jan 1, 2024')).not.toBeInTheDocument();
+
+ expect(getByRole('link', { name: 'Set Monthly Goal' })).toHaveAttribute(
+ 'href',
+ '/accountLists/account-list-1/settings/preferences?selectedTab=MonthlyGoal',
+ );
+ });
+
+ it('should set the monthly goal to 0 if both the machineCalculatedGoal and monthly goal are unset', async () => {
+ const { findByRole, queryByRole } = render(
+ ,
+ );
+
+ expect(await findByRole('heading', { name: '$0' })).toBeInTheDocument();
+
+ expect(
+ queryByRole('heading', { name: '$7,000' }),
+ ).not.toBeInTheDocument();
+
+ expect(
+ queryByRole('heading', { name: '$999.50' }),
+ ).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx b/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx
index ffe4040c34..d8089428a3 100644
--- a/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx
+++ b/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx
@@ -1,4 +1,5 @@
-import React, { ReactElement } from 'react';
+import NextLink from 'next/link';
+import React, { ReactElement, useId, useMemo } from 'react';
import {
Box,
Button,
@@ -7,23 +8,30 @@ import {
Hidden,
Skeleton,
Theme,
+ Tooltip,
Typography,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
+import { HealthIndicatorWidget } from 'src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget';
+import { PreferenceAccordion } from 'src/components/Shared/Forms/Accordions/AccordionEnum';
import {
+ AccountList,
ContactFilterPledgeReceivedEnum,
StatusEnum,
} from 'src/graphql/types.generated';
import { useLocale } from 'src/hooks/useLocale';
+import { GoalSource, getHealthIndicatorInfo } from 'src/lib/healthIndicator';
import {
currencyFormat,
+ dateFormat,
numberFormat,
percentageFormat,
} from '../../../lib/intlFormat';
import AnimatedBox from '../../AnimatedBox';
import AnimatedCard from '../../AnimatedCard';
import StyledProgress from '../../StyledProgress';
+import { useHealthIndicatorQuery } from './HealthIndicator.generated';
const useStyles = makeStyles()((_theme: Theme) => ({
received: {
@@ -45,32 +53,122 @@ const useStyles = makeStyles()((_theme: Theme) => ({
},
}));
-interface Props {
+interface Annotation {
+ label: string;
+ warning: boolean;
+}
+
+export interface MonthlyGoalProps {
accountListId: string;
- loading?: boolean;
- goal?: number;
- received?: number;
- pledged?: number;
+ accountList: Pick<
+ AccountList,
+ | 'currency'
+ | 'monthlyGoal'
+ | 'monthlyGoalUpdatedAt'
+ | 'receivedPledges'
+ | 'totalPledges'
+ > | null;
totalGiftsNotStarted?: number;
- currencyCode?: string;
+ onDashboard?: boolean;
}
const MonthlyGoal = ({
accountListId,
- loading,
- goal = 0,
- received = 0,
- pledged = 0,
+ accountList,
totalGiftsNotStarted,
- currencyCode = 'USD',
-}: Props): ReactElement => {
- const { classes } = useStyles();
+ onDashboard = false,
+}: MonthlyGoalProps): ReactElement => {
const { t } = useTranslation();
+ const { classes } = useStyles();
const locale = useLocale();
- const receivedPercentage = received / goal;
- const pledgedPercentage = pledged / goal;
- const belowGoal = goal - pledged;
- const belowGoalPercentage = belowGoal / goal;
+
+ const loading = accountList === null;
+ const {
+ receivedPledges: received = 0,
+ totalPledges: pledged = 0,
+ currency,
+ } = accountList ?? {};
+
+ const { data, loading: healthIndicatorLoading } = useHealthIndicatorQuery({
+ variables: {
+ accountListId,
+ },
+ });
+
+ const latestHealthIndicatorData = data?.accountList.healthIndicatorData;
+ const showHealthIndicator = !!latestHealthIndicatorData;
+ const {
+ goal,
+ goalSource,
+ machineCalculatedGoal,
+ preferencesGoal,
+ preferencesGoalUpdatedAt,
+ preferencesGoalLow,
+ preferencesGoalOld,
+ } = getHealthIndicatorInfo(accountList, latestHealthIndicatorData);
+ const goalOrZero = goal ?? 0;
+ const hasValidGoal = goal !== null;
+ const receivedPercentage = hasValidGoal ? received / goal : NaN;
+ const pledgedPercentage = hasValidGoal ? pledged / goal : NaN;
+ const belowGoal = goalOrZero - pledged;
+ const belowGoalPercentage = hasValidGoal ? belowGoal / goal : NaN;
+
+ const toolTipText = useMemo(() => {
+ if (preferencesGoal) {
+ return t(
+ 'Your current goal of {{goal}} is staff-entered, based on the value set in your settings preferences.',
+ { goal: currencyFormat(preferencesGoal, currency, locale) },
+ );
+ } else if (machineCalculatedGoal) {
+ return t(
+ 'Your current goal of {{goal}} is NetSuite-calculated, based on the past year of NetSuite data. You can adjust this goal in your settings preferences.',
+ { goal: currencyFormat(machineCalculatedGoal, currency, locale) },
+ );
+ } else {
+ return t(
+ 'Your current goal is set to "0" because a monthly goal has not been set. You can set your monthly goal in your settings preferences.',
+ );
+ }
+ }, [machineCalculatedGoal, preferencesGoal, currency, locale]);
+
+ const cssProps = {
+ containerGrid: showHealthIndicator ? { spacing: 2 } : {},
+ monthlyGoalGrid: showHealthIndicator
+ ? { xs: 12, md: 6, lg: 7 }
+ : { xs: 12 },
+ statGrid: showHealthIndicator ? { xs: 6 } : { sm: 6, md: 3 },
+ hIGrid: showHealthIndicator ? { xs: 12, md: 6, lg: 5 } : { xs: 0 },
+ };
+
+ const annotation: Annotation | null = preferencesGoalLow
+ ? {
+ label: t('Below NetSuite-calculated goal'),
+ warning: true,
+ }
+ : goalSource === GoalSource.MachineCalculated
+ ? {
+ label: t('NetSuite-calculated goal'),
+ warning: true,
+ }
+ : preferencesGoalUpdatedAt
+ ? {
+ label: t('Last updated {{date}}', {
+ date: dateFormat(preferencesGoalUpdatedAt, locale),
+ }),
+ warning: preferencesGoalOld,
+ }
+ : null;
+ const annotationId = useId();
+ const annotationNode = annotation && (
+
+ *
+ {annotation.label}
+
+ );
return (
<>
@@ -96,153 +194,225 @@ const MonthlyGoal = ({
- {!loading && currencyFormat(goal, currencyCode, locale)}
+ {!loading && currencyFormat(goalOrZero, currency, locale)}
-
-
-
-
-
-
-
-
- {t('Goal')}
-
-
- {loading ? (
-
- ) : (
- currencyFormat(goal, currencyCode, locale)
- )}
-
-
-
-
-
-
- {t('Gifts Started')}
-
-
- {loading ? (
-
- ) : isNaN(receivedPercentage) ? (
- '-'
- ) : (
- percentageFormat(receivedPercentage, locale)
- )}
-
-
- {loading ? (
-
- ) : (
- currencyFormat(received, currencyCode, locale)
- )}
-
-
-
-
-
- {t('Commitments')}
-
-
- {loading ? (
-
- ) : isNaN(pledgedPercentage) ? (
- '-'
- ) : (
- percentageFormat(pledgedPercentage, locale)
- )}
-
-
- {loading ? (
-
- ) : (
- currencyFormat(pledged, currencyCode, locale)
- )}
-
-
-
- {!isNaN(belowGoal) && belowGoal > 0 ? (
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('Goal')}
+
+
+ {loading ? (
+
+ ) : (
+ <>
+ {currencyFormat(goalOrZero, currency, locale)}
+ {annotation && (
+
+ *
+
+ )}
+ >
+ )}
+
+ {/* Without the HI card there is enough space for the annotation so display it here */}
+ {annotation && !showHealthIndicator && annotationNode}
+ {annotation?.warning && (
+ ({
+ marginTop: theme.spacing(1),
+ textAlign: 'center',
+ })}
+ >
+ {t('Set Monthly Goal')}
+
+ )}
+
+
+
+
+
- {t('Below Goal')}
+
+ {t('Gifts Started')}
- {percentageFormat(belowGoalPercentage, locale)}
+ {loading ? (
+
+ ) : isNaN(receivedPercentage) ? (
+ '-'
+ ) : (
+ percentageFormat(receivedPercentage, locale)
+ )}
- {currencyFormat(belowGoal, currencyCode, locale)}
+ {loading ? (
+
+ ) : (
+ currencyFormat(received, currency, locale)
+ )}
- ) : (
-
+
- {t('Above Goal')}
+
+ {t('Commitments')}
{loading ? (
- ) : isNaN(belowGoalPercentage) ? (
+ ) : isNaN(pledgedPercentage) ? (
'-'
) : (
- percentageFormat(-belowGoalPercentage, locale)
+ percentageFormat(pledgedPercentage, locale)
)}
{loading ? (
) : (
- currencyFormat(-belowGoal, currencyCode, locale)
+ currencyFormat(pledged, currency, locale)
)}
- )}
-
-
-
-
+
+ {!isNaN(belowGoal) && belowGoal > 0 ? (
+
+
+ {t('Below Goal')}
+
+
+ {percentageFormat(belowGoalPercentage, locale)}
+
+
+ {currencyFormat(belowGoal, currency, locale)}
+
+
+ ) : (
+
+
+ {t('Above Goal')}
+
+
+ {loading ? (
+
+ ) : isNaN(belowGoalPercentage) ? (
+ '-'
+ ) : (
+ percentageFormat(-belowGoalPercentage, locale)
+ )}
+
+
+ {loading ? (
+
+ ) : (
+ currencyFormat(-belowGoal, currency, locale)
+ )}
+
+
+ )}
+
+ {/* With the HI card there isn't enough space for the annotation next to the monthly goal so display it here */}
+ {annotation && showHealthIndicator && (
+
+ {annotationNode}
+
+ )}
+
+
+
+
+
+
+ {latestHealthIndicatorData && (
+
+ )}
+
+
>
);
};
diff --git a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportEmail/ExportEmail.tsx b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportEmail/ExportEmail.tsx
index 9c6ff5b6ca..bf1d1c496d 100644
--- a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportEmail/ExportEmail.tsx
+++ b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportEmail/ExportEmail.tsx
@@ -31,7 +31,7 @@ const CloseButton = styled(IconButton)(({ theme }) => ({
top: theme.spacing(1),
color: theme.palette.text.primary,
'&:hover': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
}));
@@ -39,7 +39,7 @@ const TextArea = styled(TextareaAutosize)(() => ({
width: '100%',
}));
const StyledDialogContentText = styled(DialogContentText)(({ theme }) => ({
- color: theme.palette.cruGrayDark.main,
+ color: theme.palette.mpdxGrayDark.main,
}));
const ExportEmail = ({
diff --git a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportPhysical/ExportPhysical.tsx b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportPhysical/ExportPhysical.tsx
index 1e8ccfbf8a..e939b369fd 100644
--- a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportPhysical/ExportPhysical.tsx
+++ b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/ExportPhysical/ExportPhysical.tsx
@@ -38,7 +38,7 @@ const CloseButton = styled(IconButton)(({ theme }) => ({
top: theme.spacing(1),
color: theme.palette.text.primary,
'&:hover': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
}));
diff --git a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx
index 8e278c6f9a..2ca6c4cee0 100644
--- a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx
+++ b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx
@@ -45,7 +45,7 @@ const CloseButton = styled(IconButton)(({ theme }) => ({
top: theme.spacing(1),
color: theme.palette.text.primary,
'&:hover': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
}));
diff --git a/src/components/DonationTable/DonationTable.tsx b/src/components/DonationTable/DonationTable.tsx
index dd8095e3f4..52c0c162ac 100644
--- a/src/components/DonationTable/DonationTable.tsx
+++ b/src/components/DonationTable/DonationTable.tsx
@@ -54,7 +54,7 @@ export interface DonationTableProps {
export const StyledGrid = styled(DataGrid)(({ theme }) => ({
'.MuiDataGrid-row:nth-of-type(2n + 1):not(:hover)': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
'.MuiDataGrid-cell': {
overflow: 'hidden',
@@ -82,7 +82,7 @@ export const LoadingProgressBar = styled(LinearProgress)(({ theme }) => ({
}));
export const LoadingBox = styled(Box)(({ theme }) => ({
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
height: 300,
minWidth: 700,
margin: 'auto',
diff --git a/src/components/InfiniteList/InfiniteList.tsx b/src/components/InfiniteList/InfiniteList.tsx
index c5194b21b6..6dc51e191a 100644
--- a/src/components/InfiniteList/InfiniteList.tsx
+++ b/src/components/InfiniteList/InfiniteList.tsx
@@ -35,7 +35,7 @@ export const ItemWithBorders = styled(ListItem, {
: {
borderBottom: `1px solid ${theme.palette.grey[200]}`,
'&:hover': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
}),
}));
diff --git a/src/components/Layouts/Primary/NavBar/NavBar.tsx b/src/components/Layouts/Primary/NavBar/NavBar.tsx
index 8953af2571..e90c558205 100644
--- a/src/components/Layouts/Primary/NavBar/NavBar.tsx
+++ b/src/components/Layouts/Primary/NavBar/NavBar.tsx
@@ -106,7 +106,7 @@ function reduceChildRoutes({
const useStyles = makeStyles()((theme: Theme) => ({
mobileDrawer: {
width: 290,
- backgroundColor: theme.palette.cruGrayDark.main,
+ backgroundColor: theme.palette.mpdxGrayDark.main,
},
}));
diff --git a/src/components/Layouts/Primary/NavBar/NavItem/NavItem.tsx b/src/components/Layouts/Primary/NavBar/NavItem/NavItem.tsx
index 7513943e2a..e6029485de 100644
--- a/src/components/Layouts/Primary/NavBar/NavItem/NavItem.tsx
+++ b/src/components/Layouts/Primary/NavBar/NavItem/NavItem.tsx
@@ -37,11 +37,11 @@ const StyledButton = styled(Button)(({ theme }) => ({
}));
const ExpandItemIcon = styled(ExpandLessIcon)(({ theme }) => ({
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
const CollapseItemIcon = styled(ChevronRightIcon)(({ theme }) => ({
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
}));
export const NavItem: FC = ({
diff --git a/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx b/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx
index 4312bcfd7e..db4465a676 100644
--- a/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx
+++ b/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx
@@ -29,14 +29,14 @@ type ProfileMenuContent = {
const MobileDrawer = styled(Drawer)(() => ({
'& .MuiDrawer-paper': {
width: 290,
- backgroundColor: theme.palette.cruGrayDark.main,
+ backgroundColor: theme.palette.mpdxGrayDark.main,
zIndex: theme.zIndex.drawer + 201,
},
}));
const LeafListItemHover = styled(LeafListItem)(() => ({
'&:hover': {
- backgroundColor: `${theme.palette.cruGrayMedium.main} !important`,
+ backgroundColor: `${theme.palette.mpdxGrayMedium.main} !important`,
},
}));
@@ -151,8 +151,8 @@ export const ProfileMenuPanel: React.FC = () => {
style={{
backgroundColor:
accountListId === accountList.id
- ? theme.palette.cruGrayMedium.main
- : theme.palette.cruGrayDark.main,
+ ? theme.palette.mpdxGrayMedium.main
+ : theme.palette.mpdxGrayDark.main,
}}
onClick={() => changeAccountListId(accountList.id)}
>
diff --git a/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/StyledComponents.tsx b/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/StyledComponents.tsx
index 0dadfefcad..69d8494b4d 100644
--- a/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/StyledComponents.tsx
+++ b/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/StyledComponents.tsx
@@ -12,6 +12,6 @@ export const LogFormLabel = styled(FormLabel)(({ theme }) => ({
export const FormTextField = styled(TextField)(({ theme }) => ({
'& .MuiInputBase-root.Mui-disabled': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
}));
diff --git a/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx b/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx
index 4b912fdd61..35c68300fb 100644
--- a/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx
+++ b/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx
@@ -33,7 +33,7 @@ const useStyles = makeStyles()(() => ({
display: 'none',
},
'&[aria-current=page]': {
- backgroundColor: theme.palette.cruGrayMedium.main,
+ backgroundColor: theme.palette.mpdxGrayMedium.main,
backgroundBlendMode: 'multiply',
},
},
@@ -48,11 +48,11 @@ const useStyles = makeStyles()(() => ({
transform: 'rotate(180deg)',
},
subMenu: {
- backgroundImage: `linear-gradient(0deg, ${theme.palette.cruGrayDark.main}, ${theme.palette.cruGrayDark.main})`,
+ backgroundImage: `linear-gradient(0deg, ${theme.palette.mpdxGrayDark.main}, ${theme.palette.mpdxGrayDark.main})`,
},
menuItemSelected: {
backgroundBlendMode: 'multiply',
- backgroundColor: theme.palette.cruGrayMedium.main,
+ backgroundColor: theme.palette.mpdxGrayMedium.main,
},
needsAttention: {
backgroundImage: `linear-gradient(0deg, ${theme.palette.mpdxYellow.main}, ${theme.palette.mpdxYellow.main})`,
@@ -77,7 +77,7 @@ const useStyles = makeStyles()(() => ({
},
},
darkText: {
- color: theme.palette.cruGrayDark.main,
+ color: theme.palette.mpdxGrayDark.main,
},
whiteText: {
color: 'white',
@@ -85,7 +85,7 @@ const useStyles = makeStyles()(() => ({
menuItem: {
paddingInline: '10px',
'&:focus-visible, &:hover, &[aria-current=page]': {
- backgroundColor: theme.palette.cruGrayMedium.main,
+ backgroundColor: theme.palette.mpdxGrayMedium.main,
backgroundBlendMode: 'multiply',
},
},
diff --git a/src/components/Layouts/Primary/TopBar/Items/NotificationMenu/Item/Item.tsx b/src/components/Layouts/Primary/TopBar/Items/NotificationMenu/Item/Item.tsx
index 3a2ff70816..a96ba1a91b 100644
--- a/src/components/Layouts/Primary/TopBar/Items/NotificationMenu/Item/Item.tsx
+++ b/src/components/Layouts/Primary/TopBar/Items/NotificationMenu/Item/Item.tsx
@@ -216,8 +216,7 @@ const NotificationMenuItem = ({
) && (
{monthYearFormat(
- DateTime.fromISO(item.notification.occurredAt).month,
- DateTime.fromISO(item.notification.occurredAt).year,
+ DateTime.fromISO(item.notification.occurredAt),
locale,
)}
diff --git a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx
index 8fa1525370..17975d3b36 100644
--- a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx
+++ b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx
@@ -60,8 +60,8 @@ const MenuItemFooter = styled(MenuItem)(({ theme }) => ({
const MenuButton = styled(Button)(({ theme }) => ({
width: '100%',
marginTop: theme.spacing(1),
- borderColor: theme.palette.cruGrayLight.main,
- color: theme.palette.cruGrayLight.main,
+ borderColor: theme.palette.mpdxGrayLight.main,
+ color: theme.palette.mpdxGrayLight.main,
}));
const NameBox = styled(Box)(() => ({
@@ -73,7 +73,7 @@ const NameBox = styled(Box)(() => ({
const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
'&:hover': {
- backgroundColor: `${theme.palette.cruGrayMedium.main} !important`,
+ backgroundColor: `${theme.palette.mpdxGrayMedium.main} !important`,
},
}));
@@ -91,7 +91,7 @@ const AccountListSelectorDetails = styled(AccordionDetails)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
padding: 0,
- borderBottom: `1px solid ${theme.palette.cruGrayLight.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayLight.main}`,
maxHeight: theme.spacing(44),
overflow: 'auto',
'& .MuiMenuItem-root': {
@@ -107,12 +107,12 @@ const MenuWrapper = styled(Menu)(({ theme }) => ({
boxShadow: 'none',
},
'& .MuiAccordionSummary-root': {
- borderTop: `1px solid ${theme.palette.cruGrayLight.main}`,
- borderBottom: `1px solid ${theme.palette.cruGrayLight.main}`,
+ borderTop: `1px solid ${theme.palette.mpdxGrayLight.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayLight.main}`,
},
'& .MuiPaper-root': {
color: 'white',
- backgroundColor: theme.palette.cruGrayDark.main,
+ backgroundColor: theme.palette.mpdxGrayDark.main,
},
}));
@@ -303,7 +303,7 @@ const ProfileMenu = (): ReactElement => {
style={{
backgroundColor:
accountListId === accountList.id
- ? theme.palette.cruGrayMedium.main
+ ? theme.palette.mpdxGrayMedium.main
: 'inherit',
}}
onClick={() => handleAccountListClick(accountList)}
diff --git a/src/components/Layouts/Primary/TopBar/TopBar.tsx b/src/components/Layouts/Primary/TopBar/TopBar.tsx
index bebfbb562a..edab50c69a 100644
--- a/src/components/Layouts/Primary/TopBar/TopBar.tsx
+++ b/src/components/Layouts/Primary/TopBar/TopBar.tsx
@@ -31,7 +31,7 @@ const Offset = styled('div')(({ theme }) => ({
}));
const StyledAppBar = styled(AppBar)(({ theme }) => ({
- backgroundColor: theme.palette.cruGrayDark.main,
+ backgroundColor: theme.palette.mpdxGrayDark.main,
}));
const TopBar = ({
diff --git a/src/components/Layouts/SidePanelsLayout.tsx b/src/components/Layouts/SidePanelsLayout.tsx
index 0ce8be6904..61df0b1c8a 100644
--- a/src/components/Layouts/SidePanelsLayout.tsx
+++ b/src/components/Layouts/SidePanelsLayout.tsx
@@ -53,7 +53,7 @@ const ExpandingContent = styled(Box)(({ open }: { open: boolean }) => ({
const LeftPanelWrapper = styled(FullHeightBox)(({ theme }) => ({
flexShrink: 0,
- borderRight: `1px solid ${theme.palette.cruGrayLight.main}`,
+ borderRight: `1px solid ${theme.palette.mpdxGrayLight.main}`,
left: 0,
background: theme.palette.common.white,
[theme.breakpoints.down('md')]: {
@@ -79,7 +79,7 @@ const RightPanelWrapper = styled(FullHeightBox)(({ theme, headerHeight }) => {
},
'@media (min-width:600px)': {
top: `calc(${toolbar['@media (min-width:600px)'].minHeight}px + ${headerHeight})`,
- borderLeft: `1px solid ${theme.palette.cruGrayLight.main}`,
+ borderLeft: `1px solid ${theme.palette.mpdxGrayLight.main}`,
},
[theme.breakpoints.down('sm')]: {
width: '100%',
diff --git a/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.test.tsx b/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.test.tsx
index 045a2616c6..5242ec1054 100644
--- a/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.test.tsx
+++ b/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.test.tsx
@@ -1,10 +1,6 @@
import React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import { render } from '@testing-library/react';
-import {
- afterTestResizeObserver,
- beforeTestResizeObserver,
-} from '__tests__/util/windowResizeObserver';
import theme from 'src/theme';
import { AccountListItemChart as Chart } from './Chart';
@@ -19,13 +15,6 @@ const dataMock = [
];
describe('AccountItemChart', () => {
- beforeEach(() => {
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
- });
it('default', async () => {
const { getByTestId, getByText } = render(
diff --git a/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.tsx b/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.tsx
index b5f5fbdd28..53b9dc5cc6 100644
--- a/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.tsx
+++ b/src/components/Reports/AccountsListLayout/List/ListItem/Chart/Chart.tsx
@@ -4,7 +4,6 @@ import { styled, useTheme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import {
Bar,
- BarChart,
CartesianGrid,
Legend,
ReferenceLine,
@@ -15,6 +14,7 @@ import {
YAxis,
} from 'recharts';
import AnimatedCard from 'src/components/AnimatedCard';
+import { StyledBarChart } from 'src/components/common/StyledBarChart/StyledBarChart';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
import type { Theme } from '@mui/material/styles/createTheme';
@@ -76,7 +76,7 @@ export const AccountListItemChart: FC = ({
style={{ height: '250px' }}
>
- = ({
fill={theme.palette.primary.main}
barSize={30}
/>
-
+
= ({
style={{ height: '150px' }}
>
-
+
= ({
fill={theme.palette.primary.main}
barSize={10}
/>
-
+
diff --git a/src/components/Reports/AccountsListLayout/List/ListItem/ListItem.test.tsx b/src/components/Reports/AccountsListLayout/List/ListItem/ListItem.test.tsx
index 6c55ccb516..3ce4d86ea7 100644
--- a/src/components/Reports/AccountsListLayout/List/ListItem/ListItem.test.tsx
+++ b/src/components/Reports/AccountsListLayout/List/ListItem/ListItem.test.tsx
@@ -3,10 +3,6 @@ import { ThemeProvider } from '@mui/material/styles';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TestRouter from '__tests__/util/TestRouter';
-import {
- afterTestResizeObserver,
- beforeTestResizeObserver,
-} from '__tests__/util/windowResizeObserver';
import theme from 'src/theme';
import { Account, AccountListItem as ListItem } from './ListItem';
@@ -56,14 +52,6 @@ describe('AccountItem', () => {
});
describe('AccountItem Chart', () => {
- beforeEach(() => {
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
- });
-
it('should render chart', async () => {
const entryHistoriesMock = [
{
diff --git a/src/components/Reports/DonationsReport/DonationsReport.test.tsx b/src/components/Reports/DonationsReport/DonationsReport.test.tsx
index 2a2df53822..a3ca360ebe 100644
--- a/src/components/Reports/DonationsReport/DonationsReport.test.tsx
+++ b/src/components/Reports/DonationsReport/DonationsReport.test.tsx
@@ -5,10 +5,6 @@ import userEvent from '@testing-library/user-event';
import { DateTime } from 'luxon';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
-import {
- afterTestResizeObserver,
- beforeTestResizeObserver,
-} from '__tests__/util/windowResizeObserver';
import { DonationTableQuery } from 'src/components/DonationTable/DonationTable.generated';
import theme from 'src/theme';
import { GetDonationsGraphQuery } from '../../Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.generated';
@@ -83,14 +79,6 @@ interface Mocks {
}
describe('DonationsReport', () => {
- beforeEach(() => {
- beforeTestResizeObserver();
- });
-
- afterEach(() => {
- afterTestResizeObserver();
- });
-
it('renders empty', async () => {
const {
getByTestId,
@@ -129,9 +117,7 @@ describe('DonationsReport', () => {
);
expect(getByText(title)).toBeInTheDocument();
expect(getByTestId('DonationHistoriesBoxEmpty')).toBeInTheDocument();
- expect(
- queryByTestId('DonationHistoriesGridLoading'),
- ).not.toBeInTheDocument();
+ expect(queryByTestId('BarChartSkeleton')).not.toBeInTheDocument();
expect(queryAllByRole('button')[1]).toBeInTheDocument();
});
@@ -157,9 +143,7 @@ describe('DonationsReport', () => {
expect(
getByTestId('DonationHistoriesTypographyAverage'),
).toBeInTheDocument();
- expect(
- queryByTestId('DonationHistoriesGridLoading'),
- ).not.toBeInTheDocument();
+ expect(queryByTestId('BarChartSkeleton')).not.toBeInTheDocument();
expect(await findByRole('cell', { name: 'John' })).toBeInTheDocument();
});
diff --git a/src/components/Reports/DonationsReport/DonationsReport.tsx b/src/components/Reports/DonationsReport/DonationsReport.tsx
index 346bbbb240..9ff24577c0 100644
--- a/src/components/Reports/DonationsReport/DonationsReport.tsx
+++ b/src/components/Reports/DonationsReport/DonationsReport.tsx
@@ -43,6 +43,10 @@ export const DonationsReport: React.FC = ({
designationAccountIds: designationAccounts?.length
? designationAccounts
: null,
+ periodBegin: DateTime.now()
+ .startOf('month')
+ .minus({ years: 1 })
+ .toISODate(),
},
});
@@ -68,11 +72,8 @@ export const DonationsReport: React.FC = ({
({
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
height: 300,
minWidth: 700,
maxWidth: '97%',
diff --git a/src/components/Reports/ExpectedMonthlyTotalReport/Table/ExpectedMonthlyTotalReportTable.tsx b/src/components/Reports/ExpectedMonthlyTotalReport/Table/ExpectedMonthlyTotalReportTable.tsx
index 4fe10d7a61..16677a80de 100644
--- a/src/components/Reports/ExpectedMonthlyTotalReport/Table/ExpectedMonthlyTotalReportTable.tsx
+++ b/src/components/Reports/ExpectedMonthlyTotalReport/Table/ExpectedMonthlyTotalReportTable.tsx
@@ -73,7 +73,7 @@ export const ExpectedMonthlyTotalReportTable: React.FC = ({
}
style={{
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
}}
onClick={() => setVisible((v) => !v)}
>
@@ -114,7 +114,7 @@ export const ExpectedMonthlyTotalReportTable: React.FC = ({
backgroundColor:
index % 2
? theme.palette.common.white
- : theme.palette.cruGrayLight.main,
+ : theme.palette.mpdxGrayLight.main,
}}
>
diff --git a/src/components/Reports/FinancialAccountsReport/AccountSummary/AccountSummary.tsx b/src/components/Reports/FinancialAccountsReport/AccountSummary/AccountSummary.tsx
index 632329597e..aeef7527e3 100644
--- a/src/components/Reports/FinancialAccountsReport/AccountSummary/AccountSummary.tsx
+++ b/src/components/Reports/FinancialAccountsReport/AccountSummary/AccountSummary.tsx
@@ -161,8 +161,7 @@ export const AccountSummary: React.FC = ({
// Periods
const startDateFormatted = monthYearFormat(
- DateTime.fromISO(item?.startDate ?? '').month,
- DateTime.fromISO(item?.startDate ?? '').year,
+ DateTime.fromISO(item.startDate),
locale,
false,
);
diff --git a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionTable/AccountTransactionTable.tsx b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionTable/AccountTransactionTable.tsx
index c715897b87..78be23f7e6 100644
--- a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionTable/AccountTransactionTable.tsx
+++ b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionTable/AccountTransactionTable.tsx
@@ -24,7 +24,7 @@ import { FinancialAccountEntriesQuery } from '../financialAccountTransactions.ge
const StyledDataGrid = styled(DataGrid)(({ theme }) => ({
'.MuiDataGrid-row:nth-of-type(2n + 1):not(:hover)': {
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
},
'.MuiDataGrid-cell': {
overflow: 'hidden',
@@ -38,7 +38,7 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({
'.MuiDataGrid-main .MuiDataGrid-row[data-id="openingBalanceRow"], .MuiDataGrid-main .MuiDataGrid-row[data-id="closingBalanceRow"]':
{
- backgroundColor: theme.palette.cruGrayMedium.main,
+ backgroundColor: theme.palette.mpdxGrayMedium.main,
fontWeight: 'bold',
},
}));
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx b/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx
index 637a4c5327..ca0e80af47 100644
--- a/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx
+++ b/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx
@@ -35,7 +35,7 @@ const YearTableCell = styled(TableCell)(({}) => ({
}));
const YearTypography = styled(Typography)(({ theme }) => ({
- borderLeft: `1px solid ${theme.palette.cruGrayLight.main}`,
+ borderLeft: `1px solid ${theme.palette.mpdxGrayLight.main}`,
'@media print': {
lineHeight: 1,
fontSize: '1rem',
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/ConsistencyExplanation.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/ConsistencyExplanation.tsx
new file mode 100644
index 0000000000..5f313f1335
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/ConsistencyExplanation.tsx
@@ -0,0 +1,45 @@
+import { Typography } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+export const ConsistencyExplanation: React.FC = () => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {t(
+ 'This health indicator helps you understand the stability of your staff balance over a period of 12 months. It relates the months when your staff account balance was positive to the months when it was negative. If the consistency health score is 75, it indicates that you have experienced a negative balance for 3 months within the last 12-month period. This is not a healthy practice!',
+ )}
+
+ {t('Note:')}
+
+ {t(
+ 'Depending on your national financial policy, you may have a positive account balance but if that is lower than the national approved level for a healthy balance it is considered unhealthy.',
+ )}
+
+ {t('Tips to Improve:')}
+
+
+ {t(
+ 'Review the accuracy of your support goal, as it appears you may be spending more than planned.',
+ )}
+
+
+ {t(
+ 'Review your monthly contributions and contact your partners if they missed a donation.',
+ )}
+
+
+ {t(
+ 'Examine your transactions to check for any charges related to future expense periods that could temporarily put your balance in deficit (for example, paying rent in advance for 2 months or similar).',
+ )}
+
+
+ {t(
+ 'Consider scheduling time for MPD in the coming months to prevent inadequate funds and a negative balance.',
+ )}
+
+
+ >
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/DepthExplanation.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/DepthExplanation.tsx
new file mode 100644
index 0000000000..22645606a0
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/DepthExplanation.tsx
@@ -0,0 +1,34 @@
+import { Typography } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+export const DepthExplanation: React.FC = () => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {t(
+ "The Depth MPD Health Indicator is designed to evaluate the number of local partners involved in your ministry. Regardless of where you serve in the world, there are local believers around you, or by God's grace, there will be local believers as a result of your ministry efforts.",
+ )}
+
+
+ {t(
+ 'You will receive 100 points if more than 70% of your partners are locally based, or if there has been an increase of more than 5% in local partners over the last 14 months. You will earn 50 points if there is an increase of up to 5% in the number of local partners during that same timeframe.',
+ )}
+
+ {t('Tips to Improve:')}
+
+
+ {t(
+ 'Throughout the year, add potential local contacts to your MPD database and reach out to them to join you.',
+ )}
+
+
+ {t(
+ 'Be intentional and consistent in sharing what God is doing in your ministry, as His work can inspire people to get involved.',
+ )}
+
+
+ >
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/HealthIndicatorFormula.graphql b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/HealthIndicatorFormula.graphql
new file mode 100644
index 0000000000..b57950e18e
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/HealthIndicatorFormula.graphql
@@ -0,0 +1,11 @@
+query HealthIndicatorFormula($accountListId: ID!) {
+ accountList(id: $accountListId) {
+ healthIndicatorData {
+ id
+ consistencyHi
+ depthHi
+ ownershipHi
+ successHi
+ }
+ }
+}
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/HealthIndicatorFormula.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/HealthIndicatorFormula.tsx
new file mode 100644
index 0000000000..ae8ec42509
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/HealthIndicatorFormula.tsx
@@ -0,0 +1,164 @@
+import React, { Dispatch, SetStateAction, useEffect } from 'react';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import {
+ AccordionDetails,
+ AccordionSummary,
+ Card,
+ Skeleton,
+ Typography,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { Trans, useTranslation } from 'react-i18next';
+import { GroupedAccordion } from 'src/components/Shared/Forms/Accordions/GroupedAccordion';
+import { useIndicatorColors } from '../useIndicatorColors';
+import { ConsistencyExplanation } from './ConsistencyExplanation';
+import { DepthExplanation } from './DepthExplanation';
+import { useHealthIndicatorFormulaQuery } from './HealthIndicatorFormula.generated';
+import { OwnershipExplanation } from './OwnershipExplanation';
+import { SuccessExplanation } from './SuccessExplanation';
+
+const StyledSummary = styled(AccordionSummary)({
+ '.MuiAccordionSummary-content': {
+ display: 'flex',
+ gap: '0.5ch',
+ alignItems: 'center',
+ },
+});
+
+const StyledDetails = styled(AccordionDetails)(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(2),
+
+ ul: {
+ marginLeft: theme.spacing(2),
+ },
+}));
+
+interface HealthIndicatorFormulaProps {
+ accountListId: string;
+ noHealthIndicatorData: boolean;
+ setNoHealthIndicatorData: Dispatch>;
+}
+
+export const HealthIndicatorFormula: React.FC = ({
+ accountListId,
+ noHealthIndicatorData,
+ setNoHealthIndicatorData,
+}) => {
+ const { t } = useTranslation();
+ const colors = useIndicatorColors();
+
+ const { data, loading } = useHealthIndicatorFormulaQuery({
+ variables: {
+ accountListId,
+ },
+ });
+
+ const healthIndicatorData = data?.accountList.healthIndicatorData;
+ useEffect(() => {
+ if (!healthIndicatorData && !loading) {
+ setNoHealthIndicatorData(true);
+ }
+ }, [healthIndicatorData, loading]);
+
+ if (noHealthIndicatorData) {
+ return null;
+ }
+
+ return (
+
+
+ }>
+
+ {t('MPD Health')} = [({t('Ownership')} x 3) + ({t('Success')} x 2) +
+ ({t('Consistency')} x 1) + ({t('Depth')} x 1)] / 7
+
+
+
+
+
+ The divisor (7) above in the MPD Health formula is based on the
+ total weight of all included areas: 3 (Ownership) + 2 (Success) +
+ 1 (Consistency) + 1 (Depth) = 7. If any area has no value (=0) at
+ the time of the calculation, the divisor decreases accordingly.
+ For example, if no value can be calculated for Ownership, then
+ this divisor is: 0 + 2 + 1 + 1 = 4, and the calculation would now
+ be:
+
+
+
+ {t('MPD Health')} = [({t('Success')} x 2) + ({t('Consistency')} x 1)
+ + ({t('Depth')} x 1)] / 4
+
+
+
+
+ }
+ value={healthIndicatorData?.ownershipHi ?? 0}
+ isLoading={loading && !data}
+ />
+ }
+ value={healthIndicatorData?.successHi ?? 0}
+ isLoading={loading && !data}
+ />
+ }
+ isLoading={loading && !data}
+ />
+ }
+ value={healthIndicatorData?.depthHi ?? 0}
+ isLoading={loading && !data}
+ />
+
+ );
+};
+
+interface FormulaItemProps {
+ name: string;
+ color: string;
+ description: string;
+ explanation?: React.ReactNode;
+ value: number;
+ isLoading: boolean;
+}
+
+const FormulaItem: React.FC = ({
+ name,
+ color,
+ description,
+ explanation,
+ value,
+ isLoading,
+}) => (
+
+ }>
+ {isLoading ? (
+
+ ) : (
+
+ {value}
+
+ )}
+ {name} =
+ {description}
+
+ {explanation}
+
+);
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/OwnershipExplanation.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/OwnershipExplanation.tsx
new file mode 100644
index 0000000000..cf5070d010
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/OwnershipExplanation.tsx
@@ -0,0 +1,50 @@
+import { Typography } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+export const OwnershipExplanation: React.FC = () => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {t(
+ 'This indicator helps you understand the proportion of your support that comes from your own efforts versus what has been provided through funds given to you. Self-raised funds are those you receive based on your own initiative, rather than from national subsidies or matching funds, or financial help from the National (Global) ministry.',
+ )}
+
+
+ {t(
+ 'A higher score on this indicator signifies that you have ownership of your Ministry Partner Development (MPD) and suggests a healthy financial situation.',
+ )}
+
+
+ {t(
+ 'In most cases, this indicator will score 100 points; however, it may be lower in some situations. The recommendation is that it should not drop below 70. In some ministries, especially for new staff, initial funding is provided to help them during the first few years, but this financial subsidy decreases each year.',
+ )}
+
+ {t('Tips to Improve:')}
+
+ {t('For New Staff:')}
+
+
+ {t(
+ "Set your support goals without considering the subsidy. Don't limit yourself! During the first few years, you often have a larger contact base from friends, family, and your church community.",
+ )}
+
+
+ {t('For Senior Staff:')}
+
+
+ {t(
+ 'If your indicator is not already at 100, consider dedicating some intensive time to MPD.',
+ )}
+
+
+
+
+ {t(
+ "Remember, our mission's success is not only our responsibility; we are working together with a team of ministry partners. The Great Commission is something we accomplish collectively as the body of Christ.",
+ )}
+
+ >
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/SuccessExplanation.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/SuccessExplanation.tsx
new file mode 100644
index 0000000000..833df43ab8
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorFormula/SuccessExplanation.tsx
@@ -0,0 +1,70 @@
+import Link from 'next/link';
+import { Typography } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+export const SuccessExplanation: React.FC = () => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {t(
+ 'Setting a financial support goal is crucial for every staff member to be effective in the field. Understanding and planning our financial needs helps ensure that everyone can succeed in their roles. Establishing a realistic support goal is key to effective ministry, as it ensures that our families are financially supported.',
+ )}
+
+
+ {t(
+ 'The success indicator allows us to assess our effectiveness in reaching our current support goal.',
+ )}
+
+
+ {t('Definition of 100% Staff Support:')}
+
+
+ {t(
+ 'The sum of Full Salary + Allowances + Reimbursements + Benefits + Administration Charge + Attrition.',
+ )}
+
+ {t('Note:')}
+
+ {t(
+ 'If you do not have a staff support goal set up in NetSuite, the automation calculation is as follows: All your salary expenses for the previous 12 months are multiplied by 1.6. This 1.6 multiplier is designed to ensure that you have enough resources to cover your salary and salary-related expenses, as well as some of your personal ministry expenses.',
+ )}
+
+
+ {t(
+ 'The desired Health score is always 100, meaning that you are fully funded!',
+ )}
+
+
+ {t(
+ 'If your MPD Success Health score is under 70, consider dedicating some intensive time to MPD.',
+ )}
+
+ {t('Tips to Improve:')}
+
+
+ {t(
+ 'Follow up with your partners if they have made a commitment that has not yet been received in your account.',
+ )}
+
+
+ {t('Take time at least once a month to review your donations.')}
+
+
+ {t(
+ 'Maintain monthly or bi-monthly connections with your partners. For assistance with effective and time-efficient ideas, please visit the Global Staff Web',
+ )}{' '}
+ (
+
+ {t('Global Partner Development Section')}
+
+ ).
+
+
+ {t('Review your Staff Support Goal and update it as necessary.')}
+
+
+ >
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.graphql b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.graphql
new file mode 100644
index 0000000000..a477294907
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.graphql
@@ -0,0 +1,11 @@
+query HealthIndicatorGraph($accountListId: ID!) {
+ healthIndicatorData(accountListId: $accountListId) {
+ id
+ indicationPeriodBegin
+ consistencyHi
+ depthHi
+ ownershipHi
+ successHi
+ overallHi
+ }
+}
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.test.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.test.tsx
new file mode 100644
index 0000000000..c6d3e6fb43
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.test.tsx
@@ -0,0 +1,66 @@
+import { ThemeProvider } from '@mui/material/styles';
+import { render, waitForElementToBeRemoved } from '@testing-library/react';
+import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
+import theme from 'src/theme';
+import { HealthIndicatorGraph } from './HealthIndicatorGraph';
+import { HealthIndicatorGraphQuery } from './HealthIndicatorGraph.generated';
+
+const accountListId = 'account-list-1';
+
+describe('HealthIndicatorGraph', () => {
+ it('renders nothing when there is no data', async () => {
+ const { getByTestId, queryByTestId } = render(
+
+
+ mocks={{
+ HealthIndicatorGraph: {
+ healthIndicatorData: [],
+ },
+ }}
+ >
+
+
+ ,
+ );
+
+ const skeleton = getByTestId('BarChartSkeleton');
+ expect(skeleton).toBeInTheDocument();
+ await waitForElementToBeRemoved(skeleton);
+
+ expect(queryByTestId('HealthIndicatorGraphHeader')).not.toBeInTheDocument();
+ });
+
+ it('renders a skeleton while data is loading', async () => {
+ const { getByTestId } = render(
+
+
+
+
+ ,
+ );
+
+ const skeleton = getByTestId('BarChartSkeleton');
+ expect(skeleton).toBeInTheDocument();
+ await waitForElementToBeRemoved(skeleton);
+ });
+
+ it('renders the average', async () => {
+ const { findByTestId } = render(
+
+
+ mocks={{
+ HealthIndicatorGraph: {
+ healthIndicatorData: [{ overallHi: 50 }],
+ },
+ }}
+ >
+
+
+ ,
+ );
+
+ expect(await findByTestId('HealthIndicatorGraphHeader')).toHaveTextContent(
+ 'Overall average 50',
+ );
+ });
+});
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.tsx
new file mode 100644
index 0000000000..1b3bd82695
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/HealthIndicatorGraph.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import { Card, CardContent, CardHeader, useTheme } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import {
+ Bar,
+ CartesianGrid,
+ Legend,
+ ReferenceLine,
+ ResponsiveContainer,
+ Text,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+import { BarChartSkeleton } from 'src/components/common/BarChartSkeleton/BarChartSkeleton';
+import { LegendReferenceLine } from 'src/components/common/LegendReferenceLine/LegendReferenceLine';
+import { StyledBarChart } from 'src/components/common/StyledBarChart/StyledBarChart';
+import { useIndicatorColors } from '../useIndicatorColors';
+import { useGraphData } from './useGraphData';
+
+interface HealthIndicatorGraphProps {
+ accountListId: string;
+}
+
+export const HealthIndicatorGraph: React.FC = ({
+ accountListId,
+}) => {
+ const { t } = useTranslation();
+ const { palette } = useTheme();
+ const colors = useIndicatorColors();
+
+ const { loading, average, periods } = useGraphData(accountListId);
+
+ const stacks = [
+ { field: 'ownership', label: t('Ownership') },
+ { field: 'success', label: t('Success') },
+ { field: 'consistency', label: t('Consistency') },
+ { field: 'depth', label: t('Depth') },
+ ];
+ const averageColor = palette.graphite.main;
+
+ if (periods?.length === 0) {
+ // The account has no account list entries
+ return null;
+ }
+
+ if (loading && !periods) {
+ // The account list is loading
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ )
+ }
+ />
+
+
+
+
+
+
+ typeof item.dataKey === 'string' &&
+ item.dataKey.endsWith('Scaled')
+ ? item.payload[item.dataKey.slice(0, -6)]
+ : scaledValue
+ }
+ />
+ {average !== null && (
+
+ )}
+
+
+ (%)
+
+ }
+ />
+ {stacks.map(({ field, label }) => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/useGraphData.test.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/useGraphData.test.tsx
new file mode 100644
index 0000000000..ad78949ee6
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/useGraphData.test.tsx
@@ -0,0 +1,367 @@
+import { ReactElement } from 'react';
+import { renderHook } from '@testing-library/react-hooks';
+import { ErgonoMockShape } from 'graphql-ergonomock';
+import { DeepPartial } from 'ts-essentials';
+import { GqlMockedProvider, gqlMock } from '__tests__/util/graphqlMocking';
+import { HealthIndicatorQueryVariables } from 'src/components/Dashboard/MonthlyGoal/HealthIndicator.generated';
+import {
+ HealthIndicatorGraphDocument,
+ HealthIndicatorGraphQuery,
+} from './HealthIndicatorGraph.generated';
+import {
+ calculatePeriodSpans,
+ uniqueMonths,
+ useGraphData,
+ weightedAverage,
+} from './useGraphData';
+
+// Create and return wrapper component that will provide the mock HI data to the hook
+const makeWrapper = (
+ mockHealthIndicatorData: DeepPartial<
+ HealthIndicatorGraphQuery['healthIndicatorData']
+ > &
+ Array,
+) => {
+ const Wrapper = ({ children }: { children: ReactElement }) => (
+
+ mocks={{
+ HealthIndicatorGraph: { healthIndicatorData: mockHealthIndicatorData },
+ }}
+ >
+ {children}
+
+ );
+ return Wrapper;
+};
+
+const accountListId = 'account-list-1';
+
+describe('weightedAverage', () => {
+ it('returns null when items is empty', () => {
+ expect(weightedAverage([], 'field', [])).toBeNull();
+ });
+
+ it('returns 0 when items contains only missing values', () => {
+ expect(
+ weightedAverage([{ field: null }, { field: undefined }], 'field', [1, 1]),
+ ).toBe(0);
+ });
+
+ it('calculates the weighted average of the field', () => {
+ expect(
+ weightedAverage(
+ [{ field: 1 }, { field: 3 }, { field: 5 }],
+ 'field',
+ [1, 2, 1],
+ ),
+ ).toBe(3);
+ });
+
+ it('ignores missing values', () => {
+ expect(
+ weightedAverage(
+ [
+ { field: 1 },
+ { field: null },
+ { field: 2 },
+ { field: undefined },
+ { field: 3 },
+ ],
+ 'field',
+ [1, 1, 1, 1, 1],
+ ),
+ ).toBe(1.2);
+ });
+});
+
+describe('uniqueMonths', () => {
+ it('returns an empty set when data is undefined', () => {
+ expect(uniqueMonths(undefined)).toEqual(new Set());
+ });
+
+ it('returns the unique months', () => {
+ const data = gqlMock<
+ HealthIndicatorGraphQuery,
+ HealthIndicatorQueryVariables
+ >(HealthIndicatorGraphDocument, {
+ variables: { accountListId },
+ mocks: {
+ healthIndicatorData: [
+ { indicationPeriodBegin: '2024-01-01' },
+ { indicationPeriodBegin: '2024-01-02' },
+ { indicationPeriodBegin: '2024-01-03' },
+ { indicationPeriodBegin: '2024-02-04' },
+ { indicationPeriodBegin: '2024-04-05' },
+ ],
+ },
+ });
+ expect(uniqueMonths(data)).toEqual(
+ new Set(['2024-01', '2024-02', '2024-04']),
+ );
+ });
+});
+
+const makePeriod = (
+ indicationPeriodBegin: string,
+): HealthIndicatorGraphQuery['healthIndicatorData'][number] => ({
+ id: '',
+ indicationPeriodBegin,
+});
+
+describe('calculatePeriodSpans', () => {
+ it('returns all 1s when no periods are missing', () => {
+ expect(
+ calculatePeriodSpans([
+ makePeriod('2024-01-05'),
+ makePeriod('2024-01-06'),
+ makePeriod('2024-01-07'),
+ makePeriod('2024-01-08'),
+ makePeriod('2024-01-09'),
+ makePeriod('2024-01-10'),
+ ]),
+ ).toEqual([1, 1, 1, 1, 1, 1]);
+ });
+
+ it('returns an empty array when there are no periods', () => {
+ expect(calculatePeriodSpans([])).toEqual([]);
+ });
+
+ it('extrapolates missing days', () => {
+ expect(
+ calculatePeriodSpans([
+ makePeriod('2024-01-05'),
+ makePeriod('2024-01-15'),
+ makePeriod('2024-01-28'),
+ ]),
+ ).toEqual([
+ 10, // January 5-14
+ 13, // January 15-27
+ 1, // January 28
+ ]);
+ });
+
+ it('handles spanning multiple months', () => {
+ expect(
+ calculatePeriodSpans([
+ makePeriod('2024-01-30'),
+ makePeriod('2024-02-03'),
+ makePeriod('2024-03-03'),
+ makePeriod('2024-03-04'),
+ makePeriod('2024-03-06'),
+ ]),
+ ).toEqual([
+ 4, // January 30-February 2
+ 29, // February 2-March 2
+ 1, // March 3
+ 2, // March 4-5
+ 1, // March 6
+ ]);
+ });
+
+ it('handles spanning daylight savings start', () => {
+ expect(
+ calculatePeriodSpans([
+ makePeriod('2024-03-09'),
+ makePeriod('2024-03-10'),
+ ]),
+ ).toEqual([
+ 1, // March 9
+ 1, // March 10
+ ]);
+ });
+});
+
+describe('useGraphData', () => {
+ const Wrapper = makeWrapper([
+ {
+ indicationPeriodBegin: '2024-01-10',
+ consistencyHi: 10,
+ depthHi: 10,
+ ownershipHi: 10,
+ successHi: 10,
+ },
+ {
+ indicationPeriodBegin: '2024-01-31',
+ consistencyHi: 40,
+ depthHi: 40,
+ ownershipHi: 40,
+ successHi: 40,
+ },
+ {
+ indicationPeriodBegin: '2024-02-15',
+ consistencyHi: null,
+ depthHi: null,
+ ownershipHi: null,
+ successHi: null,
+ },
+ {
+ indicationPeriodBegin: '2024-03-04',
+ consistencyHi: 40,
+ depthHi: 40,
+ ownershipHi: 40,
+ successHi: 40,
+ },
+ {
+ indicationPeriodBegin: '2024-03-06',
+ consistencyHi: null,
+ depthHi: null,
+ ownershipHi: null,
+ successHi: null,
+ },
+ {
+ indicationPeriodBegin: '2024-03-10',
+ consistencyHi: 50,
+ depthHi: 50,
+ ownershipHi: 50,
+ successHi: 50,
+ },
+ ]);
+
+ it('loading is true while the data is loading', async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => useGraphData(accountListId),
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ expect(result.current.loading).toBe(true);
+ await waitForNextUpdate();
+ expect(result.current.loading).toBe(false);
+ });
+
+ it('calculates the average overall value ignoring missing months', async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => useGraphData(accountListId),
+ {
+ wrapper: makeWrapper([
+ { indicationPeriodBegin: '2024-01-10', overallHi: 10 },
+ { indicationPeriodBegin: '2024-02-15', overallHi: null },
+ { indicationPeriodBegin: '2024-02-25', overallHi: 80 },
+ { indicationPeriodBegin: '2024-02-26', overallHi: 90 },
+ ]),
+ },
+ );
+
+ expect(result.current.average).toBe(null);
+ await waitForNextUpdate();
+ // Jan 10 - Feb 14 = 36 span * 10 HI
+ // Feb 15 - Feb 24 = 10 span * 0 HI
+ // Feb 25 = 1 span * 80 HI
+ // Feb 26 = 1 span * 90 HI
+ // Average = (36*10 + 10*0 + 1*80 + 1*90) / 48 = ~11.04, rounds to 11
+ expect(result.current.average).toBe(11);
+ });
+
+ it('extrapolates missing periods and averages periods in the same month', async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => useGraphData(accountListId),
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ expect(result.current.periods).toBe(null);
+ await waitForNextUpdate();
+ expect(result.current.periods).toEqual([
+ {
+ // Jan 10 - Jan 30 = 21 span * 10 HI
+ // Jan 31 = 1 span * 40 HI
+ // Average = (21*10 + 1*40) / 22 = ~11.36, rounds to 11
+ month: 'Jan 2024',
+ consistency: 11,
+ depth: 11,
+ ownership: 11,
+ success: 11,
+ consistencyScaled: 2,
+ depthScaled: 2,
+ ownershipScaled: 5,
+ successScaled: 3,
+ },
+ {
+ // No February periods, so all indicators are null
+ month: 'Feb 2024',
+ consistency: null,
+ depth: null,
+ ownership: null,
+ success: null,
+ consistencyScaled: null,
+ depthScaled: null,
+ ownershipScaled: null,
+ successScaled: null,
+ },
+ {
+ // Mar 4 - Mar 5 = 2 span * 40 HI
+ // Mar 6 - Mar 9 = 4 span * 0 HI
+ // Mar 10 = 1 span * 50 HI
+ // Average = (2*40 + 4*0 + 1*50) / 7 = ~18.57, rounds to 19
+ month: 'Mar 2024',
+ consistency: 19,
+ depth: 19,
+ ownership: 19,
+ success: 19,
+ consistencyScaled: 3,
+ depthScaled: 3,
+ ownershipScaled: 8,
+ successScaled: 5,
+ },
+ ]);
+ });
+
+ it('excludes missing indicators from the weighted average', async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => useGraphData(accountListId),
+ {
+ wrapper: makeWrapper([
+ {
+ indicationPeriodBegin: '2024-01-01',
+ consistencyHi: 10,
+ depthHi: 20,
+ ownershipHi: 30,
+ successHi: 40,
+ },
+ {
+ indicationPeriodBegin: '2024-02-01',
+ consistencyHi: null,
+ depthHi: null,
+ ownershipHi: 30,
+ successHi: 40,
+ },
+ {
+ indicationPeriodBegin: '2024-03-01',
+ consistencyHi: 10,
+ depthHi: null,
+ ownershipHi: null,
+ successHi: 40,
+ },
+ ]),
+ },
+ );
+
+ expect(result.current.periods).toBe(null);
+ await waitForNextUpdate();
+ expect(result.current.periods).toEqual([
+ expect.objectContaining({
+ month: 'Jan 2024',
+ consistencyScaled: 1, // 10 / 7 = ~1.43, rounds to 1
+ depthScaled: 3, // 20 / 7 = ~2.86, rounds to 3
+ ownershipScaled: 13, // 30 * 3 / 7 = ~12.86, rounds to 13
+ successScaled: 11, // 40 * 2 / 7 = ~11.43, rounds to 11
+ }),
+ expect.objectContaining({
+ month: 'Feb 2024',
+ consistencyScaled: null, // missing
+ depthScaled: null, // missing
+ ownershipScaled: 18, // 30 * 3 / 5 = 18
+ successScaled: 16, // 40 * 2 / 5 = 16
+ }),
+ expect.objectContaining({
+ month: 'Mar 2024',
+ consistencyScaled: 3, // 10 / 3 = ~3.33, rounds to 3
+ depthScaled: null, // missing
+ ownershipScaled: null, // missing
+ successScaled: 27, // 40 * 2 / 3 = ~26.67, rounds to 27
+ }),
+ ]);
+ });
+});
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/useGraphData.ts b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/useGraphData.ts
new file mode 100644
index 0000000000..aab1adef46
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorGraph/useGraphData.ts
@@ -0,0 +1,189 @@
+import { useMemo } from 'react';
+import { DateTime } from 'luxon';
+import { useLocale } from 'src/hooks/useLocale';
+import { monthYearFormat } from 'src/lib/intlFormat';
+import {
+ HealthIndicatorGraphQuery,
+ useHealthIndicatorGraphQuery,
+} from './HealthIndicatorGraph.generated';
+
+export interface Period {
+ month: string;
+ consistency: number | null | undefined;
+ consistencyScaled: number | null | undefined;
+ depth: number | null | undefined;
+ depthScaled: number | null | undefined;
+ ownership: number | null | undefined;
+ ownershipScaled: number | null | undefined;
+ success: number | null | undefined;
+ successScaled: number | null | undefined;
+}
+
+interface UseGraphDataResult {
+ loading: boolean;
+ average: number | null;
+ periods: Period[] | null;
+}
+
+// Round a health indicator value if it is set
+const round = (value: number | null): number | null => {
+ return value === null ? null : Math.round(value);
+};
+
+/**
+ * Calculate the weighted average value of a particular field in an array of items, treating missing
+ * values as 0.
+ *
+ * @param items An array of records with a property {@link field} that is `number | null | undefined`
+ * @param field The field in {@link items} to be averaged
+ * @param weights An array of each item's weight. It must have the same length as {@link items}.
+ * @returns `null` if no records had the field, or the average otherwise
+ */
+export const weightedAverage = <
+ Item extends { [_ in Field]?: number | null | undefined },
+ Field extends keyof Item,
+>(
+ items: Array- ,
+ field: Field,
+ weights: number[],
+): number | null => {
+ const { total, denominator } = items.reduce(
+ ({ total, denominator }, item, index) => {
+ const value = item[field] ?? 0;
+ const weight = weights[index];
+ return {
+ total: total + value * weight,
+ denominator: denominator + weight,
+ };
+ },
+ { total: 0, denominator: 0 },
+ );
+
+ return denominator === 0 ? null : total / denominator;
+};
+
+/**
+ * Calculate the unique months represented in the health indicator periods.
+ */
+export const uniqueMonths = (
+ data: HealthIndicatorGraphQuery | undefined,
+): Set
=> {
+ return new Set(
+ data?.healthIndicatorData.map((period) =>
+ // Extract the year and month from the ISO timestamp
+ period.indicationPeriodBegin.slice(0, 7),
+ ),
+ );
+};
+
+/**
+ * If there are missing periods, we need to extrapolate the existing periods to fill in the missing
+ * ones. When there are missing periods, the periods around them will need to span multiple days
+ * instead of just a single day. This code calculates how many days each period spans.
+ *
+ * For example:
+ * - In a month without missing periods, this will be an array of 1s, i.e. [1, 1, 1, 1, 1, ...].
+ * - In January with periods for the 5th, 15th, and 25th, this will be [10, 10, 1] because the
+ * first period spans from January 5-14, the second spans from January 15-24, and the third
+ * only covers January 25.
+ *
+ * See the test cases for more examples of the expected outputs of various inputs.
+ */
+export const calculatePeriodSpans = (
+ periods: HealthIndicatorGraphQuery['healthIndicatorData'],
+): number[] => {
+ return periods.map((period, index) => {
+ // The last period always has a span of 1
+ if (index === periods.length - 1) {
+ return 1;
+ }
+
+ const start = DateTime.fromISO(period.indicationPeriodBegin);
+ // Periods end at the start of the next period
+ const end = DateTime.fromISO(periods[index + 1].indicationPeriodBegin);
+ // Calculate how many days the period spans
+ return end.diff(start, 'days').days;
+ });
+};
+
+export const useGraphData = (accountListId: string): UseGraphDataResult => {
+ const locale = useLocale();
+
+ const { data, loading } = useHealthIndicatorGraphQuery({
+ variables: {
+ accountListId,
+ },
+ });
+
+ const averageOverallHi = useMemo(() => {
+ if (!data) {
+ return null;
+ }
+
+ const periodSpans = calculatePeriodSpans(data.healthIndicatorData);
+ return weightedAverage(data.healthIndicatorData, 'overallHi', periodSpans);
+ }, [data]);
+
+ const periods = useMemo(() => {
+ if (!data) {
+ return null;
+ }
+
+ return Array.from(uniqueMonths(data).values()).map((isoMonth) => {
+ const periods = data.healthIndicatorData.filter((period) =>
+ period.indicationPeriodBegin.startsWith(isoMonth),
+ );
+
+ const periodSpans = calculatePeriodSpans(periods);
+ // Convert 0s to null
+ const consistency =
+ weightedAverage(periods, 'consistencyHi', periodSpans) || null;
+ const depth = weightedAverage(periods, 'depthHi', periodSpans) || null;
+ const ownership =
+ weightedAverage(periods, 'ownershipHi', periodSpans) || null;
+ const success =
+ weightedAverage(periods, 'successHi', periodSpans) || null;
+
+ // Determine the weighted average denominator
+ const CONSISTENCY_WEIGHT = 1;
+ const DEPTH_WEIGHT = 1;
+ const OWNERSHIP_WEIGHT = 3;
+ const SUCCESS_WEIGHT = 2;
+ const totalWeights =
+ (consistency === null ? 0 : CONSISTENCY_WEIGHT) +
+ (depth === null ? 0 : DEPTH_WEIGHT) +
+ (ownership === null ? 0 : OWNERSHIP_WEIGHT) +
+ (success === null ? 0 : SUCCESS_WEIGHT);
+ return {
+ month: monthYearFormat(DateTime.fromISO(isoMonth), locale),
+ consistency: round(consistency),
+ depth: round(depth),
+ ownership: round(ownership),
+ success: round(success),
+ // Scale the health indicator values by their weight in the overall calculation, ignoring missing values
+ consistencyScaled:
+ consistency === null
+ ? null
+ : Math.round((consistency * CONSISTENCY_WEIGHT) / totalWeights),
+ depthScaled:
+ depth === null
+ ? null
+ : Math.round((depth * DEPTH_WEIGHT) / totalWeights),
+ ownershipScaled:
+ ownership === null
+ ? null
+ : Math.round((ownership * OWNERSHIP_WEIGHT) / totalWeights),
+ successScaled:
+ success === null
+ ? null
+ : Math.round((success * SUCCESS_WEIGHT) / totalWeights),
+ };
+ });
+ }, [data]);
+
+ return {
+ loading,
+ average: round(averageOverallHi),
+ periods,
+ };
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorReport.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorReport.tsx
new file mode 100644
index 0000000000..13fc8be7c8
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorReport.tsx
@@ -0,0 +1,88 @@
+import React, { useState } from 'react';
+import { Box, Container, Grid, Typography } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { useTranslation } from 'react-i18next';
+import MonthlyGoal from 'src/components/Dashboard/MonthlyGoal/MonthlyGoal';
+import {
+ HeaderTypeEnum,
+ MultiPageHeader,
+} from 'src/components/Shared/MultiPageLayout/MultiPageHeader';
+import { HealthIndicatorFormula } from './HealthIndicatorFormula/HealthIndicatorFormula';
+import { HealthIndicatorGraph } from './HealthIndicatorGraph/HealthIndicatorGraph';
+import { useMonthlyGoalQuery } from './MonthlyGoal.generated';
+
+const GraphTitle = styled(Typography)(({ theme }) => ({
+ marginBottom: theme.spacing(2),
+}));
+interface HealthIndicatorReportProps {
+ accountListId: string;
+ isNavListOpen: boolean;
+ onNavListToggle: () => void;
+ title: string;
+}
+
+export const HealthIndicatorReport: React.FC = ({
+ accountListId,
+ isNavListOpen,
+ onNavListToggle,
+ title,
+}) => {
+ const { t } = useTranslation();
+ const [noHealthIndicatorData, setNoHealthIndicatorData] = useState(false);
+ const { data } = useMonthlyGoalQuery({
+ variables: {
+ accountListId,
+ },
+ });
+
+ return (
+
+
+
+ {noHealthIndicatorData ? (
+
+
+
+ {t('No Health Indicator data available')}
+
+
+ {t(
+ 'Health Indicator data is only available for staff who are paid in countries that use NetSuite. If you are unsure what that means or need help, contact your financial office.',
+ )}
+
+
+
+ ) : (
+
+
+
+
+
+ {t('Health Indicator')}
+
+
+
+
+ {t('MPD Health Formula')}
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.test.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.test.tsx
new file mode 100644
index 0000000000..00c8549bda
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.test.tsx
@@ -0,0 +1,96 @@
+import { render } from '@testing-library/react';
+import { gqlMock } from '__tests__/util/graphqlMocking';
+import {
+ HealthIndicatorDocument,
+ HealthIndicatorQuery,
+ HealthIndicatorQueryVariables,
+} from 'src/components/Dashboard/MonthlyGoal/HealthIndicator.generated';
+import { HealthIndicatorWidget } from './HealthIndicatorWidget';
+
+const accountListId = 'account-list-1';
+const healthIndicatorScore = {
+ overallHi: 90,
+ ownershipHi: 80,
+ consistencyHi: 70,
+ successHi: 60,
+ depthHi: 50,
+};
+
+interface ComponentsProps {
+ loading?: boolean;
+ onDashboard?: boolean;
+}
+
+const Components = ({
+ loading = false,
+ onDashboard = true,
+}: ComponentsProps) => {
+ const { healthIndicatorData } = gqlMock<
+ HealthIndicatorQuery,
+ HealthIndicatorQueryVariables
+ >(HealthIndicatorDocument, {
+ variables: {
+ accountListId,
+ },
+ mocks: {
+ accountList: {
+ healthIndicatorData: healthIndicatorScore,
+ },
+ },
+ }).accountList;
+
+ return (
+
+ );
+};
+
+describe('HealthIndicatorWidget', () => {
+ describe('On Dashboard', () => {
+ it('should show the view details button', async () => {
+ const { findByRole } = render( );
+
+ expect(
+ await findByRole('link', { name: 'View Details' }),
+ ).toBeInTheDocument();
+ });
+
+ it('should not show view details button if not on dashboard', async () => {
+ const { findByText, queryByRole } = render(
+ ,
+ );
+
+ expect(await findByText('Ownership')).toBeInTheDocument();
+
+ expect(
+ queryByRole('button', { name: 'View Details' }),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ it('renders the data correctly', async () => {
+ const { findByText, getByText } = render( );
+
+ expect(await findByText('Ownership')).toBeInTheDocument();
+ expect(getByText('MPD Health Indicator')).toBeInTheDocument();
+
+ expect(getByText('90')).toBeInTheDocument();
+ expect(getByText('Overall Health Indicator')).toBeInTheDocument();
+
+ expect(getByText('80')).toBeInTheDocument();
+ expect(getByText('Ownership')).toBeInTheDocument();
+
+ expect(getByText('70')).toBeInTheDocument();
+ expect(getByText('Consistency')).toBeInTheDocument();
+
+ expect(getByText('60')).toBeInTheDocument();
+ expect(getByText('Success')).toBeInTheDocument();
+
+ expect(getByText('50')).toBeInTheDocument();
+ expect(getByText('Depth')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.tsx
new file mode 100644
index 0000000000..234a67eedb
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.tsx
@@ -0,0 +1,126 @@
+import NextLink from 'next/link';
+import React from 'react';
+import {
+ Box,
+ Button,
+ CardActions,
+ CardContent,
+ CardHeader,
+ Grid,
+ Tooltip,
+ Typography,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { useTranslation } from 'react-i18next';
+import AnimatedCard from 'src/components/AnimatedCard';
+import { HealthIndicatorQuery } from 'src/components/Dashboard/MonthlyGoal/HealthIndicator.generated';
+import StyledProgress from 'src/components/StyledProgress';
+import { WidgetStat } from './WidgetStat/WidgetStat';
+
+const StyledCardContent = styled(CardContent)(({ theme }) => ({
+ padding: theme.spacing(2),
+ paddingBottom: 0,
+ height: 'calc(100% - 103px)',
+}));
+
+const StyledBox = styled(Box)(() => ({
+ display: 'flex',
+ gap: 2,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+}));
+
+interface HealthIndicatorWidgetProps {
+ accountListId: string;
+ onDashboard: boolean;
+ loading: boolean;
+ data: HealthIndicatorQuery['accountList']['healthIndicatorData'];
+}
+
+export const HealthIndicatorWidget: React.FC = ({
+ accountListId,
+ onDashboard = true,
+ loading,
+ data,
+}) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+
+ {data?.overallHi}
+
+
+
+ {t('Overall Health Indicator')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {onDashboard && (
+
+
+ {t('View Details')}
+
+
+ )}
+
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/WidgetStat/WidgetStat.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/WidgetStat/WidgetStat.tsx
new file mode 100644
index 0000000000..88f7890b62
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/WidgetStat/WidgetStat.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { Box, Grid, Skeleton, Tooltip, Typography } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { Maybe } from 'src/graphql/types.generated';
+
+const WidgetStatGrid = styled(Grid)(({ theme }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(0.5),
+ marginBottom: theme.spacing(1.5),
+}));
+
+interface WidgetStatProps {
+ loading: boolean;
+ stat?: Maybe;
+ statName: string;
+ toolTip: string;
+}
+
+export const WidgetStat: React.FC = ({
+ loading,
+ stat,
+ statName,
+ toolTip,
+}) => (
+
+ {loading ? (
+
+ ) : (
+
+
+ {stat}
+
+ {statName}
+
+
+
+ )}
+
+);
diff --git a/src/components/Reports/HealthIndicatorReport/MonthlyGoal.graphql b/src/components/Reports/HealthIndicatorReport/MonthlyGoal.graphql
new file mode 100644
index 0000000000..24b70447c8
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/MonthlyGoal.graphql
@@ -0,0 +1,16 @@
+query MonthlyGoal($accountListId: ID!) {
+ accountList(id: $accountListId) {
+ id
+ monthlyGoal
+ monthlyGoalUpdatedAt
+ receivedPledges
+ totalPledges
+ currency
+ }
+ contacts(
+ accountListId: $accountListId
+ contactsFilter: { pledgeReceived: NOT_RECEIVED, status: PARTNER_FINANCIAL }
+ ) {
+ totalCount
+ }
+}
diff --git a/src/components/Reports/HealthIndicatorReport/useIndicatorColors.ts b/src/components/Reports/HealthIndicatorReport/useIndicatorColors.ts
new file mode 100644
index 0000000000..6779dd5e6b
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/useIndicatorColors.ts
@@ -0,0 +1,19 @@
+import { useTheme } from '@mui/material/styles';
+
+interface IndicatorColors {
+ ownership: string;
+ success: string;
+ consistency: string;
+ depth: string;
+}
+
+export const useIndicatorColors = (): IndicatorColors => {
+ const { palette } = useTheme();
+
+ return {
+ ownership: palette.cyan.main,
+ success: palette.pink.main,
+ consistency: palette.green.main,
+ depth: palette.yellow.main,
+ };
+};
diff --git a/src/components/Settings/Accounts/MergeForm/MergeForm.tsx b/src/components/Settings/Accounts/MergeForm/MergeForm.tsx
index c2d516d19f..40d9984445 100644
--- a/src/components/Settings/Accounts/MergeForm/MergeForm.tsx
+++ b/src/components/Settings/Accounts/MergeForm/MergeForm.tsx
@@ -37,7 +37,7 @@ const BorderBox = styled(Box, {
shouldForwardProp: (prop) => prop !== 'isSpouse',
})(({ isSpouse }: { isSpouse: boolean }) => ({
border: isSpouse ? '1px solid' : 'none',
- borderColor: theme.palette.cruYellow.main,
+ borderColor: theme.palette.yellow.main,
borderRadius: theme.shape.borderRadius,
padding: isSpouse ? '10px' : '0',
}));
@@ -199,7 +199,7 @@ export const MergeForm: React.FC = ({ isSpouse }) => {
{account.name}
({account.id})
@@ -261,7 +261,7 @@ export const MergeForm: React.FC = ({ isSpouse }) => {
{currentAccount.name}
({currentAccount.id})
diff --git a/src/components/Settings/Organization/AccountLists/AccountListRow/AccountListRow.tsx b/src/components/Settings/Organization/AccountLists/AccountListRow/AccountListRow.tsx
index d6f2bd7b49..5016cedc46 100644
--- a/src/components/Settings/Organization/AccountLists/AccountListRow/AccountListRow.tsx
+++ b/src/components/Settings/Organization/AccountLists/AccountListRow/AccountListRow.tsx
@@ -44,7 +44,7 @@ export interface AccountListRowProps {
const BorderRightGrid = styled(Grid)(() => ({
borderRight: '1px solid',
- borderColor: theme.palette.cruGrayLight.main,
+ borderColor: theme.palette.mpdxGrayLight.main,
}));
const NoItemsBox = styled(Box)(() => ({
@@ -341,7 +341,7 @@ export const AccountListRow: React.FC = ({
container
style={{
borderBottom: '1px solid',
- borderColor: theme.palette.cruGrayLight.main,
+ borderColor: theme.palette.mpdxGrayLight.main,
}}
>
diff --git a/src/components/Settings/Organization/AccountLists/AccountListRow/accountListRowHelper.ts b/src/components/Settings/Organization/AccountLists/AccountListRow/accountListRowHelper.ts
index 440227f26b..2317493c41 100644
--- a/src/components/Settings/Organization/AccountLists/AccountListRow/accountListRowHelper.ts
+++ b/src/components/Settings/Organization/AccountLists/AccountListRow/accountListRowHelper.ts
@@ -4,7 +4,7 @@ import theme from 'src/theme';
export const BorderBottomBox = styled(Box)(() => ({
borderBottom: '1px solid',
- borderColor: theme.palette.cruGrayLight.main,
+ borderColor: theme.palette.mpdxGrayLight.main,
padding: theme.spacing(1),
'&:last-child': {
borderBottom: '0px',
diff --git a/src/components/Settings/Organization/ManageOrganizationAccess/ManageOrganizationAccessAccordion.tsx b/src/components/Settings/Organization/ManageOrganizationAccess/ManageOrganizationAccessAccordion.tsx
index a40bd303f8..0e7bbb603a 100644
--- a/src/components/Settings/Organization/ManageOrganizationAccess/ManageOrganizationAccessAccordion.tsx
+++ b/src/components/Settings/Organization/ManageOrganizationAccess/ManageOrganizationAccessAccordion.tsx
@@ -54,7 +54,7 @@ const StyledBox = styled(Box)(() => ({
const StyledListItem = styled(ListItem)(() => ({
borderRadius: '6px',
'&:nth-child(even)': {
- background: theme.palette.cruGrayLight.main,
+ background: theme.palette.mpdxGrayLight.main,
},
}));
diff --git a/src/components/Settings/integrations/Google/GoogleAccordion.tsx b/src/components/Settings/integrations/Google/GoogleAccordion.tsx
index fa35354135..8335d15c56 100644
--- a/src/components/Settings/integrations/Google/GoogleAccordion.tsx
+++ b/src/components/Settings/integrations/Google/GoogleAccordion.tsx
@@ -44,7 +44,7 @@ const EditIconButton = styled(IconButton)(() => ({
},
}));
const DeleteIconButton = styled(IconButton)(() => ({
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
marginLeft: '10px',
'&:disabled': {
cursor: 'not-allowed',
@@ -164,7 +164,7 @@ export const GoogleAccordion: React.FC = ({
googleAccounts?.map((account) => (
= ({
({
- color: theme.palette.cruGrayMedium.main,
+ color: theme.palette.mpdxGrayMedium.main,
marginLeft: '10px',
'&:disabled': {
cursor: 'not-allowed',
@@ -223,7 +223,7 @@ export const OrganizationAccordion: React.FC = ({
sx={{
p: 1,
pl: 2,
- background: theme.palette.cruGrayLight.main,
+ background: theme.palette.mpdxGrayLight.main,
justifyContent: 'space-between',
display: 'flex',
}}
diff --git a/src/components/Settings/preferences/GetAccountPreferences.graphql b/src/components/Settings/preferences/GetAccountPreferences.graphql
index ca9958e4db..9bc942b472 100644
--- a/src/components/Settings/preferences/GetAccountPreferences.graphql
+++ b/src/components/Settings/preferences/GetAccountPreferences.graphql
@@ -1,12 +1,13 @@
fragment AccountList on AccountList {
id
+ currency
name
+ monthlyGoalUpdatedAt
activeMpdMonthlyGoal
activeMpdFinishAt
activeMpdStartAt
salaryOrganizationId
settings {
- currency
homeCountry
monthlyGoal
tester
diff --git a/src/components/Settings/preferences/accordions/CurrencyAccordion/CurrencyAccordion.tsx b/src/components/Settings/preferences/accordions/CurrencyAccordion/CurrencyAccordion.tsx
index f5173f72a6..3aa6e84fc3 100644
--- a/src/components/Settings/preferences/accordions/CurrencyAccordion/CurrencyAccordion.tsx
+++ b/src/components/Settings/preferences/accordions/CurrencyAccordion/CurrencyAccordion.tsx
@@ -20,7 +20,7 @@ const preferencesSchema: yup.ObjectSchema<
});
interface CurrencyAccordionProps extends AccordionProps {
- currency: string;
+ currency: string | null;
accountListId: string;
disabled?: boolean;
}
@@ -74,7 +74,7 @@ export const CurrencyAccordion: React.FC = ({
onAccordionChange={handleAccordionChange}
expandedAccordion={expandedAccordion}
label={label}
- value={currency}
+ value={currency ?? ''}
fullWidth
disabled={disabled}
>
diff --git a/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MachineCalculatedGoal.graphql b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MachineCalculatedGoal.graphql
new file mode 100644
index 0000000000..9f84eba2f8
--- /dev/null
+++ b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MachineCalculatedGoal.graphql
@@ -0,0 +1,9 @@
+query MachineCalculatedGoal($accountListId: ID!) {
+ accountList(id: $accountListId) {
+ healthIndicatorData {
+ id
+ machineCalculatedGoal
+ machineCalculatedGoalCurrency
+ }
+ }
+}
diff --git a/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.test.tsx b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.test.tsx
index 99fb0dcb5f..30ae719354 100644
--- a/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.test.tsx
+++ b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.test.tsx
@@ -6,6 +6,7 @@ import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import { PreferenceAccordion } from 'src/components/Shared/Forms/Accordions/AccordionEnum';
import theme from 'src/theme';
+import { MachineCalculatedGoalQuery } from './MachineCalculatedGoal.generated';
import { MonthlyGoalAccordion } from './MonthlyGoalAccordion';
jest.mock('next-auth/react');
@@ -34,21 +35,41 @@ const mutationSpy = jest.fn();
interface ComponentsProps {
monthlyGoal: number | null;
+ monthlyGoalUpdatedAt?: string | null;
+ machineCalculatedGoal?: number;
+ machineCalculatedGoalCurrency?: string | null;
expandedAccordion: PreferenceAccordion | null;
}
const Components: React.FC = ({
monthlyGoal,
+ monthlyGoalUpdatedAt = null,
+ machineCalculatedGoal,
+ machineCalculatedGoalCurrency = 'USD',
expandedAccordion,
}) => (
-
+
+ mocks={{
+ MachineCalculatedGoal: {
+ accountList: {
+ healthIndicatorData: machineCalculatedGoal
+ ? { machineCalculatedGoal, machineCalculatedGoalCurrency }
+ : null,
+ },
+ },
+ }}
+ onCall={mutationSpy}
+ >
{
afterEach(() => {
mutationSpy.mockClear();
});
- it('should render accordion closed', () => {
- const { getByText, queryByRole } = render(
- ,
- );
- expect(getByText(label)).toBeInTheDocument();
- expect(queryByRole('textbox')).not.toBeInTheDocument();
+ describe('closed', () => {
+ it('renders label and hides the textbox', () => {
+ const { getByText, queryByRole } = render(
+ ,
+ );
+
+ expect(getByText(label)).toBeInTheDocument();
+ expect(queryByRole('textbox')).not.toBeInTheDocument();
+ });
+
+ it('renders goal without updated date', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('AccordionSummaryValue')).toHaveTextContent('$100');
+ });
+
+ it('renders goal and updated date', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('AccordionSummaryValue')).toHaveTextContent(
+ '$100 (last updated Jan 1, 2024)',
+ );
+ });
+
+ it('renders too low warning', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('AccordionSummaryValue')).toHaveTextContent(
+ '$100 (below NetSuite-calculated support goal)',
+ ),
+ );
+ });
+
+ it('hides too low warning when currencies do not match', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('AccordionSummaryValue')).not.toHaveTextContent(
+ /below NetSuite-calculated support goal/,
+ ),
+ );
+ });
+
+ it('renders calculated goal', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('AccordionSummaryValue')).toHaveTextContent(
+ '€1,000 (estimated)',
+ ),
+ );
+ });
+
+ it('renders calculated goal without currency', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('AccordionSummaryValue')).toHaveTextContent(
+ '1,000 (estimated)',
+ ),
+ );
+ });
+
+ it('renders only goal when calculated goal is missing', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('AccordionSummaryValue')).toHaveTextContent('$100'),
+ );
+ });
+
+ it('renders nothing when goal and calculated goal are missing', async () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(queryByTestId('AccordionSummaryValue')).not.toBeInTheDocument(),
+ );
+ });
});
+
it('should render accordion open and textfield should have a value', () => {
const { getByRole } = render(
{
]);
});
});
+
+ describe('calculated goal', () => {
+ it('resets goal to calculated goal', async () => {
+ const { getByRole, findByText } = render(
+ ,
+ );
+
+ expect(
+ await findByText(
+ /Based on the past year, NetSuite estimates that you need at least €1,500 of monthly support./,
+ ),
+ ).toBeInTheDocument();
+
+ const resetButton = getByRole('button', { name: /Reset/ });
+ userEvent.click(resetButton);
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation('UpdateAccountPreferences', {
+ input: {
+ id: accountListId,
+ attributes: {
+ settings: {
+ monthlyGoal: null,
+ },
+ },
+ },
+ }),
+ );
+ });
+
+ it('hides reset button if goal is null', async () => {
+ const { findByText, queryByRole } = render(
+ ,
+ );
+
+ expect(
+ await findByText(
+ /Based on the past year, NetSuite estimates that you need at least \$1,000 of monthly support./,
+ ),
+ ).toBeInTheDocument();
+ expect(queryByRole('button', { name: /Reset/ })).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx
index da1df2f101..1fd4db8059 100644
--- a/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx
+++ b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx
@@ -1,5 +1,6 @@
import React, { ReactElement, useMemo } from 'react';
-import { TextField } from '@mui/material';
+import WarningIcon from '@mui/icons-material/Warning';
+import { Box, Button, TextField, Tooltip, Typography } from '@mui/material';
import { Formik } from 'formik';
import { useSnackbar } from 'notistack';
import { useTranslation } from 'react-i18next';
@@ -7,12 +8,13 @@ import * as yup from 'yup';
import { PreferenceAccordion } from 'src/components/Shared/Forms/Accordions/AccordionEnum';
import { AccordionItem } from 'src/components/Shared/Forms/Accordions/AccordionItem';
import { FieldWrapper } from 'src/components/Shared/Forms/FieldWrapper';
-import { FormWrapper } from 'src/components/Shared/Forms/FormWrapper';
import { AccountListSettingsInput } from 'src/graphql/types.generated';
import { useLocale } from 'src/hooks/useLocale';
-import { currencyFormat } from 'src/lib/intlFormat';
+import { GoalSource, getHealthIndicatorInfo } from 'src/lib/healthIndicator';
+import { currencyFormat, dateFormat, numberFormat } from 'src/lib/intlFormat';
import { AccordionProps } from '../../../accordionHelper';
import { useUpdateAccountPreferencesMutation } from '../UpdateAccountPreferences.generated';
+import { useMachineCalculatedGoalQuery } from './MachineCalculatedGoal.generated';
const accountPreferencesSchema: yup.ObjectSchema<
Pick
@@ -20,11 +22,27 @@ const accountPreferencesSchema: yup.ObjectSchema<
monthlyGoal: yup.number().required(),
});
+const formatMonthlyGoal = (
+ goal: number | null,
+ currency: string | null,
+ locale: string,
+): string | null => {
+ if (goal === null) {
+ return null;
+ }
+
+ if (currency) {
+ return currencyFormat(goal, currency, locale);
+ }
+ return numberFormat(goal, locale);
+};
+
interface MonthlyGoalAccordionProps
extends AccordionProps {
monthlyGoal: number | null;
+ monthlyGoalUpdatedAt: string | null;
accountListId: string;
- currency: string;
+ currency: string | null;
disabled?: boolean;
handleSetupChange: () => Promise;
}
@@ -32,7 +50,8 @@ interface MonthlyGoalAccordionProps
export const MonthlyGoalAccordion: React.FC = ({
handleAccordionChange,
expandedAccordion,
- monthlyGoal,
+ monthlyGoal: initialMonthlyGoal,
+ monthlyGoalUpdatedAt,
accountListId,
currency,
disabled,
@@ -44,13 +63,62 @@ export const MonthlyGoalAccordion: React.FC = ({
const locale = useLocale();
const label = t('Monthly Goal');
- const monthlyGoalString = useMemo(() => {
- return monthlyGoal && locale && currency
- ? currencyFormat(monthlyGoal, currency, locale)
- : monthlyGoal
- ? String(monthlyGoal)
- : '';
- }, [monthlyGoal, locale, currency]);
+ const { data } = useMachineCalculatedGoalQuery({
+ variables: {
+ accountListId,
+ },
+ });
+
+ const accountList = currency
+ ? { currency, monthlyGoal: initialMonthlyGoal, monthlyGoalUpdatedAt }
+ : null;
+ const healthIndicatorData = data?.accountList.healthIndicatorData;
+ const {
+ goalSource,
+ machineCalculatedGoalCurrency,
+ unsafeMachineCalculatedGoal,
+ preferencesGoalLow,
+ preferencesGoalUpdatedAt,
+ } = getHealthIndicatorInfo(accountList, healthIndicatorData);
+
+ const formattedCalculatedGoal = useMemo(
+ () =>
+ formatMonthlyGoal(
+ unsafeMachineCalculatedGoal,
+ machineCalculatedGoalCurrency,
+ locale,
+ ),
+ [unsafeMachineCalculatedGoal, machineCalculatedGoalCurrency, locale],
+ );
+
+ const accordionValue = useMemo(() => {
+ const goal = formatMonthlyGoal(initialMonthlyGoal, currency, locale);
+
+ if (goalSource === GoalSource.Preferences) {
+ if (preferencesGoalLow) {
+ return (
+
+ {t('{{goal}} (below NetSuite-calculated support goal)', { goal })}
+
+ );
+ } else if (preferencesGoalUpdatedAt) {
+ return t('{{goal}} (last updated {{updated}})', {
+ goal,
+ updated: dateFormat(preferencesGoalUpdatedAt, locale),
+ });
+ } else {
+ return goal;
+ }
+ } else if (formattedCalculatedGoal !== null) {
+ return t('{{goal}} (estimated)', { goal: formattedCalculatedGoal });
+ }
+ }, [
+ initialMonthlyGoal,
+ formattedCalculatedGoal,
+ preferencesGoalUpdatedAt,
+ currency,
+ locale,
+ ]);
const onSubmit = async (
attributes: Pick,
@@ -80,19 +148,65 @@ export const MonthlyGoalAccordion: React.FC = ({
handleSetupChange();
};
+ const getInstructions = () => {
+ if (unsafeMachineCalculatedGoal === null) {
+ return t(
+ 'This amount should be set to the amount your organization has determined is your target monthly goal. If you do not know, make your best guess for now. You can change it at any time.',
+ );
+ }
+
+ if (goalSource === GoalSource.MachineCalculated) {
+ return t(
+ 'Based on the past year, NetSuite estimates that you need at least {{goal}} of monthly support. You can choose your own target monthly goal or leave it blank to use the estimate.',
+ { goal: formattedCalculatedGoal },
+ );
+ } else {
+ return t(
+ 'Based on the past year, NetSuite estimates that you need at least {{goal}} of monthly support. You can choose your own target monthly goal or reset it to use the estimate.',
+ { goal: formattedCalculatedGoal },
+ );
+ }
+ };
+
+ const getWarning = (currentGoal: number | null) => {
+ if (
+ currentGoal &&
+ accountList &&
+ getHealthIndicatorInfo(
+ { ...accountList, monthlyGoal: currentGoal },
+ healthIndicatorData,
+ ).preferencesGoalLow
+ ) {
+ return (
+
+
+ {t(
+ 'Your current monthly goal is less than the amount NetSuite estimates that you need. Please review your goal and adjust it if needed.',
+ )}
+
+ );
+ }
+ };
+
return (
= ({
isValid,
handleChange,
}): ReactElement => (
-
+
+
+
+ {t('Save')}
+
+ {unsafeMachineCalculatedGoal !== null &&
+ goalSource === GoalSource.Preferences && (
+
+ {
+ onSubmit({ monthlyGoal: null });
+ }}
+ >
+ {t('Reset to Calculated Goal')}
+
+
+ )}
+
+
)}
diff --git a/src/components/Shared/Filters/FilterPanel.tsx b/src/components/Shared/Filters/FilterPanel.tsx
index faea7216a6..1bf298371b 100644
--- a/src/components/Shared/Filters/FilterPanel.tsx
+++ b/src/components/Shared/Filters/FilterPanel.tsx
@@ -97,7 +97,7 @@ const LinkButton = styled(Button)(({ theme }) => ({
const FlatAccordion = styled(Accordion)(({ theme }) => ({
'&.MuiPaper-elevation1': {
boxShadow: 'none',
- borderBottom: `1px solid ${theme.palette.cruGrayLight.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayLight.main}`,
},
'& .MuiAccordionDetails-root': {
paddingLeft: 0,
diff --git a/src/components/Shared/Filters/NullState/NullStateBox.tsx b/src/components/Shared/Filters/NullState/NullStateBox.tsx
index 6e751feb74..ae38ef3d09 100644
--- a/src/components/Shared/Filters/NullState/NullStateBox.tsx
+++ b/src/components/Shared/Filters/NullState/NullStateBox.tsx
@@ -4,17 +4,17 @@ import { styled } from '@mui/material/styles';
export const NullStateBox = styled(Box)(({ theme }) => ({
width: '100%',
border: '1px solid',
- borderColor: theme.palette.cruGrayMedium.main,
- color: theme.palette.cruGrayDark.main,
+ borderColor: theme.palette.mpdxGrayMedium.main,
+ color: theme.palette.mpdxGrayDark.main,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
- backgroundColor: theme.palette.cruGrayLight.main,
+ backgroundColor: theme.palette.mpdxGrayLight.main,
paddingTop: theme.spacing(7),
paddingBottom: theme.spacing(7),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
textAlign: 'center',
- boxShadow: `0px 0px 5px ${theme.palette.cruGrayMedium.main} inset`,
+ boxShadow: `0px 0px 5px ${theme.palette.mpdxGrayMedium.main} inset`,
}));
diff --git a/src/components/Shared/Filters/TagsSection/FilterPanelTagsSection.tsx b/src/components/Shared/Filters/TagsSection/FilterPanelTagsSection.tsx
index f90c60c1fc..a35b8f244c 100644
--- a/src/components/Shared/Filters/TagsSection/FilterPanelTagsSection.tsx
+++ b/src/components/Shared/Filters/TagsSection/FilterPanelTagsSection.tsx
@@ -44,7 +44,7 @@ const TagsSectionWrapper = styled(Box)(({ theme }) => ({
const TagsAccordionWrapper = styled(Box)(() => ({
'& .MuiPaper-elevation1': {
boxShadow: 'none',
- borderBottom: `1px solid ${theme.palette.cruGrayLight.main}`,
+ borderBottom: `1px solid ${theme.palette.mpdxGrayLight.main}`,
},
'& .MuiAccordion-root.Mui-expanded': {
margin: 0,
diff --git a/src/components/Shared/Forms/Accordions/AccordionItem.tsx b/src/components/Shared/Forms/Accordions/AccordionItem.tsx
index 572610d4f5..18bda49004 100644
--- a/src/components/Shared/Forms/Accordions/AccordionItem.tsx
+++ b/src/components/Shared/Forms/Accordions/AccordionItem.tsx
@@ -1,40 +1,22 @@
import React from 'react';
import { ExpandMore } from '@mui/icons-material';
import {
- Accordion,
AccordionDetails,
AccordionSummary,
Box,
Typography,
} from '@mui/material';
import { styled } from '@mui/material/styles';
+import { GroupedAccordion } from './GroupedAccordion';
-export const accordionShared = {
- '&:before': {
- content: 'none',
- },
- '& .MuiAccordionSummary-root.Mui-expanded': {
- minHeight: 'unset',
- },
-};
-
-const StyledAccordion = styled(Accordion)(({ theme }) => ({
- overflow: 'hidden',
+const StyledAccordion = styled(GroupedAccordion)(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
- '&:not(:last-child)': {
- borderBottom: 0,
- },
- '&.MuiAccordion-rounded.Mui-disabled': {
- color: theme.palette.cruGrayDark,
+ '&.Mui-disabled': {
backgroundColor: 'white',
},
- ...accordionShared,
}));
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
- '&.Mui-expanded': {
- backgroundColor: theme.palette.mpdxYellow.main,
- },
'& .MuiAccordionSummary-content': {
[theme.breakpoints.only('xs')]: {
flexDirection: 'column',
@@ -122,7 +104,7 @@ interface AccordionItemProps {
onAccordionChange: (accordion: AccordionEnum | null) => void;
expandedAccordion: AccordionEnum | null;
label: string;
- value: string;
+ value: React.ReactNode;
children?: React.ReactNode;
fullWidth?: boolean;
image?: React.ReactNode;
diff --git a/src/components/Shared/Forms/Accordions/GroupedAccordion.tsx b/src/components/Shared/Forms/Accordions/GroupedAccordion.tsx
new file mode 100644
index 0000000000..d396f18c44
--- /dev/null
+++ b/src/components/Shared/Forms/Accordions/GroupedAccordion.tsx
@@ -0,0 +1,20 @@
+import { Accordion } from '@mui/material';
+import { styled } from '@mui/material/styles';
+
+export const GroupedAccordion = styled(Accordion)(({ theme }) => ({
+ border: `1px solid ${theme.palette.divider}`,
+
+ // Hide built-in borders
+ '&:before': {
+ content: 'none',
+ },
+
+ // Collapse adjacent borders by removing the top border of accordions that come right after another accordion
+ '& + &': {
+ borderTop: 0,
+ },
+
+ '.MuiAccordionSummary-root.Mui-expanded': {
+ backgroundColor: theme.palette.mpdxYellow.main,
+ },
+}));
diff --git a/src/components/Shared/Forms/FieldWrapper.tsx b/src/components/Shared/Forms/FieldWrapper.tsx
index 9ddb0d82fb..83b94e990b 100644
--- a/src/components/Shared/Forms/FieldWrapper.tsx
+++ b/src/components/Shared/Forms/FieldWrapper.tsx
@@ -9,7 +9,7 @@ import {
interface FieldWrapperProps {
labelText?: string;
- helperText?: string;
+ helperText?: React.ReactNode;
helperPosition?: HelperPositionEnum;
formControlDisabled?: FormControlProps['disabled'];
formControlError?: FormControlProps['error'];
@@ -22,7 +22,7 @@ interface FieldWrapperProps {
export const FieldWrapper: React.FC = ({
labelText = '',
- helperText = '',
+ helperText = null,
helperPosition = HelperPositionEnum.Top,
formControlDisabled = false,
formControlError = false,
@@ -47,12 +47,10 @@ export const FieldWrapper: React.FC = ({
''
);
- const helperTextOutput = helperText ? (
+ const helperTextOutput = helperText && (
- {t(helperText)}
+ {helperText}
- ) : (
- ''
);
return (
diff --git a/src/components/Shared/Forms/FormWrapper.tsx b/src/components/Shared/Forms/FormWrapper.tsx
index 60acba1431..de62e0b8cd 100644
--- a/src/components/Shared/Forms/FormWrapper.tsx
+++ b/src/components/Shared/Forms/FormWrapper.tsx
@@ -7,7 +7,6 @@ interface FormWrapperProps {
onSubmit: () => void;
isValid: boolean;
isSubmitting: boolean;
- formAttrs?: { action?: string; method?: string };
children: React.ReactNode;
buttonText?: string;
}
@@ -16,7 +15,6 @@ export const FormWrapper: React.FC = ({
onSubmit,
isValid,
isSubmitting,
- formAttrs = {},
children,
buttonText,
}) => {
@@ -24,7 +22,7 @@ export const FormWrapper: React.FC = ({
const theme = useTheme();
return (
-