From 4bb7e115d4d12be78fc4e51230efe1f6b424d45a Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 6 Jan 2025 16:59:52 -0500 Subject: [PATCH] Update Project Household Enrollments table with new designs (#989) * Update Project Enrollment tables * fix a11y * More accessibility fixes * Simplify and resolve todos * Add storybook story * Remove errant comment * Add comments * Update api of TableRowActions * Fix issue with fulColSpan * Right-align table row actions Co-authored-by: Gig * Remove comment * Replace overrideTableBody with more understandable API * Implement PR suggestion of moving table row to column def * Cleanup and add comments * Port over relevant changes from other PR * Refactor to use renderCellContents * Revert out-of-scope change * Add todo * Remove circular dependency * Clean up * Centralize action column base attrs * PR feedback * Fix typescript problems * Implement Table Actions pattern and default columns (#998) * Implement Table Actions pattern and default columns * Add random comment * Add storybook for table actions and fix cde story * Implement PR suggestion of moving table row to column def * add todo * Finish refactor to use column def instead of getTableAction * Fix bulk services loading * Provide a visually hidden header when header isnt provided * Keep existing behavior of individual/household assessment viewing * Fix typescript * Add accessible text for RelativeDateDisplay * implement one possible approach for client ID typescript * Remove unnecessary cast --------- Co-authored-by: Gig Ashton --- .storybook/preview.tsx | 12 +- src/components/elements/CommonMenuButton.tsx | 4 +- .../elements/DateWithRelativeTooltip.tsx | 63 ++++ .../elements}/RelativeDateDisplay.tsx | 35 +- .../elements/table/GenericTable.stories.tsx | 39 ++- .../elements/table/GenericTable.tsx | 65 ++-- .../elements/table/TableRowActions.tsx | 73 +++++ .../elements/table/tableRowActionUtil.tsx | 87 +++++ src/components/elements/table/types.tsx | 21 +- .../components/services/ServiceTypeTable.tsx | 29 +- .../admin/components/users/AdminUsers.tsx | 1 + .../pages/ClientAssessmentsPage.tsx | 62 ++-- src/modules/assessments/util.tsx | 44 +-- .../components/AssignServiceButton.tsx | 1 + .../components/BulkServicesTable.tsx | 86 ++--- .../caseNotes/components/ClientCaseNotes.tsx | 54 +-- .../components/EnrollmentCaseNotes.tsx | 58 +++- .../components/ClientEnrollmentCard.tsx | 19 +- .../components/ClientSearchResultCard.tsx | 2 +- .../components/ClientFilesPage.tsx | 80 +++-- .../EnrollmentAssessmentActionButtons.tsx | 2 + .../components/EnrollmentAssessmentsTable.tsx | 42 ++- .../components/HouseholdAssessmentsTable.tsx | 54 +-- .../pages/ClientEnrollmentsPage.tsx | 98 +++--- .../pages/EnrollmentCeAssessmentsPage.tsx | 52 ++- .../pages/EnrollmentCeEventsPage.tsx | 54 ++- .../EnrollmentCurrentLivingSituationsPage.tsx | 31 +- .../client/useRenderLastUpdated.tsx | 2 +- .../hmis/components/EnrollmentStatus.tsx | 9 +- .../hmis/components/HudRecordMetadata.tsx | 2 +- src/modules/hmis/hmisUtil.ts | 52 ++- .../components/HouseholdMemberTable.tsx | 31 -- src/modules/household/types.ts | 6 +- .../components/AllProjectsPage.tsx | 293 ++++++++++------- .../components/ProjectAssessments.tsx | 66 ++-- .../ProjectCurrentLivingSituations.tsx | 92 +++--- .../tables/ProjectClientEnrollmentsTable.tsx | 309 +++++++++--------- .../tables/ProjectEnrollmentsTable.tsx | 14 +- .../ProjectExternalSubmissionsTable.tsx | 25 +- .../tables/ProjectHouseholdsTable.stories.tsx | 26 ++ .../tables/ProjectHouseholdsTable.tsx | 290 ++++++++-------- .../search/components/ClientSearch.tsx | 89 ++--- .../components/ClientServicesPage.tsx | 66 ++-- .../components/EnrollmentServicesPage.tsx | 53 ++- .../components/ProjectServicesTable.tsx | 62 ++-- src/modules/services/serviceColumns.tsx | 25 +- src/test/__mocks__/requests.ts | 75 +++++ 47 files changed, 1687 insertions(+), 1068 deletions(-) create mode 100644 src/components/elements/DateWithRelativeTooltip.tsx rename src/{modules/hmis/components => components/elements}/RelativeDateDisplay.tsx (57%) create mode 100644 src/components/elements/table/TableRowActions.tsx create mode 100644 src/components/elements/table/tableRowActionUtil.tsx create mode 100644 src/modules/projects/components/tables/ProjectHouseholdsTable.stories.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f5f89cb3d..06e6e0c8d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -12,11 +12,13 @@ import { MemoryRouter } from 'react-router-dom'; import theme from '../src/config/theme'; import '../src/index.css'; import { + AMERICAN_LAKE_HOUSE, applicationUserMock, fakeEnrollment, RITA_ACKROYD, } from '../src/test/__mocks__/requests'; import { RenderRouteWithOutletContext } from './components/RenderRouteWithOutletContext'; +import { ProjectDashboardContext } from '../src/modules/projects/components/ProjectDashboard'; export const parameters = { layout: 'padded', @@ -40,7 +42,7 @@ export const decorators = [ // React Router decorator can optionally provide a dashboard context if `dashboardContext` is passed. // Caller can optionally specify the client/enrollemnt mocks that should be used in the context by // passing `client` or `enrollment` parameters. - const { dashboardContext, client, enrollment } = parameters; + const { dashboardContext, client, enrollment, project } = parameters; switch (dashboardContext) { case 'enrollment': return ( @@ -64,6 +66,14 @@ export const decorators = [ {Story()} ); + case 'project': + return ( + + context={{ project: project || AMERICAN_LAKE_HOUSE }} + > + {Story()} + + ); default: return {Story()}; } diff --git a/src/components/elements/CommonMenuButton.tsx b/src/components/elements/CommonMenuButton.tsx index e98d76040..a1bf8fc4a 100644 --- a/src/components/elements/CommonMenuButton.tsx +++ b/src/components/elements/CommonMenuButton.tsx @@ -13,15 +13,17 @@ import { To } from 'react-router-dom'; import RouterLink from './RouterLink'; import { MoreMenuIcon } from './SemanticIcons'; +import { LocationState } from '@/routes/routeUtil'; export type CommonMenuItem = { key: string; + title: ReactNode; to?: To; onClick?: VoidFunction; - title?: ReactNode; divider?: boolean; disabled?: boolean; ariaLabel?: string; + linkState?: LocationState; openInNew?: boolean; }; diff --git a/src/components/elements/DateWithRelativeTooltip.tsx b/src/components/elements/DateWithRelativeTooltip.tsx new file mode 100644 index 000000000..c63bf7130 --- /dev/null +++ b/src/components/elements/DateWithRelativeTooltip.tsx @@ -0,0 +1,63 @@ +import { + Box, + Tooltip, + TooltipProps, + Typography, + TypographyProps, +} from '@mui/material'; +import { visuallyHidden } from '@mui/utils'; +import { useMemo } from 'react'; + +import { getFormattedDates } from './RelativeDateDisplay'; + +export interface DateWithRelativeTooltipProps { + dateString: string; + preciseTime?: boolean; + TooltipProps?: Omit; + TypographyProps?: TypographyProps; +} + +/** + * Date with relative date as tooltip + */ +const DateWithRelativeTooltip = ({ + dateString, + preciseTime = false, + TooltipProps = {}, + TypographyProps = {}, +}: DateWithRelativeTooltipProps) => { + const [formattedDate, formattedDateRelative] = useMemo( + () => getFormattedDates(dateString, preciseTime), + [dateString, preciseTime] + ); + + if (!dateString || !formattedDate || !formattedDateRelative) return null; + + return ( + + {formattedDateRelative} + + } + arrow + {...TooltipProps} + > + + {formattedDate} + {/* Include the tooltip text as visually hidden for accessibility */} + , {formattedDateRelative} + + + ); +}; + +export default DateWithRelativeTooltip; diff --git a/src/modules/hmis/components/RelativeDateDisplay.tsx b/src/components/elements/RelativeDateDisplay.tsx similarity index 57% rename from src/modules/hmis/components/RelativeDateDisplay.tsx rename to src/components/elements/RelativeDateDisplay.tsx index af82443c2..81c7ce60b 100644 --- a/src/modules/hmis/components/RelativeDateDisplay.tsx +++ b/src/components/elements/RelativeDateDisplay.tsx @@ -1,21 +1,37 @@ import { + Box, Tooltip, TooltipProps, Typography, TypographyProps, } from '@mui/material'; +import { visuallyHidden } from '@mui/utils'; import { useMemo } from 'react'; import { + formatDateForDisplay, formatDateTimeForDisplay, formatRelativeDateTime, parseHmisDateString, } from '@/modules/hmis/hmisUtil'; +export const getFormattedDates = ( + dateString: string, + preciseTime: boolean = true +) => { + const date = parseHmisDateString(dateString); + if (!date) return []; + return [ + preciseTime ? formatDateTimeForDisplay(date) : formatDateForDisplay(date), + formatRelativeDateTime(date), + ]; +}; + export interface RelativeDateDisplayProps { dateString: string; prefixVerb?: string; suffixText?: string; + tooltipSuffixText?: string; TooltipProps?: Omit; TypographyProps?: TypographyProps; } @@ -27,14 +43,14 @@ const RelativeDateDisplay = ({ dateString, prefixVerb, suffixText, + tooltipSuffixText, TooltipProps = {}, TypographyProps = {}, }: RelativeDateDisplayProps) => { - const [formattedDate, formattedDateRelative] = useMemo(() => { - const date = parseHmisDateString(dateString); - if (!date) return []; - return [formatDateTimeForDisplay(date), formatRelativeDateTime(date)]; - }, [dateString]); + const [formattedDate, formattedDateRelative] = useMemo( + () => getFormattedDates(dateString), + [dateString] + ); if (!dateString || !formattedDate || !formattedDateRelative) return null; @@ -42,7 +58,7 @@ const RelativeDateDisplay = ({ - {formattedDate} + {formattedDate} {tooltipSuffixText} } arrow @@ -57,7 +73,12 @@ const RelativeDateDisplay = ({ ...TypographyProps.sx, }} > - {prefixVerb || null} {formattedDateRelative} {suffixText || null} + {/* Include the tooltip text as visually hidden for accessibility */} + {prefixVerb || null} {formattedDateRelative}{' '} + + ({formattedDate} {tooltipSuffixText}) + {' '} + {suffixText || null} ); diff --git a/src/components/elements/table/GenericTable.stories.tsx b/src/components/elements/table/GenericTable.stories.tsx index 6a437eba4..3af453f32 100644 --- a/src/components/elements/table/GenericTable.stories.tsx +++ b/src/components/elements/table/GenericTable.stories.tsx @@ -2,8 +2,13 @@ import { Box, Paper } from '@mui/material'; import { Meta, StoryFn } from '@storybook/react'; import GenericTable, { Props as GenericTableProps } from './GenericTable'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { SsnDobShowContextProvider } from '@/modules/client/providers/ClientSsnDobVisibility'; -import { getCustomDataElementColumns } from '@/modules/hmis/hmisUtil'; +import { + clientBriefName, + getCustomDataElementColumns, +} from '@/modules/hmis/hmisUtil'; import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; import { RITA_ACKROYD } from '@/test/__mocks__/requests'; import { ClientFieldsFragment, DisplayHook } from '@/types/gqlTypes'; @@ -30,7 +35,6 @@ const Template = ); const clientColumns = [ - CLIENT_COLUMNS.id, CLIENT_COLUMNS.first, CLIENT_COLUMNS.last, CLIENT_COLUMNS.ssn, @@ -136,8 +140,37 @@ const rowsWithCdes = [ WithCustomDataElements.args = { rows: rowsWithCdes, columns: [ - CLIENT_COLUMNS.id, CLIENT_COLUMNS.name, ...getCustomDataElementColumns(rowsWithCdes), ], }; + +export const WithTableRowActions = Template().bind({}); +WithTableRowActions.args = { + rows: fakeRows, + columns: [ + CLIENT_COLUMNS.name, + { + ...BASE_ACTION_COLUMN_DEF, + render: (record) => ( + + alert(`Hello, ${clientBriefName(record)} ${record.id}`), + }} + secondaryActionConfigs={[ + { + title: 'Navigate to a link', + key: 'link', + to: 'https://storybook.js.org/docs', // just link somewhere random to show `to` prop working + }, + ]} + /> + ), + }, + ], +}; diff --git a/src/components/elements/table/GenericTable.tsx b/src/components/elements/table/GenericTable.tsx index 80737caaf..93fb6d783 100644 --- a/src/components/elements/table/GenericTable.tsx +++ b/src/components/elements/table/GenericTable.tsx @@ -17,8 +17,16 @@ import { TableRow, Theme, } from '@mui/material'; -import { get, includes, isNil, without } from 'lodash-es'; -import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import { visuallyHidden } from '@mui/utils'; +import { compact, get, includes, isNil, without } from 'lodash-es'; +import { + ComponentType, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { To } from 'react-router-dom'; import Loading from '../Loading'; @@ -33,13 +41,11 @@ import { isRenderFunction, RenderFunction, } from './types'; -import { LocationState } from '@/routes/routeUtil'; export interface Props { rows: T[]; handleRowClick?: (row: T) => void; rowLinkTo?: (row: T) => To | null | undefined; - rowLinkState?: LocationState; columns?: ColumnDef[]; paginated?: boolean; loading?: boolean; @@ -62,8 +68,11 @@ export interface Props { >; filterToolbar?: ReactNode; noData?: ReactNode; - renderRow?: (row: T) => ReactNode; - condensed?: boolean; + // columnKeys contains the keys of columns currently rendered, so renderRow knows about which optional columns are shown/hidden. + renderRow?: (row: T, columnKeys: string[]) => ReactNode; + // TableBodyComponent can be overridden. This should only be used by tables that take over rendering using renderRow and render a `tbody` within their custom render fn + TableBodyComponent?: ComponentType | keyof JSX.IntrinsicElements; + belowRowsContent?: ReactNode; // component to insert below all rendered rows, above footer } const clickableRowStyles = { @@ -94,6 +103,18 @@ const HeaderCell = ({ ); +export const renderCellContents = ( + row: T, + render: ColumnDef['render'] +) => { + if (isRenderFunction(render)) return <>{render(row)}; + if (isPrimitive(render)) { + const val = get(row, render); + if (!isNil(val)) return <>{`${val}`}; + } + return null; +}; + const GenericTable = ({ rows, handleRowClick, @@ -118,8 +139,8 @@ const GenericTable = ({ renderRow, noData = 'No data', loadingVariant = 'circular', - condensed = false, - rowLinkState, + TableBodyComponent = TableBody, + belowRowsContent, }: Props) => { const columns = useMemo( () => (columnProp || []).filter((c) => !c.hide), @@ -168,15 +189,6 @@ const GenericTable = ({ if (loading && loadingVariant === 'circular') return ; - const renderCellContents = (row: T, render: ColumnDef['render']) => { - if (isRenderFunction(render)) return <>{render(row)}; - if (isPrimitive(render)) { - const val = get(row, render); - if (!isNil(val)) return <>{`${val}`}; - } - return null; - }; - const verticalCellSx = (idx: number): SxProps => ({ border: (theme: Theme) => `1px solid ${theme.palette.grey[200]}`, backgroundColor: (theme: Theme) => @@ -231,7 +243,12 @@ const GenericTable = ({ width: def.width, }} > - {def.header} + {def.header ? ( + {def.header} + ) : ( + // If header isn't provided, add a visually hidden header with the column key for accessibility + {def.key} + )} ))} @@ -284,7 +301,7 @@ const GenericTable = ({ {...tableProps} > {tableHead} - + {vertical && columns.map((def, i) => ( @@ -305,7 +322,9 @@ const GenericTable = ({ {!vertical && rows.map((row) => { // prop to completely take over row rendering - if (renderRow) return renderRow(row); + if (renderRow) { + return renderRow(row, compact(columns.map((c) => c.key))); + } const isSelectable = selectable && (isRowSelectable ? isRowSelectable(row) : true); @@ -405,7 +424,6 @@ const GenericTable = ({ {isLinked ? ( ({ height: '100%', alignItems: 'center', px: 2, - py: condensed ? 1 : 2, + py: 2, }} > {renderCellContents(row, render)} @@ -440,10 +458,11 @@ const GenericTable = ({ ); })} + {belowRowsContent} {actionRow} {/* dont show "no data" row if there is an action row, which may be for adding new elements or making another selection (MCI uses it) */} {!actionRow && noResultsRow} - + {paginated && tablePaginationProps && ( diff --git a/src/components/elements/table/TableRowActions.tsx b/src/components/elements/table/TableRowActions.tsx new file mode 100644 index 000000000..014a5d662 --- /dev/null +++ b/src/components/elements/table/TableRowActions.tsx @@ -0,0 +1,73 @@ +import { Button, Stack } from '@mui/material'; +import { ReactNode } from 'react'; +import ButtonLink from '../ButtonLink'; +import CommonMenuButton, { CommonMenuItem } from '../CommonMenuButton'; + +interface TableRowActionsProps { + record: T; + recordName?: string; + // Use primaryActionConfig if the primary action is a simple navigation that can be represented by a CommonMenuItem + primaryActionConfig?: CommonMenuItem; + // Use primaryAction if the primary action needs to be customized + primaryAction?: ReactNode; + // Secondary actions are assumed to all be simple + secondaryActionConfigs?: CommonMenuItem[]; +} + +const TableRowActions = ({ + record, + recordName, + primaryAction, + primaryActionConfig, + secondaryActionConfigs, +}: TableRowActionsProps) => { + return ( + + {!!primaryActionConfig && primaryActionConfig.to && ( + + {primaryActionConfig.title} + + )} + {!!primaryActionConfig && primaryActionConfig.onClick && ( + + )} + {primaryAction} + {!!secondaryActionConfigs && secondaryActionConfigs.length > 0 && ( + + )} + + ); +}; + +export default TableRowActions; diff --git a/src/components/elements/table/tableRowActionUtil.tsx b/src/components/elements/table/tableRowActionUtil.tsx new file mode 100644 index 000000000..9b8f7b5d8 --- /dev/null +++ b/src/components/elements/table/tableRowActionUtil.tsx @@ -0,0 +1,87 @@ +import { ColumnDef } from '@/components/elements/table/types'; +import { generateAssessmentPath } from '@/modules/assessments/util'; +import { + assessmentDescription, + clientBriefName, + entryExitRange, + parseAndFormatDate, +} from '@/modules/hmis/hmisUtil'; +import { ServiceFields } from '@/modules/services/components/ProjectServicesTable'; +import { getServiceTypeForDisplay } from '@/modules/services/serviceColumns'; +import { + ClientDashboardRoutes, + EnrollmentDashboardRoutes, +} from '@/routes/routes'; +import { + AssessmentFieldsFragment, + ClientNameFragment, + EnrollmentFieldsFragment, +} from '@/types/gqlTypes'; +import { generateSafePath } from '@/utils/pathEncoding'; + +export const BASE_ACTION_COLUMN_DEF: ColumnDef = { + key: 'Actions', + tableCellProps: { sx: { py: 0 } }, + render: '', // gets overridden when used +}; + +export const getViewClientMenuItem = (client: ClientNameFragment) => { + return { + title: 'View Client', + key: 'client', + ariaLabel: `View Client, ${clientBriefName(client)}`, + to: generateSafePath(ClientDashboardRoutes.PROFILE, { + clientId: client.id, + }), + }; +}; + +export const getViewEnrollmentMenuItem = ( + enrollment: Pick, + client: ClientNameFragment +) => { + return { + title: 'View Enrollment', + key: 'enrollment', + ariaLabel: `View Enrollment, ${clientBriefName(client)} ${entryExitRange(enrollment)}`, + to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { + clientId: client.id, + enrollmentId: enrollment.id, + }), + }; +}; + +export const getViewAssessmentMenuItem = ( + assessment: AssessmentFieldsFragment, + clientId: string, + enrollmentId: string, + individualViewOnly?: boolean +) => { + return { + title: 'View Assessment', + key: 'assessment', + ariaLabel: `View Assessment, ${assessmentDescription(assessment)}`, + to: generateAssessmentPath( + assessment, + clientId, + enrollmentId, + individualViewOnly + ), + }; +}; + +export const getViewServiceMenuItem = ( + service: Pick, + enrollmentId: string, + clientId: string +) => { + return { + title: 'View Service', + key: 'service', + ariaLabel: `View Service, ${getServiceTypeForDisplay(service.serviceType)} on ${parseAndFormatDate(service.dateProvided)}`, + to: generateSafePath(EnrollmentDashboardRoutes.SERVICES, { + clientId: clientId, + enrollmentId: enrollmentId, + }), + }; +}; diff --git a/src/components/elements/table/types.tsx b/src/components/elements/table/types.tsx index 272914667..d79e5c3e7 100644 --- a/src/components/elements/table/types.tsx +++ b/src/components/elements/table/types.tsx @@ -1,16 +1,12 @@ import { TableCellProps } from '@mui/material'; -import { ReactNode } from 'react'; export type AttributeName = keyof T; export type RenderFunction = (value: T) => React.ReactNode; -export interface ColumnDef { - header?: string | ReactNode; +type BaseColumnDef = { render: AttributeName | RenderFunction; width?: string; minWidth?: string; - // unique key for element. if not provided, header is used. - key?: string; // whether to hide this column hide?: boolean; // whether to show link treatment for this cell. rowLinkTo must be provided. @@ -23,7 +19,20 @@ export interface ColumnDef { tableCellProps?: TableCellProps; optional?: boolean; defaultHidden?: boolean; -} +}; + +export type ColumnDef = + | (BaseColumnDef & { + // Header is the text to display in the header cell for this column. It's optional (see below) + header: string | React.ReactNode; + // key is an optional unique key for this column. If not provided, header is used. + key?: string; + }) + | (BaseColumnDef & { + // If header is not provided, then key is required + header?: never; + key: string; + }); export function isPrimitive(value: any): value is AttributeName { return ( diff --git a/src/modules/admin/components/services/ServiceTypeTable.tsx b/src/modules/admin/components/services/ServiceTypeTable.tsx index 2e0db0ce1..cc7417968 100644 --- a/src/modules/admin/components/services/ServiceTypeTable.tsx +++ b/src/modules/admin/components/services/ServiceTypeTable.tsx @@ -1,7 +1,10 @@ import { Chip } from '@mui/material'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { getServiceTypeForDisplay } from '@/modules/services/serviceColumns'; import { AdminDashboardRoutes } from '@/routes/routes'; import { GetServiceTypesDocument, @@ -11,11 +14,10 @@ import { } from '@/types/gqlTypes'; import { generateSafePath } from '@/utils/pathEncoding'; -const columns: ColumnDef[] = [ +const COLUMNS: ColumnDef[] = [ { header: 'Service Name', render: 'name', - linkTreatment: true, }, { header: 'Service Category', @@ -40,6 +42,22 @@ const columns: ColumnDef[] = [ ) : null, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: ServiceTypeConfigFieldsFragment) => ( + + ), + }, ]; const ServiceTypeTable = () => { @@ -57,17 +75,12 @@ const ServiceTypeTable = () => { > queryVariables={{}} queryDocument={GetServiceTypesDocument} - columns={columns} + columns={COLUMNS} pagePath='serviceTypes' noData='No service types' filters={filters} recordType='ServiceType' paginationItemName='service type' - rowLinkTo={(row) => - generateSafePath(AdminDashboardRoutes.CONFIGURE_SERVICE_TYPE, { - serviceTypeId: row.id, - }) - } /> ); diff --git a/src/modules/admin/components/users/AdminUsers.tsx b/src/modules/admin/components/users/AdminUsers.tsx index 8404aef94..9214453c7 100644 --- a/src/modules/admin/components/users/AdminUsers.tsx +++ b/src/modules/admin/components/users/AdminUsers.tsx @@ -39,6 +39,7 @@ const AdminUsers = () => { render: ({ email }) => email, }, { + key: 'Actions', textAlign: 'right', render: (user) => access && ( diff --git a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx index 95dd8031d..bd3497cb7 100644 --- a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx +++ b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx @@ -1,17 +1,17 @@ import { Paper } from '@mui/material'; -import { useCallback } from 'react'; +import { useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import { ClientAssessmentType } from '@/modules/assessments/assessmentTypes'; -import { - ASSESSMENT_COLUMNS, - ASSESSMENT_ENROLLMENT_COLUMNS, - assessmentRowLinkTo, -} from '@/modules/assessments/util'; +import { ASSESSMENT_COLUMNS } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import AssessmentDateWithStatusIndicator from '@/modules/hmis/components/AssessmentDateWithStatusIndicator'; import { useFilters } from '@/modules/hmis/filterUtil'; import { assessmentDescription } from '@/modules/hmis/hmisUtil'; import { @@ -21,32 +21,41 @@ import { GetClientAssessmentsQueryVariables, } from '@/types/gqlTypes'; -const columns: ColumnDef[] = [ - ASSESSMENT_COLUMNS.linkedType, - { - header: 'Assessment Date', - render: (a) => , - ariaLabel: (row) => assessmentDescription(row), - }, - { - header: 'Project Name', - render: (row) => row.enrollment.projectName, - }, - ASSESSMENT_ENROLLMENT_COLUMNS.period, -]; - const ClientAssessmentsPage = () => { const { clientId } = useSafeParams() as { clientId: string }; - const rowLinkTo = useCallback( - (record: ClientAssessmentType) => assessmentRowLinkTo(record, clientId), - [clientId] - ); - const filters = useFilters({ type: 'AssessmentFilterOptions', }); + const columns: ColumnDef[] = useMemo( + () => [ + ASSESSMENT_COLUMNS.date, + { + header: 'Project Name', + render: (row: ClientAssessmentType) => row.enrollment.projectName, + }, + ASSESSMENT_COLUMNS.type, + ASSESSMENT_COLUMNS.lastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: ClientAssessmentType) => ( + + ), + }, + ], + [clientId] + ); + return ( <> @@ -59,7 +68,6 @@ const ClientAssessmentsPage = () => { filters={filters} queryVariables={{ id: clientId }} queryDocument={GetClientAssessmentsDocument} - rowLinkTo={rowLinkTo} columns={columns} pagePath='client.assessments' fetchPolicy='cache-and-network' diff --git a/src/modules/assessments/util.tsx b/src/modules/assessments/util.tsx index 885eccdc5..fd58f31c1 100644 --- a/src/modules/assessments/util.tsx +++ b/src/modules/assessments/util.tsx @@ -1,10 +1,10 @@ import { AlwaysPresentLocalConstants } from '../form/util/formUtil'; import { ClientAssessmentType } from './assessmentTypes'; +import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay'; import { ColumnDef } from '@/components/elements/table/types'; import { HhmAssessmentType } from '@/modules/enrollment/components/HouseholdAssessmentsTable'; import AssessmentDateWithStatusIndicator from '@/modules/hmis/components/AssessmentDateWithStatusIndicator'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; -import { formRoleDisplay, lastUpdatedBy } from '@/modules/hmis/hmisUtil'; +import { clientBriefName, formRoleDisplay } from '@/modules/hmis/hmisUtil'; import { ProjectAssessmentType } from '@/modules/projects/components/ProjectAssessments'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { AssessmentFieldsFragment, AssessmentRole } from '@/types/gqlTypes'; @@ -65,18 +65,6 @@ export const generateAssessmentPath = ( }); }; -export const assessmentRowLinkTo = ( - record: ClientAssessmentType | ProjectAssessmentType, - clientId: string -) => - // Note: this opens the assessment for individual viewing, even - // if it's an intake/exit in a multimember household. - generateSafePath(EnrollmentDashboardRoutes.VIEW_ASSESSMENT, { - clientId: clientId, - enrollmentId: record.enrollment.id, - assessmentId: record.id, - }); - export const ASSESSMENT_COLUMNS: { [key: string]: ColumnDef< | ProjectAssessmentType @@ -93,21 +81,23 @@ export const ASSESSMENT_COLUMNS: { header: 'Assessment Type', render: (a) => formRoleDisplay(a), }, - linkedType: { - header: 'Assessment Type', - render: (a) => formRoleDisplay(a), - linkTreatment: true, - }, lastUpdated: { header: 'Last Updated', - render: (e) => lastUpdatedBy(e.dateUpdated, e.user), + render: ({ dateUpdated, user }) => { + if (dateUpdated) + return ( + + ); + }, }, }; -export const ASSESSMENT_ENROLLMENT_COLUMNS: { - [key: string]: ColumnDef; -} = { - period: { - header: 'Enrollment Period', - render: (a) => , - }, + +export const ASSESSMENT_CLIENT_NAME_COL: ColumnDef< + ProjectAssessmentType | HhmAssessmentType +> = { + header: 'Client Name', + render: (a) => clientBriefName(a.enrollment.client), }; diff --git a/src/modules/bulkServices/components/AssignServiceButton.tsx b/src/modules/bulkServices/components/AssignServiceButton.tsx index 4e02bb39b..5e6de0326 100644 --- a/src/modules/bulkServices/components/AssignServiceButton.tsx +++ b/src/modules/bulkServices/components/AssignServiceButton.tsx @@ -126,6 +126,7 @@ const AssignServiceButton: React.FC = ({ fullWidth variant='contained' color={isAssignedOnDate ? 'primary' : 'grayscale'} + aria-label={`${buttonText}, ${clientBriefName(client)}`} > {buttonText} diff --git a/src/modules/bulkServices/components/BulkServicesTable.tsx b/src/modules/bulkServices/components/BulkServicesTable.tsx index 715505d9e..ecbadd511 100644 --- a/src/modules/bulkServices/components/BulkServicesTable.tsx +++ b/src/modules/bulkServices/components/BulkServicesTable.tsx @@ -3,26 +3,31 @@ import { ServicePeriod } from '../bulkServicesTypes'; import AssignServiceButton from './AssignServiceButton'; import MultiAssignServiceButton from './MultiAssignServiceButton'; import NotCollectedText from '@/components/elements/NotCollectedText'; -import RouterLink from '@/components/elements/RouterLink'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewClientMenuItem, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import { SsnDobShowContextProvider } from '@/modules/client/providers/ClientSsnDobVisibility'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { + clientBriefName, formatDateForDisplay, formatDateForGql, formatRelativeDate, parseAndFormatDate, parseHmisDateString, } from '@/modules/hmis/hmisUtil'; +import { useHasRootPermissions } from '@/modules/permissions/useHasPermissionsHooks'; import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { BulkServicesClientSearchDocument, BulkServicesClientSearchQuery, BulkServicesClientSearchQueryVariables, ClientSortOption, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; interface Props { projectId: string; @@ -59,6 +64,8 @@ const BulkServicesTable: React.FC = ({ [cocCode, projectId, serviceDate, serviceTypeId] ); + const [canViewDob] = useHasRootPermissions(['canViewDob']); + const getColumnDefs = useCallback( (_rows: RowType[], loading?: boolean) => { const notEnrolledText = ( @@ -67,26 +74,14 @@ const BulkServicesTable: React.FC = ({ ); return [ - { ...CLIENT_COLUMNS.linkedId }, - CLIENT_COLUMNS.first, - CLIENT_COLUMNS.last, - CLIENT_COLUMNS.dobAge, + CLIENT_COLUMNS.name, + ...(canViewDob ? [CLIENT_COLUMNS.dobAge] : []), { header: 'Entry Date', render: (row: RowType) => { if (!row.activeEnrollment) return notEnrolledText; - return ( - - {parseAndFormatDate(row.activeEnrollment.entryDate)} - - ); + return parseAndFormatDate(row.activeEnrollment.entryDate); }, }, { @@ -112,30 +107,43 @@ const BulkServicesTable: React.FC = ({ }, }, { - header: `Assign ${serviceTypeName} for ${formatDateForDisplay( - serviceDate - )}`, - width: '180px', - render: (row: RowType) => { - return ( - - ); - }, + ...BASE_ACTION_COLUMN_DEF, + render: (row: RowType) => ( + + } + secondaryActionConfigs={[ + getViewClientMenuItem(row), + ...(row.activeEnrollment + ? [getViewEnrollmentMenuItem(row.activeEnrollment, row)] + : []), + ]} + /> + ), }, ] as ColumnDef[]; }, - [serviceDate, serviceTypeName, mutationQueryVariables, anyRowsSelected] + [ + anyRowsSelected, + canViewDob, + mutationQueryVariables, + serviceDate, + serviceTypeName, + ] ); const defaultFilterValues = useMemo(() => { diff --git a/src/modules/caseNotes/components/ClientCaseNotes.tsx b/src/modules/caseNotes/components/ClientCaseNotes.tsx index 096608ffe..152e91f33 100644 --- a/src/modules/caseNotes/components/ClientCaseNotes.tsx +++ b/src/modules/caseNotes/components/ClientCaseNotes.tsx @@ -1,41 +1,26 @@ import { Paper } from '@mui/material'; +import { useMemo } from 'react'; import { CASE_NOTE_COLUMNS } from './EnrollmentCaseNotes'; -import { ColumnDef } from '@/components/elements/table/types'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import PageTitle from '@/components/layout/PageTitle'; import NotFound from '@/components/pages/NotFound'; import useClientDashboardContext from '@/modules/client/hooks/useClientDashboardContext'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useViewEditRecordDialogs } from '@/modules/form/hooks/useViewEditRecordDialogs'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; +import { parseAndFormatDate } from '@/modules/hmis/hmisUtil'; import { - RecordFormRole, GetClientCaseNotesDocument, GetClientCaseNotesQuery, GetClientCaseNotesQueryVariables, + RecordFormRole, } from '@/types/gqlTypes'; type Row = NonNullable< GetClientCaseNotesQuery['client'] >['customCaseNotes']['nodes'][0]; -const columns: ColumnDef[] = [ - CASE_NOTE_COLUMNS.InformationDate, - CASE_NOTE_COLUMNS.NoteContentPreview, - { - key: 'project', - header: 'Project Name', - render: (row) => row.enrollment.projectName, - }, - { - key: 'en-period', - header: 'Enrollment Period', - render: (row) => ( - - ), - }, -]; - const ClientCaseNotes = () => { const { client } = useClientDashboardContext(); @@ -49,6 +34,34 @@ const ClientCaseNotes = () => { maxWidth: 'sm', }); + const columns = useMemo( + () => [ + CASE_NOTE_COLUMNS.InformationDate, + { + key: 'project', + header: 'Project Name', + render: (row: Row) => row.enrollment.projectName, + }, + CASE_NOTE_COLUMNS.LastUpdated, + CASE_NOTE_COLUMNS.NoteContentPreview, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: Row) => ( + onSelectRecord(row), + }} + /> + ), + }, + ], + [onSelectRecord] + ); + if (!clientId) return ; return ( @@ -66,7 +79,6 @@ const ClientCaseNotes = () => { pagePath='client.customCaseNotes' noData='No case notes' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} recordType='CustomCaseNote' paginationItemName='case note' showTopToolbar diff --git a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx index 3aff105ed..76225b3b4 100644 --- a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx +++ b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx @@ -4,13 +4,15 @@ import { Box, Button } from '@mui/material'; import { useCallback } from 'react'; import { useViewEditRecordDialogs } from '../../form/hooks/useViewEditRecordDialogs'; +import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import useEnrollmentDashboardContext from '@/modules/enrollment/hooks/useEnrollmentDashboardContext'; import { getCustomDataElementColumns, - lastUpdatedBy, parseAndFormatDate, } from '@/modules/hmis/hmisUtil'; import { cache } from '@/providers/apolloClient'; @@ -30,10 +32,9 @@ export const CASE_NOTE_COLUMNS = { width: '150px', render: ({ informationDate }: CustomCaseNoteFieldsFragment) => parseAndFormatDate(informationDate), - linkTreatment: true, }, NoteContent: { - header: 'Note', + header: 'Note Content', render: ({ content }: CustomCaseNoteFieldsFragment) => ( ( - lastUpdatedBy(dateUpdated, user), + render: ({ dateUpdated, user }: CustomCaseNoteFieldsFragment) => { + if (dateUpdated) + return ( + + ); + }, }, }; @@ -102,15 +110,34 @@ const EnrollmentCaseNotes = () => { projectId: enrollment?.project.id, }); - const getColumnDefs = useCallback((rows: CustomCaseNoteFieldsFragment[]) => { - const customColumns = getCustomDataElementColumns(rows); - return [ - CASE_NOTE_COLUMNS.InformationDate, - ...customColumns, - CASE_NOTE_COLUMNS.NoteContent, - CASE_NOTE_COLUMNS.LastUpdated, - ]; - }, []); + const getColumnDefs = useCallback( + (rows: CustomCaseNoteFieldsFragment[]) => { + const customColumns = getCustomDataElementColumns(rows); + return [ + CASE_NOTE_COLUMNS.InformationDate, + ...customColumns, + CASE_NOTE_COLUMNS.NoteContent, + CASE_NOTE_COLUMNS.LastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (caseNote: CustomCaseNoteFieldsFragment) => ( + onSelectRecord(caseNote), + }} + /> + ), + }, + ]; + }, + [onSelectRecord] + ); const caseNotesFeature = getEnrollmentFeature( DataCollectionFeatureRole.CaseNote @@ -149,7 +176,6 @@ const EnrollmentCaseNotes = () => { pagePath='enrollment.customCaseNotes' noData='No case notes' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} recordType='CustomCaseNote' paginationItemName='case note' showTopToolbar diff --git a/src/modules/client/components/ClientEnrollmentCard.tsx b/src/modules/client/components/ClientEnrollmentCard.tsx index 418b95b88..fea818e28 100644 --- a/src/modules/client/components/ClientEnrollmentCard.tsx +++ b/src/modules/client/components/ClientEnrollmentCard.tsx @@ -3,11 +3,9 @@ import { useMemo } from 'react'; import GenericTable from '@/components/elements/table/GenericTable'; import TitleCard from '@/components/elements/TitleCard'; +import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; import { isRecentEnrollment } from '@/modules/hmis/hmisUtil'; -import { - ENROLLMENT_PERIOD_COL, - ENROLLMENT_STATUS_COL, -} from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { ClientEnrollmentFieldsFragment, @@ -71,14 +69,21 @@ const RecentEnrollments = ({ noHead rows={recentEnrollments} columns={[ - ENROLLMENT_STATUS_COL, + ENROLLMENT_COLUMNS.enrollmentStatus, { key: 'name', header: 'Name', - linkTreatment: true, render: 'projectName', }, - ENROLLMENT_PERIOD_COL, + { + header: 'Enrollment Period', + render: (e) => ( + + ), + }, ]} rowLinkTo={(row) => row.access.canViewEnrollmentDetails diff --git a/src/modules/client/components/ClientSearchResultCard.tsx b/src/modules/client/components/ClientSearchResultCard.tsx index ca7249901..d51900093 100644 --- a/src/modules/client/components/ClientSearchResultCard.tsx +++ b/src/modules/client/components/ClientSearchResultCard.tsx @@ -219,7 +219,7 @@ const ClientSearchResultCard: React.FC = ({ Icon={PersonIcon} leftAlign > - Client Profile + View Client {/* disabled for now #185750557 */} {/* diff --git a/src/modules/clientFiles/components/ClientFilesPage.tsx b/src/modules/clientFiles/components/ClientFilesPage.tsx index 9d9fb9459..8175e541e 100644 --- a/src/modules/clientFiles/components/ClientFilesPage.tsx +++ b/src/modules/clientFiles/components/ClientFilesPage.tsx @@ -1,18 +1,19 @@ import UploadIcon from '@mui/icons-material/Upload'; -import { Box, Chip, Link, Paper, Stack, Typography } from '@mui/material'; +import { Box, Chip, Paper, Typography } from '@mui/material'; import { useMemo, useState } from 'react'; import useFileActions from '../hooks/useFileActions'; import ButtonLink from '@/components/elements/ButtonLink'; import NotCollectedText from '@/components/elements/NotCollectedText'; +import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import FilePreviewDialog from '@/components/elements/upload/fileDialog/FilePreviewDialog'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; -import { parseAndFormatDateTime } from '@/modules/hmis/hmisUtil'; import { useClientPermissions, useHasClientPermissions, @@ -75,19 +76,9 @@ const ClientFilesPage = () => { return [ { header: 'File Name', - render: (file) => - file.redacted ? ( - {file.name} - ) : ( - setViewingFile(file)} - align='left' - tabIndex={-1} - > - {file.name} - - ), + render: (file) => ( + {file.name} + ), }, { header: 'File Tags', @@ -111,30 +102,48 @@ const ClientFilesPage = () => { ) : null, }, { - header: 'Enrollment', - render: ({ enrollment }) => { - if (!enrollment) return N/A; - - return ( - - {enrollment.projectName} - - - ); - }, + header: 'Project Name', + render: ({ enrollment }) => + enrollment ? ( + enrollment.projectName + ) : ( + N/A + ), }, { - header: 'Uploaded At', - render: (file) => { - const uploadedAt = file.dateCreated - ? parseAndFormatDateTime(file.dateCreated) - : 'Unknown time'; - const uploadedBy = file.uploadedBy?.name - ? `by ${file.uploadedBy?.name}` + header: 'Uploaded', + render: ({ dateCreated, uploadedBy }) => { + const byUser = uploadedBy?.name + ? `by ${uploadedBy?.name}` : 'by unknown user'; - return `${uploadedAt} ${uploadedBy}`; + if (dateCreated) + return ( + + ); + return `Unknown time ${byUser}`; }, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (file) => ( + setViewingFile(file), + } + } + /> + ), + }, ]; }, [pickListData]); @@ -166,7 +175,6 @@ const ClientFilesPage = () => { queryDocument={GetClientFilesDocument} columns={columns} pagePath='client.files' - handleRowClick={(file) => !file.redacted && setViewingFile(file)} noData='No files' /> diff --git a/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx b/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx index ac4b5c6dc..717e3a036 100644 --- a/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx +++ b/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx @@ -88,6 +88,8 @@ const NewAssessmentMenu: React.FC< ); if (items.length === 0) { + // Assessment eligibilities will have length 0 for an exited enrollment where no post-exit assessments are enabled. + // (Custom Assessments can collected be post-exit) return ( [] = [ - ASSESSMENT_COLUMNS.linkedType, - ASSESSMENT_COLUMNS.date, - ASSESSMENT_COLUMNS.lastUpdated, -]; - interface Props { enrollmentId: string; clientId: string; @@ -31,9 +27,26 @@ const EnrollmentAssessmentsTable: React.FC = ({ enrollmentId, projectId, }) => { - const rowLinkTo = useCallback( - (assessment: AssessmentFieldsFragment) => - generateAssessmentPath(assessment, clientId, enrollmentId), + const columns = useMemo( + () => [ + ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, + ASSESSMENT_COLUMNS.lastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (assessment) => ( + + ), + }, + ], [clientId, enrollmentId] ); @@ -51,7 +64,6 @@ const EnrollmentAssessmentsTable: React.FC = ({ filters={filters} queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentAssessmentsDocument} - rowLinkTo={rowLinkTo} columns={columns} pagePath='enrollment.assessments' noData='No assessments' diff --git a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx index deeb6f388..2dbd7018a 100644 --- a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx +++ b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx @@ -1,13 +1,19 @@ -import { useCallback } from 'react'; - +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import { + ASSESSMENT_CLIENT_NAME_COL, ASSESSMENT_COLUMNS, - generateAssessmentPath, } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; -import { clientBriefName } from '@/modules/hmis/hmisUtil'; +import { + assessmentDescription, + clientBriefName, +} from '@/modules/hmis/hmisUtil'; import { GetHouseholdAssessmentsDocument, GetHouseholdAssessmentsQuery, @@ -18,14 +24,29 @@ export type HhmAssessmentType = NonNullable< NonNullable['assessments'] >['nodes'][0]; -const columns: ColumnDef[] = [ - { - header: 'Client Name', - render: (a) => clientBriefName(a.enrollment.client), - }, - ASSESSMENT_COLUMNS.linkedType, +const COLUMNS: ColumnDef[] = [ + ASSESSMENT_CLIENT_NAME_COL, ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, ASSESSMENT_COLUMNS.lastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (assessment: HhmAssessmentType) => ( + + ), + }, ]; interface Props { @@ -37,16 +58,6 @@ const HouseholdAssessmentsTable: React.FC = ({ householdId, projectId, }) => { - const rowLinkTo = useCallback( - (assessment: HhmAssessmentType) => - generateAssessmentPath( - assessment, - assessment.enrollment.client.id, - assessment.enrollment.id - ), - [] - ); - const filters = useFilters({ type: 'AssessmentsForHouseholdFilterOptions', pickListArgs: { projectId }, @@ -61,8 +72,7 @@ const HouseholdAssessmentsTable: React.FC = ({ filters={filters} queryVariables={{ id: householdId }} queryDocument={GetHouseholdAssessmentsDocument} - rowLinkTo={rowLinkTo} - columns={columns} + columns={COLUMNS} pagePath='household.assessments' noData='No assessments' recordType='Assessment' diff --git a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx index 8fc82c2e5..1e63f7698 100644 --- a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx +++ b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx @@ -1,19 +1,24 @@ import { Paper, Stack, Typography } from '@mui/material'; -import { ReactNode, useCallback } from 'react'; +import { ReactNode, useMemo } from 'react'; import NotCollectedText from '@/components/elements/NotCollectedText'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useClientDashboardContext from '@/modules/client/hooks/useClientDashboardContext'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; import ProjectTypeChip from '@/modules/hmis/components/ProjectTypeChip'; import { useFilters } from '@/modules/hmis/filterUtil'; import { - PERMANENT_HOUSING_PROJECT_TYPES, + entryExitRange, parseAndFormatDate, + PERMANENT_HOUSING_PROJECT_TYPES, } from '@/modules/hmis/hmisUtil'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; +import { ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { ClientEnrollmentFieldsFragment, EnrollmentSortOption, @@ -23,7 +28,6 @@ import { ProjectType, RelationshipToHoH, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; const CaptionedText: React.FC<{ caption: string; children: ReactNode }> = ({ caption, @@ -39,30 +43,25 @@ const CaptionedText: React.FC<{ caption: string; children: ReactNode }> = ({ ); }; -const columns: ColumnDef[] = [ - { - header: 'Enrollment Period', - render: (row) => , +const CLIENT_ENROLLMENT_COLUMNS: { + [key: string]: ColumnDef; +} = { + projectName: { + header: 'Project Name', + render: 'projectName', }, - { + organizationName: { header: 'Organization Name', render: 'organizationName', }, - { - header: 'Project Name', - render: 'projectName', - linkTreatment: true, - ariaLabel: (row) => row.projectName, - }, - - { + projectType: { header: 'Project Type', render: ({ projectType }) => ( ), }, - { - header: 'Details', + enrollmentDetails: { + header: 'Enrollment Details', render: ({ moveInDate, lastBedNightDate, @@ -96,45 +95,45 @@ const columns: ColumnDef[] = [ } }, }, -]; +}; const ClientEnrollmentsPage = () => { const { client } = useClientDashboardContext(); - const rowLinkTo = useCallback( - (enrollment: ClientEnrollmentFieldsFragment) => { - if (!enrollment.access.canViewEnrollmentDetails) return null; - - return generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: client.id, - enrollmentId: enrollment.id, - }); - }, - [client] - ); - const filters = useFilters({ type: 'EnrollmentsForClientFilterOptions', }); + const columns = useMemo( + () => [ + CLIENT_ENROLLMENT_COLUMNS.projectName, + CLIENT_ENROLLMENT_COLUMNS.organizationName, + ENROLLMENT_COLUMNS.entryDate, + ENROLLMENT_COLUMNS.exitDate, + ENROLLMENT_COLUMNS.enrollmentStatus, + CLIENT_ENROLLMENT_COLUMNS.projectType, + CLIENT_ENROLLMENT_COLUMNS.enrollmentDetails, + { + ...BASE_ACTION_COLUMN_DEF, + render: (enrollment) => ( + + ), + }, + ], + [client] + ); + return ( <> - - // - // Add Enrollment - // - // - // } - /> + { > queryVariables={{ id: client.id }} queryDocument={GetClientEnrollmentsDocument} - rowLinkTo={rowLinkTo} columns={columns} pagePath='client.enrollments' filters={filters} diff --git a/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx index 8961cd041..680102862 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx @@ -2,6 +2,8 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; import { useCallback, useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; @@ -59,16 +61,20 @@ const EnrollmentCeAssessmentsPage = () => { projectId: enrollment?.project.id, }); + const ceAssessmentFeature = getEnrollmentFeature( + DataCollectionFeatureRole.CeAssessment + ); + const columns: ColumnDef[] = useMemo( () => [ { header: 'Assessment Date', - render: (a) => parseAndFormatDate(a.assessmentDate), - linkTreatment: canEditCeAssessments, + render: (a: CeAssessmentFieldsFragment) => + parseAndFormatDate(a.assessmentDate), }, { - header: 'Level', - render: (a) => ( + header: 'Assessment Level', + render: (a: CeAssessmentFieldsFragment) => ( { ), }, { - header: 'Type', - render: (a) => ( + header: 'Assessment Type', + render: (a: CeAssessmentFieldsFragment) => ( { ), }, { - header: 'Location', - render: (a) => a.assessmentLocation, + header: 'Assessment Location', + render: (a: CeAssessmentFieldsFragment) => a.assessmentLocation, }, { header: 'Prioritization Status', - render: (a) => ( + render: (a: CeAssessmentFieldsFragment) => ( ), }, + ...(canEditCeAssessments + ? [ + { + ...BASE_ACTION_COLUMN_DEF, + render: (a: CeAssessmentFieldsFragment) => ( + onSelectRecord(a), + }} + /> + ), + }, + ] + : []), ], - [canEditCeAssessments] - ); - - const ceAssessmentFeature = getEnrollmentFeature( - DataCollectionFeatureRole.CeAssessment + [canEditCeAssessments, onSelectRecord] ); if (!enrollment || !enrollmentId || !clientId || !ceAssessmentFeature) @@ -138,8 +160,6 @@ const EnrollmentCeAssessmentsPage = () => { pagePath='enrollment.ceAssessments' noData='No Coordinated Entry Assessments' headerCellSx={() => ({ color: 'text.secondary' })} - // no need for read-only users to click in, because they can see all the information in the table. - handleRowClick={canEditCeAssessments ? onSelectRecord : undefined} /> {editRecordDialog()} diff --git a/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx index b35d47a40..606c424ab 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx @@ -2,7 +2,8 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; import { useCallback, useMemo } from 'react'; -import { ColumnDef } from '@/components/elements/table/types'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; @@ -25,22 +26,6 @@ import { RecordFormRole, } from '@/types/gqlTypes'; -const columns: ColumnDef[] = [ - { - header: 'Event Date', - render: (e) => parseAndFormatDate(e.eventDate), - linkTreatment: true, - }, - { - header: 'Event Type', - render: (e) => , - }, - { - header: 'Result', - render: (e) => eventReferralResult(e), - }, -]; - const EnrollmentCeEventsPage = () => { const { enrollment, getEnrollmentFeature } = useEnrollmentDashboardContext(); const enrollmentId = enrollment?.id; @@ -81,6 +66,40 @@ const EnrollmentCeEventsPage = () => { DataCollectionFeatureRole.CeEvent ); + const columns = useMemo( + () => [ + { + header: 'Event Type', + render: (e: EventFieldsFragment) => ( + + ), + }, + { + header: 'Event Date', + render: (e: EventFieldsFragment) => parseAndFormatDate(e.eventDate), + }, + { + header: 'Referral Result', + render: (e: EventFieldsFragment) => eventReferralResult(e), + }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (e: EventFieldsFragment) => ( + onSelectRecord(e), + }} + /> + ), + }, + ], + [onSelectRecord] + ); + if (!enrollment || !enrollmentId || !clientId || !ceEventFeature) return ; @@ -114,7 +133,6 @@ const EnrollmentCeEventsPage = () => { pagePath='enrollment.events' noData='No events' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} /> {viewRecordDialog()} diff --git a/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx index 4ac0dcf11..7812d2f05 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx @@ -1,6 +1,8 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; import { useCallback, useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; @@ -23,13 +25,12 @@ import { RecordFormRole, } from '@/types/gqlTypes'; -export const baseColumns = { +export const CLS_COLUMNS = { informationDate: { header: 'Information Date', width: '180px', render: (e: CurrentLivingSituationFieldsFragment) => parseAndFormatDate(e.informationDate), - linkTreatment: true, }, livingSituation: { header: 'Living Situation', @@ -95,13 +96,30 @@ const EnrollmentCurrentLivingSituationsPage = () => { (rows: CurrentLivingSituationFieldsFragment[]) => { const customColumns = getCustomDataElementColumns(rows); return [ - baseColumns.informationDate, - baseColumns.livingSituation, - baseColumns.locationDetails, + CLS_COLUMNS.informationDate, + CLS_COLUMNS.livingSituation, + CLS_COLUMNS.locationDetails, ...customColumns, + { + ...BASE_ACTION_COLUMN_DEF, + render: (cls: CurrentLivingSituationFieldsFragment) => ( + onSelectRecord(cls), + }} + /> + ), + }, ]; }, - [] + [onSelectRecord] ); if (!enrollment || !enrollmentId || !clientId || !clsFeature) @@ -137,7 +155,6 @@ const EnrollmentCurrentLivingSituationsPage = () => { noData='No current living situations' recordType='CurrentLivingSituation' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} /> {viewRecordDialog()} diff --git a/src/modules/form/components/client/useRenderLastUpdated.tsx b/src/modules/form/components/client/useRenderLastUpdated.tsx index 48bfcf29e..6775915f9 100644 --- a/src/modules/form/components/client/useRenderLastUpdated.tsx +++ b/src/modules/form/components/client/useRenderLastUpdated.tsx @@ -1,7 +1,7 @@ import { DocumentNode } from 'graphql'; import { useCallback } from 'react'; -import RelativeDateDisplay from '@/modules/hmis/components/RelativeDateDisplay'; +import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay'; import apolloClient from '@/providers/apolloClient'; export function useRenderLastUpdated( diff --git a/src/modules/hmis/components/EnrollmentStatus.tsx b/src/modules/hmis/components/EnrollmentStatus.tsx index 38cf0d679..472a9c1a8 100644 --- a/src/modules/hmis/components/EnrollmentStatus.tsx +++ b/src/modules/hmis/components/EnrollmentStatus.tsx @@ -3,7 +3,11 @@ import HistoryIcon from '@mui/icons-material/History'; import TimerIcon from '@mui/icons-material/Timer'; import { Stack, Typography } from '@mui/material'; import { useMemo } from 'react'; -import { Enrollment, Household } from '@/types/gqlTypes'; +import { + Enrollment, + EnrollmentFieldsFragment, + Household, +} from '@/types/gqlTypes'; interface CommonStatusProps { variant: 'inProgress' | 'open' | 'autoExited' | 'exited'; @@ -57,7 +61,8 @@ const CommonStatus: React.FC = ({ variant }) => { const EnrollmentStatus = ({ enrollment, }: { - enrollment: Pick; + enrollment: Pick & + Partial>; }) => { if (enrollment.inProgress) return ; if (enrollment.autoExited) return ; diff --git a/src/modules/hmis/components/HudRecordMetadata.tsx b/src/modules/hmis/components/HudRecordMetadata.tsx index 9fd8f6cd5..66ab9b933 100644 --- a/src/modules/hmis/components/HudRecordMetadata.tsx +++ b/src/modules/hmis/components/HudRecordMetadata.tsx @@ -2,7 +2,7 @@ import { Stack } from '@mui/material'; import RelativeDateDisplay, { RelativeDateDisplayProps, -} from './RelativeDateDisplay'; +} from '../../../components/elements/RelativeDateDisplay'; import { UserFieldsFragment } from '@/types/gqlTypes'; diff --git a/src/modules/hmis/hmisUtil.ts b/src/modules/hmis/hmisUtil.ts index 3a864ac81..71eb80f3f 100644 --- a/src/modules/hmis/hmisUtil.ts +++ b/src/modules/hmis/hmisUtil.ts @@ -39,7 +39,6 @@ import { DisplayHook, EnrollmentFieldsFragment, EnrollmentOccurrencePointFieldsFragment, - EnrollmentSummaryFieldsFragment, EventFieldsFragment, HouseholdClientFieldsFragment, NoYes, @@ -318,10 +317,7 @@ export const pronouns = (client: ClientFieldsFragment): React.ReactNode => : null; export const entryExitRange = ( - enrollment: - | EnrollmentFieldsFragment - | HouseholdClientFieldsFragment['enrollment'] - | EnrollmentSummaryFieldsFragment, + enrollment: Pick, endPlaceholder?: string ) => { return parseAndFormatDateRange( @@ -379,12 +375,18 @@ export const formRoleDisplay = (assessment: AssessmentFieldsFragment) => { return defaultTitle; }; -export const assessmentDescription = (assessment: ClientAssessmentType) => { +export const assessmentDescription = ( + assessment: ClientAssessmentType | AssessmentFieldsFragment +) => { const prefix = formRoleDisplay(assessment); - const name = prefix ? `${prefix} assessment` : 'Assessment'; - return `${name} at ${assessment.enrollment.projectName} on ${ - parseAndFormatDate(assessment.assessmentDate) || 'unknown date' - }`; + const name = prefix ? `${prefix} assessment ` : 'Assessment '; + // `enrollment` may not be present in the type (eg. if we are on the Enrollment Assessments page) + const atProject = + 'enrollment' in assessment && !!assessment.enrollment?.projectName + ? `at ${assessment.enrollment.projectName} ` + : ''; + const onDate = `on ${parseAndFormatDate(assessment.assessmentDate) || 'unknown date'}`; + return name + atProject + onDate; }; export const eventReferralResult = (e: EventFieldsFragment) => { @@ -414,16 +416,32 @@ export const eventReferralResult = (e: EventFieldsFragment) => { return result; }; -export const sortHouseholdMembers = ( - members?: HouseholdClientFieldsFragment[], +const hohPriorityMapping: Record = { + [RelationshipToHoH.SelfHeadOfHousehold]: 0, + [RelationshipToHoH.SpouseOrPartner]: 1, + [RelationshipToHoH.Child]: 2, + [RelationshipToHoH.OtherRelative]: 3, + [RelationshipToHoH.UnrelatedHouseholdMember]: 4, + [RelationshipToHoH.DataNotCollected]: 5, + [RelationshipToHoH.Invalid]: 6, +}; + +type GenericHouseholdClient = Pick< + HouseholdClientFieldsFragment, + 'relationshipToHoH' +> & { + client: Pick; + enrollment: Pick; +}; +export const sortHouseholdMembers = ( + members?: T[], activeEnrollmentId?: string -) => { - const sorted = sortBy(members || [], [ +): T[] => { + return sortBy(members || [], [ (c) => (c.enrollment.id === activeEnrollmentId ? -1 : 1), - (c) => c.client.lastName, - (c) => c.client.id, + (c) => hohPriorityMapping[c.relationshipToHoH], // HoH > spouse > child > relative > unrelated + (c) => c.client.id, // deterministic tie-breaker ]); - return sorted; }; export const getSchemaForType = (type: string) => { diff --git a/src/modules/household/components/HouseholdMemberTable.tsx b/src/modules/household/components/HouseholdMemberTable.tsx index e580990da..8e387b42d 100644 --- a/src/modules/household/components/HouseholdMemberTable.tsx +++ b/src/modules/household/components/HouseholdMemberTable.tsx @@ -15,7 +15,6 @@ import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentD import EnrollmentStatus from '@/modules/hmis/components/EnrollmentStatus'; import HmisEnum from '@/modules/hmis/components/HmisEnum'; import HohIndicator from '@/modules/hmis/components/HohIndicator'; -import { parseAndFormatDate } from '@/modules/hmis/hmisUtil'; import { useHmisAppSettings } from '@/modules/hmisAppSettings/useHmisAppSettings'; import { useHouseholdMembers } from '@/modules/household/hooks/useHouseholdMembers'; import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; @@ -81,17 +80,6 @@ export const HOUSEHOLD_MEMBER_COLUMNS = { ), }, - entryDate: { - header: 'Entry Date', - render: (hc: HouseholdClientFieldsFragment) => - parseAndFormatDate(hc.enrollment.entryDate), - }, - exitDate: (householdMembers: HouseholdClientFieldsFragment[]) => ({ - header: 'Exit Date', - hide: !householdMembers.some((m) => m.enrollment.exitDate), - render: (hc: HouseholdClientFieldsFragment) => - parseAndFormatDate(hc.enrollment.exitDate), - }), relationshipToHoh: { header: 'Relationship to HoH', render: (hc: HouseholdClientFieldsFragment) => ( @@ -122,25 +110,6 @@ export const HOUSEHOLD_MEMBER_COLUMNS = { ), }, mciIds: externalIdColumn(ExternalIdentifierType.MciId, 'MCI ID'), - // { - // header: 'Enrollment Period', - // key: 'enrollment_period', - // render: ({ enrollment }: HouseholdClientFieldsFragment) => ( - // - // ), - // }, - // { - // header: 'Destination', - // key: 'exit_destination', - // hide: !some(householdMembers, (m) => m.enrollment.exitDestination), - // render: (hc: HouseholdClientFieldsFragment) => null, - // }, - // { - // header: 'Move in Date', - // key: 'move_in_date', - // // hide: !some(householdMembers, (m) => m.enrollment.moveInDate), - // render: (hc: HouseholdClientFieldsFragment) => null, - // }, }; /** diff --git a/src/modules/household/types.ts b/src/modules/household/types.ts index 6d54a67ba..ed92d889f 100644 --- a/src/modules/household/types.ts +++ b/src/modules/household/types.ts @@ -6,6 +6,7 @@ import { EnrollmentFieldsFragment, HouseholdClientFieldsFragment, ProjectEnrollmentFieldsFragment, + ProjectEnrollmentsHouseholdClientFieldsFragment, } from '@/types/gqlTypes'; export type RecentHouseholdMember = HouseholdClientFieldsFragment & { @@ -18,7 +19,10 @@ export function isHouseholdClient( | HouseholdClientFieldsFragment | EnrollmentFieldsFragment | ProjectEnrollmentFieldsFragment -): value is HouseholdClientFieldsFragment { + | ProjectEnrollmentsHouseholdClientFieldsFragment +): value is + | HouseholdClientFieldsFragment + | ProjectEnrollmentsHouseholdClientFieldsFragment { return ( !isNil(value) && typeof value === 'object' && diff --git a/src/modules/projectAdministration/components/AllProjectsPage.tsx b/src/modules/projectAdministration/components/AllProjectsPage.tsx index f31813e7b..8d8e70582 100644 --- a/src/modules/projectAdministration/components/AllProjectsPage.tsx +++ b/src/modules/projectAdministration/components/AllProjectsPage.tsx @@ -1,12 +1,14 @@ import AddIcon from '@mui/icons-material/Add'; -import { Grid, Paper } from '@mui/material'; +import { Paper } from '@mui/material'; import { Box, Stack } from '@mui/system'; -import { useCallback, useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import CommonSearchInput from '../../search/components/CommonSearchInput'; import ButtonLink from '@/components/elements/ButtonLink'; import CommonToggle, { ToggleItem } from '@/components/elements/CommonToggle'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageContainer from '@/components/layout/PageContainer'; import useDebouncedState from '@/hooks/useDebouncedState'; @@ -38,11 +40,9 @@ const PROJECT_COLUMNS: ColumnDef[] = [ { header: 'Project Name', render: 'projectName', - linkTreatment: true, - ariaLabel: (row) => row.projectName, }, { - header: 'Organization', + header: 'Organization Name', render: (row) => row.organization.organizationName, }, { @@ -59,18 +59,49 @@ const PROJECT_COLUMNS: ColumnDef[] = [ project.operatingEndDate ), }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (project: ProjectAllFieldsFragment) => ( + + ), + }, ]; const ORGANIZATION_COLUMNS: ColumnDef[] = [ { header: 'Organization Name', render: 'organizationName', - linkTreatment: true, }, { header: 'Project Count', render: 'projects.nodesCount' as keyof OrganizationType, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (organization: OrganizationType) => ( + + ), + }, ]; export type ViewMode = 'projects' | 'organizations'; @@ -87,34 +118,131 @@ const toggleItemDefinitions: ToggleItem[] = [ }, ]; -const AllProjectsPage = () => { - const [viewMode, setViewMode] = useState('projects'); - - const [search, setSearch, debouncedSearch] = useDebouncedState(''); - - const projectRowLink = useCallback( - (project: ProjectAllFieldsFragment) => - generateSafePath(Routes.PROJECT, { - projectId: project.id, - }), - [] - ); - +const ProjectsTable = ({ + search, + setSearch, + debouncedSearch, +}: { + search: string; + setSearch: Dispatch>; + debouncedSearch?: string; +}) => { const projectFilters = useFilters({ type: 'ProjectFilterOptions', omit: ['searchTerm'], }); - const organizationRowLink = useCallback( - (row: OrganizationType) => - generateSafePath(Routes.ORGANIZATION, { - organizationId: row.id, - }), - [] + return ( + + + + + key='projectTable' + queryVariables={{ + filters: { searchTerm: debouncedSearch || undefined }, + }} + defaultSortOption={ProjectSortOption.OrganizationAndName} + queryDocument={GetProjectsDocument} + columns={PROJECT_COLUMNS} + noData='No projects' + pagePath='projects' + recordType='Project' + defaultFilterValues={{ + status: [ProjectFilterOptionStatus.Open], + }} + filters={projectFilters} + defaultPageSize={25} + /> + + ); - +}; +const OrganizationsTable = ({ + search, + setSearch, + debouncedSearch, +}: { + search: string; + setSearch: Dispatch>; + debouncedSearch?: string; +}) => { const isMobile = useIsMobile('sm'); + return ( + + + + + + + Add Organization + + + + + + + key='organizationTable' + queryDocument={GetOrganizationsDocument} + columns={ORGANIZATION_COLUMNS} + noData='No organizations' + pagePath='organizations' + recordType='Organization' + defaultPageSize={25} + queryVariables={{ + filters: { searchTerm: debouncedSearch || undefined }, + }} + noSort + /> + + + ); +}; + +const AllProjectsPage = () => { + const [viewMode, setViewMode] = useState('projects'); + const [search, setSearch, debouncedSearch] = useDebouncedState(''); + return ( { /> } > - - {viewMode === 'projects' && ( - - - - )} - {viewMode === 'organizations' && ( - - - - - - - Add Organization - - - - - - )} - - - {viewMode === 'projects' ? ( - - key='projectTable' - queryVariables={{ - filters: { searchTerm: debouncedSearch || undefined }, - }} - defaultSortOption={ProjectSortOption.OrganizationAndName} - queryDocument={GetProjectsDocument} - columns={PROJECT_COLUMNS} - rowLinkTo={projectRowLink} - noData='No projects' - pagePath='projects' - recordType='Project' - defaultFilterValues={{ - status: [ProjectFilterOptionStatus.Open], - }} - filters={projectFilters} - defaultPageSize={25} - /> - ) : ( - - key='organizationTable' - queryDocument={GetOrganizationsDocument} - columns={ORGANIZATION_COLUMNS} - rowLinkTo={organizationRowLink} - noData='No organizations' - pagePath='organizations' - recordType='Organization' - defaultPageSize={25} - queryVariables={{ - filters: { searchTerm: debouncedSearch || undefined }, - }} - noSort - /> - )} - - - + {viewMode === 'projects' ? ( + + ) : ( + + )} ); }; diff --git a/src/modules/projects/components/ProjectAssessments.tsx b/src/modules/projects/components/ProjectAssessments.tsx index daa5afdb8..5fde2713a 100644 --- a/src/modules/projects/components/ProjectAssessments.tsx +++ b/src/modules/projects/components/ProjectAssessments.tsx @@ -1,17 +1,22 @@ import { Paper } from '@mui/material'; import { useMemo } from 'react'; import { useProjectDashboardContext } from './ProjectDashboard'; -import { ColumnDef } from '@/components/elements/table/types'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentMenuItem, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import { + ASSESSMENT_CLIENT_NAME_COL, ASSESSMENT_COLUMNS, - ASSESSMENT_ENROLLMENT_COLUMNS, - assessmentRowLinkTo, } from '@/modules/assessments/util'; -import ClientName from '@/modules/client/components/ClientName'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { assessmentDescription } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { AssessmentSortOption, GetProjectAssessmentsDocument, @@ -29,34 +34,39 @@ const ProjectAssessments = () => { }; const { project } = useProjectDashboardContext(); - const displayColumns: ColumnDef[] = useMemo(() => { + const columns = useMemo(() => { return [ + ASSESSMENT_CLIENT_NAME_COL, + ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, { - header: 'First Name', - linkTreatment: true, - render: (a: ProjectAssessmentType) => ( - - ), - }, - { - header: 'Last Name', - linkTreatment: true, - render: (a: ProjectAssessmentType) => ( - ( + ), }, - ASSESSMENT_COLUMNS.date, - ASSESSMENT_COLUMNS.type, - ASSESSMENT_ENROLLMENT_COLUMNS.period, ]; - }, []); - - const rowLinkTo = (record: ProjectAssessmentType) => - assessmentRowLinkTo(record, record.enrollment.client.id); + }, [project]); const filters = useFilters({ type: 'AssessmentsForProjectFilterOptions', @@ -74,9 +84,7 @@ const ProjectAssessments = () => { > queryVariables={{ id: projectId }} queryDocument={GetProjectAssessmentsDocument} - rowLinkTo={rowLinkTo} - rowLinkState={{ backToLabel: project.projectName }} - columns={displayColumns} + columns={columns} noData='No assessments' pagePath='project.assessments' recordType='Assessment' diff --git a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx index 165ef1001..2a3dd9374 100644 --- a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx +++ b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx @@ -1,12 +1,17 @@ import { Paper } from '@mui/material'; - -import { useCallback, useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; +import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import ClientName from '@/modules/client/components/ClientName'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import { baseColumns } from '@/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; +import { CLS_COLUMNS } from '@/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage'; +import { clientBriefName, parseAndFormatDate } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { GetProjectCurrentLivingSituationsDocument, @@ -16,51 +21,47 @@ import { } from '@/types/gqlTypes'; import { generateSafePath } from '@/utils/pathEncoding'; +const COLUMNS: ColumnDef[] = [ + { + header: 'Client Name', + render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( + + ), + }, + CLS_COLUMNS.informationDate, + CLS_COLUMNS.livingSituation, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, + { + ...BASE_ACTION_COLUMN_DEF, + render: (cls) => ( + + ), + }, +]; + const ProjectCurrentLivingSituations = () => { const { projectId } = useSafeParams() as { projectId: string; }; - const columns = useMemo(() => { - return [ - { - header: 'First Name', - linkTreatment: true, - render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( - - ), - }, - { - header: 'Last Name', - linkTreatment: true, - render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( - - ), - }, - { ...baseColumns.informationDate, linkTreatment: false }, - baseColumns.livingSituation, - { - header: 'Enrollment Period', - render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( - - ), - }, - ]; - }, []); - - const rowLinkTo = useCallback( - (cls: ProjectCurrentLivingSituationFieldsFragment) => { - return generateSafePath( - EnrollmentDashboardRoutes.CURRENT_LIVING_SITUATIONS, - { - clientId: cls.client.id, - enrollmentId: cls.enrollment.id, - } - ); - }, - [] - ); - return ( <> @@ -72,11 +73,10 @@ const ProjectCurrentLivingSituations = () => { > queryVariables={{ id: projectId }} queryDocument={GetProjectCurrentLivingSituationsDocument} - columns={columns} + columns={COLUMNS} pagePath='project.currentLivingSituations' noData='No current living situations' recordType='CurrentLivingSituation' - rowLinkTo={rowLinkTo} /> diff --git a/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx b/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx index 5cb9dd453..6aaceab01 100644 --- a/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx +++ b/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx @@ -1,26 +1,25 @@ -import { Stack, Tooltip, Typography } from '@mui/material'; -import { useCallback, useMemo } from 'react'; +import { Box, Chip, Tooltip } from '@mui/material'; +import React, { useMemo } from 'react'; +import DateWithRelativeTooltip from '@/components/elements/DateWithRelativeTooltip'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewClientMenuItem, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; -import ClientName from '@/modules/client/components/ClientName'; -import { SsnDobShowContextProvider } from '@/modules/client/providers/ClientSsnDobVisibility'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentClientNameWithAge from '@/modules/hmis/components/EnrollmentClientNameWithAge'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; - -import EnrollmentEntryDateWithStatusIndicator from '@/modules/hmis/components/EnrollmentEntryDateWithStatusIndicator'; import EnrollmentStatus from '@/modules/hmis/components/EnrollmentStatus'; -import HohIndicator from '@/modules/hmis/components/HohIndicator'; import { useFilters } from '@/modules/hmis/filterUtil'; import { + clientBriefName, formatDateForDisplay, formatDateForGql, parseAndFormatDate, } from '@/modules/hmis/hmisUtil'; import { useProjectDashboardContext } from '@/modules/projects/components/ProjectDashboard'; -import { ASSIGNED_STAFF_COL } from '@/modules/projects/components/tables/ProjectHouseholdsTable'; import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { ClientEnrollmentFieldsFragment, EnrollmentFieldsFragment, @@ -33,32 +32,11 @@ import { ProjectEnrollmentFieldsFragment, ProjectEnrollmentQueryEnrollmentFieldsFragment, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; export type EnrollmentFields = NonNullable< GetProjectEnrollmentsQuery['project'] >['enrollments']['nodes'][number]; -export const ENROLLMENT_STATUS_COL: ColumnDef< - | EnrollmentFieldsFragment - | ProjectEnrollmentFieldsFragment - | ClientEnrollmentFieldsFragment -> = { - header: 'Status', - render: (e) => , -}; - -export const ENROLLMENT_PERIOD_COL: ColumnDef< - | EnrollmentFieldsFragment - | ProjectEnrollmentFieldsFragment - | ClientEnrollmentFieldsFragment -> = { - header: 'Enrollment Period', - render: (e) => ( - - ), -}; - const isHouseholdWithStaff = ( e: any ): e is { household: HouseholdWithStaffAssignmentsFragment } => { @@ -67,82 +45,111 @@ const isHouseholdWithStaff = ( export const ENROLLMENT_COLUMNS: { [key: string]: ColumnDef< - | EnrollmentFieldsFragment - | ProjectEnrollmentFieldsFragment + | ClientEnrollmentFieldsFragment | ProjectEnrollmentQueryEnrollmentFieldsFragment >; } = { - clientName: { - header: 'Client', - render: (e) => , - linkTreatment: true, - }, - clientNameLinkedToEnrollment: { - header: 'Client', - render: (e) => , - linkTreatment: true, - }, - clientNameLinkedToEnrollmentWithAge: { - header: 'Client', - render: (e) => ( - - ), - linkTreatment: true, - }, - firstNameLinkedToEnrollment: { - header: 'First Name', + entryDate: { + header: 'Entry Date', render: (e) => ( - + ), - linkTreatment: true, }, - lastNameLinkedToEnrollment: { - header: 'Last Name', - render: (e) => ( - - ), - linkTreatment: true, + exitDate: { + header: 'Exit Date', + render: (e) => { + if (e.exitDate) + return ( + + ); + }, }, - enrollmentStatus: ENROLLMENT_STATUS_COL, - entryDate: { - header: 'Entry Date', - // should only be used for open enrollments, because it doesnt indicate if closed or not - render: (e) => , + enrollmentStatus: { + header: 'Status', + render: (e) => , }, - enrollmentPeriod: ENROLLMENT_PERIOD_COL, - householdId: { - header: 'Household ID', - key: 'housholdId', - optional: true, - render: (e) => ( - - - - {`${e.householdShortId} (${e.householdSize})`} - - - {e.householdSize > 1 && ( - +}; + +export const ASSIGNED_STAFF_COL = { + header: 'Assigned Staff', + optional: true, + defaultHidden: true, + key: 'assigned_staff', + render: (hh: HouseholdWithStaffAssignmentsFragment) => { + if (!hh.staffAssignments?.nodes.length) return; + + const allNames = hh.staffAssignments.nodes.map( + (staffAssignment) => staffAssignment.user.name + ); + + const first = allNames[0]; + const rest = allNames.slice(1); + + return ( + + {first}{' '} + {rest.length > 0 && ( + + + )} - + + ); + }, +}; + +type WithEnrollment = { + enrollment: Pick< + EnrollmentFieldsFragment, + 'entryDate' | 'exitDate' | 'inProgress' + > & + Partial>; +}; +export const WITH_ENROLLMENT_COLUMNS: { + [key: string]: ColumnDef; +} = { + entryDate: { + header: ENROLLMENT_COLUMNS.entryDate.header, + render: (objectWithEnrollment: WithEnrollment) => ( + ), }, - clientId: { - header: 'Client ID', - key: 'id', - render: (e) => e.client.id, + exitDate: { + header: ENROLLMENT_COLUMNS.exitDate.header, + render: (objectWithEnrollment: WithEnrollment) => { + if (objectWithEnrollment.enrollment.exitDate) + return ( + + ); + }, + }, + enrollmentStatus: { + header: ENROLLMENT_COLUMNS.enrollmentStatus.header, + render: (e) => , }, +}; + +const COLUMNS: { + [key: string]: ColumnDef< + | EnrollmentFieldsFragment + | ProjectEnrollmentFieldsFragment + | ProjectEnrollmentQueryEnrollmentFieldsFragment + | ClientEnrollmentFieldsFragment + >; +} = { lastClsDate: { header: 'Last Current Living Situation Date', key: 'lastClsDate', @@ -170,7 +177,6 @@ const ProjectClientEnrollmentsTable = ({ projectId, columns, openOnDate, - linkRowToEnrollment = false, searchTerm, }: { projectId: string; @@ -182,15 +188,6 @@ const ProjectClientEnrollmentsTable = ({ // TODO: show MCI column if enabled // const { globalFeatureFlags } = useHmisAppSettings(); // globalFeatureFlags?.mciId - const rowLinkTo = useCallback( - (en: EnrollmentFields) => - generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: en.client.id, - enrollmentId: en.id, - }), - [] - ); - const openOnDateString = useMemo( () => (openOnDate ? formatDateForGql(openOnDate) : undefined), [openOnDate] @@ -202,17 +199,27 @@ const ProjectClientEnrollmentsTable = ({ const defaultColumns: ColumnDef[] = useMemo(() => { - const cols = [ - ENROLLMENT_COLUMNS.clientNameLinkedToEnrollment, - CLIENT_COLUMNS.dobAge, + return [ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, + ENROLLMENT_COLUMNS.entryDate, + ENROLLMENT_COLUMNS.exitDate, ENROLLMENT_COLUMNS.enrollmentStatus, - ENROLLMENT_COLUMNS.enrollmentPeriod, - ENROLLMENT_COLUMNS.lastClsDate, + ...(staffAssignmentsEnabled ? [COLUMNS.assignedStaff] : []), + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: ProjectEnrollmentQueryEnrollmentFieldsFragment) => { + return ( + + ); + }, + }, ]; - - if (staffAssignmentsEnabled) cols.push(ENROLLMENT_COLUMNS.assignedStaff); - - return cols; }, [staffAssignmentsEnabled]); const filters = useFilters({ @@ -226,46 +233,44 @@ const ProjectClientEnrollmentsTable = ({ }); return ( - - - queryVariables={{ - id: projectId, - filters: { - searchTerm, - openOnDate: openOnDateString, - }, - }} - queryDocument={GetProjectEnrollmentsDocument} - columns={columns || defaultColumns} - rowLinkTo={linkRowToEnrollment ? rowLinkTo : undefined} - noData={ - openOnDate - ? `No enrollments open on ${formatDateForDisplay(openOnDate)}` - : 'No enrollments' - } - pagePath='project.enrollments' - recordType='Enrollment' - filters={filters} - defaultSortOption={EnrollmentSortOption.MostRecent} - showOptionalColumns - applyOptionalColumns={(cols) => { - const result: Partial = {}; + + queryVariables={{ + id: projectId, + filters: { + searchTerm, + openOnDate: openOnDateString, + }, + }} + queryDocument={GetProjectEnrollmentsDocument} + columns={columns || defaultColumns} + noData={ + openOnDate + ? `No enrollments open on ${formatDateForDisplay(openOnDate)}` + : 'No enrollments' + } + pagePath='project.enrollments' + recordType='Enrollment' + filters={filters} + defaultSortOption={EnrollmentSortOption.MostRecent} + showOptionalColumns + applyOptionalColumns={(cols) => { + const result: Partial = {}; - if (cols.includes(ENROLLMENT_COLUMNS.lastClsDate.key || '')) - result.includeCls = true; + if (cols.includes(COLUMNS.lastClsDate.key || '')) + result.includeCls = true; - if (cols.includes(ENROLLMENT_COLUMNS.assignedStaff.key || '')) - result.includeStaffAssignment = true; + if (cols.includes(COLUMNS.assignedStaff.key || '')) + result.includeStaffAssignment = true; - return result; - }} - /> - + return result; + }} + /> ); }; + export default ProjectClientEnrollmentsTable; diff --git a/src/modules/projects/components/tables/ProjectEnrollmentsTable.tsx b/src/modules/projects/components/tables/ProjectEnrollmentsTable.tsx index 3e745d20b..dcff18baa 100644 --- a/src/modules/projects/components/tables/ProjectEnrollmentsTable.tsx +++ b/src/modules/projects/components/tables/ProjectEnrollmentsTable.tsx @@ -20,10 +20,9 @@ const ProjectEnrollmentsTable = ({ projectId, columns, openOnDate, - linkRowToEnrollment = false, searchable = true, mode: modeProp, - initialMode: initialModeProp = 'households', + initialMode: initialModeProp = 'clients', }: { mode?: Mode; initialMode?: Mode; @@ -66,16 +65,16 @@ const ProjectEnrollmentsTable = ({ }, }} items={[ - { - value: 'households', - label: 'Households', - Icon: HouseholdIcon, - }, { value: 'clients', label: 'Clients', Icon: PersonIcon, }, + { + value: 'households', + label: 'Households', + Icon: HouseholdIcon, + }, ]} /> )} @@ -96,7 +95,6 @@ const ProjectEnrollmentsTable = ({ {mode === 'clients' && ( ( - + ...BASE_ACTION_COLUMN_DEF, + render: (submission: ExternalFormSubmissionSummaryFragment) => ( + setModalOpenId(submission.id), + disabled: bulkLoading, + }} + /> ), }, ]; diff --git a/src/modules/projects/components/tables/ProjectHouseholdsTable.stories.tsx b/src/modules/projects/components/tables/ProjectHouseholdsTable.stories.tsx new file mode 100644 index 000000000..a95fc36cc --- /dev/null +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.stories.tsx @@ -0,0 +1,26 @@ +import { Card } from '@mui/material'; +import { Meta, StoryObj } from '@storybook/react'; + +import ProjectHouseholdsTable from './ProjectHouseholdsTable'; +import { getProjectHouseholdsMock } from '@/test/__mocks__/requests'; + +export default { + component: ProjectHouseholdsTable, + parameters: { + dashboardContext: 'project', + apolloClient: { + mocks: [getProjectHouseholdsMock], + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx index 79189aa79..892ea54d6 100644 --- a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx @@ -1,163 +1,129 @@ -import { Chip, Stack, Tooltip, Typography } from '@mui/material'; -import { ReactNode, useMemo } from 'react'; - +import { TableBody, TableCell, TableRow } from '@mui/material'; +import React, { useMemo } from 'react'; +import { renderCellContents } from '@/components/elements/table/GenericTable'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewClientMenuItem, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentClientNameWithAge from '@/modules/hmis/components/EnrollmentClientNameWithAge'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; -import EnrollmentStatus from '@/modules/hmis/components/EnrollmentStatus'; import HmisEnum from '@/modules/hmis/components/HmisEnum'; -import HohIndicator from '@/modules/hmis/components/HohIndicator'; import { useFilters } from '@/modules/hmis/filterUtil'; import { + clientBriefName, formatDateForDisplay, formatDateForGql, + sortHouseholdMembers, } from '@/modules/hmis/hmisUtil'; import { useProjectDashboardContext } from '@/modules/projects/components/ProjectDashboard'; +import { + ASSIGNED_STAFF_COL, + WITH_ENROLLMENT_COLUMNS, +} from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; import { HmisEnums } from '@/types/gqlEnums'; import { GetProjectHouseholdsDocument, GetProjectHouseholdsQuery, GetProjectHouseholdsQueryVariables, HouseholdFilterOptions, - HouseholdWithStaffAssignmentsFragment, - RelationshipToHoH, + ProjectEnrollmentsHouseholdClientFieldsFragment, + ProjectEnrollmentsHouseholdFieldsFragment, } from '@/types/gqlTypes'; export type HouseholdFields = NonNullable< GetProjectHouseholdsQuery['project'] >['households']['nodes'][number]; -const TableCellContainer = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -export const ASSIGNED_STAFF_COL = { - header: 'Assigned Staff', - optional: true, - defaultHidden: true, - render: (hh: HouseholdWithStaffAssignmentsFragment) => { - if (!hh.staffAssignments?.nodes.length) return; - - const first = hh.staffAssignments.nodes[0].user.name; - const rest = hh.staffAssignments.nodes - .slice(1) - .map((staffAssignment) => staffAssignment.user.name); - - return ( - <> - {first}{' '} - {rest.length > 0 && ( - - - - )} - - ); +type OneHouseholdClient = HouseholdFields['householdClients'][number]; +const BASE_COLUMNS: ColumnDef[] = [ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, + { + header: 'Relationship', + render: (householdClient) => ( + + ), }, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, + WITH_ENROLLMENT_COLUMNS.enrollmentStatus, +]; +const ACTION_COL: ColumnDef = { + ...BASE_ACTION_COLUMN_DEF, + render: (householdClient) => ( + + ), }; -export const HOUSEHOLD_COLUMNS: { - [key: string]: ColumnDef; -} = { - hohIndicator: { - header: ' ', - width: '0%', - key: 'hoh-indicator', - render: (hh) => ( - - {hh.householdClients.map((c) => - RelationshipToHoH.SelfHeadOfHousehold === c.relationshipToHoH ? ( - - ) : ( - -   - - ) - )} - - ), - }, - clients: { - header: 'Clients', - render: (hh) => ( - - {hh.householdClients.map((hc) => ( - - ))} - - ), - }, - relationshipToHoH: { - header: 'Relationship to HoH', - render: (hh) => ( - - {hh.householdClients.map((c) => - c.relationshipToHoH === RelationshipToHoH.DataNotCollected ? ( - -   - - ) : ( - - ) - )} - - ), - }, - status: { - header: 'Status', - render: (hh) => ( - - {hh.householdClients.map((c) => ( - - ))} - - ), - }, - enrollmentPeriod: { - header: 'Enrollment Period', - render: (hh) => ( - - {hh.householdClients.map((c) => ( - - ))} - - ), - }, - householdId: { - header: 'Household ID', - render: (hh) => ( - - {hh.shortId} ({hh.householdSize}) - - ), - }, - assignedStaff: ASSIGNED_STAFF_COL, +interface ProjectHouseholdsClientRowProps { + household: ProjectEnrollmentsHouseholdFieldsFragment; + householdClient: ProjectEnrollmentsHouseholdClientFieldsFragment; + lastInGroup?: boolean; + showAssignedStaff?: boolean; +} + +const ProjectHouseholdsClientRow: React.FC = ({ + household, + householdClient, + lastInGroup = false, + showAssignedStaff = false, +}) => { + const cellSx = useMemo( + () => (lastInGroup ? { borderBottom: 0, py: 0.5 } : { py: 0.5 }), + [lastInGroup] + ); + + return ( + + {BASE_COLUMNS.map((col, i) => ( + + {renderCellContents(householdClient, col.render)} + + ))} + {showAssignedStaff && ( + + {renderCellContents(household, ASSIGNED_STAFF_COL.render)} + + )} + + {renderCellContents(householdClient, ACTION_COL.render)} + + + ); }; +const CustomDividerRow = ({ colSpan }: { colSpan: number }) => ( + + { + return { + padding: 0, + backgroundColor: theme.palette.grey[100], + borderTop: `1px solid ${theme.palette.borders.main}`, + borderBottom: `1px solid ${theme.palette.borders.main}`, + }; + }} + /> + +); + const ProjectHouseholdsTable = ({ projectId, columns, @@ -178,19 +144,19 @@ const ProjectHouseholdsTable = ({ project: { staffAssignmentsEnabled }, } = useProjectDashboardContext(); + // dummy column defs for Household that are only used for the headers, not for rendering cells const defaultColumns: ColumnDef[] = useMemo(() => { - const cols: ColumnDef[] = [ - HOUSEHOLD_COLUMNS.hohIndicator, - HOUSEHOLD_COLUMNS.clients, - HOUSEHOLD_COLUMNS.relationshipToHoH, - HOUSEHOLD_COLUMNS.status, - HOUSEHOLD_COLUMNS.enrollmentPeriod, - HOUSEHOLD_COLUMNS.householdId, - ]; - - if (staffAssignmentsEnabled) cols.push(HOUSEHOLD_COLUMNS.assignedStaff); - - return cols; + return [ + ...BASE_COLUMNS, + ...(staffAssignmentsEnabled ? [ASSIGNED_STAFF_COL] : []), + ACTION_COL, + ].map(({ header, key, optional, defaultHidden }) => ({ + header, + key, + optional, + defaultHidden, + render: () => null, + })); }, [staffAssignmentsEnabled]); const filters = useFilters({ @@ -215,6 +181,36 @@ const ProjectHouseholdsTable = ({ }} queryDocument={GetProjectHouseholdsDocument} columns={columns || defaultColumns} + TableBodyComponent={React.Fragment} + renderRow={(household, columnKeys) => { + return ( + + + {sortHouseholdMembers(household.householdClients).map( + (householdClient, index) => ( + + ) + )} + + ); + }} + belowRowsContent={ + + + + } noData={ openOnDate ? `No households open on ${formatDateForDisplay(openOnDate)}` @@ -227,7 +223,7 @@ const ProjectHouseholdsTable = ({ applyOptionalColumns={(cols) => { const result: Partial = {}; - if (cols.includes(HOUSEHOLD_COLUMNS.assignedStaff.key || '')) + if (cols.includes(ASSIGNED_STAFF_COL.key || '')) result.includeStaffAssignment = true; return result; diff --git a/src/modules/search/components/ClientSearch.tsx b/src/modules/search/components/ClientSearch.tsx index abaf0b470..6605fc75b 100644 --- a/src/modules/search/components/ClientSearch.tsx +++ b/src/modules/search/components/ClientSearch.tsx @@ -14,9 +14,12 @@ import ClientSearchTypeToggle, { SearchType } from './ClientSearchTypeToggle'; import ClientTextSearchForm from './ClientTextSearchForm'; import ButtonLink from '@/components/elements/ButtonLink'; import { externalIdColumn } from '@/components/elements/ExternalIdDisplay'; -import RouterLink from '@/components/elements/RouterLink'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewClientMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; -import { useIsMobile } from '@/hooks/useIsMobile'; import ClientName from '@/modules/client/components/ClientName'; import ClientSearchResultCard from '@/modules/client/components/ClientSearchResultCard'; @@ -30,13 +33,13 @@ import { import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { SearchFormDefinition } from '@/modules/form/data'; import { useFilters } from '@/modules/hmis/filterUtil'; -import { clientNameAllParts } from '@/modules/hmis/hmisUtil'; +import { clientBriefName } from '@/modules/hmis/hmisUtil'; import { useHmisAppSettings } from '@/modules/hmisAppSettings/useHmisAppSettings'; import { isEnrollment, isHouseholdClient } from '@/modules/household/types'; import { RootPermissionsFilter } from '@/modules/permissions/PermissionsFilters'; import { useHasRootPermissions } from '@/modules/permissions/useHasPermissionsHooks'; -import { ClientDashboardRoutes, Routes } from '@/routes/routes'; +import { Routes } from '@/routes/routes'; import { ClientFieldsFragment, ClientSearchInput as ClientSearchInputType, @@ -44,17 +47,18 @@ import { ExternalIdentifierType, HouseholdClientFieldsFragment, ProjectEnrollmentFieldsFragment, + ProjectEnrollmentsHouseholdClientFieldsFragment, SearchClientsDocument, SearchClientsQuery, SearchClientsQueryVariables, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; function asClient( record: | ClientFieldsFragment | HouseholdClientFieldsFragment | ProjectEnrollmentFieldsFragment + | ProjectEnrollmentsHouseholdClientFieldsFragment ) { if (isHouseholdClient(record)) return record.client; if (isEnrollment(record)) return record.client; @@ -65,31 +69,18 @@ export const CLIENT_COLUMNS: { | ClientFieldsFragment | HouseholdClientFieldsFragment | ProjectEnrollmentFieldsFragment + | ProjectEnrollmentsHouseholdClientFieldsFragment >; } = { - id: { header: 'HMIS ID', render: 'id' }, - linkedId: { - header: 'ID', - render: (client) => ( - - {client.id} - - ), - }, name: { header: 'Name', key: 'name', render: (client) => , }, - linkedName: { - header: 'Name', - key: 'name', - render: (client) => , + age: { + header: 'Age', + key: 'age', + render: (client) => asClient(client).age, }, linkedNameNewTab: { header: 'Name', @@ -130,27 +121,19 @@ export const CLIENT_COLUMNS: { }, }; -export const SEARCH_RESULT_COLUMNS: ColumnDef[] = [ - CLIENT_COLUMNS.id, +const SEARCH_RESULT_COLUMNS: ColumnDef[] = [ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, { - ...CLIENT_COLUMNS.first, - linkTreatment: true, - ariaLabel: (row) => clientNameAllParts(row), - }, - { ...CLIENT_COLUMNS.last, linkTreatment: true }, - { ...CLIENT_COLUMNS.ssn, width: '150px' }, - { ...CLIENT_COLUMNS.dobAge, width: '180px' }, -]; - -export const MOBILE_SEARCH_RESULT_COLUMNS: ColumnDef[] = [ - CLIENT_COLUMNS.id, - { - ...CLIENT_COLUMNS.name, - linkTreatment: true, - ariaLabel: (row) => clientNameAllParts(row), + ...BASE_ACTION_COLUMN_DEF, + render: (client) => ( + + ), }, - { ...CLIENT_COLUMNS.ssn }, - { ...CLIENT_COLUMNS.dobAge }, ]; /** @@ -171,8 +154,6 @@ const ClientSearch = () => { // whether search has occurred const [hasSearched, setHasSearched] = useState(false); - const isMobile = useIsMobile(); - const [searchInput, setSearchInput] = useState( null ); @@ -190,22 +171,15 @@ const ClientSearch = () => { if (displayType === 'cards') { return []; } - let baseColumns = isMobile - ? MOBILE_SEARCH_RESULT_COLUMNS - : SEARCH_RESULT_COLUMNS; + let baseColumns = SEARCH_RESULT_COLUMNS; if (globalFeatureFlags?.mciId) { baseColumns = [ externalIdColumn(ExternalIdentifierType.MciId, 'MCI ID'), ...baseColumns, ]; } - if (!canViewSsn) baseColumns = baseColumns.filter((c) => c.key !== 'ssn'); - if (!canViewDob) - baseColumns = baseColumns.map((c) => - c.key === 'dob' ? { ...c, header: 'Age' } : c - ); return baseColumns; - }, [isMobile, globalFeatureFlags, displayType, canViewSsn, canViewDob]); + }, [globalFeatureFlags, displayType]); useEffect(() => { // if search params are derived, we don't want to perform a search on them @@ -226,14 +200,6 @@ const ClientSearch = () => { } }, [derivedSearchParams, searchParams]); - const rowLinkTo = useCallback( - (row: ClientFieldsFragment) => - generateSafePath(ClientDashboardRoutes.PROFILE, { - clientId: row.id, - }), - [] - ); - const onClearSearch = useCallback(() => { setSearchInput(null); setSearchParams({}); @@ -319,7 +285,6 @@ const ClientSearch = () => { queryVariables={{ input: searchInput }} queryDocument={SearchClientsDocument} onCompleted={() => setHasSearched(true)} - rowLinkTo={rowLinkTo} columns={columns} pagePath='clientSearch' fetchPolicy='cache-and-network' diff --git a/src/modules/services/components/ClientServicesPage.tsx b/src/modules/services/components/ClientServicesPage.tsx index d814a1f1d..fb590d088 100644 --- a/src/modules/services/components/ClientServicesPage.tsx +++ b/src/modules/services/components/ClientServicesPage.tsx @@ -1,26 +1,31 @@ import { Paper } from '@mui/material'; -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; -import RouterLink from '@/components/elements/RouterLink'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, + getViewServiceMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; +import useClientDashboardContext from '@/modules/client/hooks/useClientDashboardContext'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { entryExitRange, parseAndFormatDate } from '@/modules/hmis/hmisUtil'; import { + getServiceTypeForDisplay, SERVICE_BASIC_COLUMNS, SERVICE_COLUMNS, } from '@/modules/services/serviceColumns'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { GetClientServicesDocument, GetClientServicesQuery, GetClientServicesQueryVariables, ServiceSortOption, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; type ServiceType = NonNullable< NonNullable['services'] @@ -31,53 +36,52 @@ const ClientServicesPage: React.FC<{ enrollmentId?: string; }> = ({ omitColumns = [] }) => { const { clientId } = useSafeParams() as { clientId: string }; + const { client } = useClientDashboardContext(); const columns = useMemo( () => ( [ - SERVICE_BASIC_COLUMNS.dateProvided, + SERVICE_BASIC_COLUMNS.serviceDate, SERVICE_BASIC_COLUMNS.serviceType, { key: 'project', header: 'Project Name', - render: (row) => ( - - {row.enrollment.projectName} - - ), - }, - { - key: 'en-period', - header: 'Enrollment Period', - optional: true, - render: (row) => ( - - ), + render: (row) => row.enrollment.projectName, }, { ...SERVICE_COLUMNS.serviceDetails, optional: true, defaultHidden: true, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row) => ( + + ), + }, ] as ColumnDef[] ).filter((col) => { if (omitColumns.includes(col.key || '')) return false; return true; }), - [clientId, omitColumns] + [client, clientId, omitColumns] ); const filters = useFilters({ diff --git a/src/modules/services/components/EnrollmentServicesPage.tsx b/src/modules/services/components/EnrollmentServicesPage.tsx index bcbda40f9..3974830f9 100644 --- a/src/modules/services/components/EnrollmentServicesPage.tsx +++ b/src/modules/services/components/EnrollmentServicesPage.tsx @@ -1,14 +1,18 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import useEnrollmentDashboardContext from '@/modules/enrollment/hooks/useEnrollmentDashboardContext'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { parseAndFormatDate } from '@/modules/hmis/hmisUtil'; import { useServiceDialog } from '@/modules/services/hooks/useServiceDialog'; import { + getServiceTypeForDisplay, SERVICE_BASIC_COLUMNS, SERVICE_COLUMNS, } from '@/modules/services/serviceColumns'; @@ -43,16 +47,39 @@ const EnrollmentServicesPage = () => { DataCollectionFeatureRole.Service ); - if (!enrollment || !enrollmentId || !clientId || !serviceFeature) - return ; + const canEditServices = enrollment?.access.canEditEnrollments; - const canEditServices = enrollment.access.canEditEnrollments; + const columns = useMemo(() => { + return [ + SERVICE_BASIC_COLUMNS.serviceDate, + SERVICE_BASIC_COLUMNS.serviceType, + SERVICE_COLUMNS.serviceDetails, + ...(canEditServices + ? [ + { + ...BASE_ACTION_COLUMN_DEF, + render: (service: ServiceFieldsFragment) => ( + { + setViewingRecord(service); + openServiceDialog(); + }, + }} + /> + ), + }, + ] + : []), + ]; + }, [canEditServices, openServiceDialog]); - const columns = [ - SERVICE_BASIC_COLUMNS.dateProvided, - SERVICE_BASIC_COLUMNS.serviceType, - SERVICE_COLUMNS.serviceDetails, - ]; + if (!enrollment || !enrollmentId || !clientId || !serviceFeature) + return ; return ( <> @@ -78,14 +105,6 @@ const EnrollmentServicesPage = () => { GetEnrollmentServicesQueryVariables, ServiceFieldsFragment > - handleRowClick={ - canEditServices - ? (record) => { - setViewingRecord(record); - openServiceDialog(); - } - : undefined - } queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentServicesDocument} columns={columns} diff --git a/src/modules/services/components/ProjectServicesTable.tsx b/src/modules/services/components/ProjectServicesTable.tsx index b7830673a..ebb11f65b 100644 --- a/src/modules/services/components/ProjectServicesTable.tsx +++ b/src/modules/services/components/ProjectServicesTable.tsx @@ -1,20 +1,28 @@ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, + getViewServiceMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import ClientName from '@/modules/client/components/ClientName'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; import { useFilters } from '@/modules/hmis/filterUtil'; -import { SERVICE_BASIC_COLUMNS } from '@/modules/services/serviceColumns'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; +import { clientBriefName, parseAndFormatDate } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { + getServiceTypeForDisplay, + SERVICE_BASIC_COLUMNS, +} from '@/modules/services/serviceColumns'; import { GetProjectServicesDocument, GetProjectServicesQuery, GetProjectServicesQueryVariables, ServicesForProjectFilterOptions, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; export type ServiceFields = NonNullable< GetProjectServicesQuery['project'] @@ -31,25 +39,33 @@ const ProjectServicesTable = ({ if (columns) return columns; return [ { - header: 'First Name', - linkTreatment: true, - render: (s: ServiceFields) => ( - - ), - }, - { - header: 'Last Name', - linkTreatment: true, + header: 'Client Name', render: (s: ServiceFields) => ( - + ), }, - { ...SERVICE_BASIC_COLUMNS.dateProvided, linkTreatment: false }, + { ...SERVICE_BASIC_COLUMNS.serviceDate, linkTreatment: false }, SERVICE_BASIC_COLUMNS.serviceType, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, { - header: 'Enrollment Period', - render: (s: ServiceFields) => ( - + ...BASE_ACTION_COLUMN_DEF, + render: (service: ServiceFields) => ( + ), }, ]; @@ -59,13 +75,6 @@ const ProjectServicesTable = ({ type: 'ServicesForProjectFilterOptions', }); - const rowLinkTo = useCallback((s: ServiceFields) => { - return generateSafePath(EnrollmentDashboardRoutes.SERVICES, { - clientId: s.enrollment.client.id, - enrollmentId: s.enrollment.id, - }); - }, []); - return ( ); }; diff --git a/src/modules/services/serviceColumns.tsx b/src/modules/services/serviceColumns.tsx index 5c8640d74..3a0899ab6 100644 --- a/src/modules/services/serviceColumns.tsx +++ b/src/modules/services/serviceColumns.tsx @@ -6,24 +6,31 @@ import { parseAndFormatDate, serviceDetails } from '@/modules/hmis/hmisUtil'; import { ServiceBasicFieldsFragment, ServiceFieldsFragment, + ServiceTypeConfigFieldsFragment, } from '@/types/gqlTypes'; +export const getServiceTypeForDisplay = ( + serviceType?: Pick< + ServiceTypeConfigFieldsFragment, + 'name' | 'serviceCategory' + > | null +) => { + if (!serviceType) return 'Unknown Service'; + const { name, serviceCategory } = serviceType; + if (name === serviceCategory.name) return name; + return `${serviceCategory.name} - ${name}`; +}; + export const SERVICE_BASIC_COLUMNS: { [key: string]: ColumnDef; } = { - dateProvided: { - header: 'Date Provided', - linkTreatment: true, + serviceDate: { + header: 'Service Date', render: (s) => parseAndFormatDate(s.dateProvided), }, serviceType: { header: 'Service Type', - render: ({ serviceType }) => { - if (!serviceType) return 'Unknown Service'; - const { name, serviceCategory } = serviceType; - if (name === serviceCategory.name) return name; - return `${serviceCategory.name} - ${name}`; - }, + render: (s) => getServiceTypeForDisplay(s.serviceType), }, }; diff --git a/src/test/__mocks__/requests.ts b/src/test/__mocks__/requests.ts index d0761c2e9..3642edf7d 100644 --- a/src/test/__mocks__/requests.ts +++ b/src/test/__mocks__/requests.ts @@ -16,6 +16,7 @@ import { GetEnrollmentWithHouseholdDocument, GetFileDocument, GetPickListDocument, + GetProjectHouseholdsDocument, GetRootPermissionsDocument, NameDataQuality, NoYesReasonsForMissingData, @@ -161,6 +162,80 @@ export const RITA_ACKROYD_WITHOUT_ENROLLMENTS = { enrollments: [], }; +export const AMERICAN_LAKE_HOUSE = { + id: '441', + staffAssignmentsEnabled: true, + // TODO: flesh out project mock +}; + +export const getProjectHouseholdsMock = { + request: { + query: GetProjectHouseholdsDocument, + variables: { + limit: 25, + offset: 0, + includeStaffAssignment: false, + id: undefined, + filters: { searchTerm: undefined }, + openOnDate: undefined, + sortOrder: 'MOST_RECENT', + }, + }, + result: { + data: { + project: { + households: { + offset: 0, + limit: 25, + nodesCount: 2, + nodes: [ + { + id: '1', + householdSize: 1, + householdClients: [ + { + id: '35871:8678', + relationshipToHoH: 'SELF_HEAD_OF_HOUSEHOLD', + client: { + ...RITA_ACKROYD, + }, + enrollment: fakeEnrollment(), + }, + ], + }, + { + id: '2', + householdSize: 4, + householdClients: [ + { + id: '1:1', + relationshipToHoH: 'SELF_HEAD_OF_HOUSEHOLD', + client: { + ...RITA_ACKROYD, + firstName: 'Elmer', + lastName: 'Smith', + }, + enrollment: fakeEnrollment(), + }, + { + id: '1:2', + relationshipToHoH: 'SPOUSE_OR_PARTNER', + client: { + ...RITA_ACKROYD, + firstName: 'Ryan', + lastName: 'Smith', + }, + enrollment: fakeEnrollment(), + }, + ], + }, + ], + }, + }, + }, + }, +}; + export const projectsForSelectMock = { request: { query: GetPickListDocument,