diff --git a/pages/accountLists/[accountListId]/tools/appeals.page.tsx b/pages/accountLists/[accountListId]/tools/appeals.page.tsx
index b45e93234..6c9405c2f 100644
--- a/pages/accountLists/[accountListId]/tools/appeals.page.tsx
+++ b/pages/accountLists/[accountListId]/tools/appeals.page.tsx
@@ -1,30 +1,18 @@
-import Head from 'next/head';
import React from 'react';
import { Box, Divider, Grid, Theme, Typography } from '@mui/material';
-import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { loadSession } from 'pages/api/utils/pagePropsHelpers';
import Loading from 'src/components/Loading';
import AddAppealForm from 'src/components/Tool/Appeal/AddAppealForm';
import Appeals from 'src/components/Tool/Appeal/Appeals';
-import { useAccountListId } from 'src/hooks/useAccountListId';
-import useGetAppSettings from 'src/hooks/useGetAppSettings';
+import { ToolsWrapper } from './ToolsWrapper';
+import { useToolsHelper } from './useToolsHelper';
const useStyles = makeStyles()((theme: Theme) => ({
container: {
- padding: theme.spacing(3),
- width: '70%',
+ padding: `${theme.spacing(3)} ${theme.spacing(3)} 0`,
display: 'flex',
- [theme.breakpoints.down('lg')]: {
- width: '90%',
- },
- [theme.breakpoints.down('md')]: {
- width: '70%',
- },
- [theme.breakpoints.down('sm')]: {
- width: '100%',
- },
},
outer: {
display: 'flex',
@@ -40,71 +28,51 @@ const useStyles = makeStyles()((theme: Theme) => ({
const AppealsPage: React.FC = () => {
const { t } = useTranslation();
const { classes } = useStyles();
- const accountListId = useAccountListId();
- const { appName } = useGetAppSettings();
- const variants = {
- animate: {
- transition: {
- staggerChildren: 0.15,
- },
- },
- exit: {
- transition: {
- staggerChildren: 0.1,
- },
- },
- };
+ const { accountListId } = useToolsHelper();
+ const pageUrl = 'tools/fixCommitmentInfo';
return (
- <>
-
-
- {appName} | {t('Appeals')}
-
-
+
{accountListId ? (
-
-
-
-
-
- {t('Appeals')}
-
-
-
-
- {t(
- 'You can track recurring support goals or special need ' +
- 'support goals through our appeals wizard. Track the ' +
- 'recurring support you raise for an increase ask for example, ' +
- 'or special gifts you raise for a summer mission trip or your ' +
- 'new staff special gift goal.',
- )}
-
-
-
+
+
+
+
+ {t('Appeals')}
+
+
+
+
+ {t(
+ 'You can track recurring support goals or special need ' +
+ 'support goals through our appeals wizard. Track the ' +
+ 'recurring support you raise for an increase ask for example, ' +
+ 'or special gifts you raise for a summer mission trip or your ' +
+ 'new staff special gift goal.',
+ )}
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
) : (
)}
- >
+
);
};
diff --git a/src/components/Tool/Appeal/AddAppealForm.tsx b/src/components/Tool/Appeal/AddAppealForm.tsx
index c815d6983..0c2a860f1 100644
--- a/src/components/Tool/Appeal/AddAppealForm.tsx
+++ b/src/components/Tool/Appeal/AddAppealForm.tsx
@@ -1,7 +1,8 @@
-import React, { ReactElement, useState } from 'react';
+import React, { ReactElement, useMemo } from 'react';
import { mdiClose, mdiEqual, mdiPlus } from '@mdi/js';
import Icon from '@mdi/react';
import {
+ Alert,
Autocomplete,
Box,
Button,
@@ -10,6 +11,7 @@ import {
CircularProgress,
FormControl,
Grid,
+ Skeleton,
TextField,
Theme,
Typography,
@@ -21,9 +23,12 @@ import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import * as yup from 'yup';
import { useContactFiltersQuery } from 'pages/accountLists/[accountListId]/contacts/Contacts.generated';
+import {
+ GetAppealsDocument,
+ GetAppealsQuery,
+} from 'pages/accountLists/[accountListId]/tools/GetAppeals.generated';
import { MultiselectFilter } from 'src/graphql/types.generated';
import i18n from 'src/lib/i18n';
-import { useAccountListId } from '../../../hooks/useAccountListId';
import theme from '../../../theme';
import AnimatedCard from '../../AnimatedCard';
import { useCreateAppealMutation } from './CreateAppeal.generated';
@@ -45,14 +50,6 @@ const useStyles = makeStyles()((theme: Theme) => ({
width: '150px',
color: 'white',
},
- blueBox: {
- border: '1px solid',
- borderColor: theme.palette.mpdxBlue.main,
- borderRadius: 5,
- backgroundColor: theme.palette.cruGrayLight.main,
- color: theme.palette.mpdxBlue.main,
- padding: 10,
- },
selectAll: {
color: theme.palette.mpdxBlue.main,
marginLeft: 5,
@@ -93,10 +90,41 @@ const contactExclusions = [
},
];
-const AddAppealForm = (): ReactElement => {
+const calculateGoal = (
+ initialGoal: number,
+ letterCost: number,
+ adminCost: number,
+): number => {
+ return (initialGoal + letterCost) * (1 + adminCost / 100);
+};
+
+const appealFormSchema = yup.object({
+ name: yup.string().required('Please enter a name'),
+ initialGoal: yup.number().required(),
+ letterCost: yup.number().required(),
+ adminCost: yup.number().required(),
+ statuses: yup.array().of(
+ yup.object({
+ __typename: yup.string(),
+ name: yup.string(),
+ value: yup.string(),
+ }),
+ ),
+ tags: yup.array().of(yup.string()),
+ exclusions: yup.array().of(
+ yup.object({
+ name: yup.string(),
+ value: yup.string(),
+ }),
+ ),
+});
+interface AddAppealFormProps {
+ accountListId: string;
+}
+
+const AddAppealForm: React.FC = ({ accountListId }) => {
const { classes } = useStyles();
const { t } = useTranslation();
- const accountListId = useAccountListId() || '';
const { enqueueSnackbar } = useSnackbar();
const { data: contactFilterTags, loading: loadingTags } =
useGetContactTagsQuery({
@@ -111,47 +139,23 @@ const AddAppealForm = (): ReactElement => {
doNotBatch: true,
},
});
+ const [createNewAppeal, { loading: updating }] = useCreateAppealMutation();
- const contactStatuses = contactFilterGroups?.accountList?.contactFilterGroups
- ? (
+ const contactStatuses = useMemo(() => {
+ if (contactFilterGroups?.accountList?.contactFilterGroups) {
+ return (
contactFilterGroups.accountList.contactFilterGroups
.find((group) => group?.filters[0]?.filterKey === 'status')
?.filters.find(
(filter: { filterKey: string }) => filter.filterKey === 'status',
) as MultiselectFilter
- ).options
- : [{ name: '', value: '' }];
-
- const [filterTags, setFilterTags] = useState<{
- statuses: { name: string; value: string }[] | undefined;
- tags: string[];
- exclusions: { name: string; value: string }[];
- }>({
- statuses: [],
- tags: [],
- exclusions: [],
- });
- const [createNewAppeal, { loading: updating }] = useCreateAppealMutation();
+ ).options;
+ } else {
+ return [{ name: '', value: '' }];
+ }
+ }, [contactFilterGroups]);
- const calculateGoal = (
- initialGoal: number,
- letterCost: number,
- adminCost: number,
- ): number => {
- return (initialGoal + letterCost) * (1 + adminCost / 100);
- };
-
- const handleChange = (
- values: { name: string; value: string }[] | string[],
- props: string,
- ): void => {
- setFilterTags((prevState) => ({
- ...prevState,
- [props]: values,
- }));
- };
-
- const onSubmit = async (props: FormAttributes) => {
+ const onSubmit = async (props: FormAttributes, resetForm: () => void) => {
const attributes = {
name: props.name,
amount: calculateGoal(
@@ -166,46 +170,57 @@ const AddAppealForm = (): ReactElement => {
accountListId,
attributes,
},
+ update: (cache, result) => {
+ const query = {
+ query: GetAppealsDocument,
+ variables: { accountListId },
+ };
+ const dataFromCache = cache.readQuery(query);
+ if (dataFromCache && result.data?.createAppeal?.appeal) {
+ const data = {
+ regularAppeals: {
+ ...dataFromCache.regularAppeals,
+ nodes: [
+ { ...result.data.createAppeal.appeal },
+ ...dataFromCache.regularAppeals.nodes,
+ ],
+ },
+ };
+ cache.writeQuery({ ...query, data });
+ }
+ },
});
enqueueSnackbar(t('Appeal successfully added!'), {
variant: 'success',
});
+ resetForm();
};
- const appealFormSchema = yup.object({
- name: yup.string().required('Please enter a name'),
- });
-
const contactTagsList = contactFilterTags?.accountList.contactTagList ?? [];
- const selectAllStatuses = (): void => {
- setFilterTags((prevState) => ({
- ...prevState,
- statuses: contactStatuses?.filter(
+ const handleSelectAllStatuses = (setFieldValue) => {
+ setFieldValue(
+ 'statuses',
+ contactStatuses?.filter(
(status: { value: string }) =>
- status.value !== 'ACTIVE' && status.value !== 'HIDDEN',
+ status.value !== 'ACTIVE' &&
+ status.value !== 'HIDDEN' &&
+ status.value !== 'NULL',
),
- }));
+ );
};
- const selectAllTags = (): void => {
- setFilterTags((prevState) => ({
- ...prevState,
- tags: contactTagsList,
- }));
+ const handleSelectAllTags = (setFieldValue) => {
+ setFieldValue('tags', contactTagsList);
};
- return loadingStatuses || loadingTags ? (
-
- ) : (
+ return (
@@ -215,13 +230,26 @@ const AddAppealForm = (): ReactElement => {
initialGoal: 0,
letterCost: 0,
adminCost: 12,
+ statuses: [],
+ tags: [],
+ exclusions: [],
+ }}
+ onSubmit={async (values, { resetForm }) => {
+ await onSubmit(values, resetForm);
}}
- onSubmit={onSubmit}
validationSchema={appealFormSchema}
>
{({
- values: { initialGoal, letterCost, adminCost },
+ values: {
+ initialGoal,
+ letterCost,
+ adminCost,
+ statuses,
+ tags,
+ exclusions,
+ },
handleSubmit,
+ setFieldValue,
isSubmitting,
isValid,
errors,
@@ -238,7 +266,6 @@ const AddAppealForm = (): ReactElement => {
name="name"
type="input"
variant="outlined"
- size="small"
className={classes.input}
as={TextField}
/>
@@ -246,7 +273,7 @@ const AddAppealForm = (): ReactElement => {
-
+
{
variant="outlined"
size="small"
className={classes.input}
+ error={errors.initialGoal}
+ helperText={errors.initialGoal}
as={TextField}
/>
-
+
{
-
+
{
label={t('Letter Cost')}
size="small"
className={classes.input}
+ error={errors.letterCost}
+ helperText={errors.letterCost}
as={TextField}
/>
-
+
{
-
+
{
label={t('Admin %')}
size="small"
className={classes.input}
+ error={errors.adminCost}
+ helperText={errors.adminCost}
as={TextField}
/>
-
+
{
-
+
{
-
-
- {t(
- 'You can add contacts to your appeal based on their status and/or tags. You can also add additional contacts individually at a later time.',
- )}
+
+ {t(
+ 'You can add contacts to your appeal based on their status and/or tags. You can also add additional contacts individually at a later time.',
+ )}
+
+
+
+ {t('Add contacts with the following status(es):')}
-
- {contactStatuses && (
-
-
- {t('Add contacts with the following status(es):')}
-
+ {!!contactStatuses && (
handleSelectAllStatuses(setFieldValue)}
>
{t('select all')}
+ )}
+
+ {loadingStatuses && }
+ {!!contactStatuses && !loadingStatuses && (
- !filterTags?.statuses?.some(
- ({ value: id2 }) => id2 === id1,
- ),
+ !statuses?.some(({ value: id2 }) => id2 === id1),
)}
getOptionLabel={(option) => option.name}
- value={filterTags.statuses}
+ value={statuses}
onChange={(_event, values) =>
- handleChange(values, 'statuses')
+ setFieldValue('statuses', values)
}
renderInput={(params) => (
{
/>
)}
/>
-
- )}
- {contactTagsList && contactTagsList.length > 0 && (
-
-
- {t('Add contacts with the following tag(s):')}
-
+ )}
+
+
+
+
+ {t('Add contacts with the following tag(s):')}
+
+ {!!contactTagsList.length && (
handleSelectAllTags(setFieldValue)}
>
{t('select all')}
+ )}
+
+ {loadingTags && }
+
+ {contactTagsList && !loadingTags && (
- !filterTags.tags.some((tag2) => tag2 === tag1),
+ (tag1) => !tags.some((tag2) => tag2 === tag1),
)}
getOptionLabel={(option) => option}
- value={filterTags.tags}
+ value={tags}
onChange={(_event, values) =>
- handleChange(values, 'tags')
+ setFieldValue('tags', values)
}
renderInput={(params) => (
{
/>
)}
/>
-
- )}
+ )}
+
-
- {t('Do not add contacts who:')}
-
+ {t('Do not add contacts who:')}
- !filterTags.exclusions.some(
- ({ value: id2 }) => id2 === id1,
- ),
+ !exclusions.some(({ value: id2 }) => id2 === id1),
)}
getOptionLabel={(option) => option.name}
- value={filterTags.exclusions}
+ value={exclusions}
onChange={(_event, values) =>
- handleChange(values, 'exclusions')
+ setFieldValue('exclusions', values)
}
renderInput={(params) => (
{
)}
/>
+
+ {[errors.statuses, errors.tags, errors.exclusions].map(
+ (error, idx) => {
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+ },
+ )}
+
{
diff --git a/src/components/Tool/Appeal/AppealProgressBar.tsx b/src/components/Tool/Appeal/AppealProgressBar.tsx
index 681efddc6..50b3d6b5c 100644
--- a/src/components/Tool/Appeal/AppealProgressBar.tsx
+++ b/src/components/Tool/Appeal/AppealProgressBar.tsx
@@ -1,6 +1,8 @@
-import React, { ReactElement } from 'react';
+import React, { ReactElement, useMemo } from 'react';
import { Box, Theme, Tooltip, Typography } from '@mui/material';
import { makeStyles } from 'tss-react/mui';
+import { useLocale } from 'src/hooks/useLocale';
+import { currencyFormat } from 'src/lib/intlFormat';
import theme from '../../../theme';
const useStyles = makeStyles()((theme: Theme) => ({
@@ -31,7 +33,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
export interface Props {
given: number;
received: number;
- commited: number;
+ committed: number;
amount: number;
amountCurrency: string;
}
@@ -39,11 +41,24 @@ export interface Props {
const AppealProgressBar = ({
given,
received,
- commited,
+ committed,
amount,
amountCurrency,
}: Props): ReactElement => {
const { classes } = useStyles();
+ const locale = useLocale();
+ const givenAmount = useMemo(
+ () => currencyFormat(given, amountCurrency, locale),
+ [given, amountCurrency, locale],
+ );
+ const receivedAmount = useMemo(
+ () => currencyFormat(received + given, amountCurrency, locale),
+ [given, received, amountCurrency, locale],
+ );
+ const committedAmount = useMemo(
+ () => currencyFormat(committed + received + given, amountCurrency, locale),
+ [given, received, committed, amountCurrency, locale],
+ );
return (
<>
@@ -54,8 +69,7 @@ const AppealProgressBar = ({
display="inline"
className={classes.colorYellow}
>
- {given} {amountCurrency} (
- {`${((given / (amount || 1)) * 100).toFixed(0)}%`})
+ {givenAmount} ({`${((given / (amount || 1)) * 100).toFixed(0)}%`})
- {received + given} {amountCurrency} (
+ {receivedAmount} (
{`${(((received + given) / (amount || 1)) * 100).toFixed(0)}%`})
@@ -86,16 +100,17 @@ const AppealProgressBar = ({
>
/
-
+
- {commited + received + given} {amountCurrency} (
- {`${(((commited + received + given) / (amount || 1)) * 100).toFixed(
- 0,
- )}%`}
+ {committedAmount} (
+ {`${(
+ ((committed + received + given) / (amount || 1)) *
+ 100
+ ).toFixed(0)}%`}
)
@@ -114,8 +129,8 @@ const AppealProgressBar = ({
backgroundColor: theme.palette.progressBarYellow.main,
borderTopLeftRadius: 8,
borderBottomLeftRadius: 8,
- borderTopRightRadius: !received && !commited ? 8 : 0,
- borderBottomRightRadius: !received && !commited ? 8 : 0,
+ borderTopRightRadius: !received && !committed ? 8 : 0,
+ borderBottomRightRadius: !received && !committed ? 8 : 0,
}}
/>
@@ -127,15 +142,15 @@ const AppealProgressBar = ({
backgroundColor: theme.palette.progressBarOrange.main,
borderTopLeftRadius: !given ? 8 : 0,
borderBottomLeftRadius: !given ? 8 : 0,
- borderTopRightRadius: !commited ? 8 : 0,
- borderBottomRightRadius: !commited ? 8 : 0,
+ borderTopRightRadius: !committed ? 8 : 0,
+ borderBottomRightRadius: !committed ? 8 : 0,
}}
/>
-
+
({
- margin: theme.spacing(0, 1, 0, 0),
-}));
-
-interface Props {
+interface AppealsProps {
accountListId: string;
}
-const Appeals: React.FC = ({ accountListId }: Props) => {
+const Appeals: React.FC = ({ accountListId }) => {
const { t } = useTranslation();
const { enqueueSnackbar } = useSnackbar();
const [changePrimaryAppeal, { loading: updating }] =
@@ -34,7 +29,7 @@ const Appeals: React.FC = ({ accountListId }: Props) => {
pageInfo: data?.regularAppeals.pageInfo,
});
- const changePrimary = async (newPrimaryId: string): Promise => {
+ const handleChangePrimary = async (newPrimaryId: string): Promise => {
await changePrimaryAppeal({
variables: {
accountListId,
@@ -52,6 +47,14 @@ const Appeals: React.FC = ({ accountListId }: Props) => {
});
};
+ const primaryAppeal = useMemo(() => {
+ if (data?.primaryAppeal) {
+ return data.primaryAppeal.nodes[0];
+ } else {
+ return null;
+ }
+ }, [data]);
+
return (
@@ -59,26 +62,12 @@ const Appeals: React.FC = ({ accountListId }: Props) => {
{loading || updating ? (
-
-
-
- ) : data?.primaryAppeal && data.primaryAppeal.nodes.length > 0 ? (
+
+ ) : primaryAppeal ? (
) : (
@@ -88,25 +77,18 @@ const Appeals: React.FC = ({ accountListId }: Props) => {
{loading || updating ? (
-
-
-
- ) : data?.regularAppeals && data.regularAppeals.nodes.length > 0 ? (
+ <>
+ {Array(5)
+ .fill('')
+ .map((_, idx) => (
+
+ ))}
+ >
+ ) : data?.regularAppeals && data.regularAppeals.nodes.length ? (
<>
{data.regularAppeals.nodes.map((appeal) => (
-
+
))}
>
@@ -115,7 +97,7 @@ const Appeals: React.FC = ({ accountListId }: Props) => {
)}
{!loading && (
-
+
Showing{' '}
{(data?.primaryAppeal ? data.primaryAppeal.nodes.length : 0) +
diff --git a/src/components/Tool/Appeal/CreateAppeal.graphql b/src/components/Tool/Appeal/CreateAppeal.graphql
index 6acfc1dc2..19d24942e 100644
--- a/src/components/Tool/Appeal/CreateAppeal.graphql
+++ b/src/components/Tool/Appeal/CreateAppeal.graphql
@@ -3,7 +3,7 @@ mutation CreateAppeal($accountListId: ID!, $attributes: AppealCreateInput!) {
input: { accountListId: $accountListId, attributes: $attributes }
) {
appeal {
- id
+ ...AppealFields
}
}
}
diff --git a/src/components/Tool/Appeal/NoAppeal.test.tsx b/src/components/Tool/Appeal/NoAppeal.test.tsx
index 4bacd0bc1..79484d01f 100644
--- a/src/components/Tool/Appeal/NoAppeal.test.tsx
+++ b/src/components/Tool/Appeal/NoAppeal.test.tsx
@@ -1,14 +1,18 @@
import React from 'react';
+import { ThemeProvider } from '@mui/material/styles';
import { render } from '@testing-library/react';
import TestWrapper from '__tests__/util/TestWrapper';
+import theme from 'src/theme';
import NoAppeals from './NoAppeals';
describe('NoAppeals', () => {
it('regular', () => {
const { queryByText } = render(
-
-
- ,
+
+
+
+
+ ,
);
expect(queryByText('No Appeals have been setup yet.')).toBeInTheDocument();
expect(
@@ -18,9 +22,11 @@ describe('NoAppeals', () => {
it('primary', () => {
const { queryByText } = render(
-
-
- ,
+
+
+
+
+ ,
);
expect(
queryByText('No Appeals have been setup yet.'),
diff --git a/src/components/Tool/Appeal/NoAppeals.tsx b/src/components/Tool/Appeal/NoAppeals.tsx
index cd9e9df57..591fdfd94 100644
--- a/src/components/Tool/Appeal/NoAppeals.tsx
+++ b/src/components/Tool/Appeal/NoAppeals.tsx
@@ -1,20 +1,28 @@
import React, { ReactElement } from 'react';
import { mdiTrophy } from '@mdi/js';
import Icon from '@mdi/react';
-import { Box, CardContent, Typography } from '@mui/material';
+import { Box, CardContent, Theme, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
+import { makeStyles } from 'tss-react/mui';
import AnimatedCard from '../../AnimatedCard';
export interface Props {
primary?: boolean;
}
+const useStyles = makeStyles()((theme: Theme) => ({
+ greyedOut: {
+ backgroundColor: theme.palette.cruGrayLight.main,
+ },
+}));
+
const NoAppeals = ({ primary }: Props): ReactElement => {
const { t } = useTranslation();
+ const { classes } = useStyles();
return (
-
+