From 5750ca772132fddc016de748605bfb7b0f7a8e15 Mon Sep 17 00:00:00 2001 From: "Bizz (Daniel Bisgrove)" <56281168+dr-bizz@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:15:55 -0400 Subject: [PATCH] MPDX-8121 Add Excluded Reason (#1024) * Adding Excluded reason to List contact row. Adding functionality to grab reason in hook so we can use in grid view and adding localized function for Excluded reasons. * Fixing List contact row header * Adding reason to the flows view * using useMemo over useEffect and useState * Improving and simplifying code * Adding more tests --- .../ContactFlowColumn.test.tsx | 2 +- .../ContactFlowColumn/ContactFlowColumn.tsx | 13 +- .../ContactFlowRow/ContactFlowRow.test.tsx | 30 ++- .../Flow/ContactFlowRow/ContactFlowRow.tsx | 25 ++- .../List/ContactRow/ContactRow.test.tsx | 43 ++++- .../Appeal/List/ContactRow/ContactRow.tsx | 44 +++-- .../List/ContactsList/ContactsList.test.tsx | 175 ++++++++++++++++++ .../Appeal/List/ContactsList/ContactsList.tsx | 50 ++++- .../Shared/AppealExcludedContacts.graphql | 15 ++ .../useGetExcludedReasons.test.ts | 64 +++++++ .../useGetExcludedReasons.tsx | 30 +++ .../useGetExcludedReasonsMock.ts | 24 +++ ...LocalizedExcludedFromAppealReasons.test.ts | 55 ++++++ .../getLocalizedExcludedFromAppealReasons.ts | 34 ++++ 14 files changed, 564 insertions(+), 40 deletions(-) create mode 100644 src/components/Tool/Appeal/List/ContactsList/ContactsList.test.tsx create mode 100644 src/components/Tool/Appeal/Shared/AppealExcludedContacts.graphql create mode 100644 src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.test.ts create mode 100644 src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.tsx create mode 100644 src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasonsMock.ts create mode 100644 src/utils/functions/getLocalizedExcludedFromAppealReasons.test.ts create mode 100644 src/utils/functions/getLocalizedExcludedFromAppealReasons.ts diff --git a/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.test.tsx b/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.test.tsx index 07efd6b8c..7010a4167 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.test.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.test.tsx @@ -20,7 +20,7 @@ const title = 'Test Column'; const onContactSelected = jest.fn(); const changeContactStatus = jest.fn(); const contact = { - id: '123', + id: 'contactID', name: 'Test Person', status: StatusEnum.NotInterested, primaryAddress: { diff --git a/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx b/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx index 2b0bfa2ae..5efa0655d 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx @@ -28,6 +28,7 @@ import { } from 'src/components/Tool/Appeal/AppealsContext/AppealsContext'; import { appealHeaderInfoHeight } from '../../AppealDetails/AppealHeaderInfo/AppealHeaderInfo'; import { useContactsQuery } from '../../AppealsContext/contacts.generated'; +import { useExcludedAppealContactsQuery } from '../../Shared/AppealExcludedContacts.generated'; import { ContactFlowDropZone } from '../ContactFlowDropZone/ContactFlowDropZone'; import { ContactFlowRow } from '../ContactFlowRow/ContactFlowRow'; @@ -67,6 +68,14 @@ export const ContactFlowColumn: React.FC = ({ skip: !accountListId || !appealStatus, }); + const { data: excludedContacts } = useExcludedAppealContactsQuery({ + variables: { + appealId: appealId ?? '', + accountListId: accountListId ?? '', + }, + skip: appealStatus !== AppealStatusEnum.Excluded, + }); + const cardContentRef = useRef(); const [{ canDrop }, drop] = useDrop(() => ({ @@ -165,9 +174,11 @@ export const ContactFlowColumn: React.FC = ({ accountListId={accountListId} contact={contact} appealStatus={appealStatus} - contactStatus={contact.status} onContactSelected={onContactSelected} columnWidth={cardContentRef.current?.offsetWidth} + excludedContacts={ + excludedContacts?.appeal?.excludedAppealContacts ?? [] + } /> )} endReached={() => diff --git a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx index 1a168e6c5..e78426cf8 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx @@ -17,6 +17,8 @@ import { } from '../../AppealsContext/AppealsContext'; import { AppealContactInfoFragment } from '../../AppealsContext/contacts.generated'; import { defaultContact } from '../../List/ContactRow/ContactRowMock'; +import { ExcludedAppealContactInfoFragment } from '../../Shared/AppealExcludedContacts.generated'; +import { defaultExcludedContacts } from '../../Shared/useGetExcludedReasons/useGetExcludedReasonsMock'; import { ContactFlowRow } from './ContactFlowRow'; const accountListId = 'account-list-1'; @@ -26,12 +28,14 @@ const toggleSelectionById = jest.fn(); const isChecked = jest.fn().mockImplementation(() => false); type ComponentsProps = { - contact?: AppealContactInfoFragment; appealStatus?: AppealStatusEnum; + contact?: AppealContactInfoFragment; + excludedContacts?: ExcludedAppealContactInfoFragment[]; }; const Components = ({ + appealStatus = AppealStatusEnum.Processed, contact = defaultContact, - appealStatus = AppealStatusEnum.Asked, + excludedContacts = [], }: ComponentsProps) => ( @@ -52,6 +56,7 @@ const Components = ({ contact={contact} appealStatus={appealStatus} onContactSelected={onContactSelected} + excludedContacts={excludedContacts} /> @@ -176,4 +181,25 @@ describe('ContactFlowRow', () => { expect(await findByText('Remove Commitment')).toBeInTheDocument(); }); }); + + describe('Excluded Reason', () => { + it('should not display excluded reason if not excluded contact', async () => { + const { queryByText } = render( + , + ); + + expect(queryByText('Send Appeals?" set to No')).not.toBeInTheDocument(); + }); + + it('should display excluded reason', async () => { + const { findByText } = render( + , + ); + + expect(await findByText('Send Appeals?" set to No')).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx index 0d655bee2..7540e6c07 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx @@ -21,7 +21,6 @@ import { } from 'src/components/Contacts/ContactFlow/ContactFlowRow/ContactFlowRow'; import { StarContactIconButton } from 'src/components/Contacts/StarContactIconButton/StarContactIconButton'; import { useGetPledgeOrDonation } from 'src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation'; -import { StatusEnum } from 'src/graphql/types.generated'; import theme from 'src/theme'; import { getLocalizedContactStatus } from 'src/utils/functions/getLocalizedContactStatus'; import { @@ -39,14 +38,16 @@ import { preloadPledgeModal, } from '../../Modals/PledgeModal/DynamicPledgeModal'; import { AmountAndFrequency } from '../../Shared/AmountAndFrequency/AmountAndFrequency'; +import { ExcludedAppealContactInfoFragment } from '../../Shared/AppealExcludedContacts.generated'; +import { useGetExcludedReasons } from '../../Shared/useGetExcludedReasons/useGetExcludedReasons'; // When making changes in this file, also check to see if you don't need to make changes to the below file // src/components/Contacts/ContactFlow/ContactFlowRow/ContactFlowRow.tsx interface Props extends Omit { contact: AppealContactInfoFragment; - contactStatus?: StatusEnum | null; appealStatus: AppealStatusEnum; + excludedContacts: ExcludedAppealContactInfoFragment[]; } export interface DraggedContact extends Omit { @@ -86,10 +87,10 @@ const CommitmentActionsBox = styled(Box)(() => ({ export const ContactFlowRow: React.FC = ({ accountListId, contact, - contactStatus, appealStatus, onContactSelected, columnWidth, + excludedContacts, }) => { const { id, name, starred } = contact; const { t } = useTranslation(); @@ -104,14 +105,19 @@ export const ContactFlowRow: React.FC = ({ const { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue } = useGetPledgeOrDonation({ appealStatus, contact, appealId: appealId ?? '' }); + const reasons = useGetExcludedReasons({ + excludedContacts, + contactId: contact.id, + }); + const [{ isDragging }, drag, preview] = useDrag( () => ({ type: 'contact', item: { id, appealStatus, - status: contactStatus, - contactStatus, + status: contact.status, + contactStatus: contact.status, name, starred, width: columnWidth, @@ -134,6 +140,8 @@ export const ContactFlowRow: React.FC = ({ setDeletePledgeModalOpen(true); }; + const isExcludedContact = appealStatus === AppealStatusEnum.Excluded; + return ( <> @@ -156,7 +164,7 @@ export const ContactFlowRow: React.FC = ({ {name} - {getLocalizedContactStatus(t, contactStatus)} + {getLocalizedContactStatus(t, contact.status)} @@ -172,6 +180,11 @@ export const ContactFlowRow: React.FC = ({ + {isExcludedContact && reasons && ( + + {reasons} + + )} {appealStatus !== AppealStatusEnum.Processed && ( diff --git a/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx b/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx index d8381d50a..720b15d4a 100644 --- a/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx +++ b/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx @@ -12,6 +12,8 @@ import { AppealsType, } from '../../AppealsContext/AppealsContext'; import { AppealContactInfoFragment } from '../../AppealsContext/contacts.generated'; +import { ExcludedAppealContactInfoFragment } from '../../Shared/AppealExcludedContacts.generated'; +import { defaultExcludedContacts } from '../../Shared/useGetExcludedReasons/useGetExcludedReasonsMock'; import { ContactRow } from './ContactRow'; import { defaultContact } from './ContactRowMock'; @@ -31,10 +33,12 @@ const isRowChecked = jest.fn(); type ComponentsProps = { appealStatus?: AppealStatusEnum; contact?: AppealContactInfoFragment; + excludedContacts?: ExcludedAppealContactInfoFragment[]; }; const Components = ({ appealStatus = AppealStatusEnum.Asked, contact = defaultContact, + excludedContacts = [], }: ComponentsProps) => ( @@ -51,7 +55,11 @@ const Components = ({ } as unknown as AppealsType } > - + @@ -104,14 +112,13 @@ describe('ContactsRow', () => { }); describe('Contact Row by status type', () => { - it('Excluded', () => { + it('Excluded', async () => { isRowChecked.mockImplementationOnce(() => true); const { getByText } = render( , ); - expect(getByText('Reason')).toBeInTheDocument(); expect(getByText('CA$500')).toBeInTheDocument(); expect(getByText('Monthly')).toBeInTheDocument(); }); @@ -119,11 +126,10 @@ describe('ContactsRow', () => { it('Asked', () => { isRowChecked.mockImplementationOnce(() => true); - const { getByText, queryByText } = render( + const { getByText } = render( , ); - expect(queryByText('Reason')).not.toBeInTheDocument(); expect(getByText('CA$500')).toBeInTheDocument(); expect(getByText('Monthly')).toBeInTheDocument(); }); @@ -131,11 +137,10 @@ describe('ContactsRow', () => { it('Committed', () => { isRowChecked.mockImplementationOnce(() => true); - const { getByText, queryByText } = render( + const { getByText } = render( , ); - expect(queryByText('Reason')).not.toBeInTheDocument(); expect(getByText('$3,000')).toBeInTheDocument(); expect(getByText('(Aug 8, 2024)')).toBeInTheDocument(); }); @@ -170,12 +175,32 @@ describe('ContactsRow', () => { it('Given', () => { isRowChecked.mockImplementationOnce(() => true); - const { getByText, queryByText } = render( + const { getByText } = render( , ); - expect(queryByText('Reason')).not.toBeInTheDocument(); expect(getByText('$3,000 ($50) (Jun 25, 2019)')).toBeInTheDocument(); }); }); + + describe('Excluded Reason', () => { + it('should not display excluded reason if not excluded contact', async () => { + const { queryByText } = render( + , + ); + + expect(queryByText('Send Appeals?" set to No')).not.toBeInTheDocument(); + }); + + it('should display excluded reason', async () => { + const { findByText } = render( + , + ); + + expect(await findByText('Send Appeals?" set to No')).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx index 4af3ceee3..448db3b25 100644 --- a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx +++ b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx @@ -43,6 +43,8 @@ import { preloadPledgeModal, } from '../../Modals/PledgeModal/DynamicPledgeModal'; import { AmountAndFrequency } from '../../Shared/AmountAndFrequency/AmountAndFrequency'; +import { ExcludedAppealContactInfoFragment } from '../../Shared/AppealExcludedContacts.generated'; +import { useGetExcludedReasons } from '../../Shared/useGetExcludedReasons/useGetExcludedReasons'; // When making changes in this file, also check to see if you don't need to make changes to the below file // src/components/Contacts/ContactRow/ContactRow.tsx @@ -64,12 +66,14 @@ interface Props { contact: AppealContactInfoFragment; appealStatus: AppealStatusEnum; useTopMargin?: boolean; + excludedContacts: ExcludedAppealContactInfoFragment[]; } export const ContactRow: React.FC = ({ contact, appealStatus, useTopMargin, + excludedContacts, }) => { const { appealId, @@ -84,6 +88,11 @@ export const ContactRow: React.FC = ({ useState(false); const [removeContactModalOpen, setRemoveContactModalOpen] = useState(false); + const reasons = useGetExcludedReasons({ + excludedContacts, + contactId: contact.id, + }); + const handleContactClick = () => { onContactSelected(contact.id); }; @@ -131,23 +140,24 @@ export const ContactRow: React.FC = ({ })} data-testid="rowButton" > - - - event.stopPropagation()} - onChange={() => onContactCheckToggle(contact.id)} - value={isChecked} - /> - - + + + event.stopPropagation()} + onChange={() => onContactCheckToggle(contact.id)} + value={isChecked} + /> + + @@ -166,10 +176,14 @@ export const ContactRow: React.FC = ({ flexDirection="column" justifyContent="center" > - - {/* TODO */} - Reason - + {reasons.map((reason, idx) => ( + + {reason} + + ))} diff --git a/src/components/Tool/Appeal/List/ContactsList/ContactsList.test.tsx b/src/components/Tool/Appeal/List/ContactsList/ContactsList.test.tsx new file mode 100644 index 000000000..193a8afb9 --- /dev/null +++ b/src/components/Tool/Appeal/List/ContactsList/ContactsList.test.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { render } from '@testing-library/react'; +import TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { AppealsWrapper } from 'pages/accountLists/[accountListId]/tools/appeals/AppealsWrapper'; +import theme from 'src/theme'; +import { AppealQuery } from '../../AppealDetails/AppealsMainPanel/AppealInfo.generated'; +import { + AppealStatusEnum, + AppealsContext, + AppealsType, +} from '../../AppealsContext/AppealsContext'; +import { appealInfo } from '../../appealMockData'; +import { ContactsList } from './ContactsList'; + +const accountListId = 'account-list-1'; +const appealId = 'appealId'; + +const router = { + query: { accountListId }, + isReady: true, +}; + +const setContactFocus = jest.fn(); +const contactDetailsOpen = true; +const toggleSelectionById = jest.fn(); +const isRowChecked = jest.fn(); + +const defaultAppealQuery: AppealQuery = { + appeal: { + ...appealInfo, + }, +}; +const defaultContactsQueryResult = { + data: { contacts: { nodes: [] } }, + loading: false, +}; +type ComponentsProps = { + appealInfoLoading?: boolean; + tour?: boolean; + appealStatus?: AppealStatusEnum; + contactsQueryResult?: object; +}; +const Components = ({ + appealInfoLoading = false, + tour = false, + appealStatus = AppealStatusEnum.Asked, + contactsQueryResult = defaultContactsQueryResult, +}: ComponentsProps) => ( + + + + + + + + + + + +); + +describe('ContactsRow', () => { + describe('NullState Message', () => { + it('shows no contacts on Given', () => { + const { getByText } = render( + , + ); + + expect( + getByText('No donations yet towards this appeal'), + ).toBeInTheDocument(); + }); + + it('shows no contacts on Excluded', () => { + const { getByText } = render( + , + ); + + expect( + getByText('No contacts have been excluded from this appeal'), + ).toBeInTheDocument(); + }); + + it('shows no contacts on Asked', () => { + const { getByText } = render(); + + expect( + getByText('All contacts for this appeal have committed to this appeal'), + ).toBeInTheDocument(); + }); + + it('shows no contacts on Committed', () => { + const { getByText } = render( + , + ); + + expect( + getByText( + 'There are no contacts for this appeal that have not been received.', + ), + ).toBeInTheDocument(); + }); + + it('shows no contacts on Received', () => { + const { getByText } = render( + , + ); + + expect( + getByText( + 'No gifts have been received and not yet processed to this appeal', + ), + ).toBeInTheDocument(); + }); + }); + + describe('Layout', () => { + it('Given', async () => { + const { queryByText } = render( + , + ); + expect(await queryByText('Reason')).not.toBeInTheDocument(); + }); + + it('Excluded', async () => { + const { findByText } = render( + , + ); + expect(await findByText('Reason')).toBeInTheDocument(); + }); + + it('Asked', async () => { + const { queryByText } = render(); + expect(await queryByText('Reason')).not.toBeInTheDocument(); + }); + + it('Committed', async () => { + const { queryByText } = render( + , + ); + expect(await queryByText('Reason')).not.toBeInTheDocument(); + }); + + it('Received', async () => { + const { queryByText } = render( + , + ); + expect(await queryByText('Reason')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Tool/Appeal/List/ContactsList/ContactsList.tsx b/src/components/Tool/Appeal/List/ContactsList/ContactsList.tsx index 395cd2e79..87631eb0b 100644 --- a/src/components/Tool/Appeal/List/ContactsList/ContactsList.tsx +++ b/src/components/Tool/Appeal/List/ContactsList/ContactsList.tsx @@ -18,6 +18,7 @@ import { AppealsContext, AppealsType, } from '../../AppealsContext/AppealsContext'; +import { useExcludedAppealContactsQuery } from '../../Shared/AppealExcludedContacts.generated'; import { DynamicAppealTour } from '../AppealTour/DynamicAppealTour'; import { ContactRow } from '../ContactRow/ContactRow'; @@ -28,11 +29,11 @@ const useStyles = makeStyles()(() => ({ contactHeader: { padding: theme.spacing(1, 2), }, + excludedHeader: { + padding: theme.spacing(1, 2, 1, 0), + }, givingHeader: { - padding: '8px 16px 8px 30px', - [theme.breakpoints.down('md')]: { - padding: '8px 16px 8px 8px', - }, + padding: theme.spacing(1, 2, 1, 0), }, })); @@ -50,6 +51,8 @@ export const ContactsList: React.FC = ({ const [nullStateTitle, setNullStateTitle] = React.useState(''); const { + appealId, + accountListId, tour, contactsQueryResult, isFiltered, @@ -61,6 +64,14 @@ export const ContactsList: React.FC = ({ const { data, loading, fetchMore } = contactsQueryResult; + const { data: excludedContacts } = useExcludedAppealContactsQuery({ + variables: { + appealId: appealId ?? '', + accountListId: accountListId ?? '', + }, + skip: activeFilters.appealStatus !== AppealStatusEnum.Excluded, + }); + const appealStatus = (activeFilters.appealStatus as AppealStatusEnum) ?? AppealStatusEnum.Asked; @@ -111,6 +122,8 @@ export const ContactsList: React.FC = ({ return name; }, [activeFilters]); + const isExcludedContact = appealStatus === AppealStatusEnum.Excluded; + return ( <> {tour && } @@ -120,12 +133,34 @@ export const ContactsList: React.FC = ({ /> - + {t('Contact')} - + {isExcludedContact && ( + + + {t('Reason')} + + + )} + @@ -148,6 +183,9 @@ export const ContactsList: React.FC = ({ contact={contact} appealStatus={appealStatus} useTopMargin={index === 0} + excludedContacts={ + excludedContacts?.appeal?.excludedAppealContacts ?? [] + } /> )} groupBy={(item) => ({ label: item.name[0].toUpperCase() })} diff --git a/src/components/Tool/Appeal/Shared/AppealExcludedContacts.graphql b/src/components/Tool/Appeal/Shared/AppealExcludedContacts.graphql new file mode 100644 index 000000000..dce51448e --- /dev/null +++ b/src/components/Tool/Appeal/Shared/AppealExcludedContacts.graphql @@ -0,0 +1,15 @@ +query ExcludedAppealContacts($accountListId: ID!, $appealId: ID!) { + appeal(accountListId: $accountListId, id: $appealId) { + excludedAppealContacts { + ...ExcludedAppealContactInfo + } + } +} + +fragment ExcludedAppealContactInfo on ExcludedAppealContact { + id + contact { + id + } + reasons +} diff --git a/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.test.ts b/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.test.ts new file mode 100644 index 000000000..60cf3f1bc --- /dev/null +++ b/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.test.ts @@ -0,0 +1,64 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useGetExcludedReasons } from './useGetExcludedReasons'; +import { + contactId, + defaultExcludedContacts, +} from './useGetExcludedReasonsMock'; + +describe('useGetExcludedReasons', () => { + it('should return empty string', () => { + const { result } = renderHook(() => + useGetExcludedReasons({ + excludedContacts: defaultExcludedContacts, + contactId: '', + }), + ); + + expect(result.current.length).toEqual(0); + + const { result: resultOne } = renderHook(() => + useGetExcludedReasons({ + excludedContacts: defaultExcludedContacts, + contactId: '', + }), + ); + + expect(resultOne.current.length).toEqual(0); + + const { result: resultTwo } = renderHook(() => + useGetExcludedReasons({ + excludedContacts: defaultExcludedContacts, + contactId: 'contactID3', + }), + ); + + expect(resultTwo.current.length).toEqual(0); + }); + + it('should return the correct reason', () => { + const { result } = renderHook(() => + useGetExcludedReasons({ + excludedContacts: defaultExcludedContacts, + contactId, + }), + ); + + expect(result.current.length).toEqual(1); + expect(result.current[0]).toEqual('Send Appeals?" set to No'); + }); + + it('should return both the correct reasons', () => { + const { result } = renderHook(() => + useGetExcludedReasons({ + excludedContacts: defaultExcludedContacts, + contactId: 'contactID2', + }), + ); + + expect(result.current.length).toEqual(2); + expect(result.current[0]).toEqual( + 'May have given a special gift in the last 3 months', + ); + expect(result.current[1]).toEqual('Stopped Giving Range'); + }); +}); diff --git a/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.tsx b/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.tsx new file mode 100644 index 000000000..4f0de198d --- /dev/null +++ b/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasons.tsx @@ -0,0 +1,30 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getLocalizedExcludedFromAppealReasons } from 'src/utils/functions/getLocalizedExcludedFromAppealReasons'; +import { ExcludedAppealContactInfoFragment } from '../AppealExcludedContacts.generated'; + +type UseGetExcludedReasonsProps = { + excludedContacts: ExcludedAppealContactInfoFragment[]; + contactId: string; +}; + +export const useGetExcludedReasons = ({ + excludedContacts, + contactId, +}: UseGetExcludedReasonsProps): string[] => { + const { t } = useTranslation(); + + const reasons = useMemo(() => { + const excludedReasons = excludedContacts.find( + (excludedContact) => excludedContact.contact?.id === contactId, + )?.reasons; + if (!excludedReasons) { + return []; + } + return excludedReasons.map((reason) => + getLocalizedExcludedFromAppealReasons(t, reason), + ); + }, [excludedContacts, contactId]); + + return reasons; +}; diff --git a/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasonsMock.ts b/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasonsMock.ts new file mode 100644 index 000000000..f416c6b4d --- /dev/null +++ b/src/components/Tool/Appeal/Shared/useGetExcludedReasons/useGetExcludedReasonsMock.ts @@ -0,0 +1,24 @@ +import { ExcludedAppealContactReasonEnum } from 'src/graphql/types.generated'; +import { defaultContact } from '../../List/ContactRow/ContactRowMock'; + +export const contactId = defaultContact.id; + +export const defaultExcludedContacts = [ + { + id: 'id1', + contact: { + id: contactId, + }, + reasons: [ExcludedAppealContactReasonEnum.NoAppeals], + }, + { + id: 'id2', + contact: { + id: 'contactID2', + }, + reasons: [ + ExcludedAppealContactReasonEnum.GaveMoreThanPledgedRange, + ExcludedAppealContactReasonEnum.StoppedGivingRange, + ], + }, +]; diff --git a/src/utils/functions/getLocalizedExcludedFromAppealReasons.test.ts b/src/utils/functions/getLocalizedExcludedFromAppealReasons.test.ts new file mode 100644 index 000000000..622221988 --- /dev/null +++ b/src/utils/functions/getLocalizedExcludedFromAppealReasons.test.ts @@ -0,0 +1,55 @@ +import { ExcludedAppealContactReasonEnum } from 'src/graphql/types.generated'; +import { getLocalizedExcludedFromAppealReasons } from './getLocalizedExcludedFromAppealReasons'; + +const t = jest.fn().mockImplementation((message) => message); + +describe('getRouterQueryParam', () => { + it('IncreasedRecently', () => { + expect( + getLocalizedExcludedFromAppealReasons( + t, + ExcludedAppealContactReasonEnum.IncreasedRecently, + ), + ).toEqual('Increased Recently'); + }); + it('JoinedRecently', () => { + expect( + getLocalizedExcludedFromAppealReasons( + t, + ExcludedAppealContactReasonEnum.JoinedRecently, + ), + ).toEqual('Joined Recently'); + }); + it('MarkedDoNotAsk', () => { + expect( + getLocalizedExcludedFromAppealReasons( + t, + ExcludedAppealContactReasonEnum.MarkedDoNotAsk, + ), + ).toEqual('Marked Do Not Ask'); + }); + it('PledgeAmountIncreasedRange', () => { + expect( + getLocalizedExcludedFromAppealReasons( + t, + ExcludedAppealContactReasonEnum.PledgeAmountIncreasedRange, + ), + ).toEqual('May have increased their giving in the last 3 months'); + }); + it('PledgeLateBy', () => { + expect( + getLocalizedExcludedFromAppealReasons( + t, + ExcludedAppealContactReasonEnum.PledgeLateBy, + ), + ).toEqual('May have missed a gift in the last 30-90 days'); + }); + it('StartedGivingRange', () => { + expect( + getLocalizedExcludedFromAppealReasons( + t, + ExcludedAppealContactReasonEnum.StartedGivingRange, + ), + ).toEqual('May have joined my team in the last 3 months'); + }); +}); diff --git a/src/utils/functions/getLocalizedExcludedFromAppealReasons.ts b/src/utils/functions/getLocalizedExcludedFromAppealReasons.ts new file mode 100644 index 000000000..5133f2217 --- /dev/null +++ b/src/utils/functions/getLocalizedExcludedFromAppealReasons.ts @@ -0,0 +1,34 @@ +import { TFunction } from 'react-i18next'; +import { ExcludedAppealContactReasonEnum } from 'src/graphql/types.generated'; + +export const getLocalizedExcludedFromAppealReasons = ( + t: TFunction, + excludedReason: ExcludedAppealContactReasonEnum | null | undefined, +): string => { + switch (excludedReason) { + case ExcludedAppealContactReasonEnum.GaveMoreThanPledgedRange: + return t('May have given a special gift in the last 3 months'); + case ExcludedAppealContactReasonEnum.IncreasedRecently: + return t('Increased Recently'); + case ExcludedAppealContactReasonEnum.JoinedRecently: + return t('Joined Recently'); + case ExcludedAppealContactReasonEnum.MarkedDoNotAsk: + return t('Marked Do Not Ask'); + case ExcludedAppealContactReasonEnum.NoAppeals: + return t('Send Appeals?" set to No'); + case ExcludedAppealContactReasonEnum.PledgeAmountIncreasedRange: + return t('May have increased their giving in the last 3 months'); + case ExcludedAppealContactReasonEnum.PledgeLateBy: + return t('May have missed a gift in the last 30-90 days'); + case ExcludedAppealContactReasonEnum.SpecialGift: + return t('Special Gift'); + case ExcludedAppealContactReasonEnum.StartedGivingRange: + return t('May have joined my team in the last 3 months'); + case ExcludedAppealContactReasonEnum.StoppedGiving: + return t('Stopped Giving'); + case ExcludedAppealContactReasonEnum.StoppedGivingRange: + return t('Stopped Giving Range'); + default: + return ''; + } +};