From 986a820d460d58f690576c9221ddcd8b8dbde288 Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 13 Jan 2025 13:42:30 -0500 Subject: [PATCH 01/23] Take a first stab at household join --- graphql.schema.json | 237 ++++++ src/api/operations/access.fragments.graphql | 1 + src/api/operations/client.queries.graphql | 25 + .../operations/enrollment.fragments.graphql | 7 + src/api/operations/enrollment.queries.graphql | 9 +- .../operations/household.operations.graphql | 7 + src/components/elements/CommonDialog.tsx | 4 +- .../elements/StepDialog.stories.tsx | 25 + src/components/elements/StepDialog.tsx | 82 +++ .../elements/table/GenericTable.tsx | 55 +- src/modules/form/components/DynamicForm.tsx | 18 +- src/modules/form/hooks/useDynamicFields.tsx | 4 +- src/modules/form/hooks/useFormDialog.tsx | 4 + .../components/EditHouseholdMemberTable.tsx | 1 + .../elements/AddToHouseholdButton.tsx | 73 +- .../elements/ConflictingEnrollmentAlert.tsx | 102 +++ .../JoinHouseholdAddRelationships.tsx | 85 +++ .../elements/JoinHouseholdDialog.tsx | 151 ++++ .../elements/JoinHouseholdReviewJoin.tsx | 84 +++ .../elements/JoinHouseholdSelectClients.tsx | 113 +++ .../hooks/useAddToHouseholdColumns.tsx | 3 +- .../tables/ProjectHouseholdsTable.tsx | 6 +- src/types/gqlObjects.ts | 74 ++ src/types/gqlTypes.ts | 691 ++++++++++++++++-- 24 files changed, 1756 insertions(+), 105 deletions(-) create mode 100644 src/api/operations/household.operations.graphql create mode 100644 src/components/elements/StepDialog.stories.tsx create mode 100644 src/components/elements/StepDialog.tsx create mode 100644 src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx create mode 100644 src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx create mode 100644 src/modules/household/components/elements/JoinHouseholdDialog.tsx create mode 100644 src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx create mode 100644 src/modules/household/components/elements/JoinHouseholdSelectClients.tsx diff --git a/graphql.schema.json b/graphql.schema.json index ff8a303f0..bdc58a492 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -18273,6 +18273,26 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "project", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "projectType", "description": null, @@ -28017,6 +28037,178 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "JoinHouseholdsInput", + "description": "Autogenerated input type of JoinHouseholds", + "isOneOf": false, + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "joiningEnrollmentInputs", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "JoiningEnrollmentInput", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "receivingHouseholdId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "JoinHouseholdsPayload", + "description": "Autogenerated return type of JoinHouseholds.", + "isOneOf": null, + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "household", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Household", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "JoiningEnrollmentInput", + "description": null, + "isOneOf": false, + "fields": null, + "inputFields": [ + { + "name": "enrollmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relationshipToHoh", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "RelationshipToHoH", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "JsonObject", @@ -30465,6 +30657,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "joinHouseholds", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for JoinHouseholds", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "JoinHouseholdsInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "JoinHouseholdsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "mergeClients", "description": null, @@ -36095,6 +36316,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "canSplitHouseholds", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "canViewDob", "description": null, diff --git a/src/api/operations/access.fragments.graphql b/src/api/operations/access.fragments.graphql index 62f05685c..e29ceef6a 100644 --- a/src/api/operations/access.fragments.graphql +++ b/src/api/operations/access.fragments.graphql @@ -98,6 +98,7 @@ fragment ProjectAccessFields on ProjectAccess { canManageIncomingReferrals canManageOutgoingReferrals canManageExternalFormSubmissions + canSplitHouseholds } fragment OrganizationAccessFields on OrganizationAccess { diff --git a/src/api/operations/client.queries.graphql b/src/api/operations/client.queries.graphql index be7f79ef8..9ec5a18b7 100644 --- a/src/api/operations/client.queries.graphql +++ b/src/api/operations/client.queries.graphql @@ -74,6 +74,31 @@ query GetClientEnrollments( } } +query GetClientEnrollmentWithHousehold( + $id: ID! + $limit: Int = 1 + $offset: Int = 0 + $filters: EnrollmentsForClientFilterOptions +) { + client(id: $id) { + id + enrollments( + limit: $limit + offset: $offset + sortOrder: MOST_RECENT + filters: $filters + includeEnrollmentsWithLimitedAccess: true + ) { + offset + limit + nodesCount + nodes { + ...EnrollmentWithHouseholdFields + } + } + } +} + query GetClientServices( $id: ID! $limit: Int = 10 diff --git a/src/api/operations/enrollment.fragments.graphql b/src/api/operations/enrollment.fragments.graphql index 6085288a6..a047e2dff 100644 --- a/src/api/operations/enrollment.fragments.graphql +++ b/src/api/operations/enrollment.fragments.graphql @@ -270,3 +270,10 @@ fragment EnrollmentRangeFields on Enrollment { exitDate inProgress } + +fragment EnrollmentWithHouseholdFields on Enrollment { + ...EnrollmentFields + household { + ...HouseholdFields + } +} diff --git a/src/api/operations/enrollment.queries.graphql b/src/api/operations/enrollment.queries.graphql index d13817050..149c6537f 100644 --- a/src/api/operations/enrollment.queries.graphql +++ b/src/api/operations/enrollment.queries.graphql @@ -15,14 +15,7 @@ query GetEnrollmentDetails($id: ID!) { query GetEnrollmentWithHousehold($id: ID!) { enrollment(id: $id) { - ...EnrollmentFields - household { - id - shortId - householdClients { - ...HouseholdClientFields - } - } + ...EnrollmentWithHouseholdFields } } diff --git a/src/api/operations/household.operations.graphql b/src/api/operations/household.operations.graphql new file mode 100644 index 000000000..fd1cb69b7 --- /dev/null +++ b/src/api/operations/household.operations.graphql @@ -0,0 +1,7 @@ +mutation JoinHouseholds($input: JoinHouseholdsInput!) { + joinHouseholds(input: $input) { + household { + ...HouseholdFields + } + } +} diff --git a/src/components/elements/CommonDialog.tsx b/src/components/elements/CommonDialog.tsx index 8480202eb..165cce72c 100644 --- a/src/components/elements/CommonDialog.tsx +++ b/src/components/elements/CommonDialog.tsx @@ -4,11 +4,11 @@ import { useCallback } from 'react'; import SentryErrorBoundary from '@/modules/errors/components/SentryErrorBoundary'; -interface Props extends DialogProps { +export interface CommonDialogProps extends DialogProps { enableBackdropClick?: boolean; } -const CommonDialog: React.FC = ({ +const CommonDialog: React.FC = ({ children, onClose, enableBackdropClick = false, diff --git a/src/components/elements/StepDialog.stories.tsx b/src/components/elements/StepDialog.stories.tsx new file mode 100644 index 000000000..f56361d76 --- /dev/null +++ b/src/components/elements/StepDialog.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from '@storybook/react'; +import StepDialog from './StepDialog'; + +export default { + component: StepDialog, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + open: true, + title: 'Stepper Dialog Demo', + tabDefinitions: [ + { + title: 'One', + content: 'hello first tab', + }, + { + title: 'Two', + content: 'this is the second tab', + }, + ], + }, +}; diff --git a/src/components/elements/StepDialog.tsx b/src/components/elements/StepDialog.tsx new file mode 100644 index 000000000..c403b9de5 --- /dev/null +++ b/src/components/elements/StepDialog.tsx @@ -0,0 +1,82 @@ +import { TabContext, TabList, TabPanel } from '@mui/lab'; +import { + Box, + DialogActions, + DialogContent, + DialogTitle, + Tab, +} from '@mui/material'; +import { ReactNode, SyntheticEvent, useCallback, useState } from 'react'; +import CommonDialog, { + CommonDialogProps, +} from '@/components/elements/CommonDialog'; +import FormDialogActionContent from '@/modules/form/components/FormDialogActionContent'; + +type TabDefinition = { + title: string; + content: ReactNode; +}; + +interface Props extends Omit { + title: string; + tabDefinitions: TabDefinition[]; + onSubmit: VoidFunction; + onClose: VoidFunction; +} + +const StepDialog = ({ + title, + tabDefinitions, + onSubmit, + onClose, + ...rest +}: Props) => { + const [tabValue, setTabValue] = useState(tabDefinitions[0].title); + + const handleChange = (event: SyntheticEvent, newValue: string) => { + setTabValue(newValue); + }; + + const handleSubmit = useCallback(() => { + const currentTabIndex = tabDefinitions.findIndex( + (td: TabDefinition) => td.title === tabValue + ); + if (currentTabIndex === tabDefinitions.length - 1) { + onSubmit?.(); + } else { + const nextTabValue = tabDefinitions[currentTabIndex + 1].title; + setTabValue(nextTabValue); + } + }, [onSubmit, tabDefinitions, tabValue]); + + return ( + + {title} + + + + + {/* todo @martha aria label*/} + {tabDefinitions.map((tab: TabDefinition) => ( + + ))} + + + {tabDefinitions.map((tab: TabDefinition) => ( + {tab.content} + ))} + + + + + + + ); +}; + +export default StepDialog; diff --git a/src/components/elements/table/GenericTable.tsx b/src/components/elements/table/GenericTable.tsx index 93fb6d783..d049f2dd6 100644 --- a/src/components/elements/table/GenericTable.tsx +++ b/src/components/elements/table/GenericTable.tsx @@ -60,6 +60,7 @@ export interface Props { rowSx?: (row: T) => SxProps; headerCellSx?: (def: ColumnDef) => SxProps; selectable?: 'row' | 'checkbox'; // selectable by clicking row or by clicking checkbox + selected?: readonly string[]; // can optionally be used as a controlled component isRowSelectable?: (row: T) => boolean; onChangeSelectedRowIds?: (ids: readonly string[]) => void; EnhancedTableToolbarProps?: Omit< @@ -133,6 +134,7 @@ const GenericTable = ({ headerCellSx, selectable, isRowSelectable, + selected: selectedProp, onChangeSelectedRowIds, EnhancedTableToolbarProps, filterToolbar, @@ -150,6 +152,11 @@ const GenericTable = ({ // initially undefined so we can early return and avoid state flicker const [selected, setSelected] = useState(); + const isSelectControlled = selectedProp !== undefined; + const selectedValue = useMemo( + () => (isSelectControlled ? selectedProp : selected || []), + [isSelectControlled, selected, selectedProp] + ); const selectableRowIds = useMemo(() => { if (!selectable) return []; @@ -160,30 +167,40 @@ const GenericTable = ({ const handleSelectAllClick = useCallback( (event: React.ChangeEvent) => { if (event.target.checked) { - setSelected(selectableRowIds); + if (!isSelectControlled) { + setSelected(selectableRowIds); + } + + onChangeSelectedRowIds?.(selectableRowIds); } else { - setSelected([]); + if (!isSelectControlled) { + setSelected([]); + } + + onChangeSelectedRowIds?.([]); } }, - [selectableRowIds] + [isSelectControlled, onChangeSelectedRowIds, selectableRowIds] ); const handleSelectRow = useCallback( - (row: T) => - setSelected((old) => { - if (!old) return undefined; - return old.includes(row.id) ? without(old, row.id) : [...old, row.id]; - }), - [] + (row: T) => { + const newValue = selectedValue.includes(row.id) + ? without(selectedValue, row.id) + : [...selectedValue, row.id]; + + if (!isSelectControlled) { + setSelected(newValue); + } + + onChangeSelectedRowIds?.(newValue); + }, + [isSelectControlled, onChangeSelectedRowIds, selectedValue] ); // Clear selection when data changes useEffect(() => setSelected([]), [rows]); - useEffect(() => { - if (selected) onChangeSelectedRowIds?.(selected); - }, [selected, onChangeSelectedRowIds]); - // avoid state flicker due to state reset if (!selected) return ; @@ -221,12 +238,12 @@ const GenericTable = ({ 0 && - selected.length < selectableRowIds.length + selectedValue.length > 0 && + selectedValue.length < selectableRowIds.length } checked={ selectableRowIds.length > 0 && - selected.length === selectableRowIds.length + selectedValue.length === selectableRowIds.length } disabled={selectableRowIds.length === 0} onChange={handleSelectAllClick} @@ -286,7 +303,7 @@ const GenericTable = ({ {EnhancedTableToolbarProps && ( )} @@ -353,7 +370,7 @@ const GenericTable = ({ onClickHandler ? onClickHandler(row) : undefined } selected={ - selectable === 'row' && includes(selected, row.id) + selectable === 'row' && includes(selectedValue, row.id) } onKeyUp={ !!handleRowClick @@ -368,7 +385,7 @@ const GenericTable = ({ ; variant?: 'standard' | 'without_top_level_cards'; + onFieldChange?: (linkId?: string, value?: any) => void; } export interface DynamicFormRef { SaveIfDirty: VoidFunction; @@ -110,6 +111,7 @@ const DynamicForm = forwardRef( errorRef, onDirty, variant = 'standard', + onFieldChange: onFieldChangeProp, }: DynamicFormProps, ref: Ref ) => { @@ -119,12 +121,16 @@ const DynamicForm = forwardRef( }, [dirty, onDirty]); const [promptSave, setPromptSave] = useState(); - const onFieldChange = useCallback((type: ChangeType) => { - if (type === ChangeType.User) { - setPromptSave(true); - setDirty(true); - } - }, []); + const onFieldChange = useCallback( + (type: ChangeType, linkId?: string, value?: any) => { + onFieldChangeProp?.(linkId, value); + if (type === ChangeType.User) { + setPromptSave(true); + setDirty(true); + } + }, + [onFieldChangeProp] + ); // getValues: returns form state (used by some nested components, like MciClearance) // getValuesForSubmit: returns submittable form state (used for onSubmit/onSaveDraft) diff --git a/src/modules/form/hooks/useDynamicFields.tsx b/src/modules/form/hooks/useDynamicFields.tsx index fcca54ba1..de7f9be76 100644 --- a/src/modules/form/hooks/useDynamicFields.tsx +++ b/src/modules/form/hooks/useDynamicFields.tsx @@ -41,7 +41,7 @@ const useDynamicFields = ({ initialValues?: Record; viewOnly?: boolean; localConstants?: LocalConstants; - onFieldChange?: (type: ChangeType) => void; + onFieldChange?: (type: ChangeType, linkId?: string, value?: any) => void; }) => { const [values, setValues] = useState( Object.assign({}, initialValues) @@ -158,8 +158,8 @@ const useDynamicFields = ({ const itemChanged: ItemChangedFn = useCallback( (input) => { - if (onFieldChange) onFieldChange(input.type); const { linkId, value } = input; + if (onFieldChange) onFieldChange(input.type, linkId, value); // todo @martha - incompatible with severalItemsChanged but maybe that is OK setValues((currentValues) => { const newValues = { ...currentValues }; newValues[linkId] = value; diff --git a/src/modules/form/hooks/useFormDialog.tsx b/src/modules/form/hooks/useFormDialog.tsx index 16ef3e02c..0e61f9cf9 100644 --- a/src/modules/form/hooks/useFormDialog.tsx +++ b/src/modules/form/hooks/useFormDialog.tsx @@ -48,6 +48,7 @@ interface Args extends Omit, 'formDefinition'> { onClose?: VoidFunction; localDefinition?: FormDefinitionFieldsFragment; projectId?: string; // Project context for fetching form definition + onFieldChange?: (linkId?: string, value?: any) => void; } export function useFormDialog({ onCompleted, @@ -59,6 +60,7 @@ export function useFormDialog({ localDefinition, pickListArgs, projectId, + onFieldChange, }: Args) { const errorRef = useRef(null); const [dialogOpen, setDialogOpen] = useState(false); @@ -187,6 +189,7 @@ export function useFormDialog({ hideSubmit {...props} errorRef={errorRef} + onFieldChange={onFieldChange} /> @@ -215,6 +218,7 @@ export function useFormDialog({ formRole, initialValues, localConstants, + onFieldChange, onSubmit, pickListArgs, submitLoading, diff --git a/src/modules/household/components/EditHouseholdMemberTable.tsx b/src/modules/household/components/EditHouseholdMemberTable.tsx index 71d16627b..873315e54 100644 --- a/src/modules/household/components/EditHouseholdMemberTable.tsx +++ b/src/modules/household/components/EditHouseholdMemberTable.tsx @@ -175,6 +175,7 @@ const EditHouseholdMemberTable = ({ }, }, { + // todo @martha - this is interesting, maybe consolidate? header: Relationship to HoH, width: '25%', key: 'relationship', diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index 9258cfd2b..ea1410c32 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -1,4 +1,4 @@ -import { Button } from '@mui/material'; +import { Button, Stack } from '@mui/material'; import { useEffect, useMemo, useState } from 'react'; import ButtonTooltipContainer from '@/components/elements/ButtonTooltipContainer'; @@ -7,12 +7,16 @@ import usePrevious from '@/hooks/usePrevious'; import ClientAlertStack from '@/modules/clientAlerts/components/ClientAlertStack'; import useClientAlerts from '@/modules/clientAlerts/hooks/useClientAlerts'; import { useFormDialog } from '@/modules/form/hooks/useFormDialog'; -import { clientBriefName } from '@/modules/hmis/hmisUtil'; +import { clientBriefName, formatDateForGql } from '@/modules/hmis/hmisUtil'; +import ConflictingEnrollmentAlert from '@/modules/household/components/elements/ConflictingEnrollmentAlert'; +import JoinHouseholdDialog from '@/modules/household/components/elements/JoinHouseholdDialog'; import { useProjectCocsCountFromCache } from '@/modules/projects/hooks/useProjectCocsCountFromCache'; import { - RecordFormRole, ClientWithAlertFieldsFragment, + HouseholdFieldsFragment, + RecordFormRole, SubmittedEnrollmentResultFieldsFragment, + useGetClientEnrollmentWithHouseholdQuery, } from '@/types/gqlTypes'; interface Props { @@ -21,6 +25,7 @@ interface Props { householdId?: string; // if omitted, a new household will be created projectId: string; onSuccess: (householdId: string) => void; + household?: HouseholdFieldsFragment; } const AddToHouseholdButton = ({ @@ -29,6 +34,7 @@ const AddToHouseholdButton = ({ householdId, onSuccess, projectId, + household, }: Props) => { const prevIsMember = usePrevious(isMember); const [added, setAdded] = useState(isMember); @@ -46,6 +52,10 @@ const AddToHouseholdButton = ({ if (added) text = 'Added'; const clientId = client.id; + // todo @martha - if this is initially selected, it doesn't load + // todo @martha - need to add some kind of indication in the UI that this is loading, maybe just the enroll button is disabled + const [entryDate, setEntryDate] = useState(new Date()); + const memoedArgs = useMemo( () => ({ formRole: RecordFormRole.Enrollment, @@ -56,6 +66,11 @@ const AddToHouseholdButton = ({ inputVariables: { projectId, clientId }, pickListArgs: { projectId, householdId }, localConstants: { householdId, projectCocCount: cocCount }, + onFieldChange: (linkId?: string, value?: any) => { + if (linkId === 'entry_date') { + setEntryDate(value); + } + }, }), [projectId, clientId, householdId, cocCount, onSuccess] ); @@ -65,6 +80,28 @@ const AddToHouseholdButton = ({ const { clientAlerts } = useClientAlerts({ client: client }); + // todo @martha - add error + const { data: { client: clientWithEnrollment } = {}, loading } = + useGetClientEnrollmentWithHouseholdQuery({ + variables: { + id: client.id, + filters: { + openOnDate: formatDateForGql(entryDate || new Date()), // entryDate will be non-null bc of skip + project: [projectId], + }, + }, + skip: !entryDate, + }); + console.log(loading); // todo @martha - improve the loading experience + + const openEnrollmentOnDate = useMemo(() => { + const nodes = clientWithEnrollment?.enrollments.nodes; + if (nodes && nodes.length > 0) return nodes[0]; + }, [clientWithEnrollment]); + // todo @martha - if it has a conflicting enrollment, disable the Enroll button + + const [joinHouseholdDialogOpen, setJoinHouseholdDialogOpen] = useState(false); + return ( <> Enroll {clientBriefName(client)}, submitButtonText: `Enroll`, - preFormComponent: clientAlerts.length > 0 && ( - + preFormComponent: ( + + {clientAlerts.length > 0 && ( + + )} + {/*todo @martha - test this more thoroughly with a past enrollment*/} + {/*todo @martha - what about enrolling someone for the 1st time (not in a household) but they already have a conflicting enrollment - should be no change to behavior*/} + {household && openEnrollmentOnDate && ( + { + // todo @martha - close the regular dialog + setJoinHouseholdDialogOpen(true); + }} + /> + )} + ), })} + {/*todo @martha - don't forget to put all household alerts in the new join dialog*/} + {household && openEnrollmentOnDate && ( + setJoinHouseholdDialogOpen(false)} // todo @martha - clear out rest of state on close + receivingHousehold={household} + /> + )} ); }; diff --git a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx new file mode 100644 index 000000000..5f3c244e3 --- /dev/null +++ b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx @@ -0,0 +1,102 @@ +import { + Alert, + AlertTitle, + Button, + List, + ListItem, + Stack, +} from '@mui/material'; + +import { useMemo } from 'react'; +import { generatePath } from 'react-router-dom'; +import ButtonLink from '@/components/elements/ButtonLink'; +import { clientBriefName } from '@/modules/hmis/hmisUtil'; +import { EnrollmentDashboardRoutes } from '@/routes/routes'; +import { + ClientWithAlertFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, +} from '@/types/gqlTypes'; + +function stringifyArray(arr: string[]) { + if (arr.length === 1) return arr[0]; + const firsts = arr.slice(0, arr.length - 1); + const last = arr[arr.length - 1]; + return firsts.join(', ') + ' and ' + last; // no oxford comma in case of 2-item lists :( +} + +interface Props { + joiningClient: ClientWithAlertFieldsFragment; + receivingHousehold: HouseholdFieldsFragment; + conflictingEnrollmentId: string; + onClickJoinEnrollment: VoidFunction; +} + +const ConflictingEnrollmentAlert = ({ + joiningClient, + receivingHousehold, + conflictingEnrollmentId, + onClickJoinEnrollment, +}: Props) => { + const joiningClientName = clientBriefName(joiningClient); + // todo @martha - maybe factor this out into its own reusable hook? + const hohName = useMemo(() => { + const hoh = receivingHousehold.householdClients.find( + (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold + ); + if (!hoh) return ''; + return clientBriefName(hoh.client); + }, [receivingHousehold.householdClients]); + + const additionalMemberNames = useMemo(() => { + return receivingHousehold.householdClients + .filter( + (hc) => hc.relationshipToHoH !== RelationshipToHoH.SelfHeadOfHousehold + ) + .map((hc) => clientBriefName(hc.client)); + }, [receivingHousehold.householdClients]); + + return ( + + Conflicting Enrollment + {joiningClientName} has another enrollment in this project that conflicts + with the entry date. You have two options: + + + {joiningClientName}’s enrollment can be joined with {hohName}’s + household.{' '} + {additionalMemberNames.length > 0 && ( + <> + There {additionalMemberNames.length === 1 ? 'is' : 'are'} also{' '} + {additionalMemberNames.length} other household member + {additionalMemberNames.length > 1 && 's'} associated:{' '} + {stringifyArray(additionalMemberNames)}. + + )} + + + To retain the conflicting enrollment, you must first exit{' '} + {joiningClientName}’s conflicting enrollment, before re-enrolling. + + + + {/*todo @martha - what does this button do*/} + + + View Conflicting Enrollment + + + + ); +}; + +export default ConflictingEnrollmentAlert; diff --git a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx new file mode 100644 index 000000000..c69372d1c --- /dev/null +++ b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx @@ -0,0 +1,85 @@ +import { Box, Paper, Stack, Typography } from '@mui/material'; +import GenericTable from '@/components/elements/table/GenericTable'; +import { clientBriefName } from '@/modules/hmis/hmisUtil'; +import RelationshipToHohSelect from '@/modules/household/components/elements/RelationshipToHohSelect'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; +import { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, +} from '@/types/gqlTypes'; + +interface Props { + joiningClients: HouseholdClientFieldsFragment[]; + relationships: Record; + updateRelationship: ( + householdClientId: string, + relationship: RelationshipToHoH | null + ) => void; + receivingHousehold: HouseholdFieldsFragment; +} + +const JoinHouseholdAddRelationships = ({ + joiningClients, + relationships, + updateRelationship, + receivingHousehold, +}: Props) => { + // todo @martha - updated entry dates + + return ( + + + Step 2 + Add Relationships + + + Update joining clients' relationship to [HoH] + {/* todo @martha - new HoH*/} + + + + rows={[...receivingHousehold.householdClients, ...joiningClients]} + // todo @martha - remove exit date? (the enrollment is, probably, unexited? or it could be exited...) + columns={[ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, + { + header: 'Relationship', + key: 'relationship', + render: (hc: HouseholdClientFieldsFragment) => { + if (joiningClients.includes(hc)) { + return ( + { + updateRelationship(hc.id, selected?.value || null); + }} + textInputProps={{ + highlight: true, + placeholder: 'Select Relationship', + inputProps: { + 'aria-label': `Relationship to HoH for ${clientBriefName( + hc.client + )}`, + }, + }} + /> + ); + } else { + return hc.relationshipToHoH; // todo @martha - in an enum + } + }, + }, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, + WITH_ENROLLMENT_COLUMNS.enrollmentStatus, + ]} + /> + + + ); +}; + +export default JoinHouseholdAddRelationships; diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx new file mode 100644 index 000000000..3e0bb7323 --- /dev/null +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -0,0 +1,151 @@ +import { Alert, AlertTitle, Button, Stack } from '@mui/material'; +import { useCallback, useMemo, useState } from 'react'; +import StepDialog from '@/components/elements/StepDialog'; +import { clientBriefName } from '@/modules/hmis/hmisUtil'; +import JoinHouseholdAddRelationships from '@/modules/household/components/elements/JoinHouseholdAddRelationships'; +import JoinHouseholdReviewJoin from '@/modules/household/components/elements/JoinHouseholdReviewJoin'; +import JoinHouseholdSelectClients from '@/modules/household/components/elements/JoinHouseholdSelectClients'; +import { + EnrollmentWithHouseholdFieldsFragment, + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, + useJoinHouseholdsMutation, +} from '@/types/gqlTypes'; + +interface Props { + open: boolean; + conflictingEnrollment: EnrollmentWithHouseholdFieldsFragment; + onClose: VoidFunction; + receivingHousehold: HouseholdFieldsFragment; +} + +const JoinHouseholdDialog = ({ + open, + onClose, + conflictingEnrollment, + receivingHousehold, +}: Props) => { + const initiator = useMemo(() => { + // todo @martha - cast is safe, there will always be an initiator + // but add comments about why doing this - need to get the HouseholdClient object + return conflictingEnrollment.household.householdClients.find( + (hc) => hc.client.id === conflictingEnrollment.client.id + ) as HouseholdClientFieldsFragment; + }, [ + conflictingEnrollment.client.id, + conflictingEnrollment.household.householdClients, + ]); + + const initiatorClientName = clientBriefName(initiator.client); + + const [joiningClients, setJoiningClients] = useState< + HouseholdClientFieldsFragment[] + >([initiator]); + + // todo @martha = |nul here allows to set the dropdown back to null, but we want to disable the action (going to the next step) unless all are fileld out + const [relationships, setRelationships] = useState< + Record + >({}); + + // on success - move this + const [joinedHousehold, setJoinedHousehold] = + useState(null); + const [joinHousehold, { loading: joinLoading, error: joinError }] = + useJoinHouseholdsMutation({ + onCompleted: (data) => { + if (data.joinHouseholds?.household) { + setJoinedHousehold(receivingHousehold); + } + }, + }); + // todo @martha - deal with errors and loading + console.log(joinLoading); + console.log(joinError); + + const onSubmit = useCallback(() => { + joinHousehold({ + variables: { + input: { + receivingHouseholdId: receivingHousehold.id, + joiningEnrollmentInputs: joiningClients.map((hc) => { + return { + enrollmentId: hc.enrollment.id, + relationshipToHoh: relationships[hc.id], + }; + }), + }, + }, + }); + }, [joinHousehold, receivingHousehold.id, joiningClients, relationships]); + + return ( + + ), + }, + { + title: 'Add Relationships', + content: ( + { + setRelationships((prev) => { + return { + ...prev, + [householdClientId]: relationship, + }; + }); + }} + receivingHousehold={receivingHousehold} + /> + ), + }, + { + title: 'Review Join', + content: joinedHousehold ? ( + <> + + + Successful join + [Client name] and [x] other members enrollment[s] have have + been successfully joined to [hoh name]’s Enrollment at + [project name] + + + + + + + ) : ( + + ), + }, + ]} + /> + ); +}; + +export default JoinHouseholdDialog; diff --git a/src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx b/src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx new file mode 100644 index 000000000..caac1c4c2 --- /dev/null +++ b/src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx @@ -0,0 +1,84 @@ +import { Box, Paper, Stack, Typography } from '@mui/material'; +import { useMemo } from 'react'; +import GenericTable from '@/components/elements/table/GenericTable'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; +import { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, +} from '@/types/gqlTypes'; + +interface Props { + joiningClients: HouseholdClientFieldsFragment[]; + donorHousehold: HouseholdFieldsFragment; + receivingHousehold: HouseholdFieldsFragment; + relationships: Record; +} + +const JoinHouseholdReviewJoin = ({ + joiningClients, + donorHousehold, + receivingHousehold, + relationships, +}: Props) => { + const joinedHouseholdClients = useMemo(() => { + return [...receivingHousehold.householdClients, ...joiningClients]; + }, [joiningClients, receivingHousehold.householdClients]); + + const remainingHouseholdClients = useMemo(() => { + return donorHousehold.householdClients.filter( + (hc) => !joiningClients.includes(hc) + ); + }, [donorHousehold.householdClients, joiningClients]); + + return ( + + + Step 3 + Review Join + + + Check that the joined and remaining household members and details are + correct + + + Joined Household + + + rows={joinedHouseholdClients} + columns={[ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, + { + header: 'Relationship', + key: 'relationship', + // todo @martha - factor definition somewhere common + render: (hc: HouseholdClientFieldsFragment) => + relationships[hc.id] || hc.relationshipToHoH, + }, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, + WITH_ENROLLMENT_COLUMNS.enrollmentStatus, + ]} + /> + + Other Household + + + rows={remainingHouseholdClients} + columns={[ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, + // todo @martha - consistent cols + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, + WITH_ENROLLMENT_COLUMNS.enrollmentStatus, + ]} + /> + + + ); +}; + +export default JoinHouseholdReviewJoin; diff --git a/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx b/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx new file mode 100644 index 000000000..d9f616e19 --- /dev/null +++ b/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx @@ -0,0 +1,113 @@ +import { + Alert, + AlertTitle, + Box, + Paper, + Stack, + Typography, +} from '@mui/material'; + +import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; +import { generatePath } from 'react-router-dom'; +import ButtonLink from '@/components/elements/ButtonLink'; +import GenericTable from '@/components/elements/table/GenericTable'; +import { PROJECT_HOUSEHOLD_COLUMNS } from '@/modules/projects/components/tables/ProjectHouseholdsTable'; +import { EnrollmentDashboardRoutes } from '@/routes/routes'; +import { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, +} from '@/types/gqlTypes'; + +interface Props { + donorHousehold: HouseholdFieldsFragment; + selectedClients: HouseholdClientFieldsFragment[]; + setSelectedClients: Dispatch>; + // todo @martha - need receivingHouseholdHohName +} + +const JoinHouseholdSelectClients = ({ + donorHousehold, + selectedClients, + setSelectedClients, +}: Props) => { + const selectedClientIds = useMemo( + () => selectedClients.map((hc) => hc.id), + [selectedClients] + ); + + // todo @martha - add some comments here + const hoh = useMemo( + () => + donorHousehold.householdClients.find( + (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold + ) as HouseholdClientFieldsFragment, + [donorHousehold.householdClients] + ); + + const setSelectedClientIds = useCallback( + (clientIds: readonly string[]) => { + if (hoh && clientIds.includes(hoh.id)) { + setSelectedClients(donorHousehold.householdClients); + } else { + setSelectedClients( + donorHousehold.householdClients.filter((hc) => + clientIds.includes(hc.id) + ) + ); + } + }, + [donorHousehold.householdClients, hoh, setSelectedClients] + ); + + return ( + + + Step 1 + Select Clients + {/* todo @martha - should it really be an h3?*/} + + + Select which clients you would like to join to [HoH]’s enrollment. + + {/*todo @martha - this (above) refers to the NEW hoh*/} + {hoh && + selectedClientIds.includes(hoh.id) && + donorHousehold.householdSize > 1 && ( + + Edit Household + + } + > + Head of Household Selected + All members must accompany the Head of Household. Select a different + Head of Household if this is not your intention. + + )} + {/* todo @martha - could be nice to disable unselection on the other members, and add a tooltip*/} + + {/*todo @martha - put these in the expected order*/} + + rows={donorHousehold.householdClients} + // todo @martha - remove exit date? (the enrollment is, probably, unexited? or it could be exited...) + columns={PROJECT_HOUSEHOLD_COLUMNS} + selectable={'checkbox'} + selected={selectedClientIds} + onChangeSelectedRowIds={setSelectedClientIds} + /> + + + ); +}; + +export default JoinHouseholdSelectClients; diff --git a/src/modules/household/hooks/useAddToHouseholdColumns.tsx b/src/modules/household/hooks/useAddToHouseholdColumns.tsx index 4d5445686..353d5cf15 100644 --- a/src/modules/household/hooks/useAddToHouseholdColumns.tsx +++ b/src/modules/household/hooks/useAddToHouseholdColumns.tsx @@ -77,12 +77,13 @@ export default function useAddToHouseholdColumns({ projectId={projectId} isMember={currentMembersMap.has(client.id)} onSuccess={onSuccess} + household={data?.household || undefined} /> ); }, }, ]; - }, [currentMembersMap, householdId, projectId, onSuccess]); + }, [householdId, projectId, currentMembersMap, onSuccess, data?.household]); if (error) throw error; diff --git a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx index 892ea54d6..59ef68e3d 100644 --- a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx @@ -38,7 +38,7 @@ export type HouseholdFields = NonNullable< >['households']['nodes'][number]; type OneHouseholdClient = HouseholdFields['householdClients'][number]; -const BASE_COLUMNS: ColumnDef[] = [ +export const PROJECT_HOUSEHOLD_COLUMNS: ColumnDef[] = [ CLIENT_COLUMNS.name, CLIENT_COLUMNS.age, { @@ -91,7 +91,7 @@ const ProjectHouseholdsClientRow: React.FC = ({ return ( - {BASE_COLUMNS.map((col, i) => ( + {PROJECT_HOUSEHOLD_COLUMNS.map((col, i) => ( {renderCellContents(householdClient, col.render)} @@ -147,7 +147,7 @@ const ProjectHouseholdsTable = ({ // dummy column defs for Household that are only used for the headers, not for rendering cells const defaultColumns: ColumnDef[] = useMemo(() => { return [ - ...BASE_COLUMNS, + ...PROJECT_HOUSEHOLD_COLUMNS, ...(staffAssignmentsEnabled ? [ASSIGNED_STAFF_COL] : []), ACTION_COL, ].map(({ header, key, optional, defaultHidden }) => ({ diff --git a/src/types/gqlObjects.ts b/src/types/gqlObjects.ts index b9950c79e..9ca1a7e6c 100644 --- a/src/types/gqlObjects.ts +++ b/src/types/gqlObjects.ts @@ -4892,6 +4892,14 @@ export const HmisObjectSchemas: GqlSchema[] = [ ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, + { + name: 'canSplitHouseholds', + type: { + kind: 'NON_NULL', + name: null, + ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, + }, + }, { name: 'canViewDob', type: { @@ -7074,6 +7082,18 @@ export const HmisInputObjectSchemas: GqlInputObjectSchema[] = [ name: 'openOnDate', type: { kind: 'SCALAR', name: 'ISO8601Date', ofType: null }, }, + { + name: 'project', + type: { + kind: 'LIST', + name: null, + ofType: { + kind: 'NON_NULL', + name: null, + ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, + }, + }, + }, { name: 'projectType', type: { @@ -7393,6 +7413,60 @@ export const HmisInputObjectSchemas: GqlInputObjectSchema[] = [ }, ], }, + { + name: 'JoinHouseholdsInput', + args: [ + { + name: 'joiningEnrollmentInputs', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'LIST', + name: null, + ofType: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'INPUT_OBJECT', + name: 'JoiningEnrollmentInput', + ofType: null, + }, + }, + }, + }, + }, + { + name: 'receivingHouseholdId', + type: { + kind: 'NON_NULL', + name: null, + ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, + }, + }, + ], + }, + { + name: 'JoiningEnrollmentInput', + args: [ + { + name: 'enrollmentId', + type: { + kind: 'NON_NULL', + name: null, + ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, + }, + }, + { + name: 'relationshipToHoh', + type: { + kind: 'NON_NULL', + name: null, + ofType: { kind: 'ENUM', name: 'RelationshipToHoH', ofType: null }, + }, + }, + ], + }, { name: 'MciClearanceInput', args: [ diff --git a/src/types/gqlTypes.ts b/src/types/gqlTypes.ts index f82947e40..e215947a0 100644 --- a/src/types/gqlTypes.ts +++ b/src/types/gqlTypes.ts @@ -2624,6 +2624,7 @@ export type EnrollmentSummary = { export type EnrollmentsForClientFilterOptions = { householdTasks?: InputMaybe>; openOnDate?: InputMaybe; + project?: InputMaybe>; projectType?: InputMaybe>; status?: InputMaybe>; }; @@ -3934,6 +3935,28 @@ export enum ItemType { TimeOfDay = 'TIME_OF_DAY', } +/** Autogenerated input type of JoinHouseholds */ +export type JoinHouseholdsInput = { + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: InputMaybe; + joiningEnrollmentInputs: Array; + receivingHouseholdId: Scalars['ID']['input']; +}; + +/** Autogenerated return type of JoinHouseholds. */ +export type JoinHouseholdsPayload = { + __typename?: 'JoinHouseholdsPayload'; + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: Maybe; + errors: Array; + household: Household; +}; + +export type JoiningEnrollmentInput = { + enrollmentId: Scalars['ID']['input']; + relationshipToHoh: RelationshipToHoH; +}; + export type KeyValue = { __typename?: 'KeyValue'; key: Scalars['String']['output']; @@ -4194,6 +4217,7 @@ export type Mutation = { deleteService?: Maybe; deleteServiceType?: Maybe; deleteUnits?: Maybe; + joinHouseholds?: Maybe; mergeClients?: Maybe; publishFormDefinition?: Maybe; refreshExternalSubmissions?: Maybe; @@ -4396,6 +4420,10 @@ export type MutationDeleteUnitsArgs = { input: DeleteUnitsInput; }; +export type MutationJoinHouseholdsArgs = { + input: JoinHouseholdsInput; +}; + export type MutationMergeClientsArgs = { input: MergeClientsInput; }; @@ -5631,6 +5659,7 @@ export type ProjectAccess = { canManageIncomingReferrals: Scalars['Boolean']['output']; canManageOutgoingReferrals: Scalars['Boolean']['output']; canManageUnits: Scalars['Boolean']['output']; + canSplitHouseholds: Scalars['Boolean']['output']; canViewDob: Scalars['Boolean']['output']; canViewEnrollmentDetails: Scalars['Boolean']['output']; canViewFullSsn: Scalars['Boolean']['output']; @@ -8118,6 +8147,7 @@ export type ProjectAccessFieldsFragment = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; export type OrganizationAccessFieldsFragment = { @@ -16508,6 +16538,157 @@ export type GetClientEnrollmentsQuery = { } | null; }; +export type GetClientEnrollmentWithHouseholdQueryVariables = Exact<{ + id: Scalars['ID']['input']; + limit?: InputMaybe; + offset?: InputMaybe; + filters?: InputMaybe; +}>; + +export type GetClientEnrollmentWithHouseholdQuery = { + __typename?: 'Query'; + client?: { + __typename?: 'Client'; + id: string; + enrollments: { + __typename?: 'EnrollmentsPaginated'; + offset: number; + limit: number; + nodesCount: number; + nodes: Array<{ + __typename?: 'Enrollment'; + id: string; + lockVersion: number; + entryDate: string; + exitDate?: string | null; + exitDestination?: Destination | null; + autoExited: boolean; + inProgress: boolean; + relationshipToHoH: RelationshipToHoH; + enrollmentCoc?: string | null; + householdId: string; + householdShortId: string; + householdSize: number; + household: { + __typename?: 'Household'; + id: string; + householdSize: number; + shortId: string; + householdClients: Array<{ + __typename?: 'HouseholdClient'; + id: string; + relationshipToHoH: RelationshipToHoH; + client: { + __typename?: 'Client'; + id: string; + lockVersion: number; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + nameSuffix?: string | null; + dob?: string | null; + age?: number | null; + ssn?: string | null; + gender: Array; + race: Array; + veteranStatus: NoYesReasonsForMissingData; + access: { + __typename?: 'ClientAccess'; + id: string; + canViewFullSsn: boolean; + canViewPartialSsn: boolean; + canEditClient: boolean; + canDeleteClient: boolean; + canViewDob: boolean; + canViewClientName: boolean; + canEditEnrollments: boolean; + canDeleteEnrollments: boolean; + canViewEnrollmentDetails: boolean; + canDeleteAssessments: boolean; + canManageAnyClientFiles: boolean; + canManageOwnClientFiles: boolean; + canViewAnyConfidentialClientFiles: boolean; + canViewAnyNonconfidentialClientFiles: boolean; + canUploadClientFiles: boolean; + canViewAnyFiles: boolean; + canAuditClients: boolean; + canManageScanCards: boolean; + canMergeClients: boolean; + canViewClientAlerts: boolean; + canManageClientAlerts: boolean; + }; + externalIds: Array<{ + __typename?: 'ExternalIdentifier'; + id: string; + identifier?: string | null; + url?: string | null; + label: string; + type: ExternalIdentifierType; + }>; + alerts: Array<{ + __typename?: 'ClientAlert'; + id: string; + note: string; + expirationDate?: string | null; + createdAt: string; + priority: ClientAlertPriorityLevel; + createdBy?: { + __typename: 'ApplicationUser'; + id: string; + name: string; + firstName?: string | null; + lastName?: string | null; + email: string; + } | null; + }>; + }; + enrollment: { + __typename?: 'Enrollment'; + id: string; + lockVersion: number; + autoExited: boolean; + entryDate: string; + exitDate?: string | null; + inProgress: boolean; + currentUnit?: { + __typename?: 'Unit'; + id: string; + name: string; + } | null; + }; + }>; + }; + project: { + __typename?: 'Project'; + id: string; + projectName: string; + projectType?: ProjectType | null; + }; + client: { + __typename?: 'Client'; + dob?: string | null; + veteranStatus: NoYesReasonsForMissingData; + id: string; + lockVersion: number; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + nameSuffix?: string | null; + }; + access: { + __typename?: 'EnrollmentAccess'; + id: string; + canEditEnrollments: boolean; + canDeleteEnrollments: boolean; + canAuditEnrollments: boolean; + canViewEnrollmentLocationMap: boolean; + }; + currentUnit?: { __typename?: 'Unit'; id: string; name: string } | null; + }>; + }; + } | null; +}; + export type GetClientServicesQueryVariables = Exact<{ id: Scalars['ID']['input']; limit?: InputMaybe; @@ -21219,6 +21400,7 @@ export type AllEnrollmentDetailsFragment = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; projectCocs: { __typename?: 'ProjectCocsPaginated'; nodesCount: number }; }; @@ -21587,6 +21769,133 @@ export type EnrollmentRangeFieldsFragment = { inProgress: boolean; }; +export type EnrollmentWithHouseholdFieldsFragment = { + __typename?: 'Enrollment'; + id: string; + lockVersion: number; + entryDate: string; + exitDate?: string | null; + exitDestination?: Destination | null; + autoExited: boolean; + inProgress: boolean; + relationshipToHoH: RelationshipToHoH; + enrollmentCoc?: string | null; + householdId: string; + householdShortId: string; + householdSize: number; + household: { + __typename?: 'Household'; + id: string; + householdSize: number; + shortId: string; + householdClients: Array<{ + __typename?: 'HouseholdClient'; + id: string; + relationshipToHoH: RelationshipToHoH; + client: { + __typename?: 'Client'; + id: string; + lockVersion: number; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + nameSuffix?: string | null; + dob?: string | null; + age?: number | null; + ssn?: string | null; + gender: Array; + race: Array; + veteranStatus: NoYesReasonsForMissingData; + access: { + __typename?: 'ClientAccess'; + id: string; + canViewFullSsn: boolean; + canViewPartialSsn: boolean; + canEditClient: boolean; + canDeleteClient: boolean; + canViewDob: boolean; + canViewClientName: boolean; + canEditEnrollments: boolean; + canDeleteEnrollments: boolean; + canViewEnrollmentDetails: boolean; + canDeleteAssessments: boolean; + canManageAnyClientFiles: boolean; + canManageOwnClientFiles: boolean; + canViewAnyConfidentialClientFiles: boolean; + canViewAnyNonconfidentialClientFiles: boolean; + canUploadClientFiles: boolean; + canViewAnyFiles: boolean; + canAuditClients: boolean; + canManageScanCards: boolean; + canMergeClients: boolean; + canViewClientAlerts: boolean; + canManageClientAlerts: boolean; + }; + externalIds: Array<{ + __typename?: 'ExternalIdentifier'; + id: string; + identifier?: string | null; + url?: string | null; + label: string; + type: ExternalIdentifierType; + }>; + alerts: Array<{ + __typename?: 'ClientAlert'; + id: string; + note: string; + expirationDate?: string | null; + createdAt: string; + priority: ClientAlertPriorityLevel; + createdBy?: { + __typename: 'ApplicationUser'; + id: string; + name: string; + firstName?: string | null; + lastName?: string | null; + email: string; + } | null; + }>; + }; + enrollment: { + __typename?: 'Enrollment'; + id: string; + lockVersion: number; + autoExited: boolean; + entryDate: string; + exitDate?: string | null; + inProgress: boolean; + currentUnit?: { __typename?: 'Unit'; id: string; name: string } | null; + }; + }>; + }; + project: { + __typename?: 'Project'; + id: string; + projectName: string; + projectType?: ProjectType | null; + }; + client: { + __typename?: 'Client'; + dob?: string | null; + veteranStatus: NoYesReasonsForMissingData; + id: string; + lockVersion: number; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + nameSuffix?: string | null; + }; + access: { + __typename?: 'EnrollmentAccess'; + id: string; + canEditEnrollments: boolean; + canDeleteEnrollments: boolean; + canAuditEnrollments: boolean; + canViewEnrollmentLocationMap: boolean; + }; + currentUnit?: { __typename?: 'Unit'; id: string; name: string } | null; +}; + export type GetEnrollmentQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; @@ -22568,6 +22877,7 @@ export type GetEnrollmentDetailsQuery = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; projectCocs: { __typename?: 'ProjectCocsPaginated'; nodesCount: number }; }; @@ -22628,6 +22938,7 @@ export type GetEnrollmentWithHouseholdQuery = { household: { __typename?: 'Household'; id: string; + householdSize: number; shortId: string; householdClients: Array<{ __typename?: 'HouseholdClient'; @@ -31211,6 +31522,7 @@ export type SubmitFormMutation = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; user?: { __typename: 'ApplicationUser'; @@ -33455,6 +33767,106 @@ export type ProjectEnrollmentsHouseholdClientFieldsFragment = { }; }; +export type JoinHouseholdsMutationVariables = Exact<{ + input: JoinHouseholdsInput; +}>; + +export type JoinHouseholdsMutation = { + __typename?: 'Mutation'; + joinHouseholds?: { + __typename?: 'JoinHouseholdsPayload'; + household: { + __typename?: 'Household'; + id: string; + householdSize: number; + shortId: string; + householdClients: Array<{ + __typename?: 'HouseholdClient'; + id: string; + relationshipToHoH: RelationshipToHoH; + client: { + __typename?: 'Client'; + id: string; + lockVersion: number; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + nameSuffix?: string | null; + dob?: string | null; + age?: number | null; + ssn?: string | null; + gender: Array; + race: Array; + veteranStatus: NoYesReasonsForMissingData; + access: { + __typename?: 'ClientAccess'; + id: string; + canViewFullSsn: boolean; + canViewPartialSsn: boolean; + canEditClient: boolean; + canDeleteClient: boolean; + canViewDob: boolean; + canViewClientName: boolean; + canEditEnrollments: boolean; + canDeleteEnrollments: boolean; + canViewEnrollmentDetails: boolean; + canDeleteAssessments: boolean; + canManageAnyClientFiles: boolean; + canManageOwnClientFiles: boolean; + canViewAnyConfidentialClientFiles: boolean; + canViewAnyNonconfidentialClientFiles: boolean; + canUploadClientFiles: boolean; + canViewAnyFiles: boolean; + canAuditClients: boolean; + canManageScanCards: boolean; + canMergeClients: boolean; + canViewClientAlerts: boolean; + canManageClientAlerts: boolean; + }; + externalIds: Array<{ + __typename?: 'ExternalIdentifier'; + id: string; + identifier?: string | null; + url?: string | null; + label: string; + type: ExternalIdentifierType; + }>; + alerts: Array<{ + __typename?: 'ClientAlert'; + id: string; + note: string; + expirationDate?: string | null; + createdAt: string; + priority: ClientAlertPriorityLevel; + createdBy?: { + __typename: 'ApplicationUser'; + id: string; + name: string; + firstName?: string | null; + lastName?: string | null; + email: string; + } | null; + }>; + }; + enrollment: { + __typename?: 'Enrollment'; + id: string; + lockVersion: number; + autoExited: boolean; + entryDate: string; + exitDate?: string | null; + inProgress: boolean; + currentUnit?: { + __typename?: 'Unit'; + id: string; + name: string; + } | null; + }; + }>; + }; + } | null; +}; + export type GetHouseholdQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; @@ -34531,6 +34943,7 @@ export type ProjectAllFieldsFragment = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; user?: { __typename: 'ApplicationUser'; @@ -35448,6 +35861,7 @@ export type GetProjectQuery = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; user?: { __typename: 'ApplicationUser'; @@ -35633,6 +36047,7 @@ export type GetProjectPermissionsQuery = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; } | null; }; @@ -41344,6 +41759,7 @@ export const ProjectAccessFieldsFragmentDoc = gql` canManageIncomingReferrals canManageOutgoingReferrals canManageExternalFormSubmissions + canSplitHouseholds } `; export const AllEnrollmentDetailsFragmentDoc = gql` @@ -41410,6 +41826,65 @@ export const SubmittedEnrollmentResultFieldsFragmentDoc = gql` ${EnrollmentOccurrencePointFieldsFragmentDoc} ${CustomDataElementFieldsFragmentDoc} `; +export const HouseholdClientFieldsFragmentDoc = gql` + fragment HouseholdClientFields on HouseholdClient { + id + relationshipToHoH + client { + id + ...ClientName + ...ClientIdentificationFields + ...AssessedClientFields + access { + ...ClientAccessFields + } + externalIds { + ...ClientIdentifierFields + } + alerts { + ...ClientAlertFields + } + } + enrollment { + id + lockVersion + ...EnrollmentRangeFields + autoExited + currentUnit { + id + name + } + } + } + ${ClientNameFragmentDoc} + ${ClientIdentificationFieldsFragmentDoc} + ${AssessedClientFieldsFragmentDoc} + ${ClientAccessFieldsFragmentDoc} + ${ClientIdentifierFieldsFragmentDoc} + ${ClientAlertFieldsFragmentDoc} + ${EnrollmentRangeFieldsFragmentDoc} +`; +export const HouseholdFieldsFragmentDoc = gql` + fragment HouseholdFields on Household { + id + householdSize + shortId + householdClients { + ...HouseholdClientFields + } + } + ${HouseholdClientFieldsFragmentDoc} +`; +export const EnrollmentWithHouseholdFieldsFragmentDoc = gql` + fragment EnrollmentWithHouseholdFields on Enrollment { + ...EnrollmentFields + household { + ...HouseholdFields + } + } + ${EnrollmentFieldsFragmentDoc} + ${HouseholdFieldsFragmentDoc} +`; export const ExternalFormSubmissionSummaryFragmentDoc = gql` fragment ExternalFormSubmissionSummary on ExternalFormSubmission { id @@ -41496,55 +41971,6 @@ export const GeolocationFieldsWithMetadataFragmentDoc = gql` } ${UserFieldsFragmentDoc} `; -export const HouseholdClientFieldsFragmentDoc = gql` - fragment HouseholdClientFields on HouseholdClient { - id - relationshipToHoH - client { - id - ...ClientName - ...ClientIdentificationFields - ...AssessedClientFields - access { - ...ClientAccessFields - } - externalIds { - ...ClientIdentifierFields - } - alerts { - ...ClientAlertFields - } - } - enrollment { - id - lockVersion - ...EnrollmentRangeFields - autoExited - currentUnit { - id - name - } - } - } - ${ClientNameFragmentDoc} - ${ClientIdentificationFieldsFragmentDoc} - ${AssessedClientFieldsFragmentDoc} - ${ClientAccessFieldsFragmentDoc} - ${ClientIdentifierFieldsFragmentDoc} - ${ClientAlertFieldsFragmentDoc} - ${EnrollmentRangeFieldsFragmentDoc} -`; -export const HouseholdFieldsFragmentDoc = gql` - fragment HouseholdFields on Household { - id - householdSize - shortId - householdClients { - ...HouseholdClientFields - } - } - ${HouseholdClientFieldsFragmentDoc} -`; export const ProjectEnrollmentsHouseholdClientFieldsFragmentDoc = gql` fragment ProjectEnrollmentsHouseholdClientFields on HouseholdClient { id @@ -44347,6 +44773,108 @@ export type GetClientEnrollmentsQueryResult = Apollo.QueryResult< GetClientEnrollmentsQuery, GetClientEnrollmentsQueryVariables >; +export const GetClientEnrollmentWithHouseholdDocument = gql` + query GetClientEnrollmentWithHousehold( + $id: ID! + $limit: Int = 1 + $offset: Int = 0 + $filters: EnrollmentsForClientFilterOptions + ) { + client(id: $id) { + id + enrollments( + limit: $limit + offset: $offset + sortOrder: MOST_RECENT + filters: $filters + includeEnrollmentsWithLimitedAccess: true + ) { + offset + limit + nodesCount + nodes { + ...EnrollmentWithHouseholdFields + } + } + } + } + ${EnrollmentWithHouseholdFieldsFragmentDoc} +`; + +/** + * __useGetClientEnrollmentWithHouseholdQuery__ + * + * To run a query within a React component, call `useGetClientEnrollmentWithHouseholdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetClientEnrollmentWithHouseholdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetClientEnrollmentWithHouseholdQuery({ + * variables: { + * id: // value for 'id' + * limit: // value for 'limit' + * offset: // value for 'offset' + * filters: // value for 'filters' + * }, + * }); + */ +export function useGetClientEnrollmentWithHouseholdQuery( + baseOptions: Apollo.QueryHookOptions< + GetClientEnrollmentWithHouseholdQuery, + GetClientEnrollmentWithHouseholdQueryVariables + > & + ( + | { + variables: GetClientEnrollmentWithHouseholdQueryVariables; + skip?: boolean; + } + | { skip: boolean } + ) +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery< + GetClientEnrollmentWithHouseholdQuery, + GetClientEnrollmentWithHouseholdQueryVariables + >(GetClientEnrollmentWithHouseholdDocument, options); +} +export function useGetClientEnrollmentWithHouseholdLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + GetClientEnrollmentWithHouseholdQuery, + GetClientEnrollmentWithHouseholdQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery< + GetClientEnrollmentWithHouseholdQuery, + GetClientEnrollmentWithHouseholdQueryVariables + >(GetClientEnrollmentWithHouseholdDocument, options); +} +export function useGetClientEnrollmentWithHouseholdSuspenseQuery( + baseOptions?: Apollo.SuspenseQueryHookOptions< + GetClientEnrollmentWithHouseholdQuery, + GetClientEnrollmentWithHouseholdQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useSuspenseQuery< + GetClientEnrollmentWithHouseholdQuery, + GetClientEnrollmentWithHouseholdQueryVariables + >(GetClientEnrollmentWithHouseholdDocument, options); +} +export type GetClientEnrollmentWithHouseholdQueryHookResult = ReturnType< + typeof useGetClientEnrollmentWithHouseholdQuery +>; +export type GetClientEnrollmentWithHouseholdLazyQueryHookResult = ReturnType< + typeof useGetClientEnrollmentWithHouseholdLazyQuery +>; +export type GetClientEnrollmentWithHouseholdSuspenseQueryHookResult = + ReturnType; +export type GetClientEnrollmentWithHouseholdQueryResult = Apollo.QueryResult< + GetClientEnrollmentWithHouseholdQuery, + GetClientEnrollmentWithHouseholdQueryVariables +>; export const GetClientServicesDocument = gql` query GetClientServices( $id: ID! @@ -46882,18 +47410,10 @@ export type GetEnrollmentDetailsQueryResult = Apollo.QueryResult< export const GetEnrollmentWithHouseholdDocument = gql` query GetEnrollmentWithHousehold($id: ID!) { enrollment(id: $id) { - ...EnrollmentFields - household { - id - shortId - householdClients { - ...HouseholdClientFields - } - } + ...EnrollmentWithHouseholdFields } } - ${EnrollmentFieldsFragmentDoc} - ${HouseholdClientFieldsFragmentDoc} + ${EnrollmentWithHouseholdFieldsFragmentDoc} `; /** @@ -49572,6 +50092,59 @@ export type GetEnrollmentGeolocationsQueryResult = Apollo.QueryResult< GetEnrollmentGeolocationsQuery, GetEnrollmentGeolocationsQueryVariables >; +export const JoinHouseholdsDocument = gql` + mutation JoinHouseholds($input: JoinHouseholdsInput!) { + joinHouseholds(input: $input) { + household { + ...HouseholdFields + } + } + } + ${HouseholdFieldsFragmentDoc} +`; +export type JoinHouseholdsMutationFn = Apollo.MutationFunction< + JoinHouseholdsMutation, + JoinHouseholdsMutationVariables +>; + +/** + * __useJoinHouseholdsMutation__ + * + * To run a mutation, you first call `useJoinHouseholdsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useJoinHouseholdsMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [joinHouseholdsMutation, { data, loading, error }] = useJoinHouseholdsMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useJoinHouseholdsMutation( + baseOptions?: Apollo.MutationHookOptions< + JoinHouseholdsMutation, + JoinHouseholdsMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + JoinHouseholdsMutation, + JoinHouseholdsMutationVariables + >(JoinHouseholdsDocument, options); +} +export type JoinHouseholdsMutationHookResult = ReturnType< + typeof useJoinHouseholdsMutation +>; +export type JoinHouseholdsMutationResult = + Apollo.MutationResult; +export type JoinHouseholdsMutationOptions = Apollo.BaseMutationOptions< + JoinHouseholdsMutation, + JoinHouseholdsMutationVariables +>; export const GetHouseholdDocument = gql` query GetHousehold($id: ID!) { household(id: $id) { From 1d3bb68defb40478a3a7cceb46dce84e306dac7f Mon Sep 17 00:00:00 2001 From: martha Date: Wed, 15 Jan 2025 12:19:04 -0500 Subject: [PATCH 02/23] Determine on server side whether there is a conflicting enrollment --- src/api/operations/client.queries.graphql | 25 -- src/modules/errors/components/ErrorAlert.tsx | 11 +- src/modules/errors/util.ts | 3 + src/modules/form/components/DynamicForm.tsx | 28 +- src/modules/form/hooks/useDynamicFields.tsx | 4 +- src/modules/form/hooks/useFormDialog.tsx | 29 +- .../elements/AddToHouseholdButton.tsx | 99 ++++--- .../elements/ConflictingEnrollmentAlert.tsx | 12 +- src/types/gqlTypes.ts | 253 ------------------ 9 files changed, 111 insertions(+), 353 deletions(-) diff --git a/src/api/operations/client.queries.graphql b/src/api/operations/client.queries.graphql index 9ec5a18b7..be7f79ef8 100644 --- a/src/api/operations/client.queries.graphql +++ b/src/api/operations/client.queries.graphql @@ -74,31 +74,6 @@ query GetClientEnrollments( } } -query GetClientEnrollmentWithHousehold( - $id: ID! - $limit: Int = 1 - $offset: Int = 0 - $filters: EnrollmentsForClientFilterOptions -) { - client(id: $id) { - id - enrollments( - limit: $limit - offset: $offset - sortOrder: MOST_RECENT - filters: $filters - includeEnrollmentsWithLimitedAccess: true - ) { - offset - limit - nodesCount - nodes { - ...EnrollmentWithHouseholdFields - } - } - } -} - query GetClientServices( $id: ID! $limit: Int = 10 diff --git a/src/modules/errors/components/ErrorAlert.tsx b/src/modules/errors/components/ErrorAlert.tsx index af38a8326..c352896de 100644 --- a/src/modules/errors/components/ErrorAlert.tsx +++ b/src/modules/errors/components/ErrorAlert.tsx @@ -2,6 +2,7 @@ import { Alert, AlertProps, AlertTitle } from '@mui/material'; import { find, reject } from 'lodash-es'; import { + ErrorFilterFn, ErrorRenderFn, FIXABLE_ERROR_HEADING, UNKNOWN_VALIDATION_ERROR_HEADING, @@ -16,14 +17,18 @@ const ErrorAlert = ({ fixable = false, AlertProps = {}, renderError, + errorFilter, }: { errors: ValidationError[]; fixable?: boolean; AlertProps?: AlertProps; renderError?: ErrorRenderFn; + errorFilter?: ErrorFilterFn; // Allows the caller to filter out error messages (e.g. to selectively render outside the ErrorAlert, as in Join Households) }) => { - const filtered = reject(errors, ['severity', 'warning']); - if (filtered.length === 0) return null; + const filteredErrors = errorFilter ? errors.filter(errorFilter) : errors; + + const errorsWithoutWarnings = reject(filteredErrors, ['severity', 'warning']); + if (errorsWithoutWarnings.length === 0) return null; let title = fixable ? FIXABLE_ERROR_HEADING @@ -41,7 +46,7 @@ const ErrorAlert = ({ {...AlertProps} > {title} - + ); }; diff --git a/src/modules/errors/util.ts b/src/modules/errors/util.ts index 3bbb000eb..dd8fc05e3 100644 --- a/src/modules/errors/util.ts +++ b/src/modules/errors/util.ts @@ -41,6 +41,9 @@ export type ErrorRenderFn = ( args?: { attributeOnly?: boolean } ) => React.ReactNode; +export type ErrorFilterFn = (error: ValidationError) => boolean; +export type OnChangeErrorsFn = (errors: ErrorState) => void; + export const hasAnyValue = (state: ErrorState): boolean => !!state.apolloError || state.errors.length > 0 || state.warnings.length > 0; diff --git a/src/modules/form/components/DynamicForm.tsx b/src/modules/form/components/DynamicForm.tsx index ea304fe91..4566b71a2 100644 --- a/src/modules/form/components/DynamicForm.tsx +++ b/src/modules/form/components/DynamicForm.tsx @@ -22,7 +22,7 @@ import ApolloErrorAlert from '@/modules/errors/components/ApolloErrorAlert'; import ErrorAlert from '@/modules/errors/components/ErrorAlert'; import { ValidationDialogProps } from '@/modules/errors/components/ValidationDialog'; import { useValidationDialog } from '@/modules/errors/hooks/useValidationDialog'; -import { ErrorState, hasErrors } from '@/modules/errors/util'; +import { ErrorFilterFn, ErrorState, hasErrors } from '@/modules/errors/util'; import { formAutoCompleteOff } from '@/modules/form/util/formUtil'; import { FormDefinitionJson } from '@/types/gqlTypes'; @@ -79,7 +79,7 @@ export interface DynamicFormProps localConstants?: LocalConstants; errorRef?: RefObject; variant?: 'standard' | 'without_top_level_cards'; - onFieldChange?: (linkId?: string, value?: any) => void; + errorFilter?: ErrorFilterFn; } export interface DynamicFormRef { SaveIfDirty: VoidFunction; @@ -111,7 +111,7 @@ const DynamicForm = forwardRef( errorRef, onDirty, variant = 'standard', - onFieldChange: onFieldChangeProp, + errorFilter, }: DynamicFormProps, ref: Ref ) => { @@ -121,16 +121,12 @@ const DynamicForm = forwardRef( }, [dirty, onDirty]); const [promptSave, setPromptSave] = useState(); - const onFieldChange = useCallback( - (type: ChangeType, linkId?: string, value?: any) => { - onFieldChangeProp?.(linkId, value); - if (type === ChangeType.User) { - setPromptSave(true); - setDirty(true); - } - }, - [onFieldChangeProp] - ); + const onFieldChange = useCallback((type: ChangeType) => { + if (type === ChangeType.User) { + setPromptSave(true); + setDirty(true); + } + }, []); // getValues: returns form state (used by some nested components, like MciClearance) // getValuesForSubmit: returns submittable form state (used for onSubmit/onSaveDraft) @@ -248,7 +244,11 @@ const DynamicForm = forwardRef( - + )} diff --git a/src/modules/form/hooks/useDynamicFields.tsx b/src/modules/form/hooks/useDynamicFields.tsx index de7f9be76..fcca54ba1 100644 --- a/src/modules/form/hooks/useDynamicFields.tsx +++ b/src/modules/form/hooks/useDynamicFields.tsx @@ -41,7 +41,7 @@ const useDynamicFields = ({ initialValues?: Record; viewOnly?: boolean; localConstants?: LocalConstants; - onFieldChange?: (type: ChangeType, linkId?: string, value?: any) => void; + onFieldChange?: (type: ChangeType) => void; }) => { const [values, setValues] = useState( Object.assign({}, initialValues) @@ -158,8 +158,8 @@ const useDynamicFields = ({ const itemChanged: ItemChangedFn = useCallback( (input) => { + if (onFieldChange) onFieldChange(input.type); const { linkId, value } = input; - if (onFieldChange) onFieldChange(input.type, linkId, value); // todo @martha - incompatible with severalItemsChanged but maybe that is OK setValues((currentValues) => { const newValues = { ...currentValues }; newValues[linkId] = value; diff --git a/src/modules/form/hooks/useFormDialog.tsx b/src/modules/form/hooks/useFormDialog.tsx index 0e61f9cf9..38141050d 100644 --- a/src/modules/form/hooks/useFormDialog.tsx +++ b/src/modules/form/hooks/useFormDialog.tsx @@ -5,7 +5,14 @@ import { DialogTitle, Grid, } from '@mui/material'; -import { ReactNode, useCallback, useMemo, useRef, useState } from 'react'; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import DynamicForm, { DynamicFormProps, @@ -23,7 +30,11 @@ import useFormDefinition from './useFormDefinition'; import CommonDialog from '@/components/elements/CommonDialog'; import Loading from '@/components/elements/Loading'; -import { emptyErrorState } from '@/modules/errors/util'; +import { + emptyErrorState, + ErrorFilterFn, + OnChangeErrorsFn, +} from '@/modules/errors/util'; import { FormDefinitionFieldsFragment, @@ -48,7 +59,8 @@ interface Args extends Omit, 'formDefinition'> { onClose?: VoidFunction; localDefinition?: FormDefinitionFieldsFragment; projectId?: string; // Project context for fetching form definition - onFieldChange?: (linkId?: string, value?: any) => void; + onChangeErrors?: OnChangeErrorsFn; + errorFilter?: ErrorFilterFn; } export function useFormDialog({ onCompleted, @@ -60,7 +72,8 @@ export function useFormDialog({ localDefinition, pickListArgs, projectId, - onFieldChange, + onChangeErrors, + errorFilter, }: Args) { const errorRef = useRef(null); const [dialogOpen, setDialogOpen] = useState(false); @@ -118,6 +131,10 @@ export function useFormDialog({ const { initialValues, errors, onSubmit, submitLoading, setErrors } = useDynamicFormHandlersForRecord(hookArgs); + useEffect(() => { + onChangeErrors?.(errors); + }, [errors, onChangeErrors]); + const closeDialog = useCallback(() => { setDialogOpen(false); setErrors(emptyErrorState); @@ -187,9 +204,9 @@ export function useFormDialog({ }} variant={variant} hideSubmit + errorFilter={errorFilter} {...props} errorRef={errorRef} - onFieldChange={onFieldChange} /> @@ -213,12 +230,12 @@ export function useFormDialog({ closeDialog, definitionLoading, dialogOpen, + errorFilter, errors, formDefinition, formRole, initialValues, localConstants, - onFieldChange, onSubmit, pickListArgs, submitLoading, diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index ea1410c32..f643cc041 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -2,12 +2,14 @@ import { Button, Stack } from '@mui/material'; import { useEffect, useMemo, useState } from 'react'; import ButtonTooltipContainer from '@/components/elements/ButtonTooltipContainer'; +import Loading from '@/components/elements/Loading'; import usePrevious from '@/hooks/usePrevious'; import ClientAlertStack from '@/modules/clientAlerts/components/ClientAlertStack'; import useClientAlerts from '@/modules/clientAlerts/hooks/useClientAlerts'; +import { ErrorState } from '@/modules/errors/util'; import { useFormDialog } from '@/modules/form/hooks/useFormDialog'; -import { clientBriefName, formatDateForGql } from '@/modules/hmis/hmisUtil'; +import { clientBriefName } from '@/modules/hmis/hmisUtil'; import ConflictingEnrollmentAlert from '@/modules/household/components/elements/ConflictingEnrollmentAlert'; import JoinHouseholdDialog from '@/modules/household/components/elements/JoinHouseholdDialog'; import { useProjectCocsCountFromCache } from '@/modules/projects/hooks/useProjectCocsCountFromCache'; @@ -16,7 +18,8 @@ import { HouseholdFieldsFragment, RecordFormRole, SubmittedEnrollmentResultFieldsFragment, - useGetClientEnrollmentWithHouseholdQuery, + useGetEnrollmentWithHouseholdQuery, + ValidationError, } from '@/types/gqlTypes'; interface Props { @@ -52,9 +55,9 @@ const AddToHouseholdButton = ({ if (added) text = 'Added'; const clientId = client.id; - // todo @martha - if this is initially selected, it doesn't load - // todo @martha - need to add some kind of indication in the UI that this is loading, maybe just the enroll button is disabled - const [entryDate, setEntryDate] = useState(new Date()); + const [conflictingEnrollmentId, setConflictingEnrollmentId] = useState< + string | undefined + >(); const memoedArgs = useMemo( () => ({ @@ -66,41 +69,47 @@ const AddToHouseholdButton = ({ inputVariables: { projectId, clientId }, pickListArgs: { projectId, householdId }, localConstants: { householdId, projectCocCount: cocCount }, - onFieldChange: (linkId?: string, value?: any) => { - if (linkId === 'entry_date') { - setEntryDate(value); + errorFilter: (error: ValidationError) => { + // If there's an error about a conflicting enrollment, we will show the ConflictingEnrollmentAlert, + // so here, we filter that error out of the ErrorAlert displayed by the form dialog. + if (!householdId) return true; // Except if this is a new household. Then show the error, since we can't Join Households. + if (!error.data) return true; + return !error.data?.hasOwnProperty('conflictingEnrollmentId'); + }, + onChangeErrors: (errors: ErrorState) => { + const error = errors.errors.find((e) => + e.data?.hasOwnProperty('conflictingEnrollmentId') + ); + if (error) { + setConflictingEnrollmentId(error.data.conflictingEnrollmentId); + } else { + setConflictingEnrollmentId(undefined); } }, }), [projectId, clientId, householdId, cocCount, onSuccess] ); - const { openFormDialog, renderFormDialog } = + const { openFormDialog, renderFormDialog, closeDialog } = useFormDialog(memoedArgs); - const { clientAlerts } = useClientAlerts({ client: client }); + const [joinHouseholdDialogOpen, setJoinHouseholdDialogOpen] = useState(false); - // todo @martha - add error - const { data: { client: clientWithEnrollment } = {}, loading } = - useGetClientEnrollmentWithHouseholdQuery({ - variables: { - id: client.id, - filters: { - openOnDate: formatDateForGql(entryDate || new Date()), // entryDate will be non-null bc of skip - project: [projectId], - }, - }, - skip: !entryDate, - }); - console.log(loading); // todo @martha - improve the loading experience + // todo @martha - need to use enrollmentLockVersion? + const { + data: { enrollment: conflictingEnrollment } = {}, + loading: conflictingEnrollmentLoading, + error, + } = useGetEnrollmentWithHouseholdQuery({ + variables: { + id: conflictingEnrollmentId || '', + }, + skip: !conflictingEnrollmentId, + }); - const openEnrollmentOnDate = useMemo(() => { - const nodes = clientWithEnrollment?.enrollments.nodes; - if (nodes && nodes.length > 0) return nodes[0]; - }, [clientWithEnrollment]); - // todo @martha - if it has a conflicting enrollment, disable the Enroll button + const { clientAlerts } = useClientAlerts({ client: client }); - const [joinHouseholdDialogOpen, setJoinHouseholdDialogOpen] = useState(false); + if (error) throw error; return ( <> @@ -126,27 +135,29 @@ const AddToHouseholdButton = ({ {clientAlerts.length > 0 && ( )} - {/*todo @martha - test this more thoroughly with a past enrollment*/} - {/*todo @martha - what about enrolling someone for the 1st time (not in a household) but they already have a conflicting enrollment - should be no change to behavior*/} - {household && openEnrollmentOnDate && ( - { - // todo @martha - close the regular dialog - setJoinHouseholdDialogOpen(true); - }} - /> - )} + {household && + conflictingEnrollmentId && + (conflictingEnrollmentLoading || !conflictingEnrollment ? ( + + ) : ( + { + closeDialog(); + setJoinHouseholdDialogOpen(true); + }} + /> + ))} ), })} {/*todo @martha - don't forget to put all household alerts in the new join dialog*/} - {household && openEnrollmentOnDate && ( + {household && !!conflictingEnrollment && ( setJoinHouseholdDialogOpen(false)} // todo @martha - clear out rest of state on close receivingHousehold={household} /> diff --git a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx index 5f3c244e3..000aa333b 100644 --- a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx +++ b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx @@ -14,6 +14,7 @@ import { clientBriefName } from '@/modules/hmis/hmisUtil'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { ClientWithAlertFieldsFragment, + EnrollmentWithHouseholdFieldsFragment, HouseholdFieldsFragment, RelationshipToHoH, } from '@/types/gqlTypes'; @@ -28,14 +29,14 @@ function stringifyArray(arr: string[]) { interface Props { joiningClient: ClientWithAlertFieldsFragment; receivingHousehold: HouseholdFieldsFragment; - conflictingEnrollmentId: string; + conflictingEnrollment: EnrollmentWithHouseholdFieldsFragment; onClickJoinEnrollment: VoidFunction; } const ConflictingEnrollmentAlert = ({ joiningClient, receivingHousehold, - conflictingEnrollmentId, + conflictingEnrollment, onClickJoinEnrollment, }: Props) => { const joiningClientName = clientBriefName(joiningClient); @@ -49,12 +50,12 @@ const ConflictingEnrollmentAlert = ({ }, [receivingHousehold.householdClients]); const additionalMemberNames = useMemo(() => { - return receivingHousehold.householdClients + return conflictingEnrollment.household.householdClients .filter( (hc) => hc.relationshipToHoH !== RelationshipToHoH.SelfHeadOfHousehold ) .map((hc) => clientBriefName(hc.client)); - }, [receivingHousehold.householdClients]); + }, [conflictingEnrollment.household.householdClients]); return ( @@ -80,14 +81,13 @@ const ConflictingEnrollmentAlert = ({ - {/*todo @martha - what does this button do*/} ; - offset?: InputMaybe; - filters?: InputMaybe; -}>; - -export type GetClientEnrollmentWithHouseholdQuery = { - __typename?: 'Query'; - client?: { - __typename?: 'Client'; - id: string; - enrollments: { - __typename?: 'EnrollmentsPaginated'; - offset: number; - limit: number; - nodesCount: number; - nodes: Array<{ - __typename?: 'Enrollment'; - id: string; - lockVersion: number; - entryDate: string; - exitDate?: string | null; - exitDestination?: Destination | null; - autoExited: boolean; - inProgress: boolean; - relationshipToHoH: RelationshipToHoH; - enrollmentCoc?: string | null; - householdId: string; - householdShortId: string; - householdSize: number; - household: { - __typename?: 'Household'; - id: string; - householdSize: number; - shortId: string; - householdClients: Array<{ - __typename?: 'HouseholdClient'; - id: string; - relationshipToHoH: RelationshipToHoH; - client: { - __typename?: 'Client'; - id: string; - lockVersion: number; - firstName?: string | null; - middleName?: string | null; - lastName?: string | null; - nameSuffix?: string | null; - dob?: string | null; - age?: number | null; - ssn?: string | null; - gender: Array; - race: Array; - veteranStatus: NoYesReasonsForMissingData; - access: { - __typename?: 'ClientAccess'; - id: string; - canViewFullSsn: boolean; - canViewPartialSsn: boolean; - canEditClient: boolean; - canDeleteClient: boolean; - canViewDob: boolean; - canViewClientName: boolean; - canEditEnrollments: boolean; - canDeleteEnrollments: boolean; - canViewEnrollmentDetails: boolean; - canDeleteAssessments: boolean; - canManageAnyClientFiles: boolean; - canManageOwnClientFiles: boolean; - canViewAnyConfidentialClientFiles: boolean; - canViewAnyNonconfidentialClientFiles: boolean; - canUploadClientFiles: boolean; - canViewAnyFiles: boolean; - canAuditClients: boolean; - canManageScanCards: boolean; - canMergeClients: boolean; - canViewClientAlerts: boolean; - canManageClientAlerts: boolean; - }; - externalIds: Array<{ - __typename?: 'ExternalIdentifier'; - id: string; - identifier?: string | null; - url?: string | null; - label: string; - type: ExternalIdentifierType; - }>; - alerts: Array<{ - __typename?: 'ClientAlert'; - id: string; - note: string; - expirationDate?: string | null; - createdAt: string; - priority: ClientAlertPriorityLevel; - createdBy?: { - __typename: 'ApplicationUser'; - id: string; - name: string; - firstName?: string | null; - lastName?: string | null; - email: string; - } | null; - }>; - }; - enrollment: { - __typename?: 'Enrollment'; - id: string; - lockVersion: number; - autoExited: boolean; - entryDate: string; - exitDate?: string | null; - inProgress: boolean; - currentUnit?: { - __typename?: 'Unit'; - id: string; - name: string; - } | null; - }; - }>; - }; - project: { - __typename?: 'Project'; - id: string; - projectName: string; - projectType?: ProjectType | null; - }; - client: { - __typename?: 'Client'; - dob?: string | null; - veteranStatus: NoYesReasonsForMissingData; - id: string; - lockVersion: number; - firstName?: string | null; - middleName?: string | null; - lastName?: string | null; - nameSuffix?: string | null; - }; - access: { - __typename?: 'EnrollmentAccess'; - id: string; - canEditEnrollments: boolean; - canDeleteEnrollments: boolean; - canAuditEnrollments: boolean; - canViewEnrollmentLocationMap: boolean; - }; - currentUnit?: { __typename?: 'Unit'; id: string; name: string } | null; - }>; - }; - } | null; -}; - export type GetClientServicesQueryVariables = Exact<{ id: Scalars['ID']['input']; limit?: InputMaybe; @@ -44773,108 +44622,6 @@ export type GetClientEnrollmentsQueryResult = Apollo.QueryResult< GetClientEnrollmentsQuery, GetClientEnrollmentsQueryVariables >; -export const GetClientEnrollmentWithHouseholdDocument = gql` - query GetClientEnrollmentWithHousehold( - $id: ID! - $limit: Int = 1 - $offset: Int = 0 - $filters: EnrollmentsForClientFilterOptions - ) { - client(id: $id) { - id - enrollments( - limit: $limit - offset: $offset - sortOrder: MOST_RECENT - filters: $filters - includeEnrollmentsWithLimitedAccess: true - ) { - offset - limit - nodesCount - nodes { - ...EnrollmentWithHouseholdFields - } - } - } - } - ${EnrollmentWithHouseholdFieldsFragmentDoc} -`; - -/** - * __useGetClientEnrollmentWithHouseholdQuery__ - * - * To run a query within a React component, call `useGetClientEnrollmentWithHouseholdQuery` and pass it any options that fit your needs. - * When your component renders, `useGetClientEnrollmentWithHouseholdQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetClientEnrollmentWithHouseholdQuery({ - * variables: { - * id: // value for 'id' - * limit: // value for 'limit' - * offset: // value for 'offset' - * filters: // value for 'filters' - * }, - * }); - */ -export function useGetClientEnrollmentWithHouseholdQuery( - baseOptions: Apollo.QueryHookOptions< - GetClientEnrollmentWithHouseholdQuery, - GetClientEnrollmentWithHouseholdQueryVariables - > & - ( - | { - variables: GetClientEnrollmentWithHouseholdQueryVariables; - skip?: boolean; - } - | { skip: boolean } - ) -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery< - GetClientEnrollmentWithHouseholdQuery, - GetClientEnrollmentWithHouseholdQueryVariables - >(GetClientEnrollmentWithHouseholdDocument, options); -} -export function useGetClientEnrollmentWithHouseholdLazyQuery( - baseOptions?: Apollo.LazyQueryHookOptions< - GetClientEnrollmentWithHouseholdQuery, - GetClientEnrollmentWithHouseholdQueryVariables - > -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery< - GetClientEnrollmentWithHouseholdQuery, - GetClientEnrollmentWithHouseholdQueryVariables - >(GetClientEnrollmentWithHouseholdDocument, options); -} -export function useGetClientEnrollmentWithHouseholdSuspenseQuery( - baseOptions?: Apollo.SuspenseQueryHookOptions< - GetClientEnrollmentWithHouseholdQuery, - GetClientEnrollmentWithHouseholdQueryVariables - > -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useSuspenseQuery< - GetClientEnrollmentWithHouseholdQuery, - GetClientEnrollmentWithHouseholdQueryVariables - >(GetClientEnrollmentWithHouseholdDocument, options); -} -export type GetClientEnrollmentWithHouseholdQueryHookResult = ReturnType< - typeof useGetClientEnrollmentWithHouseholdQuery ->; -export type GetClientEnrollmentWithHouseholdLazyQueryHookResult = ReturnType< - typeof useGetClientEnrollmentWithHouseholdLazyQuery ->; -export type GetClientEnrollmentWithHouseholdSuspenseQueryHookResult = - ReturnType; -export type GetClientEnrollmentWithHouseholdQueryResult = Apollo.QueryResult< - GetClientEnrollmentWithHouseholdQuery, - GetClientEnrollmentWithHouseholdQueryVariables ->; export const GetClientServicesDocument = gql` query GetClientServices( $id: ID! From 5ff4c3fb8be551816ee77585c1ae5c81b69e899a Mon Sep 17 00:00:00 2001 From: martha Date: Wed, 15 Jan 2025 13:58:21 -0500 Subject: [PATCH 03/23] Move query for conflicting enrollment into join households dialog --- .../elements/AddToHouseholdButton.tsx | 48 +++++-------------- .../elements/ConflictingEnrollmentAlert.tsx | 34 ++----------- .../elements/JoinHouseholdDialog.tsx | 39 ++++++++++----- 3 files changed, 46 insertions(+), 75 deletions(-) diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index f643cc041..a4fd34080 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -2,7 +2,6 @@ import { Button, Stack } from '@mui/material'; import { useEffect, useMemo, useState } from 'react'; import ButtonTooltipContainer from '@/components/elements/ButtonTooltipContainer'; -import Loading from '@/components/elements/Loading'; import usePrevious from '@/hooks/usePrevious'; import ClientAlertStack from '@/modules/clientAlerts/components/ClientAlertStack'; @@ -18,7 +17,6 @@ import { HouseholdFieldsFragment, RecordFormRole, SubmittedEnrollmentResultFieldsFragment, - useGetEnrollmentWithHouseholdQuery, ValidationError, } from '@/types/gqlTypes'; @@ -82,8 +80,6 @@ const AddToHouseholdButton = ({ ); if (error) { setConflictingEnrollmentId(error.data.conflictingEnrollmentId); - } else { - setConflictingEnrollmentId(undefined); } }, }), @@ -95,22 +91,8 @@ const AddToHouseholdButton = ({ const [joinHouseholdDialogOpen, setJoinHouseholdDialogOpen] = useState(false); - // todo @martha - need to use enrollmentLockVersion? - const { - data: { enrollment: conflictingEnrollment } = {}, - loading: conflictingEnrollmentLoading, - error, - } = useGetEnrollmentWithHouseholdQuery({ - variables: { - id: conflictingEnrollmentId || '', - }, - skip: !conflictingEnrollmentId, - }); - const { clientAlerts } = useClientAlerts({ client: client }); - if (error) throw error; - return ( <> 0 && ( )} - {household && - conflictingEnrollmentId && - (conflictingEnrollmentLoading || !conflictingEnrollment ? ( - - ) : ( - { - closeDialog(); - setJoinHouseholdDialogOpen(true); - }} - /> - ))} + {household && conflictingEnrollmentId && ( + { + closeDialog(); + setJoinHouseholdDialogOpen(true); + }} + /> + )} ), })} {/*todo @martha - don't forget to put all household alerts in the new join dialog*/} - {household && !!conflictingEnrollment && ( + {household && conflictingEnrollmentId && ( setJoinHouseholdDialogOpen(false)} // todo @martha - clear out rest of state on close receivingHousehold={household} /> diff --git a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx index 000aa333b..0428751bb 100644 --- a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx +++ b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx @@ -14,33 +14,25 @@ import { clientBriefName } from '@/modules/hmis/hmisUtil'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { ClientWithAlertFieldsFragment, - EnrollmentWithHouseholdFieldsFragment, HouseholdFieldsFragment, RelationshipToHoH, } from '@/types/gqlTypes'; -function stringifyArray(arr: string[]) { - if (arr.length === 1) return arr[0]; - const firsts = arr.slice(0, arr.length - 1); - const last = arr[arr.length - 1]; - return firsts.join(', ') + ' and ' + last; // no oxford comma in case of 2-item lists :( -} - interface Props { joiningClient: ClientWithAlertFieldsFragment; receivingHousehold: HouseholdFieldsFragment; - conflictingEnrollment: EnrollmentWithHouseholdFieldsFragment; + conflictingEnrollmentId: string; onClickJoinEnrollment: VoidFunction; } const ConflictingEnrollmentAlert = ({ joiningClient, receivingHousehold, - conflictingEnrollment, + conflictingEnrollmentId, onClickJoinEnrollment, }: Props) => { const joiningClientName = clientBriefName(joiningClient); - // todo @martha - maybe factor this out into its own reusable hook? + const hohName = useMemo(() => { const hoh = receivingHousehold.householdClients.find( (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold @@ -49,14 +41,6 @@ const ConflictingEnrollmentAlert = ({ return clientBriefName(hoh.client); }, [receivingHousehold.householdClients]); - const additionalMemberNames = useMemo(() => { - return conflictingEnrollment.household.householdClients - .filter( - (hc) => hc.relationshipToHoH !== RelationshipToHoH.SelfHeadOfHousehold - ) - .map((hc) => clientBriefName(hc.client)); - }, [conflictingEnrollment.household.householdClients]); - return ( Conflicting Enrollment @@ -65,15 +49,7 @@ const ConflictingEnrollmentAlert = ({ {joiningClientName}’s enrollment can be joined with {hohName}’s - household.{' '} - {additionalMemberNames.length > 0 && ( - <> - There {additionalMemberNames.length === 1 ? 'is' : 'are'} also{' '} - {additionalMemberNames.length} other household member - {additionalMemberNames.length > 1 && 's'} associated:{' '} - {stringifyArray(additionalMemberNames)}. - - )} + household. To retain the conflicting enrollment, you must first exit{' '} @@ -87,7 +63,7 @@ const ConflictingEnrollmentAlert = ({ { + // todo @martha - need to use enrollmentLockVersion? + const { + data: { enrollment: conflictingEnrollment } = {}, + loading, + error, + } = useGetEnrollmentWithHouseholdQuery({ + variables: { + id: conflictingEnrollmentId, + }, + }); + console.log(loading); //todo @martha + const initiator = useMemo(() => { - // todo @martha - cast is safe, there will always be an initiator - // but add comments about why doing this - need to get the HouseholdClient object - return conflictingEnrollment.household.householdClients.find( + return conflictingEnrollment?.household.householdClients.find( (hc) => hc.client.id === conflictingEnrollment.client.id - ) as HouseholdClientFieldsFragment; + ); }, [ - conflictingEnrollment.client.id, - conflictingEnrollment.household.householdClients, + conflictingEnrollment?.client.id, + conflictingEnrollment?.household.householdClients, ]); - const initiatorClientName = clientBriefName(initiator.client); + const initiatorClientName = initiator?.client + ? clientBriefName(initiator.client) + : ''; const [joiningClients, setJoiningClients] = useState< HouseholdClientFieldsFragment[] - >([initiator]); + >([]); // todo @martha - initially select joining client // todo @martha = |nul here allows to set the dropdown back to null, but we want to disable the action (going to the next step) unless all are fileld out const [relationships, setRelationships] = useState< @@ -79,6 +92,10 @@ const JoinHouseholdDialog = ({ }); }, [joinHousehold, receivingHousehold.id, joiningClients, relationships]); + if (error) throw error; + + if (!conflictingEnrollment) return ; + return ( Date: Thu, 16 Jan 2025 15:36:37 -0500 Subject: [PATCH 04/23] Progress the ux and resolve some todos (still wip) --- graphql.schema.json | 14 +- .../operations/household.operations.graphql | 5 +- src/components/elements/StepDialog.tsx | 124 +++++--- .../components/FormDialogActionContent.tsx | 62 ++-- src/modules/hmis/hmisUtil.ts | 27 ++ .../components/CreateHouseholdPage.tsx | 1 + .../components/EditHouseholdMemberTable.tsx | 1 - .../components/EditHouseholdPage.tsx | 1 + .../household/components/ManageHousehold.tsx | 3 + .../elements/AddToHouseholdButton.tsx | 50 ++-- .../elements/ConflictingEnrollmentAlert.tsx | 73 ++--- .../JoinHouseholdAddRelationships.tsx | 62 ++-- .../elements/JoinHouseholdDialog.tsx | 271 ++++++++++++------ .../elements/JoinHouseholdReview.tsx | 88 ++++++ .../elements/JoinHouseholdReviewJoin.tsx | 84 ------ .../elements/JoinHouseholdSelectClients.tsx | 99 +++++-- .../elements/JoinHouseholdSuccess.tsx | 78 +++++ .../hooks/useAddToHouseholdColumns.tsx | 12 +- .../tables/ProjectHouseholdsTable.tsx | 22 +- .../search/components/ClientSearch.tsx | 2 +- src/types/gqlTypes.ts | 99 ++++++- 21 files changed, 819 insertions(+), 359 deletions(-) create mode 100644 src/modules/household/components/elements/JoinHouseholdReview.tsx delete mode 100644 src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx create mode 100644 src/modules/household/components/elements/JoinHouseholdSuccess.tsx diff --git a/graphql.schema.json b/graphql.schema.json index bdc58a492..2233dd29e 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -28119,6 +28119,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "donorHousehold", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Household", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "errors", "description": null, @@ -28144,7 +28156,7 @@ "deprecationReason": null }, { - "name": "household", + "name": "receivingHousehold", "description": null, "args": [], "type": { diff --git a/src/api/operations/household.operations.graphql b/src/api/operations/household.operations.graphql index fd1cb69b7..1f805f98a 100644 --- a/src/api/operations/household.operations.graphql +++ b/src/api/operations/household.operations.graphql @@ -1,6 +1,9 @@ mutation JoinHouseholds($input: JoinHouseholdsInput!) { joinHouseholds(input: $input) { - household { + receivingHousehold { + ...HouseholdFields + } + donorHousehold { ...HouseholdFields } } diff --git a/src/components/elements/StepDialog.tsx b/src/components/elements/StepDialog.tsx index c403b9de5..5c18bb30a 100644 --- a/src/components/elements/StepDialog.tsx +++ b/src/components/elements/StepDialog.tsx @@ -1,80 +1,134 @@ import { TabContext, TabList, TabPanel } from '@mui/lab'; import { Box, + Button, + ButtonProps, DialogActions, DialogContent, DialogTitle, Tab, } from '@mui/material'; -import { ReactNode, SyntheticEvent, useCallback, useState } from 'react'; +import { ArrowLeftIcon, ArrowRightIcon } from '@mui/x-date-pickers'; +import { + ReactNode, + SyntheticEvent, + useCallback, + useMemo, + useState, +} from 'react'; import CommonDialog, { CommonDialogProps, } from '@/components/elements/CommonDialog'; -import FormDialogActionContent from '@/modules/form/components/FormDialogActionContent'; +import Loading from '@/components/elements/Loading'; +import FormDialogActionContent, { + FormDialogActionProps, +} from '@/modules/form/components/FormDialogActionContent'; -type TabDefinition = { +export type TabDefinition = { title: string; content: ReactNode; + FormDialogActionProps?: Partial; + disableSubmit?: boolean; }; interface Props extends Omit { title: string; tabDefinitions: TabDefinition[]; + submitButtonTitle?: string; + SubmitButtonProps?: ButtonProps; + successContent?: ReactNode; onSubmit: VoidFunction; onClose: VoidFunction; + loading?: boolean; } const StepDialog = ({ title, + submitButtonTitle, + SubmitButtonProps, + successContent, tabDefinitions, onSubmit, onClose, + loading, ...rest }: Props) => { const [tabValue, setTabValue] = useState(tabDefinitions[0].title); - const handleChange = (event: SyntheticEvent, newValue: string) => { - setTabValue(newValue); - }; + const handleChange = useCallback( + (event: SyntheticEvent, newValue: string) => { + setTabValue(newValue); + }, + [] + ); - const handleSubmit = useCallback(() => { + const [previousTab, currentTab, nextTab] = useMemo(() => { const currentTabIndex = tabDefinitions.findIndex( - (td: TabDefinition) => td.title === tabValue + (t) => t.title === tabValue ); - if (currentTabIndex === tabDefinitions.length - 1) { - onSubmit?.(); + + return [ + tabDefinitions[currentTabIndex - 1], + tabDefinitions[currentTabIndex], + tabDefinitions[currentTabIndex + 1], + ]; + }, [tabDefinitions, tabValue]); + + const handleSubmit = useCallback(() => { + if (nextTab) { + setTabValue(nextTab.title); } else { - const nextTabValue = tabDefinitions[currentTabIndex + 1].title; - setTabValue(nextTabValue); + onSubmit(); } - }, [onSubmit, tabDefinitions, tabValue]); + }, [nextTab, onSubmit]); return ( - + {title} - - - - {/* todo @martha aria label*/} - {tabDefinitions.map((tab: TabDefinition) => ( - - ))} - - - {tabDefinitions.map((tab: TabDefinition) => ( - {tab.content} - ))} - + {loading && } + {!loading && !successContent && ( + + + + {tabDefinitions.map((tab: TabDefinition) => ( + + ))} + + + {tabDefinitions.map((tab: TabDefinition) => ( + + {tab.content} + + ))} + + )} + {successContent && {successContent}} - - - + {!successContent && ( + + } : SubmitButtonProps + } + onDiscard={onClose} + otherActions={ + previousTab && ( + + ) + } + {...currentTab.FormDialogActionProps} + /> + + )} ); }; diff --git a/src/modules/form/components/FormDialogActionContent.tsx b/src/modules/form/components/FormDialogActionContent.tsx index 6cb1e4140..f21d53099 100644 --- a/src/modules/form/components/FormDialogActionContent.tsx +++ b/src/modules/form/components/FormDialogActionContent.tsx @@ -1,6 +1,19 @@ import { LoadingButton } from '@mui/lab'; import { Box, Button, ButtonProps, Stack } from '@mui/material'; -import { ReactNode } from 'react'; +import React, { ReactNode, useMemo } from 'react'; +import ButtonTooltipContainer from '@/components/elements/ButtonTooltipContainer'; + +export interface FormDialogActionProps { + onDiscard: ButtonProps['onClick']; + onSubmit: ButtonProps['onClick']; + submitLoading?: boolean; + discardButtonText?: string; + submitButtonText?: string; + disabled?: boolean; + disabledReason?: string; + otherActions?: ReactNode; + PrimaryActionProps?: ButtonProps; +} export const FormDialogActionContent = ({ onDiscard, @@ -9,16 +22,27 @@ export const FormDialogActionContent = ({ submitButtonText, submitLoading, disabled, + disabledReason, otherActions, -}: { - onDiscard: ButtonProps['onClick']; - onSubmit: ButtonProps['onClick']; - submitLoading?: boolean; - discardButtonText?: string; - submitButtonText?: string; - disabled?: boolean; - otherActions?: ReactNode; -}) => { + PrimaryActionProps, +}: FormDialogActionProps) => { + const primaryAction = useMemo( + () => ( + + {submitButtonText || 'Save'} + + ), + [PrimaryActionProps, disabled, onSubmit, submitButtonText, submitLoading] + ); + return ( {discardButtonText || 'Cancel'} - - {submitButtonText || 'Save'} - + + {disabled && disabledReason ? ( + + {primaryAction} + + ) : ( + primaryAction + )} ); diff --git a/src/modules/hmis/hmisUtil.ts b/src/modules/hmis/hmisUtil.ts index 71eb80f3f..548c26d18 100644 --- a/src/modules/hmis/hmisUtil.ts +++ b/src/modules/hmis/hmisUtil.ts @@ -657,3 +657,30 @@ export const raceEthnicityDisplayString = (race?: Race[]) => { return race.map((r) => HmisEnums.Race[r]).join(', '); }; + +function stringifyArray(arr: string[]) { + if (arr.length === 1) return arr[0]; + const firsts = arr.slice(0, arr.length - 1); + const last = arr[arr.length - 1]; + const finalJoiner = arr.length === 2 ? ' and ' : ', and '; // oxford comma + return firsts.join(', ') + finalJoiner + last; +} + +export const stringifyHousehold = ( + householdClients: HouseholdClientFieldsFragment[] +) => { + return stringifyArray( + householdClients.map((hc) => clientBriefName(hc.client)) + ); +}; + +export const findHohOrRep = ( + householdClients: HouseholdClientFieldsFragment[] +) => { + // It's invalid, but possible in case of bad data, to have no HoH. If so (rare), return the first household member + return ( + householdClients.find( + (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold + ) || householdClients[0] + ); +}; diff --git a/src/modules/household/components/CreateHouseholdPage.tsx b/src/modules/household/components/CreateHouseholdPage.tsx index 25ca8aa97..b8fc07783 100644 --- a/src/modules/household/components/CreateHouseholdPage.tsx +++ b/src/modules/household/components/CreateHouseholdPage.tsx @@ -28,6 +28,7 @@ const CreateHouseholdPage = () => { ( { diff --git a/src/modules/household/components/EditHouseholdMemberTable.tsx b/src/modules/household/components/EditHouseholdMemberTable.tsx index 873315e54..71d16627b 100644 --- a/src/modules/household/components/EditHouseholdMemberTable.tsx +++ b/src/modules/household/components/EditHouseholdMemberTable.tsx @@ -175,7 +175,6 @@ const EditHouseholdMemberTable = ({ }, }, { - // todo @martha - this is interesting, maybe consolidate? header: Relationship to HoH, width: '25%', key: 'relationship', diff --git a/src/modules/household/components/EditHouseholdPage.tsx b/src/modules/household/components/EditHouseholdPage.tsx index 5ddc867f4..e894ca12d 100644 --- a/src/modules/household/components/EditHouseholdPage.tsx +++ b/src/modules/household/components/EditHouseholdPage.tsx @@ -24,6 +24,7 @@ const EditHousehold = () => { ReactNode; } @@ -46,6 +47,7 @@ interface Props { const ManageHousehold = ({ householdId: initialHouseholdId, projectId, + projectName, BackButton, renderBackButton, }: Props) => { @@ -65,6 +67,7 @@ const ManageHousehold = ({ } = useAddToHouseholdColumns({ householdId: initialHouseholdId, projectId, + projectName, }); // Fetch members to show in "previously associated" table diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index a4fd34080..9e7aaffbf 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -25,6 +25,7 @@ interface Props { isMember: boolean; householdId?: string; // if omitted, a new household will be created projectId: string; + projectName: string; onSuccess: (householdId: string) => void; household?: HouseholdFieldsFragment; } @@ -35,6 +36,7 @@ const AddToHouseholdButton = ({ householdId, onSuccess, projectId, + projectName, household, }: Props) => { const prevIsMember = usePrevious(isMember); @@ -93,6 +95,14 @@ const AddToHouseholdButton = ({ const { clientAlerts } = useClientAlerts({ client: client }); + const clientAlertsComponent = useMemo( + () => + clientAlerts.length > 0 ? ( + + ) : undefined, + [clientAlerts] + ); + return ( <> Enroll {clientBriefName(client)}, submitButtonText: `Enroll`, - preFormComponent: ( - - {clientAlerts.length > 0 && ( - - )} - {household && conflictingEnrollmentId && ( - { - closeDialog(); - setJoinHouseholdDialogOpen(true); - }} - /> - )} - - ), + // This wrapper is necessary around the preFormComponent Stack, + // otherwise the form dialog renders some awkward extra spacing. + // It could be fixed more systematically with some updates to renderFormDialog's internals. + preFormComponent: + !!clientAlertsComponent || (household && conflictingEnrollmentId) ? ( + + {clientAlertsComponent} + {household && conflictingEnrollmentId && ( + { + closeDialog(); + setJoinHouseholdDialogOpen(true); + }} + /> + )} + + ) : undefined, })} {/*todo @martha - don't forget to put all household alerts in the new join dialog*/} {household && conflictingEnrollmentId && ( @@ -138,6 +149,9 @@ const AddToHouseholdButton = ({ conflictingEnrollmentId={conflictingEnrollmentId} onClose={() => setJoinHouseholdDialogOpen(false)} // todo @martha - clear out rest of state on close receivingHousehold={household} + clientAlertsComponent={clientAlertsComponent} // todo @martha - client alerts should include household member names + projectId={projectId} + projectName={projectName} /> )} diff --git a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx index 0428751bb..38e295ee5 100644 --- a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx +++ b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx @@ -1,75 +1,42 @@ -import { - Alert, - AlertTitle, - Button, - List, - ListItem, - Stack, -} from '@mui/material'; - -import { useMemo } from 'react'; +import { Alert, AlertTitle, Button, Stack } from '@mui/material'; import { generatePath } from 'react-router-dom'; import ButtonLink from '@/components/elements/ButtonLink'; import { clientBriefName } from '@/modules/hmis/hmisUtil'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; -import { - ClientWithAlertFieldsFragment, - HouseholdFieldsFragment, - RelationshipToHoH, -} from '@/types/gqlTypes'; +import { ClientWithAlertFieldsFragment } from '@/types/gqlTypes'; interface Props { joiningClient: ClientWithAlertFieldsFragment; - receivingHousehold: HouseholdFieldsFragment; conflictingEnrollmentId: string; onClickJoinEnrollment: VoidFunction; } const ConflictingEnrollmentAlert = ({ joiningClient, - receivingHousehold, conflictingEnrollmentId, onClickJoinEnrollment, }: Props) => { - const joiningClientName = clientBriefName(joiningClient); - - const hohName = useMemo(() => { - const hoh = receivingHousehold.householdClients.find( - (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold - ); - if (!hoh) return ''; - return clientBriefName(hoh.client); - }, [receivingHousehold.householdClients]); - return ( Conflicting Enrollment - {joiningClientName} has another enrollment in this project that conflicts - with the entry date. You have two options: - - - {joiningClientName}’s enrollment can be joined with {hohName}’s - household. - - - To retain the conflicting enrollment, you must first exit{' '} - {joiningClientName}’s conflicting enrollment, before re-enrolling. - - - - - - View Conflicting Enrollment - + + {clientBriefName(joiningClient)} has another enrollment in this project + that conflicts with this entry date. You have two options: + + + + View Conflicting Enrollment + + ); diff --git a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx index c69372d1c..f7ece9e10 100644 --- a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx +++ b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx @@ -1,9 +1,16 @@ -import { Box, Paper, Stack, Typography } from '@mui/material'; +import { Box, Chip, Paper, Stack, Typography } from '@mui/material'; +import React from 'react'; import GenericTable from '@/components/elements/table/GenericTable'; +import ClientName from '@/modules/client/components/ClientName'; +import HmisEnum from '@/modules/hmis/components/HmisEnum'; import { clientBriefName } from '@/modules/hmis/hmisUtil'; import RelationshipToHohSelect from '@/modules/household/components/elements/RelationshipToHohSelect'; import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; -import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; +import { + asClient, + CLIENT_COLUMNS, +} from '@/modules/search/components/ClientSearch'; +import { HmisEnums } from '@/types/gqlEnums'; import { HouseholdClientFieldsFragment, HouseholdFieldsFragment, @@ -12,12 +19,13 @@ import { interface Props { joiningClients: HouseholdClientFieldsFragment[]; - relationships: Record; + relationships: Record; updateRelationship: ( - householdClientId: string, + enrollmentId: string, relationship: RelationshipToHoH | null ) => void; receivingHousehold: HouseholdFieldsFragment; + receivingHohName?: string; } const JoinHouseholdAddRelationships = ({ @@ -25,9 +33,8 @@ const JoinHouseholdAddRelationships = ({ relationships, updateRelationship, receivingHousehold, + receivingHohName, }: Props) => { - // todo @martha - updated entry dates - return ( @@ -35,29 +42,45 @@ const JoinHouseholdAddRelationships = ({ Add Relationships - Update joining clients' relationship to [HoH] - {/* todo @martha - new HoH*/} + Update joining clients' relationships{' '} + {receivingHohName && <>to {receivingHohName}} rows={[...receivingHousehold.householdClients, ...joiningClients]} - // todo @martha - remove exit date? (the enrollment is, probably, unexited? or it could be exited...) columns={[ - CLIENT_COLUMNS.name, + { + ...CLIENT_COLUMNS.name, + render: (client) => ( + + + {joiningClients.includes(client) && ( + + )} + + ), + sticky: 'left', + }, CLIENT_COLUMNS.age, { - header: 'Relationship', + header: 'Relationship', // todo @martha - add required indicator. star for required is not an existing pattern? key: 'relationship', + // todo @martha - padding issue on these cells + // header should also have table cell props applied? render: (hc: HouseholdClientFieldsFragment) => { if (joiningClients.includes(hc)) { return ( + // todo @martha - associate with column header as input { - updateRelationship(hc.id, selected?.value || null); + updateRelationship( + hc.enrollment.id, + selected?.value || null + ); }} textInputProps={{ - highlight: true, + highlight: true, // todo @martha - use WarnIfEmpty treatment (?) placeholder: 'Select Relationship', inputProps: { 'aria-label': `Relationship to HoH for ${clientBriefName( @@ -68,12 +91,19 @@ const JoinHouseholdAddRelationships = ({ /> ); } else { - return hc.relationshipToHoH; // todo @martha - in an enum + return ( + + ); } }, + tableCellProps: { sx: { p: 0 } }, }, WITH_ENROLLMENT_COLUMNS.entryDate, - WITH_ENROLLMENT_COLUMNS.exitDate, WITH_ENROLLMENT_COLUMNS.enrollmentStatus, ]} /> diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index 7d1177ca5..a26733447 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -1,11 +1,12 @@ -import { Alert, AlertTitle, Button, Stack } from '@mui/material'; -import { useCallback, useMemo, useState } from 'react'; +import { MergeTypeRounded } from '@mui/icons-material'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; import Loading from '@/components/elements/Loading'; -import StepDialog from '@/components/elements/StepDialog'; -import { clientBriefName } from '@/modules/hmis/hmisUtil'; +import StepDialog, { TabDefinition } from '@/components/elements/StepDialog'; +import { clientBriefName, findHohOrRep } from '@/modules/hmis/hmisUtil'; import JoinHouseholdAddRelationships from '@/modules/household/components/elements/JoinHouseholdAddRelationships'; -import JoinHouseholdReviewJoin from '@/modules/household/components/elements/JoinHouseholdReviewJoin'; +import JoinHouseholdReview from '@/modules/household/components/elements/JoinHouseholdReview'; import JoinHouseholdSelectClients from '@/modules/household/components/elements/JoinHouseholdSelectClients'; +import JoinHouseholdSuccess from '@/modules/household/components/elements/JoinHouseholdSuccess'; import { HouseholdClientFieldsFragment, HouseholdFieldsFragment, @@ -19,6 +20,9 @@ interface Props { conflictingEnrollmentId: string; onClose: VoidFunction; receivingHousehold: HouseholdFieldsFragment; + clientAlertsComponent: ReactNode; + projectId: string; + projectName: string; } const JoinHouseholdDialog = ({ @@ -26,55 +30,82 @@ const JoinHouseholdDialog = ({ onClose, conflictingEnrollmentId, receivingHousehold, + clientAlertsComponent, + projectId, + projectName, }: Props) => { + const [joiningClients, setJoiningClients] = useState< + HouseholdClientFieldsFragment[] + >([]); + // todo @martha - need to use enrollmentLockVersion? const { data: { enrollment: conflictingEnrollment } = {}, - loading, - error, + loading: fetchLoading, + error: fetchError, } = useGetEnrollmentWithHouseholdQuery({ variables: { id: conflictingEnrollmentId, }, + onCompleted: (data) => { + if (data?.enrollment?.household) { + const household = data?.enrollment?.household; + const initiator = household?.householdClients.find( + (hc) => hc.enrollment.id === conflictingEnrollmentId + ); + + if (initiator) { + // Once the household loads, set the joining clients list to the initiating client + // (the one with the conflicting enrollment), plus if they are the HoH, all their household members. + if ( + initiator.relationshipToHoH === + RelationshipToHoH.SelfHeadOfHousehold + ) { + setJoiningClients(household.householdClients); + } else { + setJoiningClients([initiator]); + } + } + } + }, }); - console.log(loading); //todo @martha - const initiator = useMemo(() => { - return conflictingEnrollment?.household.householdClients.find( - (hc) => hc.client.id === conflictingEnrollment.client.id - ); - }, [ - conflictingEnrollment?.client.id, - conflictingEnrollment?.household.householdClients, - ]); + const donorHousehold = useMemo( + () => conflictingEnrollment?.household, + [conflictingEnrollment] + ); - const initiatorClientName = initiator?.client - ? clientBriefName(initiator.client) - : ''; + const receivingHoh = useMemo( + () => findHohOrRep(receivingHousehold.householdClients), + [receivingHousehold.householdClients] + ); - const [joiningClients, setJoiningClients] = useState< - HouseholdClientFieldsFragment[] - >([]); // todo @martha - initially select joining client + const receivingHohName = useMemo(() => { + return clientBriefName(receivingHoh.client); + }, [receivingHoh]); - // todo @martha = |nul here allows to set the dropdown back to null, but we want to disable the action (going to the next step) unless all are fileld out const [relationships, setRelationships] = useState< Record >({}); - // on success - move this - const [joinedHousehold, setJoinedHousehold] = - useState(null); + // todo @martha on success - move this, make this prettier, it's so messy now, deal with errors + const [joinedHousehold, setJoinedHousehold] = useState< + HouseholdFieldsFragment | undefined + >(undefined); + const [remainingHousehold, setRemainingHousehold] = useState< + HouseholdFieldsFragment | undefined + >(undefined); const [joinHousehold, { loading: joinLoading, error: joinError }] = useJoinHouseholdsMutation({ onCompleted: (data) => { - if (data.joinHouseholds?.household) { - setJoinedHousehold(receivingHousehold); + if (data.joinHouseholds?.receivingHousehold) { + setJoinedHousehold(data.joinHouseholds?.receivingHousehold); + } + if (data.joinHouseholds?.donorHousehold) { + setRemainingHousehold(data.joinHouseholds.donorHousehold); } }, }); - // todo @martha - deal with errors and loading - console.log(joinLoading); - console.log(joinError); const onSubmit = useCallback(() => { joinHousehold({ @@ -84,7 +115,10 @@ const JoinHouseholdDialog = ({ joiningEnrollmentInputs: joiningClients.map((hc) => { return { enrollmentId: hc.enrollment.id, - relationshipToHoh: relationships[hc.id], + relationshipToHoh: + relationships[hc.enrollment.id] || + RelationshipToHoH.DataNotCollected, // todo @martha 1 - this should never be dnc, + // also the fact that I specify it twice is its own problem }; }), }, @@ -92,75 +126,128 @@ const JoinHouseholdDialog = ({ }); }, [joinHousehold, receivingHousehold.id, joiningClients, relationships]); - if (error) throw error; + const missingRelationshipsCount = useMemo( + () => + joiningClients.filter((jc) => !relationships[jc.enrollment.id]).length, + [joiningClients, relationships] + ); + + const missingRelationshipsProps = useMemo(() => { + return { + disabled: missingRelationshipsCount > 0, + disabledReason: + missingRelationshipsCount > 0 + ? `Missing ${missingRelationshipsCount} required fields` + : undefined, + }; + }, [missingRelationshipsCount]); + + const tabDefinitions: TabDefinition[] = useMemo( + () => [ + { + title: 'Select Clients', + content: ( + <> + {donorHousehold && ( + + )} + + ), + FormDialogActionProps: { + disabled: joiningClients.length === 0, + disabledReason: 'Select a client', + }, + }, + { + title: 'Add Relationships', + content: ( + { + setRelationships((prev) => { + return { + ...prev, + [enrollmentId]: relationship, + }; + }); + }} + receivingHousehold={receivingHousehold} + receivingHohName={receivingHohName} + /> + ), + FormDialogActionProps: missingRelationshipsProps, + }, + { + // todo @martha - actually test what happens if there is an error (like server error). you want to stay inside the dialog + title: 'Review Join', + content: !donorHousehold ? ( + + ) : ( + + ), + FormDialogActionProps: { + ...missingRelationshipsProps, + submitLoading: joinLoading, + disabled: joinLoading || missingRelationshipsCount > 0, + }, + }, + ], + [ + clientAlertsComponent, + donorHousehold, + joinLoading, + joiningClients, + missingRelationshipsCount, + missingRelationshipsProps, + receivingHohName, + receivingHousehold, + relationships, + ] + ); + + // const handleClose - if (!conflictingEnrollment) return ; + if (fetchError) throw fetchError; + if (joinError) throw joinError; return ( , + }} + successContent={ + joinedHousehold && ( + + ) + } + loading={fetchLoading} open={open} fullWidth maxWidth='lg' onClose={onClose} onSubmit={onSubmit} - tabDefinitions={[ - { - title: 'Select Clients', - content: ( - - ), - }, - { - title: 'Add Relationships', - content: ( - { - setRelationships((prev) => { - return { - ...prev, - [householdClientId]: relationship, - }; - }); - }} - receivingHousehold={receivingHousehold} - /> - ), - }, - { - title: 'Review Join', - content: joinedHousehold ? ( - <> - - - Successful join - [Client name] and [x] other members enrollment[s] have have - been successfully joined to [hoh name]’s Enrollment at - [project name] - - - - - - - ) : ( - - ), - }, - ]} + tabDefinitions={tabDefinitions} /> ); }; diff --git a/src/modules/household/components/elements/JoinHouseholdReview.tsx b/src/modules/household/components/elements/JoinHouseholdReview.tsx new file mode 100644 index 000000000..f5b20d112 --- /dev/null +++ b/src/modules/household/components/elements/JoinHouseholdReview.tsx @@ -0,0 +1,88 @@ +import { Box, Paper, Stack, Typography } from '@mui/material'; +import { useMemo } from 'react'; +import GenericTable from '@/components/elements/table/GenericTable'; +import { stringifyHousehold } from '@/modules/hmis/hmisUtil'; +import { JOIN_HOUSEHOLD_COLUMNS } from '@/modules/household/components/elements/JoinHouseholdSelectClients'; +import { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, +} from '@/types/gqlTypes'; + +interface Props { + joiningClients: HouseholdClientFieldsFragment[]; + donorHousehold: HouseholdFieldsFragment; + receivingHousehold: HouseholdFieldsFragment; + relationships: Record; +} + +const JoinHouseholdReview = ({ + joiningClients, + donorHousehold, + receivingHousehold, + relationships, +}: Props) => { + const joinedHouseholdClients = useMemo(() => { + return [ + ...receivingHousehold.householdClients, + ...joiningClients.map((jc) => { + return { + ...jc, + relationshipToHoH: + relationships[jc.enrollment.id] || + RelationshipToHoH.DataNotCollected, + }; // todo @martha 1 - figure out typescript so this is never dnc + }), + ]; + }, [joiningClients, receivingHousehold.householdClients, relationships]); + + const remainingHouseholdClients = useMemo(() => { + return donorHousehold.householdClients.filter( + (hc) => !joiningClients.includes(hc) + ); + }, [donorHousehold.householdClients, joiningClients]); + + const joining = useMemo( + () => stringifyHousehold(joiningClients), + [joiningClients] + ); + + return ( + + + Step 3 + Review Join + + + Check that the joined and remaining household members and details are + correct + + Joining Household + + The household that {joining} will join + + + + rows={joinedHouseholdClients} + columns={JOIN_HOUSEHOLD_COLUMNS} + /> + + {remainingHouseholdClients.length > 0 && ( + <> + Remaining Household + + The household that {joining} will leave + + + + rows={remainingHouseholdClients} + columns={JOIN_HOUSEHOLD_COLUMNS} + /> + + + )} + + ); +}; + +export default JoinHouseholdReview; diff --git a/src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx b/src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx deleted file mode 100644 index caac1c4c2..000000000 --- a/src/modules/household/components/elements/JoinHouseholdReviewJoin.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Box, Paper, Stack, Typography } from '@mui/material'; -import { useMemo } from 'react'; -import GenericTable from '@/components/elements/table/GenericTable'; -import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; -import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; -import { - HouseholdClientFieldsFragment, - HouseholdFieldsFragment, - RelationshipToHoH, -} from '@/types/gqlTypes'; - -interface Props { - joiningClients: HouseholdClientFieldsFragment[]; - donorHousehold: HouseholdFieldsFragment; - receivingHousehold: HouseholdFieldsFragment; - relationships: Record; -} - -const JoinHouseholdReviewJoin = ({ - joiningClients, - donorHousehold, - receivingHousehold, - relationships, -}: Props) => { - const joinedHouseholdClients = useMemo(() => { - return [...receivingHousehold.householdClients, ...joiningClients]; - }, [joiningClients, receivingHousehold.householdClients]); - - const remainingHouseholdClients = useMemo(() => { - return donorHousehold.householdClients.filter( - (hc) => !joiningClients.includes(hc) - ); - }, [donorHousehold.householdClients, joiningClients]); - - return ( - - - Step 3 - Review Join - - - Check that the joined and remaining household members and details are - correct - - - Joined Household - - - rows={joinedHouseholdClients} - columns={[ - CLIENT_COLUMNS.name, - CLIENT_COLUMNS.age, - { - header: 'Relationship', - key: 'relationship', - // todo @martha - factor definition somewhere common - render: (hc: HouseholdClientFieldsFragment) => - relationships[hc.id] || hc.relationshipToHoH, - }, - WITH_ENROLLMENT_COLUMNS.entryDate, - WITH_ENROLLMENT_COLUMNS.exitDate, - WITH_ENROLLMENT_COLUMNS.enrollmentStatus, - ]} - /> - - Other Household - - - rows={remainingHouseholdClients} - columns={[ - CLIENT_COLUMNS.name, - CLIENT_COLUMNS.age, - // todo @martha - consistent cols - WITH_ENROLLMENT_COLUMNS.entryDate, - WITH_ENROLLMENT_COLUMNS.exitDate, - WITH_ENROLLMENT_COLUMNS.enrollmentStatus, - ]} - /> - - - ); -}; - -export default JoinHouseholdReviewJoin; diff --git a/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx b/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx index d9f616e19..94cfb52dd 100644 --- a/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx +++ b/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx @@ -1,3 +1,4 @@ +import { InfoOutlined } from '@mui/icons-material'; import { Alert, AlertTitle, @@ -7,11 +8,21 @@ import { Typography, } from '@mui/material'; -import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; +import { + Dispatch, + ReactNode, + SetStateAction, + useCallback, + useMemo, +} from 'react'; import { generatePath } from 'react-router-dom'; import ButtonLink from '@/components/elements/ButtonLink'; import GenericTable from '@/components/elements/table/GenericTable'; -import { PROJECT_HOUSEHOLD_COLUMNS } from '@/modules/projects/components/tables/ProjectHouseholdsTable'; +import { ColumnDef } from '@/components/elements/table/types'; +import { clientBriefName, sortHouseholdMembers } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { HOUSEHOLD_CLIENT_COLUMNS } from '@/modules/projects/components/tables/ProjectHouseholdsTable'; +import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { HouseholdClientFieldsFragment, @@ -19,35 +30,55 @@ import { RelationshipToHoH, } from '@/types/gqlTypes'; +export const JOIN_HOUSEHOLD_COLUMNS: ColumnDef[] = + [ + { ...CLIENT_COLUMNS.name, sticky: 'left' }, + CLIENT_COLUMNS.age, + HOUSEHOLD_CLIENT_COLUMNS.relationship, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.enrollmentStatus, + ]; + interface Props { donorHousehold: HouseholdFieldsFragment; selectedClients: HouseholdClientFieldsFragment[]; setSelectedClients: Dispatch>; - // todo @martha - need receivingHouseholdHohName + receivingHohName?: string; + clientAlertsComponent: ReactNode; } const JoinHouseholdSelectClients = ({ donorHousehold, selectedClients, setSelectedClients, + receivingHohName, + clientAlertsComponent, }: Props) => { - const selectedClientIds = useMemo( - () => selectedClients.map((hc) => hc.id), - [selectedClients] - ); - - // todo @martha - add some comments here - const hoh = useMemo( + const donorHoh = useMemo( () => donorHousehold.householdClients.find( (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold - ) as HouseholdClientFieldsFragment, + ), + [donorHousehold.householdClients] + ); + const donorHouseholdMembers = useMemo( + () => sortHouseholdMembers(donorHousehold.householdClients), [donorHousehold.householdClients] ); + // Selection is controlled, so the selection state is stored in the parent. + // Here, translate the list of selected HouseholdClients from the parent to a list of row IDs for GenericTable. + const selectedClientIds = useMemo( + () => selectedClients.map((hc) => hc.id), + [selectedClients] + ); + + // .. and here, translate back again const setSelectedClientIds = useCallback( (clientIds: readonly string[]) => { - if (hoh && clientIds.includes(hoh.id)) { + // If the HoH from the donor household is selected, all other hh members must be selected. + // (Can't leave behind a household without a HoH) + if (donorHoh && clientIds.includes(donorHoh.id)) { setSelectedClients(donorHousehold.householdClients); } else { setSelectedClients( @@ -57,7 +88,21 @@ const JoinHouseholdSelectClients = ({ ); } }, - [donorHousehold.householdClients, hoh, setSelectedClients] + [donorHousehold.householdClients, donorHoh, setSelectedClients] + ); + + const isRowSelectable = useCallback( + (row: HouseholdClientFieldsFragment) => { + // todo @martha - want to be able to select all and then deselect all. + if (donorHoh && selectedClientIds.includes(donorHoh.id)) { + // Visually disable de-selecting other clients if the HoH is selected + // (This is also functionally disabled by the logic in setSelectedClientIds above) + return row.id === donorHoh.id; + } + + return true; + }, + [donorHoh, selectedClientIds] ); return ( @@ -65,24 +110,25 @@ const JoinHouseholdSelectClients = ({ Step 1 Select Clients - {/* todo @martha - should it really be an h3?*/} - Select which clients you would like to join to [HoH]’s enrollment. + Select which clients you would like to join{' '} + {receivingHohName ? `${receivingHohName}’s` : 'this'} household. - {/*todo @martha - this (above) refers to the NEW hoh*/} - {hoh && - selectedClientIds.includes(hoh.id) && + {clientAlertsComponent} + {donorHoh && + selectedClientIds.includes(donorHoh.id) && donorHousehold.householdSize > 1 && ( } action={ Edit Household @@ -90,20 +136,19 @@ const JoinHouseholdSelectClients = ({ } > Head of Household Selected - All members must accompany the Head of Household. Select a different - Head of Household if this is not your intention. + If the current Head of Household is selected, all members must also + join. Edit {clientBriefName(donorHoh.client)}’s household and select + a different Head of Household if this is not your intention. )} - {/* todo @martha - could be nice to disable unselection on the other members, and add a tooltip*/} - {/*todo @martha - put these in the expected order*/} - rows={donorHousehold.householdClients} - // todo @martha - remove exit date? (the enrollment is, probably, unexited? or it could be exited...) - columns={PROJECT_HOUSEHOLD_COLUMNS} + rows={donorHouseholdMembers} + columns={JOIN_HOUSEHOLD_COLUMNS} selectable={'checkbox'} selected={selectedClientIds} onChangeSelectedRowIds={setSelectedClientIds} + isRowSelectable={isRowSelectable} /> diff --git a/src/modules/household/components/elements/JoinHouseholdSuccess.tsx b/src/modules/household/components/elements/JoinHouseholdSuccess.tsx new file mode 100644 index 000000000..1a269e9ab --- /dev/null +++ b/src/modules/household/components/elements/JoinHouseholdSuccess.tsx @@ -0,0 +1,78 @@ +import { Alert, AlertTitle, Button, Stack } from '@mui/material'; +import React from 'react'; +import { generatePath } from 'react-router-dom'; +import ButtonLink from '@/components/elements/ButtonLink'; +import { + clientBriefName, + findHohOrRep, + stringifyHousehold, +} from '@/modules/hmis/hmisUtil'; +import { + EnrollmentDashboardRoutes, + ProjectDashboardRoutes, +} from '@/routes/routes'; +import { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, +} from '@/types/gqlTypes'; + +interface Props { + receivingHohName: string; + joinedClients: HouseholdClientFieldsFragment[]; + remainingHousehold?: HouseholdFieldsFragment; + projectId: string; + projectName: string; + onClose: VoidFunction; +} + +const JoinHouseholdSuccess = ({ + receivingHohName, + joinedClients, + remainingHousehold, + projectId, + projectName, + onClose, +}: Props) => { + // todo @martha - test what actually happens if there is no HoH + const remainingHouseholdClient = findHohOrRep( + remainingHousehold?.householdClients || [] + ); + const remainingName = remainingHouseholdClient + ? clientBriefName(remainingHouseholdClient.client) + : undefined; + + return ( + + + Successful Join + {stringifyHousehold(joinedClients)}{' '} + {joinedClients.length > 1 ? 'have' : 'has'} been successfully joined to{' '} + {receivingHohName}’s Enrollment at {projectName} + + + {remainingHousehold && remainingHouseholdClient && ( + + View {remainingName}’s Enrollment + + )} + + View Enrollments at {projectName} + + + ); +}; + +export default JoinHouseholdSuccess; diff --git a/src/modules/household/hooks/useAddToHouseholdColumns.tsx b/src/modules/household/hooks/useAddToHouseholdColumns.tsx index 353d5cf15..5a10278a0 100644 --- a/src/modules/household/hooks/useAddToHouseholdColumns.tsx +++ b/src/modules/household/hooks/useAddToHouseholdColumns.tsx @@ -11,11 +11,13 @@ import { interface Args { householdId?: string; projectId: string; + projectName: string; } export default function useAddToHouseholdColumns({ householdId: initialHouseholdId, projectId, + projectName, }: Args) { const [householdId, setHouseholdId] = useState(initialHouseholdId); const [getHousehold, { data, loading, error }] = useGetHouseholdLazyQuery({ @@ -75,6 +77,7 @@ export default function useAddToHouseholdColumns({ client={client} householdId={householdId} projectId={projectId} + projectName={projectName} isMember={currentMembersMap.has(client.id)} onSuccess={onSuccess} household={data?.household || undefined} @@ -83,7 +86,14 @@ export default function useAddToHouseholdColumns({ }, }, ]; - }, [householdId, projectId, currentMembersMap, onSuccess, data?.household]); + }, [ + householdId, + projectId, + projectName, + currentMembersMap, + onSuccess, + data?.household, + ]); if (error) throw error; diff --git a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx index 21ac5416b..7ef63f9d8 100644 --- a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx @@ -31,6 +31,7 @@ import { GetProjectHouseholdsDocument, GetProjectHouseholdsQuery, GetProjectHouseholdsQueryVariables, + HouseholdClientFieldsFragment, HouseholdFilterOptions, ProjectEnrollmentsHouseholdClientFieldsFragment, ProjectEnrollmentsHouseholdFieldsFragment, @@ -40,13 +41,15 @@ export type HouseholdFields = NonNullable< GetProjectHouseholdsQuery['project'] >['households']['nodes'][number]; -type OneHouseholdClient = HouseholdFields['householdClients'][number]; -export const PROJECT_HOUSEHOLD_COLUMNS: ColumnDef[] = [ - { ...CLIENT_COLUMNS.name, sticky: 'left' }, - CLIENT_COLUMNS.age, - { +export const HOUSEHOLD_CLIENT_COLUMNS: Record< + string, + ColumnDef +> = { + relationship: { header: 'Relationship', - render: (householdClient) => ( + render: ( + householdClient: OneHouseholdClient | HouseholdClientFieldsFragment + ) => ( [] = [ /> ), }, +}; + +type OneHouseholdClient = HouseholdFields['householdClients'][number]; +export const PROJECT_HOUSEHOLD_COLUMNS: ColumnDef[] = [ + { ...CLIENT_COLUMNS.name, sticky: 'left' }, + CLIENT_COLUMNS.age, + HOUSEHOLD_CLIENT_COLUMNS.relationship, WITH_ENROLLMENT_COLUMNS.entryDate, WITH_ENROLLMENT_COLUMNS.exitDate, WITH_ENROLLMENT_COLUMNS.enrollmentStatus, diff --git a/src/modules/search/components/ClientSearch.tsx b/src/modules/search/components/ClientSearch.tsx index 6d089c091..4f8c71b96 100644 --- a/src/modules/search/components/ClientSearch.tsx +++ b/src/modules/search/components/ClientSearch.tsx @@ -53,7 +53,7 @@ import { SearchClientsQueryVariables, } from '@/types/gqlTypes'; -function asClient( +export function asClient( record: | ClientFieldsFragment | HouseholdClientFieldsFragment diff --git a/src/types/gqlTypes.ts b/src/types/gqlTypes.ts index 179ff6c05..3d6db538d 100644 --- a/src/types/gqlTypes.ts +++ b/src/types/gqlTypes.ts @@ -3948,8 +3948,9 @@ export type JoinHouseholdsPayload = { __typename?: 'JoinHouseholdsPayload'; /** A unique identifier for the client performing the mutation. */ clientMutationId?: Maybe; + donorHousehold?: Maybe; errors: Array; - household: Household; + receivingHousehold: Household; }; export type JoiningEnrollmentInput = { @@ -33624,7 +33625,7 @@ export type JoinHouseholdsMutation = { __typename?: 'Mutation'; joinHouseholds?: { __typename?: 'JoinHouseholdsPayload'; - household: { + receivingHousehold: { __typename?: 'Household'; id: string; householdSize: number; @@ -33713,6 +33714,95 @@ export type JoinHouseholdsMutation = { }; }>; }; + donorHousehold?: { + __typename?: 'Household'; + id: string; + householdSize: number; + shortId: string; + householdClients: Array<{ + __typename?: 'HouseholdClient'; + id: string; + relationshipToHoH: RelationshipToHoH; + client: { + __typename?: 'Client'; + id: string; + lockVersion: number; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + nameSuffix?: string | null; + dob?: string | null; + age?: number | null; + ssn?: string | null; + gender: Array; + race: Array; + veteranStatus: NoYesReasonsForMissingData; + access: { + __typename?: 'ClientAccess'; + id: string; + canViewFullSsn: boolean; + canViewPartialSsn: boolean; + canEditClient: boolean; + canDeleteClient: boolean; + canViewDob: boolean; + canViewClientName: boolean; + canEditEnrollments: boolean; + canDeleteEnrollments: boolean; + canViewEnrollmentDetails: boolean; + canDeleteAssessments: boolean; + canManageAnyClientFiles: boolean; + canManageOwnClientFiles: boolean; + canViewAnyConfidentialClientFiles: boolean; + canViewAnyNonconfidentialClientFiles: boolean; + canUploadClientFiles: boolean; + canViewAnyFiles: boolean; + canAuditClients: boolean; + canManageScanCards: boolean; + canMergeClients: boolean; + canViewClientAlerts: boolean; + canManageClientAlerts: boolean; + }; + externalIds: Array<{ + __typename?: 'ExternalIdentifier'; + id: string; + identifier?: string | null; + url?: string | null; + label: string; + type: ExternalIdentifierType; + }>; + alerts: Array<{ + __typename?: 'ClientAlert'; + id: string; + note: string; + expirationDate?: string | null; + createdAt: string; + priority: ClientAlertPriorityLevel; + createdBy?: { + __typename: 'ApplicationUser'; + id: string; + name: string; + firstName?: string | null; + lastName?: string | null; + email: string; + } | null; + }>; + }; + enrollment: { + __typename?: 'Enrollment'; + id: string; + lockVersion: number; + autoExited: boolean; + entryDate: string; + exitDate?: string | null; + inProgress: boolean; + currentUnit?: { + __typename?: 'Unit'; + id: string; + name: string; + } | null; + }; + }>; + } | null; } | null; }; @@ -49842,7 +49932,10 @@ export type GetEnrollmentGeolocationsQueryResult = Apollo.QueryResult< export const JoinHouseholdsDocument = gql` mutation JoinHouseholds($input: JoinHouseholdsInput!) { joinHouseholds(input: $input) { - household { + receivingHousehold { + ...HouseholdFields + } + donorHousehold { ...HouseholdFields } } From caff6f9b3b98730a65aa00043b31a584e70a1d4e Mon Sep 17 00:00:00 2001 From: martha Date: Fri, 17 Jan 2025 09:17:37 -0500 Subject: [PATCH 05/23] Resolve some todos (still wip) --- .../elements/AddToHouseholdButton.tsx | 11 ++- .../elements/JoinHouseholdDialog.tsx | 70 ++++++++++--------- .../elements/JoinHouseholdReview.tsx | 2 +- .../elements/JoinHouseholdSuccess.tsx | 1 - 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index 9e7aaffbf..ff3b60610 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -1,5 +1,5 @@ import { Button, Stack } from '@mui/material'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import ButtonTooltipContainer from '@/components/elements/ButtonTooltipContainer'; import usePrevious from '@/hooks/usePrevious'; @@ -103,6 +103,13 @@ const AddToHouseholdButton = ({ [clientAlerts] ); + const onCloseJoinHouseholds = useCallback(() => { + setJoinHouseholdDialogOpen(false); + setConflictingEnrollmentId(undefined); + }, []); + + // todo @martha - the button should say "Added" after the join workflow is complete + return ( <> setJoinHouseholdDialogOpen(false)} // todo @martha - clear out rest of state on close + onClose={onCloseJoinHouseholds} receivingHousehold={household} clientAlertsComponent={clientAlertsComponent} // todo @martha - client alerts should include household member names projectId={projectId} diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index a26733447..b6d6122bc 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -34,11 +34,15 @@ const JoinHouseholdDialog = ({ projectId, projectName, }: Props) => { + // Data this join workflow is collecting: What clients are joining and what their relationships are const [joiningClients, setJoiningClients] = useState< HouseholdClientFieldsFragment[] >([]); + const [relationships, setRelationships] = useState< + Record + >({}); - // todo @martha - need to use enrollmentLockVersion? + // Fetch the conflicting enrollment by ID since we need the full EnrollmentWithHousehold const { data: { enrollment: conflictingEnrollment } = {}, loading: fetchLoading, @@ -84,11 +88,22 @@ const JoinHouseholdDialog = ({ return clientBriefName(receivingHoh.client); }, [receivingHoh]); - const [relationships, setRelationships] = useState< - Record - >({}); + const missingRelationshipsCount = useMemo( + () => + joiningClients.filter((jc) => !relationships[jc.enrollment.id]).length, + [joiningClients, relationships] + ); + + const missingRelationshipsProps = useMemo(() => { + return { + disabled: missingRelationshipsCount > 0, + disabledReason: + missingRelationshipsCount > 0 + ? `Missing ${missingRelationshipsCount} required fields` + : undefined, + }; + }, [missingRelationshipsCount]); - // todo @martha on success - move this, make this prettier, it's so messy now, deal with errors const [joinedHousehold, setJoinedHousehold] = useState< HouseholdFieldsFragment | undefined >(undefined); @@ -98,49 +113,43 @@ const JoinHouseholdDialog = ({ const [joinHousehold, { loading: joinLoading, error: joinError }] = useJoinHouseholdsMutation({ onCompleted: (data) => { - if (data.joinHouseholds?.receivingHousehold) { - setJoinedHousehold(data.joinHouseholds?.receivingHousehold); - } - if (data.joinHouseholds?.donorHousehold) { - setRemainingHousehold(data.joinHouseholds.donorHousehold); + if (data.joinHouseholds) { + setJoinedHousehold(data.joinHouseholds.receivingHousehold); + setRemainingHousehold( + data.joinHouseholds.donorHousehold || undefined + ); } }, }); const onSubmit = useCallback(() => { + if (missingRelationshipsCount > 0) return; // This should never happen; the button will be disabled + joinHousehold({ variables: { input: { receivingHouseholdId: receivingHousehold.id, joiningEnrollmentInputs: joiningClients.map((hc) => { return { + // todo @martha - need to use enrollmentLockVersion? enrollmentId: hc.enrollment.id, + // `|| RelationshipToHoH.DataNotCollected` is to keep typescript happy; + // thanks to the missingRelationshipsCount logic, we know the relationships will be non-null relationshipToHoh: relationships[hc.enrollment.id] || - RelationshipToHoH.DataNotCollected, // todo @martha 1 - this should never be dnc, - // also the fact that I specify it twice is its own problem + RelationshipToHoH.DataNotCollected, }; }), }, }, }); - }, [joinHousehold, receivingHousehold.id, joiningClients, relationships]); - - const missingRelationshipsCount = useMemo( - () => - joiningClients.filter((jc) => !relationships[jc.enrollment.id]).length, - [joiningClients, relationships] - ); - - const missingRelationshipsProps = useMemo(() => { - return { - disabled: missingRelationshipsCount > 0, - disabledReason: - missingRelationshipsCount > 0 - ? `Missing ${missingRelationshipsCount} required fields` - : undefined, - }; - }, [missingRelationshipsCount]); + }, [ + missingRelationshipsCount, + joinHousehold, + receivingHousehold.id, + joiningClients, + relationships, + ]); const tabDefinitions: TabDefinition[] = useMemo( () => [ @@ -185,7 +194,6 @@ const JoinHouseholdDialog = ({ FormDialogActionProps: missingRelationshipsProps, }, { - // todo @martha - actually test what happens if there is an error (like server error). you want to stay inside the dialog title: 'Review Join', content: !donorHousehold ? ( @@ -217,8 +225,6 @@ const JoinHouseholdDialog = ({ ] ); - // const handleClose - if (fetchError) throw fetchError; if (joinError) throw joinError; diff --git a/src/modules/household/components/elements/JoinHouseholdReview.tsx b/src/modules/household/components/elements/JoinHouseholdReview.tsx index f5b20d112..61578c907 100644 --- a/src/modules/household/components/elements/JoinHouseholdReview.tsx +++ b/src/modules/household/components/elements/JoinHouseholdReview.tsx @@ -31,7 +31,7 @@ const JoinHouseholdReview = ({ relationshipToHoH: relationships[jc.enrollment.id] || RelationshipToHoH.DataNotCollected, - }; // todo @martha 1 - figure out typescript so this is never dnc + }; }), ]; }, [joiningClients, receivingHousehold.householdClients, relationships]); diff --git a/src/modules/household/components/elements/JoinHouseholdSuccess.tsx b/src/modules/household/components/elements/JoinHouseholdSuccess.tsx index 1a269e9ab..5d0ac2337 100644 --- a/src/modules/household/components/elements/JoinHouseholdSuccess.tsx +++ b/src/modules/household/components/elements/JoinHouseholdSuccess.tsx @@ -33,7 +33,6 @@ const JoinHouseholdSuccess = ({ projectName, onClose, }: Props) => { - // todo @martha - test what actually happens if there is no HoH const remainingHouseholdClient = findHohOrRep( remainingHousehold?.householdClients || [] ); From 39def8384ffb16aad4d433d09d2fdf8172d1f632 Mon Sep 17 00:00:00 2001 From: martha Date: Fri, 17 Jan 2025 09:43:58 -0500 Subject: [PATCH 06/23] Add client alerts to join workflow --- .../clientAlerts/hooks/useClientAlerts.tsx | 11 +++++++++-- .../elements/AddToHouseholdButton.tsx | 2 -- .../JoinHouseholdAddRelationships.tsx | 1 + .../elements/JoinHouseholdDialog.tsx | 6 +----- .../elements/JoinHouseholdSelectClients.tsx | 19 +++++++++---------- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/modules/clientAlerts/hooks/useClientAlerts.tsx b/src/modules/clientAlerts/hooks/useClientAlerts.tsx index cd03d9ec3..93492de9d 100644 --- a/src/modules/clientAlerts/hooks/useClientAlerts.tsx +++ b/src/modules/clientAlerts/hooks/useClientAlerts.tsx @@ -4,6 +4,7 @@ import { useHasRootPermissions } from '@/modules/permissions/useHasPermissionsHo import { ClientAccess, ClientWithAlertFieldsFragment, + GetHouseholdClientAlertsQuery, useGetHouseholdClientAlertsQuery, } from '@/types/gqlTypes'; @@ -15,6 +16,7 @@ enum AlertPriority { interface ClientAlertParams { householdId?: string; + household?: GetHouseholdClientAlertsQuery['household']; client?: Omit & { access?: Partial; }; @@ -26,14 +28,19 @@ export default function useClientAlerts(params: ClientAlertParams) { const [canViewClientAlerts] = useHasRootPermissions(['canViewClientAlerts']); const { - data: { household } = {}, + data: { household: queriedHousehold } = {}, loading, error, } = useGetHouseholdClientAlertsQuery({ variables: { id: params.householdId || '' }, - skip: !params.householdId || !canViewClientAlerts, + skip: !params.householdId || !!params.household || !canViewClientAlerts, }); + const household = useMemo( + () => queriedHousehold || params.household, + [params.household, queriedHousehold] + ); + const canViewAlertsForAnyHouseholdMembers = useMemo(() => { if (!canViewClientAlerts) return false; if (!household) return true; diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index ff3b60610..65bac8ac2 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -149,14 +149,12 @@ const AddToHouseholdButton = ({ ) : undefined, })} - {/*todo @martha - don't forget to put all household alerts in the new join dialog*/} {household && conflictingEnrollmentId && ( diff --git a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx index f7ece9e10..ab1f57149 100644 --- a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx +++ b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx @@ -44,6 +44,7 @@ const JoinHouseholdAddRelationships = ({ Update joining clients' relationships{' '} {receivingHohName && <>to {receivingHohName}} + {/* todo @martha - add warning here about entry dates. see design ? */} diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index b6d6122bc..ba3fb9dc3 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -1,5 +1,5 @@ import { MergeTypeRounded } from '@mui/icons-material'; -import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import Loading from '@/components/elements/Loading'; import StepDialog, { TabDefinition } from '@/components/elements/StepDialog'; import { clientBriefName, findHohOrRep } from '@/modules/hmis/hmisUtil'; @@ -20,7 +20,6 @@ interface Props { conflictingEnrollmentId: string; onClose: VoidFunction; receivingHousehold: HouseholdFieldsFragment; - clientAlertsComponent: ReactNode; projectId: string; projectName: string; } @@ -30,7 +29,6 @@ const JoinHouseholdDialog = ({ onClose, conflictingEnrollmentId, receivingHousehold, - clientAlertsComponent, projectId, projectName, }: Props) => { @@ -163,7 +161,6 @@ const JoinHouseholdDialog = ({ selectedClients={joiningClients} setSelectedClients={setJoiningClients} receivingHohName={receivingHohName} - clientAlertsComponent={clientAlertsComponent} /> )} @@ -213,7 +210,6 @@ const JoinHouseholdDialog = ({ }, ], [ - clientAlertsComponent, donorHousehold, joinLoading, joiningClients, diff --git a/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx b/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx index 94cfb52dd..162b46c22 100644 --- a/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx +++ b/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx @@ -8,17 +8,13 @@ import { Typography, } from '@mui/material'; -import { - Dispatch, - ReactNode, - SetStateAction, - useCallback, - useMemo, -} from 'react'; +import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; import { generatePath } from 'react-router-dom'; import ButtonLink from '@/components/elements/ButtonLink'; import GenericTable from '@/components/elements/table/GenericTable'; import { ColumnDef } from '@/components/elements/table/types'; +import ClientAlertStack from '@/modules/clientAlerts/components/ClientAlertStack'; +import useClientAlerts from '@/modules/clientAlerts/hooks/useClientAlerts'; import { clientBriefName, sortHouseholdMembers } from '@/modules/hmis/hmisUtil'; import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { HOUSEHOLD_CLIENT_COLUMNS } from '@/modules/projects/components/tables/ProjectHouseholdsTable'; @@ -44,7 +40,6 @@ interface Props { selectedClients: HouseholdClientFieldsFragment[]; setSelectedClients: Dispatch>; receivingHohName?: string; - clientAlertsComponent: ReactNode; } const JoinHouseholdSelectClients = ({ @@ -52,7 +47,6 @@ const JoinHouseholdSelectClients = ({ selectedClients, setSelectedClients, receivingHohName, - clientAlertsComponent, }: Props) => { const donorHoh = useMemo( () => @@ -105,6 +99,11 @@ const JoinHouseholdSelectClients = ({ [donorHoh, selectedClientIds] ); + const { clientAlerts } = useClientAlerts({ + household: donorHousehold, + showClientName: true, + }); + return ( @@ -115,7 +114,7 @@ const JoinHouseholdSelectClients = ({ Select which clients you would like to join{' '} {receivingHohName ? `${receivingHohName}’s` : 'this'} household. - {clientAlertsComponent} + {} {donorHoh && selectedClientIds.includes(donorHoh.id) && donorHousehold.householdSize > 1 && ( From 11b23f2456537fe814d862c2e92ab933091d0033 Mon Sep 17 00:00:00 2001 From: martha Date: Fri, 17 Jan 2025 11:02:32 -0500 Subject: [PATCH 07/23] Fix some bugs and todos --- .../elements/DateWithRelativeTooltip.tsx | 5 +++- .../JoinHouseholdAddRelationships.tsx | 24 +++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/components/elements/DateWithRelativeTooltip.tsx b/src/components/elements/DateWithRelativeTooltip.tsx index c63bf7130..fdfa7a43d 100644 --- a/src/components/elements/DateWithRelativeTooltip.tsx +++ b/src/components/elements/DateWithRelativeTooltip.tsx @@ -54,7 +54,10 @@ const DateWithRelativeTooltip = ({ > {formattedDate} {/* Include the tooltip text as visually hidden for accessibility */} - , {formattedDateRelative} + {/* Add position: fixed to address visual bug when visuallyHidden is used inside dialog */} + + , {formattedDateRelative} + ); diff --git a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx index ab1f57149..38ac25318 100644 --- a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx +++ b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx @@ -1,7 +1,8 @@ import { Box, Chip, Paper, Stack, Typography } from '@mui/material'; -import React from 'react'; +import React, { useId } from 'react'; import GenericTable from '@/components/elements/table/GenericTable'; import ClientName from '@/modules/client/components/ClientName'; +import RequiredLabel from '@/modules/form/components/RequiredLabel'; import HmisEnum from '@/modules/hmis/components/HmisEnum'; import { clientBriefName } from '@/modules/hmis/hmisUtil'; import RelationshipToHohSelect from '@/modules/household/components/elements/RelationshipToHohSelect'; @@ -35,6 +36,8 @@ const JoinHouseholdAddRelationships = ({ receivingHousehold, receivingHohName, }: Props) => { + const relationshipHeaderId = useId(); + return ( @@ -64,15 +67,22 @@ const JoinHouseholdAddRelationships = ({ }, CLIENT_COLUMNS.age, { - header: 'Relationship', // todo @martha - add required indicator. star for required is not an existing pattern? + header: ( + + ), key: 'relationship', - // todo @martha - padding issue on these cells - // header should also have table cell props applied? render: (hc: HouseholdClientFieldsFragment) => { if (joiningClients.includes(hc)) { return ( - // todo @martha - associate with column header as input { updateRelationship( @@ -81,7 +91,7 @@ const JoinHouseholdAddRelationships = ({ ); }} textInputProps={{ - highlight: true, // todo @martha - use WarnIfEmpty treatment (?) + warnIfEmptyTreatment: !relationships[hc.enrollment.id], placeholder: 'Select Relationship', inputProps: { 'aria-label': `Relationship to HoH for ${clientBriefName( @@ -102,7 +112,7 @@ const JoinHouseholdAddRelationships = ({ ); } }, - tableCellProps: { sx: { p: 0 } }, + tableCellProps: { sx: { py: 0 } }, }, WITH_ENROLLMENT_COLUMNS.entryDate, WITH_ENROLLMENT_COLUMNS.enrollmentStatus, From d2968b0702cf3b6b1341993c120583f5a07e70f2 Mon Sep 17 00:00:00 2001 From: martha Date: Fri, 17 Jan 2025 11:30:48 -0500 Subject: [PATCH 08/23] Set button to Added --- .../components/elements/AddToHouseholdButton.tsx | 15 ++++++++++++++- .../components/elements/JoinHouseholdDialog.tsx | 9 +++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index 65bac8ac2..7a286af37 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -108,7 +108,19 @@ const AddToHouseholdButton = ({ setConflictingEnrollmentId(undefined); }, []); - // todo @martha - the button should say "Added" after the join workflow is complete + const onCompleteJoinHouseholds = useCallback( + (joinedHousehold: HouseholdFieldsFragment) => { + // This only updates the initiating client's button + if ( + joinedHousehold.householdClients.find( + (hc) => hc.client.id === client.id + ) + ) { + setAdded(true); + } + }, + [client.id] + ); return ( <> @@ -154,6 +166,7 @@ const AddToHouseholdButton = ({ open={joinHouseholdDialogOpen} conflictingEnrollmentId={conflictingEnrollmentId} onClose={onCloseJoinHouseholds} + onComplete={onCompleteJoinHouseholds} receivingHousehold={household} projectId={projectId} projectName={projectName} diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index ba3fb9dc3..18de4cb63 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -19,6 +19,10 @@ interface Props { open: boolean; conflictingEnrollmentId: string; onClose: VoidFunction; + onComplete?: ( + joinedHousehold: HouseholdFieldsFragment, + remainingHousehold?: HouseholdFieldsFragment | null + ) => void; receivingHousehold: HouseholdFieldsFragment; projectId: string; projectName: string; @@ -27,6 +31,7 @@ interface Props { const JoinHouseholdDialog = ({ open, onClose, + onComplete, conflictingEnrollmentId, receivingHousehold, projectId, @@ -116,6 +121,10 @@ const JoinHouseholdDialog = ({ setRemainingHousehold( data.joinHouseholds.donorHousehold || undefined ); + onComplete?.( + data.joinHouseholds.receivingHousehold, + data.joinHouseholds.donorHousehold + ); } }, }); From daabab9ac9f73196b7383dc10c6d5730e0935e4c Mon Sep 17 00:00:00 2001 From: martha Date: Fri, 17 Jan 2025 11:40:01 -0500 Subject: [PATCH 09/23] Fix some todos, wip --- src/components/elements/table/GenericTable.tsx | 3 ++- .../components/elements/ConflictingEnrollmentAlert.tsx | 1 + .../components/elements/JoinHouseholdSelectClients.tsx | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/elements/table/GenericTable.tsx b/src/components/elements/table/GenericTable.tsx index 6bed86971..bb3e109bc 100644 --- a/src/components/elements/table/GenericTable.tsx +++ b/src/components/elements/table/GenericTable.tsx @@ -310,7 +310,8 @@ const GenericTable = ({ } checked={ selectableRowIds.length > 0 && - selectedValue.length === selectableRowIds.length + // >= instead of === accommodates disabling de-selection on rows that are selected + selectedValue.length >= selectableRowIds.length } disabled={selectableRowIds.length === 0} onChange={handleSelectAllClick} diff --git a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx index 38e295ee5..5e773f7fd 100644 --- a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx +++ b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx @@ -22,6 +22,7 @@ const ConflictingEnrollmentAlert = ({ {clientBriefName(joiningClient)} has another enrollment in this project that conflicts with this entry date. You have two options: + {/*todo @martha - add back, see designg*/} - - View Conflicting Enrollment - - + {joiningClientName} has another enrollment in this project that conflicts + with this enrollment. You have two options: + + + {joiningClientName}’s enrollment can be joined with {hohName}’s + household. + + + To retain {joiningClientName}’s enrollment, you must first edit the + entry and/or exit dates so that it does not conflict, before + re-enrolling. + + + + + + View Conflicting Enrollment + ); diff --git a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx index 38ac25318..58d9b79fc 100644 --- a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx +++ b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx @@ -47,7 +47,7 @@ const JoinHouseholdAddRelationships = ({ Update joining clients' relationships{' '} {receivingHohName && <>to {receivingHohName}} - {/* todo @martha - add warning here about entry dates. see design ? */} + {/* todo @martha - add warning here about entry dates, pending conversation with design */} diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index 18de4cb63..48cc81cf8 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -138,7 +138,7 @@ const JoinHouseholdDialog = ({ receivingHouseholdId: receivingHousehold.id, joiningEnrollmentInputs: joiningClients.map((hc) => { return { - // todo @martha - need to use enrollmentLockVersion? + // todo @martha - discuss, should we use enrollmentLockVersion? enrollmentId: hc.enrollment.id, // `|| RelationshipToHoH.DataNotCollected` is to keep typescript happy; // thanks to the missingRelationshipsCount logic, we know the relationships will be non-null From 2feaf4f5507b78dacd7857ff50bf3e7b7bd14360 Mon Sep 17 00:00:00 2001 From: martha Date: Fri, 17 Jan 2025 12:13:49 -0500 Subject: [PATCH 11/23] Hide workflow if no permission --- .../components/CreateHouseholdPage.tsx | 3 +- .../components/EditHouseholdPage.tsx | 3 +- .../household/components/ManageHousehold.tsx | 14 ++-- .../elements/AddToHouseholdButton.tsx | 19 +++-- .../elements/ConflictingEnrollmentAlert.tsx | 71 ++++++++++++------- .../elements/JoinHouseholdDialog.tsx | 10 ++- .../elements/JoinHouseholdSuccess.tsx | 13 ++-- .../hooks/useAddToHouseholdColumns.tsx | 19 ++--- 8 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/modules/household/components/CreateHouseholdPage.tsx b/src/modules/household/components/CreateHouseholdPage.tsx index b8fc07783..da16e4ba1 100644 --- a/src/modules/household/components/CreateHouseholdPage.tsx +++ b/src/modules/household/components/CreateHouseholdPage.tsx @@ -27,8 +27,7 @@ const CreateHouseholdPage = () => { <> ( { diff --git a/src/modules/household/components/EditHouseholdPage.tsx b/src/modules/household/components/EditHouseholdPage.tsx index e894ca12d..b5c41ef7e 100644 --- a/src/modules/household/components/EditHouseholdPage.tsx +++ b/src/modules/household/components/EditHouseholdPage.tsx @@ -23,8 +23,7 @@ const EditHousehold = () => { ; BackButton?: ReactNode; renderBackButton?: (householdId?: string) => ReactNode; } const ManageHousehold = ({ householdId: initialHouseholdId, - projectId, - projectName, + project, BackButton, renderBackButton, }: Props) => { @@ -66,8 +65,7 @@ const ManageHousehold = ({ householdId, } = useAddToHouseholdColumns({ householdId: initialHouseholdId, - projectId, - projectName, + project, }); // Fetch members to show in "previously associated" table @@ -152,7 +150,7 @@ const ManageHousehold = ({ currentDashboardEnrollmentId={currentDashboardEnrollmentId} refetchHousehold={refetchHousehold} loading={loading} - projectId={projectId} + projectId={project.id} /> )} @@ -187,7 +185,7 @@ const ManageHousehold = ({ {hasSearched && ( diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index 30c2e1cec..1cbaafd55 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -15,6 +15,7 @@ import { useProjectCocsCountFromCache } from '@/modules/projects/hooks/useProjec import { ClientWithAlertFieldsFragment, HouseholdFieldsFragment, + ProjectAllFieldsFragment, RecordFormRole, SubmittedEnrollmentResultFieldsFragment, ValidationError, @@ -24,8 +25,7 @@ interface Props { client: ClientWithAlertFieldsFragment; isMember: boolean; householdId?: string; // if omitted, a new household will be created - projectId: string; - projectName: string; + project: Pick; onSuccess: (householdId: string) => void; household?: HouseholdFieldsFragment; } @@ -35,13 +35,12 @@ const AddToHouseholdButton = ({ isMember, householdId, onSuccess, - projectId, - projectName, + project, household, }: Props) => { const prevIsMember = usePrevious(isMember); const [added, setAdded] = useState(isMember); - const cocCount = useProjectCocsCountFromCache(projectId); + const cocCount = useProjectCocsCountFromCache(project.id); useEffect(() => { // If client was previously added but has since been removed @@ -66,8 +65,8 @@ const AddToHouseholdButton = ({ setAdded(true); onSuccess(data.householdId); }, - inputVariables: { projectId, clientId }, - pickListArgs: { projectId, householdId }, + inputVariables: { projectId: project.id, clientId }, + pickListArgs: { projectId: project.id, householdId }, localConstants: { householdId, projectCocCount: cocCount }, errorFilter: (error: ValidationError) => { // If there's an error about a conflicting enrollment, we will show the ConflictingEnrollmentAlert, @@ -85,7 +84,7 @@ const AddToHouseholdButton = ({ } }, }), - [projectId, clientId, householdId, cocCount, onSuccess] + [project.id, clientId, householdId, cocCount, onSuccess] ); const { openFormDialog, renderFormDialog, closeDialog } = @@ -150,6 +149,7 @@ const AddToHouseholdButton = ({ {clientAlertsComponent} {household && conflictingEnrollmentId && ( )} diff --git a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx index e09e8908b..c1195b84a 100644 --- a/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx +++ b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx @@ -1,6 +1,7 @@ import { Alert, AlertTitle, + Box, Button, List, ListItem, @@ -14,10 +15,12 @@ import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { ClientWithAlertFieldsFragment, HouseholdFieldsFragment, + ProjectAllFieldsFragment, RelationshipToHoH, } from '@/types/gqlTypes'; interface Props { + project: Pick; joiningClient: ClientWithAlertFieldsFragment; conflictingEnrollmentId: string; receivingHousehold: HouseholdFieldsFragment; @@ -25,11 +28,14 @@ interface Props { } const ConflictingEnrollmentAlert = ({ + project, joiningClient, receivingHousehold, conflictingEnrollmentId, onClickJoinEnrollment, }: Props) => { + const canSplitHouseholds = project.access.canSplitHouseholds; + const joiningClientName = clientBriefName(joiningClient); const hohName = useMemo(() => { @@ -43,33 +49,44 @@ const ConflictingEnrollmentAlert = ({ return ( Conflicting Enrollment - {joiningClientName} has another enrollment in this project that conflicts - with this enrollment. You have two options: - - - {joiningClientName}’s enrollment can be joined with {hohName}’s - household. - - - To retain {joiningClientName}’s enrollment, you must first edit the - entry and/or exit dates so that it does not conflict, before - re-enrolling. - - - - - - View Conflicting Enrollment - + + + {joiningClientName} has another enrollment in this project that + conflicts with this enrollment.{' '} + {canSplitHouseholds && ( + <> + You have two options: + + + {joiningClientName}’s enrollment can be joined with {hohName} + ’s household. + + + To retain {joiningClientName}’s enrollment, you must first + edit the entry and/or exit dates so that it does not conflict, + before re-enrolling. + + + + )} + + + {canSplitHouseholds && ( + + )} + + View Conflicting Enrollment + + ); diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index 48cc81cf8..0a7240b5b 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -10,6 +10,7 @@ import JoinHouseholdSuccess from '@/modules/household/components/elements/JoinHo import { HouseholdClientFieldsFragment, HouseholdFieldsFragment, + ProjectAllFieldsFragment, RelationshipToHoH, useGetEnrollmentWithHouseholdQuery, useJoinHouseholdsMutation, @@ -24,8 +25,7 @@ interface Props { remainingHousehold?: HouseholdFieldsFragment | null ) => void; receivingHousehold: HouseholdFieldsFragment; - projectId: string; - projectName: string; + project: Pick; } const JoinHouseholdDialog = ({ @@ -34,8 +34,7 @@ const JoinHouseholdDialog = ({ onComplete, conflictingEnrollmentId, receivingHousehold, - projectId, - projectName, + project, }: Props) => { // Data this join workflow is collecting: What clients are joining and what their relationships are const [joiningClients, setJoiningClients] = useState< @@ -246,8 +245,7 @@ const JoinHouseholdDialog = ({ receivingHohName={receivingHohName} joinedClients={joiningClients} remainingHousehold={remainingHousehold} - projectId={projectId} - projectName={projectName} + project={project} onClose={onClose} /> ) diff --git a/src/modules/household/components/elements/JoinHouseholdSuccess.tsx b/src/modules/household/components/elements/JoinHouseholdSuccess.tsx index 5d0ac2337..f96a842eb 100644 --- a/src/modules/household/components/elements/JoinHouseholdSuccess.tsx +++ b/src/modules/household/components/elements/JoinHouseholdSuccess.tsx @@ -14,14 +14,14 @@ import { import { HouseholdClientFieldsFragment, HouseholdFieldsFragment, + ProjectAllFieldsFragment, } from '@/types/gqlTypes'; interface Props { receivingHohName: string; joinedClients: HouseholdClientFieldsFragment[]; remainingHousehold?: HouseholdFieldsFragment; - projectId: string; - projectName: string; + project: Pick; onClose: VoidFunction; } @@ -29,8 +29,7 @@ const JoinHouseholdSuccess = ({ receivingHohName, joinedClients, remainingHousehold, - projectId, - projectName, + project, onClose, }: Props) => { const remainingHouseholdClient = findHohOrRep( @@ -46,7 +45,7 @@ const JoinHouseholdSuccess = ({ Successful Join {stringifyHousehold(joinedClients)}{' '} {joinedClients.length > 1 ? 'have' : 'has'} been successfully joined to{' '} - {receivingHohName}’s Enrollment at {projectName} + {receivingHohName}’s Enrollment at {project.projectName} {disabled && disabledReason ? ( - + {primaryAction} ) : ( diff --git a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx index 82655c1f7..c4e88eb8f 100644 --- a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx +++ b/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx @@ -45,7 +45,7 @@ const JoinHouseholdAddRelationships = ({ Add Relationships - Update joining clients' relationships{' '} + Update joining clients’ relationships{' '} {receivingHohName && <>to {receivingHohName}} {/* todo @martha - add warning here about entry dates, pending conversation with design */} diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index aa3c9d857..30a8d33a2 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -105,7 +105,7 @@ const JoinHouseholdDialog = ({ disabled: missingRelationshipsCount > 0, disabledReason: missingRelationshipsCount > 0 - ? `Missing ${missingRelationshipsCount} required fields` + ? `Required fields missing (${missingRelationshipsCount})` : undefined, }; }, [missingRelationshipsCount]); From 1ba0159017f90f2e0ff10682f11ed4c169cc8bb4 Mon Sep 17 00:00:00 2001 From: martha Date: Wed, 22 Jan 2025 15:09:57 -0500 Subject: [PATCH 21/23] Renames --- graphql.schema.json | 106 +++++++++--------- .../operations/household.operations.graphql | 4 +- .../elements/AddToHouseholdButton.tsx | 8 +- .../elements/JoinHouseholdDialog.tsx | 16 ++- src/types/gqlObjects.ts | 46 ++++---- src/types/gqlTypes.ts | 88 +++++++-------- 6 files changed, 133 insertions(+), 135 deletions(-) diff --git a/graphql.schema.json b/graphql.schema.json index 276650fc5..59315a380 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -17990,6 +17990,50 @@ ], "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "EnrollmentRelationshipInput", + "description": null, + "isOneOf": false, + "fields": null, + "inputFields": [ + { + "name": "enrollmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relationshipToHoh", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "RelationshipToHoH", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "EnrollmentSortOption", @@ -28019,8 +28063,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "JoinHouseholdsInput", - "description": "Autogenerated input type of JoinHouseholds", + "name": "JoinHouseholdInput", + "description": "Autogenerated input type of JoinHousehold", "isOneOf": false, "fields": null, "inputFields": [ @@ -28050,7 +28094,7 @@ "name": null, "ofType": { "kind": "INPUT_OBJECT", - "name": "JoiningEnrollmentInput", + "name": "EnrollmentRelationshipInput", "ofType": null } } @@ -28083,8 +28127,8 @@ }, { "kind": "OBJECT", - "name": "JoinHouseholdsPayload", - "description": "Autogenerated return type of JoinHouseholds.", + "name": "JoinHouseholdPayload", + "description": "Autogenerated return type of JoinHousehold.", "isOneOf": null, "fields": [ { @@ -28157,50 +28201,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "INPUT_OBJECT", - "name": "JoiningEnrollmentInput", - "description": null, - "isOneOf": false, - "fields": null, - "inputFields": [ - { - "name": "enrollmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "relationshipToHoh", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "RelationshipToHoH", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "SCALAR", "name": "JsonObject", @@ -30650,18 +30650,18 @@ "deprecationReason": null }, { - "name": "joinHouseholds", + "name": "joinHousehold", "description": null, "args": [ { "name": "input", - "description": "Parameters for JoinHouseholds", + "description": "Parameters for JoinHousehold", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "INPUT_OBJECT", - "name": "JoinHouseholdsInput", + "name": "JoinHouseholdInput", "ofType": null } }, @@ -30672,7 +30672,7 @@ ], "type": { "kind": "OBJECT", - "name": "JoinHouseholdsPayload", + "name": "JoinHouseholdPayload", "ofType": null }, "isDeprecated": false, diff --git a/src/api/operations/household.operations.graphql b/src/api/operations/household.operations.graphql index 1f805f98a..e7b8c7397 100644 --- a/src/api/operations/household.operations.graphql +++ b/src/api/operations/household.operations.graphql @@ -1,5 +1,5 @@ -mutation JoinHouseholds($input: JoinHouseholdsInput!) { - joinHouseholds(input: $input) { +mutation JoinHousehold($input: JoinHouseholdInput!) { + joinHousehold(input: $input) { receivingHousehold { ...HouseholdFields } diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index 1cbaafd55..c5ce00f2d 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -102,12 +102,12 @@ const AddToHouseholdButton = ({ [clientAlerts] ); - const onCloseJoinHouseholds = useCallback(() => { + const onCloseJoinHousehold = useCallback(() => { setJoinHouseholdDialogOpen(false); setConflictingEnrollmentId(undefined); }, []); - const onCompleteJoinHouseholds = useCallback( + const onCompleteJoinHousehold = useCallback( (joinedHousehold: HouseholdFieldsFragment) => { // This only updates the initiating client's button if ( @@ -166,8 +166,8 @@ const AddToHouseholdButton = ({ diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/elements/JoinHouseholdDialog.tsx index 30a8d33a2..3df605eac 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -17,7 +17,7 @@ import { ProjectAllFieldsFragment, RelationshipToHoH, useGetEnrollmentWithHouseholdQuery, - useJoinHouseholdsMutation, + useJoinHouseholdMutation, } from '@/types/gqlTypes'; interface Props { @@ -117,16 +117,14 @@ const JoinHouseholdDialog = ({ HouseholdFieldsFragment | undefined >(undefined); const [joinHousehold, { loading: joinLoading, error: joinError }] = - useJoinHouseholdsMutation({ + useJoinHouseholdMutation({ onCompleted: (data) => { - if (data.joinHouseholds) { - setJoinedHousehold(data.joinHouseholds.receivingHousehold); - setRemainingHousehold( - data.joinHouseholds.donorHousehold || undefined - ); + if (data.joinHousehold) { + setJoinedHousehold(data.joinHousehold.receivingHousehold); + setRemainingHousehold(data.joinHousehold.donorHousehold || undefined); onComplete?.( - data.joinHouseholds.receivingHousehold, - data.joinHouseholds.donorHousehold + data.joinHousehold.receivingHousehold, + data.joinHousehold.donorHousehold ); } }, diff --git a/src/types/gqlObjects.ts b/src/types/gqlObjects.ts index 73f3edca5..794b27cbb 100644 --- a/src/types/gqlObjects.ts +++ b/src/types/gqlObjects.ts @@ -7059,6 +7059,27 @@ export const HmisInputObjectSchemas: GqlInputObjectSchema[] = [ }, ], }, + { + name: 'EnrollmentRelationshipInput', + args: [ + { + name: 'enrollmentId', + type: { + kind: 'NON_NULL', + name: null, + ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, + }, + }, + { + name: 'relationshipToHoh', + type: { + kind: 'NON_NULL', + name: null, + ofType: { kind: 'ENUM', name: 'RelationshipToHoH', ofType: null }, + }, + }, + ], + }, { name: 'EnrollmentsForClientFilterOptions', args: [ @@ -7402,7 +7423,7 @@ export const HmisInputObjectSchemas: GqlInputObjectSchema[] = [ ], }, { - name: 'JoinHouseholdsInput', + name: 'JoinHouseholdInput', args: [ { name: 'joiningEnrollmentInputs', @@ -7417,7 +7438,7 @@ export const HmisInputObjectSchemas: GqlInputObjectSchema[] = [ name: null, ofType: { kind: 'INPUT_OBJECT', - name: 'JoiningEnrollmentInput', + name: 'EnrollmentRelationshipInput', ofType: null, }, }, @@ -7434,27 +7455,6 @@ export const HmisInputObjectSchemas: GqlInputObjectSchema[] = [ }, ], }, - { - name: 'JoiningEnrollmentInput', - args: [ - { - name: 'enrollmentId', - type: { - kind: 'NON_NULL', - name: null, - ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, - }, - }, - { - name: 'relationshipToHoh', - type: { - kind: 'NON_NULL', - name: null, - ofType: { kind: 'ENUM', name: 'RelationshipToHoH', ofType: null }, - }, - }, - ], - }, { name: 'MciClearanceInput', args: [ diff --git a/src/types/gqlTypes.ts b/src/types/gqlTypes.ts index 242567f50..c54ef3484 100644 --- a/src/types/gqlTypes.ts +++ b/src/types/gqlTypes.ts @@ -2573,6 +2573,11 @@ export enum EnrollmentFilterOptionStatus { Incomplete = 'INCOMPLETE', } +export type EnrollmentRelationshipInput = { + enrollmentId: Scalars['ID']['input']; + relationshipToHoh: RelationshipToHoH; +}; + /** HUD Enrollment Sorting Options */ export enum EnrollmentSortOption { /** Age: Oldest to Youngest */ @@ -3934,17 +3939,17 @@ export enum ItemType { TimeOfDay = 'TIME_OF_DAY', } -/** Autogenerated input type of JoinHouseholds */ -export type JoinHouseholdsInput = { +/** Autogenerated input type of JoinHousehold */ +export type JoinHouseholdInput = { /** A unique identifier for the client performing the mutation. */ clientMutationId?: InputMaybe; - joiningEnrollmentInputs: Array; + joiningEnrollmentInputs: Array; receivingHouseholdId: Scalars['ID']['input']; }; -/** Autogenerated return type of JoinHouseholds. */ -export type JoinHouseholdsPayload = { - __typename?: 'JoinHouseholdsPayload'; +/** Autogenerated return type of JoinHousehold. */ +export type JoinHouseholdPayload = { + __typename?: 'JoinHouseholdPayload'; /** A unique identifier for the client performing the mutation. */ clientMutationId?: Maybe; donorHousehold?: Maybe; @@ -3952,11 +3957,6 @@ export type JoinHouseholdsPayload = { receivingHousehold: Household; }; -export type JoiningEnrollmentInput = { - enrollmentId: Scalars['ID']['input']; - relationshipToHoh: RelationshipToHoH; -}; - export type KeyValue = { __typename?: 'KeyValue'; key: Scalars['String']['output']; @@ -4217,7 +4217,7 @@ export type Mutation = { deleteService?: Maybe; deleteServiceType?: Maybe; deleteUnits?: Maybe; - joinHouseholds?: Maybe; + joinHousehold?: Maybe; mergeClients?: Maybe; publishFormDefinition?: Maybe; refreshExternalSubmissions?: Maybe; @@ -4420,8 +4420,8 @@ export type MutationDeleteUnitsArgs = { input: DeleteUnitsInput; }; -export type MutationJoinHouseholdsArgs = { - input: JoinHouseholdsInput; +export type MutationJoinHouseholdArgs = { + input: JoinHouseholdInput; }; export type MutationMergeClientsArgs = { @@ -33616,14 +33616,14 @@ export type ProjectEnrollmentsHouseholdClientFieldsFragment = { }; }; -export type JoinHouseholdsMutationVariables = Exact<{ - input: JoinHouseholdsInput; +export type JoinHouseholdMutationVariables = Exact<{ + input: JoinHouseholdInput; }>; -export type JoinHouseholdsMutation = { +export type JoinHouseholdMutation = { __typename?: 'Mutation'; - joinHouseholds?: { - __typename?: 'JoinHouseholdsPayload'; + joinHousehold?: { + __typename?: 'JoinHouseholdPayload'; receivingHousehold: { __typename?: 'Household'; id: string; @@ -49928,9 +49928,9 @@ export type GetEnrollmentGeolocationsQueryResult = Apollo.QueryResult< GetEnrollmentGeolocationsQuery, GetEnrollmentGeolocationsQueryVariables >; -export const JoinHouseholdsDocument = gql` - mutation JoinHouseholds($input: JoinHouseholdsInput!) { - joinHouseholds(input: $input) { +export const JoinHouseholdDocument = gql` + mutation JoinHousehold($input: JoinHouseholdInput!) { + joinHousehold(input: $input) { receivingHousehold { ...HouseholdFields } @@ -49941,48 +49941,48 @@ export const JoinHouseholdsDocument = gql` } ${HouseholdFieldsFragmentDoc} `; -export type JoinHouseholdsMutationFn = Apollo.MutationFunction< - JoinHouseholdsMutation, - JoinHouseholdsMutationVariables +export type JoinHouseholdMutationFn = Apollo.MutationFunction< + JoinHouseholdMutation, + JoinHouseholdMutationVariables >; /** - * __useJoinHouseholdsMutation__ + * __useJoinHouseholdMutation__ * - * To run a mutation, you first call `useJoinHouseholdsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useJoinHouseholdsMutation` returns a tuple that includes: + * To run a mutation, you first call `useJoinHouseholdMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useJoinHouseholdMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example - * const [joinHouseholdsMutation, { data, loading, error }] = useJoinHouseholdsMutation({ + * const [joinHouseholdMutation, { data, loading, error }] = useJoinHouseholdMutation({ * variables: { * input: // value for 'input' * }, * }); */ -export function useJoinHouseholdsMutation( +export function useJoinHouseholdMutation( baseOptions?: Apollo.MutationHookOptions< - JoinHouseholdsMutation, - JoinHouseholdsMutationVariables + JoinHouseholdMutation, + JoinHouseholdMutationVariables > ) { const options = { ...defaultOptions, ...baseOptions }; return Apollo.useMutation< - JoinHouseholdsMutation, - JoinHouseholdsMutationVariables - >(JoinHouseholdsDocument, options); -} -export type JoinHouseholdsMutationHookResult = ReturnType< - typeof useJoinHouseholdsMutation ->; -export type JoinHouseholdsMutationResult = - Apollo.MutationResult; -export type JoinHouseholdsMutationOptions = Apollo.BaseMutationOptions< - JoinHouseholdsMutation, - JoinHouseholdsMutationVariables + JoinHouseholdMutation, + JoinHouseholdMutationVariables + >(JoinHouseholdDocument, options); +} +export type JoinHouseholdMutationHookResult = ReturnType< + typeof useJoinHouseholdMutation +>; +export type JoinHouseholdMutationResult = + Apollo.MutationResult; +export type JoinHouseholdMutationOptions = Apollo.BaseMutationOptions< + JoinHouseholdMutation, + JoinHouseholdMutationVariables >; export const GetHouseholdDocument = gql` query GetHousehold($id: ID!) { From 01dd94c3ca72e9663a0e37f2685da10cc29d4a37 Mon Sep 17 00:00:00 2001 From: martha Date: Wed, 22 Jan 2025 15:20:04 -0500 Subject: [PATCH 22/23] Fix typescript from merge --- .../projects/components/tables/ProjectHouseholdsTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx index c47d6078e..76423dccb 100644 --- a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx @@ -179,6 +179,7 @@ const ProjectHouseholdsTable = ({ ].map(({ render, ariaLabel, ...rest }) => ({ ...rest, render: () => null, + tableCellProps: undefined, })); }, [staffAssignmentsEnabled]); From 0c799c7c2f1f8058893d1288022b5410c56f2d36 Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 27 Jan 2025 09:41:47 -0500 Subject: [PATCH 23/23] Refactor for reusability --- .../elements/AddToHouseholdButton.tsx | 2 +- .../elements/JoinHouseholdSelectClients.tsx | 163 ------------------ .../AddRelationshipsStep.tsx} | 39 ++--- .../JoinHouseholdDialog.tsx | 39 +++-- .../JoinHouseholdReview.tsx | 60 +++---- .../JoinHouseholdSelectClients.tsx | 117 +++++++++++++ .../householdActions/ReviewHouseholdsStep.tsx | 45 +++++ .../householdActions/SelectClientsStep.tsx | 87 ++++++++++ .../SuccessWayfindingStep.tsx} | 48 +++--- 9 files changed, 332 insertions(+), 268 deletions(-) delete mode 100644 src/modules/household/components/elements/JoinHouseholdSelectClients.tsx rename src/modules/household/components/{elements/JoinHouseholdAddRelationships.tsx => householdActions/AddRelationshipsStep.tsx} (79%) rename src/modules/household/components/{elements => householdActions}/JoinHouseholdDialog.tsx (84%) rename src/modules/household/components/{elements => householdActions}/JoinHouseholdReview.tsx (52%) create mode 100644 src/modules/household/components/householdActions/JoinHouseholdSelectClients.tsx create mode 100644 src/modules/household/components/householdActions/ReviewHouseholdsStep.tsx create mode 100644 src/modules/household/components/householdActions/SelectClientsStep.tsx rename src/modules/household/components/{elements/JoinHouseholdSuccess.tsx => householdActions/SuccessWayfindingStep.tsx} (50%) diff --git a/src/modules/household/components/elements/AddToHouseholdButton.tsx b/src/modules/household/components/elements/AddToHouseholdButton.tsx index c5ce00f2d..61143c69d 100644 --- a/src/modules/household/components/elements/AddToHouseholdButton.tsx +++ b/src/modules/household/components/elements/AddToHouseholdButton.tsx @@ -10,7 +10,7 @@ import { ErrorState } from '@/modules/errors/util'; import { useFormDialog } from '@/modules/form/hooks/useFormDialog'; import { clientBriefName } from '@/modules/hmis/hmisUtil'; import ConflictingEnrollmentAlert from '@/modules/household/components/elements/ConflictingEnrollmentAlert'; -import JoinHouseholdDialog from '@/modules/household/components/elements/JoinHouseholdDialog'; +import JoinHouseholdDialog from '@/modules/household/components/householdActions/JoinHouseholdDialog'; import { useProjectCocsCountFromCache } from '@/modules/projects/hooks/useProjectCocsCountFromCache'; import { ClientWithAlertFieldsFragment, diff --git a/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx b/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx deleted file mode 100644 index d837a4e33..000000000 --- a/src/modules/household/components/elements/JoinHouseholdSelectClients.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { InfoOutlined } from '@mui/icons-material'; -import { - Alert, - AlertTitle, - Box, - Paper, - Stack, - Typography, -} from '@mui/material'; - -import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; -import { generatePath } from 'react-router-dom'; -import ButtonLink from '@/components/elements/ButtonLink'; -import GenericTable from '@/components/elements/table/GenericTable'; -import { ColumnDef } from '@/components/elements/table/types'; -import ClientAlertStack from '@/modules/clientAlerts/components/ClientAlertStack'; -import useClientAlerts from '@/modules/clientAlerts/hooks/useClientAlerts'; -import { clientBriefName, sortHouseholdMembers } from '@/modules/hmis/hmisUtil'; -import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; -import { HOUSEHOLD_CLIENT_COLUMNS } from '@/modules/projects/components/tables/ProjectHouseholdsTable'; -import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; -import { - HouseholdClientFieldsFragment, - HouseholdFieldsFragment, - RelationshipToHoH, -} from '@/types/gqlTypes'; - -export const JOIN_HOUSEHOLD_COLUMNS: ColumnDef[] = - [ - { ...CLIENT_COLUMNS.name, sticky: 'left' }, - CLIENT_COLUMNS.age, - HOUSEHOLD_CLIENT_COLUMNS.relationship, - WITH_ENROLLMENT_COLUMNS.entryDate, - WITH_ENROLLMENT_COLUMNS.enrollmentStatus, - ]; - -interface Props { - donorHousehold: HouseholdFieldsFragment; - selectedClients: HouseholdClientFieldsFragment[]; - setSelectedClients: Dispatch>; - receivingHohName?: string; -} - -const JoinHouseholdSelectClients = ({ - donorHousehold, - selectedClients, - setSelectedClients, - receivingHohName, -}: Props) => { - const donorHoh = useMemo( - () => - donorHousehold.householdClients.find( - (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold - ), - [donorHousehold.householdClients] - ); - const donorHouseholdMembers = useMemo( - () => sortHouseholdMembers(donorHousehold.householdClients), - [donorHousehold.householdClients] - ); - - // Selection is controlled, so the selection state is stored in the parent. - // Here, translate the list of selected HouseholdClients from the parent to a list of row IDs for GenericTable. - const selectedClientIds = useMemo( - () => selectedClients.map((hc) => hc.id), - [selectedClients] - ); - - // .. and here, translate back again - const setSelectedClientIds = useCallback( - (clientIds: readonly string[]) => { - // If the HoH from the donor household is selected, all other hh members must be selected. - // (Can't leave behind a household without a HoH) - if (donorHoh && clientIds.includes(donorHoh.id)) { - setSelectedClients( - sortHouseholdMembers(donorHousehold.householdClients) - ); - } else { - setSelectedClients( - sortHouseholdMembers( - donorHousehold.householdClients.filter((hc) => - clientIds.includes(hc.id) - ) - ) - ); - } - }, - [donorHousehold.householdClients, donorHoh, setSelectedClients] - ); - - const isRowSelectable = useCallback( - (row: HouseholdClientFieldsFragment) => { - if (donorHoh && selectedClientIds.includes(donorHoh.id)) { - // Visually disable de-selecting other clients if the HoH is selected - // (This is also functionally disabled by the logic in setSelectedClientIds above) - return row.id === donorHoh.id; - } - - return true; - }, - [donorHoh, selectedClientIds] - ); - - const { clientAlerts } = useClientAlerts({ - household: donorHousehold, - showClientName: true, - }); - - return ( - - - Step 1 - Select Clients - - - Select which clients you would like to join{' '} - {receivingHohName ? `${receivingHohName}’s` : 'this'} household. - - {} - {donorHoh && - selectedClientIds.includes(donorHoh.id) && - donorHousehold.householdSize > 1 && ( - } - action={ - - Edit Household - - } - > - Head of Household Selected - If the current Head of Household is selected, all members must also - join. Edit {clientBriefName(donorHoh.client)}’s household and select - a different Head of Household if this is not your intention. - - )} - - - rows={donorHouseholdMembers} - columns={JOIN_HOUSEHOLD_COLUMNS} - selectable={'checkbox'} - selected={selectedClientIds} - onChangeSelectedRowIds={setSelectedClientIds} - isRowSelectable={isRowSelectable} - tableProps={{ - 'aria-label': 'Select Clients for Join', - }} - /> - - - ); -}; - -export default JoinHouseholdSelectClients; diff --git a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx b/src/modules/household/components/householdActions/AddRelationshipsStep.tsx similarity index 79% rename from src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx rename to src/modules/household/components/householdActions/AddRelationshipsStep.tsx index c4e88eb8f..6c8c7440d 100644 --- a/src/modules/household/components/elements/JoinHouseholdAddRelationships.tsx +++ b/src/modules/household/components/householdActions/AddRelationshipsStep.tsx @@ -1,10 +1,9 @@ import { Box, Chip, Paper, Stack, Typography } from '@mui/material'; -import React, { useId } from 'react'; +import React, { ReactNode, useId } from 'react'; import GenericTable from '@/components/elements/table/GenericTable'; import ClientName from '@/modules/client/components/ClientName'; import RequiredLabel from '@/modules/form/components/RequiredLabel'; import HmisEnum from '@/modules/hmis/components/HmisEnum'; -import { sortHouseholdMembers } from '@/modules/hmis/hmisUtil'; import RelationshipToHohSelect from '@/modules/household/components/elements/RelationshipToHohSelect'; import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { @@ -14,27 +13,28 @@ import { import { HmisEnums } from '@/types/gqlEnums'; import { HouseholdClientFieldsFragment, - HouseholdFieldsFragment, RelationshipToHoH, } from '@/types/gqlTypes'; interface Props { - joiningClients: HouseholdClientFieldsFragment[]; + existingClients: HouseholdClientFieldsFragment[]; + newClients: HouseholdClientFieldsFragment[]; relationships: Record; updateRelationship: ( enrollmentId: string, relationship: RelationshipToHoH | null ) => void; - receivingHousehold: HouseholdFieldsFragment; - receivingHohName?: string; + showNewIndicator?: boolean; + children?: ReactNode; } -const JoinHouseholdAddRelationships = ({ - joiningClients, +const AddRelationshipsStep = ({ + existingClients, + newClients, relationships, updateRelationship, - receivingHousehold, - receivingHohName, + showNewIndicator, + children, }: Props) => { const relationshipHeaderId = useId(); @@ -44,24 +44,17 @@ const JoinHouseholdAddRelationships = ({ Step 2 Add Relationships - - Update joining clients’ relationships{' '} - {receivingHohName && <>to {receivingHohName}} - {/* todo @martha - add warning here about entry dates, pending conversation with design */} - + {children} - rows={[ - ...sortHouseholdMembers(receivingHousehold.householdClients), - ...joiningClients, - ]} + rows={[...existingClients, ...newClients]} columns={[ { ...CLIENT_COLUMNS.name, render: (client) => ( - {joiningClients.includes(client) && ( + {showNewIndicator && newClients.includes(client) && ( )} @@ -89,7 +82,7 @@ const JoinHouseholdAddRelationships = ({ }, key: 'relationship', render: (hc: HouseholdClientFieldsFragment) => { - if (joiningClients.includes(hc)) { + if (newClients.includes(hc)) { return ( @@ -133,4 +126,4 @@ const JoinHouseholdAddRelationships = ({ ); }; -export default JoinHouseholdAddRelationships; +export default AddRelationshipsStep; diff --git a/src/modules/household/components/elements/JoinHouseholdDialog.tsx b/src/modules/household/components/householdActions/JoinHouseholdDialog.tsx similarity index 84% rename from src/modules/household/components/elements/JoinHouseholdDialog.tsx rename to src/modules/household/components/householdActions/JoinHouseholdDialog.tsx index 3df605eac..ee8f245e0 100644 --- a/src/modules/household/components/elements/JoinHouseholdDialog.tsx +++ b/src/modules/household/components/householdActions/JoinHouseholdDialog.tsx @@ -1,16 +1,18 @@ import { MergeTypeRounded } from '@mui/icons-material'; -import { useCallback, useMemo, useState } from 'react'; +import { Typography } from '@mui/material'; +import React, { useCallback, useMemo, useState } from 'react'; import Loading from '@/components/elements/Loading'; import StepDialog, { TabDefinition } from '@/components/elements/StepDialog'; import { clientBriefName, findHohOrRep, sortHouseholdMembers, + stringifyHousehold, } from '@/modules/hmis/hmisUtil'; -import JoinHouseholdAddRelationships from '@/modules/household/components/elements/JoinHouseholdAddRelationships'; -import JoinHouseholdReview from '@/modules/household/components/elements/JoinHouseholdReview'; -import JoinHouseholdSelectClients from '@/modules/household/components/elements/JoinHouseholdSelectClients'; -import JoinHouseholdSuccess from '@/modules/household/components/elements/JoinHouseholdSuccess'; +import AddRelationshipsStep from '@/modules/household/components/householdActions/AddRelationshipsStep'; +import JoinHouseholdReview from '@/modules/household/components/householdActions/JoinHouseholdReview'; +import JoinHouseholdSelectClients from '@/modules/household/components/householdActions/JoinHouseholdSelectClients'; +import SuccessWayfindingStep from '@/modules/household/components/householdActions/SuccessWayfindingStep'; import { HouseholdClientFieldsFragment, HouseholdFieldsFragment, @@ -183,8 +185,12 @@ const JoinHouseholdDialog = ({ { title: 'Add Relationships', content: ( - { setRelationships((prev) => { @@ -194,9 +200,13 @@ const JoinHouseholdDialog = ({ }; }); }} - receivingHousehold={receivingHousehold} - receivingHohName={receivingHohName} - /> + > + + Update joining clients’ relationships{' '} + {receivingHohName && <>to {receivingHohName}} + {/* todo @martha - add warning here about entry dates, pending conversation with design */} + + ), FormDialogActionProps: missingRelationshipsProps, }, @@ -243,10 +253,11 @@ const JoinHouseholdDialog = ({ }} successContent={ joinedHousehold && ( - 1 ? 'have' : 'has'} been successfully joined to ${receivingHohName}’s Enrollment at ${project.projectName}`} + primaryClientName={receivingHohName} + secondary={findHohOrRep(remainingHousehold?.householdClients || [])} project={project} onClose={onClose} /> diff --git a/src/modules/household/components/elements/JoinHouseholdReview.tsx b/src/modules/household/components/householdActions/JoinHouseholdReview.tsx similarity index 52% rename from src/modules/household/components/elements/JoinHouseholdReview.tsx rename to src/modules/household/components/householdActions/JoinHouseholdReview.tsx index 6d4088fe4..8301ffd0a 100644 --- a/src/modules/household/components/elements/JoinHouseholdReview.tsx +++ b/src/modules/household/components/householdActions/JoinHouseholdReview.tsx @@ -1,11 +1,10 @@ -import { Box, Paper, Stack, Typography } from '@mui/material'; +import { Typography } from '@mui/material'; import { useMemo } from 'react'; -import GenericTable from '@/components/elements/table/GenericTable'; import { sortHouseholdMembers, stringifyHousehold, } from '@/modules/hmis/hmisUtil'; -import { JOIN_HOUSEHOLD_COLUMNS } from '@/modules/household/components/elements/JoinHouseholdSelectClients'; +import ReviewHouseholdsStep from '@/modules/household/components/householdActions/ReviewHouseholdsStep'; import { HouseholdClientFieldsFragment, HouseholdFieldsFragment, @@ -53,46 +52,29 @@ const JoinHouseholdReview = ({ ); return ( - - - Step 3 - Review Join - + Check that the joined and remaining household members and details are correct - Joining Household - - The household that {joining} will join - - - - rows={joinedHouseholdClients} - columns={JOIN_HOUSEHOLD_COLUMNS} - tableProps={{ - 'aria-label': 'Review Joined Household', - }} - /> - - {remainingHouseholdClients.length > 0 && ( - <> - Remaining Household - - The household that {joining} will leave - - - - rows={remainingHouseholdClients} - columns={JOIN_HOUSEHOLD_COLUMNS} - tableProps={{ - 'aria-label': 'Review Remaining Household', - }} - /> - - - )} - + ); }; diff --git a/src/modules/household/components/householdActions/JoinHouseholdSelectClients.tsx b/src/modules/household/components/householdActions/JoinHouseholdSelectClients.tsx new file mode 100644 index 000000000..73fffde49 --- /dev/null +++ b/src/modules/household/components/householdActions/JoinHouseholdSelectClients.tsx @@ -0,0 +1,117 @@ +import { InfoOutlined } from '@mui/icons-material'; +import { Alert, AlertTitle, Typography } from '@mui/material'; + +import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; +import { generatePath } from 'react-router-dom'; +import ButtonLink from '@/components/elements/ButtonLink'; +import ClientAlertStack from '@/modules/clientAlerts/components/ClientAlertStack'; +import useClientAlerts from '@/modules/clientAlerts/hooks/useClientAlerts'; +import { clientBriefName, sortHouseholdMembers } from '@/modules/hmis/hmisUtil'; +import SelectClientsStep from '@/modules/household/components/householdActions/SelectClientsStep'; +import { EnrollmentDashboardRoutes } from '@/routes/routes'; +import { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, +} from '@/types/gqlTypes'; + +interface Props { + donorHousehold: HouseholdFieldsFragment; + selectedClients: HouseholdClientFieldsFragment[]; + setSelectedClients: Dispatch>; + receivingHohName?: string; +} + +const JoinHouseholdSelectClients = ({ + donorHousehold, + selectedClients, + setSelectedClients: setSelectedClientsProp, + receivingHohName, +}: Props) => { + const donorHoh = useMemo( + () => + donorHousehold.householdClients.find( + (hc) => hc.relationshipToHoH === RelationshipToHoH.SelfHeadOfHousehold + ), + [donorHousehold.householdClients] + ); + + const isHohSelected = useMemo(() => { + return donorHoh && selectedClients.includes(donorHoh); + }, [donorHoh, selectedClients]); + + // This is a controlled component, so the selected clients state is stored a level above, + // but here hijack the selection logic in order to select *all* members when HoH is selected. + // (Can't leave behind a household without a HoH) + const setSelectedClients = useCallback( + (clients: HouseholdClientFieldsFragment[]) => { + // If the HoH from the donor household is selected, all other hh members must be selected. + if (donorHoh && clients.includes(donorHoh)) { + setSelectedClientsProp( + sortHouseholdMembers(donorHousehold.householdClients) + ); + } else { + setSelectedClientsProp(clients); + } + }, + [donorHoh, setSelectedClientsProp, donorHousehold.householdClients] + ); + + const isRowSelectable = useCallback( + (row: HouseholdClientFieldsFragment) => { + if (donorHoh && isHohSelected) { + // Visually disable de-selecting other clients if the HoH is selected + // (This is also functionally disabled by the logic in setSelectedClientIds above) + return row.id === donorHoh.id; + } + + return true; + }, + [donorHoh, isHohSelected] + ); + + const { clientAlerts } = useClientAlerts({ + household: donorHousehold, + showClientName: true, + }); + + return ( + + + Select which clients you would like to join{' '} + {receivingHohName ? `${receivingHohName}’s` : 'this'} household. + + {} + {donorHoh && isHohSelected && donorHousehold.householdSize > 1 && ( + } + action={ + + Edit Household + + } + > + Head of Household Selected + If the current Head of Household is selected, all members must also + join. Edit {clientBriefName(donorHoh.client)}’s household and select a + different Head of Household if this is not your intention. + + )} + + ); +}; + +export default JoinHouseholdSelectClients; diff --git a/src/modules/household/components/householdActions/ReviewHouseholdsStep.tsx b/src/modules/household/components/householdActions/ReviewHouseholdsStep.tsx new file mode 100644 index 000000000..4e665a2b7 --- /dev/null +++ b/src/modules/household/components/householdActions/ReviewHouseholdsStep.tsx @@ -0,0 +1,45 @@ +import { Box, Paper, Stack, Typography } from '@mui/material'; +import { ReactNode } from 'react'; +import GenericTable from '@/components/elements/table/GenericTable'; +import { MANAGE_HOUSEHOLD_COLUMNS } from '@/modules/household/components/householdActions/SelectClientsStep'; +import { HouseholdClientFieldsFragment } from '@/types/gqlTypes'; + +type ReviewableHousehold = { + title: string; + description: string; + members: HouseholdClientFieldsFragment[]; +}; + +interface Props { + reviewableHouseholds: ReviewableHousehold[]; + children?: ReactNode; +} + +const ReviewHouseholdsStep = ({ reviewableHouseholds, children }: Props) => { + return ( + + + Step 3 + Review Join + + {children} + {reviewableHouseholds.map((household) => { + return ( + <> + {household.title} + {household.description} + + + rows={household.members} + columns={MANAGE_HOUSEHOLD_COLUMNS} + tableProps={{ 'aria-label': household.title }} + /> + + + ); + })} + + ); +}; + +export default ReviewHouseholdsStep; diff --git a/src/modules/household/components/householdActions/SelectClientsStep.tsx b/src/modules/household/components/householdActions/SelectClientsStep.tsx new file mode 100644 index 000000000..816292e4f --- /dev/null +++ b/src/modules/household/components/householdActions/SelectClientsStep.tsx @@ -0,0 +1,87 @@ +import { Box, Paper, Stack, Typography } from '@mui/material'; + +import { ReactNode, useCallback, useMemo } from 'react'; +import GenericTable from '@/components/elements/table/GenericTable'; +import { ColumnDef } from '@/components/elements/table/types'; +import { sortHouseholdMembers } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { HOUSEHOLD_CLIENT_COLUMNS } from '@/modules/projects/components/tables/ProjectHouseholdsTable'; +import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; +import { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, +} from '@/types/gqlTypes'; + +export const MANAGE_HOUSEHOLD_COLUMNS: ColumnDef[] = + [ + { ...CLIENT_COLUMNS.name, sticky: 'left' }, + CLIENT_COLUMNS.age, + HOUSEHOLD_CLIENT_COLUMNS.relationship, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.enrollmentStatus, + ]; + +interface Props { + donorHousehold: HouseholdFieldsFragment; + selectedClients: HouseholdClientFieldsFragment[]; + setSelectedClients: (clients: HouseholdClientFieldsFragment[]) => void; + children?: ReactNode; + isRowSelectable?: (client: HouseholdClientFieldsFragment) => boolean; +} + +const SelectClientsStep = ({ + donorHousehold, + selectedClients, + setSelectedClients, + isRowSelectable, + children, +}: Props) => { + const donorHouseholdMembers = useMemo( + () => sortHouseholdMembers(donorHousehold.householdClients), + [donorHousehold.householdClients] + ); + + // Selection is controlled, so the selection state is stored in the parent. + // Here, translate the list of selected HouseholdClients from the parent to a list of row IDs for GenericTable. + const selectedClientIds = useMemo( + () => selectedClients.map((hc) => hc.id), + [selectedClients] + ); + + // .. and here, translate back again + const setSelectedClientIds = useCallback( + (clientIds: readonly string[]) => { + setSelectedClients( + sortHouseholdMembers( + donorHousehold.householdClients.filter((hc) => + clientIds.includes(hc.id) + ) + ) + ); + }, + [donorHousehold.householdClients, setSelectedClients] + ); + + return ( + + + Step 1 + Select Clients + + {children} + + + rows={donorHouseholdMembers} + columns={MANAGE_HOUSEHOLD_COLUMNS} + selectable={'checkbox'} + selected={selectedClientIds} + onChangeSelectedRowIds={setSelectedClientIds} + isRowSelectable={isRowSelectable} + tableProps={{ 'aria-label': 'Select Clients' }} + /> + + + ); +}; + +export default SelectClientsStep; diff --git a/src/modules/household/components/elements/JoinHouseholdSuccess.tsx b/src/modules/household/components/householdActions/SuccessWayfindingStep.tsx similarity index 50% rename from src/modules/household/components/elements/JoinHouseholdSuccess.tsx rename to src/modules/household/components/householdActions/SuccessWayfindingStep.tsx index f96a842eb..a4f712a41 100644 --- a/src/modules/household/components/elements/JoinHouseholdSuccess.tsx +++ b/src/modules/household/components/householdActions/SuccessWayfindingStep.tsx @@ -2,63 +2,55 @@ import { Alert, AlertTitle, Button, Stack } from '@mui/material'; import React from 'react'; import { generatePath } from 'react-router-dom'; import ButtonLink from '@/components/elements/ButtonLink'; -import { - clientBriefName, - findHohOrRep, - stringifyHousehold, -} from '@/modules/hmis/hmisUtil'; +import { clientBriefName } from '@/modules/hmis/hmisUtil'; import { EnrollmentDashboardRoutes, ProjectDashboardRoutes, } from '@/routes/routes'; import { HouseholdClientFieldsFragment, - HouseholdFieldsFragment, ProjectAllFieldsFragment, } from '@/types/gqlTypes'; interface Props { - receivingHohName: string; - joinedClients: HouseholdClientFieldsFragment[]; - remainingHousehold?: HouseholdFieldsFragment; + title: string; + description: string; + primaryClientName: string; + secondary?: HouseholdClientFieldsFragment; project: Pick; onClose: VoidFunction; } -const JoinHouseholdSuccess = ({ - receivingHohName, - joinedClients, - remainingHousehold, +const SuccessWayfindingStep = ({ + title, + description, + primaryClientName, + secondary, project, onClose, }: Props) => { - const remainingHouseholdClient = findHohOrRep( - remainingHousehold?.householdClients || [] - ); - const remainingName = remainingHouseholdClient - ? clientBriefName(remainingHouseholdClient.client) + const secondaryName = secondary + ? clientBriefName(secondary.client) : undefined; return ( - Successful Join - {stringifyHousehold(joinedClients)}{' '} - {joinedClients.length > 1 ? 'have' : 'has'} been successfully joined to{' '} - {receivingHohName}’s Enrollment at {project.projectName} + {title} + {description} - {remainingHousehold && remainingHouseholdClient && ( + {secondary && ( - View {remainingName}’s Enrollment + View {secondaryName}’s Enrollment )}