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;
+ }
+};