From 660f33e25b2f129414245f1ecf9eeeef4c60d9dd Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 29 Aug 2024 10:51:23 -0400 Subject: [PATCH 1/4] Moving logic for determining pledge, commitment and normal donations into useHook --- .../Appeal/List/ContactRow/ContactRow.tsx | 150 +---------------- src/hooks/useGetPledgeOrDonation.test.ts | 105 ++++++++++++ src/hooks/useGetPledgeOrDonation.ts | 158 ++++++++++++++++++ 3 files changed, 268 insertions(+), 145 deletions(-) create mode 100644 src/hooks/useGetPledgeOrDonation.test.ts create mode 100644 src/hooks/useGetPledgeOrDonation.ts diff --git a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx index d90175ce4..2fd26f4ed 100644 --- a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx +++ b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; @@ -13,19 +13,14 @@ import { } from '@mui/material'; import { styled } from '@mui/material/styles'; import clsx from 'clsx'; -import { TFunction } from 'i18next'; -import { DateTime } from 'luxon'; -import { useTranslation } from 'react-i18next'; import { ListItemButton, StyledCheckbox, } from 'src/components/Contacts/ContactRow/ContactRow'; import { preloadContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/DynamicContactsRightPanel'; -import { PledgeFrequencyEnum } from 'src/graphql/types.generated'; +import { useGetPledgeOrDonation } from 'src/hooks/useGetPledgeOrDonation'; import { useLocale } from 'src/hooks/useLocale'; -import { currencyFormat, dateFormat } from 'src/lib/intlFormat'; import theme from 'src/theme'; -import { getLocalizedPledgeFrequency } from 'src/utils/functions/getLocalizedPledgeFrequency'; import { AppealStatusEnum, AppealsContext, @@ -65,46 +60,6 @@ const ContactRowActions = styled(Box)(() => ({ paddingRight: theme.spacing(2), })); -type FormatPledgeOrDonationProps = { - amount?: number | null; - currency?: string | null; - appealStatus: AppealStatusEnum; - dateOrFrequency?: PledgeFrequencyEnum | string | null; - locale: string; - t: TFunction; -}; - -const formatPledgeOrDonation = ({ - amount, - currency, - appealStatus, - dateOrFrequency, - locale, - t, -}: FormatPledgeOrDonationProps) => { - const pledgeOrDonationAmount = - amount && currency - ? currencyFormat(amount, currency, locale) - : amount || currencyFormat(0, currency, locale); - - const pledgeOrDonationDate = - appealStatus === AppealStatusEnum.Asked || - appealStatus === AppealStatusEnum.Excluded - ? (dateOrFrequency && - getLocalizedPledgeFrequency( - t, - dateOrFrequency as PledgeFrequencyEnum, - )) ?? - '' - : dateOrFrequency - ? dateFormat(DateTime.fromISO(dateOrFrequency), locale) - : null; - return { - amount: pledgeOrDonationAmount, - dateOrFrequency: pledgeOrDonationDate, - }; -}; - interface Props { contact: AppealContactInfoFragment; appealStatus: AppealStatusEnum; @@ -123,116 +78,21 @@ export const ContactRow: React.FC = ({ setContactFocus: onContactSelected, toggleSelectionById: onContactCheckToggle, } = React.useContext(AppealsContext) as AppealsType; - const { t } = useTranslation(); const locale = useLocale(); const [createPledgeModalOpen, setPledgeModalOpen] = useState(false); const [deletePledgeModalOpen, setDeletePledgeModalOpen] = useState(false); const [addExcludedContactModalOpen, setAddExcludedContactModalOpen] = useState(false); const [removeContactModalOpen, setRemoveContactModalOpen] = useState(false); - const [pledgeValues, setPledgeValues] = - useState(); - const [amountAndFrequency, setAmountAndFrequency] = useState(); - const [pledgeDonations, setPledgeDonations] = useState(null); const handleContactClick = () => { onContactSelected(contact.id); }; - const { - id: contactId, - name, - pledgeAmount, - pledgeCurrency, - pledgeFrequency, - pledges, - donations, - } = contact; - - useEffect(() => { - if ( - appealStatus === AppealStatusEnum.Asked || - appealStatus === AppealStatusEnum.Excluded - ) { - const { amount, dateOrFrequency } = formatPledgeOrDonation({ - amount: pledgeAmount, - currency: pledgeCurrency, - appealStatus, - dateOrFrequency: pledgeFrequency, - locale, - t, - }); - setAmountAndFrequency(`${amount} ${dateOrFrequency}`); - setPledgeValues(undefined); - } else if ( - appealStatus === AppealStatusEnum.NotReceived || - appealStatus === AppealStatusEnum.ReceivedNotProcessed - ) { - const appealPledge = pledges?.find( - (pledge) => pledge.appeal.id === appealId, - ); - - if (appealPledge) { - const { amount, dateOrFrequency } = formatPledgeOrDonation({ - amount: appealPledge?.amount, - currency: appealPledge.amountCurrency, - appealStatus, - dateOrFrequency: appealPledge.expectedDate, - locale, - t, - }); - - setPledgeValues(appealPledge); - setAmountAndFrequency(`${amount} (${dateOrFrequency})`); - } else { - setAmountAndFrequency(`${currencyFormat(0, 'USD', locale)}`); - } - } else if (appealStatus === AppealStatusEnum.Processed) { - const appealPledge = pledges?.find( - (pledge) => pledge.appeal.id === appealId, - ); - - if (appealPledge) { - const { amount } = formatPledgeOrDonation({ - amount: appealPledge?.amount, - currency: appealPledge.amountCurrency, - appealStatus, - locale, - t, - }); - setPledgeValues(appealPledge); - setAmountAndFrequency(`${amount}`); - } else { - setAmountAndFrequency(`${currencyFormat(0, 'USD', locale)}`); - } - - // Currently we grab all the donations and filter them by the appeal id - // We need a query that allows us to filter by the appeal id - // Maybe buy the backend team some donuts and ask them to add a filter to the donations query - const appealDonations = donations.nodes.filter( - (donation) => donation?.appeal?.id === appealId, - ); - - const givenDonations = appealDonations.map((donation) => { - const amount = donation?.appealAmount?.amount; - const currency = donation?.appealAmount?.convertedCurrency; - const donationAmount = currencyFormat( - amount && currency ? amount : 0, - currency, - locale, - ); - - const donationDate = dateFormat( - DateTime.fromISO(donation.donationDate), - locale, - ); - - return `(${donationAmount}) (${donationDate})`; - }); + const { id: contactId, name } = contact; - setPledgeDonations(givenDonations); - } - }, [appealStatus, contact, locale]); + const { pledgeValues, amountAndFrequency, pledgeDonations } = + useGetPledgeOrDonation(appealStatus, contact, appealId ?? '', locale); const handleCreatePledge = () => { setPledgeModalOpen(true); diff --git a/src/hooks/useGetPledgeOrDonation.test.ts b/src/hooks/useGetPledgeOrDonation.test.ts new file mode 100644 index 000000000..1889ac7fd --- /dev/null +++ b/src/hooks/useGetPledgeOrDonation.test.ts @@ -0,0 +1,105 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { AppealStatusEnum } from 'src/components/Tool/Appeal/AppealsContext/AppealsContext'; +import { defaultContact } from 'src/components/Tool/Appeal/List/ContactRow/ContactRowMock'; +import { useGetPledgeOrDonation } from './useGetPledgeOrDonation'; + +const appealId = 'appealId'; +describe('useGetPledgeOrDonation', () => { + it('returns the normal donation amount when in appeal status Asked', () => { + const { result } = renderHook(() => + useGetPledgeOrDonation( + AppealStatusEnum.Asked, + defaultContact, + appealId, + 'en-US', + ), + ); + + expect(result.current.amountAndFrequency).toEqual('CA$500 Monthly'); + + expect(result.current.pledgeDonations).toBeNull(); + expect(result.current.pledgeValues).toBeUndefined(); + }); + + it('returns the normal donation amount when in appeal status Excluded', () => { + const { result } = renderHook(() => + useGetPledgeOrDonation( + AppealStatusEnum.Excluded, + defaultContact, + appealId, + 'en-US', + ), + ); + + expect(result.current.amountAndFrequency).toEqual('CA$500 Monthly'); + + expect(result.current.pledgeDonations).toBeNull(); + expect(result.current.pledgeValues).toBeUndefined(); + }); + + it('returns the pledge when in appeal status Committed', () => { + const { result } = renderHook(() => + useGetPledgeOrDonation( + AppealStatusEnum.NotReceived, + defaultContact, + appealId, + 'en-US', + ), + ); + + expect(result.current.amountAndFrequency).toEqual('$3,000 (Aug 8, 2024)'); + + expect(result.current.pledgeDonations).toBeNull(); + expect(result.current.pledgeValues).toEqual({ + amount: 3000, + amountCurrency: 'USD', + appeal: { id: appealId }, + expectedDate: '2024-08-08', + id: 'pledge-1', + }); + }); + + it('returns the pledge when in appeal status Received', () => { + const { result } = renderHook(() => + useGetPledgeOrDonation( + AppealStatusEnum.ReceivedNotProcessed, + defaultContact, + appealId, + 'en-US', + ), + ); + + expect(result.current.amountAndFrequency).toEqual('$3,000 (Aug 8, 2024)'); + + expect(result.current.pledgeDonations).toBeNull(); + expect(result.current.pledgeValues).toEqual({ + amount: 3000, + amountCurrency: 'USD', + appeal: { id: appealId }, + expectedDate: '2024-08-08', + id: 'pledge-1', + }); + }); + + it('returns the donations to appeal when in appeal status Given', () => { + const { result } = renderHook(() => + useGetPledgeOrDonation( + AppealStatusEnum.Processed, + defaultContact, + appealId, + 'en-US', + ), + ); + + expect(result.current.amountAndFrequency).toEqual('$3,000'); + + expect(result.current.pledgeDonations).toEqual(['($50) (Jun 25, 2019)']); + expect(result.current.pledgeValues).toEqual({ + amount: 3000, + amountCurrency: 'USD', + appeal: { id: appealId }, + expectedDate: '2024-08-08', + id: 'pledge-1', + }); + }); +}); diff --git a/src/hooks/useGetPledgeOrDonation.ts b/src/hooks/useGetPledgeOrDonation.ts new file mode 100644 index 000000000..6d464f6c4 --- /dev/null +++ b/src/hooks/useGetPledgeOrDonation.ts @@ -0,0 +1,158 @@ +import { useEffect, useState } from 'react'; +import { TFunction } from 'i18next'; +import { DateTime } from 'luxon'; +import { useTranslation } from 'react-i18next'; +import { AppealStatusEnum } from 'src/components/Tool/Appeal/AppealsContext/AppealsContext'; +import { AppealContactInfoFragment } from 'src/components/Tool/Appeal/AppealsContext/contacts.generated'; +import { PledgeFrequencyEnum } from 'src/graphql/types.generated'; +import { currencyFormat, dateFormat } from 'src/lib/intlFormat'; +import { getLocalizedPledgeFrequency } from 'src/utils/functions/getLocalizedPledgeFrequency'; + +type FormatPledgeOrDonationProps = { + amount?: number | null; + currency?: string | null; + appealStatus: AppealStatusEnum; + dateOrFrequency?: PledgeFrequencyEnum | string | null; + locale: string; + t: TFunction; +}; + +const formatPledgeOrDonation = ({ + amount, + currency, + appealStatus, + dateOrFrequency, + locale, + t, +}: FormatPledgeOrDonationProps) => { + const pledgeOrDonationAmount = + amount && currency + ? currencyFormat(amount, currency, locale) + : amount || currencyFormat(0, currency, locale); + + const pledgeOrDonationDate = + appealStatus === AppealStatusEnum.Asked || + appealStatus === AppealStatusEnum.Excluded + ? (dateOrFrequency && + getLocalizedPledgeFrequency( + t, + dateOrFrequency as PledgeFrequencyEnum, + )) ?? + '' + : dateOrFrequency + ? dateFormat(DateTime.fromISO(dateOrFrequency), locale) + : null; + return { + amount: pledgeOrDonationAmount, + dateOrFrequency: pledgeOrDonationDate, + }; +}; + +// The return value doesn't change until `delay` milliseconds have elapsed since the last time `value` changed +export const useGetPledgeOrDonation = ( + appealStatus: AppealStatusEnum, + contact: AppealContactInfoFragment, + appealId: string, + locale: string, +) => { + const { t } = useTranslation(); + const [pledgeValues, setPledgeValues] = + useState(); + const [amountAndFrequency, setAmountAndFrequency] = useState(); + const [pledgeDonations, setPledgeDonations] = useState(null); + + useEffect(() => { + const { + pledgeAmount, + pledgeCurrency, + pledgeFrequency, + pledges, + donations, + } = contact; + + if ( + appealStatus === AppealStatusEnum.Asked || + appealStatus === AppealStatusEnum.Excluded + ) { + const { amount, dateOrFrequency } = formatPledgeOrDonation({ + amount: pledgeAmount, + currency: pledgeCurrency, + appealStatus, + dateOrFrequency: pledgeFrequency, + locale, + t, + }); + setAmountAndFrequency(`${amount} ${dateOrFrequency}`); + setPledgeValues(undefined); + } else if ( + appealStatus === AppealStatusEnum.NotReceived || + appealStatus === AppealStatusEnum.ReceivedNotProcessed + ) { + const appealPledge = pledges?.find( + (pledge) => pledge.appeal.id === appealId, + ); + + if (appealPledge) { + const { amount, dateOrFrequency } = formatPledgeOrDonation({ + amount: appealPledge?.amount, + currency: appealPledge.amountCurrency, + appealStatus, + dateOrFrequency: appealPledge.expectedDate, + locale, + t, + }); + + setPledgeValues(appealPledge); + setAmountAndFrequency(`${amount} (${dateOrFrequency})`); + } else { + setAmountAndFrequency(`${currencyFormat(0, 'USD', locale)}`); + } + } else if (appealStatus === AppealStatusEnum.Processed) { + const appealPledge = pledges?.find( + (pledge) => pledge.appeal.id === appealId, + ); + + if (appealPledge) { + const { amount } = formatPledgeOrDonation({ + amount: appealPledge?.amount, + currency: appealPledge.amountCurrency, + appealStatus, + locale, + t, + }); + setPledgeValues(appealPledge); + setAmountAndFrequency(`${amount}`); + } else { + setAmountAndFrequency(`${currencyFormat(0, 'USD', locale)}`); + } + + // Currently we grab all the donations and filter them by the appeal id + // We need a query that allows us to filter by the appeal id + // Maybe buy the backend team some donuts and ask them to add a filter to the donations query + const appealDonations = donations.nodes.filter( + (donation) => donation?.appeal?.id === appealId, + ); + + const givenDonations = appealDonations.map((donation) => { + const amount = donation?.appealAmount?.amount; + const currency = donation?.appealAmount?.convertedCurrency; + const donationAmount = currencyFormat( + amount && currency ? amount : 0, + currency, + locale, + ); + + const donationDate = dateFormat( + DateTime.fromISO(donation.donationDate), + locale, + ); + + return `(${donationAmount}) (${donationDate})`; + }); + + setPledgeDonations(givenDonations); + } + }, [appealStatus, contact, locale]); + + return { pledgeValues, amountAndFrequency, pledgeDonations }; +}; From ec2ddb272dc465d0f36c9e39ee79a4635720359f Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 29 Aug 2024 14:22:18 -0400 Subject: [PATCH 2/4] Pulling correct useContactQuery --- src/components/Tool/Appeal/AppealsContext/contacts.graphql | 2 ++ .../Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx | 2 +- .../Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx | 4 +++- src/components/Tool/Appeal/List/ContactRow/ContactRowMock.ts | 4 +++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Tool/Appeal/AppealsContext/contacts.graphql b/src/components/Tool/Appeal/AppealsContext/contacts.graphql index e91f5bc53..202eda212 100644 --- a/src/components/Tool/Appeal/AppealsContext/contacts.graphql +++ b/src/components/Tool/Appeal/AppealsContext/contacts.graphql @@ -29,6 +29,8 @@ fragment AppealContactInfo on Contact { pledgeFrequency pledgeReceived pledgeStartDate + starred + status pledges { id amount diff --git a/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx b/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx index e000a25ba..2b0bfa2ae 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowColumn/ContactFlowColumn.tsx @@ -11,7 +11,6 @@ import { } from '@mui/material'; import { useDrop } from 'react-dnd'; import { useTranslation } from 'react-i18next'; -import { useContactsQuery } from 'pages/accountLists/[accountListId]/contacts/Contacts.generated'; import { CardContentInner, ColumnTitle, @@ -28,6 +27,7 @@ import { AppealsType, } from 'src/components/Tool/Appeal/AppealsContext/AppealsContext'; import { appealHeaderInfoHeight } from '../../AppealDetails/AppealHeaderInfo/AppealHeaderInfo'; +import { useContactsQuery } from '../../AppealsContext/contacts.generated'; import { ContactFlowDropZone } from '../ContactFlowDropZone/ContactFlowDropZone'; import { ContactFlowRow } from '../ContactFlowRow/ContactFlowRow'; diff --git a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx index 4f0512a85..9fc76efd4 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx @@ -22,11 +22,13 @@ import { AppealsContext, AppealsType, } from '../../AppealsContext/AppealsContext'; +import { AppealContactInfoFragment } from '../../AppealsContext/contacts.generated'; // 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 { +interface Props extends Omit { + contact: AppealContactInfoFragment; contactStatus?: StatusEnum | null; appealStatus: AppealStatusEnum; } diff --git a/src/components/Tool/Appeal/List/ContactRow/ContactRowMock.ts b/src/components/Tool/Appeal/List/ContactRow/ContactRowMock.ts index af636c902..f939e1f22 100644 --- a/src/components/Tool/Appeal/List/ContactRow/ContactRowMock.ts +++ b/src/components/Tool/Appeal/List/ContactRow/ContactRowMock.ts @@ -1,4 +1,4 @@ -import { PledgeFrequencyEnum } from 'src/graphql/types.generated'; +import { PledgeFrequencyEnum, StatusEnum } from 'src/graphql/types.generated'; import { AppealContactInfoFragment } from '../../AppealsContext/contacts.generated'; export const defaultContact: AppealContactInfoFragment = { @@ -8,6 +8,8 @@ export const defaultContact: AppealContactInfoFragment = { pledgeCurrency: 'CAD', pledgeFrequency: PledgeFrequencyEnum.Monthly, pledgeReceived: true, + status: StatusEnum.AskInFuture, + starred: false, pledges: [ { id: 'pledge-1', From d1ac4fd3e8019b944ad81bf75ae76abd354e65d6 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 29 Aug 2024 16:05:57 -0400 Subject: [PATCH 3/4] Allow for overdue date to be made red and adding a lot more tests. --- .../ContactFlowRow/ContactFlowRow.test.tsx | 179 +++++++++++---- .../Flow/ContactFlowRow/ContactFlowRow.tsx | 204 +++++++++++++----- .../List/ContactRow/ContactRow.test.tsx | 15 +- .../Appeal/List/ContactRow/ContactRow.tsx | 12 +- .../AmountAndFrequency/AmountAndFrequency.tsx | 25 +++ .../useGetPledgeOrDonation.test.ts | 81 +++++-- .../useGetPledgeOrDonation.ts | 46 +++- 7 files changed, 435 insertions(+), 127 deletions(-) create mode 100644 src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx rename src/{hooks => components/Tool/Appeal/Shared/useGetPledgeOrDonation}/useGetPledgeOrDonation.test.ts (55%) rename src/{hooks => components/Tool/Appeal/Shared/useGetPledgeOrDonation}/useGetPledgeOrDonation.ts (77%) diff --git a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx index 8d3901d26..1a168e6c5 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.test.tsx @@ -1,70 +1,95 @@ 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, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; +import { I18nextProvider } from 'react-i18next'; import TestWrapper from '__tests__/util/TestWrapper'; -import { ContactRowFragment } from 'src/components/Contacts/ContactRow/ContactRow.generated'; +import i18n from 'src/lib/i18n'; import theme from 'src/theme'; import { AppealStatusEnum, AppealsContext, AppealsType, } from '../../AppealsContext/AppealsContext'; +import { AppealContactInfoFragment } from '../../AppealsContext/contacts.generated'; +import { defaultContact } from '../../List/ContactRow/ContactRowMock'; import { ContactFlowRow } from './ContactFlowRow'; -const accountListId = 'abc'; -const contact = { - id: '123', - name: 'Test Name', - starred: true, - avatar: 'avatar.jpg', - pledgeAmount: 100, - pledgeCurrency: 'USD', - pledgeReceived: false, - uncompletedTasksCount: 0, -} as ContactRowFragment; +const accountListId = 'account-list-1'; +const appealId = 'appealId'; const onContactSelected = jest.fn(); const toggleSelectionById = jest.fn(); const isChecked = jest.fn().mockImplementation(() => false); -const Components = () => ( - - - - - - - - - +type ComponentsProps = { + contact?: AppealContactInfoFragment; + appealStatus?: AppealStatusEnum; +}; +const Components = ({ + contact = defaultContact, + appealStatus = AppealStatusEnum.Asked, +}: ComponentsProps) => ( + + + + + + + + + + + + + ); describe('ContactFlowRow', () => { it('should display contact name and status', () => { const { getByText, getByTitle } = render(); - expect(getByText('Test Name')).toBeInTheDocument(); + expect(getByText(defaultContact.name)).toBeInTheDocument(); + expect(getByTitle('Outline Star Icon')).toBeInTheDocument(); + }); + + it('should display contact as starred', () => { + const { getByText, getByTitle } = render( + , + ); + expect(getByText(defaultContact.name)).toBeInTheDocument(); expect(getByTitle('Filled Star Icon')).toBeInTheDocument(); }); it('should call contact selected function', () => { const { getByText } = render(); - userEvent.click(getByText('Test Name')); - expect(getByText('Test Name')).toBeInTheDocument(); - expect(onContactSelected).toHaveBeenCalledWith('123', true, true); + userEvent.click(getByText(defaultContact.name)); + expect(getByText(defaultContact.name)).toBeInTheDocument(); + expect(onContactSelected).toHaveBeenCalledWith( + defaultContact.id, + true, + true, + ); }); it('should call check contact', async () => { @@ -72,7 +97,83 @@ describe('ContactFlowRow', () => { userEvent.click(getByRole('checkbox')); await waitFor(() => { - expect(toggleSelectionById).toHaveBeenLastCalledWith(contact.id); + expect(toggleSelectionById).toHaveBeenLastCalledWith(defaultContact.id); + }); + }); + + describe('Contact Row by status type', () => { + it('Excluded', () => { + const { getByText } = render( + , + ); + expect(getByText('CA$500')).toBeInTheDocument(); + expect(getByText('Monthly')).toBeInTheDocument(); + }); + + it('Asked', () => { + const { getByText } = render( + , + ); + expect(getByText('CA$500')).toBeInTheDocument(); + expect(getByText('Monthly')).toBeInTheDocument(); + }); + + it('Committed', () => { + const { getByText } = render( + , + ); + expect(getByText('$3,000')).toBeInTheDocument(); + expect(getByText('(Aug 8, 2024)')).toBeInTheDocument(); + }); + + it('Committed - with no pledges', () => { + const { getByText } = render( + , + ); + expect(getByText('$0')).toBeInTheDocument(); + }); + + it('Received', () => { + const { getByText } = render( + , + ); + expect(getByText('$3,000')).toBeInTheDocument(); + expect(getByText('(Aug 8, 2024)')).toBeInTheDocument(); + }); + + it('Given', () => { + const { getByText } = render( + , + ); + expect(getByText('$3,000 ($50) (Jun 25, 2019)')).toBeInTheDocument(); + }); + }); + + describe('Edit/Add Pledge', () => { + it('Open up Edit pledge modal', async () => { + const { getByTestId, findByText } = render( + , + ); + + userEvent.click(getByTestId('editPledgeButton')); + + expect(await findByText('Edit Commitment')).toBeInTheDocument(); + }); + + it('Open up delete pledge modal', async () => { + const { getByTestId, findByText } = render( + , + ); + + userEvent.click(getByTestId('deletePledgeButton')); + + expect(await findByText('Remove Commitment')).toBeInTheDocument(); }); }); }); diff --git a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx index 9fc76efd4..8f6976d18 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx @@ -1,5 +1,13 @@ -import React, { useEffect, useMemo } from 'react'; -import { Box, Checkbox, ListItemIcon, Typography } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import { + Box, + Checkbox, + IconButton, + ListItemIcon, + Typography, +} from '@mui/material'; import { styled } from '@mui/material/styles'; import { useDrag } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; @@ -12,9 +20,9 @@ import { DraggableBox, } 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 { useLocale } from 'src/hooks/useLocale'; -import { currencyFormat } from 'src/lib/intlFormat'; import theme from 'src/theme'; import { getLocalizedContactStatus } from 'src/utils/functions/getLocalizedContactStatus'; import { @@ -23,6 +31,15 @@ import { AppealsType, } from '../../AppealsContext/AppealsContext'; import { AppealContactInfoFragment } from '../../AppealsContext/contacts.generated'; +import { + DynamicDeletePledgeModal, + preloadDeletePledgeModal, +} from '../../Modals/DeletePledgeModal/DynamicDeletePledgeModal'; +import { + DynamicPledgeModal, + preloadPledgeModal, +} from '../../Modals/PledgeModal/DynamicPledgeModal'; +import { AmountAndFrequency } from '../../Shared/AmountAndFrequency/AmountAndFrequency'; // 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 @@ -53,6 +70,20 @@ const FlexCenterAlignedBox = styled(Box)(() => ({ width: '100%', })); +const CommitmentsBox = styled(Box)(() => ({ + display: 'flex', + alignItems: 'center', + width: '100%', + justifyContent: 'space-between', + marginTop: theme.spacing(2), +})); + +const CommitmentActionsBox = styled(Box)(() => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), +})); + export const ContactFlowRow: React.FC = ({ accountListId, contact, @@ -61,11 +92,19 @@ export const ContactFlowRow: React.FC = ({ onContactSelected, columnWidth, }) => { - const { id, name, starred, pledgeAmount, pledgeCurrency } = contact; + const { id, name, starred } = contact; const { t } = useTranslation(); const locale = useLocale(); - const { isRowChecked: isChecked, toggleSelectionById: onContactCheckToggle } = - React.useContext(AppealsContext) as AppealsType; + const { + appealId, + isRowChecked: isChecked, + toggleSelectionById: onContactCheckToggle, + } = React.useContext(AppealsContext) as AppealsType; + const [createPledgeModalOpen, setPledgeModalOpen] = useState(false); + const [deletePledgeModalOpen, setDeletePledgeModalOpen] = useState(false); + + const { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue } = + useGetPledgeOrDonation(appealStatus, contact, appealId ?? '', locale); const [{ isDragging }, drag, preview] = useDrag( () => ({ @@ -90,58 +129,113 @@ export const ContactFlowRow: React.FC = ({ preview(getEmptyImage(), { captureDraggingState: true }); }, []); - const pledgedAmount = useMemo(() => { - if (pledgeAmount && pledgeCurrency) { - return currencyFormat(pledgeAmount ?? 0, pledgeCurrency, locale); - } else { - return null; - } - }, [pledgeAmount, pledgeCurrency, locale]); + const handleEditContact = () => { + setPledgeModalOpen(true); + }; + const handleRemovePledge = () => { + setDeletePledgeModalOpen(true); + }; return ( - - - - - - - onContactSelected(id, true, true)}> - {name} - - - {getLocalizedContactStatus(t, contactStatus)} - - - - - - event.stopPropagation()} - onChange={() => onContactCheckToggle(contact.id)} + <> + + + + + - - - - - {pledgedAmount && ( - - {pledgedAmount} + + onContactSelected(id, true, true)}> + {name} + + + {getLocalizedContactStatus(t, contactStatus)} + + + + + + event.stopPropagation()} + onChange={() => onContactCheckToggle(contact.id)} + /> + - )} - - - + + + + + {appealStatus !== AppealStatusEnum.Processed && ( + + + + )} + + {appealStatus === AppealStatusEnum.Processed && + pledgeDonations?.map((donation, idx) => ( + + {amountAndFrequency} {donation} + + ))} + + + {(appealStatus === AppealStatusEnum.NotReceived || + appealStatus === AppealStatusEnum.Processed || + appealStatus === AppealStatusEnum.ReceivedNotProcessed) && ( + + + + + + + + + )} + + + + + + + {createPledgeModalOpen && ( + setPledgeModalOpen(false)} + pledge={pledgeValues} + /> + )} + {deletePledgeModalOpen && pledgeValues && ( + setDeletePledgeModalOpen(false)} + /> + )} + ); }; diff --git a/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx b/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx index d2bb921e6..d8381d50a 100644 --- a/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx +++ b/src/components/Tool/Appeal/List/ContactRow/ContactRow.test.tsx @@ -64,7 +64,8 @@ describe('ContactsRow', () => { const { getByText } = render(); expect(getByText('Test, Name')).toBeInTheDocument(); - expect(getByText('CA$500 Monthly')).toBeInTheDocument(); + expect(getByText('CA$500')).toBeInTheDocument(); + expect(getByText('Monthly')).toBeInTheDocument(); }); it('should render check event', async () => { @@ -111,7 +112,8 @@ describe('ContactsRow', () => { ); expect(getByText('Reason')).toBeInTheDocument(); - expect(getByText('CA$500 Monthly')).toBeInTheDocument(); + expect(getByText('CA$500')).toBeInTheDocument(); + expect(getByText('Monthly')).toBeInTheDocument(); }); it('Asked', () => { @@ -122,7 +124,8 @@ describe('ContactsRow', () => { ); expect(queryByText('Reason')).not.toBeInTheDocument(); - expect(getByText('CA$500 Monthly')).toBeInTheDocument(); + expect(getByText('CA$500')).toBeInTheDocument(); + expect(getByText('Monthly')).toBeInTheDocument(); }); it('Committed', () => { @@ -133,7 +136,8 @@ describe('ContactsRow', () => { ); expect(queryByText('Reason')).not.toBeInTheDocument(); - expect(getByText('$3,000 (Aug 8, 2024)')).toBeInTheDocument(); + expect(getByText('$3,000')).toBeInTheDocument(); + expect(getByText('(Aug 8, 2024)')).toBeInTheDocument(); }); it('Committed - with no pledges', () => { @@ -159,7 +163,8 @@ describe('ContactsRow', () => { ); expect(queryByText('Reason')).not.toBeInTheDocument(); - expect(getByText('$3,000 (Aug 8, 2024)')).toBeInTheDocument(); + expect(getByText('$3,000')).toBeInTheDocument(); + expect(getByText('(Aug 8, 2024)')).toBeInTheDocument(); }); it('Given', () => { diff --git a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx index 2fd26f4ed..13340e5d6 100644 --- a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx +++ b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx @@ -18,7 +18,7 @@ import { StyledCheckbox, } from 'src/components/Contacts/ContactRow/ContactRow'; import { preloadContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/DynamicContactsRightPanel'; -import { useGetPledgeOrDonation } from 'src/hooks/useGetPledgeOrDonation'; +import { useGetPledgeOrDonation } from 'src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation'; import { useLocale } from 'src/hooks/useLocale'; import theme from 'src/theme'; import { @@ -43,6 +43,7 @@ import { DynamicPledgeModal, preloadPledgeModal, } from '../../Modals/PledgeModal/DynamicPledgeModal'; +import { AmountAndFrequency } from '../../Shared/AmountAndFrequency/AmountAndFrequency'; // 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 @@ -91,7 +92,7 @@ export const ContactRow: React.FC = ({ const { id: contactId, name } = contact; - const { pledgeValues, amountAndFrequency, pledgeDonations } = + const { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue } = useGetPledgeOrDonation(appealStatus, contact, appealId ?? '', locale); const handleCreatePledge = () => { @@ -188,7 +189,12 @@ export const ContactRow: React.FC = ({ justifyContent="center" > {appealStatus !== AppealStatusEnum.Processed && ( - {amountAndFrequency} + + + )} {appealStatus === AppealStatusEnum.Processed && diff --git a/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx b/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx new file mode 100644 index 000000000..810c59117 --- /dev/null +++ b/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx @@ -0,0 +1,25 @@ +import { UseGetPledgeOrDonation } from 'src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation'; +import theme from 'src/theme'; + +export const AmountAndFrequency: React.FC< + Omit +> = ({ amountAndFrequency, pledgeOverdue }) => { + const amount = amountAndFrequency?.length ? amountAndFrequency[0] : ''; + const dateString = + amountAndFrequency?.length === 2 ? ( + + {amountAndFrequency[1]} + + ) : ( + '' + ); + return ( + <> + {amount} {dateString} + + ); +}; diff --git a/src/hooks/useGetPledgeOrDonation.test.ts b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts similarity index 55% rename from src/hooks/useGetPledgeOrDonation.test.ts rename to src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts index 1889ac7fd..ca7c9be91 100644 --- a/src/hooks/useGetPledgeOrDonation.test.ts +++ b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts @@ -7,15 +7,10 @@ const appealId = 'appealId'; describe('useGetPledgeOrDonation', () => { it('returns the normal donation amount when in appeal status Asked', () => { const { result } = renderHook(() => - useGetPledgeOrDonation( - AppealStatusEnum.Asked, - defaultContact, - appealId, - 'en-US', - ), + useGetPledgeOrDonation(AppealStatusEnum.Asked, defaultContact, appealId), ); - expect(result.current.amountAndFrequency).toEqual('CA$500 Monthly'); + expect(result.current.amountAndFrequency).toEqual(['CA$500', 'Monthly']); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toBeUndefined(); @@ -27,11 +22,10 @@ describe('useGetPledgeOrDonation', () => { AppealStatusEnum.Excluded, defaultContact, appealId, - 'en-US', ), ); - expect(result.current.amountAndFrequency).toEqual('CA$500 Monthly'); + expect(result.current.amountAndFrequency).toEqual(['CA$500', 'Monthly']); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toBeUndefined(); @@ -43,11 +37,13 @@ describe('useGetPledgeOrDonation', () => { AppealStatusEnum.NotReceived, defaultContact, appealId, - 'en-US', ), ); - expect(result.current.amountAndFrequency).toEqual('$3,000 (Aug 8, 2024)'); + expect(result.current.amountAndFrequency).toEqual([ + '$3,000', + '(Aug 8, 2024)', + ]); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toEqual({ @@ -65,11 +61,13 @@ describe('useGetPledgeOrDonation', () => { AppealStatusEnum.ReceivedNotProcessed, defaultContact, appealId, - 'en-US', ), ); - expect(result.current.amountAndFrequency).toEqual('$3,000 (Aug 8, 2024)'); + expect(result.current.amountAndFrequency).toEqual([ + '$3,000', + '(Aug 8, 2024)', + ]); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toEqual({ @@ -87,11 +85,10 @@ describe('useGetPledgeOrDonation', () => { AppealStatusEnum.Processed, defaultContact, appealId, - 'en-US', ), ); - expect(result.current.amountAndFrequency).toEqual('$3,000'); + expect(result.current.amountAndFrequency).toEqual(['$3,000']); expect(result.current.pledgeDonations).toEqual(['($50) (Jun 25, 2019)']); expect(result.current.pledgeValues).toEqual({ @@ -102,4 +99,58 @@ describe('useGetPledgeOrDonation', () => { id: 'pledge-1', }); }); + + describe('pledgeOverdue', () => { + it('returns an overdue date when appeal status Received', () => { + const contact = { + ...defaultContact, + pledges: [ + { + id: 'pledge-1', + amount: 3000, + amountCurrency: 'USD', + appeal: { + id: 'appealId', + }, + expectedDate: '2001-08-08', + }, + ], + }; + const { result } = renderHook(() => + useGetPledgeOrDonation(AppealStatusEnum.NotReceived, contact, appealId), + ); + expect(result.current.amountAndFrequency).toEqual([ + '$3,000', + '(Aug 8, 2001)', + ]); + expect(result.current.pledgeOverdue).toEqual(true); + }); + + it('returns an overdue date when appeal status Given', () => { + const contact = { + ...defaultContact, + donations: { + nodes: [ + { + id: 'donation-3', + appeal: { + id: 'appealId', + }, + donationDate: '2001-06-25', + appealAmount: { + amount: 50, + convertedAmount: 50, + convertedCurrency: 'USD', + }, + }, + ], + }, + }; + const { result } = renderHook(() => + useGetPledgeOrDonation(AppealStatusEnum.Processed, contact, appealId), + ); + expect(result.current.pledgeDonations).toEqual(['($50) (Jun 25, 2001)']); + expect(result.current.pledgeOverdue).toEqual(false); + }); + }); }); diff --git a/src/hooks/useGetPledgeOrDonation.ts b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts similarity index 77% rename from src/hooks/useGetPledgeOrDonation.ts rename to src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts index 6d464f6c4..c728b867a 100644 --- a/src/hooks/useGetPledgeOrDonation.ts +++ b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts @@ -7,6 +7,7 @@ import { AppealContactInfoFragment } from 'src/components/Tool/Appeal/AppealsCon import { PledgeFrequencyEnum } from 'src/graphql/types.generated'; import { currencyFormat, dateFormat } from 'src/lib/intlFormat'; import { getLocalizedPledgeFrequency } from 'src/utils/functions/getLocalizedPledgeFrequency'; +import { useLocale } from '../../../../../hooks/useLocale'; type FormatPledgeOrDonationProps = { amount?: number | null; @@ -30,6 +31,18 @@ const formatPledgeOrDonation = ({ ? currencyFormat(amount, currency, locale) : amount || currencyFormat(0, currency, locale); + let pledgeOverdue = false; + if ( + (appealStatus === AppealStatusEnum.NotReceived || + appealStatus === AppealStatusEnum.ReceivedNotProcessed) && + dateOrFrequency + ) { + const date = DateTime.fromISO(dateOrFrequency).startOf('day'); + if (date <= DateTime.local().startOf('day')) { + pledgeOverdue = true; + } + } + const pledgeOrDonationDate = appealStatus === AppealStatusEnum.Asked || appealStatus === AppealStatusEnum.Excluded @@ -45,21 +58,29 @@ const formatPledgeOrDonation = ({ return { amount: pledgeOrDonationAmount, dateOrFrequency: pledgeOrDonationDate, + pledgeOverdue, }; }; +export interface UseGetPledgeOrDonation { + pledgeValues: AppealContactInfoFragment['pledges'][0] | undefined; + amountAndFrequency: string[] | undefined; + pledgeDonations: string[] | null; + pledgeOverdue: boolean; +} // The return value doesn't change until `delay` milliseconds have elapsed since the last time `value` changed export const useGetPledgeOrDonation = ( appealStatus: AppealStatusEnum, contact: AppealContactInfoFragment, appealId: string, - locale: string, -) => { +): UseGetPledgeOrDonation => { + const locale = useLocale(); const { t } = useTranslation(); const [pledgeValues, setPledgeValues] = useState(); - const [amountAndFrequency, setAmountAndFrequency] = useState(); + const [amountAndFrequency, setAmountAndFrequency] = useState(); const [pledgeDonations, setPledgeDonations] = useState(null); + const [pledgeOverdue, setPledgeOverdue] = useState(false); useEffect(() => { const { @@ -82,7 +103,7 @@ export const useGetPledgeOrDonation = ( locale, t, }); - setAmountAndFrequency(`${amount} ${dateOrFrequency}`); + setAmountAndFrequency([`${amount}`, `${dateOrFrequency}`]); setPledgeValues(undefined); } else if ( appealStatus === AppealStatusEnum.NotReceived || @@ -93,7 +114,11 @@ export const useGetPledgeOrDonation = ( ); if (appealPledge) { - const { amount, dateOrFrequency } = formatPledgeOrDonation({ + const { + amount, + dateOrFrequency, + pledgeOverdue: overdue, + } = formatPledgeOrDonation({ amount: appealPledge?.amount, currency: appealPledge.amountCurrency, appealStatus, @@ -103,9 +128,10 @@ export const useGetPledgeOrDonation = ( }); setPledgeValues(appealPledge); - setAmountAndFrequency(`${amount} (${dateOrFrequency})`); + setAmountAndFrequency([`${amount}`, `(${dateOrFrequency})`]); + setPledgeOverdue(overdue); } else { - setAmountAndFrequency(`${currencyFormat(0, 'USD', locale)}`); + setAmountAndFrequency([`${currencyFormat(0, 'USD', locale)}`]); } } else if (appealStatus === AppealStatusEnum.Processed) { const appealPledge = pledges?.find( @@ -121,9 +147,9 @@ export const useGetPledgeOrDonation = ( t, }); setPledgeValues(appealPledge); - setAmountAndFrequency(`${amount}`); + setAmountAndFrequency([`${amount}`]); } else { - setAmountAndFrequency(`${currencyFormat(0, 'USD', locale)}`); + setAmountAndFrequency([`${currencyFormat(0, 'USD', locale)}`]); } // Currently we grab all the donations and filter them by the appeal id @@ -154,5 +180,5 @@ export const useGetPledgeOrDonation = ( } }, [appealStatus, contact, locale]); - return { pledgeValues, amountAndFrequency, pledgeDonations }; + return { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue }; }; From aad2fc4103907f6fb388e589c980af51bd9ba9df Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Fri, 30 Aug 2024 14:33:52 -0400 Subject: [PATCH 4/4] Updating the useGetPledgeOrDonation hook to return a object and use a useMemo and Making hook take one object instead of 3 arguments --- .../Flow/ContactFlowRow/ContactFlowRow.tsx | 10 +- .../Appeal/List/ContactRow/ContactRow.tsx | 14 +- .../AmountAndFrequency/AmountAndFrequency.tsx | 27 ++-- .../useGetPledgeOrDonation.test.ts | 97 ++++++++------ .../useGetPledgeOrDonation.ts | 125 ++++++++++++------ 5 files changed, 172 insertions(+), 101 deletions(-) diff --git a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx index 8f6976d18..0d655bee2 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlowRow/ContactFlowRow.tsx @@ -22,7 +22,6 @@ import { 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 { useLocale } from 'src/hooks/useLocale'; import theme from 'src/theme'; import { getLocalizedContactStatus } from 'src/utils/functions/getLocalizedContactStatus'; import { @@ -94,7 +93,6 @@ export const ContactFlowRow: React.FC = ({ }) => { const { id, name, starred } = contact; const { t } = useTranslation(); - const locale = useLocale(); const { appealId, isRowChecked: isChecked, @@ -104,7 +102,7 @@ export const ContactFlowRow: React.FC = ({ const [deletePledgeModalOpen, setDeletePledgeModalOpen] = useState(false); const { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue } = - useGetPledgeOrDonation(appealStatus, contact, appealId ?? '', locale); + useGetPledgeOrDonation({ appealStatus, contact, appealId: appealId ?? '' }); const [{ isDragging }, drag, preview] = useDrag( () => ({ @@ -188,7 +186,11 @@ export const ContactFlowRow: React.FC = ({ {appealStatus === AppealStatusEnum.Processed && pledgeDonations?.map((donation, idx) => ( - {amountAndFrequency} {donation} + {' '} + {donation} ))} diff --git a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx index 13340e5d6..4af3ceee3 100644 --- a/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx +++ b/src/components/Tool/Appeal/List/ContactRow/ContactRow.tsx @@ -19,7 +19,6 @@ import { } from 'src/components/Contacts/ContactRow/ContactRow'; import { preloadContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/DynamicContactsRightPanel'; import { useGetPledgeOrDonation } from 'src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation'; -import { useLocale } from 'src/hooks/useLocale'; import theme from 'src/theme'; import { AppealStatusEnum, @@ -79,7 +78,6 @@ export const ContactRow: React.FC = ({ setContactFocus: onContactSelected, toggleSelectionById: onContactCheckToggle, } = React.useContext(AppealsContext) as AppealsType; - const locale = useLocale(); const [createPledgeModalOpen, setPledgeModalOpen] = useState(false); const [deletePledgeModalOpen, setDeletePledgeModalOpen] = useState(false); const [addExcludedContactModalOpen, setAddExcludedContactModalOpen] = @@ -93,7 +91,11 @@ export const ContactRow: React.FC = ({ const { id: contactId, name } = contact; const { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue } = - useGetPledgeOrDonation(appealStatus, contact, appealId ?? '', locale); + useGetPledgeOrDonation({ + appealStatus, + contact, + appealId: appealId ?? '', + }); const handleCreatePledge = () => { setPledgeModalOpen(true); @@ -200,7 +202,11 @@ export const ContactRow: React.FC = ({ {appealStatus === AppealStatusEnum.Processed && pledgeDonations?.map((donation, idx) => ( - {amountAndFrequency} {donation} + + {donation} ))} diff --git a/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx b/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx index 810c59117..972174b3d 100644 --- a/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx +++ b/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx @@ -2,21 +2,20 @@ import { UseGetPledgeOrDonation } from 'src/components/Tool/Appeal/Shared/useGet import theme from 'src/theme'; export const AmountAndFrequency: React.FC< - Omit + Pick > = ({ amountAndFrequency, pledgeOverdue }) => { - const amount = amountAndFrequency?.length ? amountAndFrequency[0] : ''; - const dateString = - amountAndFrequency?.length === 2 ? ( - - {amountAndFrequency[1]} - - ) : ( - '' - ); + const amount = amountAndFrequency?.amount ?? ''; + const dateString = amountAndFrequency?.dateOrFrequency ? ( + + {amountAndFrequency?.dateOrFrequency} + + ) : ( + '' + ); return ( <> {amount} {dateString} diff --git a/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts index ca7c9be91..27f30fbff 100644 --- a/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts +++ b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts @@ -7,10 +7,17 @@ const appealId = 'appealId'; describe('useGetPledgeOrDonation', () => { it('returns the normal donation amount when in appeal status Asked', () => { const { result } = renderHook(() => - useGetPledgeOrDonation(AppealStatusEnum.Asked, defaultContact, appealId), + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.Asked, + contact: defaultContact, + appealId: appealId, + }), ); - expect(result.current.amountAndFrequency).toEqual(['CA$500', 'Monthly']); + expect(result.current.amountAndFrequency).toEqual({ + amount: 'CA$500', + dateOrFrequency: 'Monthly', + }); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toBeUndefined(); @@ -18,14 +25,17 @@ describe('useGetPledgeOrDonation', () => { it('returns the normal donation amount when in appeal status Excluded', () => { const { result } = renderHook(() => - useGetPledgeOrDonation( - AppealStatusEnum.Excluded, - defaultContact, - appealId, - ), + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.Excluded, + contact: defaultContact, + appealId: appealId, + }), ); - expect(result.current.amountAndFrequency).toEqual(['CA$500', 'Monthly']); + expect(result.current.amountAndFrequency).toEqual({ + amount: 'CA$500', + dateOrFrequency: 'Monthly', + }); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toBeUndefined(); @@ -33,17 +43,17 @@ describe('useGetPledgeOrDonation', () => { it('returns the pledge when in appeal status Committed', () => { const { result } = renderHook(() => - useGetPledgeOrDonation( - AppealStatusEnum.NotReceived, - defaultContact, - appealId, - ), + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.NotReceived, + contact: defaultContact, + appealId: appealId, + }), ); - expect(result.current.amountAndFrequency).toEqual([ - '$3,000', - '(Aug 8, 2024)', - ]); + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '(Aug 8, 2024)', + }); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toEqual({ @@ -57,17 +67,17 @@ describe('useGetPledgeOrDonation', () => { it('returns the pledge when in appeal status Received', () => { const { result } = renderHook(() => - useGetPledgeOrDonation( - AppealStatusEnum.ReceivedNotProcessed, - defaultContact, - appealId, - ), + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.ReceivedNotProcessed, + contact: defaultContact, + appealId: appealId, + }), ); - expect(result.current.amountAndFrequency).toEqual([ - '$3,000', - '(Aug 8, 2024)', - ]); + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '(Aug 8, 2024)', + }); expect(result.current.pledgeDonations).toBeNull(); expect(result.current.pledgeValues).toEqual({ @@ -81,14 +91,17 @@ describe('useGetPledgeOrDonation', () => { it('returns the donations to appeal when in appeal status Given', () => { const { result } = renderHook(() => - useGetPledgeOrDonation( - AppealStatusEnum.Processed, - defaultContact, - appealId, - ), + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.Processed, + contact: defaultContact, + appealId: appealId, + }), ); - expect(result.current.amountAndFrequency).toEqual(['$3,000']); + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '', + }); expect(result.current.pledgeDonations).toEqual(['($50) (Jun 25, 2019)']); expect(result.current.pledgeValues).toEqual({ @@ -117,12 +130,16 @@ describe('useGetPledgeOrDonation', () => { ], }; const { result } = renderHook(() => - useGetPledgeOrDonation(AppealStatusEnum.NotReceived, contact, appealId), + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.NotReceived, + contact: contact, + appealId: appealId, + }), ); - expect(result.current.amountAndFrequency).toEqual([ - '$3,000', - '(Aug 8, 2001)', - ]); + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '(Aug 8, 2001)', + }); expect(result.current.pledgeOverdue).toEqual(true); }); @@ -147,7 +164,11 @@ describe('useGetPledgeOrDonation', () => { }, }; const { result } = renderHook(() => - useGetPledgeOrDonation(AppealStatusEnum.Processed, contact, appealId), + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.Processed, + contact: contact, + appealId: appealId, + }), ); expect(result.current.pledgeDonations).toEqual(['($50) (Jun 25, 2001)']); expect(result.current.pledgeOverdue).toEqual(false); diff --git a/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts index c728b867a..e49c90183 100644 --- a/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts +++ b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { TFunction } from 'i18next'; import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; @@ -29,7 +29,7 @@ const formatPledgeOrDonation = ({ const pledgeOrDonationAmount = amount && currency ? currencyFormat(amount, currency, locale) - : amount || currencyFormat(0, currency, locale); + : amount?.toString() || currencyFormat(0, currency, locale); let pledgeOverdue = false; if ( @@ -54,7 +54,7 @@ const formatPledgeOrDonation = ({ '' : dateOrFrequency ? dateFormat(DateTime.fromISO(dateOrFrequency), locale) - : null; + : ''; return { amount: pledgeOrDonationAmount, dateOrFrequency: pledgeOrDonationDate, @@ -62,27 +62,38 @@ const formatPledgeOrDonation = ({ }; }; +interface AmountAndFrequency { + amount: string; + dateOrFrequency?: string; +} export interface UseGetPledgeOrDonation { pledgeValues: AppealContactInfoFragment['pledges'][0] | undefined; - amountAndFrequency: string[] | undefined; + amountAndFrequency: AmountAndFrequency | null; pledgeDonations: string[] | null; pledgeOverdue: boolean; } -// The return value doesn't change until `delay` milliseconds have elapsed since the last time `value` changed + +interface UseGetPledgeOrDonationProps { + appealStatus: AppealStatusEnum; + contact: AppealContactInfoFragment; + appealId: string; +} + export const useGetPledgeOrDonation = ( - appealStatus: AppealStatusEnum, - contact: AppealContactInfoFragment, - appealId: string, + props: UseGetPledgeOrDonationProps, ): UseGetPledgeOrDonation => { const locale = useLocale(); const { t } = useTranslation(); - const [pledgeValues, setPledgeValues] = - useState(); - const [amountAndFrequency, setAmountAndFrequency] = useState(); - const [pledgeDonations, setPledgeDonations] = useState(null); - const [pledgeOverdue, setPledgeOverdue] = useState(false); + const { appealStatus, contact, appealId } = props; + + const defaultValues = { + amountAndFrequency: null, + pledgeValues: undefined, + pledgeOverdue: false, + pledgeDonations: null, + }; - useEffect(() => { + const pledgeOrDonation = useMemo(() => { const { pledgeAmount, pledgeCurrency, @@ -103,8 +114,14 @@ export const useGetPledgeOrDonation = ( locale, t, }); - setAmountAndFrequency([`${amount}`, `${dateOrFrequency}`]); - setPledgeValues(undefined); + + return { + ...defaultValues, + amountAndFrequency: { + amount, + dateOrFrequency, + }, + }; } else if ( appealStatus === AppealStatusEnum.NotReceived || appealStatus === AppealStatusEnum.ReceivedNotProcessed @@ -113,31 +130,50 @@ export const useGetPledgeOrDonation = ( (pledge) => pledge.appeal.id === appealId, ); - if (appealPledge) { - const { - amount, - dateOrFrequency, - pledgeOverdue: overdue, - } = formatPledgeOrDonation({ - amount: appealPledge?.amount, - currency: appealPledge.amountCurrency, - appealStatus, - dateOrFrequency: appealPledge.expectedDate, - locale, - t, - }); - - setPledgeValues(appealPledge); - setAmountAndFrequency([`${amount}`, `(${dateOrFrequency})`]); - setPledgeOverdue(overdue); - } else { - setAmountAndFrequency([`${currencyFormat(0, 'USD', locale)}`]); + if (!appealPledge) { + return { + ...defaultValues, + amountAndFrequency: { + amount: currencyFormat(0, 'USD', locale), + dateOrFrequency: '', + }, + }; } + + const { + amount, + dateOrFrequency, + pledgeOverdue: overdue, + } = formatPledgeOrDonation({ + amount: appealPledge?.amount, + currency: appealPledge.amountCurrency, + appealStatus, + dateOrFrequency: appealPledge.expectedDate, + locale, + t, + }); + + return { + ...defaultValues, + amountAndFrequency: { + amount, + dateOrFrequency: `(${dateOrFrequency})`, + }, + pledgeValues: appealPledge, + pledgeOverdue: overdue, + }; } else if (appealStatus === AppealStatusEnum.Processed) { const appealPledge = pledges?.find( (pledge) => pledge.appeal.id === appealId, ); + const amountAndFrequency = { + amount: currencyFormat(0, 'USD', locale), + dateOrFrequency: '', + }; + let pledgeValues: AppealContactInfoFragment['pledges'][0] | undefined = + undefined; + if (appealPledge) { const { amount } = formatPledgeOrDonation({ amount: appealPledge?.amount, @@ -146,10 +182,8 @@ export const useGetPledgeOrDonation = ( locale, t, }); - setPledgeValues(appealPledge); - setAmountAndFrequency([`${amount}`]); - } else { - setAmountAndFrequency([`${currencyFormat(0, 'USD', locale)}`]); + amountAndFrequency.amount = amount; + pledgeValues = appealPledge; } // Currently we grab all the donations and filter them by the appeal id @@ -176,9 +210,18 @@ export const useGetPledgeOrDonation = ( return `(${donationAmount}) (${donationDate})`; }); - setPledgeDonations(givenDonations); + return { + ...defaultValues, + amountAndFrequency, + pledgeValues, + pledgeDonations: givenDonations, + }; } }, [appealStatus, contact, locale]); - return { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue }; + if (pledgeOrDonation) { + return pledgeOrDonation; + } else { + return defaultValues; + } };