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 000000000..f88c25dc2
--- /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 000000000..a05948a4d
--- /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/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx
index e0c89c10b..9fa42828c 100644
--- a/src/components/Dashboard/Dashboard.tsx
+++ b/src/components/Dashboard/Dashboard.tsx
@@ -48,6 +48,7 @@ const Dashboard = ({ data, accountListId }: Props): ReactElement => {
pledged={data.accountList.totalPledges}
totalGiftsNotStarted={data.contacts.totalCount}
currencyCode={data.accountList.currency}
+ onDashboard={true}
/>
diff --git a/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx b/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx
index ac52790aa..518e167f7 100644
--- a/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx
+++ b/src/components/Dashboard/MonthlyGoal/MonthlyGoal.tsx
@@ -55,6 +55,7 @@ export interface MonthlyGoalProps {
pledged?: number;
totalGiftsNotStarted?: number;
currencyCode?: string;
+ onDashboard?: boolean;
}
const MonthlyGoal = ({
@@ -65,6 +66,7 @@ const MonthlyGoal = ({
pledged = 0,
totalGiftsNotStarted,
currencyCode = 'USD',
+ onDashboard = false,
}: MonthlyGoalProps): ReactElement => {
const { t } = useTranslation();
const { classes } = useStyles();
@@ -286,6 +288,7 @@ const MonthlyGoal = ({
({
+ display: 'flex',
+ gap: 2,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: theme.spacing(2),
+}));
+
+interface HealthIndicatorFormulaProps {
+ accountListId: string;
+ noHealthIndicatorData: boolean;
+ setNoHealthIndicatorData: Dispatch>;
+}
+
+export const HealthIndicatorFormula: React.FC = ({
+ accountListId,
+ noHealthIndicatorData,
+ setNoHealthIndicatorData,
+}) => {
+ const { t } = useTranslation();
+
+ const { data, loading } = useHealthIndicatorFormulaQuery({
+ variables: {
+ accountListId,
+ },
+ });
+
+ useEffect(() => {
+ if (!data?.healthIndicatorData?.length && !loading) {
+ setNoHealthIndicatorData(true);
+ }
+ }, [data, loading]);
+
+ const latestMpdHealthData = data?.healthIndicatorData.at(-1);
+
+ 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
+
+
+
+
+
+
+
+
+ );
+};
+
+interface FormulaItemProps {
+ name: string;
+ explanation: string;
+ value: number;
+ isLoading: boolean;
+}
+
+const FormulaItem: React.FC = ({
+ name,
+ explanation,
+ value,
+ isLoading,
+}) => (
+
+ {isLoading ? (
+
+ ) : (
+
+ {value}
+
+ )}
+
+ {name} =
+ {explanation}
+
+
+);
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorReport.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorReport.tsx
new file mode 100644
index 000000000..b0c0f717c
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorReport.tsx
@@ -0,0 +1,91 @@
+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, loading } = useMonthlyGoalQuery({
+ variables: {
+ accountListId,
+ },
+ });
+ return (
+
+
+
+ {noHealthIndicatorData ? (
+
+
+
+ {t('No Health Indicator data available')}
+
+
+ {t(
+ 'There is currently no Health Indicator data available for this account. Please switch to an account that is an MPD Global account. If you are unsure whether you have access to an MPD Global account or need assistance in switching accounts, please reach out to your coach or our support team for guidance.',
+ )}
+
+
+
+ ) : (
+
+
+
+
+
+ {t('Health Indicator')}
+
+
+
+
+ {t('MPD Health Formula')}
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.graphql b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.graphql
index 3562f0227..41ae0640e 100644
--- a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.graphql
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.graphql
@@ -1,9 +1,5 @@
-query HealthIndicatorWidget($accountListId: ID!, $month: ISO8601Date!) {
- healthIndicatorData(
- accountListId: $accountListId
- beginDate: $month
- endDate: $month
- ) {
+query HealthIndicatorWidget($accountListId: ID!) {
+ healthIndicatorData(accountListId: $accountListId) {
id
overallHi
ownershipHi
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.test.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.test.tsx
index a61c85ccd..0155b4e8c 100644
--- a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.test.tsx
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.test.tsx
@@ -22,11 +22,13 @@ interface ComponentsProps {
healthIndicatorData?: HealthIndicatorWidgetQuery['healthIndicatorData'];
showHealthIndicator?: boolean;
goal?: number;
+ onDashboard?: boolean;
}
const Components = ({
healthIndicatorData = [],
showHealthIndicator = true,
goal = 7000,
+ onDashboard = true,
}: ComponentsProps) => (
mocks={{
@@ -39,6 +41,7 @@ const Components = ({
{
expect(setShowHealthIndicator).toHaveBeenCalledWith(true);
});
+ 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(
,
diff --git a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.tsx b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.tsx
index e6cd4d2f2..90afca3fa 100644
--- a/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.tsx
+++ b/src/components/Reports/HealthIndicatorReport/HealthIndicatorWidget/HealthIndicatorWidget.tsx
@@ -11,7 +11,6 @@ import {
Typography,
} from '@mui/material';
import { styled } from '@mui/material/styles';
-import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import AnimatedCard from 'src/components/AnimatedCard';
import StyledProgress from 'src/components/StyledProgress';
@@ -34,6 +33,7 @@ const StyledBox = styled(Box)(() => ({
interface HealthIndicatorWidgetProps {
accountListId: string;
goal: number;
+ onDashboard: boolean;
showHealthIndicator: boolean;
setShowHealthIndicator: Dispatch>;
setUsingMachineCalculatedGoal: Dispatch>;
@@ -42,6 +42,7 @@ interface HealthIndicatorWidgetProps {
export const HealthIndicatorWidget: React.FC = ({
accountListId,
goal,
+ onDashboard = true,
showHealthIndicator,
setShowHealthIndicator,
setUsingMachineCalculatedGoal,
@@ -51,14 +52,12 @@ export const HealthIndicatorWidget: React.FC = ({
const { data, loading } = useHealthIndicatorWidgetQuery({
variables: {
accountListId,
- month: DateTime.now().startOf('month').toISODate(),
},
});
useEffect(() => {
setShowHealthIndicator(!!data?.healthIndicatorData.length);
- const machineCalculatedGoal =
- data?.healthIndicatorData[0]?.machineCalculatedGoal;
+ const { machineCalculatedGoal } = data?.healthIndicatorData.at(-1) ?? {};
setUsingMachineCalculatedGoal(
!!machineCalculatedGoal && goal === machineCalculatedGoal,
);
@@ -68,7 +67,7 @@ export const HealthIndicatorWidget: React.FC = ({
return null;
}
- const currentStats = data?.healthIndicatorData[0];
+ const currentStats = data?.healthIndicatorData.at(-1);
return (
@@ -80,9 +79,10 @@ export const HealthIndicatorWidget: React.FC = ({
/>
@@ -135,16 +135,18 @@ export const HealthIndicatorWidget: React.FC = ({
/>
-
-
-
+ {onDashboard && (
+
+
+
+ )}
);
};
diff --git a/src/components/Reports/HealthIndicatorReport/MonthlyGoal.graphql b/src/components/Reports/HealthIndicatorReport/MonthlyGoal.graphql
new file mode 100644
index 000000000..f947e2c58
--- /dev/null
+++ b/src/components/Reports/HealthIndicatorReport/MonthlyGoal.graphql
@@ -0,0 +1,15 @@
+query MonthlyGoal($accountListId: ID!) {
+ accountList(id: $accountListId) {
+ id
+ monthlyGoal
+ receivedPledges
+ totalPledges
+ currency
+ }
+ contacts(
+ accountListId: $accountListId
+ contactsFilter: { pledgeReceived: NOT_RECEIVED, status: PARTNER_FINANCIAL }
+ ) {
+ totalCount
+ }
+}
diff --git a/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts
index 42a7dd6cb..0df872e37 100644
--- a/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts
+++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts
@@ -44,6 +44,10 @@ export const reportNavItems: NavItems[] = [
id: 'coaching',
title: i18n.t('Coaching'),
},
+ {
+ id: 'healthIndicator',
+ title: i18n.t('MPD Health'),
+ },
];
export const settingsNavItems: NavItems[] = [