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/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 b5dcacac2..6bed86971 100644 --- a/src/components/elements/table/GenericTable.tsx +++ b/src/components/elements/table/GenericTable.tsx @@ -62,6 +62,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< @@ -192,6 +193,7 @@ const GenericTable = ({ headerCellSx, selectable, isRowSelectable, + selected: selectedProp, onChangeSelectedRowIds, EnhancedTableToolbarProps, filterToolbar, @@ -209,6 +211,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 []; @@ -219,30 +226,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 ; @@ -288,12 +305,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} @@ -358,7 +375,7 @@ const GenericTable = ({ {EnhancedTableToolbarProps && ( )} @@ -425,7 +442,7 @@ const GenericTable = ({ onClickHandler ? onClickHandler(row) : undefined } selected={ - selectable === 'row' && includes(selected, row.id) + selectable === 'row' && includes(selectedValue, row.id) } onKeyUp={ !!handleRowClick @@ -447,7 +464,7 @@ const GenericTable = ({ { - 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 ea459643c..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,6 +79,7 @@ export interface DynamicFormProps localConstants?: LocalConstants; errorRef?: RefObject; variant?: 'standard' | 'without_top_level_cards'; + errorFilter?: ErrorFilterFn; } export interface DynamicFormRef { SaveIfDirty: VoidFunction; @@ -110,6 +111,7 @@ const DynamicForm = forwardRef( errorRef, onDirty, variant = 'standard', + errorFilter, }: DynamicFormProps, ref: Ref ) => { @@ -242,7 +244,11 @@ const DynamicForm = forwardRef( - + )} diff --git a/src/modules/form/hooks/useFormDialog.tsx b/src/modules/form/hooks/useFormDialog.tsx index 16ef3e02c..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,6 +59,8 @@ interface Args extends Omit, 'formDefinition'> { onClose?: VoidFunction; localDefinition?: FormDefinitionFieldsFragment; projectId?: string; // Project context for fetching form definition + onChangeErrors?: OnChangeErrorsFn; + errorFilter?: ErrorFilterFn; } export function useFormDialog({ onCompleted, @@ -59,6 +72,8 @@ export function useFormDialog({ localDefinition, pickListArgs, projectId, + onChangeErrors, + errorFilter, }: Args) { const errorRef = useRef(null); const [dialogOpen, setDialogOpen] = useState(false); @@ -116,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); @@ -185,6 +204,7 @@ export function useFormDialog({ }} variant={variant} hideSubmit + errorFilter={errorFilter} {...props} errorRef={errorRef} /> @@ -210,6 +230,7 @@ export function useFormDialog({ closeDialog, definitionLoading, dialogOpen, + errorFilter, errors, formDefinition, formRole, 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..a4fd34080 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'; @@ -6,13 +6,18 @@ 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 } 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, + ValidationError, } from '@/types/gqlTypes'; interface Props { @@ -21,6 +26,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 +35,7 @@ const AddToHouseholdButton = ({ householdId, onSuccess, projectId, + household, }: Props) => { const prevIsMember = usePrevious(isMember); const [added, setAdded] = useState(isMember); @@ -46,6 +53,10 @@ const AddToHouseholdButton = ({ if (added) text = 'Added'; const clientId = client.id; + const [conflictingEnrollmentId, setConflictingEnrollmentId] = useState< + string | undefined + >(); + const memoedArgs = useMemo( () => ({ formRole: RecordFormRole.Enrollment, @@ -56,13 +67,30 @@ const AddToHouseholdButton = ({ inputVariables: { projectId, clientId }, pickListArgs: { projectId, householdId }, localConstants: { householdId, projectCocCount: cocCount }, + 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); + } + }, }), [projectId, clientId, householdId, cocCount, onSuccess] ); - const { openFormDialog, renderFormDialog } = + const { openFormDialog, renderFormDialog, closeDialog } = useFormDialog(memoedArgs); + const [joinHouseholdDialogOpen, setJoinHouseholdDialogOpen] = useState(false); + const { clientAlerts } = useClientAlerts({ client: client }); return ( @@ -84,10 +112,34 @@ const AddToHouseholdButton = ({ {renderFormDialog({ title: <>Enroll {clientBriefName(client)}, submitButtonText: `Enroll`, - preFormComponent: clientAlerts.length > 0 && ( - + preFormComponent: ( + + {clientAlerts.length > 0 && ( + + )} + {household && conflictingEnrollmentId && ( + { + closeDialog(); + setJoinHouseholdDialogOpen(true); + }} + /> + )} + ), })} + {/*todo @martha - don't forget to put all household alerts in the new join dialog*/} + {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 new file mode 100644 index 000000000..0428751bb --- /dev/null +++ b/src/modules/household/components/elements/ConflictingEnrollmentAlert.tsx @@ -0,0 +1,78 @@ +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'; + +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 + + + + ); +}; + +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..7d1177ca5 --- /dev/null +++ b/src/modules/household/components/elements/JoinHouseholdDialog.tsx @@ -0,0 +1,168 @@ +import { Alert, AlertTitle, Button, Stack } from '@mui/material'; +import { useCallback, useMemo, useState } from 'react'; +import Loading from '@/components/elements/Loading'; +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 { + HouseholdClientFieldsFragment, + HouseholdFieldsFragment, + RelationshipToHoH, + useGetEnrollmentWithHouseholdQuery, + useJoinHouseholdsMutation, +} from '@/types/gqlTypes'; + +interface Props { + open: boolean; + conflictingEnrollmentId: string; + onClose: VoidFunction; + receivingHousehold: HouseholdFieldsFragment; +} + +const JoinHouseholdDialog = ({ + open, + onClose, + conflictingEnrollmentId, + receivingHousehold, +}: Props) => { + // todo @martha - need to use enrollmentLockVersion? + const { + data: { enrollment: conflictingEnrollment } = {}, + loading, + error, + } = useGetEnrollmentWithHouseholdQuery({ + variables: { + id: conflictingEnrollmentId, + }, + }); + 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 initiatorClientName = initiator?.client + ? clientBriefName(initiator.client) + : ''; + + const [joiningClients, setJoiningClients] = useState< + HouseholdClientFieldsFragment[] + >([]); // 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< + 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]); + + if (error) throw error; + + if (!conflictingEnrollment) return ; + + 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 c6739c6ef..21ac5416b 100644 --- a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx @@ -41,7 +41,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, sticky: 'left' }, CLIENT_COLUMNS.age, { @@ -100,7 +100,7 @@ const ProjectHouseholdsClientRow: React.FC = ({ return ( - {BASE_COLUMNS.map((col, i) => ( + {PROJECT_HOUSEHOLD_COLUMNS.map((col, i) => ( {renderCellContents(householdClient, col.render)} @@ -156,7 +156,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, ariaLabel: undefined }] : []), // typescript appeasement 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..179ff6c05 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 = { @@ -21219,6 +21249,7 @@ export type AllEnrollmentDetailsFragment = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; projectCocs: { __typename?: 'ProjectCocsPaginated'; nodesCount: number }; }; @@ -21587,6 +21618,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 +22726,7 @@ export type GetEnrollmentDetailsQuery = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; projectCocs: { __typename?: 'ProjectCocsPaginated'; nodesCount: number }; }; @@ -22628,6 +22787,7 @@ export type GetEnrollmentWithHouseholdQuery = { household: { __typename?: 'Household'; id: string; + householdSize: number; shortId: string; householdClients: Array<{ __typename?: 'HouseholdClient'; @@ -31211,6 +31371,7 @@ export type SubmitFormMutation = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; user?: { __typename: 'ApplicationUser'; @@ -33455,6 +33616,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 +34792,7 @@ export type ProjectAllFieldsFragment = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; user?: { __typename: 'ApplicationUser'; @@ -35448,6 +35710,7 @@ export type GetProjectQuery = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; user?: { __typename: 'ApplicationUser'; @@ -35633,6 +35896,7 @@ export type GetProjectPermissionsQuery = { canManageIncomingReferrals: boolean; canManageOutgoingReferrals: boolean; canManageExternalFormSubmissions: boolean; + canSplitHouseholds: boolean; }; } | null; }; @@ -41344,6 +41608,7 @@ export const ProjectAccessFieldsFragmentDoc = gql` canManageIncomingReferrals canManageOutgoingReferrals canManageExternalFormSubmissions + canSplitHouseholds } `; export const AllEnrollmentDetailsFragmentDoc = gql` @@ -41410,6 +41675,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 +41820,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 @@ -46882,18 +47157,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 +49839,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) {