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