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.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 4f0512a85..0d655bee2 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,8 @@ 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 { @@ -22,11 +29,22 @@ import { AppealsContext, 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 -interface Props extends Omit { +interface Props extends Omit { + contact: AppealContactInfoFragment; contactStatus?: StatusEnum | null; appealStatus: AppealStatusEnum; } @@ -51,6 +69,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, @@ -59,11 +91,18 @@ 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: appealId ?? '' }); const [{ isDragging }, drag, preview] = useDrag( () => ({ @@ -88,58 +127,117 @@ 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) => ( + + {' '} + {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 d90175ce4..4af3ceee3 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,13 @@ 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 { useLocale } from 'src/hooks/useLocale'; -import { currencyFormat, dateFormat } from 'src/lib/intlFormat'; +import { useGetPledgeOrDonation } from 'src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation'; import theme from 'src/theme'; -import { getLocalizedPledgeFrequency } from 'src/utils/functions/getLocalizedPledgeFrequency'; import { AppealStatusEnum, AppealsContext, @@ -48,6 +42,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 @@ -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,24 @@ 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, - ); + const { id: contactId, name } = contact; - 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]); + const { pledgeValues, amountAndFrequency, pledgeDonations, pledgeOverdue } = + useGetPledgeOrDonation({ + appealStatus, + contact, + appealId: appealId ?? '', + }); const handleCreatePledge = () => { setPledgeModalOpen(true); @@ -328,13 +191,22 @@ export const ContactRow: React.FC = ({ justifyContent="center" > {appealStatus !== AppealStatusEnum.Processed && ( - {amountAndFrequency} + + + )} {appealStatus === AppealStatusEnum.Processed && pledgeDonations?.map((donation, idx) => ( - {amountAndFrequency} {donation} + + {donation} ))} 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', 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..972174b3d --- /dev/null +++ b/src/components/Tool/Appeal/Shared/AmountAndFrequency/AmountAndFrequency.tsx @@ -0,0 +1,24 @@ +import { UseGetPledgeOrDonation } from 'src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation'; +import theme from 'src/theme'; + +export const AmountAndFrequency: React.FC< + Pick +> = ({ amountAndFrequency, pledgeOverdue }) => { + 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 new file mode 100644 index 000000000..27f30fbff --- /dev/null +++ b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.test.ts @@ -0,0 +1,177 @@ +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({ + appealStatus: AppealStatusEnum.Asked, + contact: defaultContact, + appealId: appealId, + }), + ); + + expect(result.current.amountAndFrequency).toEqual({ + amount: 'CA$500', + dateOrFrequency: '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({ + appealStatus: AppealStatusEnum.Excluded, + contact: defaultContact, + appealId: appealId, + }), + ); + + expect(result.current.amountAndFrequency).toEqual({ + amount: 'CA$500', + dateOrFrequency: 'Monthly', + }); + + expect(result.current.pledgeDonations).toBeNull(); + expect(result.current.pledgeValues).toBeUndefined(); + }); + + it('returns the pledge when in appeal status Committed', () => { + const { result } = renderHook(() => + useGetPledgeOrDonation({ + appealStatus: AppealStatusEnum.NotReceived, + contact: defaultContact, + appealId: appealId, + }), + ); + + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '(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({ + appealStatus: AppealStatusEnum.ReceivedNotProcessed, + contact: defaultContact, + appealId: appealId, + }), + ); + + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '(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({ + appealStatus: AppealStatusEnum.Processed, + contact: defaultContact, + appealId: appealId, + }), + ); + + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '', + }); + + 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', + }); + }); + + 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({ + appealStatus: AppealStatusEnum.NotReceived, + contact: contact, + appealId: appealId, + }), + ); + expect(result.current.amountAndFrequency).toEqual({ + amount: '$3,000', + dateOrFrequency: '(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({ + 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 new file mode 100644 index 000000000..e49c90183 --- /dev/null +++ b/src/components/Tool/Appeal/Shared/useGetPledgeOrDonation/useGetPledgeOrDonation.ts @@ -0,0 +1,227 @@ +import { useMemo } 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'; +import { useLocale } from '../../../../../hooks/useLocale'; + +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?.toString() || 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 + ? (dateOrFrequency && + getLocalizedPledgeFrequency( + t, + dateOrFrequency as PledgeFrequencyEnum, + )) ?? + '' + : dateOrFrequency + ? dateFormat(DateTime.fromISO(dateOrFrequency), locale) + : ''; + return { + amount: pledgeOrDonationAmount, + dateOrFrequency: pledgeOrDonationDate, + pledgeOverdue, + }; +}; + +interface AmountAndFrequency { + amount: string; + dateOrFrequency?: string; +} +export interface UseGetPledgeOrDonation { + pledgeValues: AppealContactInfoFragment['pledges'][0] | undefined; + amountAndFrequency: AmountAndFrequency | null; + pledgeDonations: string[] | null; + pledgeOverdue: boolean; +} + +interface UseGetPledgeOrDonationProps { + appealStatus: AppealStatusEnum; + contact: AppealContactInfoFragment; + appealId: string; +} + +export const useGetPledgeOrDonation = ( + props: UseGetPledgeOrDonationProps, +): UseGetPledgeOrDonation => { + const locale = useLocale(); + const { t } = useTranslation(); + const { appealStatus, contact, appealId } = props; + + const defaultValues = { + amountAndFrequency: null, + pledgeValues: undefined, + pledgeOverdue: false, + pledgeDonations: null, + }; + + const pledgeOrDonation = useMemo(() => { + 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, + }); + + return { + ...defaultValues, + amountAndFrequency: { + amount, + dateOrFrequency, + }, + }; + } else if ( + appealStatus === AppealStatusEnum.NotReceived || + appealStatus === AppealStatusEnum.ReceivedNotProcessed + ) { + const appealPledge = pledges?.find( + (pledge) => pledge.appeal.id === appealId, + ); + + 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, + currency: appealPledge.amountCurrency, + appealStatus, + locale, + t, + }); + amountAndFrequency.amount = amount; + pledgeValues = appealPledge; + } + + // 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})`; + }); + + return { + ...defaultValues, + amountAndFrequency, + pledgeValues, + pledgeDonations: givenDonations, + }; + } + }, [appealStatus, contact, locale]); + + if (pledgeOrDonation) { + return pledgeOrDonation; + } else { + return defaultValues; + } +};